cf-workers-og 1.0.1 → 2.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/README.md CHANGED
@@ -1,13 +1,14 @@
1
1
  # cf-workers-og
2
2
 
3
- Generate Open Graph images on Cloudflare Workers with Vite support.
3
+ Generate Open Graph images on Cloudflare Workers with Node.js bindings for local Vite dev.
4
4
 
5
5
  A thin wrapper around [@cf-wasm/og](https://github.com/fineshopdesign/cf-wasm) that provides:
6
6
 
7
+ - Designed for Workers; includes Node.js bindings for local dev
7
8
  - Works with both **Vite dev** and **Wrangler dev**
8
9
  - Uses modern, maintained WASM dependencies
9
- - Robust HTML string parsing (using battle-tested libraries)
10
- - Backwards-compatible API for workers-og users
10
+ - Optional HTML string parsing (using battle-tested libraries)
11
+ - Backwards-compatible entrypoint for workers-og users (via `cf-workers-og/compat`)
11
12
  - TypeScript support
12
13
 
13
14
  ## Installation
@@ -20,7 +21,7 @@ pnpm add cf-workers-og
20
21
 
21
22
  ## Quick Start
22
23
 
23
- ### Basic Usage (JSX)
24
+ ### Basic Usage (JSX, recommended)
24
25
 
25
26
  ```tsx
26
27
  import { ImageResponse } from "cf-workers-og";
@@ -86,10 +87,11 @@ export default {
86
87
 
87
88
  ### HTML String Usage
88
89
 
89
- We added this to be backwards-comatible with workers-og, but prefer JSX.
90
+ Use HTML parsing only if you need it. For new projects, JSX is the simplest and most reliable.
91
+ HTML parsing is available via the opt-in `cf-workers-og/html` entrypoint. For workers-og constructor compatibility, use `cf-workers-og/compat`.
90
92
 
91
93
  ```typescript
92
- import { ImageResponse, parseHtml } from "cf-workers-og";
94
+ import { ImageResponse, parseHtml } from "cf-workers-og/html";
93
95
 
94
96
  export default {
95
97
  async fetch(request: Request, env: Env, ctx: ExecutionContext) {
@@ -120,29 +122,81 @@ export default defineConfig({
120
122
  });
121
123
  ```
122
124
 
125
+ Vite dev runs in Node.js, so this package ships Node bindings that should be picked automatically. If your bundler does not respect export conditions, use explicit paths like `cf-workers-og/node`.
126
+
127
+ ## Which entrypoint should I use?
128
+
129
+ - `cf-workers-og` (recommended): JSX input only, clean API for new users.
130
+ - `cf-workers-og/html`: adds `parseHtml` and accepts HTML strings in `ImageResponse.create`.
131
+ - `cf-workers-og/compat`: legacy constructor behavior and HTML strings for migrating from workers-og.
132
+ - If your bundler ignores export conditions, use explicit paths like `cf-workers-og/node`, `cf-workers-og/workerd`, and their `/html` or `/compat` variants.
133
+
123
134
  ## Why Not workers-og?
124
135
 
125
- The original [workers-og](https://github.com/syedashar1/workers-og) library has several issues:
136
+ The original [workers-og](https://github.com/syedashar1/workers-og) has fundamental issues that make it unsuitable for production use.
137
+
138
+ ### 1. Outdated WASM Dependencies
139
+
140
+ workers-og uses `yoga-wasm-web@0.3.3` which has been **unmaintained since 2023**. The Yoga project moved to Yoga 3.0 with a `SINGLE_FILE=1` compilation that inlines WASM as base64 - incompatible with Cloudflare Workers' module-based WASM loading. Satori itself now uses an internal patched version instead of `yoga-wasm-web`, fragmenting the ecosystem.
141
+
142
+ ### 2. Brittle HTML Parsing
143
+
144
+ The HTML parser builds JSON via string concatenation:
145
+
146
+ ```typescript
147
+ // workers-og approach - error-prone
148
+ vdomStr += `{"type":"${element.tagName}", "props":{${attrs}"children": [`;
149
+ ```
150
+
151
+ The code comments even acknowledge: *"very error prone. So it might need more hardening"*. The `sanitizeJSON` function only handles basic escapes, missing edge cases.
152
+
153
+ ### 3. Style Parsing Fails on Complex CSS
154
+
155
+ workers-og uses regex to parse CSS: `;(?![^(]*\))`. This fails on:
156
+ - Nested parentheses: `calc(100% - (10px + 5px))`
157
+ - Data URIs: `url(data:image/png;base64,...)`
158
+ - Complex CSS with multiple function calls
159
+
160
+ ### 4. No Vite Support
126
161
 
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 |
162
+ workers-og uses esbuild's copy loader for WASM, which is incompatible with Vite. The library only works with `wrangler dev`, not `vite dev` with `@cloudflare/vite-plugin`.
163
+
164
+ ### 5. Debug Logs in Production
165
+
166
+ ```typescript
167
+ console.log("init RESVG"); // Left in production code
168
+ ```
133
169
 
134
170
  ### How cf-workers-og Solves These
135
171
 
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
172
+ | Issue | cf-workers-og Solution |
173
+ |-------|----------------------|
174
+ | **Outdated WASM** | Uses `@cf-wasm/og` (actively maintained, Dec 2025) with current yoga and resvg |
175
+ | **Brittle HTML parsing** | Uses [htmlparser2](https://github.com/fb55/htmlparser2) - a battle-tested streaming parser with no browser dependencies |
176
+ | **Style parsing** | Uses [style-to-js](https://www.npmjs.com/package/style-to-js) (1M+ weekly downloads) - handles all CSS edge cases |
177
+ | **No Vite support** | Works with `@cloudflare/vite-plugin` via proper conditional exports for node/workerd |
178
+ | **Debug logs** | Clean production code |
179
+
180
+ ### Design Decisions
181
+
182
+ **Why wrap @cf-wasm/og instead of building from scratch?**
183
+
184
+ WASM on Cloudflare Workers is genuinely hard. Workers cannot compile WASM from arbitrary data blobs - you must import as modules. Rather than maintain custom yoga-wasm builds, we wrap `@cf-wasm/og` which already solves Vite + Wrangler compatibility.
185
+
186
+ **Why htmlparser2 instead of html-react-parser?**
187
+
188
+ `html-react-parser` uses `html-dom-parser` which detects the environment. Cloudflare Workers is incorrectly detected as a browser, causing it to use `document.implementation.createHTMLDocument` which doesn't exist. `htmlparser2` is a pure streaming parser that works everywhere.
140
189
 
141
190
  ## API Reference
142
191
 
192
+ ### Entry points
193
+
194
+ Use `cf-workers-og` for Workers with JSX input, `cf-workers-og/html` for HTML strings, and `cf-workers-og/compat` only when migrating from workers-og. If your bundler ignores export conditions, use explicit paths like `cf-workers-og/node` or `cf-workers-og/workerd` (and the `/html` or `/compat` variants).
195
+
143
196
  ### `ImageResponse.create(element, options)`
144
197
 
145
198
  Generate an OG image Response.
199
+ Main entrypoint expects a React element; `cf-workers-og/html` also accepts HTML strings.
146
200
 
147
201
  ```typescript
148
202
  const response = await ImageResponse.create(element, {
@@ -160,7 +214,7 @@ const response = await ImageResponse.create(element, {
160
214
 
161
215
  ### `parseHtml(html)`
162
216
 
163
- Parse an HTML string into React elements for Satori.
217
+ Parse an HTML string into React elements for Satori. Exported from `cf-workers-og/html` and `cf-workers-og/compat`.
164
218
 
165
219
  ```typescript
166
220
  const element = parseHtml('<div style="display: flex;">Hello</div>');
@@ -203,9 +257,11 @@ The `GoogleFont` class caches font files using Cloudflare's Cache API to avoid r
203
257
 
204
258
  ## Migrating from workers-og
205
259
 
260
+ Note that cache is only used if you use GoogleFonts. Otherwise it is a drop-in replacement.
261
+
206
262
  ```diff
207
263
  - import { ImageResponse } from 'workers-og';
208
- + import { ImageResponse, cache } from 'cf-workers-og';
264
+ + import { ImageResponse, cache } from 'cf-workers-og/compat';
209
265
 
210
266
  export default {
211
267
  async fetch(request, env, ctx) {
@@ -220,7 +276,7 @@ For HTML string users:
220
276
 
221
277
  ```diff
222
278
  - return new ImageResponse(htmlString, options);
223
- + import { parseHtml } from 'cf-workers-og';
279
+ + import { ImageResponse, parseHtml } from 'cf-workers-og/html';
224
280
  + return ImageResponse.create(parseHtml(htmlString), options);
225
281
  ```
226
282
 
@@ -236,70 +292,6 @@ This package is a **thin wrapper** (6 KB) around `@cf-wasm/og`. The heavy liftin
236
292
 
237
293
  The WASM files are installed as transitive dependencies - they're not bundled in this package.
238
294
 
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
295
  ## License
304
296
 
305
297
  MIT
@@ -0,0 +1,11 @@
1
+ /**
2
+ * cf-workers-og (compat entry point)
3
+ *
4
+ * Keeps workers-og constructor behavior and HTML string parsing support.
5
+ */
6
+ import type { ReactNode } from "react";
7
+ export { cache } from "@cf-wasm/og/workerd";
8
+ export declare const ImageResponse: import("./core/image-response").ImageResponseCompatClass<ReactNode>;
9
+ export { parseHtml } from "./html-parser";
10
+ export { GoogleFont, CustomFont, loadGoogleFont, createFontConfig } from "./fonts";
11
+ export type { ImageResponseOptions, FontConfig, FontWeight, FontStyle, EmojiType, GoogleFontOptions, } from "./types";
package/dist/compat.js ADDED
@@ -0,0 +1,20 @@
1
+ import { ImageResponse as ImageResponse$1 } from "@cf-wasm/og/workerd";
2
+ import { CustomFont, GoogleFont, cache } from "@cf-wasm/og/workerd";
3
+ import { c as createImageResponseClass } from "./fonts.shared-RU0Lt2Ku.js";
4
+ import { a, l } from "./fonts.shared-RU0Lt2Ku.js";
5
+ import { p as parseHtml } from "./html-parser-DRzlsDtB.js";
6
+ const ImageResponse = createImageResponseClass({
7
+ cfImageResponse: ImageResponse$1,
8
+ parseHtml,
9
+ compatConstructor: true
10
+ });
11
+ export {
12
+ CustomFont,
13
+ GoogleFont,
14
+ ImageResponse,
15
+ cache,
16
+ a as createFontConfig,
17
+ l as loadGoogleFont,
18
+ parseHtml
19
+ };
20
+ //# sourceMappingURL=compat.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"compat.js","sources":["../src/compat.ts"],"sourcesContent":["/**\n * cf-workers-og (compat entry point)\n *\n * Keeps workers-og constructor behavior and HTML string parsing support.\n */\n\nimport { ImageResponse as CfImageResponse } from \"@cf-wasm/og/workerd\";\nimport type { ReactNode } from \"react\";\nimport { createImageResponseClass } from \"./core/image-response\";\nimport { parseHtml } from \"./html-parser\";\n\nexport { cache } from \"@cf-wasm/og/workerd\";\n\nexport const ImageResponse = createImageResponseClass<ReactNode | string>({\n cfImageResponse: CfImageResponse,\n parseHtml,\n compatConstructor: true,\n});\n\nexport { parseHtml } from \"./html-parser\";\n\nexport { GoogleFont, CustomFont, loadGoogleFont, createFontConfig } from \"./fonts\";\n\nexport type {\n ImageResponseOptions,\n FontConfig,\n FontWeight,\n FontStyle,\n EmojiType,\n GoogleFontOptions,\n} from \"./types\";\n"],"names":["CfImageResponse"],"mappings":";;;;;AAaO,MAAM,gBAAgB,yBAA6C;AAAA,EACxE,iBAAiBA;AAAAA,EACjB;AAAA,EACA,mBAAmB;AACrB,CAAC;"}
@@ -0,0 +1,9 @@
1
+ /**
2
+ * cf-workers-og (Node.js compat entry point)
3
+ */
4
+ import type { ReactNode } from "react";
5
+ export { cache } from "@cf-wasm/og/node";
6
+ export declare const ImageResponse: import("./core/image-response").ImageResponseCompatClass<ReactNode>;
7
+ export { parseHtml } from "./html-parser";
8
+ export { GoogleFont, CustomFont, loadGoogleFont, createFontConfig } from "./fonts.node";
9
+ export type { ImageResponseOptions, FontConfig, FontWeight, FontStyle, EmojiType, GoogleFontOptions, } from "./types";
@@ -0,0 +1,20 @@
1
+ import { ImageResponse as ImageResponse$1 } from "@cf-wasm/og/node";
2
+ import { CustomFont, GoogleFont, cache } from "@cf-wasm/og/node";
3
+ import { c as createImageResponseClass } from "./fonts.shared-RU0Lt2Ku.js";
4
+ import { a, l } from "./fonts.shared-RU0Lt2Ku.js";
5
+ import { p as parseHtml } from "./html-parser-DRzlsDtB.js";
6
+ const ImageResponse = createImageResponseClass({
7
+ cfImageResponse: ImageResponse$1,
8
+ parseHtml,
9
+ compatConstructor: true
10
+ });
11
+ export {
12
+ CustomFont,
13
+ GoogleFont,
14
+ ImageResponse,
15
+ cache,
16
+ a as createFontConfig,
17
+ l as loadGoogleFont,
18
+ parseHtml
19
+ };
20
+ //# sourceMappingURL=compat.node.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"compat.node.js","sources":["../src/compat.node.ts"],"sourcesContent":["/**\n * cf-workers-og (Node.js compat entry point)\n */\n\nimport { ImageResponse as CfImageResponse } from \"@cf-wasm/og/node\";\nimport type { ReactNode } from \"react\";\nimport { createImageResponseClass } from \"./core/image-response\";\nimport { parseHtml } from \"./html-parser\";\n\nexport { cache } from \"@cf-wasm/og/node\";\n\nexport const ImageResponse = createImageResponseClass<ReactNode | string>({\n cfImageResponse: CfImageResponse,\n parseHtml,\n compatConstructor: true,\n});\n\nexport { parseHtml } from \"./html-parser\";\n\nexport { GoogleFont, CustomFont, loadGoogleFont, createFontConfig } from \"./fonts.node\";\n\nexport type {\n ImageResponseOptions,\n FontConfig,\n FontWeight,\n FontStyle,\n EmojiType,\n GoogleFontOptions,\n} from \"./types\";\n"],"names":["CfImageResponse"],"mappings":";;;;;AAWO,MAAM,gBAAgB,yBAA6C;AAAA,EACxE,iBAAiBA;AAAAA,EACjB;AAAA,EACA,mBAAmB;AACrB,CAAC;"}
@@ -0,0 +1,34 @@
1
+ import type { ReactNode } from "react";
2
+ import type { EmojiType, FontInput, ImageResponseOptions } from "../types";
3
+ type CfImageResponseOptions = {
4
+ width: number;
5
+ height: number;
6
+ format: "png" | "svg";
7
+ fonts: FontInput[];
8
+ emoji?: EmojiType;
9
+ };
10
+ type CfImageResponse = {
11
+ async: (element: ReactNode, options: CfImageResponseOptions) => Promise<Response>;
12
+ };
13
+ type ParseHtml = (html: string) => ReactNode;
14
+ type CreateImageResponseConfig = {
15
+ cfImageResponse: CfImageResponse;
16
+ parseHtml?: ParseHtml;
17
+ compatConstructor?: boolean;
18
+ };
19
+ type ImageInput = ReactNode | string;
20
+ export type ImageResponseClass<Input extends ImageInput> = {
21
+ new (element: Input, options?: ImageResponseOptions): Response;
22
+ create(element: Input, options?: ImageResponseOptions): Promise<Response>;
23
+ };
24
+ export type ImageResponseCompatClass<Input extends ImageInput> = {
25
+ new (element: Input, options?: ImageResponseOptions): Promise<Response>;
26
+ create(element: Input, options?: ImageResponseOptions): Promise<Response>;
27
+ };
28
+ export declare function createImageResponseClass<Input extends ImageInput>(config: CreateImageResponseConfig & {
29
+ compatConstructor: true;
30
+ }): ImageResponseCompatClass<Input>;
31
+ export declare function createImageResponseClass<Input extends ImageInput>(config: CreateImageResponseConfig & {
32
+ compatConstructor?: false;
33
+ }): ImageResponseClass<Input>;
34
+ export {};
package/dist/fonts.d.ts CHANGED
@@ -1,52 +1,2 @@
1
- import type { FontWeight, GoogleFontOptions } from "./types";
2
1
  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
- };
2
+ export { loadGoogleFont, createFontConfig } from "./fonts.shared";
@@ -1,52 +1,2 @@
1
- import type { FontWeight, GoogleFontOptions } from "./types";
2
1
  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
- };
2
+ export { loadGoogleFont, createFontConfig } from "./fonts.shared";
@@ -0,0 +1,135 @@
1
+ function createImageResponseClass(config) {
2
+ const { cfImageResponse, parseHtml, compatConstructor = false } = config;
3
+ class ImageResponseCore extends Response {
4
+ static async create(element, options = {}) {
5
+ const reactElement = resolveElement(element, parseHtml);
6
+ const normalized = normalizeOptions(options);
7
+ const response = await cfImageResponse.async(reactElement, {
8
+ width: normalized.width,
9
+ height: normalized.height,
10
+ format: normalized.format,
11
+ fonts: normalized.fonts,
12
+ emoji: normalized.emoji
13
+ });
14
+ const responseHeaders = buildResponseHeaders(
15
+ response.headers,
16
+ normalized.format,
17
+ normalized.debug,
18
+ normalized.headers
19
+ );
20
+ return new Response(response.body, {
21
+ headers: responseHeaders,
22
+ status: normalized.status,
23
+ statusText: normalized.statusText
24
+ });
25
+ }
26
+ constructor(element, options = {}) {
27
+ super(null);
28
+ if (!compatConstructor) {
29
+ throw new Error(
30
+ "cf-workers-og: use ImageResponse.create(...) or import from cf-workers-og/compat"
31
+ );
32
+ }
33
+ return ImageResponseCore.create(element, options);
34
+ }
35
+ }
36
+ if (compatConstructor) {
37
+ return ImageResponseCore;
38
+ }
39
+ return ImageResponseCore;
40
+ }
41
+ function resolveElement(element, parseHtml) {
42
+ if (typeof element === "string") {
43
+ if (!parseHtml) {
44
+ throw new Error(
45
+ "cf-workers-og: HTML string input requires cf-workers-og/html or cf-workers-og/compat"
46
+ );
47
+ }
48
+ return parseHtml(element);
49
+ }
50
+ return element;
51
+ }
52
+ function normalizeOptions(options) {
53
+ return {
54
+ width: options.width ?? 1200,
55
+ height: options.height ?? 630,
56
+ format: options.format ?? "png",
57
+ fonts: options.fonts ?? [],
58
+ emoji: options.emoji,
59
+ debug: options.debug ?? false,
60
+ headers: options.headers ?? {},
61
+ status: options.status ?? 200,
62
+ statusText: options.statusText
63
+ };
64
+ }
65
+ function buildResponseHeaders(baseHeaders, format, debug, customHeaders) {
66
+ const responseHeaders = new Headers(baseHeaders);
67
+ responseHeaders.set(
68
+ "Content-Type",
69
+ format === "svg" ? "image/svg+xml" : "image/png"
70
+ );
71
+ responseHeaders.set(
72
+ "Cache-Control",
73
+ debug ? "no-cache, no-store" : "public, immutable, no-transform, max-age=31536000"
74
+ );
75
+ for (const [key, value] of Object.entries(customHeaders)) {
76
+ responseHeaders.set(key, value);
77
+ }
78
+ return responseHeaders;
79
+ }
80
+ async function loadGoogleFont(options) {
81
+ const { family, weight, text } = options;
82
+ const params = {
83
+ family: `${encodeURIComponent(family)}${weight ? `:wght@${weight}` : ""}`
84
+ };
85
+ if (text) {
86
+ params.text = text;
87
+ } else {
88
+ params.subset = "latin";
89
+ }
90
+ const cssUrl = `https://fonts.googleapis.com/css2?${Object.keys(params).map((key) => `${key}=${params[key]}`).join("&")}`;
91
+ const cfCache = typeof caches !== "undefined" ? caches.default : void 0;
92
+ let cssResponse;
93
+ if (cfCache) {
94
+ cssResponse = await cfCache.match(cssUrl);
95
+ }
96
+ if (!cssResponse) {
97
+ cssResponse = await fetch(cssUrl, {
98
+ headers: {
99
+ "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"
100
+ }
101
+ });
102
+ if (cfCache) {
103
+ const cacheResponse = new Response(cssResponse.body, cssResponse);
104
+ cacheResponse.headers.set("Cache-Control", "s-maxage=3600");
105
+ await cfCache.put(cssUrl, cacheResponse.clone());
106
+ cssResponse = cacheResponse;
107
+ }
108
+ }
109
+ const css = await cssResponse.text();
110
+ const fontUrlMatch = css.match(
111
+ /src: url\(([^)]+)\) format\(['"]?(opentype|truetype)['"]?\)/
112
+ );
113
+ if (!fontUrlMatch?.[1]) {
114
+ throw new Error(
115
+ `Could not find font URL for "${family}" (weight: ${weight ?? "default"})`
116
+ );
117
+ }
118
+ const fontUrl = fontUrlMatch[1];
119
+ const fontResponse = await fetch(fontUrl);
120
+ return fontResponse.arrayBuffer();
121
+ }
122
+ function createFontConfig(name, data, weight = 400, style = "normal") {
123
+ return {
124
+ name,
125
+ data,
126
+ weight,
127
+ style
128
+ };
129
+ }
130
+ export {
131
+ createFontConfig as a,
132
+ createImageResponseClass as c,
133
+ loadGoogleFont as l
134
+ };
135
+ //# sourceMappingURL=fonts.shared-RU0Lt2Ku.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fonts.shared-RU0Lt2Ku.js","sources":["../src/core/image-response.ts","../src/fonts.shared.ts"],"sourcesContent":["import type { ReactNode } from \"react\";\nimport type { EmojiType, FontInput, ImageResponseOptions } from \"../types\";\n\ntype CfImageResponseOptions = {\n width: number;\n height: number;\n format: \"png\" | \"svg\";\n fonts: FontInput[];\n emoji?: EmojiType;\n};\n\ntype CfImageResponse = {\n async: (element: ReactNode, options: CfImageResponseOptions) => Promise<Response>;\n};\n\ntype ParseHtml = (html: string) => ReactNode;\n\ntype CreateImageResponseConfig = {\n cfImageResponse: CfImageResponse;\n parseHtml?: ParseHtml;\n compatConstructor?: boolean;\n};\n\ntype ImageInput = ReactNode | string;\n\nexport type ImageResponseClass<Input extends ImageInput> = {\n new (element: Input, options?: ImageResponseOptions): Response;\n create(element: Input, options?: ImageResponseOptions): Promise<Response>;\n};\n\nexport type ImageResponseCompatClass<Input extends ImageInput> = {\n new (element: Input, options?: ImageResponseOptions): Promise<Response>;\n create(element: Input, options?: ImageResponseOptions): Promise<Response>;\n};\n\nexport function createImageResponseClass<Input extends ImageInput>(\n config: CreateImageResponseConfig & { compatConstructor: true }\n): ImageResponseCompatClass<Input>;\nexport function createImageResponseClass<Input extends ImageInput>(\n config: CreateImageResponseConfig & { compatConstructor?: false }\n): ImageResponseClass<Input>;\nexport function createImageResponseClass<Input extends ImageInput>(\n config: CreateImageResponseConfig\n): ImageResponseClass<Input> | ImageResponseCompatClass<Input> {\n const { cfImageResponse, parseHtml, compatConstructor = false } = config;\n\n class ImageResponseCore extends Response {\n static async create(\n element: Input,\n options: ImageResponseOptions = {}\n ): Promise<Response> {\n const reactElement = resolveElement(element, parseHtml);\n const normalized = normalizeOptions(options);\n\n const response = await cfImageResponse.async(reactElement, {\n width: normalized.width,\n height: normalized.height,\n format: normalized.format,\n fonts: normalized.fonts,\n emoji: normalized.emoji,\n });\n\n const responseHeaders = buildResponseHeaders(\n response.headers,\n normalized.format,\n normalized.debug,\n normalized.headers\n );\n\n return new Response(response.body, {\n headers: responseHeaders,\n status: normalized.status,\n statusText: normalized.statusText,\n });\n }\n\n constructor(element: Input, options: ImageResponseOptions = {}) {\n super(null);\n if (!compatConstructor) {\n throw new Error(\n \"cf-workers-og: use ImageResponse.create(...) or import from cf-workers-og/compat\"\n );\n }\n return ImageResponseCore.create(element, options) as unknown as ImageResponseCore;\n }\n }\n\n if (compatConstructor) {\n // Cast through unknown to model legacy workers-og constructor behavior.\n // The runtime returns a Promise, but TS cannot express that on classes.\n return ImageResponseCore as unknown as ImageResponseCompatClass<Input>;\n }\n\n return ImageResponseCore as ImageResponseClass<Input>;\n}\n\nfunction resolveElement<Input extends ImageInput>(\n element: Input,\n parseHtml?: ParseHtml\n): ReactNode {\n if (typeof element === \"string\") {\n if (!parseHtml) {\n throw new Error(\n \"cf-workers-og: HTML string input requires cf-workers-og/html or cf-workers-og/compat\"\n );\n }\n return parseHtml(element);\n }\n\n return element;\n}\n\nfunction normalizeOptions(options: ImageResponseOptions) {\n return {\n width: options.width ?? 1200,\n height: options.height ?? 630,\n format: options.format ?? \"png\",\n fonts: options.fonts ?? [],\n emoji: options.emoji,\n debug: options.debug ?? false,\n headers: options.headers ?? {},\n status: options.status ?? 200,\n statusText: options.statusText,\n };\n}\n\nfunction buildResponseHeaders(\n baseHeaders: Headers,\n format: \"png\" | \"svg\",\n debug: boolean,\n customHeaders: Record<string, string>\n): Headers {\n const responseHeaders = new Headers(baseHeaders);\n\n responseHeaders.set(\n \"Content-Type\",\n format === \"svg\" ? \"image/svg+xml\" : \"image/png\"\n );\n\n responseHeaders.set(\n \"Cache-Control\",\n debug\n ? \"no-cache, no-store\"\n : \"public, immutable, no-transform, max-age=31536000\"\n );\n\n for (const [key, value] of Object.entries(customHeaders)) {\n responseHeaders.set(key, value);\n }\n\n return responseHeaders;\n}\n","import type { FontWeight, GoogleFontOptions } from \"./types\";\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 */\nexport async function loadGoogleFont(\n options: GoogleFontOptions\n): Promise<ArrayBuffer> {\n const { family, weight, text } = options;\n\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 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 \"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 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 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 const fontResponse = await fetch(fontUrl);\n return fontResponse.arrayBuffer();\n}\n\n/**\n * Create a font configuration object for ImageResponse.\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":[],"mappings":"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"}
@@ -0,0 +1,17 @@
1
+ import type { FontWeight, GoogleFontOptions } from "./types";
2
+ /**
3
+ * Load a Google Font and return its data as an ArrayBuffer.
4
+ *
5
+ * This is a backwards-compatible function for users migrating from workers-og.
6
+ * For new code, prefer using `GoogleFont` class from `@cf-wasm/og`.
7
+ */
8
+ export declare function loadGoogleFont(options: GoogleFontOptions): Promise<ArrayBuffer>;
9
+ /**
10
+ * Create a font configuration object for ImageResponse.
11
+ */
12
+ export declare function createFontConfig(name: string, data: ArrayBuffer, weight?: FontWeight, style?: "normal" | "italic"): {
13
+ name: string;
14
+ data: ArrayBuffer;
15
+ weight: FontWeight;
16
+ style: "normal" | "italic";
17
+ };
package/dist/html.d.ts ADDED
@@ -0,0 +1,11 @@
1
+ /**
2
+ * cf-workers-og (HTML entry point)
3
+ *
4
+ * Includes HTML string parsing support without legacy constructor behavior.
5
+ */
6
+ import type { ReactNode } from "react";
7
+ export { cache } from "@cf-wasm/og/workerd";
8
+ export declare const ImageResponse: import("./core/image-response").ImageResponseClass<ReactNode>;
9
+ export { parseHtml } from "./html-parser";
10
+ export { GoogleFont, CustomFont, loadGoogleFont, createFontConfig } from "./fonts";
11
+ export type { ImageResponseOptions, FontConfig, FontWeight, FontStyle, EmojiType, GoogleFontOptions, } from "./types";
package/dist/html.js ADDED
@@ -0,0 +1,19 @@
1
+ import { ImageResponse as ImageResponse$1 } from "@cf-wasm/og/workerd";
2
+ import { CustomFont, GoogleFont, cache } from "@cf-wasm/og/workerd";
3
+ import { c as createImageResponseClass } from "./fonts.shared-RU0Lt2Ku.js";
4
+ import { a, l } from "./fonts.shared-RU0Lt2Ku.js";
5
+ import { p as parseHtml } from "./html-parser-DRzlsDtB.js";
6
+ const ImageResponse = createImageResponseClass({
7
+ cfImageResponse: ImageResponse$1,
8
+ parseHtml
9
+ });
10
+ export {
11
+ CustomFont,
12
+ GoogleFont,
13
+ ImageResponse,
14
+ cache,
15
+ a as createFontConfig,
16
+ l as loadGoogleFont,
17
+ parseHtml
18
+ };
19
+ //# sourceMappingURL=html.js.map