@ethercorps/sveltekit-og 4.3.1-next.3 → 4.4.0-next.1

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.
Files changed (69) hide show
  1. package/README.md +35 -193
  2. package/dist/client/assets/NotoSans-Bold.ttf +0 -0
  3. package/dist/client/assets/NotoSans-Regular.ttf +0 -0
  4. package/dist/client/component.d.ts +19 -0
  5. package/dist/client/component.js +32 -0
  6. package/dist/client/create.d.ts +7 -0
  7. package/dist/client/create.js +8 -0
  8. package/dist/client/engines/satori.d.ts +4 -0
  9. package/dist/client/engines/satori.js +81 -0
  10. package/dist/client/engines/takumi.d.ts +7 -0
  11. package/dist/client/engines/takumi.js +9 -0
  12. package/dist/client/fonts.d.ts +2 -0
  13. package/dist/client/fonts.js +36 -0
  14. package/dist/client/image-response.d.ts +13 -0
  15. package/dist/client/image-response.js +88 -0
  16. package/dist/client/index.d.ts +4 -0
  17. package/dist/client/index.js +6 -0
  18. package/dist/client/render.d.ts +7 -0
  19. package/dist/client/render.js +37 -0
  20. package/dist/client/types.d.ts +14 -0
  21. package/dist/client/types.js +1 -0
  22. package/dist/fonts.d.ts +4 -4
  23. package/dist/fonts.js +17 -13
  24. package/dist/helpers/create.d.ts +7 -4
  25. package/dist/helpers/create.js +39 -28
  26. package/dist/helpers/defaults.d.ts +4 -4
  27. package/dist/helpers/defaults.js +28 -18
  28. package/dist/helpers/emoji.d.ts +1 -1
  29. package/dist/helpers/emoji.js +31 -21
  30. package/dist/helpers/error-handler.d.ts +38 -0
  31. package/dist/helpers/error-handler.js +76 -0
  32. package/dist/helpers/logger.d.ts +7 -0
  33. package/dist/helpers/logger.js +21 -0
  34. package/dist/helpers/response.d.ts +30 -0
  35. package/dist/helpers/response.js +52 -0
  36. package/dist/helpers/to-html.d.ts +10 -0
  37. package/dist/helpers/to-html.js +10 -0
  38. package/dist/helpers/toJSX.d.ts +3 -3
  39. package/dist/helpers/toJSX.js +9 -7
  40. package/dist/helpers/utils.d.ts +2 -0
  41. package/dist/helpers/utils.js +25 -0
  42. package/dist/image-response.d.ts +2 -2
  43. package/dist/image-response.js +13 -21
  44. package/dist/plugin.d.ts +11 -0
  45. package/dist/plugin.js +44 -12
  46. package/dist/providers/instances.d.ts +3 -3
  47. package/dist/providers/instances.js +43 -15
  48. package/dist/providers/resvg/edge.d.ts +1 -1
  49. package/dist/providers/resvg/edge.js +10 -6
  50. package/dist/providers/resvg/node.d.ts +1 -1
  51. package/dist/providers/resvg/node.js +18 -9
  52. package/dist/providers/satori/edge.d.ts +6 -0
  53. package/dist/providers/satori/edge.js +12 -0
  54. package/dist/providers/satori/node.d.ts +1 -1
  55. package/dist/providers/satori/node.js +4 -4
  56. package/dist/takumi/fonts.d.ts +16 -0
  57. package/dist/takumi/fonts.js +15 -0
  58. package/dist/takumi/image-response.d.ts +6 -0
  59. package/dist/takumi/image-response.js +27 -0
  60. package/dist/takumi/index.d.ts +5 -0
  61. package/dist/takumi/index.js +4 -0
  62. package/dist/takumi/render.d.ts +4 -0
  63. package/dist/takumi/render.js +23 -0
  64. package/dist/takumi/renderer.d.ts +7 -0
  65. package/dist/takumi/renderer.js +39 -0
  66. package/dist/takumi/types.d.ts +64 -0
  67. package/dist/takumi/types.js +1 -0
  68. package/dist/types.d.ts +10 -14
  69. package/package.json +76 -21
package/README.md CHANGED
@@ -4,225 +4,67 @@
4
4
 
5
5
  # SvelteKit Open Graph Image Generation
6
6
 
