33 *
44 * Extracts brand tokens, type colors, font loading, and HTML generation
55 * into a single module so all OG endpoints stay DRY.
6+ *
7+ * IMPORTANT: Satori (via workers-og's HTMLRewriter parser) is extremely
8+ * strict about HTML structure:
9+ * - Every element with multiple children MUST have display:flex
10+ * - No HTML comments
11+ * - No whitespace between elements (parsed as text nodes)
12+ * - No mixed text + element content (wrap text in <span>)
13+ * All HTML must be fully minified — no newlines, no indentation.
614 */
715
816// @ts -ignore — loaded by rawFonts vite plugin as Uint8Array
@@ -65,17 +73,6 @@ export function loadFonts() {
6573 ] ;
6674}
6775
68- // ── Minify HTML for Satori ───────────────────────────────────────────
69- // Satori's HTML parser (via HTMLRewriter) treats whitespace between
70- // elements as text nodes. Template literal formatting creates phantom
71- // text nodes that violate Satori's strict "display: flex" requirement.
72- function minifyHtml ( html : string ) : string {
73- return html
74- . replace ( / > \s + < / g, "><" ) // Remove whitespace between tags
75- . replace ( / \s + / g, " " ) // Collapse all remaining whitespace (including inside style attrs)
76- . trim ( ) ;
77- }
78-
7976// ── Adaptive title font size ─────────────────────────────────────────
8077export function titleFontSize ( title : string ) : number {
8178 if ( title . length > 60 ) return 42 ;
@@ -94,6 +91,8 @@ function authorInitials(author: string): string {
9491}
9592
9693// ── HTML generators ──────────────────────────────────────────────────
94+ // All HTML is built as single-line strings with NO whitespace between tags.
95+ // This prevents HTMLRewriter from creating phantom text nodes.
9796
9897export interface OgHtmlOptions {
9998 title : string ;
@@ -103,126 +102,39 @@ export interface OgHtmlOptions {
103102 episodeNumber ?: string ;
104103}
105104
105+ // Shared building blocks
106+ const logo = `<div style="display:flex;align-items:center;gap:12px"><div style="font-size:40px">🐱</div><div style="display:flex;font-size:24px;font-weight:700;color:${ BRAND . text } "><span>CodingCat</span><span style="color:${ BRAND . primary } ">.dev</span></div></div>` ;
107+
108+ function badge ( type : string , typeColor : string , episodeNumber ?: string ) : string {
109+ const ep = episodeNumber ? `<div style="display:flex;font-size:14px;font-weight:600;color:${ typeColor } ;margin-top:4px">Episode ${ episodeNumber } </div>` : "" ;
110+ return `<div style="display:flex;flex-direction:column;align-items:flex-end"><div style="display:flex;padding:6px 16px;border-radius:6px;font-size:14px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:${ typeColor } ;background:${ typeColor } 26;border:1px solid ${ typeColor } 40">${ type } </div>${ ep } </div>` ;
111+ }
112+
113+ function authorBlock ( author : string ) : string {
114+ return `<div style="display:flex;align-items:center;gap:12px"><div style="display:flex;width:44px;height:44px;background:linear-gradient(135deg,${ BRAND . primary } ,${ BRAND . primaryLight } );border-radius:50%;align-items:center;justify-content:center;font-weight:700;font-size:16px;color:white">${ authorInitials ( author ) } </div><div style="font-size:18px;font-weight:500;color:${ BRAND . textSecondary } ">${ author } </div></div>` ;
115+ }
116+
117+ function urlBlock ( ) : string {
118+ return `<div style="font-size:16px;color:${ BRAND . textTertiary } ">codingcat.dev</div>` ;
119+ }
120+
106121/**
107122 * Generates the full-featured OG HTML (blog, podcast, course).
108- * Includes logo, type badge, title, author avatar, and optional episode number.
109123 */
110124export function generateOgHtml ( {
111125 title,
112126 author = "CodingCat.dev" ,
113127 type = "Blog" ,
114- subtitle,
115128 episodeNumber,
116129} : OgHtmlOptions ) : string {
117130 const typeColor = TYPE_COLORS [ type ] || BRAND . typeBlog ;
118131 const fontSize = titleFontSize ( title ) ;
119132
120- const episodeLine = episodeNumber
121- ? `<div style="
122- font-size: 14px;
123- font-weight: 600;
124- color: ${ typeColor } ;
125- margin-top: 4px;
126- ">Episode ${ episodeNumber } </div>`
127- : "" ;
128-
129- const badgeBlock = `
130- <div style="display: flex; flex-direction: column; align-items: flex-end;">
131- <div style="
132- display: flex;
133- padding: 6px 16px;
134- border-radius: 6px;
135- font-size: 14px;
136- font-weight: 600;
137- text-transform: uppercase;
138- letter-spacing: 0.05em;
139- color: ${ typeColor } ;
140- background: ${ typeColor } 26;
141- border: 1px solid ${ typeColor } 40;
142- ">${ type } </div>
143- ${ episodeLine }
144- </div>
145- ` ;
146-
147- return minifyHtml ( `
148- <div style="
149- display: flex;
150- width: 1200px;
151- height: 630px;
152- background: ${ BRAND . bgGradient } ;
153- padding: 60px;
154- box-sizing: border-box;
155- font-family: 'Inter', sans-serif;
156- ">
157- <div style="
158- display: flex;
159- flex-direction: column;
160- width: 100%;
161- height: 100%;
162- justify-content: space-between;
163- ">
164- <div style="display: flex; align-items: center; justify-content: space-between;">
165- <div style="display: flex; align-items: center; gap: 12px;">
166- <div style="font-size: 40px;">🐱</div>
167- <div style="
168- font-size: 24px;
169- font-weight: 700;
170- color: ${ BRAND . text } ;
171- display: flex;
172- "><span>CodingCat</span><span style="color: ${ BRAND . primary } ;">.dev</span></div>
173- </div>
174- ${ badgeBlock }
175- </div>
176-
177- <div style="
178- display: flex;
179- flex: 1;
180- align-items: center;
181- padding: 20px 0;
182- ">
183- <div style="
184- font-size: ${ fontSize } px;
185- font-weight: 700;
186- line-height: 1.15;
187- color: ${ BRAND . text } ;
188- max-width: 100%;
189- overflow: hidden;
190- ">${ title } </div>
191- </div>
192-
193- <div style="display: flex; align-items: center; justify-content: space-between;">
194- <div style="display: flex; align-items: center; gap: 12px;">
195- <div style="
196- display: flex;
197- width: 44px;
198- height: 44px;
199- background: linear-gradient(135deg, ${ BRAND . primary } , ${ BRAND . primaryLight } );
200- border-radius: 50%;
201- align-items: center;
202- justify-content: center;
203- font-weight: 700;
204- font-size: 16px;
205- color: white;
206- ">${ authorInitials ( author ) } </div>
207- <div style="
208- font-size: 18px;
209- font-weight: 500;
210- color: ${ BRAND . textSecondary } ;
211- ">${ author } </div>
212- </div>
213- <div style="
214- font-size: 16px;
215- color: ${ BRAND . textTertiary } ;
216- ">codingcat.dev</div>
217- </div>
218- </div>
219- </div>
220- ` ) ;
133+ return `<div style="display:flex;width:1200px;height:630px;background:${ BRAND . bgGradient } ;padding:60px;box-sizing:border-box;font-family:'Inter',sans-serif"><div style="display:flex;flex-direction:column;width:100%;height:100%;justify-content:space-between"><div style="display:flex;align-items:center;justify-content:space-between">${ logo } ${ badge ( type , typeColor , episodeNumber ) } </div><div style="display:flex;flex:1;align-items:center;padding:20px 0"><div style="font-size:${ fontSize } px;font-weight:700;line-height:1.15;color:${ BRAND . text } ;max-width:100%;overflow:hidden">${ title } </div></div><div style="display:flex;align-items:center;justify-content:space-between">${ authorBlock ( author ) } ${ urlBlock ( ) } </div></div></div>` ;
221134}
222135
223136/**
224137 * Generates a simpler OG HTML for default/generic pages.
225- * No author avatar, no type badge — just branding + title + optional subtitle.
226138 */
227139export function generateDefaultOgHtml ( {
228140 title,
@@ -232,69 +144,9 @@ export function generateDefaultOgHtml({
232144 subtitle ?: string ;
233145} ) : string {
234146 const fontSize = titleFontSize ( title ) ;
235-
236- const subtitleBlock = subtitle
237- ? `<div style="
238- font-size: 24px;
239- font-weight: 400;
240- color: ${ BRAND . textSecondary } ;
241- margin-top: 16px;
242- line-height: 1.4;
243- ">${ subtitle } </div>`
147+ const subtitleHtml = subtitle
148+ ? `<div style="font-size:24px;font-weight:400;color:${ BRAND . textSecondary } ;margin-top:16px;line-height:1.4">${ subtitle } </div>`
244149 : "" ;
245150
246- return minifyHtml ( `
247- <div style="
248- display: flex;
249- width: 1200px;
250- height: 630px;
251- background: ${ BRAND . bgGradient } ;
252- padding: 60px;
253- box-sizing: border-box;
254- font-family: 'Inter', sans-serif;
255- ">
256- <div style="
257- display: flex;
258- flex-direction: column;
259- width: 100%;
260- height: 100%;
261- justify-content: space-between;
262- ">
263- <div style="display: flex; align-items: center; gap: 12px;">
264- <div style="font-size: 40px;">🐱</div>
265- <div style="
266- font-size: 24px;
267- font-weight: 700;
268- color: ${ BRAND . text } ;
269- display: flex;
270- "><span>CodingCat</span><span style="color: ${ BRAND . primary } ;">.dev</span></div>
271- </div>
272-
273- <div style="
274- display: flex;
275- flex-direction: column;
276- flex: 1;
277- justify-content: center;
278- padding: 20px 0;
279- ">
280- <div style="
281- font-size: ${ fontSize } px;
282- font-weight: 700;
283- line-height: 1.15;
284- color: ${ BRAND . text } ;
285- max-width: 100%;
286- overflow: hidden;
287- ">${ title } </div>
288- ${ subtitleBlock }
289- </div>
290-
291- <div style="display: flex; align-items: center; justify-content: flex-end;">
292- <div style="
293- font-size: 16px;
294- color: ${ BRAND . textTertiary } ;
295- ">codingcat.dev</div>
296- </div>
297- </div>
298- </div>
299- ` ) ;
151+ return `<div style="display:flex;width:1200px;height:630px;background:${ BRAND . bgGradient } ;padding:60px;box-sizing:border-box;font-family:'Inter',sans-serif"><div style="display:flex;flex-direction:column;width:100%;height:100%;justify-content:space-between">${ logo } <div style="display:flex;flex-direction:column;flex:1;justify-content:center;padding:20px 0"><div style="font-size:${ fontSize } px;font-weight:700;line-height:1.15;color:${ BRAND . text } ;max-width:100%;overflow:hidden">${ title } </div>${ subtitleHtml } </div><div style="display:flex;align-items:center;justify-content:flex-end">${ urlBlock ( ) } </div></div></div>` ;
300152}
0 commit comments