fix: lang switch
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 6s
Build & Deploy / 🧪 QA (push) Successful in 2m14s
Build & Deploy / 🏗️ Build (push) Successful in 3m24s
Build & Deploy / 🚀 Deploy (push) Successful in 16s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 42m44s
Build & Deploy / 🔔 Notify (push) Successful in 2s

This commit is contained in:
2026-02-27 02:56:23 +01:00
parent b3057d8be0
commit 9e7f6ec76f
8 changed files with 311 additions and 71 deletions

View File

@@ -1,4 +1,4 @@
import { notFound } from 'next/navigation';
import { notFound, redirect } from 'next/navigation';
import { Container, Badge, Heading } from '@/components/ui';
import { getTranslations, setRequestLocale } from 'next-intl/server';
import { Metadata } from 'next';
@@ -21,15 +21,18 @@ export async function generateMetadata({ params }: PageProps): Promise<Metadata>
if (!pageData) return {};
const fileSlug = await mapSlugToFileSlug(slug, locale);
const fileSlug = await mapSlugToFileSlug(pageData.slug || slug, locale);
const deSlug = await mapFileSlugToTranslated(fileSlug, 'de');
const enSlug = await mapFileSlugToTranslated(fileSlug, 'en');
// Determine correct localized slug based on current locale
const currentLocaleSlug = locale === 'de' ? deSlug : enSlug;
return {
title: pageData.frontmatter.title,
description: pageData.frontmatter.excerpt || '',
alternates: {
canonical: `${SITE_URL}/${locale}/${slug}`,
canonical: `${SITE_URL}/${locale}/${currentLocaleSlug}`,
languages: {
de: `${SITE_URL}/de/${deSlug}`,
en: `${SITE_URL}/en/${enSlug}`,
@@ -39,7 +42,7 @@ export async function generateMetadata({ params }: PageProps): Promise<Metadata>
openGraph: {
title: `${pageData.frontmatter.title} | KLZ Cables`,
description: pageData.frontmatter.excerpt || '',
url: `${SITE_URL}/${locale}/${slug}`,
url: `${SITE_URL}/${locale}/${currentLocaleSlug}`,
},
twitter: {
card: 'summary_large_image',
@@ -59,6 +62,13 @@ export default async function StandardPage({ params }: PageProps) {
notFound();
}
// Redirect if accessed via a different locale's slug
const fileSlug = await mapSlugToFileSlug(pageData.slug || slug, locale);
const correctSlug = await mapFileSlugToTranslated(fileSlug, locale);
if (correctSlug && correctSlug !== slug) {
redirect(`/${locale}/${correctSlug}`);
}
// Full-bleed pages render blocks edge-to-edge without the generic article wrapper
if (pageData.frontmatter.layout === 'fullBleed') {
return (

View File

@@ -1,4 +1,4 @@
import { notFound } from 'next/navigation';
import { notFound, redirect } from 'next/navigation';
import JsonLd from '@/components/JsonLd';
import { SITE_URL } from '@/lib/schema';
import { getPostBySlug, getAdjacentPosts, getReadingTime } from '@/lib/blog';
@@ -32,7 +32,7 @@ export async function generateMetadata({ params }: BlogPostProps): Promise<Metad
title: post.frontmatter.title,
description: description,
alternates: {
canonical: `${SITE_URL}/${locale}/blog/${slug}`,
canonical: `${SITE_URL}/${locale}/blog/${post.slug}`,
},
openGraph: {
title: `${post.frontmatter.title} | KLZ Cables`,
@@ -40,7 +40,7 @@ export async function generateMetadata({ params }: BlogPostProps): Promise<Metad
type: 'article',
publishedTime: post.frontmatter.date,
authors: ['KLZ Cables'],
url: `${SITE_URL}/${locale}/blog/${slug}`,
url: `${SITE_URL}/${locale}/blog/${post.slug}`,
},
twitter: {
card: 'summary_large_image',
@@ -54,12 +54,19 @@ export default async function BlogPost({ params }: BlogPostProps) {
const { locale, slug } = await params;
setRequestLocale(locale);
const post = await getPostBySlug(slug, locale);
const { prev, next, isPrevRandom, isNextRandom } = await getAdjacentPosts(slug, locale);
if (!post) {
notFound();
}
// If the user accessed this post using a slug from a different locale
// (e.g. via the generic language switcher), redirect them to the correct localized slug URL
if (post.slug && post.slug !== slug) {
redirect(`/${locale}/blog/${post.slug}`);
}
const { prev, next, isPrevRandom, isNextRandom } = await getAdjacentPosts(post.slug, locale);
// Convert Lexical content into a plain string to estimate reading time roughly
const rawTextContent = JSON.stringify(post.content);

View File

@@ -59,6 +59,21 @@ export default async function ContactPage({ params }: ContactPageProps) {
const { locale } = await params;
setRequestLocale(locale);
const t = await getTranslations({ locale, namespace: 'Contact' });
// Get translated slug to redirect if user used incorrect static slug
const { headers } = await import('next/headers');
const headersList = await headers();
const urlPath = headersList.get('x-invoke-path') || '';
const currentSlug = urlPath.split('/').pop();
if (currentSlug) {
const contactSlugDe = locale === 'de' ? 'kontakt' : 'contact';
if (currentSlug !== contactSlugDe && (currentSlug === 'kontakt' || currentSlug === 'contact')) {
const { redirect } = await import('next/navigation');
redirect(`/${locale}/${contactSlugDe}`);
}
}
return (
<div className="flex flex-col min-h-screen bg-neutral-light">
<JsonLd

View File

@@ -1,66 +1,137 @@
'use client';
import { useTranslations } from 'next-intl';
import { getTranslations } from 'next-intl/server';
import { Container, Button, Heading } from '@/components/ui';
import Scribble from '@/components/Scribble';
import { useEffect } from 'react';
import { useAnalytics } from '@/components/analytics/useAnalytics';
import { AnalyticsEvents } from '@/components/analytics/analytics-events';
import { getPayload } from 'payload';
import configPromise from '@payload-config';
import { headers } from 'next/headers';
import ClientNotFoundTracker from '@/components/analytics/ClientNotFoundTracker';
export default function NotFound() {
const t = useTranslations('Error.notFound');
const { trackEvent } = useAnalytics();
export default async function NotFound() {
const t = await getTranslations('Error.notFound');
useEffect(() => {
const errorUrl = typeof window !== 'undefined' ? window.location.pathname : 'unknown';
trackEvent(AnalyticsEvents.ERROR, {
type: '404_not_found',
path: errorUrl,
});
// Try to determine the requested path
const headersList = await headers();
const urlPath = headersList.get('x-invoke-path') || '';
// Explicitly send the 404 to Sentry so we have visibility into broken links
import('@sentry/nextjs').then((Sentry) => {
Sentry.withScope((scope) => {
scope.setTag('status_code', '404');
scope.setTag('path', errorUrl);
Sentry.captureMessage(`Route Not Found: ${errorUrl}`, 'warning');
});
});
}, [trackEvent]);
let suggestedUrl = null;
let suggestedLang = null;
// If we have a path, try to see if the last segment (slug) exists in ANY locale
if (urlPath) {
const slug = urlPath.split('/').filter(Boolean).pop();
if (slug) {
try {
const payload = await getPayload({ config: configPromise });
// Check posts
const postRes = await payload.find({
collection: 'posts',
where: { slug: { equals: slug } },
locale: 'all',
limit: 1,
});
// Check products
const productRes =
postRes.docs.length === 0
? await payload.find({
collection: 'products',
where: { slug: { equals: slug } },
locale: 'all',
limit: 1,
})
: { docs: [] };
// Check pages
const pageRes =
postRes.docs.length === 0 && productRes.docs.length === 0
? await payload.find({
collection: 'pages',
where: { slug: { equals: slug } },
locale: 'all',
limit: 1,
})
: { docs: [] };
const anyDoc = postRes.docs[0] || productRes.docs[0] || pageRes.docs[0];
if (anyDoc) {
// If the doc exists, we can figure out its native locale or
// offer the alternative locale (if we are in 'de', offer 'en')
const currentLocale = urlPath.startsWith('/en') ? 'en' : 'de';
const alternativeLocale = currentLocale === 'de' ? 'en' : 'de';
suggestedLang = alternativeLocale === 'de' ? 'Deutsch' : 'English';
// Reconstruct the URL for the alternative locale
const pathParts = urlPath.split('/').filter(Boolean);
if (pathParts.length > 0 && (pathParts[0] === 'en' || pathParts[0] === 'de')) {
pathParts[0] = alternativeLocale;
} else {
pathParts.unshift(alternativeLocale);
}
suggestedUrl = '/' + pathParts.join('/');
}
} catch (e) {
// Ignore Payload errors in 404
}
}
}
return (
<Container className="relative py-24 flex flex-col items-center justify-center text-center min-h-[70vh] overflow-hidden">
{/* Industrial Background Element */}
<div className="absolute inset-0 -z-10 opacity-[0.03] pointer-events-none flex items-center justify-center">
<span className="text-[20rem] font-bold select-none">404</span>
</div>
<>
<ClientNotFoundTracker path={urlPath} />
<Container className="relative py-24 flex flex-col items-center justify-center text-center min-h-[70vh] overflow-hidden">
{/* Industrial Background Element */}
<div className="absolute inset-0 -z-10 opacity-[0.03] pointer-events-none flex items-center justify-center">
<span className="text-[20rem] font-bold select-none">404</span>
</div>
<div className="relative mb-8">
<Heading level={1} className="text-6xl md:text-8xl font-bold mb-2">
404
<div className="relative mb-8">
<Heading level={1} className="text-6xl md:text-8xl font-bold mb-2">
404
</Heading>
<Scribble
variant="circle"
className="w-[150%] h-[150%] -top-[25%] -left-[25%] text-accent/40"
/>
</div>
<Heading level={2} className="text-2xl md:text-3xl font-bold mb-4 text-primary">
{t('title')}
</Heading>
<Scribble
variant="circle"
className="w-[150%] h-[150%] -top-[25%] -left-[25%] text-accent/40"
/>
</div>
<Heading level={2} className="text-2xl md:text-3xl font-bold mb-4 text-primary">
{t('title')}
</Heading>
<p className="text-text-secondary mb-10 max-w-md text-lg">{t('description')}</p>
<p className="text-white/60 mb-10 max-w-md text-lg">{t('description')}</p>
{suggestedUrl && (
<div className="mb-12 p-6 bg-accent/10 border border-accent/20 rounded-2xl animate-fade-in shadow-lg relative overflow-hidden group">
<div className="absolute inset-0 bg-accent/5 -skew-x-12 translate-x-full group-hover:translate-x-0 transition-transform duration-700" />
<div className="relative z-10">
<h3 className="text-primary font-bold mb-2 text-lg">
Did you mean to visit the {suggestedLang} version?
</h3>
<p className="text-text-secondary text-sm mb-4">
This page exists, but in another language.
</p>
<Button href={suggestedUrl} variant="accent" size="md" className="w-full sm:w-auto">
Go to {suggestedLang} Version
</Button>
</div>
</div>
)}
<div className="flex flex-col sm:flex-row gap-4">
<Button href="/" variant="accent" size="lg">
{t('cta')}
</Button>
<Button href="/contact" variant="outline" size="lg">
Contact Support
</Button>
</div>
<div className="flex flex-col sm:flex-row gap-4">
<Button href="/" variant={suggestedUrl ? 'outline' : 'accent'} size="lg">
{t('cta')}
</Button>
<Button href="/contact" variant={suggestedUrl ? 'ghost' : 'outline'} size="lg">
Contact Support
</Button>
</div>
{/* Decorative Industrial Line */}
<div className="absolute bottom-0 left-1/2 -translate-x-1/2 w-px h-24 bg-gradient-to-t from-accent/50 to-transparent" />
</Container>
{/* Decorative Industrial Line */}
<div className="absolute bottom-0 left-1/2 -translate-x-1/2 w-px h-24 bg-gradient-to-t from-accent/50 to-transparent" />
</Container>
</>
);
}

View File

@@ -14,7 +14,7 @@ import { getTranslations, setRequestLocale } from 'next-intl/server';
import { getProductOGImageMetadata } from '@/lib/metadata';
import Image from 'next/image';
import Link from 'next/link';
import { notFound } from 'next/navigation';
import { notFound, redirect } from 'next/navigation';
import ProductEngagementTracker from '@/components/analytics/ProductEngagementTracker';
import PayloadRichText from '@/components/PayloadRichText';
@@ -53,7 +53,7 @@ export async function generateMetadata({ params }: ProductPageProps): Promise<Me
title: categoryTitle,
description: categoryDesc,
alternates: {
canonical: `${SITE_URL}/${locale}/${await mapFileSlugToTranslated('products', locale)}/${productSlug}`,
canonical: `${SITE_URL}/${locale}/${await mapFileSlugToTranslated('products', locale)}/${await mapFileSlugToTranslated(fileSlug, locale)}`,
languages: {
de: `${SITE_URL}/de/${await mapFileSlugToTranslated('products', 'de')}/${await mapFileSlugToTranslated(fileSlug, 'de')}`,
en: `${SITE_URL}/en/${await mapFileSlugToTranslated('products', 'en')}/${await mapFileSlugToTranslated(fileSlug, 'en')}`,
@@ -75,11 +75,13 @@ export async function generateMetadata({ params }: ProductPageProps): Promise<Me
const product = await getProductBySlug(productSlug, locale);
if (!product) return {};
const currentLocalePath = await getLocalizedPath(locale);
return {
title: product.frontmatter.title,
description: product.frontmatter.description,
alternates: {
canonical: `${SITE_URL}/${locale}/${await mapFileSlugToTranslated('products', locale)}/${slug.join('/')}`,
canonical: `${SITE_URL}/${locale}/${currentLocalePath}`,
languages: {
de: `${SITE_URL}/de/${await getLocalizedPath('de')}`,
en: `${SITE_URL}/en/${await getLocalizedPath('en')}`,
@@ -90,7 +92,7 @@ export async function generateMetadata({ params }: ProductPageProps): Promise<Me
title: product.frontmatter.title,
description: product.frontmatter.description,
type: 'website',
url: `${SITE_URL}/${locale}/products/${slug.join('/')}`,
url: `${SITE_URL}/${locale}/${currentLocalePath}`,
images: getProductOGImageMetadata(productSlug, product.frontmatter.title, locale),
},
twitter: {
@@ -114,7 +116,19 @@ export default async function ProductPage({ params }: ProductPageProps) {
'high-voltage-cables',
'solar-cables',
];
const fileSlug = await mapSlugToFileSlug(productSlug, locale);
const fileSlugs = await Promise.all(slug.map((s) => mapSlugToFileSlug(s, locale)));
const translatedSlugsForLocale = await Promise.all(
fileSlugs.map((fs) => mapFileSlugToTranslated(fs, locale)),
);
// If the requested slugs don't exactly match the translated slugs for the current locale
// (i.e. if the user used the static language switcher but kept the original locale's slugs)
if (slug.join('/') !== translatedSlugsForLocale.join('/')) {
redirect(`/${locale}/${productsSlug}/${translatedSlugsForLocale.join('/')}`);
}
const fileSlug = fileSlugs[fileSlugs.length - 1];
if (categories.includes(fileSlug)) {
const allProducts = await getAllProducts(locale);