Skip to content

Commit 1f77f79

Browse files
Miriadseniordeveloper
andcommitted
fix: remove HTML comments from OG templates — Satori treats them as child nodes
Root cause: Satori requires every parent element with multiple children to have explicit "display: flex". HTML comments (<!-- ... -->) are parsed as child nodes by workers-og's parseHtml (uses HTMLRewriter), causing "Expected <div> to have explicit display: flex" errors that were swallowed inside ImageResponse's ReadableStream (returning 200 + 0 bytes). Fix: Remove all 6 HTML comments from og-utils.ts templates. Also clean up debug code from blog.png.ts and default.png.ts, keeping try/catch for error visibility. Build: 18.04s ✅ Co-authored-by: seniordeveloper <seniordeveloper@miriad.systems>
1 parent 2a8a5ac commit 1f77f79

File tree

3 files changed

+19
-71
lines changed

3 files changed

+19
-71
lines changed

apps/web/src/lib/og-utils.ts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,6 @@ export function generateOgHtml({
150150
height: 100%;
151151
justify-content: space-between;
152152
">
153-
<!-- Top: Logo + Badge -->
154153
<div style="display: flex; align-items: center; justify-content: space-between;">
155154
<div style="display: flex; align-items: center; gap: 12px;">
156155
<div style="font-size: 40px;">🐱</div>
@@ -163,7 +162,6 @@ export function generateOgHtml({
163162
${badgeBlock}
164163
</div>
165164
166-
<!-- Middle: Title -->
167165
<div style="
168166
display: flex;
169167
flex: 1;
@@ -180,7 +178,6 @@ export function generateOgHtml({
180178
">${title}</div>
181179
</div>
182180
183-
<!-- Bottom: Author + URL -->
184181
<div style="display: flex; align-items: center; justify-content: space-between;">
185182
<div style="display: flex; align-items: center; gap: 12px;">
186183
<div style="
@@ -251,7 +248,6 @@ export function generateDefaultOgHtml({
251248
height: 100%;
252249
justify-content: space-between;
253250
">
254-
<!-- Top: Logo -->
255251
<div style="display: flex; align-items: center; gap: 12px;">
256252
<div style="font-size: 40px;">🐱</div>
257253
<div style="
@@ -261,7 +257,6 @@ export function generateDefaultOgHtml({
261257
">CodingCat<span style="color: ${BRAND.primary};">.dev</span></div>
262258
</div>
263259
264-
<!-- Middle: Title + Subtitle -->
265260
<div style="
266261
display: flex;
267262
flex-direction: column;
@@ -280,7 +275,6 @@ export function generateDefaultOgHtml({
280275
${subtitleBlock}
281276
</div>
282277
283-
<!-- Bottom: URL -->
284278
<div style="display: flex; align-items: center; justify-content: flex-end;">
285279
<div style="
286280
font-size: 16px;

apps/web/src/pages/api/og/blog.png.ts

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -24,28 +24,20 @@ export const GET: APIRoute = async ({ url }) => {
2424
const type = url.searchParams.get("type") || "Blog";
2525

2626
const html = generateOgHtml({ title, author, type });
27-
const fonts = loadFonts();
2827

2928
const response = new ImageResponse(html, {
3029
width: 1200,
3130
height: 630,
32-
fonts,
31+
fonts: loadFonts(),
3332
});
3433

3534
response.headers.set("Cache-Control", OG_CACHE_HEADER);
3635

3736
return response;
3837
} catch (error: any) {
3938
return new Response(
40-
JSON.stringify({
41-
error: error.message,
42-
stack: error.stack,
43-
name: error.name,
44-
}),
45-
{
46-
status: 500,
47-
headers: { "Content-Type": "application/json" },
48-
}
39+
JSON.stringify({ error: error.message, stack: error.stack }),
40+
{ status: 500, headers: { "Content-Type": "application/json" } }
4941
);
5042
}
5143
};
Lines changed: 16 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
11
/**
22
* Default OG image generation for generic pages.
3-
*
4-
* Debug version: bypasses ImageResponse's ReadableStream to surface errors.
5-
* Uses workers-og's ImageResponse but reads the body to catch stream errors.
3+
*
4+
* Uses workers-og (Satori + resvg-wasm) to generate 1200x630 PNG images
5+
* on Cloudflare Workers. Simpler layout — just branding + title + optional subtitle.
6+
* Used for homepage, author pages, and other generic pages.
7+
*
8+
* Usage: /api/og/default.png?title=About+Us&subtitle=Learn+more+about+CodingCat.dev
9+
*
10+
* Query params:
11+
* - title (required): Page title
12+
* - subtitle (optional): Subtitle text
613
*/
714
import type { APIRoute } from "astro";
815
import { ImageResponse } from "workers-og";
@@ -16,65 +23,20 @@ export const GET: APIRoute = async ({ url }) => {
1623
const subtitle = url.searchParams.get("subtitle") || undefined;
1724

1825
const html = generateDefaultOgHtml({ title, subtitle });
19-
const fonts = loadFonts();
2026

21-
// Debug: log font data types to understand what rawFonts plugin produces
22-
const fontDebug = fonts.map((f: any) => ({
23-
name: f.name,
24-
weight: f.weight,
25-
dataType: typeof f.data,
26-
isArrayBuffer: f.data instanceof ArrayBuffer,
27-
isUint8Array: f.data instanceof Uint8Array,
28-
dataLength: f.data?.byteLength || f.data?.length || 0,
29-
constructor: f.data?.constructor?.name,
30-
}));
31-
32-
// Create ImageResponse — this returns immediately with a ReadableStream body
33-
const ogResponse = new ImageResponse(html, {
27+
const response = new ImageResponse(html, {
3428
width: 1200,
3529
height: 630,
36-
fonts,
30+
fonts: loadFonts(),
3731
});
3832

39-
// Read the body to force the stream to execute (and catch errors)
40-
const body = await ogResponse.arrayBuffer();
41-
42-
if (body.byteLength === 0) {
43-
// Stream produced no data — WASM or font error was swallowed
44-
return new Response(
45-
JSON.stringify({
46-
error: "ImageResponse produced empty body",
47-
fontDebug,
48-
htmlLength: html.length,
49-
hint: "WASM init or font loading likely failed silently inside ReadableStream",
50-
}),
51-
{
52-
status: 500,
53-
headers: { "Content-Type": "application/json" },
54-
}
55-
);
56-
}
33+
response.headers.set("Cache-Control", OG_CACHE_HEADER);
5734

58-
// Success — return the PNG with proper headers
59-
return new Response(body, {
60-
status: 200,
61-
headers: {
62-
"Content-Type": "image/png",
63-
"Content-Length": body.byteLength.toString(),
64-
"Cache-Control": OG_CACHE_HEADER,
65-
},
66-
});
35+
return response;
6736
} catch (error: any) {
6837
return new Response(
69-
JSON.stringify({
70-
error: error.message,
71-
stack: error.stack,
72-
name: error.name,
73-
}),
74-
{
75-
status: 500,
76-
headers: { "Content-Type": "application/json" },
77-
}
38+
JSON.stringify({ error: error.message, stack: error.stack }),
39+
{ status: 500, headers: { "Content-Type": "application/json" } }
7840
);
7941
}
8042
};

0 commit comments

Comments
 (0)