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,17 +0,0 @@
import configPromise from '@payload-config';
import { RootPage } from '@payloadcms/next/views';
import { importMap } from '../importMap';
type Args = {
params: Promise<{
segments: string[];
}>;
searchParams: Promise<{
[key: string]: string | string[];
}>;
};
const Page = ({ params, searchParams }: Args) =>
RootPage({ config: configPromise, importMap, params, searchParams });
export default Page;

View File

@@ -1,57 +0,0 @@
import { RscEntryLexicalCell as RscEntryLexicalCell_44fe37237e0ebf4470c9990d8cb7b07e } from '@payloadcms/richtext-lexical/rsc'
import { RscEntryLexicalField as RscEntryLexicalField_44fe37237e0ebf4470c9990d8cb7b07e } from '@payloadcms/richtext-lexical/rsc'
import { LexicalDiffComponent as LexicalDiffComponent_44fe37237e0ebf4470c9990d8cb7b07e } from '@payloadcms/richtext-lexical/rsc'
import { BlocksFeatureClient as BlocksFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { InlineToolbarFeatureClient as InlineToolbarFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { HorizontalRuleFeatureClient as HorizontalRuleFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { UploadFeatureClient as UploadFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { BlockquoteFeatureClient as BlockquoteFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { RelationshipFeatureClient as RelationshipFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { LinkFeatureClient as LinkFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { ChecklistFeatureClient as ChecklistFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { OrderedListFeatureClient as OrderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { UnorderedListFeatureClient as UnorderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { IndentFeatureClient as IndentFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { AlignFeatureClient as AlignFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { HeadingFeatureClient as HeadingFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { ParagraphFeatureClient as ParagraphFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { InlineCodeFeatureClient as InlineCodeFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { SuperscriptFeatureClient as SuperscriptFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { SubscriptFeatureClient as SubscriptFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { StrikethroughFeatureClient as StrikethroughFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { UnderlineFeatureClient as UnderlineFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { BoldFeatureClient as BoldFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { ItalicFeatureClient as ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { default as default_9ed509b5e5f7d08a16335393f27586cc } from '../../../src/payload/components/Icon'
import { default as default_5470ea90f7a8fd882c2fe59ff2b1c5b9 } from '../../../src/payload/components/Logo'
import { CollectionCards as CollectionCards_f9c02e79a4aed9a3924487c0cd4cafb1 } from '@payloadcms/next/rsc'
export const importMap = {
"@payloadcms/richtext-lexical/rsc#RscEntryLexicalCell": RscEntryLexicalCell_44fe37237e0ebf4470c9990d8cb7b07e,
"@payloadcms/richtext-lexical/rsc#RscEntryLexicalField": RscEntryLexicalField_44fe37237e0ebf4470c9990d8cb7b07e,
"@payloadcms/richtext-lexical/rsc#LexicalDiffComponent": LexicalDiffComponent_44fe37237e0ebf4470c9990d8cb7b07e,
"@payloadcms/richtext-lexical/client#BlocksFeatureClient": BlocksFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#InlineToolbarFeatureClient": InlineToolbarFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#HorizontalRuleFeatureClient": HorizontalRuleFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#UploadFeatureClient": UploadFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#BlockquoteFeatureClient": BlockquoteFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#RelationshipFeatureClient": RelationshipFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#LinkFeatureClient": LinkFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#ChecklistFeatureClient": ChecklistFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#OrderedListFeatureClient": OrderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#UnorderedListFeatureClient": UnorderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#IndentFeatureClient": IndentFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#AlignFeatureClient": AlignFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#HeadingFeatureClient": HeadingFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#ParagraphFeatureClient": ParagraphFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#InlineCodeFeatureClient": InlineCodeFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#SuperscriptFeatureClient": SuperscriptFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#SubscriptFeatureClient": SubscriptFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#StrikethroughFeatureClient": StrikethroughFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#UnderlineFeatureClient": UnderlineFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#BoldFeatureClient": BoldFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#ItalicFeatureClient": ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"/src/payload/components/Icon#default": default_9ed509b5e5f7d08a16335393f27586cc,
"/src/payload/components/Logo#default": default_5470ea90f7a8fd882c2fe59ff2b1c5b9,
"@payloadcms/next/rsc#CollectionCards": CollectionCards_f9c02e79a4aed9a3924487c0cd4cafb1
}

View File

@@ -1,14 +0,0 @@
import config from '@payload-config';
import {
REST_GET,
REST_OPTIONS,
REST_PATCH,
REST_POST,
REST_DELETE,
} from '@payloadcms/next/routes';
export const GET = REST_GET(config);
export const POST = REST_POST(config);
export const DELETE = REST_DELETE(config);
export const PATCH = REST_PATCH(config);
export const OPTIONS = REST_OPTIONS(config);

View File

@@ -1,4 +0,0 @@
import config from '@payload-config';
import { GRAPHQL_POST } from '@payloadcms/next/routes';
export const POST = GRAPHQL_POST(config);

View File

@@ -1,151 +0,0 @@
/* =================================================================
KLZ Cables Payload Admin Theme
Strictly follows docs/STYLEGUIDE.md & tailwind.config.cjs
IMPORTANT: We use `html` selector (not `:root`) because Payload's
own CSS defines variables on `:root` and loads AFTER this file.
`html` has higher specificity than `:root`, so our values win.
================================================================= */
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap');
/* =================================================================
COLOR OVERRIDES
Payload internally maps:
--theme-elevation-* → --color-base-*
--theme-success-* → --color-success-*
We override the SOURCE variables on `html` to beat Payload's `:root`.
================================================================= */
html {
/* ---------------------------------------------------------------
KLZ Primary Blue (#011dff) → Buttons, links, active states
--------------------------------------------------------------- */
--color-success-50: #eef0ff !important;
--color-success-100: #dfe2ff !important;
--color-success-150: #cdd2ff !important;
--color-success-200: #b8bfff !important;
--color-success-250: #a0a9ff !important;
--color-success-300: #8892ff !important;
--color-success-350: #707bff !important;
--color-success-400: #5564ff !important;
--color-success-450: #3a4dff !important;
--color-success-500: #011dff !important;
/* KLZ Primary */
--color-success-550: #0119e6 !important;
--color-success-600: #0116cc !important;
--color-success-650: #0112b3 !important;
--color-success-700: #000e99 !important;
--color-success-750: #000b80 !important;
--color-success-800: #000866 !important;
--color-success-850: #00054d !important;
--color-success-900: #000333 !important;
--color-success-950: #00011a !important;
/* ---------------------------------------------------------------
KLZ "Foundation Neutrals" → Backgrounds, cards, borders, text
Based on tailwind.config.cjs: neutral.light=#fff,
neutral.DEFAULT=#f8f9fa, neutral.dark=#263336, neutral.black=#0a0a0a
text.primary=#1a1a1a, text.secondary=#6c757d, text.light=#adb5bd
--------------------------------------------------------------- */
--color-base-0: #ffffff !important;
--color-base-50: #f8f9fa !important;
--color-base-100: #f1f3f5 !important;
--color-base-150: #e9ecef !important;
--color-base-200: #dee2e6 !important;
--color-base-250: #ced4da !important;
--color-base-300: #adb5bd !important;
--color-base-350: #9ba3ab !important;
--color-base-400: #868e96 !important;
--color-base-450: #6c757d !important;
--color-base-500: #5c636a !important;
--color-base-550: #4d5358 !important;
--color-base-600: #3d4246 !important;
--color-base-650: #343a40 !important;
--color-base-700: #2b3035 !important;
--color-base-750: #263336 !important;
--color-base-800: #212529 !important;
--color-base-850: #1a1a1a !important;
--color-base-900: #121212 !important;
--color-base-950: #0a0a0a !important;
--color-base-1000: #000000 !important;
/* Typography */
--font-body: 'Inter', system-ui, -apple-system, sans-serif !important;
--font-headings: 'Inter', system-ui, -apple-system, sans-serif !important;
}
/* Base Body Application */
body {
font-family: var(--font-body) !important;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
/* =================================================================
Login / Setup Page
================================================================= */
.template-default.template-default--has-bg {
background: radial-gradient(circle at top right, #e6ebf5 0%, #f8f9fa 60%, #f3f4f6 100%) !important;
}
.login__wrap,
.create-first-user__wrap {
border-top: none !important;
padding: 3rem !important;
background: rgba(255, 255, 255, 0.85) !important;
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border: 1px solid var(--theme-elevation-150) !important;
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.15) !important;
border-radius: 1.5rem !important;
}
/* =================================================================
Buttons override Payload's dark buttons with KLZ Blue
Payload uses .btn--style-primary { --bg-color: var(--theme-elevation-800) }
which makes all primary buttons near-black. We override to KLZ Blue.
================================================================= */
.btn--style-primary,
.btn--style-pill {
--bg-color: #011dff !important;
--color: #ffffff !important;
--hover-bg: #0116cc !important;
--hover-color: #ffffff !important;
}
.btn--style-primary.btn--disabled,
.btn--style-pill.btn--disabled {
--bg-color: #b8bfff !important;
--color: #ffffff !important;
--hover-bg: #b8bfff !important;
}
/* Sidebar Active Items */
[class*="nav-group__link--active"],
[class*="nav__link--active"] {
--theme-elevation-800: #011dff !important;
color: #011dff !important;
border-left-color: #011dff !important;
}
.btn--style-secondary {
--box-shadow: inset 0 0 0 1px #011dff !important;
--color: #011dff !important;
--hover-color: #0116cc !important;
--hover-box-shadow: inset 0 0 0 1px #0116cc !important;
}
/* =================================================================
Logo & Icon
================================================================= */
.klz-admin-logo,
.klz-admin-icon {
display: block !important;
visibility: visible !important;
opacity: 1 !important;
height: 32px !important;
width: auto !important;
max-width: 100% !important;
object-fit: contain !important;
}

View File

@@ -1,31 +0,0 @@
import configPromise from '@payload-config';
import { RootLayout } from '@payloadcms/next/layouts';
import React from 'react';
import '@payloadcms/next/css';
import './custom.scss';
import { handleServerFunctions } from '@payloadcms/next/layouts';
import { importMap } from './admin/importMap';
type Args = {
children: React.ReactNode;
};
const serverFunction: any = async function (args: any) {
'use server';
return handleServerFunctions({
...args,
config: configPromise,
importMap,
});
};
const Layout = ({ children }: Args) => {
return (
<RootLayout config={configPromise} importMap={importMap} serverFunction={serverFunction}>
{children}
</RootLayout>
);
};
export default Layout;

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>
);
}

View File

@@ -41,28 +41,7 @@ export async function requestBrochureAction(formData: FormData) {
return { success: false, error: 'Invalid email address' };
}
// 1. Save to CMS
try {
const { getPayload } = await import('payload');
const configPromise = (await import('@payload-config')).default;
const payload = await getPayload({ config: configPromise });
await payload.create({
collection: 'form-submissions',
data: {
name: email.split('@')[0],
email,
message: `Brochure download request (${locale})`,
type: 'brochure_download' as any,
},
overrideAccess: true,
});
logger.info('Successfully saved brochure request to Payload CMS', { email });
} catch (error) {
logger.error('Failed to store brochure request in Payload CMS', { error });
services.errors.captureException(error, { action: 'payload_store_brochure_request' });
}
// 1. Save to CMS - Removed along with Payload CMS
// 2. Notify via Gotify
try {

View File

@@ -47,32 +47,7 @@ export async function sendContactFormAction(formData: FormData) {
return { success: false, error: 'Missing required fields' };
}
// 1. Save to CMS
try {
const { getPayload } = await import('payload');
const configPromise = (await import('@payload-config')).default;
const payload = await getPayload({ config: configPromise });
await payload.create({
collection: 'form-submissions',
data: {
name,
email,
message,
type: productName ? 'product_quote' : 'contact',
productName: productName || undefined,
},
overrideAccess: true,
});
logger.info('Successfully saved form submission to Payload CMS', {
type: productName ? 'product_quote' : 'contact',
email,
});
} catch (error) {
logger.error('Failed to store submission in Payload CMS', { error });
services.errors.captureException(error, { action: 'payload_store_submission' });
}
// 1. Save to CMS - Removed along with Payload CMS
// 2. Send Emails
logger.info('Sending branded emails', { email, productName });

View File

@@ -1,41 +0,0 @@
import { NextResponse } from 'next/server';
import { getPayload } from 'payload';
import configPromise from '@payload-config';
export const dynamic = 'force-dynamic';
/**
* Deep CMS Health Check
* Validates that Payload CMS can actually query the database.
* Used by post-deploy smoke tests to catch migration/schema issues.
*/
export async function GET() {
const checks: Record<string, string> = {};
try {
const payload = await getPayload({ config: configPromise });
checks.init = 'ok';
// Verify each collection can be queried (catches missing locale tables, broken migrations)
const collections = ['posts', 'pages', 'media', 'competences'] as const;
for (const collection of collections) {
try {
await payload.find({ collection, limit: 1 });
checks[collection] = 'ok';
} catch (e: any) {
checks[collection] = `error: ${e.message?.substring(0, 100)}`;
}
}
const hasErrors = Object.values(checks).some((v) => v.startsWith('error'));
return NextResponse.json(
{ status: hasErrors ? 'degraded' : 'ok', checks },
{ status: hasErrors ? 503 : 200 },
);
} catch (e: any) {
return NextResponse.json(
{ status: 'error', message: e.message?.substring(0, 200), checks },
{ status: 503 },
);
}
}