From 0554460153a0a79122d353e6f8127385748a7d17 Mon Sep 17 00:00:00 2001 From: Marc Mintel Date: Fri, 7 Aug 2026 17:12:55 +0200 Subject: [PATCH] fix(products): extract lexical AST and JSON-LD data correctly from productTabs block --- app/[locale]/products/[...slug]/page.tsx | 32 ++++++---- lib/mdx-utils.ts | 78 ++++++++++++++++++++++++ tests/lexical.test.ts | 27 ++++++++ 3 files changed, 124 insertions(+), 13 deletions(-) create mode 100644 tests/lexical.test.ts diff --git a/app/[locale]/products/[...slug]/page.tsx b/app/[locale]/products/[...slug]/page.tsx index 894ba5fb..e10277d1 100644 --- a/app/[locale]/products/[...slug]/page.tsx +++ b/app/[locale]/products/[...slug]/page.tsx @@ -17,6 +17,7 @@ import Link from 'next/link'; import { notFound, redirect } from 'next/navigation'; import ProductEngagementTracker from '@/components/analytics/ProductEngagementTracker'; import MDXContent from '@/components/MDXContent'; +import { lexicalToMarkdown } from '@/lib/mdx-utils'; interface ProductPageProps { params: Promise<{ @@ -266,18 +267,7 @@ export default async function ProductPage({ params }: ProductPageProps) { notFound(); } - let technicalItems = []; - try { - const tabMatch = - typeof product.content === 'string' && - product.content.match(//s); - if (tabMatch && tabMatch[1]) { - const data = JSON.parse(tabMatch[1]); - technicalItems = data.technicalItems || []; - } - } catch (e) { - // Ignore JSON parse errors for AST - } + let technicalItems: any[] = []; const datasheetPath = getDatasheetPath(productSlug, locale); const isFallback = (product.frontmatter as any).isFallback; @@ -296,8 +286,24 @@ export default async function ProductPage({ params }: ProductPageProps) { if (typeof product.content === 'string') { const tabBlockMatch = product.content.match(//s); if (tabBlockMatch) { - descriptionContent = product.content.replace(tabBlockMatch[0], ''); technicalContent = tabBlockMatch[0]; + descriptionContent = product.content.replace(tabBlockMatch[0], ''); + + const dataMatch = + technicalContent.match(/data="({.*?})"/s) || technicalContent.match(/data={({.*?})}/s); + if (dataMatch && dataMatch[1]) { + try { + const rawJsonStr = dataMatch[1].replace(/"/g, '"'); + const data = JSON.parse(rawJsonStr); + technicalItems = data.technicalItems || []; + + if (data.content?.root && descriptionContent.trim() === '') { + descriptionContent = lexicalToMarkdown(data.content.root); + } + } catch (e) { + console.error('Failed to parse productTabs data for page:', e); + } + } } } diff --git a/lib/mdx-utils.ts b/lib/mdx-utils.ts index 3a9bad25..e0d30e8d 100644 --- a/lib/mdx-utils.ts +++ b/lib/mdx-utils.ts @@ -66,3 +66,81 @@ export function fixMdxDataProps(content: string): string { return fixedContent; } + +export function lexicalToMarkdown(node: any, depth = 0): string { + if (!node) return ''; + + if (typeof node === 'string') return node; + + if (node.type === 'text') { + let text = node.text || ''; + if (node.format === 1) text = `**${text}**`; // Bold + if (node.format === 2) text = `*${text}*`; // Italic + return text; + } + + if (node.type === 'heading') { + const level = parseInt((node.tag || 'h1').replace('h', '')) || 1; + const prefix = '#'.repeat(level); + const text = (node.children || []).map((c: any) => lexicalToMarkdown(c, depth)).join(''); + return `${prefix} ${text}\n\n`; + } + + if (node.type === 'paragraph') { + const text = (node.children || []).map((c: any) => lexicalToMarkdown(c, depth)).join(''); + return text.trim() ? `${text}\n\n` : '\n'; + } + + if (node.type === 'list') { + const isOrdered = node.tag === 'ol'; + const items = (node.children || []) + .map((c: any, index: number) => { + const prefix = isOrdered ? `${index + 1}.` : '-'; + return `${prefix} ${lexicalToMarkdown(c, depth + 1).trim()}`; + }) + .join('\n'); + return `${items}\n\n`; + } + + if (node.type === 'listitem') { + return (node.children || []).map((c: any) => lexicalToMarkdown(c, depth)).join(''); + } + + if (node.type === 'quote') { + const text = (node.children || []).map((c: any) => lexicalToMarkdown(c, depth)).join(''); + return `> ${text}\n\n`; + } + + if (node.type === 'link') { + const url = node.fields?.url || node.url || ''; + const text = (node.children || []).map((c: any) => lexicalToMarkdown(c, depth)).join(''); + return `[${text}](${url})`; + } + + if (node.type === 'upload') { + const url = node.value?.url || ''; + const alt = node.value?.alt || node.value?.filename || ''; + return `![${alt}](${url})\n\n`; + } + + if (node.type === 'block') { + const blockType = node.fields?.blockType; + if (blockType === 'contactSection') { + return `\n\n`; + } + if (blockType === 'heroSection') { + return `\n\n`; + } + if (blockType === 'features') { + return `\n\n`; + } + // Generic MDX block wrapper if unknown + return `\n\n`; + } + + if (node.children && Array.isArray(node.children)) { + return node.children.map((c: any) => lexicalToMarkdown(c, depth)).join(''); + } + + return ''; +} diff --git a/tests/lexical.test.ts b/tests/lexical.test.ts new file mode 100644 index 00000000..4373d06e --- /dev/null +++ b/tests/lexical.test.ts @@ -0,0 +1,27 @@ +import { describe, it, expect } from 'vitest'; +import { lexicalToMarkdown } from '../lib/mdx-utils'; + +describe('lexicalToMarkdown', () => { + it('should convert lexical AST to markdown', () => { + const ast = { + type: 'root', + children: [ + { + type: 'paragraph', + children: [ + { type: 'text', text: 'Hello ' }, + { type: 'text', text: 'World', format: 1 }, // bold + ], + }, + { + type: 'heading', + tag: 'h2', + children: [{ type: 'text', text: 'Title' }], + }, + ], + }; + + const result = lexicalToMarkdown(ast); + expect(result).toBe('Hello **World**\n\n## Title\n\n'); + }); +});