feat: migrate Payload CMS to MDX and harden static infrastructure
This commit is contained in:
@@ -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;
|
||||
@@ -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
|
||||
}
|
||||
@@ -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);
|
||||
@@ -1,4 +0,0 @@
|
||||
import config from '@payload-config';
|
||||
import { GRAPHQL_POST } from '@payloadcms/next/routes';
|
||||
|
||||
export const POST = GRAPHQL_POST(config);
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
@@ -4,7 +4,7 @@ import { getTranslations, setRequestLocale } from 'next-intl/server';
|
||||
import { Metadata } from 'next';
|
||||
import { getPageBySlug, getAllPages } from '@/lib/pages';
|
||||
import { mapSlugToFileSlug, mapFileSlugToTranslated } from '@/lib/slugs';
|
||||
import PayloadRichText from '@/components/PayloadRichText';
|
||||
import MDXContent from '@/components/MDXContent';
|
||||
import { SITE_URL } from '@/lib/schema';
|
||||
import TrackedLink from '@/components/analytics/TrackedLink';
|
||||
|
||||
@@ -82,7 +82,7 @@ export default async function StandardPage({ params }: PageProps) {
|
||||
if (pageData.frontmatter.layout === 'fullBleed') {
|
||||
return (
|
||||
<div className="flex flex-col min-h-screen">
|
||||
<PayloadRichText data={pageData.content} className="" />
|
||||
<MDXContent data={pageData.content} className="" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -121,7 +121,7 @@ export default async function StandardPage({ params }: PageProps) {
|
||||
|
||||
{/* Main content with shared blog components */}
|
||||
<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} />
|
||||
<MDXContent data={pageData.content} />
|
||||
</div>
|
||||
|
||||
{/* Support Section */}
|
||||
|
||||
@@ -19,7 +19,7 @@ import { setRequestLocale } from 'next-intl/server';
|
||||
import BlogEngagementTracker from '@/components/analytics/BlogEngagementTracker';
|
||||
|
||||
// Payload CMS Imports
|
||||
import PayloadRichText from '@/components/PayloadRichText';
|
||||
import MDXContent from '@/components/MDXContent';
|
||||
|
||||
interface BlogPostProps {
|
||||
params: Promise<{
|
||||
@@ -215,7 +215,7 @@ export default async function BlogPost({ params }: BlogPostProps) {
|
||||
|
||||
{/* Main content with enhanced styling rendering Payload Lexical */}
|
||||
<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} />
|
||||
<MDXContent data={post.content} />
|
||||
</div>
|
||||
|
||||
{/* Power CTA */}
|
||||
|
||||
@@ -2,7 +2,6 @@ import Footer from '@/components/Footer';
|
||||
import Header from '@/components/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';
|
||||
@@ -144,9 +143,6 @@ export default async function Layout(props: {
|
||||
{children}
|
||||
</main>
|
||||
<Footer />
|
||||
|
||||
<CMSConnectivityNotice />
|
||||
|
||||
<AnalyticsShell />
|
||||
<FeedbackClientWrapper feedbackEnabled={feedbackEnabled} />
|
||||
</NextIntlClientProvider>
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
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';
|
||||
|
||||
@@ -20,61 +18,19 @@ export default async function NotFound() {
|
||||
if (urlPath) {
|
||||
const slug = urlPath.split('/').filter(Boolean).pop();
|
||||
if (slug) {
|
||||
try {
|
||||
const payload = await getPayload({ config: configPromise });
|
||||
// Logic for alternative locale checking is tricky without DB.
|
||||
// We'll just provide a simple fallback if the user is in wrong locale
|
||||
const currentLocale = urlPath.startsWith('/en') ? 'en' : 'de';
|
||||
const alternativeLocale = currentLocale === 'de' ? 'en' : 'de';
|
||||
suggestedLang = alternativeLocale === 'de' ? 'Deutsch' : 'English';
|
||||
|
||||
// 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
|
||||
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('/');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
import { notFound, redirect } from 'next/navigation';
|
||||
import ProductEngagementTracker from '@/components/analytics/ProductEngagementTracker';
|
||||
import PayloadRichText from '@/components/PayloadRichText';
|
||||
import MDXContent from '@/components/MDXContent';
|
||||
|
||||
interface ProductPageProps {
|
||||
params: Promise<{
|
||||
@@ -266,15 +266,17 @@ export default async function ProductPage({ params }: ProductPageProps) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
// Extract technical data natively from the Lexical AST for Schema.org
|
||||
let technicalItems = [];
|
||||
if (product.content?.root?.children) {
|
||||
const productTabsBlock = product.content.root.children.find(
|
||||
(node: any) => node.type === 'block' && node.fields?.blockType === 'productTabs',
|
||||
);
|
||||
if (productTabsBlock && productTabsBlock.fields?.technicalItems) {
|
||||
technicalItems = productTabsBlock.fields.technicalItems;
|
||||
try {
|
||||
const tabMatch =
|
||||
typeof product.content === 'string' &&
|
||||
product.content.match(/<Block type="productTabs" data={({.*?})}\s*\/>/s);
|
||||
if (tabMatch && tabMatch[1]) {
|
||||
const data = JSON.parse(tabMatch[1]);
|
||||
technicalItems = data.technicalItems || [];
|
||||
}
|
||||
} catch (e) {
|
||||
// Ignore JSON parse errors for AST
|
||||
}
|
||||
|
||||
const datasheetPath = getDatasheetPath(productSlug, locale);
|
||||
@@ -288,56 +290,17 @@ export default async function ProductPage({ params }: ProductPageProps) {
|
||||
? t(`categories.${categoryKey}.title`)
|
||||
: categoryFileSlug;
|
||||
|
||||
// Split content into Description and Technical Data
|
||||
const rootChildren = product.content?.root?.children || [];
|
||||
const technicalBlocks = rootChildren.filter(
|
||||
(node: any) =>
|
||||
node.type === 'block' &&
|
||||
(node.fields?.blockType === 'productTabs' ||
|
||||
node.fields?.blockType === 'productTechnicalData'),
|
||||
);
|
||||
let descriptionChildren = rootChildren.filter(
|
||||
(node: any) =>
|
||||
!(
|
||||
node.type === 'block' &&
|
||||
(node.fields?.blockType === 'productTabs' ||
|
||||
node.fields?.blockType === 'productTechnicalData')
|
||||
),
|
||||
);
|
||||
let descriptionContent = typeof product.content === 'string' ? product.content : '';
|
||||
let technicalContent = '';
|
||||
|
||||
// If no standalone description nodes, extract from the productTabs block's embedded content
|
||||
if (descriptionChildren.length === 0) {
|
||||
const tabsBlock = rootChildren.find(
|
||||
(node: any) => node.type === 'block' && node.fields?.blockType === 'productTabs',
|
||||
);
|
||||
if (tabsBlock?.fields?.content?.root?.children) {
|
||||
descriptionChildren = tabsBlock.fields.content.root.children.filter((node: any) => {
|
||||
// Filter out MDX parsing artifacts like `}>`
|
||||
if (node.type === 'paragraph' && node.children?.length === 1) {
|
||||
const text = node.children[0]?.text?.trim();
|
||||
return text !== '}>' && text !== '{' && text !== '}';
|
||||
}
|
||||
return true;
|
||||
});
|
||||
if (typeof product.content === 'string') {
|
||||
const tabBlockMatch = product.content.match(/<Block type="productTabs".*?\/>/s);
|
||||
if (tabBlockMatch) {
|
||||
descriptionContent = product.content.replace(tabBlockMatch[0], '');
|
||||
technicalContent = tabBlockMatch[0];
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`[DEBUG PAGE] Slug: ${productSlug}, children count: ${descriptionChildren.length}`);
|
||||
|
||||
const descriptionContent = {
|
||||
root: {
|
||||
...product.content.root,
|
||||
children: descriptionChildren,
|
||||
},
|
||||
};
|
||||
|
||||
const technicalContent = {
|
||||
root: {
|
||||
...product.content.root,
|
||||
children: technicalBlocks,
|
||||
},
|
||||
};
|
||||
|
||||
const sidebar = (
|
||||
<ProductSidebar
|
||||
productName={product.frontmatter.title}
|
||||
@@ -461,17 +424,19 @@ export default async function ProductPage({ params }: ProductPageProps) {
|
||||
{/* Description Area Next to Sidebar */}
|
||||
<div className="lg:col-span-8">
|
||||
<div className="max-w-none prose prose-primary prose-base md:prose-lg xl:prose-xl mb-8 md:mb-16 pb-8 md:pb-16 border-b border-neutral-dark/5">
|
||||
{descriptionChildren.length > 0 ? (
|
||||
<PayloadRichText data={descriptionContent} />
|
||||
{descriptionContent ? (
|
||||
<MDXContent data={descriptionContent} />
|
||||
) : product.frontmatter.description ? (
|
||||
<p className="text-lg md:text-xl text-text-secondary leading-relaxed">
|
||||
{product.frontmatter.description}
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
{product.application?.root?.children?.length > 0 && (
|
||||
{product.application && (
|
||||
<div className="mt-12">
|
||||
<PayloadRichText data={product.application} />
|
||||
<MDXContent
|
||||
data={typeof product.application === 'string' ? product.application : ''}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -484,7 +449,7 @@ export default async function ProductPage({ params }: ProductPageProps) {
|
||||
{/* Full-width Technical Data Below */}
|
||||
<div className="mt-8 md:mt-16 pt-8 md:pt-16 border-t-0">
|
||||
<div className="max-w-none prose prose-primary prose-lg md:prose-xl">
|
||||
<PayloadRichText data={technicalContent} />
|
||||
<MDXContent data={technicalContent} />
|
||||
</div>
|
||||
|
||||
{/* Datasheet Download Section */}
|
||||
|
||||
@@ -40,32 +40,10 @@ 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' });
|
||||
}
|
||||
logger.info('Payload CMS saving skipped because it has been removed', {
|
||||
type: productName ? 'product_quote' : 'contact',
|
||||
email,
|
||||
});
|
||||
|
||||
// 2. Send Emails
|
||||
logger.info('Sending branded emails', { email, productName });
|
||||
@@ -107,10 +85,10 @@ export async function sendContactFormAction(formData: FormData) {
|
||||
});
|
||||
services.errors.captureException(
|
||||
new Error(`Notification email failed: ${notificationResult.error}`),
|
||||
{
|
||||
action: 'sendContactFormAction_notification',
|
||||
email,
|
||||
recipients: env.MAIL_RECIPIENTS
|
||||
{
|
||||
action: 'sendContactFormAction_notification',
|
||||
email,
|
||||
recipients: env.MAIL_RECIPIENTS,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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', 'products', 'pages', 'media'] as const;
|
||||
for (const collection of collections) {
|
||||
try {
|
||||
await payload.find({ collection, limit: 1, locale: 'en' });
|
||||
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 },
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
import { getPayload } from 'payload';
|
||||
import config from '@/payload.config';
|
||||
import { NextResponse } from 'next/server';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const payload = await getPayload({ config });
|
||||
|
||||
// Clear the excerpt for page ID 6
|
||||
await payload.update({
|
||||
collection: 'pages',
|
||||
id: 6,
|
||||
data: {
|
||||
excerpt: '',
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json({ success: true, message: 'AVB excerpt cleared successfully on production!' });
|
||||
} catch (error: any) {
|
||||
console.error('Error fixing AVB:', error);
|
||||
return NextResponse.json({ success: false, error: error.message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -1,111 +0,0 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { getPayload } from 'payload';
|
||||
import configPromise from '@payload-config';
|
||||
import { renderToStream } from '@react-pdf/renderer';
|
||||
import React from 'react';
|
||||
import { PDFPage } from '@/lib/pdf-page';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
export async function GET(req: NextRequest, { params }: { params: Promise<{ slug: string }> }) {
|
||||
try {
|
||||
const { slug } = await params;
|
||||
|
||||
// Handle AGBs specifically - either fetch from collection or use fallback file
|
||||
if (slug === 'agbs') {
|
||||
const payload = await getPayload({ config: configPromise });
|
||||
const currentAgbs = await payload.find({
|
||||
collection: 'agbs-collection',
|
||||
where: {
|
||||
isCurrent: { equals: true },
|
||||
},
|
||||
limit: 1,
|
||||
});
|
||||
|
||||
if (currentAgbs.totalDocs > 0) {
|
||||
const agb = currentAgbs.docs[0];
|
||||
const media = agb.file as any;
|
||||
if (media && media.url) {
|
||||
// If it's a remote URL or absolute path, we might need to fetch it
|
||||
// For now assume it's a local file in public/media
|
||||
const filePath = path.join(process.cwd(), 'public', media.url);
|
||||
if (fs.existsSync(filePath)) {
|
||||
const fileBuffer = fs.readFileSync(filePath);
|
||||
return new NextResponse(fileBuffer, {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/pdf',
|
||||
'Content-Disposition': `attachment; filename="${media.filename}"`,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback for hardcoded legacy AGB
|
||||
const filePath = path.join(process.cwd(), 'public', 'AVB-KLZ-4-2026.pdf');
|
||||
if (fs.existsSync(filePath)) {
|
||||
const fileBuffer = fs.readFileSync(filePath);
|
||||
return new NextResponse(fileBuffer, {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/pdf',
|
||||
'Content-Disposition': `attachment; filename="AVB-KLZ-4-2026.pdf"`,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Get Payload App
|
||||
const payload = await getPayload({ config: configPromise });
|
||||
|
||||
// Fetch the page
|
||||
const pages = await payload.find({
|
||||
collection: 'pages',
|
||||
where: {
|
||||
slug: { equals: slug },
|
||||
_status: { equals: 'published' },
|
||||
},
|
||||
limit: 1,
|
||||
});
|
||||
|
||||
if (pages.totalDocs === 0) {
|
||||
return new NextResponse('Page not found', { status: 404 });
|
||||
}
|
||||
|
||||
const page = pages.docs[0];
|
||||
|
||||
// Determine locale from searchParams or default to 'de'
|
||||
const searchParams = req.nextUrl.searchParams;
|
||||
const locale = (searchParams.get('locale') as 'en' | 'de') || 'de';
|
||||
|
||||
// Render the React-PDF document into a stream
|
||||
const stream = await renderToStream(<PDFPage page={page} locale={locale} />);
|
||||
|
||||
// Pipe the Node.js Readable stream into a valid fetch/Web Response stream
|
||||
const body = new ReadableStream({
|
||||
start(controller) {
|
||||
stream.on('data', (chunk) => controller.enqueue(chunk));
|
||||
stream.on('end', () => controller.close());
|
||||
stream.on('error', (err) => controller.error(err));
|
||||
},
|
||||
cancel() {
|
||||
(stream as any).destroy?.();
|
||||
},
|
||||
});
|
||||
|
||||
const filename = `${slug}.pdf`;
|
||||
|
||||
return new NextResponse(body, {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/pdf',
|
||||
'Content-Disposition': `attachment; filename="${filename}"`,
|
||||
// Cache control if needed, skip for now.
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error generating PDF:', error);
|
||||
return new NextResponse('Internal Server Error', { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
import { getPayload } from 'payload'
|
||||
import config from '@payload-config'
|
||||
|
||||
export async function GET() {
|
||||
const payload = await getPayload({ config })
|
||||
|
||||
await payload.update({
|
||||
collection: 'pages',
|
||||
id: 6,
|
||||
data: {
|
||||
excerpt: '',
|
||||
},
|
||||
})
|
||||
|
||||
return Response.json({ success: true, message: 'AVB excerpt cleared' })
|
||||
}
|
||||
Reference in New Issue
Block a user