fix(blog): preserve newlines in Lexical text nodes as <br> for proper list rendering
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 7s
Build & Deploy / 🧪 QA (push) Successful in 2m28s
Build & Deploy / 🏗️ Build (push) Successful in 6m15s
Build & Deploy / 🚀 Deploy (push) Failing after 7s
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 1s

test(og): use real blog slug from sitemap instead of hardcoded hello-world

chore(release): bump version to 2.2.10
This commit is contained in:
2026-03-01 23:21:35 +01:00
parent acf642d7e6
commit fb3ec6e10a
3 changed files with 72 additions and 24 deletions

View File

@@ -1,7 +1,7 @@
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 { Suspense, Fragment } from 'react';
// Import all custom React components that were previously mapped via Markdown
import StickyNarrative from '@/components/blog/StickyNarrative';
@@ -36,9 +36,45 @@ import GallerySection from '@/components/home/GallerySection';
import VideoSection from '@/components/home/VideoSection';
import CTA from '@/components/home/CTA';
/**
* Splits a text string on \n and intersperses <br /> 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) => (
<Fragment key={`${key}-${i}`}>
{part}
{i < parts.length - 1 && <br />}
</Fragment>
));
}
const jsxConverters: JSXConverters = {
...defaultJSXConverters,
// Let the default converters handle text nodes to preserve valid formatting
// Handle Lexical linebreak nodes (explicit shift+enter)
linebreak: () => <br />,
// Custom text converter: preserve \n inside text nodes as <br />
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)}`);
}
// Apply Lexical formatting flags
if (node.format) {
if (node.format & 1) content = <strong>{content}</strong>;
if (node.format & 2) content = <em>{content}</em>;
if (node.format & 8) content = <u>{content}</u>;
if (node.format & 4) content = <s>{content}</s>;
if (node.format & 16) content = <code className="px-1.5 py-0.5 bg-neutral-100 rounded text-sm font-mono text-primary">{content}</code>;
if (node.format & 32) content = <sub>{content}</sub>;
if (node.format & 64) content = <sup>{content}</sup>;
}
return <>{content}</>;
},
// Use div instead of p for paragraphs to allow nested block elements (like the lists above)
paragraph: ({ node, nodesToJSX }: any) => {
return (
@@ -57,16 +93,16 @@ const jsxConverters: JSXConverters = {
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, '')
.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')