import { defaultJSXConverters, RichText } from '@payloadcms/richtext-lexical/react'; import type { JSXConverters } from '@payloadcms/richtext-lexical/react'; import Image from 'next/image'; import { Suspense, Fragment } from 'react'; // Import all custom React components that were previously mapped via Markdown import StickyNarrative from '@/components/blog/StickyNarrative'; import ComparisonGrid from '@/components/blog/ComparisonGrid'; import VisualLinkPreview from '@/components/blog/VisualLinkPreview'; import TechnicalGrid from '@/components/blog/TechnicalGrid'; import HighlightBox from '@/components/blog/HighlightBox'; import AnimatedImage from '@/components/blog/AnimatedImage'; import ChatBubble from '@/components/blog/ChatBubble'; import PowerCTA from '@/components/blog/PowerCTA'; import { Callout } from '@/components/ui/Callout'; import Stats from '@/components/blog/Stats'; import SplitHeading from '@/components/blog/SplitHeading'; import ProductTabs from '@/components/ProductTabs'; import ProductTechnicalData from '@/components/ProductTechnicalData'; import ContactForm from '@/components/ContactForm'; import ContactMap from '@/components/ContactMap'; import Gallery from '@/components/team/Gallery'; import Reveal from '@/components/Reveal'; import { Badge, Container, Heading, Section, Card } from '@/components/ui'; import TrackedLink from '@/components/analytics/TrackedLink'; import { useLocale } from 'next-intl'; import ObfuscatedEmail from '@/components/ObfuscatedEmail'; import ObfuscatedPhone from '@/components/ObfuscatedPhone'; import HomeHero from '@/components/home/Hero'; import ProductCategories from '@/components/home/ProductCategories'; import WhatWeDo from '@/components/home/WhatWeDo'; import RecentPosts from '@/components/home/RecentPosts'; import Experience from '@/components/home/Experience'; import WhyChooseUs from '@/components/home/WhyChooseUs'; import MeetTheTeam from '@/components/home/MeetTheTeam'; import GallerySection from '@/components/home/GallerySection'; import VideoSection from '@/components/home/VideoSection'; import CTA from '@/components/home/CTA'; import { PDFDownloadBlock } from '@/components/PDFDownloadBlock'; /** * Splits a text string on \n and intersperses
elements. * This is needed because Lexical stores newlines as literal \n characters inside * text nodes (e.g. dash-lists typed in the editor), but HTML collapses whitespace. */ function textWithLineBreaks(text: string, key: string) { const parts = text.split('\n'); if (parts.length === 1) return text; return parts.map((part, i) => ( {part} {i < parts.length - 1 &&
}
)); } const jsxConverters: JSXConverters = { ...defaultJSXConverters, // Handle Lexical linebreak nodes (explicit shift+enter) linebreak: () =>
, // Custom text converter: preserve \n inside text nodes as
and obfuscate emails text: ({ node }: any) => { let content: React.ReactNode = node.text || ''; // Split newlines first if (typeof content === 'string' && content.includes('\n')) { content = textWithLineBreaks(content, `t-${(node.text || '').slice(0, 8)}`); } // Obfuscate emails in text content if (typeof content === 'string' && content.includes('@')) { const emailRegex = /([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})/g; const parts = content.split(emailRegex); content = parts.map((part, i) => { if (part.match(emailRegex)) { return ; } return part; }); } // Obfuscate phone numbers in text content (simple pattern for +XX XXX ...) if (typeof content === 'string' && content.match(/\+\d+/)) { const phoneRegex = /(\+\d{1,4}[\d\s-]{5,15})/g; const parts = content.split(phoneRegex); content = parts.map((part, i) => { if (part.match(phoneRegex)) { return ; } return part; }); } // Handle array content (from previous mappings) if (Array.isArray(content)) { content = content.map((item, idx) => { if (typeof item === 'string') { // Re-apply phone regex to strings in array const phoneRegex = /(\+\d{1,4}[\d\s-]{5,15})/g; if (item.match(phoneRegex)) { const parts = item.split(phoneRegex); return parts.map((part, i) => { if (part.match(phoneRegex)) { return ; } return part; }); } } return item; }); } // Apply Lexical formatting flags if (node.format) { if (node.format & 1) content = {content}; if (node.format & 2) content = {content}; if (node.format & 8) content = {content}; if (node.format & 4) content = {content}; if (node.format & 16) content = ( {content} ); if (node.format & 32) content = {content}; if (node.format & 64) content = {content}; } return <>{content}; }, // Use div instead of p for paragraphs to allow nested block elements (like the lists above) paragraph: ({ node, nodesToJSX }: any) => { return (
{nodesToJSX({ nodes: node.children })}
); }, // Scale headings to prevent multiple H1s (H1 -> H2, etc) and style natively heading: ({ node, nodesToJSX }: any) => { const children = nodesToJSX({ nodes: node.children }); const tag = node?.tag; // Extract text to generate an ID for the TOC // Lexical children might contain various nodes; we need a plain text representation const textContent = node.children ? node.children.map((c: any) => c.text || '').join('') : ''; const id = textContent ? textContent .toLowerCase() .replace(/ä/g, 'ae') .replace(/ö/g, 'oe') .replace(/ü/g, 'ue') .replace(/ß/g, 'ss') .replace(/[*_`]/g, '') .replace(/[^\w\s-]/g, '') .replace(/\s+/g, '-') .replace(/-+/g, '-') .replace(/^-+|-+$/g, '') : undefined; if (tag === 'h1') return (

