feat: implement dynamic news pages and listing components with internationalization support
This commit is contained in:
parent
5731cf245d
commit
aa54417618
|
|
@ -5,6 +5,7 @@ import "dayjs/locale/es";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import utc from "dayjs/plugin/utc";
|
import utc from "dayjs/plugin/utc";
|
||||||
const regionNames = new Intl.DisplayNames(['es'], { type: 'region' });
|
const regionNames = new Intl.DisplayNames(['es'], { type: 'region' });
|
||||||
|
import { getLocalizedRoute } from "../../i18n";
|
||||||
|
|
||||||
const locale = Astro.currentLocale;
|
const locale = Astro.currentLocale;
|
||||||
dayjs.extend(utc);
|
dayjs.extend(utc);
|
||||||
|
|
@ -24,7 +25,7 @@ locationArray.filter(Boolean).join(', ');
|
||||||
{locationArray.filter(Boolean).join(', ')}<br />
|
{locationArray.filter(Boolean).join(', ')}<br />
|
||||||
({nicedate}):
|
({nicedate}):
|
||||||
</p>
|
</p>
|
||||||
<h3 class="text-2xl font-bold mb-8"><a href={`/${locale}/news/${data.id}`}>{data.data.title}</a></h3>
|
<h3 class="text-2xl font-bold mb-8"><a href={`/${locale}/${getLocalizedRoute('news', locale)}/${data.id}`}>{data.data.title}</a></h3>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Image
|
<Image
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import utc from "dayjs/plugin/utc";
|
||||||
const regionNames = new Intl.DisplayNames(["es"], { type: "region" });
|
const regionNames = new Intl.DisplayNames(["es"], { type: "region" });
|
||||||
|
|
||||||
const locale = Astro.currentLocale;
|
const locale = Astro.currentLocale;
|
||||||
import { createTranslator } from "@/i18n";
|
import { createTranslator, getLocalizedRoute } from "@/i18n";
|
||||||
const tl = createTranslator(Astro.currentLocale);
|
const tl = createTranslator(Astro.currentLocale);
|
||||||
dayjs.extend(utc);
|
dayjs.extend(utc);
|
||||||
dayjs.locale(locale);
|
dayjs.locale(locale);
|
||||||
|
|
@ -21,7 +21,7 @@ const countryName = data?.data?.country
|
||||||
const locationArray = [data.data.city, data.data.state, countryName];
|
const locationArray = [data.data.city, data.data.state, countryName];
|
||||||
const location = locationArray.filter(Boolean).join(", ");
|
const location = locationArray.filter(Boolean).join(", ");
|
||||||
|
|
||||||
const newsUrl = `/${locale}/news/${data.id}`;
|
const newsUrl = `/${locale}/${getLocalizedRoute('news', locale)}/${data.id}`;
|
||||||
|
|
||||||
const rawContent = content?.body || "";
|
const rawContent = content?.body || "";
|
||||||
const plainText = rawContent
|
const plainText = rawContent
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,7 @@ const newsItems = await getCollection("news", (post)=>{
|
||||||
const currentLocale = Astro.currentLocale;
|
const currentLocale = Astro.currentLocale;
|
||||||
return post.data.locale == currentLocale
|
return post.data.locale == currentLocale
|
||||||
});
|
});
|
||||||
|
import { createTranslator, getLocalizedRoute } from '../../i18n';
|
||||||
import { createTranslator, t } from '../../i18n';
|
|
||||||
const tl = createTranslator(Astro.currentLocale);
|
const tl = createTranslator(Astro.currentLocale);
|
||||||
---
|
---
|
||||||
<div id="news" class="bg-[#22523F] py-12 lg:py-20">
|
<div id="news" class="bg-[#22523F] py-12 lg:py-20">
|
||||||
|
|
@ -19,7 +18,7 @@ const tl = createTranslator(Astro.currentLocale);
|
||||||
<h4 class="text-white text-2xl uppercase font-bold text-center mb-4 font-primary">{tl("news.title")}</h4>
|
<h4 class="text-white text-2xl uppercase font-bold text-center mb-4 font-primary">{tl("news.title")}</h4>
|
||||||
<h2 class="text-white text-3xl lg:text-5xl font-bold text-center font-secondary mb-4">{tl("news.text")}</h2>
|
<h2 class="text-white text-3xl lg:text-5xl font-bold text-center font-secondary mb-4">{tl("news.text")}</h2>
|
||||||
<p class="text-white text-xl text-center">{tl("news.text2")}</p>
|
<p class="text-white text-xl text-center">{tl("news.text2")}</p>
|
||||||
<Button class="px-6 py-3 uppercase mt-4" url=`/${currentLocale}/news` variant="primary" title={tl("news.buttonLable")} />
|
<Button class="px-6 py-3 uppercase mt-4" url={`/${currentLocale}/${getLocalizedRoute('news', currentLocale)}`} variant="primary" title={tl("news.buttonLable")} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,9 +11,27 @@ import kr from "./kr.json";
|
||||||
const dictionaries = { es, en, fr, he, uk, pt, ru, rw, kr } as const;
|
const dictionaries = { es, en, fr, he, uk, pt, ru, rw, kr } as const;
|
||||||
export type Locale = keyof typeof dictionaries;
|
export type Locale = keyof typeof dictionaries;
|
||||||
|
|
||||||
// Optional: type-safe keys from the default dictionary
|
|
||||||
export type I18nKey = keyof typeof es;
|
export type I18nKey = keyof typeof es;
|
||||||
|
|
||||||
|
export const routeTranslations = {
|
||||||
|
news: {
|
||||||
|
es: "noticias",
|
||||||
|
en: "news",
|
||||||
|
fr: "informations",
|
||||||
|
he: "חדשות",
|
||||||
|
uk: "noticias",
|
||||||
|
pt: "noticias",
|
||||||
|
ru: "новости",
|
||||||
|
rw: "amakuru",
|
||||||
|
kr: "nouvel",
|
||||||
|
}
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export function getLocalizedRoute(route: keyof typeof routeTranslations, locale: string | undefined): string {
|
||||||
|
const l = (locale in routeTranslations[route] ? locale : "es") as Locale;
|
||||||
|
return routeTranslations[route][l as keyof (typeof routeTranslations)[typeof route]] || routeTranslations[route]["en" as keyof (typeof routeTranslations)[typeof route]];
|
||||||
|
}
|
||||||
|
|
||||||
export function t(
|
export function t(
|
||||||
locale: string | undefined,
|
locale: string | undefined,
|
||||||
key: I18nKey,
|
key: I18nKey,
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import "@fontsource/poppins/500.css";
|
||||||
import "@fontsource/poppins/700.css";
|
import "@fontsource/poppins/700.css";
|
||||||
import "@fontsource-variable/kameron";
|
import "@fontsource-variable/kameron";
|
||||||
import ShareSticky from "../components/ShareSticky.vue";
|
import ShareSticky from "../components/ShareSticky.vue";
|
||||||
|
import { routeTranslations } from "../i18n";
|
||||||
const {
|
const {
|
||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
|
|
@ -17,6 +18,9 @@ const {
|
||||||
|
|
||||||
const currentLocale = Astro.currentLocale ?? 'es';
|
const currentLocale = Astro.currentLocale ?? 'es';
|
||||||
const direction = currentLocale === 'he' ? 'rtl' : 'ltr';
|
const direction = currentLocale === 'he' ? 'rtl' : 'ltr';
|
||||||
|
|
||||||
|
const newsSegments = Object.values(routeTranslations.news);
|
||||||
|
const isNewsPage = newsSegments.some(segment => Astro.url.pathname.includes(`/${segment}/`));
|
||||||
---
|
---
|
||||||
|
|
||||||
<html lang={currentLocale} dir={direction} class="scroll-smooth">
|
<html lang={currentLocale} dir={direction} class="scroll-smooth">
|
||||||
|
|
@ -35,7 +39,7 @@ const direction = currentLocale === 'he' ? 'rtl' : 'ltr';
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
<body class="font-primary">
|
<body class="font-primary">
|
||||||
{Astro.url.pathname.includes('/news/') && (
|
{isNewsPage && (
|
||||||
<ShareSticky client:only url={Astro.url.href} />
|
<ShareSticky client:only url={Astro.url.href} />
|
||||||
)}
|
)}
|
||||||
<slot />
|
<slot />
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,103 @@
|
||||||
|
---
|
||||||
|
import { YouTube } from "astro-embed";
|
||||||
|
import MainLayout from "../../../layouts/MainLayout.astro";
|
||||||
|
import Header from "../../../components/Header.astro";
|
||||||
|
import CarouselSection from "../../../components/section/CarouselSection.astro";
|
||||||
|
import { Image } from "@unpic/astro";
|
||||||
|
import { getCollection, render } from "astro:content";
|
||||||
|
import TitleSection from "../../../components/section/TitleSection.astro";
|
||||||
|
import FooterSection from "../../../components/section/FooterSection.astro";
|
||||||
|
import { getLocalizedRoute } from "@/i18n";
|
||||||
|
export const prerender = true;
|
||||||
|
// 1. Generate a new path for every collection entry
|
||||||
|
export async function getStaticPaths() {
|
||||||
|
const posts = await getCollection("news");
|
||||||
|
return posts.map((post) => ({
|
||||||
|
params: {
|
||||||
|
id: post.id,
|
||||||
|
locale: post.data.locale,
|
||||||
|
news_slug: getLocalizedRoute("news", post.data.locale),
|
||||||
|
},
|
||||||
|
props: { post },
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
const { locale, news_slug } = Astro.params;
|
||||||
|
|
||||||
|
// 2. For your template, you can get the entry directly from the prop
|
||||||
|
const { post } = Astro.props;
|
||||||
|
const { Content } = await render(post);
|
||||||
|
|
||||||
|
const baseSlug = news_slug;
|
||||||
|
|
||||||
|
const rawContent = post.body || "";
|
||||||
|
const plainText = rawContent
|
||||||
|
.replace(/^#.*$/gm, "")
|
||||||
|
.replace(/^###.*$/gm, "")
|
||||||
|
.replace(/\*\*([^*]+)\*\*/g, "$1")
|
||||||
|
.replace(/\*([^*]+)\*/g, "$1")
|
||||||
|
.replace(/_([^_]+)_/g, "$1")
|
||||||
|
.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1")
|
||||||
|
.replace(/^>.*$/gm, "")
|
||||||
|
.replace(/`[^`]+`/g, "")
|
||||||
|
.replace(/^[-*]\s+/gm, "")
|
||||||
|
.trim();
|
||||||
|
const words = plainText
|
||||||
|
.split(/\s+/)
|
||||||
|
.filter((w) => w.length > 0)
|
||||||
|
.slice(0, 35);
|
||||||
|
const excerpt = words.join(" ") + (words.length === 35 ? "..." : "");
|
||||||
|
---
|
||||||
|
|
||||||
|
<MainLayout
|
||||||
|
title={post.data.title}
|
||||||
|
description={excerpt}
|
||||||
|
image={post.data.thumbnail}
|
||||||
|
url={new URL(`/${locale}/${news_slug}/${post.id}`, Astro.site)}
|
||||||
|
>
|
||||||
|
<div class="container mx-auto md:py-16 py-8">
|
||||||
|
<Header />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a href={`/${locale}/${news_slug}/${post.id}`} class="block">
|
||||||
|
<TitleSection title={post.data.title} />
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<div class="container mx-auto">
|
||||||
|
<a href={`/${locale}/${news_slug}/${post.id}`} class="block">
|
||||||
|
{
|
||||||
|
post.data.gallery?.length ? (
|
||||||
|
<CarouselSection images={post.data.gallery} />
|
||||||
|
) : post.data.thumbnail ? (
|
||||||
|
<Image
|
||||||
|
src={post.data.thumbnail}
|
||||||
|
alt={post.data.title}
|
||||||
|
class="hover:opacity-90 transition-opacity"
|
||||||
|
/>
|
||||||
|
) : null
|
||||||
|
}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="grid md:grid-cols-10">
|
||||||
|
<div
|
||||||
|
class="md:col-span-7 content bg-white p-8 md:p-20 prose-p:mb-4 text-[#003421] text-justify"
|
||||||
|
>
|
||||||
|
<!-- <p class="text-lg font-semibold text-tertiary mb-8 pb-6 border-b border-tertiary/20 italic">
|
||||||
|
{excerpt}
|
||||||
|
</p> -->
|
||||||
|
<Content />
|
||||||
|
</div>
|
||||||
|
<div class="md:col-span-3 bg-tertiary md:sticky top-0 h-fit">
|
||||||
|
{post.data.youtube && <YouTube id={post.data.youtube} />}
|
||||||
|
|
||||||
|
{
|
||||||
|
post.data.gallery &&
|
||||||
|
post.data.gallery.map((galleryImage) => (
|
||||||
|
<Image src={galleryImage.image} alt={galleryImage.text} />
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</MainLayout>
|
||||||
|
|
||||||
|
<FooterSection />
|
||||||
|
|
@ -6,9 +6,21 @@ import { getCollection, getEntry } from "astro:content";
|
||||||
import FooterSection from "@/components/section/FooterSection.astro";
|
import FooterSection from "@/components/section/FooterSection.astro";
|
||||||
|
|
||||||
|
|
||||||
import { createTranslator } from '@/i18n';
|
import { createTranslator, getLocalizedRoute, routeTranslations } from '@/i18n';
|
||||||
const tl = createTranslator(Astro.currentLocale);
|
const tl = createTranslator(Astro.currentLocale);
|
||||||
|
|
||||||
|
export function getStaticPaths() {
|
||||||
|
const locales = Object.keys(routeTranslations.news);
|
||||||
|
return locales.map((locale) => ({
|
||||||
|
params: {
|
||||||
|
locale,
|
||||||
|
news_slug: getLocalizedRoute('news', locale)
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
const { locale, news_slug } = Astro.params;
|
||||||
|
|
||||||
const newsItems = await getCollection("news", (post)=>{
|
const newsItems = await getCollection("news", (post)=>{
|
||||||
const currentLocale = Astro.currentLocale;
|
const currentLocale = Astro.currentLocale;
|
||||||
return post.data.locale == currentLocale
|
return post.data.locale == currentLocale
|
||||||
|
|
@ -1,95 +0,0 @@
|
||||||
---
|
|
||||||
import { YouTube } from 'astro-embed';
|
|
||||||
import MainLayout from "../../../layouts/MainLayout.astro";
|
|
||||||
import Header from "../../../components/Header.astro";
|
|
||||||
import CarouselSection from "../../../components/section/CarouselSection.astro";
|
|
||||||
import { Image } from "@unpic/astro";
|
|
||||||
import { getCollection, render } from "astro:content";
|
|
||||||
import TitleSection from "../../../components/section/TitleSection.astro";
|
|
||||||
import FooterSection from '../../../components/section/FooterSection.astro';
|
|
||||||
export const prerender = true;
|
|
||||||
// 1. Generate a new path for every collection entry
|
|
||||||
export async function getStaticPaths() {
|
|
||||||
const posts = await getCollection("news");
|
|
||||||
return posts.map((post) => ({
|
|
||||||
params: { id: post.id, locale: post.data.locale },
|
|
||||||
props: { post },
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
const { locale } = Astro.params;
|
|
||||||
|
|
||||||
// 2. For your template, you can get the entry directly from the prop
|
|
||||||
const { post } = Astro.props;
|
|
||||||
const { Content } = await render(post);
|
|
||||||
|
|
||||||
const routeTranslations = {
|
|
||||||
news: {
|
|
||||||
es: "noticias",
|
|
||||||
en: "news",
|
|
||||||
fr: "actualites",
|
|
||||||
pt: "noticias",
|
|
||||||
de: "nachrichten",
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const baseSlug = routeTranslations.news[locale] || routeTranslations.news.en;
|
|
||||||
|
|
||||||
const rawContent = post.body || "";
|
|
||||||
const plainText = rawContent
|
|
||||||
.replace(/^#.*$/gm, '')
|
|
||||||
.replace(/^###.*$/gm, '')
|
|
||||||
.replace(/\*\*([^*]+)\*\*/g, '$1')
|
|
||||||
.replace(/\*([^*]+)\*/g, '$1')
|
|
||||||
.replace(/_([^_]+)_/g, '$1')
|
|
||||||
.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1')
|
|
||||||
.replace(/^>.*$/gm, '')
|
|
||||||
.replace(/`[^`]+`/g, '')
|
|
||||||
.replace(/^[-*]\s+/gm, '')
|
|
||||||
.trim();
|
|
||||||
const words = plainText.split(/\s+/).filter(w => w.length > 0).slice(0, 35);
|
|
||||||
const excerpt = words.join(' ') + (words.length === 35 ? '...' : '');
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
<MainLayout
|
|
||||||
title={post.data.title}
|
|
||||||
description={excerpt}
|
|
||||||
image={post.data.thumbnail}
|
|
||||||
url={new URL(`/${locale}/${"news"}/${post.id}`, Astro.site)}
|
|
||||||
>
|
|
||||||
<div class="container mx-auto md:py-16 py-8">
|
|
||||||
<Header />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<a href={`/${locale}/news/${post.id}`} class="block">
|
|
||||||
<TitleSection title={post.data.title} />
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<div class="container mx-auto">
|
|
||||||
<a href={`/${locale}/news/${post.id}`} class="block">
|
|
||||||
{post.data.gallery?.length ? ( <CarouselSection images={post.data.gallery} />) : post.data.thumbnail ? (<Image src={post.data.thumbnail} alt={post.data.title} class="hover:opacity-90 transition-opacity" /> ) : null}
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<div class="grid md:grid-cols-10">
|
|
||||||
<div class="md:col-span-7 content bg-white p-8 md:p-20 prose-p:mb-4 text-[#003421] text-justify">
|
|
||||||
<!-- <p class="text-lg font-semibold text-tertiary mb-8 pb-6 border-b border-tertiary/20 italic">
|
|
||||||
{excerpt}
|
|
||||||
</p> -->
|
|
||||||
<Content />
|
|
||||||
</div>
|
|
||||||
<div class="md:col-span-3 bg-tertiary md:sticky top-0 h-fit">
|
|
||||||
{ post.data.youtube && (
|
|
||||||
<YouTube id={post.data.youtube} />
|
|
||||||
)}
|
|
||||||
|
|
||||||
{post.data.gallery && (
|
|
||||||
post.data.gallery.map(galleryImage => (
|
|
||||||
<Image src={galleryImage.image} alt={galleryImage.text} />
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</MainLayout>
|
|
||||||
|
|
||||||
<FooterSection />
|
|
||||||
|
|
@ -7,22 +7,27 @@ import { Image } from "@unpic/astro";
|
||||||
import { getCollection, render } from "astro:content";
|
import { getCollection, render } from "astro:content";
|
||||||
import TitleSection from "../../components/section/TitleSection.astro";
|
import TitleSection from "../../components/section/TitleSection.astro";
|
||||||
import FooterSection from '../../components/section/FooterSection.astro';
|
import FooterSection from '../../components/section/FooterSection.astro';
|
||||||
|
import { getLocalizedRoute } from '@/i18n';
|
||||||
export const prerender = true;
|
export const prerender = true;
|
||||||
// 1. Generate a new path for every collection entry
|
// 1. Generate a new path for every collection entry
|
||||||
export async function getStaticPaths() {
|
export async function getStaticPaths() {
|
||||||
const posts = await getCollection("news");
|
const posts = await getCollection("news");
|
||||||
return posts.map((post) => ({
|
return posts.map((post) => ({
|
||||||
params: { id: post.id },
|
params: {
|
||||||
|
id: post.id,
|
||||||
|
news_slug: getLocalizedRoute('news', post.data.locale)
|
||||||
|
},
|
||||||
props: { post },
|
props: { post },
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
const { news_slug } = Astro.params;
|
||||||
const { post } = Astro.props;
|
const { post } = Astro.props;
|
||||||
const { Content } = await render(post);
|
const { Content } = await render(post);
|
||||||
|
|
||||||
console.log("astro site", Astro.site);
|
console.log("astro site", Astro.site);
|
||||||
const baseUrl = Astro.site ?? "https://mk8nrc8p-4321.brs.devtunnels.ms";
|
const baseUrl = Astro.site ?? "https://mk8nrc8p-4321.brs.devtunnels.ms";
|
||||||
|
|
||||||
const pageUrl = new URL(`/es/news/${post.id}`, baseUrl).toString();
|
const pageUrl = new URL(`/${post.data.locale}/${news_slug}/${post.id}`, baseUrl).toString();
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -8,9 +8,18 @@ import FooterSection from "@/components/section/FooterSection.astro";
|
||||||
import NewsList from "@/components/cards/NewsList.astro";
|
import NewsList from "@/components/cards/NewsList.astro";
|
||||||
|
|
||||||
|
|
||||||
import { createTranslator, t } from '@/i18n';
|
import { createTranslator, getLocalizedRoute, routeTranslations } from '@/i18n';
|
||||||
const tl = createTranslator(Astro.currentLocale);
|
const tl = createTranslator(Astro.currentLocale);
|
||||||
|
|
||||||
|
export function getStaticPaths() {
|
||||||
|
const locales = Object.keys(routeTranslations.news);
|
||||||
|
return locales.map((locale) => ({
|
||||||
|
params: {
|
||||||
|
news_slug: getLocalizedRoute('news', locale)
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
const newsItems = await getCollection("news", (post)=>{
|
const newsItems = await getCollection("news", (post)=>{
|
||||||
const currentLocale = Astro.currentLocale;
|
const currentLocale = Astro.currentLocale;
|
||||||
return post.data.locale == currentLocale
|
return post.data.locale == currentLocale
|
||||||
Loading…
Reference in New Issue