feat: complete MDX migration, stabilize environment & verify via E2E tests

Former-commit-id: ec3e64156a2e182535cbdcf0d975cd54603a517d
This commit is contained in:
2026-05-03 18:46:41 +02:00
parent e57661a586
commit b13f628b55
131 changed files with 1388 additions and 25288 deletions

View File

@@ -1,20 +0,0 @@
export default function DatenschutzPage() {
return (
<div className="pt-32 pb-24">
<div className="container max-w-3xl">
<h1 className="text-5xl font-heading font-extrabold text-neutral-dark mb-8">Datenschutzerklärung</h1>
<div className="prose prose-neutral max-w-none">
<h3>1. Datenschutz auf einen Blick</h3>
<p>
Die folgenden Hinweise geben einen einfachen Überblick darüber, was mit Ihren personenbezogenen Daten passiert, wenn Sie diese Website besuchen.
Die vollständige Datenschutzerklärung der E-TIB GmbH wird aktuell überarbeitet und an dieser Stelle zur Verfügung gestellt.
</p>
<h3>2. Allgemeine Hinweise und Pflichtinformationen</h3>
<p>
Die Betreiber dieser Seiten nehmen den Schutz Ihrer persönlichen Daten sehr ernst. Wir behandeln Ihre personenbezogenen Daten vertraulich und entsprechend der gesetzlichen Datenschutzvorschriften sowie dieser Datenschutzerklärung.
</p>
</div>
</div>
</div>
);
}

View File

@@ -1,37 +0,0 @@
export default function ImpressumPage() {
return (
<div className="pt-32 pb-24">
<div className="container max-w-3xl">
<h1 className="text-5xl font-heading font-extrabold text-neutral-dark mb-8">Impressum</h1>
<div className="prose prose-neutral max-w-none">
<p><strong>Angaben gemäß § 5 TMG</strong></p>
<p>
E-TIB GmbH<br />
Gewerbestraße 22<br />
03172 Guben
</p>
<p>
<strong>Vertreten durch:</strong><br />
Geschäftsführer: Danny Joseph
</p>
<p>
<strong>Kontakt:</strong><br />
Telefon: +49 15207230518<br />
E-Mail: d.joseph@e-tib.com
</p>
<p>
<strong>Registereintrag:</strong><br />
Eintragung im Handelsregister.<br />
Registergericht: Amtsgericht Cottbus<br />
Registernummer: HRB 12403 CB
</p>
<p>
<strong>Umsatzsteuer-ID:</strong><br />
Umsatzsteuer-Identifikationsnummer gemäß § 27 a Umsatzsteuergesetz:<br />
DE304799919
</p>
</div>
</div>
</div>
);
}

View File