{children}

); if (tag === 'h2') return (

{children}

); if (tag === 'h3') return (

{children}

); if (tag === 'h4') return (
{children}
); if (tag === 'h5') return (
{children}
); return (
{children}
); }, list: ({ node, nodesToJSX }: any) => { const children = nodesToJSX({ nodes: node.children }); if (node?.listType === 'number') { return (
    {children}
); } if (node?.listType === 'check') { return
    {children}
; } return (
    {children}
); }, listitem: ({ node, nodesToJSX }: any) => { const children = nodesToJSX({ nodes: node.children }); if (node?.checked != null) { return (
  • {children}
  • ); } return
  • {children}
  • ; }, quote: ({ node, nodesToJSX }: any) => { const children = nodesToJSX({ nodes: node.children }); return (
    {children}
    ); }, link: ({ node, nodesToJSX }: any) => { const children = nodesToJSX({ nodes: node.children }); // Handling Payload CMS link nodes const href = node?.fields?.url || node?.url || '#'; const newTab = node?.fields?.newTab || node?.newTab; if (href.startsWith('mailto:')) { const email = href.replace('mailto:', ''); return ( ); } return ( {children} ); }, blocks: { // ... preserved existing blocks ... // Map the custom Payload Blocks created in src/payload/blocks to their React components // Payload Lexical exposes blocks using the 'block-[slug]' pattern stickyNarrative: ({ node }: any) => ( ), 'block-stickyNarrative': ({ node }: any) => ( ), comparisonGrid: ({ node }: any) => ( ), 'block-comparisonGrid': ({ node }: any) => ( ), visualLinkPreview: ({ node }: any) => ( ), 'block-visualLinkPreview': ({ node }: any) => ( ), technicalGrid: ({ node }: any) => ( ), 'block-technicalGrid': ({ node }: any) => { if (!node?.fields) return null; return ; }, highlightBox: ({ node }: any) => ( ), 'block-highlightBox': ({ node }: any) => ( ), animatedImage: ({ node }: any) => ( ), 'block-animatedImage': ({ node }: any) => ( ), chatBubble: ({ node }: any) => ( ), 'block-chatBubble': ({ node }: any) => ( ), powerCTA: ({ node }: any) => , 'block-powerCTA': ({ node }: any) => , callout: ({ node }: any) => ( ), 'block-callout': ({ node }: any) => ( ), stats: ({ node }: any) => , 'block-stats': ({ node }: any) => , splitHeading: ({ node }: any) => ( {node.fields.title} ), 'block-splitHeading': ({ node }: any) => ( {node.fields.title} ), productTabs: ({ node }: any) => { if (!node?.fields) return null; return ( } > <> ); }, 'block-productTabs': ({ node }: any) => ( } > {node.fields.content && } ), pdfDownload: ({ node }: any) => ( ), 'block-pdfDownload': ({ node }: any) => ( ), // ─── New Page Blocks ─────────────────────────────────────────── heroSection: ({ node }: any) => { const f = node.fields; const bgSrc = f.backgroundImage?.sizes?.card?.url || f.backgroundImage?.url; return (
    {bgSrc && ( <>
    {f.title}
    )}
    {f.badge && ( {f.badge} )} {f.title} {f.subtitle && (

    {f.subtitle}

    )} {f.ctaLabel && f.ctaHref && ( )}
    ); }, 'block-heroSection': ({ node }: any) => { const f = node.fields; const bgSrc = f.backgroundImage?.sizes?.card?.url || f.backgroundImage?.url; return (
    {bgSrc && ( <>
    {f.title}
    )}
    {f.badge && ( {f.badge} )} {f.title} {f.subtitle && (

    {f.subtitle}

    )} {f.ctaLabel && f.ctaHref && ( )}
    ); }, teamProfile: ({ node }: any) => { const f = node.fields; const imgSrc = f.image?.sizes?.card?.url || f.image?.url; const isDark = f.colorScheme === 'dark'; const isImageRight = f.layout === 'imageRight'; return (
    {f.role} {f.name} {f.quote && (

    {f.quote}

    )} {f.description && (

    {f.description}

    )} {f.linkedinUrl && ( {f.linkedinLabel || 'LinkedIn'} )}
    {imgSrc && ( <> {f.name}
    )}
    ); }, 'block-teamProfile': ({ node }: any) => { const f = node.fields; const imgSrc = f.image?.sizes?.card?.url || f.image?.url; const isDark = f.colorScheme === 'dark'; const isImageRight = f.layout === 'imageRight'; return (
    {f.role} {f.name} {f.quote && (

    {f.quote}

    )} {f.description && (

    {f.description}

    )} {f.linkedinUrl && ( {f.linkedinLabel || 'LinkedIn'} )}
    {imgSrc && ( <> {f.name}
    )}
    ); }, contactSection: ({ node }: any) => { const f = node.fields; return (
    {f.showForm && (
    } >
    )}
    {f.showMap && (
    } >
    )}
    ); }, 'block-contactSection': ({ node }: any) => { const f = node.fields; return (
    {f.showForm && (
    } >
    )}
    {f.showMap && (
    } >
    )}
    ); }, imageGallery: ({ node }: any) => , 'block-imageGallery': ({ node }: any) => , categoryGrid: ({ node }: any) => { const cats = node.fields.categories || []; return (
    {cats.map((cat: any, idx: number) => { const imgSrc = cat.image?.sizes?.card?.url || cat.image?.url; const iconSrc = cat.icon?.url; return (
    ); }, 'block-categoryGrid': ({ node }: any) => { const cats = node.fields.categories || []; return (
    {cats.map((cat: any, idx: number) => { const imgSrc = cat.image?.sizes?.card?.url || cat.image?.url; const iconSrc = cat.icon?.url; return (
    ); }, manifestoGrid: ({ node }: any) => { const f = node.fields; return (
    {f.title && ( {f.title} )} {f.tagline && (

    {f.tagline}

    )}
      {(f.items || []).map((item: any, idx: number) => (
    • 0{idx + 1}

      {item.title}

      {item.description}

    • ))}
    ); }, 'block-manifestoGrid': ({ node }: any) => { const f = node.fields; return (
    {f.title && ( {f.title} )} {f.tagline && (

    {f.tagline}

    )}
      {(f.items || []).map((item: any, idx: number) => (
    • 0{idx + 1}

      {item.title}

      {item.description}

    • ))}
    ); }, homeHero: ({ node }: any) => { console.log('[PayloadRichText] Rendering homeHero block'); return ; }, 'block-homeHero': ({ node }: any) => { console.log('[PayloadRichText] Rendering block-homeHero block'); return ; }, homeProductCategories: ({ node }: any) => ( ), 'block-homeProductCategories': ({ node }: any) => ( ), homeWhatWeDo: ({ node }: any) => ( ), 'block-homeWhatWeDo': ({ node }: any) => ( ), homeExperience: ({ node }: any) => ( ), 'block-homeExperience': ({ node }: any) => ( ), homeWhyChooseUs: ({ node }: any) => ( ), 'block-homeWhyChooseUs': ({ node }: any) => ( ), homeMeetTheTeam: ({ node }: any) => ( ), 'block-homeMeetTheTeam': ({ node }: any) => ( ), homeGallery: ({ node }: any) => ( ), 'block-homeGallery': ({ node }: any) => ( ), homeVideo: ({ node }: any) => ( ), 'block-homeVideo': ({ node }: any) => ( ), homeCTA: ({ node }: any) => ( ), 'block-homeCTA': ({ node }: any) => ( ), }, // Custom converter for the Payload "upload" Lexical node (Media collection) // This natively reconstructs Next.js tags pointing to the focal-point cropped sizes upload: ({ node }: any) => { // Attempt to extract the highly optimized 'card' generated size from Payload, fallback to raw url let src = node?.value?.sizes?.card?.url || node?.value?.url; const alt = node?.value?.alt || 'Blog Post Media'; if (!src) return null; // Strip legacy imgproxy query parameters (e.g. ?ar=16:9) that crash Next.js 14+ localPatterns if (src.includes('?')) { src = src.split('?')[0]; } // Fallback dimensions if unmapped or loading from raw const width = node?.value?.sizes?.card?.width || 800; const height = node?.value?.sizes?.card?.height || 600; return (
    {alt} {node?.value?.caption && (
    {node.value.caption}
    )}
    ); }, }; export default function PayloadRichText({ data, className = 'article-content max-w-none', }: { data: any; className?: string; }) { const locale = useLocale(); if (!data) return null; if (data.root?.children?.length > 0) { console.log('[PayloadRichText DEBUG] received children', data.root.children.length); } const dynamicConverters: JSXConverters = { ...jsxConverters, blocks: { ...jsxConverters.blocks, homeRecentPosts: () => ( ), 'block-homeRecentPosts': () => ( ), }, }; return (
    ); }