feat: implement article detail pages with SEO and layout components

This commit is contained in:
Esteban Paz 2026-05-07 21:08:46 -05:00
parent aa54417618
commit 6cea6fe48b
7 changed files with 86 additions and 22 deletions

View File

@ -7,6 +7,7 @@ const {
description = "", description = "",
image = null, image = null,
url = null, url = null,
date = null,
} = Astro.props; } = Astro.props;
const imageUrl = image ? new URL(image, Astro.site).toString() : null; const imageUrl = image ? new URL(image, Astro.site).toString() : null;
@ -35,6 +36,8 @@ const imageUrl = image ? new URL(image, Astro.site).toString() : null;
<meta property="og:image:width" content="1200"> <meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630"> <meta property="og:image:height" content="630">
{date && <meta property="article:published_time" content={date instanceof Date ? date.toISOString() : date} />}
<!-- Twitter --> <!-- Twitter -->
<meta name="twitter:card" content="summary_large_image" /> <meta name="twitter:card" content="summary_large_image" />

View File

@ -15,27 +15,27 @@
<ul class="flex flex-col gap-4 bg-white/90 backdrop-blur-md py-4 px-4 rounded-r-2xl shadow-xl border border-gray-200"> <ul class="flex flex-col gap-4 bg-white/90 backdrop-blur-md py-4 px-4 rounded-r-2xl shadow-xl border border-gray-200">
<li class="border-b pb-3"> <li class="border-b pb-3">
<a :href="twitterUrl" target="_blank"> <a :href="twitterUrl" target="_blank">
<Icon icon="ph:x-logo-thin" class="text-2xl" /> <Icon icon="ph:x-logo-thin" class="text-2xl text-black" />
</a> </a>
</li> </li>
<li class="border-b pb-3"> <li class="border-b pb-3">
<a :href="facebookUrl" target="_blank"> <a :href="facebookUrl" target="_blank">
<Icon icon="ph:facebook-logo-thin" class="text-2xl" /> <Icon icon="ph:facebook-logo-thin" class="text-2xl text-black" />
</a> </a>
</li> </li>
<li class="border-b pb-3"> <li class="border-b pb-3">
<a :href="whatsappUrl" target="_blank"> <a :href="whatsappUrl" target="_blank">
<Icon icon="ph:whatsapp-logo-thin" class="text-2xl" /> <Icon icon="ph:whatsapp-logo-thin" class="text-2xl text-black" />
</a> </a>
</li> </li>
<li class="border-b pb-3"> <li class="border-b pb-3">
<a :href="linkedinUrl" target="_blank"> <a :href="linkedinUrl" target="_blank">
<Icon icon="ph:linkedin-logo-thin" class="text-2xl" /> <Icon icon="ph:linkedin-logo-thin" class="text-2xl text-black" />
</a> </a>
</li> </li>
<li> <li>
<button @click="copyLink" class="cursor-pointer"> <button @click="copyLink" class="cursor-pointer">
<Icon :icon="copied ? 'ph:check-thin' : 'ph:link-thin'" class="text-2xl" /> <Icon :icon="copied ? 'ph:check-thin' : 'ph:link-thin'" class="text-2xl text-black" />
</button> </button>
</li> </li>
</ul> </ul>

View File

@ -6,7 +6,7 @@ const { title } = Astro.props;
<div class="md:py-16 p-4 bg-white"> <div class="md:py-16 p-4 bg-white">
<div class="container mx-auto"> <div class="container mx-auto">
<div class="flex justify-between px-4"> <div class="flex justify-between px-4">
<h2 class="text-tertiary font-secondary text-xl sm:text-2xl md:text-3xl lg:text-5xl font-bold">{title}</h2> <h2 id="article-title" class="text-tertiary font-secondary text-xl sm:text-2xl md:text-3xl lg:text-5xl font-bold">{title}</h2>
<img class="md:w-20 md:h-20 w-10 h-10" src="/img/lion.svg" alt="Leon"> <img class="md:w-20 md:h-20 w-10 h-10" src="/img/lion.svg" alt="Leon">
</div> </div>
</div> </div>

View File

