+
{nodesToJSX({ nodes: node.children })}
);
@@ -51,33 +51,80 @@ const jsxConverters: JSXConverters = {
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}
+
+ {children}
+
);
if (tag === 'h2')
return (
-
{children}
+
+ {children}
+
);
if (tag === 'h3')
return (
-
{children}
+
+ {children}
+
);
if (tag === 'h4')
return (
-
{children}
+
+ {children}
+
);
if (tag === 'h5')
return (
-
{children}
+
+ {children}
+
);
- return
{children}
;
+ return (
+
+ {children}
+
+ );
},
list: ({ node, nodesToJSX }: any) => {
const children = nodesToJSX({ nodes: node.children });
if (node?.listType === 'number') {
return (
-
+
{children}
);
@@ -86,7 +133,7 @@ const jsxConverters: JSXConverters = {
return ;
}
return (
-
+
);
@@ -95,18 +142,18 @@ const jsxConverters: JSXConverters = {
const children = nodesToJSX({ nodes: node.children });
if (node?.checked != null) {
return (
- -
+
-
- {children}
+
{children}
);
}
- return - {children}
;
+ return - {children}
;
},
quote: ({ node, nodesToJSX }: any) => {
const children = nodesToJSX({ nodes: node.children });
diff --git a/lib/blog.ts b/lib/blog.ts
index db765d6f..3d4f15a0 100644
--- a/lib/blog.ts
+++ b/lib/blog.ts
@@ -116,7 +116,7 @@ export async function getPostBySlug(slug: string, locale: string): Promise {
category: doc.category || '',
featuredImage:
typeof doc.featuredImage === 'object' && doc.featuredImage !== null
- ? doc.featuredImage.url || doc.featuredImage.sizes?.card?.url
+ ? doc.featuredImage.sizes?.card?.url || doc.featuredImage.url
: null,
focalX:
typeof doc.featuredImage === 'object' && doc.featuredImage !== null
@@ -286,3 +286,38 @@ export function getHeadings(content: string): { id: string; text: string; level:
return { id, text: cleanText, level };
});
}
+
+export function extractLexicalHeadings(
+ node: any,
+ headings: { id: string; text: string; level: number }[] = [],
+): { id: string; text: string; level: number }[] {
+ if (!node) return headings;
+
+ if (node.type === 'heading' && node.tag) {
+ const level = parseInt(node.tag.replace('h', ''));
+ const text = getTextContentFromLexical(node);
+ if (text) {
+ headings.push({
+ id: generateHeadingId(text),
+ text,
+ level,
+ });
+ }
+ }
+
+ if (node.children && Array.isArray(node.children)) {
+ node.children.forEach((child: any) => extractLexicalHeadings(child, headings));
+ }
+
+ return headings;
+}
+
+function getTextContentFromLexical(node: any): string {
+ if (node.type === 'text') {
+ return node.text || '';
+ }
+ if (node.children && Array.isArray(node.children)) {
+ return node.children.map(getTextContentFromLexical).join('');
+ }
+ return '';
+}
diff --git a/package.json b/package.json
index 973753cd..b95cfd79 100644
--- a/package.json
+++ b/package.json
@@ -139,7 +139,7 @@
"prepare": "husky",
"preinstall": "npx only-allow pnpm"
},
- "version": "2.2.5",
+ "version": "2.2.6",
"pnpm": {
"onlyBuiltDependencies": [
"@parcel/watcher",
@@ -161,4 +161,4 @@
"peerDependencies": {
"lucide-react": "^0.563.0"
}
-}
+}
\ No newline at end of file
diff --git a/tests/og-image.test.ts b/tests/og-image.test.ts
index c23d95a7..7dc3e38f 100644
--- a/tests/og-image.test.ts
+++ b/tests/og-image.test.ts
@@ -1,6 +1,7 @@
import { describe, it, expect, beforeAll } from 'vitest';
-const BASE_URL = process.env.TEST_URL || process.env.NEXT_PUBLIC_BASE_URL || 'http://localhost:3000';
+const BASE_URL =
+ process.env.TEST_URL || process.env.NEXT_PUBLIC_BASE_URL || 'http://localhost:3000';
describe('OG Image Generation', () => {
const locales = ['de', 'en'];
@@ -18,7 +19,9 @@ describe('OG Image Generation', () => {
return;
}
}
- console.log(`\n⚠️ KLZ Application not detected at ${BASE_URL}. Skipping integration tests.\n`);
+ console.log(
+ `\n⚠️ KLZ Application not detected at ${BASE_URL}. Skipping integration tests.\n`,
+ );
} catch (e) {
isServerUp = false;
}
@@ -34,7 +37,7 @@ describe('OG Image Generation', () => {
// Check for PNG signature: 89 50 4E 47 0D 0A 1A 0A
expect(bytes[0]).toBe(0x89);
expect(bytes[1]).toBe(0x50);
- expect(bytes[2]).toBe(0x4E);
+ expect(bytes[2]).toBe(0x4e);
expect(bytes[3]).toBe(0x47);
// Check that the image is not empty and has a reasonable size
@@ -49,7 +52,9 @@ describe('OG Image Generation', () => {
await verifyImageResponse(response);
}, 30000);
- it(`should generate product OG image for ${locale} with slug ${productSlugs[0]}`, async ({ skip }) => {
+ it(`should generate product OG image for ${locale} with slug ${productSlugs[0]}`, async ({
+ skip,
+ }) => {
if (!isServerUp) skip();
const url = `${BASE_URL}/${locale}/api/og/product?slug=${productSlugs[0]}`;
const response = await fetch(url);
@@ -64,11 +69,26 @@ describe('OG Image Generation', () => {
}, 30000);
});
- it('should generate blog OG image', async ({ skip }) => {
+ it('should generate static blog overview OG image', async ({ skip }) => {
if (!isServerUp) skip();
const url = `${BASE_URL}/de/blog/opengraph-image`;
const response = await fetch(url);
await verifyImageResponse(response);
}, 30000);
-});
+ it('should generate dynamic blog post OG image', async ({ skip }) => {
+ if (!isServerUp) skip();
+ // Assuming 'hello-world' or a newly created post slug.
+ // If it 404s, it still tests the routing, though 200 is expected for an actual post.
+ const url = `${BASE_URL}/de/blog/hello-world/opengraph-image`;
+ const response = await fetch(url);
+ // Even if the post "hello-world" doesn't exist and returns 404 in some environments,
+ // we should at least check it doesn't 500. We'll accept 200 or 404 as valid "working" states
+ // vs a 500 compilation/satori error.
+ expect([200, 404]).toContain(response.status);
+
+ if (response.status === 200) {
+ await verifyImageResponse(response);
+ }
+ }, 30000);
+});