fix(seo): correct canonical tags and localized blog post hreflang
This commit is contained in:
@@ -6,6 +6,7 @@ import {
|
|||||||
getAdjacentPosts,
|
getAdjacentPosts,
|
||||||
getReadingTime,
|
getReadingTime,
|
||||||
extractLexicalHeadings,
|
extractLexicalHeadings,
|
||||||
|
getPostSlugs,
|
||||||
} from '@/lib/blog';
|
} from '@/lib/blog';
|
||||||
import { Metadata } from 'next';
|
import { Metadata } from 'next';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
@@ -33,12 +34,21 @@ export async function generateMetadata({ params }: BlogPostProps): Promise<Metad
|
|||||||
|
|
||||||
if (!post) return {};
|
if (!post) return {};
|
||||||
|
|
||||||
|
const slugs = await getPostSlugs(slug, locale);
|
||||||
|
const deSlug = slugs?.de || post.slug;
|
||||||
|
const enSlug = slugs?.en || post.slug;
|
||||||
|
|
||||||
const description = post.frontmatter.excerpt || '';
|
const description = post.frontmatter.excerpt || '';
|
||||||
return {
|
return {
|
||||||
title: post.frontmatter.title,
|
title: post.frontmatter.title,
|
||||||
description: description,
|
description: description,
|
||||||
alternates: {
|
alternates: {
|
||||||
canonical: `${SITE_URL}/${locale}/blog/${post.slug}`,
|
canonical: `${SITE_URL}/${locale}/blog/${post.slug}`,
|
||||||
|
languages: {
|
||||||
|
de: `${SITE_URL}/de/blog/${deSlug}`,
|
||||||
|
en: `${SITE_URL}/en/blog/${enSlug}`,
|
||||||
|
'x-default': `${SITE_URL}/en/blog/${enSlug}`,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
openGraph: {
|
openGraph: {
|
||||||
title: `${post.frontmatter.title} | KLZ Cables`,
|
title: `${post.frontmatter.title} | KLZ Cables`,
|
||||||
@@ -134,13 +144,13 @@ export default async function BlogPost({ params }: BlogPostProps) {
|
|||||||
<span>{getReadingTime(rawTextContent)} min read</span>
|
<span>{getReadingTime(rawTextContent)} min read</span>
|
||||||
{(new Date(post.frontmatter.date) > new Date() ||
|
{(new Date(post.frontmatter.date) > new Date() ||
|
||||||
post.frontmatter.public === false) && (
|
post.frontmatter.public === false) && (
|
||||||
<>
|
<>
|
||||||
<span className="w-1 h-1 bg-white/30 rounded-full" />
|
<span className="w-1 h-1 bg-white/30 rounded-full" />
|
||||||
<span className="px-2 py-0.5 border border-white/40 text-white/80 rounded uppercase tracking-widest text-[10px] md:text-xs font-bold">
|
<span className="px-2 py-0.5 border border-white/40 text-white/80 rounded uppercase tracking-widest text-[10px] md:text-xs font-bold">
|
||||||
Draft Preview
|
Draft Preview
|
||||||
</span>
|
</span>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -171,13 +181,13 @@ export default async function BlogPost({ params }: BlogPostProps) {
|
|||||||
<span>{getReadingTime(rawTextContent)} min read</span>
|
<span>{getReadingTime(rawTextContent)} min read</span>
|
||||||
{(new Date(post.frontmatter.date) > new Date() ||
|
{(new Date(post.frontmatter.date) > new Date() ||
|
||||||
post.frontmatter.public === false) && (
|
post.frontmatter.public === false) && (
|
||||||
<>
|
<>
|
||||||
<span className="w-1 h-1 bg-neutral-300 rounded-full" />
|
<span className="w-1 h-1 bg-neutral-300 rounded-full" />
|
||||||
<span className="px-2 py-0.5 border border-orange-500/50 text-orange-600 rounded uppercase tracking-widest text-[10px] md:text-xs font-bold">
|
<span className="px-2 py-0.5 border border-orange-500/50 text-orange-600 rounded uppercase tracking-widest text-[10px] md:text-xs font-bold">
|
||||||
Draft Preview
|
Draft Preview
|
||||||
</span>
|
</span>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
@@ -35,13 +35,6 @@ export async function generateMetadata(props: {
|
|||||||
},
|
},
|
||||||
metadataBase: new URL(baseUrl),
|
metadataBase: new URL(baseUrl),
|
||||||
manifest: '/manifest.webmanifest',
|
manifest: '/manifest.webmanifest',
|
||||||
alternates: {
|
|
||||||
canonical: `${baseUrl}/${locale}`,
|
|
||||||
languages: {
|
|
||||||
de: `${baseUrl}/de`,
|
|
||||||
en: `${baseUrl}/en`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
icons: {
|
icons: {
|
||||||
icon: [
|
icon: [
|
||||||
{ url: '/favicon.ico', sizes: 'any' },
|
{ url: '/favicon.ico', sizes: 'any' },
|
||||||
|
|||||||
54
lib/blog.ts
54
lib/blog.ts
@@ -136,6 +136,60 @@ export async function getPostBySlug(slug: string, locale: string): Promise<PostD
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getPostSlugs(slug: string, locale: string): Promise<Record<string, string>> {
|
||||||
|
try {
|
||||||
|
const payload = await getPayload({ config: configPromise });
|
||||||
|
|
||||||
|
// First, find the post in the current locale to get its ID
|
||||||
|
let { docs } = await payload.find({
|
||||||
|
collection: 'posts',
|
||||||
|
where: {
|
||||||
|
slug: { equals: slug },
|
||||||
|
...(!config.showDrafts ? { _status: { equals: 'published' } } : {}),
|
||||||
|
},
|
||||||
|
locale: locale as any,
|
||||||
|
draft: config.showDrafts,
|
||||||
|
limit: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!docs || docs.length === 0) {
|
||||||
|
// Fallback: search across all locales
|
||||||
|
const { docs: crossLocaleDocs } = await payload.find({
|
||||||
|
collection: 'posts',
|
||||||
|
where: {
|
||||||
|
slug: { equals: slug },
|
||||||
|
...(!config.showDrafts ? { _status: { equals: 'published' } } : {}),
|
||||||
|
},
|
||||||
|
locale: 'all',
|
||||||
|
draft: config.showDrafts,
|
||||||
|
limit: 1,
|
||||||
|
});
|
||||||
|
docs = crossLocaleDocs;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!docs || docs.length === 0) return {};
|
||||||
|
|
||||||
|
const postId = docs[0].id;
|
||||||
|
|
||||||
|
// Fetch the post with locale 'all' to get all localized fields
|
||||||
|
const { docs: allLocalesDocs } = await payload.find({
|
||||||
|
collection: 'posts',
|
||||||
|
where: {
|
||||||
|
id: { equals: postId },
|
||||||
|
},
|
||||||
|
locale: 'all',
|
||||||
|
draft: config.showDrafts,
|
||||||
|
limit: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!allLocalesDocs || allLocalesDocs.length === 0) return {};
|
||||||
|
return (allLocalesDocs[0].slug as unknown as Record<string, string>) || {};
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[Payload] getPostSlugs failed for ${slug}:`, error);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function getAllPosts(locale: string): Promise<PostData[]> {
|
export async function getAllPosts(locale: string): Promise<PostData[]> {
|
||||||
try {
|
try {
|
||||||
const payload = await getPayload({ config: configPromise });
|
const payload = await getPayload({ config: configPromise });
|
||||||
|
|||||||
Reference in New Issue
Block a user