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
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:
@@ -1,4 +1,4 @@
|
|||||||
import { notFound } from 'next/navigation';
|
import { notFound, redirect } from 'next/navigation';
|
||||||
import { Container, Badge, Heading } from '@/components/ui';
|
import { Container, Badge, Heading } from '@/components/ui';
|
||||||
import { getTranslations, setRequestLocale } from 'next-intl/server';
|
import { getTranslations, setRequestLocale } from 'next-intl/server';
|
||||||
import { Metadata } from 'next';
|
import { Metadata } from 'next';
|
||||||
@@ -21,15 +21,18 @@ export async function generateMetadata({ params }: PageProps): Promise<Metadata>
|
|||||||
|
|
||||||
if (!pageData) return {};
|
if (!pageData) return {};
|
||||||
|
|
||||||
const fileSlug = await mapSlugToFileSlug(slug, locale);
|
const fileSlug = await mapSlugToFileSlug(pageData.slug || slug, locale);
|
||||||
const deSlug = await mapFileSlugToTranslated(fileSlug, 'de');
|
const deSlug = await mapFileSlugToTranslated(fileSlug, 'de');
|
||||||
const enSlug = await mapFileSlugToTranslated(fileSlug, 'en');
|
const enSlug = await mapFileSlugToTranslated(fileSlug, 'en');
|
||||||
|
|
||||||
|
// Determine correct localized slug based on current locale
|
||||||
|
const currentLocaleSlug = locale === 'de' ? deSlug : enSlug;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title: pageData.frontmatter.title,
|
title: pageData.frontmatter.title,
|
||||||
description: pageData.frontmatter.excerpt || '',
|
description: pageData.frontmatter.excerpt || '',
|
||||||
alternates: {
|
alternates: {
|
||||||
canonical: `${SITE_URL}/${locale}/${slug}`,
|
canonical: `${SITE_URL}/${locale}/${currentLocaleSlug}`,
|
||||||
languages: {
|
languages: {
|
||||||
de: `${SITE_URL}/de/${deSlug}`,
|
de: `${SITE_URL}/de/${deSlug}`,
|
||||||
en: `${SITE_URL}/en/${enSlug}`,
|
en: `${SITE_URL}/en/${enSlug}`,
|
||||||
@@ -39,7 +42,7 @@ export async function generateMetadata({ params }: PageProps): Promise<Metadata>
|
|||||||
openGraph: {
|
openGraph: {
|
||||||
title: `${pageData.frontmatter.title} | KLZ Cables`,
|
title: `${pageData.frontmatter.title} | KLZ Cables`,
|
||||||
description: pageData.frontmatter.excerpt || '',
|
description: pageData.frontmatter.excerpt || '',
|
||||||
url: `${SITE_URL}/${locale}/${slug}`,
|
url: `${SITE_URL}/${locale}/${currentLocaleSlug}`,
|
||||||
},
|
},
|
||||||
twitter: {
|
twitter: {
|
||||||
card: 'summary_large_image',
|
card: 'summary_large_image',
|
||||||
@@ -59,6 +62,13 @@ export default async function StandardPage({ params }: PageProps) {
|
|||||||
notFound();
|
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
|
// Full-bleed pages render blocks edge-to-edge without the generic article wrapper
|
||||||
if (pageData.frontmatter.layout === 'fullBleed') {
|
if (pageData.frontmatter.layout === 'fullBleed') {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { notFound } from 'next/navigation';
|
import { notFound, redirect } from 'next/navigation';
|
||||||
import JsonLd from '@/components/JsonLd';
|
import JsonLd from '@/components/JsonLd';
|
||||||
import { SITE_URL } from '@/lib/schema';
|
import { SITE_URL } from '@/lib/schema';
|
||||||
import { getPostBySlug, getAdjacentPosts, getReadingTime } from '@/lib/blog';
|
import { getPostBySlug, getAdjacentPosts, getReadingTime } from '@/lib/blog';
|
||||||
@@ -32,7 +32,7 @@ export async function generateMetadata({ params }: BlogPostProps): Promise<Metad
|
|||||||
title: post.frontmatter.title,
|
title: post.frontmatter.title,
|
||||||
description: description,
|
description: description,
|
||||||
alternates: {
|
alternates: {
|
||||||
canonical: `${SITE_URL}/${locale}/blog/${slug}`,
|
canonical: `${SITE_URL}/${locale}/blog/${post.slug}`,
|
||||||
},
|
},
|
||||||
openGraph: {
|
openGraph: {
|
||||||
title: `${post.frontmatter.title} | KLZ Cables`,
|
title: `${post.frontmatter.title} | KLZ Cables`,
|
||||||
@@ -40,7 +40,7 @@ export async function generateMetadata({ params }: BlogPostProps): Promise<Metad
|
|||||||
type: 'article',
|
type: 'article',
|
||||||
publishedTime: post.frontmatter.date,
|
publishedTime: post.frontmatter.date,
|
||||||
authors: ['KLZ Cables'],
|
authors: ['KLZ Cables'],
|
||||||
url: `${SITE_URL}/${locale}/blog/${slug}`,
|
url: `${SITE_URL}/${locale}/blog/${post.slug}`,
|
||||||
},
|
},
|
||||||
twitter: {
|
twitter: {
|
||||||
card: 'summary_large_image',
|
card: 'summary_large_image',
|
||||||
@@ -54,12 +54,19 @@ export default async function BlogPost({ params }: BlogPostProps) {
|
|||||||
const { locale, slug } = await params;
|
const { locale, slug } = await params;
|
||||||
setRequestLocale(locale);
|
setRequestLocale(locale);
|
||||||
const post = await getPostBySlug(slug, locale);
|
const post = await getPostBySlug(slug, locale);
|
||||||
const { prev, next, isPrevRandom, isNextRandom } = await getAdjacentPosts(slug, locale);
|
|
||||||
|
|
||||||
if (!post) {
|
if (!post) {
|
||||||
notFound();
|
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
|
// Convert Lexical content into a plain string to estimate reading time roughly
|
||||||
const rawTextContent = JSON.stringify(post.content);
|
const rawTextContent = JSON.stringify(post.content);
|
||||||
|
|
||||||
|
|||||||
@@ -59,6 +59,21 @@ export default async function ContactPage({ params }: ContactPageProps) {
|
|||||||
const { locale } = await params;
|
const { locale } = await params;
|
||||||
setRequestLocale(locale);
|
setRequestLocale(locale);
|
||||||
const t = await getTranslations({ locale, namespace: 'Contact' });
|
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 (
|
return (
|
||||||
<div className="flex flex-col min-h-screen bg-neutral-light">
|
<div className="flex flex-col min-h-screen bg-neutral-light">
|
||||||
<JsonLd
|
<JsonLd
|
||||||
|
|||||||
@@ -1,66 +1,137 @@
|
|||||||
'use client';
|
import { getTranslations } from 'next-intl/server';
|
||||||
import { useTranslations } from 'next-intl';
|
|
||||||
import { Container, Button, Heading } from '@/components/ui';
|
import { Container, Button, Heading } from '@/components/ui';
|
||||||
import Scribble from '@/components/Scribble';
|
import Scribble from '@/components/Scribble';
|
||||||
import { useEffect } from 'react';
|
import { getPayload } from 'payload';
|
||||||
import { useAnalytics } from '@/components/analytics/useAnalytics';
|
import configPromise from '@payload-config';
|
||||||
import { AnalyticsEvents } from '@/components/analytics/analytics-events';
|
import { headers } from 'next/headers';
|
||||||
|
import ClientNotFoundTracker from '@/components/analytics/ClientNotFoundTracker';
|
||||||
|
|
||||||
export default function NotFound() {
|
export default async function NotFound() {
|
||||||
const t = useTranslations('Error.notFound');
|
const t = await getTranslations('Error.notFound');
|
||||||
const { trackEvent } = useAnalytics();
|
|
||||||
|
|
||||||
useEffect(() => {
|
// Try to determine the requested path
|
||||||
const errorUrl = typeof window !== 'undefined' ? window.location.pathname : 'unknown';
|
const headersList = await headers();
|
||||||
trackEvent(AnalyticsEvents.ERROR, {
|
const urlPath = headersList.get('x-invoke-path') || '';
|
||||||
type: '404_not_found',
|
|
||||||
path: errorUrl,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Explicitly send the 404 to Sentry so we have visibility into broken links
|
let suggestedUrl = null;
|
||||||
import('@sentry/nextjs').then((Sentry) => {
|
let suggestedLang = null;
|
||||||
Sentry.withScope((scope) => {
|
|
||||||
scope.setTag('status_code', '404');
|
// If we have a path, try to see if the last segment (slug) exists in ANY locale
|
||||||
scope.setTag('path', errorUrl);
|
if (urlPath) {
|
||||||
Sentry.captureMessage(`Route Not Found: ${errorUrl}`, 'warning');
|
const slug = urlPath.split('/').filter(Boolean).pop();
|
||||||
});
|
if (slug) {
|
||||||
});
|
try {
|
||||||
}, [trackEvent]);
|
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 (
|
return (
|
||||||
<Container className="relative py-24 flex flex-col items-center justify-center text-center min-h-[70vh] overflow-hidden">
|
<>
|
||||||
{/* Industrial Background Element */}
|
<ClientNotFoundTracker path={urlPath} />
|
||||||
<div className="absolute inset-0 -z-10 opacity-[0.03] pointer-events-none flex items-center justify-center">
|
<Container className="relative py-24 flex flex-col items-center justify-center text-center min-h-[70vh] overflow-hidden">
|
||||||
<span className="text-[20rem] font-bold select-none">404</span>
|
{/* Industrial Background Element */}
|
||||||
</div>
|
<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">
|
<div className="relative mb-8">
|
||||||
<Heading level={1} className="text-6xl md:text-8xl font-bold mb-2">
|
<Heading level={1} className="text-6xl md:text-8xl font-bold mb-2">
|
||||||
404
|
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>
|
</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">
|
<p className="text-text-secondary mb-10 max-w-md text-lg">{t('description')}</p>
|
||||||
{t('title')}
|
|
||||||
</Heading>
|
|
||||||
|
|
||||||
<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">
|
<div className="flex flex-col sm:flex-row gap-4">
|
||||||
<Button href="/" variant="accent" size="lg">
|
<Button href="/" variant={suggestedUrl ? 'outline' : 'accent'} size="lg">
|
||||||
{t('cta')}
|
{t('cta')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button href="/contact" variant="outline" size="lg">
|
<Button href="/contact" variant={suggestedUrl ? 'ghost' : 'outline'} size="lg">
|
||||||
Contact Support
|
Contact Support
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Decorative Industrial Line */}
|
{/* 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" />
|
<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>
|
</Container>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import { getTranslations, setRequestLocale } from 'next-intl/server';
|
|||||||
import { getProductOGImageMetadata } from '@/lib/metadata';
|
import { getProductOGImageMetadata } from '@/lib/metadata';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { notFound } from 'next/navigation';
|
import { notFound, redirect } from 'next/navigation';
|
||||||
import ProductEngagementTracker from '@/components/analytics/ProductEngagementTracker';
|
import ProductEngagementTracker from '@/components/analytics/ProductEngagementTracker';
|
||||||
import PayloadRichText from '@/components/PayloadRichText';
|
import PayloadRichText from '@/components/PayloadRichText';
|
||||||
|
|
||||||
@@ -53,7 +53,7 @@ export async function generateMetadata({ params }: ProductPageProps): Promise<Me
|
|||||||
title: categoryTitle,
|
title: categoryTitle,
|
||||||
description: categoryDesc,
|
description: categoryDesc,
|
||||||
alternates: {
|
alternates: {
|
||||||
canonical: `${SITE_URL}/${locale}/${await mapFileSlugToTranslated('products', locale)}/${productSlug}`,
|
canonical: `${SITE_URL}/${locale}/${await mapFileSlugToTranslated('products', locale)}/${await mapFileSlugToTranslated(fileSlug, locale)}`,
|
||||||
languages: {
|
languages: {
|
||||||
de: `${SITE_URL}/de/${await mapFileSlugToTranslated('products', 'de')}/${await mapFileSlugToTranslated(fileSlug, 'de')}`,
|
de: `${SITE_URL}/de/${await mapFileSlugToTranslated('products', 'de')}/${await mapFileSlugToTranslated(fileSlug, 'de')}`,
|
||||||
en: `${SITE_URL}/en/${await mapFileSlugToTranslated('products', 'en')}/${await mapFileSlugToTranslated(fileSlug, 'en')}`,
|
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);
|
const product = await getProductBySlug(productSlug, locale);
|
||||||
if (!product) return {};
|
if (!product) return {};
|
||||||
|
|
||||||
|
const currentLocalePath = await getLocalizedPath(locale);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title: product.frontmatter.title,
|
title: product.frontmatter.title,
|
||||||
description: product.frontmatter.description,
|
description: product.frontmatter.description,
|
||||||
alternates: {
|
alternates: {
|
||||||
canonical: `${SITE_URL}/${locale}/${await mapFileSlugToTranslated('products', locale)}/${slug.join('/')}`,
|
canonical: `${SITE_URL}/${locale}/${currentLocalePath}`,
|
||||||
languages: {
|
languages: {
|
||||||
de: `${SITE_URL}/de/${await getLocalizedPath('de')}`,
|
de: `${SITE_URL}/de/${await getLocalizedPath('de')}`,
|
||||||
en: `${SITE_URL}/en/${await getLocalizedPath('en')}`,
|
en: `${SITE_URL}/en/${await getLocalizedPath('en')}`,
|
||||||
@@ -90,7 +92,7 @@ export async function generateMetadata({ params }: ProductPageProps): Promise<Me
|
|||||||
title: product.frontmatter.title,
|
title: product.frontmatter.title,
|
||||||
description: product.frontmatter.description,
|
description: product.frontmatter.description,
|
||||||
type: 'website',
|
type: 'website',
|
||||||
url: `${SITE_URL}/${locale}/products/${slug.join('/')}`,
|
url: `${SITE_URL}/${locale}/${currentLocalePath}`,
|
||||||
images: getProductOGImageMetadata(productSlug, product.frontmatter.title, locale),
|
images: getProductOGImageMetadata(productSlug, product.frontmatter.title, locale),
|
||||||
},
|
},
|
||||||
twitter: {
|
twitter: {
|
||||||
@@ -114,7 +116,19 @@ export default async function ProductPage({ params }: ProductPageProps) {
|
|||||||
'high-voltage-cables',
|
'high-voltage-cables',
|
||||||
'solar-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)) {
|
if (categories.includes(fileSlug)) {
|
||||||
const allProducts = await getAllProducts(locale);
|
const allProducts = await getAllProducts(locale);
|
||||||
|
|||||||
26
components/analytics/ClientNotFoundTracker.tsx
Normal file
26
components/analytics/ClientNotFoundTracker.tsx
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
import { useAnalytics } from './useAnalytics';
|
||||||
|
import { AnalyticsEvents } from './analytics-events';
|
||||||
|
|
||||||
|
export default function ClientNotFoundTracker({ path }: { path: string }) {
|
||||||
|
const { trackEvent } = useAnalytics();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
trackEvent(AnalyticsEvents.ERROR, {
|
||||||
|
type: '404_not_found',
|
||||||
|
path,
|
||||||
|
});
|
||||||
|
|
||||||
|
import('@sentry/nextjs').then((Sentry) => {
|
||||||
|
Sentry.withScope((scope) => {
|
||||||
|
scope.setTag('status_code', '404');
|
||||||
|
scope.setTag('path', path);
|
||||||
|
Sentry.captureMessage(`Route Not Found: ${path}`, 'warning');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}, [trackEvent, path]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
35
lib/blog.ts
35
lib/blog.ts
@@ -59,7 +59,8 @@ export async function getPostBySlug(slug: string, locale: string): Promise<PostD
|
|||||||
try {
|
try {
|
||||||
const payload = await getPayload({ config: configPromise });
|
const payload = await getPayload({ config: configPromise });
|
||||||
|
|
||||||
const { docs } = await payload.find({
|
// First try: Find in the requested locale
|
||||||
|
let { docs } = await payload.find({
|
||||||
collection: 'posts',
|
collection: 'posts',
|
||||||
where: {
|
where: {
|
||||||
slug: { equals: slug },
|
slug: { equals: slug },
|
||||||
@@ -70,6 +71,38 @@ export async function getPostBySlug(slug: string, locale: string): Promise<PostD
|
|||||||
limit: 1,
|
limit: 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Fallback: If not found, try searching across all locales.
|
||||||
|
// This happens when a user uses the static language switcher
|
||||||
|
// e.g. switching from /en/blog/en-slug to /de/blog/en-slug.
|
||||||
|
if (!docs || docs.length === 0) {
|
||||||
|
const { docs: crossLocaleDocs } = await payload.find({
|
||||||
|
collection: 'posts',
|
||||||
|
where: {
|
||||||
|
slug: { equals: slug },
|
||||||
|
...(!config.showDrafts ? { _status: { equals: 'published' } } : {}),
|
||||||
|
},
|
||||||
|
locale: 'all',
|
||||||
|
draft: config.showDrafts,
|
||||||
|
limit: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (crossLocaleDocs && crossLocaleDocs.length > 0) {
|
||||||
|
// Fetch the found document again, but strictly in the requested locale
|
||||||
|
// so we get the correctly translated fields (like the localized slug)
|
||||||
|
const { docs: correctLocaleDocs } = await payload.find({
|
||||||
|
collection: 'posts',
|
||||||
|
where: {
|
||||||
|
id: { equals: crossLocaleDocs[0].id },
|
||||||
|
},
|
||||||
|
locale: locale as any,
|
||||||
|
draft: config.showDrafts,
|
||||||
|
limit: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
docs = correctLocaleDocs;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!docs || docs.length === 0) return null;
|
if (!docs || docs.length === 0) return null;
|
||||||
|
|
||||||
const doc = docs[0];
|
const doc = docs[0];
|
||||||
|
|||||||
76
lib/pages.ts
76
lib/pages.ts
@@ -1,5 +1,7 @@
|
|||||||
import { getPayload } from 'payload';
|
import { getPayload } from 'payload';
|
||||||
import configPromise from '@payload-config';
|
import configPromise from '@payload-config';
|
||||||
|
import { mapSlugToFileSlug } from './slugs';
|
||||||
|
import { config } from '@/lib/config';
|
||||||
|
|
||||||
export interface PageFrontmatter {
|
export interface PageFrontmatter {
|
||||||
title: string;
|
title: string;
|
||||||
@@ -44,19 +46,81 @@ function mapDoc(doc: any): PageData {
|
|||||||
export async function getPageBySlug(slug: string, locale: string): Promise<PageData | null> {
|
export async function getPageBySlug(slug: string, locale: string): Promise<PageData | null> {
|
||||||
try {
|
try {
|
||||||
const payload = await getPayload({ config: configPromise });
|
const payload = await getPayload({ config: configPromise });
|
||||||
|
const fileSlug = await mapSlugToFileSlug(slug, locale);
|
||||||
|
|
||||||
const result = await payload.find({
|
// Try finding exact match first
|
||||||
collection: 'pages' as any,
|
let result = await payload.find({
|
||||||
|
collection: 'pages',
|
||||||
where: {
|
where: {
|
||||||
slug: { equals: slug },
|
and: [
|
||||||
|
{ slug: { equals: fileSlug } },
|
||||||
|
...(!config.showDrafts ? [{ _status: { equals: 'published' } }] : []),
|
||||||
|
],
|
||||||
},
|
},
|
||||||
locale: locale as any,
|
locale: locale as any,
|
||||||
|
depth: 1,
|
||||||
limit: 1,
|
limit: 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
const docs = result.docs as any[];
|
// Fallback: search ALL locales
|
||||||
if (!docs || docs.length === 0) return null;
|
if (result.docs.length === 0) {
|
||||||
return mapDoc(docs[0]);
|
const crossResult = await payload.find({
|
||||||
|
collection: 'pages',
|
||||||
|
where: {
|
||||||
|
and: [
|
||||||
|
{ slug: { equals: fileSlug } },
|
||||||
|
...(!config.showDrafts ? [{ _status: { equals: 'published' } }] : []),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
locale: 'all',
|
||||||
|
depth: 1,
|
||||||
|
limit: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (crossResult.docs.length > 0) {
|
||||||
|
// Fetch missing exact match by internal id
|
||||||
|
result = await payload.find({
|
||||||
|
collection: 'pages',
|
||||||
|
where: {
|
||||||
|
id: { equals: crossResult.docs[0].id },
|
||||||
|
},
|
||||||
|
locale: locale as any,
|
||||||
|
depth: 1,
|
||||||
|
limit: 1,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.docs.length > 0) {
|
||||||
|
const doc = result.docs[0];
|
||||||
|
|
||||||
|
return {
|
||||||
|
slug: doc.slug,
|
||||||
|
frontmatter: {
|
||||||
|
title: doc.title,
|
||||||
|
excerpt: doc.excerpt || '',
|
||||||
|
featuredImage:
|
||||||
|
typeof doc.featuredImage === 'object' && doc.featuredImage !== null
|
||||||
|
? doc.featuredImage.sizes?.card?.url || doc.featuredImage.url
|
||||||
|
: null,
|
||||||
|
focalX:
|
||||||
|
typeof doc.featuredImage === 'object' && doc.featuredImage !== null
|
||||||
|
? doc.featuredImage.focalX
|
||||||
|
: 50,
|
||||||
|
focalY:
|
||||||
|
typeof doc.featuredImage === 'object' && doc.featuredImage !== null
|
||||||
|
? doc.featuredImage.focalY
|
||||||
|
: 50,
|
||||||
|
layout:
|
||||||
|
doc.layout === 'fullBleed' || doc.layout === 'default'
|
||||||
|
? doc.layout
|
||||||
|
: ('default' as const),
|
||||||
|
},
|
||||||
|
content: doc.content,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`[Payload] getPageBySlug failed for ${slug}:`, error);
|
console.error(`[Payload] getPageBySlug failed for ${slug}:`, error);
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
Reference in New Issue
Block a user