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 +21 -0
- package/README.md +305 -0
- package/dist/fonts.d.ts +52 -0
- package/dist/fonts.node.d.ts +52 -0
- package/dist/html-parser-DRzlsDtB.js +80 -0
- package/dist/html-parser-DRzlsDtB.js.map +1 -0
- package/dist/html-parser.d.ts +17 -0
- package/dist/image-response.d.ts +64 -0
- package/dist/image-response.node.d.ts +64 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.js +131 -0
- package/dist/index.js.map +1 -0
- package/dist/index.node.d.ts +12 -0
- package/dist/index.node.js +131 -0
- package/dist/index.node.js.map +1 -0
- package/dist/types.d.ts +85 -0
- package/package.json +87 -0
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
|
package/dist/fonts.d.ts
ADDED
|
@@ -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
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -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;"}
|
package/dist/types.d.ts
ADDED
|
@@ -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
|
+
}
|