import { defaultJSXConverters, RichText } from '@payloadcms/richtext-lexical/react'; import type { JSXConverters } from '@payloadcms/richtext-lexical/react'; import Image from 'next/image'; import { Suspense } 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 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'; const jsxConverters: JSXConverters = { ...defaultJSXConverters, // Let the default converters handle text nodes to preserve valid formatting // If the text node contains raw HTML (from messy migrations), render it as HTML instead of escaping it text: ({ node }: any) => { const text = node.text; // Handle markdown-style lists embedded in text nodes from Markdown migration if (text && text.includes('\n- ')) { const parts = text.split('\n- ').filter((p: string) => p.trim() !== ''); // If first part doesn't start with "- ", it's a prefix paragraph const startsWithDash = text.trimStart().startsWith('- '); const prefix = startsWithDash ? null : parts.shift(); return ( <> {prefix && ( {!prefix.includes('<') ? prefix : undefined} )} ); } if (text && (text.includes('<') || text.includes('data-start'))) { return ; } // Handle markdown-style links [text](url) from Markdown migration if (text && /\[([^\]]+)\]\(([^)]+)\)/.test(text)) { const parts: React.ReactNode[] = []; const remaining = text; let key = 0; const linkRegex = /\[([^\]]+)\]\(([^)]+)\)/g; let match; let lastIndex = 0; while ((match = linkRegex.exec(remaining)) !== null) { if (match.index > lastIndex) { parts.push({remaining.slice(lastIndex, match.index)}); } parts.push( {match[1]} , ); lastIndex = match.index + match[0].length; } if (lastIndex < remaining.length) { parts.push({remaining.slice(lastIndex)}); } return <>{parts}; } // Handle newlines in text nodes — convert to
for proper line breaks if (text && text.includes('\n')) { const lines = text.split('\n'); return ( <> {lines.map((line: string, i: number) => ( {line} {i < lines.length - 1 &&
}
))} ); } if (node.format === 1) return {text}; if (node.format === 2) return {text}; return {text}; }, // Scale headings to prevent multiple H1s (H1 -> H2, etc) h1: ({ children }: any) =>

{children}

, h2: ({ children }: any) =>

{children}

, h3: ({ children }: any) =>

{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) => { console.log('[PayloadRichText] Rendering block-technicalGrid:', node.fields.title); 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) => ( } > <> ), 'block-productTabs': ({ node }: any) => ( } > {node.fields.content && } ), // ─── 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; const dynamicConverters: JSXConverters = { ...jsxConverters, blocks: { ...jsxConverters.blocks, homeRecentPosts: () => ( ), 'block-homeRecentPosts': () => ( ), }, }; return (
); }