@ -13,7 +13,8 @@ const {
title, title,
description, description,
image, image,
url url,
date
} = Astro.props; } = Astro.props;
const currentLocale = Astro.currentLocale ?? 'es'; const currentLocale = Astro.currentLocale ?? 'es';
@ -29,6 +30,7 @@ const isNewsPage = newsSegments.some(segment => Astro.url.pathname.includes(`/${
description={description} description={description}
image={image} image={image}
url={url} url={url}
date={date}
/> />
<script> <script>
document.addEventListener('contextmenu', (event) => { document.addEventListener('contextmenu', (event) => {

View File

@ -47,14 +47,41 @@ const words = plainText
.filter((w) => w.length > 0) .filter((w) => w.length > 0)
.slice(0, 35); .slice(0, 35);
const excerpt = words.join(" ") + (words.length === 35 ? "..." : ""); const excerpt = words.join(" ") + (words.length === 35 ? "..." : "");
const canonicalUrl = new URL(`/${locale}/${news_slug}/${post.id}`, Astro.site);
const imageUrl = post.data.thumbnail
? new URL(post.data.thumbnail, Astro.site).toString()
: null;
const localeDate = new Intl.DateTimeFormat(locale || "es", {
year: "numeric",
month: "long",
day: "numeric",
}).format(post.data.date);
--- ---
<MainLayout <MainLayout
title={post.data.title} title={post.data.title}
description={excerpt} description={excerpt}
image={post.data.thumbnail} image={post.data.thumbnail}
url={new URL(`/${locale}/${news_slug}/${post.id}`, Astro.site)} url={canonicalUrl}
date={post.data.date}
> >
<script
type="application/ld+json"
set:html={JSON.stringify({
"@context": "https://schema.org",
"@type": "NewsArticle",
headline: post.data.title,
datePublished: post.data.date,
description: excerpt,
image: imageUrl,
url: canonicalUrl.toString(),
author: {
"@type": "Organization",
name: "Centro del Reino de Paz y Justicia",
},
})}
></script>
<div class="container mx-auto md:py-16 py-8"> <div class="container mx-auto md:py-16 py-8">
<Header /> <Header />
</div> </div>
@ -78,14 +105,21 @@ const excerpt = words.join(" ") + (words.length === 35 ? "..." : "");
} }
</a> </a>
</div> </div>
<div class="grid md:grid-cols-10"> <div class="grid md:grid-cols-10 container mx-auto">
<div <div
id="article-content"
class="md:col-span-7 content bg-white p-8 md:p-20 prose-p:mb-4 text-[#003421] text-justify" 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"> <article id="article-body">
{excerpt} <time
</p> --> id="article-date"
<Content /> class="text-center text-[#003421]/60 text-sm hidden"
datetime={post.data.date.toISOString()}
>
{localeDate}
</time>
<Content />
</article>
</div> </div>
<div class="md:col-span-3 bg-tertiary md:sticky top-0 h-fit"> <div class="md:col-span-3 bg-tertiary md:sticky top-0 h-fit">
{post.data.youtube && <YouTube id={post.data.youtube} />} {post.data.youtube && <YouTube id={post.data.youtube} />}

View File

@ -29,6 +29,8 @@ const baseUrl = Astro.site ?? "https://mk8nrc8p-4321.brs.devtunnels.ms";
const pageUrl = new URL(`/${post.data.locale}/${news_slug}/${post.id}`, baseUrl).toString(); const pageUrl = new URL(`/${post.data.locale}/${news_slug}/${post.id}`, baseUrl).toString();
const imageUrl = post.data.thumbnail ? new URL(post.data.thumbnail, baseUrl).toString() : null;
const localeDate = new Intl.DateTimeFormat(post.data.locale || 'es', { year: 'numeric', month: 'long', day: 'numeric' }).format(post.data.date);
--- ---
@ -39,7 +41,10 @@ const pageUrl = new URL(`/${post.data.locale}/${news_slug}/${post.id}`, baseUrl)
} }
</style> </style>
<MainLayout> <MainLayout
title={post.data.title}
date={post.data.date}
>
<Fragment slot="head"> <Fragment slot="head">
<!-- Título --> <!-- Título -->
<title>{post.data.title}</title> <title>{post.data.title}</title>
@ -60,18 +65,37 @@ const pageUrl = new URL(`/${post.data.locale}/${news_slug}/${post.id}`, baseUrl)
<meta property="og:image:type" content={`image/${post.data.gallery[0].image.format}`} /> <meta property="og:image:type" content={`image/${post.data.gallery[0].image.format}`} />
</> </>
)} )}
<meta property="article:published_time" content={post.data.date.toISOString()} />
</Fragment> </Fragment>
<script type="application/ld+json" set:html={JSON.stringify({
"@context": "https://schema.org",
"@type": "NewsArticle",
"headline": post.data.title,
"datePublished": post.data.date,
"image": imageUrl,
"url": pageUrl,
"author": {
"@type": "Organization",
"name": "Centro del Reino de Paz y Justicia"
}
})} />
<div class="container mx-auto py-16"> <div class="container mx-auto py-16">
<Header /> <Header />
</div> </div>
<TitleSection title={post.data.title} /> <TitleSection title={post.data.title} />
<time id="article-date" class="block text-center text-[#003421]/60 text-sm mt-4 mb-8" datetime={post.data.date.toISOString()}>
{localeDate}
</time>
<div class="container mx-auto"> <div class="container mx-auto">
{post.data.gallery && <CarouselSection images={post.data.gallery} />} {post.data.gallery && <CarouselSection images={post.data.gallery} />}
<div class="grid md:grid-cols-10"> <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]"> <div id="article-content" class="md:col-span-7 content bg-white p-8 md:p-20 prose-p:mb-4 text-[#003421]">
<Content /> <article id="article-body">
<Content />
</article>
</div> </div>
<div class="md:col-span-3 bg-tertiary md:sticky top-0 h-fit"> <div class="md:col-span-3 bg-tertiary md:sticky top-0 h-fit">
{ post.data.youtube && ( { post.data.youtube && (

View File

@ -24,24 +24,24 @@ body {
background-color: var(--background); background-color: var(--background);
} }
.content h2{ .content h2 {
font-size: 22px; font-size: 22px;
margin-bottom: 10px; margin-bottom: 10px;
line-height: 100%; line-height: 100%;
font-weight: 700; font-weight: 700;
} }
.content h3{ .content h3 {
font-size: 20px; font-size: 19px;
margin-bottom: 10px; margin-bottom: 10px;
line-height: 100%; line-height: 100%;
font-weight: 700; font-weight: 700;
} }
.content h1{ .content h1 {
font-size: 32px; font-size: 32px;
margin-bottom: 30px; margin-bottom: 30px;
line-height: 100%; line-height: 150%;
font-weight: 700; font-weight: 700;
text-align: left; text-align: left;
font-family: var(--font-secondary); font-family: var(--font-secondary);
@ -60,6 +60,7 @@ body {
font-size: 22px; font-size: 22px;
margin-bottom: 20px; margin-bottom: 20px;
line-height: 120%; line-height: 120%;
text-align: left; /* puedes cambiar a center si quieres */ text-align: left;
/* puedes cambiar a center si quieres */
} }
} }