@@ -4,10 +4,23 @@ import { getTranslations, setRequestLocale } from 'next-intl/server';
import { Metadata } from 'next';
import { getPageBySlug } from '@/lib/pages';
import { mapSlugToFileSlug, mapFileSlugToTranslated } from '@/lib/slugs';
import PayloadRichText from '@/components/PayloadRichText';
import { MDXRemote } from 'next-mdx-remote/rsc';
import { SITE_URL } from '@/lib/schema';
import TrackedLink from '@/components/analytics/TrackedLink';
// Import components used in MDX
import { HeroVideo as HomeHero } from '@/components/blocks/HeroVideo';
import { SubCompanyTiles as HomeSubCompanyTiles } from '@/components/blocks/SubCompanyTiles';
import { CompetenceBentoGrid as HomeCompetenceBentoGrid } from '@/components/blocks/CompetenceBentoGrid';
import { ReferencesSlider as HomeReferencesSlider } from '@/components/blocks/ReferencesSlider';
const mdxComponents = {
HomeHero,
HomeSubCompanyTiles,
HomeCompetenceBentoGrid,
HomeReferencesSlider,
};
interface PageProps {
params: Promise<{
locale: string;
@@ -72,18 +85,20 @@ export default async function Page(props: { params: Promise<{ locale: string; sl
notFound();
}
// Redirect if accessed via a different locale's slug
// Redirect logic removed to simplify for now, as slugs match file names
/*
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 (
<div className="flex flex-col min-h-screen">
<PayloadRichText data={pageData.content} className="" />
<MDXRemote source={pageData.content} components={mdxComponents} />
</div>
);
}
@@ -120,9 +135,9 @@ export default async function Page(props: { params: Promise<{ locale: string; sl
</div>
)}
{/* Main content with shared blog components */}
{/* Main content */}
<div className="prose prose-lg md:prose-xl max-w-none prose-headings:font-bold prose-headings:text-text-primary prose-p:text-text-secondary prose-p:leading-relaxed prose-a:text-primary prose-a:no-underline hover:prose-a:underline prose-img:rounded-2xl prose-img:shadow-2xl prose-blockquote:border-primary prose-blockquote:bg-primary/5 prose-blockquote:rounded-r-2xl prose-strong:text-primary">
<PayloadRichText data={pageData.content} />
<MDXRemote source={pageData.content} components={mdxComponents} />
</div>
{/* Support Section */}

View File

@@ -5,7 +5,7 @@ import {
getPostBySlug,
getAdjacentPosts,
getReadingTime,
extractLexicalHeadings,
getHeadings,
} from '@/lib/blog';
import { Metadata } from 'next';
import Link from 'next/link';
@@ -16,9 +16,7 @@ import TableOfContents from '@/components/blog/TableOfContents';
import { Heading } from '@/components/ui';
import { setRequestLocale } from 'next-intl/server';
import BlogEngagementTracker from '@/components/analytics/BlogEngagementTracker';
// Payload CMS Imports
import PayloadRichText from '@/components/PayloadRichText';
import { MDXRemote } from 'next-mdx-remote/rsc';
interface BlogPostProps {
params: Promise<{
@@ -65,20 +63,9 @@ export default async function BlogPost({ params }: BlogPostProps) {
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
// Extract headings for TOC
const headings = extractLexicalHeadings(post.content?.root || post.content);
// Convert Lexical content into a plain string to estimate reading time roughly
const rawTextContent = JSON.stringify(post.content);
const headings = getHeadings(post.content);
const readingTime = getReadingTime(post.content);
return (
<article className="bg-white min-h-screen font-sans selection:bg-primary/10 selection:text-primary">
@@ -86,7 +73,7 @@ export default async function BlogPost({ params }: BlogPostProps) {
title={post.frontmatter.title}
slug={slug}
category={post.frontmatter.category}
readingTime={getReadingTime(rawTextContent)}
readingTime={readingTime}
/>
{/* Featured Image Header */}
@@ -131,7 +118,7 @@ export default async function BlogPost({ params }: BlogPostProps) {
})}
</time>
<span className="w-1 h-1 bg-white/30 rounded-full" />
<span>{getReadingTime(rawTextContent)} min read</span>
<span>{readingTime} min read</span>
{(new Date(post.frontmatter.date) > new Date() ||
post.frontmatter.public === false) && (
<>
@@ -168,7 +155,7 @@ export default async function BlogPost({ params }: BlogPostProps) {
})}
</time>
<span className="w-1 h-1 bg-neutral-400 rounded-full" />
<span>{getReadingTime(rawTextContent)} min read</span>
<span>{readingTime} min read</span>
{(new Date(post.frontmatter.date) > new Date() ||
post.frontmatter.public === false) && (
<>
@@ -197,9 +184,9 @@ export default async function BlogPost({ params }: BlogPostProps) {
</div>
)}
{/* Main content with enhanced styling rendering Payload Lexical */}
{/* Main content */}
<div className="prose prose-lg md:prose-xl max-w-none prose-headings:font-bold prose-headings:text-text-primary prose-p:text-text-secondary prose-p:leading-relaxed prose-a:text-primary prose-a:no-underline hover:prose-a:underline prose-img:rounded-2xl prose-img:shadow-2xl prose-blockquote:border-primary prose-blockquote:bg-primary/5 prose-blockquote:rounded-r-2xl prose-strong:text-primary">
<PayloadRichText data={post.content} />
<MDXRemote source={post.content} />
</div>
{/* Power CTA */}
@@ -284,8 +271,8 @@ export default async function BlogPost({ params }: BlogPostProps) {
'@id': `${SITE_URL}/${locale}/blog/${slug}`,
},
articleSection: post.frontmatter.category,
wordCount: rawTextContent.split(/\s+/).length,
timeRequired: `PT${getReadingTime(rawTextContent)}M`,
wordCount: post.content.split(/\s+/).length,
timeRequired: `PT${readingTime}M`,
} as any
}
/>

View File

@@ -62,7 +62,8 @@ export default async function ContactPage({ params }: ContactPageProps) {
setRequestLocale(locale);
const t = await getTranslations({ locale, namespace: 'Contact' });
// Get translated slug to redirect if user used incorrect static slug
// Get translated slug - redirect logic removed to prevent loop with next.config.js rewrites
/*
const { headers } = await import('next/headers');
const headersList = await headers();
const urlPath = headersList.get('x-invoke-path') || '';
@@ -75,6 +76,7 @@ export default async function ContactPage({ params }: ContactPageProps) {
redirect(`/${locale}/${contactSlugDe}`);
}
}
*/
return (
<div className="flex flex-col min-h-screen bg-neutral-light">

View File

@@ -2,7 +2,6 @@ import { Footer } from '@/components/layout/Footer';
import { Header } from '@/components/layout/Header';
import JsonLd from '@/components/JsonLd';
import SkipLink from '@/components/SkipLink';
import CMSConnectivityNotice from '@/components/CMSConnectivityNotice';
import AnalyticsShell from '@/components/analytics/AnalyticsShell';
import { Metadata, Viewport } from 'next';
import { NextIntlClientProvider } from 'next-intl';
@@ -12,8 +11,7 @@ import { SITE_URL } from '@/lib/schema';
import FeedbackClientWrapper from '@/components/FeedbackClientWrapper';
import { setRequestLocale } from 'next-intl/server';
import { Inter } from 'next/font/google';
import { getPayload } from 'payload';
import configPromise from '@payload-config';
import { mapFileSlugToTranslated } from '@/lib/slugs';
const inter = Inter({
subsets: ['latin'],
@@ -76,36 +74,34 @@ export default async function Layout(props: {
const safeLocale = supportedLocales.includes(localeStr) ? localeStr : 'en';
setRequestLocale(safeLocale);
let messages: Record<string, any> = {};
try {
messages = await getMessages();
} catch {
} catch (err) {
console.error('[Layout] Failed to load messages:', err);
messages = {};
}
// Fetch navigation and company info from Payload Global Settings
const payload = await getPayload({ config: configPromise });
const settings = await payload.findGlobal({
slug: 'settings',
locale: safeLocale as any,
}).catch(() => null);
const navLinks = (settings?.mainNav && settings.mainNav.length > 0)
? settings.mainNav.map((n) => ({
label: n.label,
url: n.url,
}))
: [
{ label: 'Kompetenzen', url: `/${safeLocale}/kompetenzen` },
{ label: 'Über uns', url: `/${safeLocale}/ueber-uns` },
{ label: 'Karriere & Messen', url: `/${safeLocale}/karriere` }
];
const navLinks = [
{
label: safeLocale === 'de' ? 'Kompetenzen' : 'Competencies',
url: `/${safeLocale}/${await mapFileSlugToTranslated('kompetenzen', safeLocale)}`
},
{
label: safeLocale === 'de' ? 'Über uns' : 'About Us',
url: `/${safeLocale}/${await mapFileSlugToTranslated('ueber-uns', safeLocale)}`
},
{
label: safeLocale === 'de' ? 'Karriere & Messen' : 'Careers & Events',
url: `/${safeLocale}/${await mapFileSlugToTranslated('karriere', safeLocale)}`
}
];
const companyInfo = {
contactEmail: settings?.contactEmail || 'info@e-tib.com',
contactPhone: settings?.contactPhone || '+49 15207230518',
address: settings?.address || 'Gewerbestraße 22\\n03172 Guben'
contactEmail: 'info@e-tib.com',
contactPhone: '+49 15207230518',
address: 'Gewerbestraße 22\n03172 Guben'
};
// Pick only the namespaces required by client components to reduce the hydration payload size
@@ -185,7 +181,6 @@ export default async function Layout(props: {
<Footer companyInfo={companyInfo} />
<JsonLd />
<CMSConnectivityNotice />
<AnalyticsShell />
{feedbackEnabled && <FeedbackClientWrapper feedbackEnabled={feedbackEnabled} />}
</NextIntlClientProvider>

View File

@@ -1,87 +1,19 @@
import { getTranslations } from 'next-intl/server';
import { Container, Button, Heading } from '@/components/ui';
import Scribble from '@/components/Scribble';
import { getPayload } from 'payload';
import configPromise from '@payload-config';
import { headers } from 'next/headers';
import ClientNotFoundTracker from '@/components/analytics/ClientNotFoundTracker';
export default async function NotFound() {
console.log('--- NOT FOUND RENDERED ---');
const t = await getTranslations('Error.notFound');
// Try to determine the requested path
const headersList = await headers();
const urlPath = headersList.get('x-invoke-path') || '';
// Suggested URL logic removed along with Payload CMS
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 competences
const competenceRes =
postRes.docs.length === 0
? await payload.find({
collection: 'competences',
where: { slug: { equals: slug } },
locale: 'all',
limit: 1,
})
: { docs: [] };
// Check pages
const pageRes =
postRes.docs.length === 0 && competenceRes.docs.length === 0
? await payload.find({
collection: 'pages',
where: { slug: { equals: slug } },
locale: 'all',
limit: 1,
})
: { docs: [] };
const anyDoc = postRes.docs[0] || competenceRes.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 {
// Ignore Payload errors in 404
}
}
}
return (
<>
<ClientNotFoundTracker path={urlPath} />
<ClientNotFoundTracker path="" />
<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">

View File

@@ -1,80 +1,52 @@
import { Metadata } from 'next';
import { setRequestLocale } from 'next-intl/server';
import { notFound } from 'next/navigation';
import { getPayload } from 'payload';
import configPromise from '@payload-config';
import PayloadRichText from '@/components/PayloadRichText';
import { getMdxContent } from '@/lib/mdx';
import { MDXRemote } from 'next-mdx-remote/rsc';
// Import components used in MDX
import { HeroVideo as HomeHero } from '@/components/blocks/HeroVideo';
import { SubCompanyTiles as HomeSubCompanyTiles } from '@/components/blocks/SubCompanyTiles';
import { CompetenceBentoGrid as HomeCompetenceBentoGrid } from '@/components/blocks/CompetenceBentoGrid';
import { ReferencesSlider as HomeReferencesSlider } from '@/components/blocks/ReferencesSlider';
const mdxComponents = {
HomeHero,
HomeSubCompanyTiles,
HomeCompetenceBentoGrid,
HomeReferencesSlider,
};
export async function generateMetadata(props: any): Promise<Metadata> {
const { locale } = await props.params;
if (locale !== 'de' && locale !== 'en') return {};
const mdx = await getMdxContent(locale, 'home');
return {
title: locale === 'de' ? 'Startseite' : 'Home',
title: mdx?.frontmatter?.title || (locale === 'de' ? 'Startseite' : 'Home'),
description: mdx?.frontmatter?.description,
};
}
export default async function Home(props: any) {
const { locale } = await props.params;
// Guard against invalid locales (e.g. favicon.ico) hitting the DB
if (locale !== 'de' && locale !== 'en') {
notFound();
}
setRequestLocale(locale);
const payload = await getPayload({ config: configPromise });
// 1. Fetch references for the ReferencesSlider (passed globally to PayloadRichText)
const { docs: references } = await payload.find({
collection: 'references',
locale: locale as any,
limit: 10,
});
const mdx = await getMdxContent(locale, 'home');
const mappedReferences = references.map((ref: any) => ({
id: ref.id,
title: ref.title,
slug: ref.slug,
category: ref.category,
image: ref.image,
}));
// 2. Fetch the Home page content
const { docs: pages } = await payload.find({
collection: 'pages',
where: {
slug: {
equals: 'home',
},
},
locale: locale as any,
limit: 1,
draft: true, // Allow finding drafts in development
});
const page = pages[0];
// Fallback to static components if page doesn't exist in CMS yet
// This prevents the site from breaking before the first seed/save
if (!page) {
console.warn(`[Home] Page slug "home" not found in CMS for locale "${locale}".`);
const { totalDocs: allPages } = await payload.find({ collection: 'pages', locale: 'all' });
console.log(`[Home] Total pages in CMS (all locales): ${allPages}`);
return (
<main className="min-h-screen flex items-center justify-center bg-neutral-dark text-white p-10 text-center">
<div>
<h1 className="text-4xl font-bold mb-4">Willkommen bei E-TIB</h1>
<p className="text-xl text-white/60">Die CMS-Inhalte für diese Seite müssen noch angelegt werden.</p>
</div>
</main>
);
if (!mdx) {
notFound();
}
return (
<main>
<PayloadRichText data={page.content} references={mappedReferences} />
<MDXRemote source={mdx.content} components={mdxComponents} />
</main>
);
}