cf-workers-og 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Jilles Soeters
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,305 @@
1
+ # cf-workers-og
2
+
3
+ Generate Open Graph images on Cloudflare Workers with Vite support.
4
+
5
+ A thin wrapper around [@cf-wasm/og](https://github.com/fineshopdesign/cf-wasm) that provides:
6
+
7
+ - Works with both **Vite dev** and **Wrangler dev**
8
+ - Uses modern, maintained WASM dependencies
9
+ - Robust HTML string parsing (using battle-tested libraries)
10
+ - Backwards-compatible API for workers-og users
11
+ - TypeScript support
12
+
13
+ ## Installation
14
+
15
+ ```bash
16
+ npm install cf-workers-og
17
+ # or
18
+ pnpm add cf-workers-og
19
+ ```
20
+
21
+ ## Quick Start
22
+
23
+ ### Basic Usage (JSX)
24
+
25
+ ```tsx
26
+ import { ImageResponse } from "cf-workers-og";
27
+
28
+ export default {
29
+ async fetch(request: Request, env: Env, ctx: ExecutionContext) {
30
+ return ImageResponse.create(
31
+ <div
32
+ style={{
33
+ display: "flex",
34
+ alignItems: "center",
35
+ justifyContent: "center",
36
+ width: "100%",
37
+ height: "100%",
38
+ background: "linear-gradient(135deg, #667eea 0%, #764ba2 100%)",
39
+ color: "white",
40
+ fontSize: 60,
41
+ }}
42
+ >
43
+ Hello World
44
+ </div>,
45
+ { width: 1200, height: 630 }
46
+ );
47
+ },
48
+ };
49
+ ```
50
+
51
+ ### With Google Fonts
52
+
53
+ ```tsx
54
+ import { ImageResponse, GoogleFont, cache } from "cf-workers-og";
55
+
56
+ export default {
57
+ async fetch(request: Request, env: Env, ctx: ExecutionContext) {
58
+ // Required when using GoogleFont
59
+ cache.setExecutionContext(ctx);
60
+
61
+ return ImageResponse.create(
62
+ <div
63
+ style={{
64
+ display: "flex",
65
+ alignItems: "center",
66
+ justifyContent: "center",
67
+ width: "100%",
68
+ height: "100%",
69
+ background: "#000",
70
+ color: "#fff",
71
+ fontFamily: "Inter",
72
+ fontSize: 60,
73
+ }}
74
+ >
75
+ Hello World
76
+ </div>,
77
+ {
78
+ width: 1200,
79
+ height: 630,
80
+ fonts: [new GoogleFont("Inter", { weight: 700 })],
81
+ }
82
+ );
83
+ },
84
+ };
85
+ ```
86
+
87
+ ### HTML String Usage
88
+
89
+ We added this to be backwards-comatible with workers-og, but prefer JSX.
90
+
91
+ ```typescript
92
+ import { ImageResponse, parseHtml } from "cf-workers-og";
93
+
94
+ export default {
95
+ async fetch(request: Request, env: Env, ctx: ExecutionContext) {
96
+ const html = `
97
+ <div style="display: flex; align-items: center; justify-content: center; width: 100%; height: 100%; background: #000; color: #fff;">
98
+ <h1 style="font-size: 60px;">Hello from HTML</h1>
99
+ </div>
100
+ `;
101
+
102
+ return ImageResponse.create(parseHtml(html), {
103
+ width: 1200,
104
+ height: 630,
105
+ });
106
+ },
107
+ };
108
+ ```
109
+
110
+ ## Vite Configuration
111
+
112
+ Add the Cloudflare Vite plugin to your `vite.config.ts`:
113
+
114
+ ```typescript
115
+ import { defineConfig } from "vite";
116
+ import { cloudflare } from "@cloudflare/vite-plugin";
117
+
118
+ export default defineConfig({
119
+ plugins: [cloudflare()],
120
+ });
121
+ ```
122
+
123
+ ## Why Not workers-og?
124
+
125
+ The original [workers-og](https://github.com/syedashar1/workers-og) library has several issues:
126
+
127
+ | Issue | Details |
128
+ | ------------------------- | --------------------------------------------------------------------------- |
129
+ | **Outdated WASM** | Uses yoga-wasm-web 0.3.3 (unmaintained since 2023) and resvg-wasm 2.4.0 |
130
+ | **Console logs** | Debug logs left in production code (`og.ts:16,18,20`) |
131
+ | **Brittle HTML parsing** | Manual JSON string concatenation (`vdomStr +=`) - error-prone |
132
+ | **No Vite support** | Uses esbuild copy loader, incompatible with Vite's WASM handling |
133
+
134
+ ### How cf-workers-og Solves These
135
+
136
+ - **Modern WASM**: Uses `@cf-wasm/og` which is actively maintained (Dec 2025) with up-to-date yoga and resvg
137
+ - **No debug logs**: Clean production code
138
+ - **Robust HTML parsing**: Uses [htmlparser2](https://github.com/fb55/htmlparser2) + [style-to-js](https://www.npmjs.com/package/style-to-js) for proper DOM/CSS parsing (works in Workers, unlike browser-based parsers)
139
+ - **Vite compatible**: Works with `@cloudflare/vite-plugin` out of the box
140
+
141
+ ## API Reference
142
+
143
+ ### `ImageResponse.create(element, options)`
144
+
145
+ Generate an OG image Response.
146
+
147
+ ```typescript
148
+ const response = await ImageResponse.create(element, {
149
+ width: 1200, // Default: 1200
150
+ height: 630, // Default: 630
151
+ format: "png", // 'png' | 'svg', Default: 'png'
152
+ fonts: [], // Font configurations
153
+ emoji: "twemoji", // Emoji provider
154
+ debug: false, // Disable caching for debugging
155
+ headers: {}, // Additional response headers
156
+ status: 200, // HTTP status code
157
+ statusText: "", // HTTP status text
158
+ });
159
+ ```
160
+
161
+ ### `parseHtml(html)`
162
+
163
+ Parse an HTML string into React elements for Satori.
164
+
165
+ ```typescript
166
+ const element = parseHtml('<div style="display: flex;">Hello</div>');
167
+ ```
168
+
169
+ ### `GoogleFont(family, options)`
170
+
171
+ Load a Google Font.
172
+
173
+ ```typescript
174
+ const font = new GoogleFont("Inter", { weight: 700 });
175
+ ```
176
+
177
+ ### `loadGoogleFont(options)` (Deprecated)
178
+
179
+ Backwards-compatible function for loading Google Fonts. Prefer `GoogleFont` class.
180
+
181
+ ```typescript
182
+ const fontData = await loadGoogleFont({ family: "Inter", weight: 700 });
183
+ ```
184
+
185
+ ### `cache.setExecutionContext(ctx)`
186
+
187
+ **Only required when using `GoogleFont`**. Not needed if you use the default font or `CustomFont`.
188
+
189
+ ```typescript
190
+ // Only if using GoogleFont:
191
+ cache.setExecutionContext(ctx);
192
+
193
+ const response = await ImageResponse.create(element, {
194
+ fonts: [new GoogleFont("Inter", { weight: 700 })],
195
+ });
196
+ ```
197
+
198
+ The `GoogleFont` class caches font files using Cloudflare's Cache API to avoid re-fetching on every request. The Cache API requires access to the execution context.
199
+
200
+ **When you DON'T need it:**
201
+ - Using no fonts (default Noto Sans is bundled)
202
+ - Using `CustomFont` with your own font data
203
+
204
+ ## Migrating from workers-og
205
+
206
+ ```diff
207
+ - import { ImageResponse } from 'workers-og';
208
+ + import { ImageResponse, cache } from 'cf-workers-og';
209
+
210
+ export default {
211
+ async fetch(request, env, ctx) {
212
+ + cache.setExecutionContext(ctx);
213
+
214
+ return new ImageResponse(element, options);
215
+ }
216
+ };
217
+ ```
218
+
219
+ For HTML string users:
220
+
221
+ ```diff
222
+ - return new ImageResponse(htmlString, options);
223
+ + import { parseHtml } from 'cf-workers-og';
224
+ + return ImageResponse.create(parseHtml(htmlString), options);
225
+ ```
226
+
227
+ ## Architecture
228
+
229
+ This package is a **thin wrapper** (6 KB) around `@cf-wasm/og`. The heavy lifting is done by:
230
+
231
+ | Package | Size | Purpose |
232
+ |---------|------|---------|
233
+ | `@cf-wasm/resvg` | 2.4 MB | SVG → PNG rendering (WASM) |
234
+ | `@cf-wasm/satori` | 87 KB | Flexbox layout engine (WASM) |
235
+ | `htmlparser2` | ~42 KB | HTML parsing (pure JS) |
236
+
237
+ The WASM files are installed as transitive dependencies - they're not bundled in this package.
238
+
239
+ ## Local Development
240
+
241
+ To test changes locally before publishing:
242
+
243
+ ```bash
244
+ # In cf-workers-og directory
245
+ pnpm build
246
+ pnpm link --global
247
+
248
+ # In your Astro/other project
249
+ pnpm link --global cf-workers-og
250
+ ```
251
+
252
+ Then in your Astro project's API route:
253
+
254
+ ```tsx
255
+ // src/pages/og/[...slug].ts (Astro on Cloudflare)
256
+ import type { APIRoute } from "astro";
257
+ import { ImageResponse } from "cf-workers-og";
258
+
259
+ export const GET: APIRoute = async ({ params }) => {
260
+ // No cache.setExecutionContext needed - using default font
261
+ return ImageResponse.create(
262
+ <div style={{ display: "flex", alignItems: "center", justifyContent: "center", background: "#000", color: "#fff", width: "100%", height: "100%" }}>
263
+ <h1 style={{ fontSize: 60 }}>Hello {params.slug}</h1>
264
+ </div>,
265
+ { width: 1200, height: 630 }
266
+ );
267
+ };
268
+ ```
269
+
270
+ If using Google Fonts:
271
+
272
+ ```tsx
273
+ import { ImageResponse, GoogleFont, cache } from "cf-workers-og";
274
+
275
+ export const GET: APIRoute = async ({ params, locals }) => {
276
+ // Required for GoogleFont caching
277
+ const ctx = (locals as any).runtime?.ctx;
278
+ if (ctx) cache.setExecutionContext(ctx);
279
+
280
+ return ImageResponse.create(
281
+ <div style={{ display: "flex", alignItems: "center", justifyContent: "center", background: "#000", color: "#fff", width: "100%", height: "100%" }}>
282
+ <h1 style={{ fontSize: 60, fontFamily: "Inter" }}>Hello {params.slug}</h1>
283
+ </div>,
284
+ {
285
+ width: 1200,
286
+ height: 630,
287
+ fonts: [new GoogleFont("Inter", { weight: 700 })],
288
+ }
289
+ );
290
+ };
291
+ ```
292
+
293
+ To unlink after testing:
294
+
295
+ ```bash
296
+ # In your Astro project
297
+ pnpm unlink cf-workers-og
298
+
299
+ # In cf-workers-og directory
300
+ pnpm unlink --global
301
+ ```
302
+
303
+ ## License
304
+
305
+ MIT
@@ -0,0 +1,52 @@
1
+ import type { FontWeight, GoogleFontOptions } from "./types";
2
+ export { GoogleFont, CustomFont } from "@cf-wasm/og/workerd";
3
+ /**
4
+ * Load a Google Font and return its data as an ArrayBuffer.
5
+ *
6
+ * This is a backwards-compatible function for users migrating from workers-og.
7
+ * For new code, prefer using `GoogleFont` class from `@cf-wasm/og`.
8
+ *
9
+ * @param options - Font loading options
10
+ * @returns Font data as ArrayBuffer
11
+ *
12
+ * @example
13
+ * ```typescript
14
+ * const fontData = await loadGoogleFont({
15
+ * family: 'Inter',
16
+ * weight: 700,
17
+ * });
18
+ *
19
+ * return ImageResponse.create(element, {
20
+ * fonts: [{
21
+ * name: 'Inter',
22
+ * data: fontData,
23
+ * weight: 700,
24
+ * style: 'normal',
25
+ * }],
26
+ * });
27
+ * ```
28
+ *
29
+ * @deprecated Use `GoogleFont` class instead for better caching:
30
+ * ```typescript
31
+ * import { GoogleFont, cache } from 'cf-workers-og';
32
+ * cache.setExecutionContext(ctx);
33
+ * const fonts = [new GoogleFont('Inter', { weight: 700 })];
34
+ * ```
35
+ */
36
+ export declare function loadGoogleFont(options: GoogleFontOptions): Promise<ArrayBuffer>;
37
+ /**
38
+ * Create a font configuration object for ImageResponse.
39
+ *
40
+ * Helper function to build the font config with proper types.
41
+ *
42
+ * @param name - Font family name
43
+ * @param data - Font data as ArrayBuffer
44
+ * @param weight - Font weight (optional, defaults to 400)
45
+ * @param style - Font style (optional, defaults to 'normal')
46
+ */
47
+ export declare function createFontConfig(name: string, data: ArrayBuffer, weight?: FontWeight, style?: "normal" | "italic"): {
48
+ name: string;
49
+ data: ArrayBuffer;
50
+ weight: FontWeight;
51
+ style: "normal" | "italic";
52
+ };
@@ -0,0 +1,52 @@
1
+ import type { FontWeight, GoogleFontOptions } from "./types";
2
+ export { GoogleFont, CustomFont } from "@cf-wasm/og/node";
3
+ /**
4
+ * Load a Google Font and return its data as an ArrayBuffer.
5
+ *
6
+ * This is a backwards-compatible function for users migrating from workers-og.
7
+ * For new code, prefer using `GoogleFont` class from `@cf-wasm/og`.
8
+ *
9
+ * @param options - Font loading options
10
+ * @returns Font data as ArrayBuffer
11
+ *
12
+ * @example
13
+ * ```typescript
14
+ * const fontData = await loadGoogleFont({
15
+ * family: 'Inter',
16
+ * weight: 700,
17
+ * });
18
+ *
19
+ * return ImageResponse.create(element, {
20
+ * fonts: [{
21
+ * name: 'Inter',
22
+ * data: fontData,
23
+ * weight: 700,
24
+ * style: 'normal',
25
+ * }],
26
+ * });
27
+ * ```
28
+ *
29
+ * @deprecated Use `GoogleFont` class instead for better caching:
30
+ * ```typescript
31
+ * import { GoogleFont, cache } from 'cf-workers-og';
32
+ * cache.setExecutionContext(ctx);
33
+ * const fonts = [new GoogleFont('Inter', { weight: 700 })];
34
+ * ```
35
+ */
36
+ export declare function loadGoogleFont(options: GoogleFontOptions): Promise<ArrayBuffer>;
37
+ /**
38
+ * Create a font configuration object for ImageResponse.
39
+ *
40
+ * Helper function to build the font config with proper types.
41
+ *
42
+ * @param name - Font family name
43
+ * @param data - Font data as ArrayBuffer
44
+ * @param weight - Font weight (optional, defaults to 400)
45
+ * @param style - Font style (optional, defaults to 'normal')
46
+ */
47
+ export declare function createFontConfig(name: string, data: ArrayBuffer, weight?: FontWeight, style?: "normal" | "italic"): {
48
+ name: string;
49
+ data: ArrayBuffer;
50
+ weight: FontWeight;
51
+ style: "normal" | "italic";
52
+ };
@@ -0,0 +1,80 @@
1
+ import { parseDocument } from "htmlparser2";
2
+ import { createElement } from "react";
3
+ import styleToJS from "style-to-js";
4
+ function parseHtml(html) {
5
+ const wrappedHtml = `<div style="display: flex; flex-direction: column;">${html}</div>`;
6
+ const doc = parseDocument(wrappedHtml);
7
+ const rootNode = doc.childNodes[0];
8
+ if (!rootNode) {
9
+ return null;
10
+ }
11
+ return convertNode(rootNode);
12
+ }
13
+ function convertNode(node) {
14
+ if (node.type === "text") {
15
+ const textNode = node;
16
+ const text = textNode.data.trim();
17
+ return text || null;
18
+ }
19
+ if (node.type === "tag") {
20
+ const element = node;
21
+ const tagName = element.name.toLowerCase();
22
+ const props = {};
23
+ if (element.attribs.style) {
24
+ props.style = styleToJS(element.attribs.style, { reactCompat: true });
25
+ }
26
+ if (element.attribs.class) {
27
+ props.className = element.attribs.class;
28
+ }
29
+ for (const [key, value] of Object.entries(element.attribs)) {
30
+ if (key === "style" || key === "class") continue;
31
+ const reactKey = htmlAttrToReactProp(key);
32
+ props[reactKey] = value;
33
+ }
34
+ if (tagName === "img" && props.src) {
35
+ if (!props.width) {
36
+ console.warn(
37
+ "cf-workers-og: <img> elements should have explicit width for Satori"
38
+ );
39
+ }
40
+ if (!props.height) {
41
+ console.warn(
42
+ "cf-workers-og: <img> elements should have explicit height for Satori"
43
+ );
44
+ }
45
+ }
46
+ const children = (element.children || []).map(convertNode).filter((child) => child !== null);
47
+ return createElement(tagName, props, ...children);
48
+ }
49
+ return null;
50
+ }
51
+ function htmlAttrToReactProp(attr) {
52
+ const mappings = {
53
+ class: "className",
54
+ for: "htmlFor",
55
+ tabindex: "tabIndex",
56
+ readonly: "readOnly",
57
+ maxlength: "maxLength",
58
+ cellspacing: "cellSpacing",
59
+ cellpadding: "cellPadding",
60
+ rowspan: "rowSpan",
61
+ colspan: "colSpan",
62
+ usemap: "useMap",
63
+ frameborder: "frameBorder",
64
+ contenteditable: "contentEditable",
65
+ crossorigin: "crossOrigin",
66
+ srcset: "srcSet",
67
+ srcdoc: "srcDoc"
68
+ };
69
+ if (mappings[attr]) {
70
+ return mappings[attr];
71
+ }
72
+ if (attr.startsWith("data-") || attr.startsWith("aria-")) {
73
+ return attr;
74
+ }
75
+ return attr.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
76
+ }
77
+ export {
78
+ parseHtml as p
79
+ };
80
+ //# sourceMappingURL=html-parser-DRzlsDtB.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"html-parser-DRzlsDtB.js","sources":["../src/html-parser.ts"],"sourcesContent":["import { parseDocument } from \"htmlparser2\";\nimport { createElement, type ReactNode } from \"react\";\nimport styleToJS from \"style-to-js\";\n\n/**\n * Parse an HTML string into React elements compatible with Satori.\n *\n * This uses htmlparser2 for server-side DOM parsing (works in Cloudflare Workers),\n * and transforms HTML attributes to React props (style strings to objects, etc.)\n *\n * @param html - HTML string to parse\n * @returns React node tree compatible with Satori\n *\n * @example\n * ```typescript\n * const element = parseHtml('<div style=\"display: flex; color: white;\">Hello</div>');\n * return ImageResponse.create(element, { width: 1200, height: 630 });\n * ```\n */\nexport function parseHtml(html: string): ReactNode {\n // Wrap in a flex container to ensure single root and proper layout\n const wrappedHtml = `<div style=\"display: flex; flex-direction: column;\">${html}</div>`;\n\n // Parse HTML to DOM tree\n const doc = parseDocument(wrappedHtml);\n\n // Convert first child (our wrapper div)\n const rootNode = doc.childNodes[0];\n if (!rootNode) {\n return null;\n }\n\n return convertNode(rootNode);\n}\n\n/**\n * Convert a DOM node to a React element\n */\nfunction convertNode(node: Node): ReactNode {\n // Text node\n if (node.type === \"text\") {\n const textNode = node as TextNode;\n const text = textNode.data.trim();\n return text || null;\n }\n\n // Element node\n if (node.type === \"tag\") {\n const element = node as ElementNode;\n const tagName = element.name.toLowerCase();\n\n // Build React props from HTML attributes\n const props: Record<string, unknown> = {};\n\n // Convert style string to object using style-to-js\n if (element.attribs.style) {\n props.style = styleToJS(element.attribs.style, { reactCompat: true });\n }\n\n // Map class to className\n if (element.attribs.class) {\n props.className = element.attribs.class;\n }\n\n // Copy other attributes, converting to React naming conventions\n for (const [key, value] of Object.entries(element.attribs)) {\n if (key === \"style\" || key === \"class\") continue;\n\n // Convert HTML attribute names to React prop names\n const reactKey = htmlAttrToReactProp(key);\n props[reactKey] = value;\n }\n\n // Handle image src specially - Satori requires width/height\n if (tagName === \"img\" && props.src) {\n if (!props.width) {\n console.warn(\n \"cf-workers-og: <img> elements should have explicit width for Satori\"\n );\n }\n if (!props.height) {\n console.warn(\n \"cf-workers-og: <img> elements should have explicit height for Satori\"\n );\n }\n }\n\n // Recursively convert children\n const children = (element.children || [])\n .map(convertNode)\n .filter((child): child is ReactNode => child !== null);\n\n return createElement(tagName, props, ...children);\n }\n\n // Other node types (comments, etc.) - skip\n return null;\n}\n\n/**\n * Convert HTML attribute name to React prop name\n */\nfunction htmlAttrToReactProp(attr: string): string {\n // Common mappings\n const mappings: Record<string, string> = {\n class: \"className\",\n for: \"htmlFor\",\n tabindex: \"tabIndex\",\n readonly: \"readOnly\",\n maxlength: \"maxLength\",\n cellspacing: \"cellSpacing\",\n cellpadding: \"cellPadding\",\n rowspan: \"rowSpan\",\n colspan: \"colSpan\",\n usemap: \"useMap\",\n frameborder: \"frameBorder\",\n contenteditable: \"contentEditable\",\n crossorigin: \"crossOrigin\",\n srcset: \"srcSet\",\n srcdoc: \"srcDoc\",\n };\n\n if (mappings[attr]) {\n return mappings[attr];\n }\n\n // Convert data-* and aria-* attributes (keep as-is in React)\n if (attr.startsWith(\"data-\") || attr.startsWith(\"aria-\")) {\n return attr;\n }\n\n // Convert kebab-case to camelCase for other attributes\n return attr.replace(/-([a-z])/g, (_, c: string) => c.toUpperCase());\n}\n\n// Type definitions for htmlparser2 nodes\ninterface Node {\n type: string;\n}\n\ninterface TextNode extends Node {\n type: \"text\";\n data: string;\n}\n\ninterface ElementNode extends Node {\n type: \"tag\";\n name: string;\n attribs: Record<string, string>;\n children: Node[];\n}\n"],"names":[],"mappings":";;;AAmBO,SAAS,UAAU,MAAyB;AAEjD,QAAM,cAAc,uDAAuD,IAAI;AAG/E,QAAM,MAAM,cAAc,WAAW;AAGrC,QAAM,WAAW,IAAI,WAAW,CAAC;AACjC,MAAI,CAAC,UAAU;AACb,WAAO;AAAA,EACT;AAEA,SAAO,YAAY,QAAQ;AAC7B;AAKA,SAAS,YAAY,MAAuB;AAE1C,MAAI,KAAK,SAAS,QAAQ;AACxB,UAAM,WAAW;AACjB,UAAM,OAAO,SAAS,KAAK,KAAA;AAC3B,WAAO,QAAQ;AAAA,EACjB;AAGA,MAAI,KAAK,SAAS,OAAO;AACvB,UAAM,UAAU;AAChB,UAAM,UAAU,QAAQ,KAAK,YAAA;AAG7B,UAAM,QAAiC,CAAA;AAGvC,QAAI,QAAQ,QAAQ,OAAO;AACzB,YAAM,QAAQ,UAAU,QAAQ,QAAQ,OAAO,EAAE,aAAa,MAAM;AAAA,IACtE;AAGA,QAAI,QAAQ,QAAQ,OAAO;AACzB,YAAM,YAAY,QAAQ,QAAQ;AAAA,IACpC;AAGA,eAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,QAAQ,OAAO,GAAG;AAC1D,UAAI,QAAQ,WAAW,QAAQ,QAAS;AAGxC,YAAM,WAAW,oBAAoB,GAAG;AACxC,YAAM,QAAQ,IAAI;AAAA,IACpB;AAGA,QAAI,YAAY,SAAS,MAAM,KAAK;AAClC,UAAI,CAAC,MAAM,OAAO;AAChB,gBAAQ;AAAA,UACN;AAAA,QAAA;AAAA,MAEJ;AACA,UAAI,CAAC,MAAM,QAAQ;AACjB,gBAAQ;AAAA,UACN;AAAA,QAAA;AAAA,MAEJ;AAAA,IACF;AAGA,UAAM,YAAY,QAAQ,YAAY,CAAA,GACnC,IAAI,WAAW,EACf,OAAO,CAAC,UAA8B,UAAU,IAAI;AAEvD,WAAO,cAAc,SAAS,OAAO,GAAG,QAAQ;AAAA,EAClD;AAGA,SAAO;AACT;AAKA,SAAS,oBAAoB,MAAsB;AAEjD,QAAM,WAAmC;AAAA,IACvC,OAAO;AAAA,IACP,KAAK;AAAA,IACL,UAAU;AAAA,IACV,UAAU;AAAA,IACV,WAAW;AAAA,IACX,aAAa;AAAA,IACb,aAAa;AAAA,IACb,SAAS;AAAA,IACT,SAAS;AAAA,IACT,QAAQ;AAAA,IACR,aAAa;AAAA,IACb,iBAAiB;AAAA,IACjB,aAAa;AAAA,IACb,QAAQ;AAAA,IACR,QAAQ;AAAA,EAAA;AAGV,MAAI,SAAS,IAAI,GAAG;AAClB,WAAO,SAAS,IAAI;AAAA,EACtB;AAGA,MAAI,KAAK,WAAW,OAAO,KAAK,KAAK,WAAW,OAAO,GAAG;AACxD,WAAO;AAAA,EACT;AAGA,SAAO,KAAK,QAAQ,aAAa,CAAC,GAAG,MAAc,EAAE,aAAa;AACpE;"}
@@ -0,0 +1,17 @@
1
+ import { type ReactNode } from "react";
2
+ /**
3
+ * Parse an HTML string into React elements compatible with Satori.
4
+ *
5
+ * This uses htmlparser2 for server-side DOM parsing (works in Cloudflare Workers),
6
+ * and transforms HTML attributes to React props (style strings to objects, etc.)
7
+ *
8
+ * @param html - HTML string to parse
9
+ * @returns React node tree compatible with Satori
10
+ *
11
+ * @example
12
+ * ```typescript
13
+ * const element = parseHtml('<div style="display: flex; color: white;">Hello</div>');
14
+ * return ImageResponse.create(element, { width: 1200, height: 630 });
15
+ * ```
16
+ */
17
+ export declare function parseHtml(html: string): ReactNode;
@@ -0,0 +1,64 @@
1
+ import type { ReactNode } from "react";
2
+ import type { ImageResponseOptions } from "./types";
3
+ export { cache } from "@cf-wasm/og/workerd";
4
+ /**
5
+ * Generate an OG image Response from a React element or HTML string.
6
+ *
7
+ * This is a wrapper around @cf-wasm/og that provides:
8
+ * - Backwards compatibility with workers-og API
9
+ * - HTML string parsing support
10
+ * - Simplified options interface
11
+ *
12
+ * @example JSX usage (recommended):
13
+ * ```tsx
14
+ * import { ImageResponse, cache } from 'cf-workers-og';
15
+ *
16
+ * export default {
17
+ * async fetch(request, env, ctx) {
18
+ * cache.setExecutionContext(ctx);
19
+ *
20
+ * return ImageResponse.create(
21
+ * <div style={{ display: 'flex', background: '#000' }}>
22
+ * <h1 style={{ color: 'white' }}>Hello World</h1>
23
+ * </div>,
24
+ * { width: 1200, height: 630 }
25
+ * );
26
+ * }
27
+ * };
28
+ * ```
29
+ *
30
+ * @example HTML string usage:
31
+ * ```typescript
32
+ * import { ImageResponse, parseHtml } from 'cf-workers-og';
33
+ *
34
+ * const html = '<div style="display: flex;"><h1>Hello</h1></div>';
35
+ * return ImageResponse.create(parseHtml(html), options);
36
+ * ```
37
+ */
38
+ export declare class ImageResponse extends Response {
39
+ /**
40
+ * Create an OG image Response (async, recommended).
41
+ *
42
+ * @param element - React element or HTML string to render
43
+ * @param options - Image generation options
44
+ * @returns Promise<Response> with the generated image
45
+ */
46
+ static create(element: ReactNode | string, options?: ImageResponseOptions): Promise<Response>;
47
+ /**
48
+ * Constructor for backwards compatibility with workers-og.
49
+ *
50
+ * Note: This returns a Promise, not an ImageResponse instance.
51
+ * For TypeScript, use `ImageResponse.create()` instead.
52
+ *
53
+ * @param element - React element or HTML string to render
54
+ * @param options - Image generation options
55
+ * @returns Response (via Promise trick for constructor)
56
+ *
57
+ * @example
58
+ * ```typescript
59
+ * // Works like old workers-og
60
+ * return new ImageResponse(element, options);
61
+ * ```
62
+ */
63
+ constructor(element: ReactNode | string, options?: ImageResponseOptions);
64
+ }
@@ -0,0 +1,64 @@
1
+ import type { ReactNode } from "react";
2
+ import type { ImageResponseOptions } from "./types";
3
+ export { cache } from "@cf-wasm/og/node";
4
+ /**
5
+ * Generate an OG image Response from a React element or HTML string.
6
+ *
7
+ * This is a wrapper around @cf-wasm/og that provides:
8
+ * - Backwards compatibility with workers-og API
9
+ * - HTML string parsing support
10
+ * - Simplified options interface
11
+ *
12
+ * @example JSX usage (recommended):
13
+ * ```tsx
14
+ * import { ImageResponse, cache } from 'cf-workers-og';
15
+ *
16
+ * export default {
17
+ * async fetch(request, env, ctx) {
18
+ * cache.setExecutionContext(ctx);
19
+ *
20
+ * return ImageResponse.create(
21
+ * <div style={{ display: 'flex', background: '#000' }}>
22
+ * <h1 style={{ color: 'white' }}>Hello World</h1>
23
+ * </div>,
24
+ * { width: 1200, height: 630 }
25
+ * );
26
+ * }
27
+ * };
28
+ * ```
29
+ *
30
+ * @example HTML string usage:
31
+ * ```typescript
32
+ * import { ImageResponse, parseHtml } from 'cf-workers-og';
33
+ *
34
+ * const html = '<div style="display: flex;"><h1>Hello</h1></div>';
35
+ * return ImageResponse.create(parseHtml(html), options);
36
+ * ```
37
+ */
38
+ export declare class ImageResponse extends Response {
39
+ /**
40
+ * Create an OG image Response (async, recommended).
41
+ *
42
+ * @param element - React element or HTML string to render
43
+ * @param options - Image generation options
44
+ * @returns Promise<Response> with the generated image
45
+ */
46
+ static create(element: ReactNode | string, options?: ImageResponseOptions): Promise<Response>;
47
+ /**
48
+ * Constructor for backwards compatibility with workers-og.
49
+ *
50
+ * Note: This returns a Promise, not an ImageResponse instance.
51
+ * For TypeScript, use `ImageResponse.create()` instead.
52
+ *
53
+ * @param element - React element or HTML string to render
54
+ * @param options - Image generation options
55
+ * @returns Response (via Promise trick for constructor)
56
+ *
57
+ * @example
58
+ * ```typescript
59
+ * // Works like old workers-og
60
+ * return new ImageResponse(element, options);
61
+ * ```
62
+ */
63
+ constructor(element: ReactNode | string, options?: ImageResponseOptions);
64
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * cf-workers-og
3
+ *
4
+ * Generate Open Graph images on Cloudflare Workers with Vite support.
5
+ *
6
+ * @example
7
+ * ```tsx
8
+ * import { ImageResponse, GoogleFont, cache } from 'cf-workers-og';
9
+ *
10
+ * export default {
11
+ * async fetch(request, env, ctx) {
12
+ * cache.setExecutionContext(ctx);
13
+ *
14
+ * return ImageResponse.create(
15
+ * <div style={{ display: 'flex', background: '#000', color: '#fff' }}>
16
+ * <h1>Hello World</h1>
17
+ * </div>,
18
+ * {
19
+ * width: 1200,
20
+ * height: 630,
21
+ * fonts: [new GoogleFont('Inter', { weight: 700 })],
22
+ * }
23
+ * );
24
+ * }
25
+ * };
26
+ * ```
27
+ *
28
+ * @packageDocumentation
29
+ */
30
+ export { ImageResponse, cache } from "./image-response";
31
+ export { parseHtml } from "./html-parser";
32
+ export { GoogleFont, CustomFont, loadGoogleFont, createFontConfig } from "./fonts";
33
+ export type { ImageResponseOptions, FontConfig, FontWeight, FontStyle, EmojiType, GoogleFontOptions, } from "./types";
package/dist/index.js ADDED
@@ -0,0 +1,131 @@
1
+ import { ImageResponse as ImageResponse$1 } from "@cf-wasm/og/workerd";
2
+ import { CustomFont, GoogleFont, cache } from "@cf-wasm/og/workerd";
3
+ import { p as parseHtml } from "./html-parser-DRzlsDtB.js";
4
+ class ImageResponse extends Response {
5
+ /**
6
+ * Create an OG image Response (async, recommended).
7
+ *
8
+ * @param element - React element or HTML string to render
9
+ * @param options - Image generation options
10
+ * @returns Promise<Response> with the generated image
11
+ */
12
+ static async create(element, options = {}) {
13
+ const reactElement = typeof element === "string" ? parseHtml(element) : element;
14
+ const {
15
+ width = 1200,
16
+ height = 630,
17
+ format = "png",
18
+ fonts,
19
+ emoji,
20
+ debug = false,
21
+ headers = {},
22
+ status = 200,
23
+ statusText
24
+ } = options;
25
+ const response = await ImageResponse$1.async(reactElement, {
26
+ width,
27
+ height,
28
+ format,
29
+ fonts,
30
+ emoji
31
+ });
32
+ const responseHeaders = new Headers(response.headers);
33
+ responseHeaders.set(
34
+ "Content-Type",
35
+ format === "svg" ? "image/svg+xml" : "image/png"
36
+ );
37
+ responseHeaders.set(
38
+ "Cache-Control",
39
+ debug ? "no-cache, no-store" : "public, immutable, no-transform, max-age=31536000"
40
+ );
41
+ for (const [key, value] of Object.entries(headers)) {
42
+ responseHeaders.set(key, value);
43
+ }
44
+ return new Response(response.body, {
45
+ headers: responseHeaders,
46
+ status,
47
+ statusText
48
+ });
49
+ }
50
+ /**
51
+ * Constructor for backwards compatibility with workers-og.
52
+ *
53
+ * Note: This returns a Promise, not an ImageResponse instance.
54
+ * For TypeScript, use `ImageResponse.create()` instead.
55
+ *
56
+ * @param element - React element or HTML string to render
57
+ * @param options - Image generation options
58
+ * @returns Response (via Promise trick for constructor)
59
+ *
60
+ * @example
61
+ * ```typescript
62
+ * // Works like old workers-og
63
+ * return new ImageResponse(element, options);
64
+ * ```
65
+ */
66
+ constructor(element, options = {}) {
67
+ super(null);
68
+ return ImageResponse.create(element, options);
69
+ }
70
+ }
71
+ async function loadGoogleFont(options) {
72
+ const { family, weight, text } = options;
73
+ const params = {
74
+ family: `${encodeURIComponent(family)}${weight ? `:wght@${weight}` : ""}`
75
+ };
76
+ if (text) {
77
+ params.text = text;
78
+ } else {
79
+ params.subset = "latin";
80
+ }
81
+ const cssUrl = `https://fonts.googleapis.com/css2?${Object.keys(params).map((key) => `${key}=${params[key]}`).join("&")}`;
82
+ const cfCache = typeof caches !== "undefined" ? caches.default : void 0;
83
+ let cssResponse;
84
+ if (cfCache) {
85
+ cssResponse = await cfCache.match(cssUrl);
86
+ }
87
+ if (!cssResponse) {
88
+ cssResponse = await fetch(cssUrl, {
89
+ headers: {
90
+ // Request TTF format (works better with Satori)
91
+ "User-Agent": "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_8; de-at) AppleWebKit/533.21.1 (KHTML, like Gecko) Version/5.0.5 Safari/533.21.1"
92
+ }
93
+ });
94
+ if (cfCache) {
95
+ const cacheResponse = new Response(cssResponse.body, cssResponse);
96
+ cacheResponse.headers.set("Cache-Control", "s-maxage=3600");
97
+ await cfCache.put(cssUrl, cacheResponse.clone());
98
+ cssResponse = cacheResponse;
99
+ }
100
+ }
101
+ const css = await cssResponse.text();
102
+ const fontUrlMatch = css.match(
103
+ /src: url\(([^)]+)\) format\(['"]?(opentype|truetype)['"]?\)/
104
+ );
105
+ if (!fontUrlMatch?.[1]) {
106
+ throw new Error(
107
+ `Could not find font URL for "${family}" (weight: ${weight ?? "default"})`
108
+ );
109
+ }
110
+ const fontUrl = fontUrlMatch[1];
111
+ const fontResponse = await fetch(fontUrl);
112
+ return fontResponse.arrayBuffer();
113
+ }
114
+ function createFontConfig(name, data, weight = 400, style = "normal") {
115
+ return {
116
+ name,
117
+ data,
118
+ weight,
119
+ style
120
+ };
121
+ }
122
+ export {
123
+ CustomFont,
124
+ GoogleFont,
125
+ ImageResponse,
126
+ cache,
127
+ createFontConfig,
128
+ loadGoogleFont,
129
+ parseHtml
130
+ };
131
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sources":["../src/image-response.ts","../src/fonts.ts"],"sourcesContent":["import { ImageResponse as CfImageResponse } from \"@cf-wasm/og/workerd\";\nimport type { ReactNode } from \"react\";\nimport { parseHtml } from \"./html-parser\";\nimport type { ImageResponseOptions } from \"./types\";\n\n// Re-export cache from @cf-wasm/og for font caching\nexport { cache } from \"@cf-wasm/og/workerd\";\n\n/**\n * Generate an OG image Response from a React element or HTML string.\n *\n * This is a wrapper around @cf-wasm/og that provides:\n * - Backwards compatibility with workers-og API\n * - HTML string parsing support\n * - Simplified options interface\n *\n * @example JSX usage (recommended):\n * ```tsx\n * import { ImageResponse, cache } from 'cf-workers-og';\n *\n * export default {\n * async fetch(request, env, ctx) {\n * cache.setExecutionContext(ctx);\n *\n * return ImageResponse.create(\n * <div style={{ display: 'flex', background: '#000' }}>\n * <h1 style={{ color: 'white' }}>Hello World</h1>\n * </div>,\n * { width: 1200, height: 630 }\n * );\n * }\n * };\n * ```\n *\n * @example HTML string usage:\n * ```typescript\n * import { ImageResponse, parseHtml } from 'cf-workers-og';\n *\n * const html = '<div style=\"display: flex;\"><h1>Hello</h1></div>';\n * return ImageResponse.create(parseHtml(html), options);\n * ```\n */\nexport class ImageResponse extends Response {\n /**\n * Create an OG image Response (async, recommended).\n *\n * @param element - React element or HTML string to render\n * @param options - Image generation options\n * @returns Promise<Response> with the generated image\n */\n static async create(\n element: ReactNode | string,\n options: ImageResponseOptions = {}\n ): Promise<Response> {\n // Parse HTML strings\n const reactElement =\n typeof element === \"string\" ? parseHtml(element) : element;\n\n const {\n width = 1200,\n height = 630,\n format = \"png\",\n fonts,\n emoji,\n debug = false,\n headers = {},\n status = 200,\n statusText,\n } = options;\n\n // Use @cf-wasm/og to generate the image\n const response = await CfImageResponse.async(reactElement, {\n width,\n height,\n format,\n fonts,\n emoji,\n });\n\n // Build response headers\n const responseHeaders = new Headers(response.headers);\n\n // Set content type\n responseHeaders.set(\n \"Content-Type\",\n format === \"svg\" ? \"image/svg+xml\" : \"image/png\"\n );\n\n // Set cache headers\n responseHeaders.set(\n \"Cache-Control\",\n debug\n ? \"no-cache, no-store\"\n : \"public, immutable, no-transform, max-age=31536000\"\n );\n\n // Apply custom headers\n for (const [key, value] of Object.entries(headers)) {\n responseHeaders.set(key, value);\n }\n\n return new Response(response.body, {\n headers: responseHeaders,\n status,\n statusText,\n });\n }\n\n /**\n * Constructor for backwards compatibility with workers-og.\n *\n * Note: This returns a Promise, not an ImageResponse instance.\n * For TypeScript, use `ImageResponse.create()` instead.\n *\n * @param element - React element or HTML string to render\n * @param options - Image generation options\n * @returns Response (via Promise trick for constructor)\n *\n * @example\n * ```typescript\n * // Works like old workers-og\n * return new ImageResponse(element, options);\n * ```\n */\n constructor(element: ReactNode | string, options: ImageResponseOptions = {}) {\n // Must call super() since we extend Response\n super(null);\n // Return a Promise from the constructor (workers-og pattern)\n // This hack allows `new ImageResponse()` to work like workers-og\n return ImageResponse.create(element, options) as unknown as ImageResponse;\n }\n}\n","import type { FontWeight, GoogleFontOptions } from \"./types\";\n\n// Re-export GoogleFont from @cf-wasm/og for the new API\nexport { GoogleFont, CustomFont } from \"@cf-wasm/og/workerd\";\n\n/**\n * Load a Google Font and return its data as an ArrayBuffer.\n *\n * This is a backwards-compatible function for users migrating from workers-og.\n * For new code, prefer using `GoogleFont` class from `@cf-wasm/og`.\n *\n * @param options - Font loading options\n * @returns Font data as ArrayBuffer\n *\n * @example\n * ```typescript\n * const fontData = await loadGoogleFont({\n * family: 'Inter',\n * weight: 700,\n * });\n *\n * return ImageResponse.create(element, {\n * fonts: [{\n * name: 'Inter',\n * data: fontData,\n * weight: 700,\n * style: 'normal',\n * }],\n * });\n * ```\n *\n * @deprecated Use `GoogleFont` class instead for better caching:\n * ```typescript\n * import { GoogleFont, cache } from 'cf-workers-og';\n * cache.setExecutionContext(ctx);\n * const fonts = [new GoogleFont('Inter', { weight: 700 })];\n * ```\n */\nexport async function loadGoogleFont(\n options: GoogleFontOptions\n): Promise<ArrayBuffer> {\n const { family, weight, text } = options;\n\n // Build Google Fonts CSS URL\n const params: Record<string, string> = {\n family: `${encodeURIComponent(family)}${weight ? `:wght@${weight}` : \"\"}`,\n };\n\n if (text) {\n params.text = text;\n } else {\n params.subset = \"latin\";\n }\n\n const cssUrl = `https://fonts.googleapis.com/css2?${Object.keys(params)\n .map((key) => `${key}=${params[key]}`)\n .join(\"&\")}`;\n\n // Try to use Cloudflare's cache\n const cfCache =\n typeof caches !== \"undefined\"\n ? (caches as unknown as { default: Cache }).default\n : undefined;\n\n let cssResponse: Response | undefined;\n\n if (cfCache) {\n cssResponse = await cfCache.match(cssUrl);\n }\n\n if (!cssResponse) {\n cssResponse = await fetch(cssUrl, {\n headers: {\n // Request TTF format (works better with Satori)\n \"User-Agent\":\n \"Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_8; de-at) AppleWebKit/533.21.1 (KHTML, like Gecko) Version/5.0.5 Safari/533.21.1\",\n },\n });\n\n if (cfCache) {\n // Clone and add cache headers\n const cacheResponse = new Response(cssResponse.body, cssResponse);\n cacheResponse.headers.set(\"Cache-Control\", \"s-maxage=3600\");\n await cfCache.put(cssUrl, cacheResponse.clone());\n cssResponse = cacheResponse;\n }\n }\n\n const css = await cssResponse.text();\n\n // Extract font URL from CSS\n const fontUrlMatch = css.match(\n /src: url\\(([^)]+)\\) format\\(['\"]?(opentype|truetype)['\"]?\\)/\n );\n\n if (!fontUrlMatch?.[1]) {\n throw new Error(\n `Could not find font URL for \"${family}\" (weight: ${weight ?? \"default\"})`\n );\n }\n\n const fontUrl = fontUrlMatch[1];\n\n // Fetch the actual font file\n const fontResponse = await fetch(fontUrl);\n return fontResponse.arrayBuffer();\n}\n\n/**\n * Create a font configuration object for ImageResponse.\n *\n * Helper function to build the font config with proper types.\n *\n * @param name - Font family name\n * @param data - Font data as ArrayBuffer\n * @param weight - Font weight (optional, defaults to 400)\n * @param style - Font style (optional, defaults to 'normal')\n */\nexport function createFontConfig(\n name: string,\n data: ArrayBuffer,\n weight: FontWeight = 400,\n style: \"normal\" | \"italic\" = \"normal\"\n) {\n return {\n name,\n data,\n weight,\n style,\n };\n}\n"],"names":["CfImageResponse"],"mappings":";;;AA0CO,MAAM,sBAAsB,SAAS;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQ1C,aAAa,OACX,SACA,UAAgC,IACb;AAEnB,UAAM,eACJ,OAAO,YAAY,WAAW,UAAU,OAAO,IAAI;AAErD,UAAM;AAAA,MACJ,QAAQ;AAAA,MACR,SAAS;AAAA,MACT,SAAS;AAAA,MACT;AAAA,MACA;AAAA,MACA,QAAQ;AAAA,MACR,UAAU,CAAA;AAAA,MACV,SAAS;AAAA,MACT;AAAA,IAAA,IACE;AAGJ,UAAM,WAAW,MAAMA,gBAAgB,MAAM,cAAc;AAAA,MACzD;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IAAA,CACD;AAGD,UAAM,kBAAkB,IAAI,QAAQ,SAAS,OAAO;AAGpD,oBAAgB;AAAA,MACd;AAAA,MACA,WAAW,QAAQ,kBAAkB;AAAA,IAAA;AAIvC,oBAAgB;AAAA,MACd;AAAA,MACA,QACI,uBACA;AAAA,IAAA;AAIN,eAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,OAAO,GAAG;AAClD,sBAAgB,IAAI,KAAK,KAAK;AAAA,IAChC;AAEA,WAAO,IAAI,SAAS,SAAS,MAAM;AAAA,MACjC,SAAS;AAAA,MACT;AAAA,MACA;AAAA,IAAA,CACD;AAAA,EACH;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAkBA,YAAY,SAA6B,UAAgC,IAAI;AAE3E,UAAM,IAAI;AAGV,WAAO,cAAc,OAAO,SAAS,OAAO;AAAA,EAC9C;AACF;AC7FA,eAAsB,eACpB,SACsB;AACtB,QAAM,EAAE,QAAQ,QAAQ,KAAA,IAAS;AAGjC,QAAM,SAAiC;AAAA,IACrC,QAAQ,GAAG,mBAAmB,MAAM,CAAC,GAAG,SAAS,SAAS,MAAM,KAAK,EAAE;AAAA,EAAA;AAGzE,MAAI,MAAM;AACR,WAAO,OAAO;AAAA,EAChB,OAAO;AACL,WAAO,SAAS;AAAA,EAClB;AAEA,QAAM,SAAS,qCAAqC,OAAO,KAAK,MAAM,EACnE,IAAI,CAAC,QAAQ,GAAG,GAAG,IAAI,OAAO,GAAG,CAAC,EAAE,EACpC,KAAK,GAAG,CAAC;AAGZ,QAAM,UACJ,OAAO,WAAW,cACb,OAAyC,UAC1C;AAEN,MAAI;AAEJ,MAAI,SAAS;AACX,kBAAc,MAAM,QAAQ,MAAM,MAAM;AAAA,EAC1C;AAEA,MAAI,CAAC,aAAa;AAChB,kBAAc,MAAM,MAAM,QAAQ;AAAA,MAChC,SAAS;AAAA;AAAA,QAEP,cACE;AAAA,MAAA;AAAA,IACJ,CACD;AAED,QAAI,SAAS;AAEX,YAAM,gBAAgB,IAAI,SAAS,YAAY,MAAM,WAAW;AAChE,oBAAc,QAAQ,IAAI,iBAAiB,eAAe;AAC1D,YAAM,QAAQ,IAAI,QAAQ,cAAc,OAAO;AAC/C,oBAAc;AAAA,IAChB;AAAA,EACF;AAEA,QAAM,MAAM,MAAM,YAAY,KAAA;AAG9B,QAAM,eAAe,IAAI;AAAA,IACvB;AAAA,EAAA;AAGF,MAAI,CAAC,eAAe,CAAC,GAAG;AACtB,UAAM,IAAI;AAAA,MACR,gCAAgC,MAAM,cAAc,UAAU,SAAS;AAAA,IAAA;AAAA,EAE3E;AAEA,QAAM,UAAU,aAAa,CAAC;AAG9B,QAAM,eAAe,MAAM,MAAM,OAAO;AACxC,SAAO,aAAa,YAAA;AACtB;AAYO,SAAS,iBACd,MACA,MACA,SAAqB,KACrB,QAA6B,UAC7B;AACA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EAAA;AAEJ;"}
@@ -0,0 +1,12 @@
1
+ /**
2
+ * cf-workers-og (Node.js entry point)
3
+ *
4
+ * This entry point is used when running in Node.js environments (e.g., Astro dev server).
5
+ * It uses @cf-wasm/og/node which has fonts pre-inlined, avoiding .bin file imports.
6
+ *
7
+ * @packageDocumentation
8
+ */
9
+ export { ImageResponse, cache } from "./image-response.node";
10
+ export { parseHtml } from "./html-parser";
11
+ export { GoogleFont, CustomFont, loadGoogleFont, createFontConfig } from "./fonts.node";
12
+ export type { ImageResponseOptions, FontConfig, FontWeight, FontStyle, EmojiType, GoogleFontOptions, } from "./types";
@@ -0,0 +1,131 @@
1
+ import { ImageResponse as ImageResponse$1 } from "@cf-wasm/og/node";
2
+ import { CustomFont, GoogleFont, cache } from "@cf-wasm/og/node";
3
+ import { p as parseHtml } from "./html-parser-DRzlsDtB.js";
4
+ class ImageResponse extends Response {
5
+ /**
6
+ * Create an OG image Response (async, recommended).
7
+ *
8
+ * @param element - React element or HTML string to render
9
+ * @param options - Image generation options
10
+ * @returns Promise<Response> with the generated image
11
+ */
12
+ static async create(element, options = {}) {
13
+ const reactElement = typeof element === "string" ? parseHtml(element) : element;
14
+ const {
15
+ width = 1200,
16
+ height = 630,
17
+ format = "png",
18
+ fonts,
19
+ emoji,
20
+ debug = false,
21
+ headers = {},
22
+ status = 200,
23
+ statusText
24
+ } = options;
25
+ const response = await ImageResponse$1.async(reactElement, {
26
+ width,
27
+ height,
28
+ format,
29
+ fonts: fonts ?? [],
30
+ emoji
31
+ });
32
+ const responseHeaders = new Headers(response.headers);
33
+ responseHeaders.set(
34
+ "Content-Type",
35
+ format === "svg" ? "image/svg+xml" : "image/png"
36
+ );
37
+ responseHeaders.set(
38
+ "Cache-Control",
39
+ debug ? "no-cache, no-store" : "public, immutable, no-transform, max-age=31536000"
40
+ );
41
+ for (const [key, value] of Object.entries(headers)) {
42
+ responseHeaders.set(key, value);
43
+ }
44
+ return new Response(response.body, {
45
+ headers: responseHeaders,
46
+ status,
47
+ statusText
48
+ });
49
+ }
50
+ /**
51
+ * Constructor for backwards compatibility with workers-og.
52
+ *
53
+ * Note: This returns a Promise, not an ImageResponse instance.
54
+ * For TypeScript, use `ImageResponse.create()` instead.
55
+ *
56
+ * @param element - React element or HTML string to render
57
+ * @param options - Image generation options
58
+ * @returns Response (via Promise trick for constructor)
59
+ *
60
+ * @example
61
+ * ```typescript
62
+ * // Works like old workers-og
63
+ * return new ImageResponse(element, options);
64
+ * ```
65
+ */
66
+ constructor(element, options = {}) {
67
+ super(null);
68
+ return ImageResponse.create(element, options);
69
+ }
70
+ }
71
+ async function loadGoogleFont(options) {
72
+ const { family, weight, text } = options;
73
+ const params = {
74
+ family: `${encodeURIComponent(family)}${weight ? `:wght@${weight}` : ""}`
75
+ };
76
+ if (text) {
77
+ params.text = text;
78
+ } else {
79
+ params.subset = "latin";
80
+ }
81
+ const cssUrl = `https://fonts.googleapis.com/css2?${Object.keys(params).map((key) => `${key}=${params[key]}`).join("&")}`;
82
+ const cfCache = typeof caches !== "undefined" ? caches.default : void 0;
83
+ let cssResponse;
84
+ if (cfCache) {
85
+ cssResponse = await cfCache.match(cssUrl);
86
+ }
87
+ if (!cssResponse) {
88
+ cssResponse = await fetch(cssUrl, {
89
+ headers: {
90
+ // Request TTF format (works better with Satori)
91
+ "User-Agent": "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_8; de-at) AppleWebKit/533.21.1 (KHTML, like Gecko) Version/5.0.5 Safari/533.21.1"
92
+ }
93
+ });
94
+ if (cfCache) {
95
+ const cacheResponse = new Response(cssResponse.body, cssResponse);
96
+ cacheResponse.headers.set("Cache-Control", "s-maxage=3600");
97
+ await cfCache.put(cssUrl, cacheResponse.clone());
98
+ cssResponse = cacheResponse;
99
+ }
100
+ }
101
+ const css = await cssResponse.text();
102
+ const fontUrlMatch = css.match(
103
+ /src: url\(([^)]+)\) format\(['"]?(opentype|truetype)['"]?\)/
104
+ );
105
+ if (!fontUrlMatch?.[1]) {
106
+ throw new Error(
107
+ `Could not find font URL for "${family}" (weight: ${weight ?? "default"})`
108
+ );
109
+ }
110
+ const fontUrl = fontUrlMatch[1];
111
+ const fontResponse = await fetch(fontUrl);
112
+ return fontResponse.arrayBuffer();
113
+ }
114
+ function createFontConfig(name, data, weight = 400, style = "normal") {
115
+ return {
116
+ name,
117
+ data,
118
+ weight,
119
+ style
120
+ };
121
+ }
122
+ export {
123
+ CustomFont,
124
+ GoogleFont,
125
+ ImageResponse,
126
+ cache,
127
+ createFontConfig,
128
+ loadGoogleFont,
129
+ parseHtml
130
+ };
131
+ //# sourceMappingURL=index.node.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.node.js","sources":["../src/image-response.node.ts","../src/fonts.node.ts"],"sourcesContent":["import { ImageResponse as CfImageResponse } from \"@cf-wasm/og/node\";\nimport type { ReactNode } from \"react\";\nimport { parseHtml } from \"./html-parser\";\nimport type { ImageResponseOptions } from \"./types\";\n\n// Re-export cache from @cf-wasm/og for font caching\nexport { cache } from \"@cf-wasm/og/node\";\n\n/**\n * Generate an OG image Response from a React element or HTML string.\n *\n * This is a wrapper around @cf-wasm/og that provides:\n * - Backwards compatibility with workers-og API\n * - HTML string parsing support\n * - Simplified options interface\n *\n * @example JSX usage (recommended):\n * ```tsx\n * import { ImageResponse, cache } from 'cf-workers-og';\n *\n * export default {\n * async fetch(request, env, ctx) {\n * cache.setExecutionContext(ctx);\n *\n * return ImageResponse.create(\n * <div style={{ display: 'flex', background: '#000' }}>\n * <h1 style={{ color: 'white' }}>Hello World</h1>\n * </div>,\n * { width: 1200, height: 630 }\n * );\n * }\n * };\n * ```\n *\n * @example HTML string usage:\n * ```typescript\n * import { ImageResponse, parseHtml } from 'cf-workers-og';\n *\n * const html = '<div style=\"display: flex;\"><h1>Hello</h1></div>';\n * return ImageResponse.create(parseHtml(html), options);\n * ```\n */\nexport class ImageResponse extends Response {\n /**\n * Create an OG image Response (async, recommended).\n *\n * @param element - React element or HTML string to render\n * @param options - Image generation options\n * @returns Promise<Response> with the generated image\n */\n static async create(\n element: ReactNode | string,\n options: ImageResponseOptions = {}\n ): Promise<Response> {\n // Parse HTML strings\n const reactElement =\n typeof element === \"string\" ? parseHtml(element) : element;\n\n const {\n width = 1200,\n height = 630,\n format = \"png\",\n fonts,\n emoji,\n debug = false,\n headers = {},\n status = 200,\n statusText,\n } = options;\n\n // Use @cf-wasm/og to generate the image\n // Note: @cf-wasm/og/node requires fonts to be an array, not undefined\n const response = await CfImageResponse.async(reactElement, {\n width,\n height,\n format,\n fonts: fonts ?? [],\n emoji,\n });\n\n // Build response headers\n const responseHeaders = new Headers(response.headers);\n\n // Set content type\n responseHeaders.set(\n \"Content-Type\",\n format === \"svg\" ? \"image/svg+xml\" : \"image/png\"\n );\n\n // Set cache headers\n responseHeaders.set(\n \"Cache-Control\",\n debug\n ? \"no-cache, no-store\"\n : \"public, immutable, no-transform, max-age=31536000\"\n );\n\n // Apply custom headers\n for (const [key, value] of Object.entries(headers)) {\n responseHeaders.set(key, value);\n }\n\n return new Response(response.body, {\n headers: responseHeaders,\n status,\n statusText,\n });\n }\n\n /**\n * Constructor for backwards compatibility with workers-og.\n *\n * Note: This returns a Promise, not an ImageResponse instance.\n * For TypeScript, use `ImageResponse.create()` instead.\n *\n * @param element - React element or HTML string to render\n * @param options - Image generation options\n * @returns Response (via Promise trick for constructor)\n *\n * @example\n * ```typescript\n * // Works like old workers-og\n * return new ImageResponse(element, options);\n * ```\n */\n constructor(element: ReactNode | string, options: ImageResponseOptions = {}) {\n // Must call super() since we extend Response\n super(null);\n // Return a Promise from the constructor (workers-og pattern)\n // This hack allows `new ImageResponse()` to work like workers-og\n return ImageResponse.create(element, options) as unknown as ImageResponse;\n }\n}\n","import type { FontWeight, GoogleFontOptions } from \"./types\";\n\n// Re-export GoogleFont from @cf-wasm/og/node for Node.js environments\nexport { GoogleFont, CustomFont } from \"@cf-wasm/og/node\";\n\n/**\n * Load a Google Font and return its data as an ArrayBuffer.\n *\n * This is a backwards-compatible function for users migrating from workers-og.\n * For new code, prefer using `GoogleFont` class from `@cf-wasm/og`.\n *\n * @param options - Font loading options\n * @returns Font data as ArrayBuffer\n *\n * @example\n * ```typescript\n * const fontData = await loadGoogleFont({\n * family: 'Inter',\n * weight: 700,\n * });\n *\n * return ImageResponse.create(element, {\n * fonts: [{\n * name: 'Inter',\n * data: fontData,\n * weight: 700,\n * style: 'normal',\n * }],\n * });\n * ```\n *\n * @deprecated Use `GoogleFont` class instead for better caching:\n * ```typescript\n * import { GoogleFont, cache } from 'cf-workers-og';\n * cache.setExecutionContext(ctx);\n * const fonts = [new GoogleFont('Inter', { weight: 700 })];\n * ```\n */\nexport async function loadGoogleFont(\n options: GoogleFontOptions\n): Promise<ArrayBuffer> {\n const { family, weight, text } = options;\n\n // Build Google Fonts CSS URL\n const params: Record<string, string> = {\n family: `${encodeURIComponent(family)}${weight ? `:wght@${weight}` : \"\"}`,\n };\n\n if (text) {\n params.text = text;\n } else {\n params.subset = \"latin\";\n }\n\n const cssUrl = `https://fonts.googleapis.com/css2?${Object.keys(params)\n .map((key) => `${key}=${params[key]}`)\n .join(\"&\")}`;\n\n // Try to use Cloudflare's cache\n const cfCache =\n typeof caches !== \"undefined\"\n ? (caches as unknown as { default: Cache }).default\n : undefined;\n\n let cssResponse: Response | undefined;\n\n if (cfCache) {\n cssResponse = await cfCache.match(cssUrl);\n }\n\n if (!cssResponse) {\n cssResponse = await fetch(cssUrl, {\n headers: {\n // Request TTF format (works better with Satori)\n \"User-Agent\":\n \"Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_8; de-at) AppleWebKit/533.21.1 (KHTML, like Gecko) Version/5.0.5 Safari/533.21.1\",\n },\n });\n\n if (cfCache) {\n // Clone and add cache headers\n const cacheResponse = new Response(cssResponse.body, cssResponse);\n cacheResponse.headers.set(\"Cache-Control\", \"s-maxage=3600\");\n await cfCache.put(cssUrl, cacheResponse.clone());\n cssResponse = cacheResponse;\n }\n }\n\n const css = await cssResponse.text();\n\n // Extract font URL from CSS\n const fontUrlMatch = css.match(\n /src: url\\(([^)]+)\\) format\\(['\"]?(opentype|truetype)['\"]?\\)/\n );\n\n if (!fontUrlMatch?.[1]) {\n throw new Error(\n `Could not find font URL for \"${family}\" (weight: ${weight ?? \"default\"})`\n );\n }\n\n const fontUrl = fontUrlMatch[1];\n\n // Fetch the actual font file\n const fontResponse = await fetch(fontUrl);\n return fontResponse.arrayBuffer();\n}\n\n/**\n * Create a font configuration object for ImageResponse.\n *\n * Helper function to build the font config with proper types.\n *\n * @param name - Font family name\n * @param data - Font data as ArrayBuffer\n * @param weight - Font weight (optional, defaults to 400)\n * @param style - Font style (optional, defaults to 'normal')\n */\nexport function createFontConfig(\n name: string,\n data: ArrayBuffer,\n weight: FontWeight = 400,\n style: \"normal\" | \"italic\" = \"normal\"\n) {\n return {\n name,\n data,\n weight,\n style,\n };\n}\n"],"names":["CfImageResponse"],"mappings":";;;AA0CO,MAAM,sBAAsB,SAAS;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQ1C,aAAa,OACX,SACA,UAAgC,IACb;AAEnB,UAAM,eACJ,OAAO,YAAY,WAAW,UAAU,OAAO,IAAI;AAErD,UAAM;AAAA,MACJ,QAAQ;AAAA,MACR,SAAS;AAAA,MACT,SAAS;AAAA,MACT;AAAA,MACA;AAAA,MACA,QAAQ;AAAA,MACR,UAAU,CAAA;AAAA,MACV,SAAS;AAAA,MACT;AAAA,IAAA,IACE;AAIJ,UAAM,WAAW,MAAMA,gBAAgB,MAAM,cAAc;AAAA,MACzD;AAAA,MACA;AAAA,MACA;AAAA,MACA,OAAO,SAAS,CAAA;AAAA,MAChB;AAAA,IAAA,CACD;AAGD,UAAM,kBAAkB,IAAI,QAAQ,SAAS,OAAO;AAGpD,oBAAgB;AAAA,MACd;AAAA,MACA,WAAW,QAAQ,kBAAkB;AAAA,IAAA;AAIvC,oBAAgB;AAAA,MACd;AAAA,MACA,QACI,uBACA;AAAA,IAAA;AAIN,eAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,OAAO,GAAG;AAClD,sBAAgB,IAAI,KAAK,KAAK;AAAA,IAChC;AAEA,WAAO,IAAI,SAAS,SAAS,MAAM;AAAA,MACjC,SAAS;AAAA,MACT;AAAA,MACA;AAAA,IAAA,CACD;AAAA,EACH;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAkBA,YAAY,SAA6B,UAAgC,IAAI;AAE3E,UAAM,IAAI;AAGV,WAAO,cAAc,OAAO,SAAS,OAAO;AAAA,EAC9C;AACF;AC9FA,eAAsB,eACpB,SACsB;AACtB,QAAM,EAAE,QAAQ,QAAQ,KAAA,IAAS;AAGjC,QAAM,SAAiC;AAAA,IACrC,QAAQ,GAAG,mBAAmB,MAAM,CAAC,GAAG,SAAS,SAAS,MAAM,KAAK,EAAE;AAAA,EAAA;AAGzE,MAAI,MAAM;AACR,WAAO,OAAO;AAAA,EAChB,OAAO;AACL,WAAO,SAAS;AAAA,EAClB;AAEA,QAAM,SAAS,qCAAqC,OAAO,KAAK,MAAM,EACnE,IAAI,CAAC,QAAQ,GAAG,GAAG,IAAI,OAAO,GAAG,CAAC,EAAE,EACpC,KAAK,GAAG,CAAC;AAGZ,QAAM,UACJ,OAAO,WAAW,cACb,OAAyC,UAC1C;AAEN,MAAI;AAEJ,MAAI,SAAS;AACX,kBAAc,MAAM,QAAQ,MAAM,MAAM;AAAA,EAC1C;AAEA,MAAI,CAAC,aAAa;AAChB,kBAAc,MAAM,MAAM,QAAQ;AAAA,MAChC,SAAS;AAAA;AAAA,QAEP,cACE;AAAA,MAAA;AAAA,IACJ,CACD;AAED,QAAI,SAAS;AAEX,YAAM,gBAAgB,IAAI,SAAS,YAAY,MAAM,WAAW;AAChE,oBAAc,QAAQ,IAAI,iBAAiB,eAAe;AAC1D,YAAM,QAAQ,IAAI,QAAQ,cAAc,OAAO;AAC/C,oBAAc;AAAA,IAChB;AAAA,EACF;AAEA,QAAM,MAAM,MAAM,YAAY,KAAA;AAG9B,QAAM,eAAe,IAAI;AAAA,IACvB;AAAA,EAAA;AAGF,MAAI,CAAC,eAAe,CAAC,GAAG;AACtB,UAAM,IAAI;AAAA,MACR,gCAAgC,MAAM,cAAc,UAAU,SAAS;AAAA,IAAA;AAAA,EAE3E;AAEA,QAAM,UAAU,aAAa,CAAC;AAG9B,QAAM,eAAe,MAAM,MAAM,OAAO;AACxC,SAAO,aAAa,YAAA;AACtB;AAYO,SAAS,iBACd,MACA,MACA,SAAqB,KACrB,QAA6B,UAC7B;AACA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EAAA;AAEJ;"}
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Emoji provider options for rendering emoji in OG images
3
+ */
4
+ export type EmojiType = "twemoji" | "openmoji" | "blobmoji" | "noto" | "fluent" | "fluentFlat";
5
+ /**
6
+ * Font weight options
7
+ */
8
+ export type FontWeight = 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900;
9
+ /**
10
+ * Font style options
11
+ */
12
+ export type FontStyle = "normal" | "italic";
13
+ /**
14
+ * Font configuration for custom fonts
15
+ */
16
+ export interface FontConfig {
17
+ name: string;
18
+ data: ArrayBuffer;
19
+ weight?: FontWeight;
20
+ style?: FontStyle;
21
+ }
22
+ /**
23
+ * Options for ImageResponse
24
+ */
25
+ export interface ImageResponseOptions {
26
+ /**
27
+ * Width of the output image in pixels
28
+ * @default 1200
29
+ */
30
+ width?: number;
31
+ /**
32
+ * Height of the output image in pixels
33
+ * @default 630
34
+ */
35
+ height?: number;
36
+ /**
37
+ * Output format
38
+ * @default "png"
39
+ */
40
+ format?: "png" | "svg";
41
+ /**
42
+ * Fonts to use for rendering text
43
+ */
44
+ fonts?: FontConfig[];
45
+ /**
46
+ * Emoji provider for rendering emoji
47
+ */
48
+ emoji?: EmojiType;
49
+ /**
50
+ * Enable debug mode (disables caching)
51
+ * @default false
52
+ */
53
+ debug?: boolean;
54
+ /**
55
+ * Additional headers to include in the response
56
+ */
57
+ headers?: Record<string, string>;
58
+ /**
59
+ * HTTP status code for the response
60
+ * @default 200
61
+ */
62
+ status?: number;
63
+ /**
64
+ * HTTP status text for the response
65
+ */
66
+ statusText?: string;
67
+ }
68
+ /**
69
+ * Options for loading Google Fonts
70
+ */
71
+ export interface GoogleFontOptions {
72
+ /**
73
+ * Font family name (e.g., "Inter", "Roboto")
74
+ */
75
+ family: string;
76
+ /**
77
+ * Font weight
78
+ * @default 400
79
+ */
80
+ weight?: FontWeight;
81
+ /**
82
+ * Subset of characters to load (for optimization)
83
+ */
84
+ text?: string;
85
+ }
package/package.json ADDED
@@ -0,0 +1,87 @@
1
+ {
2
+ "name": "cf-workers-og",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "description": "Generate Open Graph images on Cloudflare Workers with Vite support",
6
+ "main": "./dist/index.js",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "node": {
12
+ "types": "./dist/index.node.d.ts",
13
+ "import": "./dist/index.node.js"
14
+ },
15
+ "workerd": {
16
+ "types": "./dist/index.d.ts",
17
+ "import": "./dist/index.js"
18
+ },
19
+ "default": {
20
+ "types": "./dist/index.d.ts",
21
+ "import": "./dist/index.js"
22
+ }
23
+ }
24
+ },
25
+ "files": [
26
+ "dist"
27
+ ],
28
+ "dependencies": {
29
+ "@cf-wasm/og": "^0.3.0",
30
+ "htmlparser2": "^10.0.0",
31
+ "style-to-js": "^1.1.21"
32
+ },
33
+ "devDependencies": {
34
+ "@cloudflare/workers-types": "^4.20250610.0",
35
+ "@eslint/js": "^9.0.0",
36
+ "@types/react": "^18.3.0",
37
+ "@vitest/coverage-v8": "^2.0.0",
38
+ "eslint": "^9.0.0",
39
+ "prettier": "^3.3.0",
40
+ "typescript": "^5.8.3",
41
+ "typescript-eslint": "^8.0.0",
42
+ "vite": "^7.0.0",
43
+ "vite-plugin-dts": "^4.5.0",
44
+ "vitest": "^2.0.0"
45
+ },
46
+ "peerDependencies": {
47
+ "react": ">=18.0.0"
48
+ },
49
+ "peerDependenciesMeta": {
50
+ "react": {
51
+ "optional": true
52
+ }
53
+ },
54
+ "keywords": [
55
+ "cloudflare",
56
+ "workers",
57
+ "og",
58
+ "opengraph",
59
+ "image",
60
+ "satori",
61
+ "vite"
62
+ ],
63
+ "license": "MIT",
64
+ "repository": {
65
+ "type": "git",
66
+ "url": "https://github.com/jillesme/cf-workers-og"
67
+ },
68
+ "bugs": {
69
+ "url": "https://github.com/jillesme/cf-workers-og/issues"
70
+ },
71
+ "homepage": "https://github.com/jillesme/cf-workers-og#readme",
72
+ "engines": {
73
+ "node": ">=18"
74
+ },
75
+ "scripts": {
76
+ "build": "vite build && tsc --emitDeclarationOnly",
77
+ "dev": "vite build --watch",
78
+ "typecheck": "tsc --noEmit",
79
+ "test": "vitest run",
80
+ "test:watch": "vitest",
81
+ "test:coverage": "vitest run --coverage",
82
+ "lint": "eslint src/",
83
+ "lint:fix": "eslint src/ --fix",
84
+ "format": "prettier --write src/",
85
+ "format:check": "prettier --check src/"
86
+ }
87
+ }