xpeditis2.0/apps/frontend/app/[locale]/blog/[slug]/page.tsx
David 99e01f97fc
Some checks failed
Dev CI / Security gate (push) Successful in 31s
Dev CI / Backend — Lint (push) Successful in 1m8s
Dev CI / Frontend — Lint & Type-check (push) Successful in 1m14s
Dev CI / Backend — Unit Tests (push) Successful in 1m10s
Dev CI / Frontend — Unit Tests (push) Successful in 46s
Dev CI / Notify Failure (push) Has been skipped
CD Preprod / Security gate (push) Successful in 29s
CD Preprod / Backend — Lint (push) Successful in 1m7s
CD Preprod / Frontend — Lint & Type-check (push) Successful in 1m13s
CD Preprod / Backend — Unit Tests (push) Successful in 1m6s
CD Preprod / Frontend — Unit Tests (push) Successful in 44s
CD Preprod / Backend — Integration Tests (push) Failing after 37s
CD Preprod / Build Log Exporter (push) Has been skipped
CD Preprod / Image security (${{ matrix.service }}, ${{ matrix.arch }}) (amd64, backend) (push) Has been skipped
CD Preprod / Build Backend (push) Has been skipped
CD Preprod / Build Frontend (push) Has been skipped
CD Preprod / Image security (${{ matrix.service }}, ${{ matrix.arch }}) (amd64, frontend) (push) Has been skipped
CD Preprod / Image security (${{ matrix.service }}, ${{ matrix.arch }}) (amd64, log-exporter) (push) Has been skipped
CD Preprod / Image security (${{ matrix.service }}, ${{ matrix.arch }}) (arm64, backend) (push) Has been skipped
CD Preprod / Image security (${{ matrix.service }}, ${{ matrix.arch }}) (arm64, frontend) (push) Has been skipped
CD Preprod / Image security (${{ matrix.service }}, ${{ matrix.arch }}) (arm64, log-exporter) (push) Has been skipped
CD Preprod / Deploy to Preprod (push) Has been skipped
CD Preprod / Notify Success (push) Has been skipped
CD Preprod / Notify Failure (push) Has been skipped
fix
2026-09-24 21:32:47 +02:00

155 lines
4.0 KiB
TypeScript

import type { Metadata } from 'next';
import BlogPostContent from './BlogPostContent';
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:4000';
interface BlogFaqItem {
question: string;
answer: string;
}
interface BlogPostMeta {
title: string;
slug: string;
excerpt: string;
metaTitle?: string;
metaDescription?: string;
primaryKeyword?: string;
secondaryKeywords?: string[];
coverImageUrl?: string;
authorName: string;
publishedAt?: string;
updatedAt: string;
aiSummary?: string | null;
faq?: BlogFaqItem[];
keyTakeaways?: string[];
}
async function fetchPostMeta(slug: string): Promise<BlogPostMeta | null> {
try {
const res = await fetch(`${API_BASE_URL}/api/v1/blog/${slug}`, {
cache: 'no-store',
});
if (!res.ok) return null;
return (await res.json()) as BlogPostMeta;
} catch {
return null;
}
}
function buildJsonLd(post: BlogPostMeta, slug: string) {
const coverImage = post.coverImageUrl
? post.coverImageUrl.startsWith('http')
? post.coverImageUrl
: `${API_BASE_URL}${post.coverImageUrl}`
: undefined;
const article = {
'@context': 'https://schema.org',
'@type': 'Article',
headline: post.metaTitle || post.title,
description: post.metaDescription || post.aiSummary || post.excerpt,
image: coverImage ? [coverImage] : undefined,
datePublished: post.publishedAt,
dateModified: post.updatedAt,
author: { '@type': 'Person', name: post.authorName },
publisher: { '@type': 'Organization', name: 'Xpeditis' },
mainEntityOfPage: { '@type': 'WebPage', '@id': `https://xpeditis.com/blog/${slug}` },
keywords: [post.primaryKeyword, ...(post.secondaryKeywords ?? [])]
.filter(Boolean)
.join(', '),
};
const faqPage =
post.faq && post.faq.length > 0
? {
'@context': 'https://schema.org',
'@type': 'FAQPage',
mainEntity: post.faq.map(item => ({
'@type': 'Question',
name: item.question,
acceptedAnswer: { '@type': 'Answer', text: item.answer },
})),
}
: null;
return { article, faqPage };
}
export async function generateMetadata({
params,
}: {
params: Promise<{ slug: string; locale: string }>;
}): Promise<Metadata> {
const { slug } = await params;
const post = await fetchPostMeta(slug);
if (!post) {
return { title: 'Blog — Xpeditis' };
}
const title = post.metaTitle || post.title;
const description = post.metaDescription || post.excerpt;
const keywords = [post.primaryKeyword, ...(post.secondaryKeywords ?? [])]
.filter(Boolean)
.join(', ');
const coverImage = post.coverImageUrl
? post.coverImageUrl.startsWith('http')
? post.coverImageUrl
: `${API_BASE_URL}${post.coverImageUrl}`
: undefined;
return {
title,
description,
keywords: keywords || undefined,
openGraph: {
title,
description,
type: 'article',
publishedTime: post.publishedAt,
authors: [post.authorName],
images: coverImage ? [{ url: coverImage, alt: post.title }] : [],
},
twitter: {
card: 'summary_large_image',
title,
description,
images: coverImage ? [coverImage] : [],
},
alternates: {
canonical: `/blog/${slug}`,
},
};
}
export default async function BlogPostPage({
params,
}: {
params: Promise<{ slug: string; locale: string }>;
}) {
const { slug } = await params;
const post = await fetchPostMeta(slug);
const jsonLd = post ? buildJsonLd(post, slug) : null;
return (
<>
{jsonLd && (
<>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd.article) }}
/>
{jsonLd.faqPage && (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd.faqPage) }}
/>
)}
</>
)}
<BlogPostContent slug={slug} />
</>
);
}