7
- Dynamically generate Open Graph images from an HTML+CSS template or Svelte component using fast and efficient conversion from HTML > SVG > PNG. Based on [Satori](https://github.com/vercel/satori#documentation). No headless browser required.
7
+ Dynamically generate Open Graph images from an HTML+CSS template or Svelte component. No headless browser required.
8
8
 
9
- ## Docs
10
- - With `sveltekit-og@4`, we have [official documentation](https://sveltekit-og.dev).
9
+ Pick the rendering engine that fits your needs:
11
10
 
12
- ## Installation
11
+ - **Satori** (default) — HTML → SVG → PNG, based on [Satori](https://github.com/vercel/satori#documentation).
12
+ - **[Takumi](https://takumi.kane.tw)** — a Rust/WASM engine with more output formats (`webp`, `jpeg`, `ico`, `svg`, …) and a built-in font, available from `@ethercorps/sveltekit-og/takumi` (v4.3.0+).
13
13
 
14
- ```bash
15
- pnpm install @ethercorps/sveltekit-og
16
- ```
14
+ ## Table of Contents
17
15
 
18
- ## Usage
16
+ - [SvelteKit Open Graph Image Generation](#sveltekit-open-graph-image-generation)
17
+ - [Table of Contents](#table-of-contents)
18
+ - [Docs](#docs)
19
+ - [Installation](#installation)
20
+ - [Usage](#usage)
21
+ - [Examples](#examples)
22
+ - [Contributing](#contributing)
23
+ - [Changelog](#changelog)
24
+ - [License](#license)
25
+ - [Acknowledgements](#acknowledgements)
26
+ - [Authors](#authors)
27
+ - [Contributors](#contributors)
19
28
 
20
- ### Vite (Recommended)
21
-
22
- - Add vite plugin
23
-
24
- ```typescript title="vite.cofig.js"
25
- import { sveltekit } from '@sveltejs/kit/vite';
26
- import { sveltekitOG } from '@ethercorps/sveltekit-og/plugin';
27
- const config = {
28
- plugins: [sveltekit(), sveltekitOG()]
29
- };
30
-
31
- export default config;
32
- ```
29
+ ## Docs
33
30
 
34
- ### Rollup (will be deprecated in v5)
35
- - Add `rollupWasm` to `build.rollupOptions.plugins` in `vite.cofig.js` file.
36
- - For more information, check [docs](https://sveltekit-og.dev/docs/getting-started)
37
-
38
- ```ts title="vite.cofig.js"
39
- import { sveltekit } from '@sveltejs/kit/vite';
40
- import { defineConfig } from 'vitest/config';
41
- import { rollupWasm } from '@ethercorps/sveltekit-og/plugin';
42
-
43
- export default defineConfig({
44
- plugins: [sveltekit()],
45
- build: {
46
- rollupOptions: {
47
- plugins: [rollupWasm()],
48
- }
49
- }
50
- });
51
- ```
31
+ For more detailed information and advanced usage, please refer to the [official documentation](https://sveltekit-og.dev).
52
32
 
53
- - For node adapter update config with `rollupWasm`
54
- - Check node runtime [docs](https://sveltekit-og.dev/docs/runtime/node)
55
-
56
- ```ts title="vite.cofig.js"
57
- import { sveltekit } from '@sveltejs/kit/vite';
58
- import { defineConfig } from 'vitest/config';
59
- import { rollupWasm } from '@ethercorps/sveltekit-og/plugin';
60
-
61
- export default defineConfig({
62
- plugins: [sveltekit()],
63
- build: {
64
- rollupOptions: {
65
- plugins: [
66
- rollupWasm({ esmImport: false })
67
- ],
68
- }
69
- }
70
- });
71
- ```
33
+ ## Installation
72
34
 
73
- - Create a file at `/src/routes/og/+server.ts`. Alternatively, you can use JavaScript by removing the types from this example.
74
-
75
- ```typescript
76
- // src/routes/og/+server.ts
77
- import { ImageResponse } from '@ethercorps/sveltekit-og';
78
- import { RequestHandler } from './$types';
79
-
80
- const template = `
81
- <div tw="bg-gray-50 flex w-full h-full items-center justify-center">
82
- <div tw="flex flex-col md:flex-row w-full py-12 px-4 md:items-center justify-between p-8">
83
- <h2 tw="flex flex-col text-3xl sm:text-4xl font-bold tracking-tight text-gray-900 text-left">
84
- <span>Ready to dive in?</span>
85
- <span tw="text-indigo-600">Start your free trial today.</span>
86
- </h2>
87
- <div tw="mt-8 flex md:mt-0">
88
- <div tw="flex rounded-md shadow">
89
- <a href="#" tw="flex items-center justify-center rounded-md border border-transparent bg-indigo-600 px-5 py-3 text-base font-medium text-white">Get started</a>
90
- </div>
91
- <div tw="ml-3 flex rounded-md shadow">
92
- <a href="#" tw="flex items-center justify-center rounded-md border border-transparent bg-white px-5 py-3 text-base font-medium text-indigo-600">Learn more</a>
93
- </div>
94
- </div>
95
- </div>
96
- </div>
97
- `;
98
-
99
- const fontFile = await fetch('https://og-playground.vercel.app/inter-latin-ext-400-normal.woff');
100
- const fontData: ArrayBuffer = await fontFile.arrayBuffer();
101
-
102
- export const GET: RequestHandler = async () => {
103
- return await new ImageResponse(template, {
104
- height: 630,
105
- width: 1200,
106
- fonts: [
107
- {
108
- name: 'Inter Latin',
109
- data: fontData,
110
- weight: 400
111
- }
112
- ]
113
- });
114
- };
35
+ ```bash
36
+ pnpm install @ethercorps/sveltekit-og
115
37
  ```
116
38
 
117
- Then run `npm dev` and visit `localhost:5173/og` to view your generated PNG. Remember that hot module reloading does not work with server routes, so if you change your HTML or CSS, hard refresh the route to see changes.
118
-
119
- ## Example Output
120
-
121
- ![Rendered OG image](https://github.com/etherCorps/sveltekit-og/blob/main/static/og.png)
122
-
123
- ## Headers
124
-
125
- When run in development, image headers contain `cache-control: no-cache, no-store`. In production, image headers contain `'cache-control': 'public, immutable, no-transform, max-age=31536000'`, which caches the image for 1 year. In both cases, the `'content-type': 'image/png'` is used.
126
-
127
- ## Styling
128
-
129
- Notice that our example uses TailwindCSS classes (e.g. `tw="bg-gray-50"`). Alternatively, your HTML can contain style attributes using any of [the subset of CSS supported by Satori](https://github.com/vercel/satori#css).
130
-
131
- Satori supports only a subset of HTML and CSS. For full details, see [Satori’s documentation](https://github.com/vercel/satori#documentation). Notably, Satori only supports flex-based layouts.
132
-
133
- ## Fonts
39
+ ## Usage
134
40
 
135
- Satori supports `ttf`, `otf`, and `woff` font formats; `woff2` is not supported. To maximize the font parsing speed, `ttf` or `otf` are recommended over `woff`.
41
+ For detailed usage instructions, please see the [Getting Started](https://sveltekit-og.dev/docs/getting-started) section of our documentation.
136
42
 
137
- By default, `@ethercorps/sveltekit-og` includes only 'Noto Sans' font. If you need to use other fonts, you can specify them as shown in the example. Notably, you can also import a font file that is stored locally within your project and are not required to use fetch.
43
+ Prefer the Takumi engine — more output formats and a built-in font? See the [Takumi Engine](https://sveltekit-og.dev/docs/usage/takumi) guide.
138
44
 
139
45
  ## Examples
140
46
 
141
- - `ImageResponse` · [_source_](/src/routes/+server.ts) · [_demo_](https://vercel.sveltekit-og.dev)
142
- - `Component Rendering` · [_source_](/src/routes/sc/+server.ts) · [_demo_](https://vercel.sveltekit-og.dev/sc)
143
-
144
- ## API Reference
145
-
146
- The package exposes an `ImageResponse` constructors, with the following options available:
147
-
148
- ```typescript
149
- import {ImageResponse} from '@ethercorps/sveltekit-og'
150
- import {SvelteComponent} from "svelte";
151
-
152
- ImageResponse(
153
- element : string | Component,
154
- options : {
155
- width ? : number = 1200
156
- height ? : number = 630,
157
- backgroundColor ? : string = "#fff"
158
- fonts ? : {
159
- name: string,
160
- data: ArrayBuffer,
161
- weight: number,
162
- style: 'normal' | 'italic'
163
- }[]
164
- debug ? : boolean = false
165
- // Options that will be passed to the HTTP response
166
- status ? : number = 200
167
- statusText ? : string
168
- headers ? : Record<string, string>
169
- },
170
- // Component props if components.
171
- ComponentProps<Component>
172
- )
173
- ```
174
-
175
- ## Changelog
176
-
177
- ### v4.0.0 (Breaking Changes)
178
-
179
- > Just install @ethercorps/sveltekit-og
180
-
181
- > Support for NodeJS, Deno, Cloudflare Pages, Cloudflare Workers, Vercel and Netlify.
182
-
183
- > No support for Bun tried and failed.
184
-
185
-
186
- ### v3.0.0 (Breaking Changes)
47
+ - **ImageResponse**: [_source_](/src/routes/+server.ts) · [_demo_](https://vercel.sveltekit-og.dev)
48
+ - **Component Rendering**: [_source_](/src/routes/sc/+server.ts) · [_demo_](https://vercel.sveltekit-og.dev/sc)
187
49
 
188
- > Just install @ethercorps/sveltekit-og
189
- > No wasm as of now, only support for nodejs based runtime.
50
+ ## Contributing
190
51
 
191
- ### v1.2.3 Update (Breaking Changes)
52
+ Contributions are welcome! Please read our [contributing guidelines](CONTRIBUTING.md) to get started.
192
53
 
193
- > Now you have to install dependency by yourself which will make it easier to build for all plateforms.
194
-
195
- ```
196
- npm i @resvg/resvg-js
197
- ```
198
-
199
- ```
200
- npm i satori
201
- ```
202
-
203
- > From now on their will be no issues related to build, and soon this library going to have its own documentation.
204
-
205
- ### v1.2.2 Update (Breaking Change)
206
-
207
- - We don't provide access to satori from `@ethercorps/sveltekit-og`.
208
-
209
- ### v1.0.0 Update (Breaking Changes)
54
+ ## Changelog
210
55
 
211
- Finally, We have added html to react like element like object converter out of the box and with svelte compiler.
212
- Now you can use `{ toReactElement }` with `"@ethercorps/sveltekit-og"` like:
56
+ All notable changes to this project are documented in the [changelog](CHANGELOG.md).
213
57
 
214
- - We have changed to function based instead of class based ImageResponse and componentToImageResponse.
215
- - Removed `@resvg/resvg-wasm` with `@resvg/resvg-js` because of internal errors.
216
- - Removed `satori-html` because now we have `toReactElement` out of the box with svelte compiler.
217
- > If you find a problem related to undefined a please check [_vite.config.js_](/vite.config.ts) and add ` define: { _a: 'undefined' } in config.`
58
+ ## License
218
59
 
219
- > If you find any issue and have suggestion for this project please open a ticket and if you want to contribute please create a new discussion.
60
+ This project is licensed under the [MIT License](LICENSE).
220
61
 
221
62
  ## Acknowledgements
222
63
 
223
- This project will not be possible without the following projects:
64
+ This project would not be possible without the following projects:
224
65
 
225
66
  - [Satori & @vercel/og](https://github.com/vercel/satori)
67
+ - [Takumi](https://takumi.kane.tw)
226
68
  - [Noto by Google Fonts](https://fonts.google.com/noto)
227
69
  - [fineshopdesign](https://github.com/fineshopdesign/cf-wasm)
228
70
 
@@ -0,0 +1,19 @@
1
+ import { type Component } from "svelte";
2
+ /**
3
+ * Render a Svelte component to an HTML string in the browser. The server path uses
4
+ * `svelte/server`'s `render`, which isn't reliable client-side, so here we mount the
5
+ * component instead.
6
+ *
7
+ * Mounted inside a detached shadow root: the host is never added to the document, so
8
+ * page CSS can't cascade into the component (nothing from the page leaks into the
9
+ * rendered image), and the component's own scoped styles stay in the shadow root
10
+ * instead of piling up in the page's <head> on every render.
11
+ *
12
+ * Loaded lazily by render.js, only on the main thread and only when a component is
13
+ * passed — string-only users and workers never ship this (or Svelte's mount runtime).
14
+ *
15
+ * ponytail: captures innerHTML only — inline styles come through, but scoped /
16
+ * `css="injected"` styles won't (they're separate nodes an engine ignores). Use
17
+ * inline styles (or the `stylesheets`/tailwind options) for client component rendering.
18
+ */
19
+ export declare function componentToHtml(component: Component<any>, props: Record<string, unknown>): string;
@@ -0,0 +1,32 @@
1
+ import { mount, unmount, flushSync } from "svelte";
2
+ /**
3
+ * Render a Svelte component to an HTML string in the browser. The server path uses
4
+ * `svelte/server`'s `render`, which isn't reliable client-side, so here we mount the
5
+ * component instead.
6
+ *
7
+ * Mounted inside a detached shadow root: the host is never added to the document, so
8
+ * page CSS can't cascade into the component (nothing from the page leaks into the
9
+ * rendered image), and the component's own scoped styles stay in the shadow root
10
+ * instead of piling up in the page's <head> on every render.
11
+ *
12
+ * Loaded lazily by render.js, only on the main thread and only when a component is
13
+ * passed — string-only users and workers never ship this (or Svelte's mount runtime).
14
+ *
15
+ * ponytail: captures innerHTML only — inline styles come through, but scoped /
16
+ * `css="injected"` styles won't (they're separate nodes an engine ignores). Use
17
+ * inline styles (or the `stylesheets`/tailwind options) for client component rendering.
18
+ */
19
+ export function componentToHtml(component, props) {
20
+ const host = document.createElement("div");
21
+ const shadow = host.attachShadow({ mode: "open" });
22
+ const target = document.createElement("div");
23
+ shadow.appendChild(target);
24
+ const instance = mount(component, { target, props });
25
+ try {
26
+ flushSync();
27
+ return target.innerHTML;
28
+ }
29
+ finally {
30
+ unmount(instance);
31
+ }
32
+ }
@@ -0,0 +1,7 @@
1
+ import type { Component, ComponentProps } from "svelte";
2
+ import type { ClientImageResponseOptions } from "./types.js";
3
+ /**
4
+ * Same API as the client `ImageResponse`, as a plain function. Returns a
5
+ * `Response` carrying the rendered image bytes.
6
+ */
7
+ export declare function createImage<T extends string | Component<any>>(element: T, options?: ClientImageResponseOptions, props?: T extends Component<any> ? ComponentProps<T> : never): Response;
@@ -0,0 +1,8 @@
1
+ import { ImageResponse } from "./image-response.js";
2
+ /**
3
+ * Same API as the client `ImageResponse`, as a plain function. Returns a
4
+ * `Response` carrying the rendered image bytes.
5
+ */
6
+ export function createImage(element, options, props) {
7
+ return new ImageResponse(element, options, props);
8
+ }
@@ -0,0 +1,4 @@
1
+ import type { ImageOptions } from "../../types.js";
2
+ /** Engine entry: HTML string in, svg string or png bytes out. Fonts are required here —
3
+ * render.js fills in the bundled defaults when the caller passes none. */
4
+ export declare function render(html: string, options: ImageOptions): Promise<Uint8Array | string>;
@@ -0,0 +1,81 @@
1
+ import _satori, { init as initSatoriWasm } from "satori/standalone";
2
+ import { Resvg as _Resvg, initWasm as initResvgWasm } from "@resvg/resvg-wasm";
3
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
4
+ // @ts-ignore - Vite resolves ?url to the emitted asset path
5
+ import yogaUrl from "satori/yoga.wasm?url";
6
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
7
+ // @ts-ignore - Vite resolves ?url to the emitted asset path
8
+ import resvgWasmUrl from "@resvg/resvg-wasm/index_bg.wasm?url";
9
+ import { loadDynamicAsset } from "../../helpers/emoji.js";
10
+ import { DEFAULT_WIDTH } from "../../helpers/defaults.js";
11
+ import { createVNode } from "../../helpers/toJSX.js";
12
+ import { createLogger } from "../../helpers/logger.js";
13
+ import { handleAsync, ErrorCodes } from "../../helpers/error-handler.js";
14
+ /*
15
+ * Browser/worker Satori + ReSVG engine. Loaded lazily by render.js so takumi users
16
+ * never download it. The wasm comes from same-origin bundler-emitted assets
17
+ * (Vite ?url + fetch(new URL(...))): no CORS, works in browsers and workers alike.
18
+ * Kept out of the server providers so neither bundle pulls in the other's runtime.
19
+ *
20
+ * Each init is memoized; a rejected init clears the cache so the next call retries.
21
+ */
22
+ let satoriPromise;
23
+ let resvgPromise;
24
+ function useSatori() {
25
+ satoriPromise ??= (async () => {
26
+ const bytes = await fetch(new URL(yogaUrl, import.meta.url)).then((r) => r.arrayBuffer());
27
+ await initSatoriWasm(await WebAssembly.compile(bytes));
28
+ return _satori;
29
+ })().catch((error) => {
30
+ satoriPromise = undefined;
31
+ throw error;
32
+ });
33
+ return satoriPromise;
34
+ }
35
+ function useResvg() {
36
+ resvgPromise ??= (async () => {
37
+ await initResvgWasm(fetch(new URL(resvgWasmUrl, import.meta.url)));
38
+ return _Resvg;
39
+ })().catch((error) => {
40
+ resvgPromise = undefined;
41
+ throw error;
42
+ });
43
+ return resvgPromise;
44
+ }
45
+ /** Engine entry: HTML string in, svg string or png bytes out. Fonts are required here —
46
+ * render.js fills in the bundled defaults when the caller passes none. */
47
+ export function render(html, options) {
48
+ return options.format === "svg" ? createSvg(html, options) : createPng(html, options);
49
+ }
50
+ async function createSvg(html, options) {
51
+ const log = createLogger(options.debug ?? false);
52
+ const vnodes = createVNode(html);
53
+ const satori = await handleAsync(() => useSatori(), ErrorCodes.SATORI_INIT_FAILED, "Failed to initialize Satori");
54
+ const satoriOptions = { ...options };
55
+ satoriOptions.loadAdditionalAsset = loadDynamicAsset({
56
+ emoji: options.emoji,
57
+ });
58
+ log.debug("Generating SVG with Satori (client)");
59
+ return handleAsync(() => satori(vnodes, satoriOptions), ErrorCodes.SATORI_RENDER_FAILED, "Failed to render SVG with Satori");
60
+ }
61
+ async function createPng(html, options) {
62
+ const log = createLogger(options.debug ?? false);
63
+ const svg = await createSvg(html, options);
64
+ const Resvg = await handleAsync(() => useResvg(), ErrorCodes.RESVG_INIT_FAILED, "Failed to initialize ReSVG");
65
+ const resvg_options = {
66
+ fitTo: { mode: "width", value: options.width || DEFAULT_WIDTH },
67
+ };
68
+ log.debug("Rendering PNG with ReSVG (client)");
69
+ return handleAsync(async () => {
70
+ // free the wasm-backed objects so repeated client renders don't grow wasm memory
71
+ const resvg = new Resvg(svg, resvg_options);
72
+ const rendered = resvg.render();
73
+ try {
74
+ return rendered.asPng();
75
+ }
76
+ finally {
77
+ rendered.free();
78
+ resvg.free();
79
+ }
80
+ }, ErrorCodes.RESVG_RENDER_FAILED, "Failed to render PNG with ReSVG");
81
+ }
@@ -0,0 +1,7 @@
1
+ import type { TakumiImageOptions } from "../../takumi/types.js";
2
+ /**
3
+ * Takumi engine boundary for the client. Exists only so render.js can `import()` it
4
+ * lazily (own Vite chunk); the actual render is the shared takumi path. Takumi's wasm
5
+ * embeds a sans-serif font, so no default fonts are injected here.
6
+ */
7
+ export declare function render(html: string, options: TakumiImageOptions): Promise<Uint8Array | string>;
@@ -0,0 +1,9 @@
1
+ import { createTakumiImage } from "../../takumi/render.js";
2
+ /**
3
+ * Takumi engine boundary for the client. Exists only so render.js can `import()` it
4
+ * lazily (own Vite chunk); the actual render is the shared takumi path. Takumi's wasm
5
+ * embeds a sans-serif font, so no default fonts are injected here.
6
+ */
7
+ export function render(html, options) {
8
+ return createTakumiImage(html, options);
9
+ }
@@ -0,0 +1,2 @@
1
+ import type { Font } from "../types.js";
2
+ export declare function defaultClientFonts(): Promise<Font[]>;
@@ -0,0 +1,36 @@
1
+ import { handleAsync, ErrorCodes } from "../helpers/error-handler.js";
2
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
3
+ // @ts-ignore - Vite resolves ?url to the emitted asset path
4
+ import regularUrl from "./assets/NotoSans-Regular.ttf?url";
5
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
6
+ // @ts-ignore - Vite resolves ?url to the emitted asset path
7
+ import boldUrl from "./assets/NotoSans-Bold.ttf?url";
8
+ /*
9
+ * Bundled fallback fonts for the client satori engine. The server falls back to a
10
+ * CDN that doesn't send CORS headers, so browsers can't use it; these ship with the
11
+ * package and are served same-origin from the consumer's own build output.
12
+ *
13
+ * Loaded once and memoized; a rejected load clears the memo so the next render retries.
14
+ * Takumi doesn't need this — its wasm embeds a sans-serif.
15
+ */
16
+ let fontsPromise;
17
+ export function defaultClientFonts() {
18
+ fontsPromise ??= load().catch((error) => {
19
+ fontsPromise = undefined;
20
+ throw error;
21
+ });
22
+ return fontsPromise;
23
+ }
24
+ async function fetchFont(url) {
25
+ const res = await fetch(new URL(url, import.meta.url));
26
+ if (!res.ok)
27
+ throw new Error(`${res.status} fetching ${url}`);
28
+ return res.arrayBuffer();
29
+ }
30
+ async function load() {
31
+ const [regular, bold] = await handleAsync(() => Promise.all([fetchFont(regularUrl), fetchFont(boldUrl)]), ErrorCodes.FONT_LOAD_FAILED, "Failed to load the bundled default fonts");
32
+ return [
33
+ { name: "Noto Sans", data: regular, weight: 400, style: "normal" },
34
+ { name: "Noto Sans", data: bold, weight: 700, style: "normal" },
35
+ ];
36
+ }
@@ -0,0 +1,13 @@
1
+ import type { Component, ComponentProps } from "svelte";
2
+ import type { ClientImageResponseOptions } from "./types.js";
3
+ /**
4
+ * Client-side OG image, rendered in the browser (or a worker) with the engine
5
+ * you pick via `options.engine` ("takumi" | "satori", defaults to "takumi").
6
+ * Extends `Response`, so consume it with `URL.createObjectURL(await res.blob())`.
7
+ */
8
+ export declare class ImageResponse<T extends string | Component<any>> extends Response {
9
+ constructor(element: T, options?: ClientImageResponseOptions, props?: T extends Component<any> ? ComponentProps<T> : never);
10
+ arrayBuffer(): Promise<ArrayBuffer>;
11
+ blob(): Promise<Blob>;
12
+ text(): Promise<string>;
13
+ }
@@ -0,0 +1,88 @@
1
+ import { createClientImage } from "./render.js";
2
+ import { buildImageResponse, CONTENT_TYPES } from "../helpers/response.js";
3
+ import { ImageResponseError, ErrorCodes } from "../helpers/error-handler.js";
4
+ import { DEFAULT_WIDTH, DEFAULT_HEIGHT } from "../helpers/defaults.js";
5
+ const DEFAULT_OPTIONS = {
6
+ engine: "takumi",
7
+ width: DEFAULT_WIDTH,
8
+ height: DEFAULT_HEIGHT,
9
+ format: "png",
10
+ emoji: "twemoji",
11
+ debug: false,
12
+ };
13
+ /**
14
+ * Client-side OG image, rendered in the browser (or a worker) with the engine
15
+ * you pick via `options.engine` ("takumi" | "satori", defaults to "takumi").
16
+ * Extends `Response`, so consume it with `URL.createObjectURL(await res.blob())`.
17
+ */
18
+ export class ImageResponse extends Response {
19
+ constructor(element, options, props) {
20
+ const merged = { ...DEFAULT_OPTIONS, ...options };
21
+ const engine = (merged.engine ?? "takumi");
22
+ // Satori only emits png or svg; any other raster format renders as png, so pin it
23
+ // to png here too — otherwise the Content-Type would mislabel png bytes (e.g. webp).
24
+ const format = (engine === "satori" && merged.format !== "svg" ? "png" : (merged.format ?? "png"));
25
+ const opts = { ...merged, format };
26
+ // response-only fields live on both engine option shapes; read them off the raw input
27
+ const resp = (options ?? {});
28
+ const { body, init } = buildImageResponse(() => createClientImage(element, opts, props), {
29
+ label: format.toUpperCase(),
30
+ contentType: CONTENT_TYPES[format],
31
+ debug: merged.debug ?? false,
32
+ headers: resp.headers,
33
+ status: resp.status,
34
+ statusText: resp.statusText,
35
+ });
36
+ super(body, init);
37
+ }
38
+ /*
39
+ * Browsers turn an errored body stream into a bare `TypeError: Failed to fetch`
40
+ * when read through Response's own arrayBuffer()/blob()/text(), which drops the
41
+ * ImageResponseError (and its .code). Reading the stream directly keeps the
42
+ * original error, so override the three readers users actually call.
43
+ */
44
+ async arrayBuffer() {
45
+ const reader = this.body.getReader();
46
+ const chunks = [];
47
+ let size = 0;
48
+ for (;;) {
49
+ let step;
50
+ try {
51
+ step = await reader.read();
52
+ }
53
+ catch (error) {
54
+ throw unwrapRenderError(error);
55
+ }
56
+ if (step.done)
57
+ break;
58
+ chunks.push(step.value);
59
+ size += step.value.byteLength;
60
+ }
61
+ const out = new Uint8Array(size);
62
+ let offset = 0;
63
+ for (const chunk of chunks) {
64
+ out.set(chunk, offset);
65
+ offset += chunk.byteLength;
66
+ }
67
+ return out.buffer;
68
+ }
69
+ async blob() {
70
+ return new Blob([await this.arrayBuffer()], { type: this.headers.get("Content-Type") ?? "" });
71
+ }
72
+ async text() {
73
+ return new TextDecoder().decode(await this.arrayBuffer());
74
+ }
75
+ }
76
+ /**
77
+ * The shared body builder wraps every failure as UNKNOWN_ERROR "Failed to generate X"
78
+ * with the real error in `originalError`. Surface the inner one so callers can check
79
+ * `error.code` (e.g. COMPONENT_IN_WORKER, FONT_LOAD_FAILED).
80
+ * ponytail: unwrap here rather than change the shared helper; fold into the v3 realignment.
81
+ */
82
+ function unwrapRenderError(error) {
83
+ return error instanceof ImageResponseError &&
84
+ error.code === ErrorCodes.UNKNOWN_ERROR &&
85
+ error.originalError instanceof ImageResponseError
86
+ ? error.originalError
87
+ : error;
88
+ }
@@ -0,0 +1,4 @@
1
+ export { ImageResponse } from "./image-response.js";
2
+ export { createImage } from "./create.js";
3
+ export type { ClientImageResponseOptions } from "./types.js";
4
+ export { CustomFont, resolveFonts } from "../fonts.js";
@@ -0,0 +1,6 @@
1
+ export { ImageResponse } from "./image-response.js";
2
+ export { createImage } from "./create.js";
3
+ // CustomFont works anywhere (you supply the bytes); resolveFonts turns it into the
4
+ // shape satori needs. GoogleFont/loadGoogleFont are deliberately not re-exported: a
5
+ // browser can't set the User-Agent, so Google serves woff2 and the loader rejects it.
6
+ export { CustomFont, resolveFonts } from "../fonts.js";
@@ -0,0 +1,7 @@
1
+ import type { Component } from "svelte";
2
+ import type { ClientImageResponseOptions } from "./types.js";
3
+ /**
4
+ * Dispatch to the chosen engine, both running in the browser/worker. Response-only
5
+ * keys are stripped so they don't leak into the engine options.
6
+ */
7
+ export declare function createClientImage(element: string | Component<any>, options: ClientImageResponseOptions, props?: Record<string, unknown>): Promise<Uint8Array | string>;
@@ -0,0 +1,37 @@
1
+ import { ImageResponseError, ErrorCodes } from "../helpers/error-handler.js";
2
+ /** Components need the DOM to mount; workers have none. */
3
+ const hasDocument = () => typeof document !== "undefined";
4
+ async function toHtml(element, props) {
5
+ if (typeof element === "string")
6
+ return element;
7
+ if (!hasDocument()) {
8
+ throw new ImageResponseError("Svelte components can't be rendered in a worker (no document). Pass an HTML string instead.", ErrorCodes.COMPONENT_IN_WORKER);
9
+ }
10
+ // lazy: only main-thread component users download the mounter + Svelte's mount runtime
11
+ const { componentToHtml } = await import("./component.js");
12
+ return componentToHtml(element, props ?? {});
13
+ }
14
+ /**
15
+ * Dispatch to the chosen engine, both running in the browser/worker. Response-only
16
+ * keys are stripped so they don't leak into the engine options.
17
+ */
18
+ export async function createClientImage(element, options, props) {
19
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
20
+ const { engine = "takumi", status, statusText, headers, ...imageOptions } = options;
21
+ // components are rendered to HTML here (browser mount), so engines only ever see a string
22
+ const html = await toHtml(element, props);
23
+ // lazy: one Vite chunk per engine, so the other engine's JS + wasm are never fetched
24
+ if (engine === "satori") {
25
+ const satoriOptions = imageOptions;
26
+ // satori has no built-in font; use the bundled ones unless the caller passed some.
27
+ // Lives here (not in the engine) so it's unit-testable in node, where the engine can't run.
28
+ if (!satoriOptions.fonts?.length) {
29
+ const { defaultClientFonts } = await import("./fonts.js");
30
+ satoriOptions.fonts = await defaultClientFonts();
31
+ }
32
+ const { render } = await import("./engines/satori.js");
33
+ return render(html, satoriOptions);
34
+ }
35
+ const { render } = await import("./engines/takumi.js");
36
+ return render(html, imageOptions);
37
+ }