Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
b2c3d43
example(srvx-build): add srvx/universal-adapter build example
theetherGit Oct 5, 2026
fbea5f3
fix(deps): upgrade ultrahtml to 1.7.0 and patch pseudo-selector regex
theetherGit Oct 5, 2026
f1fee6c
ci(release): publish -next.x from dev to next, drop beta channel
theetherGit Oct 5, 2026
09704d1
docs(changelog): add 4.3.0 and 4.2.1, drop stray 4.3.1-next.3 entry
theetherGit Oct 5, 2026
c038e50
feat(client): add browser/worker rendering via new /client entry
theetherGit Oct 5, 2026
1bc1b6e
docs(spec): client-side rendering design
theetherGit Oct 5, 2026
afe0b60
docs(spec): vitest-only testing for client rendering
theetherGit Oct 5, 2026
c9461e3
docs(spec): takumi has built-in font; lazy component mounter; known c…
theetherGit Oct 5, 2026
99a2b94
docs(spec): fix error ctor order, cross-origin claim, takumi loader note
theetherGit Oct 5, 2026
c846415
docs(plan): client-side rendering implementation plan
theetherGit Oct 5, 2026
0b64d31
chore(client): drop adapter-node switch and unrelated edits from the …
theetherGit Oct 5, 2026
9a0009b
feat(client): reject components in workers, lazy-load the component m…
theetherGit Oct 5, 2026
9473403
feat(client): load engines lazily so users download only the one they…
theetherGit Oct 5, 2026
87c4ef7
feat(client): bundle Noto Sans as satori's default fonts (same-origin…
theetherGit Oct 5, 2026
395d4f1
chore(client): add dev-only /client/worker demo for the manual checklist
theetherGit Oct 5, 2026
e3055dc
fix(client): surface ImageResponseError (with .code) from blob()/arra…
theetherGit Oct 5, 2026
f5cb77e
docs(client): add client-side rendering guide and changelog entry
theetherGit Oct 5, 2026
0a77220
fix(client): es worker format so workers importing /client build (cod…
theetherGit Oct 5, 2026
8458af8
fix(client): drop GoogleFont from the client entry, export resolveFon…
theetherGit Oct 5, 2026
ae53007
fix(plugin): apply the wasm rollup plugin to the SSR build only
theetherGit Oct 5, 2026
61e88b9
example: add a /client page to every example
theetherGit Oct 5, 2026
5a76e74
chore(examples): remove srvx-build example
theetherGit Oct 5, 2026
43e6b70
example(client): render the client-side image on the index card, fix …
theetherGit Oct 5, 2026
221776d
example(client): add a Satori client-side view
theetherGit Oct 6, 2026
8a3233b
Merge pull request #74 from etherCorps/feat/client-side-rendering
theetherGit Oct 6, 2026
7d6747e
chore: release v4.4.0-next.0
theetherGit Oct 6, 2026
93ee6f9
fix(plugin): gate the wasm loader per environment so it works on Svel…
theetherGit Oct 6, 2026
a40ad28
chore(examples): migrate satori-only and takumi-only to SvelteKit 3
theetherGit Oct 6, 2026
ece2986
chore: release v4.4.0-next.1
theetherGit Oct 6, 2026
5b71010
chore: pin Node 24 for hosted builds (.node-version)
theetherGit Oct 6, 2026
bd0e52f
example(bun-build): add @sveltejs/adapter-bun example
theetherGit Oct 7, 2026
3060498
fix(examples/vercel): pin runtime nodejs22.x so the example builds on…
theetherGit Oct 7, 2026
250b681
docs(runtime): add Bun page for @sveltejs/adapter-bun
theetherGit Oct 7, 2026
a48c3e4
chore(bun-build): Dockerfile + Render Blueprint for the Bun example
theetherGit Oct 7, 2026
c35baa4
fix(satori): normalize injected component styles before parsing
theetherGit Oct 8, 2026
4abf3ff
docs(playground): add /docs/playground for the client API
theetherGit Oct 8, 2026
6493c90
docs(playground): redesign as a four-stage flow with twinkleplop high…
theetherGit Oct 8, 2026
9d2a9d9
docs(playground): colour-highlighted live HTML editor
theetherGit Oct 8, 2026
a799bdf
docs(playground): redesign as a workbench
theetherGit Oct 8, 2026
1f58a02
docs(playground): compare the engine render with the browser's own la…
theetherGit Oct 8, 2026
84b914f
docs(playground): resizable panes, engine and browser renders side by…
theetherGit Oct 8, 2026
0b5f16f
fix(satori): empty elements no longer trip the multiple-children check
theetherGit Oct 8, 2026
4cd9483
docs(playground): show the engine's own error under the ImageResponse…
theetherGit Oct 8, 2026
abad75d
docs(playground): Format button, Tailwind template, satori-safe Docs …
theetherGit Oct 8, 2026
e22962e
docs(playground): satori / takumi / tailwind variant of every template
theetherGit Oct 8, 2026
102df4d
docs(playground): Styling control (Vanilla CSS / Tailwind) keyed by e…
theetherGit Oct 8, 2026
48ad5b7
docs(playground): no page scroll — workbench height accounts for the …
theetherGit Oct 8, 2026
a0b9e85
docs(playground): display names and a one-row mobile toolbar
theetherGit Oct 8, 2026
7591998
docs(playground): animate the code drawer and the popovers
theetherGit Oct 8, 2026
7ddad6d
docs(playground): Render and Format as real buttons in the editor head
theetherGit Oct 8, 2026
e64f48f
docs(playground): Format no longer counts as an edit
theetherGit Oct 8, 2026
7a957f8
docs(playground): Blog post keeps the same layout in every styling
theetherGit Oct 8, 2026
40ddea5
docs(playground): Docs, Release and Profile keep one layout in every …
theetherGit Oct 8, 2026
f628d1e
chore: release v4.4.0-next.2
theetherGit Oct 8, 2026
b4bd4be
docs: use @ethercorps/sveltekit-og 4.4.0-next.2
theetherGit Oct 8, 2026
fb15450
docs: pin Node 24 for Cloudflare Pages
theetherGit Oct 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
.git
**/node_modules
**/build
**/.svelte-kit
**/.vercel
**/.netlify
**/.wrangler
**/dist
.superpowers
.remember
docs
apps/docs/.svelte-kit
13 changes: 5 additions & 8 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
name: Release

# Release channel is driven by the branch you merge into:
# main -> production (dist-tag: latest)
# dev -> beta (dist-tag: beta) version must be a -beta.x prerelease
# any -> next (dist-tag: next) whenever the version contains -next.x
# main -> production (dist-tag: latest) version must be a clean x.y.z
# dev -> next (dist-tag: next) version must be a -next.x prerelease
# A merge only publishes when the version in package.json is NOT already on npm,
# so ordinary merges that don't bump the version are no-ops.
on:
Expand Down Expand Up @@ -44,8 +43,8 @@ jobs:
- run: pnpm install --frozen-lockfile

# Decide the dist-tag from branch + version, or skip if this channel/version
# combination is not allowed. Enforces: production only from main, beta only
# from dev, next from anywhere.
# combination is not allowed. Enforces: production only from main, next only
# from dev.
- name: Resolve release channel
id: channel
working-directory: packages/sveltekit-og
Expand All @@ -55,10 +54,8 @@ jobs:
BRANCH="${GITHUB_REF_NAME}"
echo "name=$NAME"; echo "version=$VERSION"; echo "branch=$BRANCH"

if [[ "$VERSION" == *-next.* ]]; then
if [[ "$BRANCH" == "dev" && "$VERSION" == *-next.* ]]; then
TAG=next
elif [[ "$BRANCH" == "dev" && "$VERSION" == *-beta.* ]]; then
TAG=beta
elif [[ "$BRANCH" == "main" && "$VERSION" != *-* ]]; then
TAG=latest
else
Expand Down
1 change: 1 addition & 0 deletions .node-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
24
15 changes: 3 additions & 12 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,7 @@ npm run format
| Branch | Version | Published to |
| ------ | ----------------- | ---------------- |
| `main` | clean (`4.4.0`) | `latest` (prod) |
| `dev` | `-beta.x` | `beta` |
| any | `-next.x` | `next` |
| `dev` | `-next.x` | `next` |

Any other branch/version combination does **not** publish. A push/merge only publishes when the version is not already on npm — merges that don't bump the version are no-ops.

Expand All @@ -75,22 +74,14 @@ Bump the version with the interactive helper (it commits and pushes to the curre
pnpm release
```

### Beta
### Next (prerelease)

```bash
git checkout dev
pnpm release # pick a -beta version, e.g. 4.4.0-beta.0
```

Pushing to `dev` publishes `@beta`. Install with `npm i @ethercorps/sveltekit-og@beta`.

### Next (experimental preview)

```bash
pnpm release # pick a -next version, e.g. 4.4.0-next.0
```

Publishes `@next` from any branch. Use for throwaway previews you don't want on `@beta`.
Pushing to `dev` publishes `@next`. Install with `npm i @ethercorps/sveltekit-og@next`.

### Production

Expand Down
23 changes: 23 additions & 0 deletions apps/docs/.hallmark/log.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
[
{
"date": "2026-10-08",
"macrostructure": "Workbench",
"theme": "project tokens (svecodocs rose)",
"enrichment": "none",
"brief": "sveltekit-og docs \u00b7 /docs/playground redesigned as a Workbench: toolbar, editor | live render, code drawer"
},
{
"date": "2026-10-08",
"macrostructure": "Narrative Workflow",
"theme": "project tokens (svecodocs rose)",
"enrichment": "none",
"brief": "sveltekit-og docs \u00b7 /docs/playground redesigned as a four-stage flow with twinkleplop highlighting"
},
{
"date": "2026-10-08",
"macrostructure": "Component Playground",
"theme": "project tokens (svecodocs rose)",
"enrichment": "none",
"brief": "sveltekit-og docs \u00b7 /docs/playground for the client API"
}
]
1 change: 1 addition & 0 deletions apps/docs/.node-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
24
8 changes: 6 additions & 2 deletions apps/docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,11 @@
"vite": "^7.2.4"
},
"dependencies": {
"@ethercorps/sveltekit-og": "^4.3.1-next.3",
"octokit": "^5.0.5"
"@ethercorps/sveltekit-og": "^4.4.0-next.2",
"@twinkleplop/html": "^0.1.7",
"@twinkleplop/theme-github": "^0.2.4",
"@twinkleplop/typescript": "^0.1.7",
"octokit": "^5.0.5",
"paneforge": "1.0.2"
}
}
6 changes: 6 additions & 0 deletions apps/docs/src/app.css
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
@import '@svecodocs/kit/theme-rose.css';
@import '@svecodocs/kit/globals.css';
@source "../node_modules/@svecodocs/kit";

/* Hallmark responsive gate 34: no horizontal scroll from clipped-edge content; clip (not hidden) keeps sticky working */
html,
body {
overflow-x: clip;
}
107 changes: 107 additions & 0 deletions apps/docs/src/content/runtime/bun.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
---
title: Bun
description: How to use SvelteKit OG with the official Bun adapter (@sveltejs/adapter-bun)
section: Runtime
priority: 6
---

<script>
import { Callout } from '@svecodocs/kit';
import NodePackageInstallerTabs from "$lib/components/add-ons/installer-tabs.svelte";
import InstallBunAdapter from "$lib/components/add-ons/packages/sveltekit-adapter/bun.md";
</script>

This section details the configuration needed to run SvelteKit OG on the [Bun](https://bun.sh) runtime with the official Bun adapter (`@sveltejs/adapter-bun`). Both engines work: Satori + ReSVG take the Node code path (the wasm is read from `node_modules`), and Takumi resolves its native backend through its `bun` export condition.

<Callout type="note" title="Requirements">

`@sveltejs/adapter-bun` needs **SvelteKit 3** and **Bun 1.4 or newer**. The adapter also requires the build itself to run under Bun — `vite build` started by Node fails with "adapter-bun requires running the SvelteKit build with Bun".

</Callout>

## Installation

Install the Bun adapter:

<NodePackageInstallerTabs component={InstallBunAdapter} selected="bun"/>

SvelteKit 3 has no `svelte.config.js`; pass the adapter to `sveltekit()` in `vite.config.js`:

```javascript title="vite.config.js" showLineNumbers
import adapter from '@sveltejs/adapter-bun';
import { sveltekit } from '@sveltejs/kit/vite';
import { sveltekitOG } from '@ethercorps/sveltekit-og/plugin';

const config = {
plugins: [sveltekit({ adapter: adapter() }), sveltekitOG({ esmImport: false })]
};

export default config;
```

## Plugin Configuration

Use the `sveltekitOG` Vite plugin with `{ esmImport: false }`, exactly as on Node: the Wasm module is then loaded with Bun's Node-compatible file APIs instead of an ESM `.wasm` import. The plugin only touches the server bundle, so the [client-side entry](/docs/usage/client) keeps working alongside it.

## Build and run

Build with Bun (not Node) and start the generated server:

```json title="package.json" showLineNumbers
{
"scripts": {
"build": "bun run --bun vite build",
"start": "bun ./build"
}
}
```

```shell title="Bash"
bun run build
bun run start
```

The server listens on `PORT` (default `3000`). See the adapter docs for `HOST`, `SOCKET_PATH`, proxy headers and the other environment variables.

## Usage

Once configured, usage is the same as any other SvelteKit environment.

- Svelte Components: refer to the [Svelte Component](/docs/usage/svelte) usage.
- Raw HTML: refer to the [Raw HTML section](/docs/usage/html) for string templates.
- Takumi: refer to the [Takumi engine](/docs/usage/takumi) page.

## Preview (Self Test)

Source: https://github.com/etherCorps/sveltekit-og/tree/main/examples/bun-build

### Step-by-Step Guide

- Clone the repository and navigate:

```shell title="Bash"
git clone https://github.com/etherCorps/sveltekit-og.git
cd sveltekit-og/examples/bun-build
```

- Install dependencies:

```shell title="Bash"
pnpm install
```

- Build with Bun:

```shell title="Bash"
bun run build
```

- Start the server:

```shell title="Bash"
bun run start
```

Then open [http://localhost:3000](http://localhost:3000) to browse the example gallery — the **PNG**, **SVG** and **Takumi** routes, each as an HTML string, a Svelte component, and a pre-rendered image, plus the client-side renders.

More on how to use [adapter-bun in SvelteKit](https://svelte.dev/docs/kit/adapter-bun)
131 changes: 131 additions & 0 deletions apps/docs/src/content/usage/client.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
---
title: Client-side Rendering
description: Generate OG images in the browser or a web worker, with no server request.
section: Usage
priority: 4
---

<script>
import { Callout } from "@svecodocs/kit";
</script>

## Overview

The `@ethercorps/sveltekit-og/client` entry renders images **in the browser** — on the main thread or inside a web worker — using the same two engines as the server: **Takumi** (default) or **Satori + ReSVG**. Nothing is sent to your server.

Use it for live previews in an editor or CMS, or for sites with no server (`adapter-static`) that still want images generated at runtime.

<Callout type="note" title="Try it">

Open the [Playground](/docs/playground) — it renders in your browser with this API and shows the matching `createImage(...)` call.

</Callout>

<Callout type="note" title="Available from v4.4.0">

Client-side rendering is available from `sveltekit-og@4.4.0`. Try it early from the `next` tag: `npm i @ethercorps/sveltekit-og@next`.

</Callout>

## Requirements

- **Vite** (every SvelteKit app). The entry loads its WebAssembly and fonts through Vite `?url` asset imports; other bundlers are not supported.
- `takumi-js` must be installed to use the client entry with **either** engine: the bundler resolves the Takumi chunk at build time even if you only ever pick Satori.
- Pages that render client-side must run in the browser: set `export const ssr = false;` in the route's `+page.ts`, or only call the API inside `onMount`/event handlers.

## Usage

```ts title="src/routes/preview/+page.svelte (script)" showLineNumbers
import { createImage } from "@ethercorps/sveltekit-og/client";

const html = `<div style="display:flex;width:100%;height:100%;align-items:center;justify-content:center;font-size:64px">Hello</div>`;

// Takumi is the default engine
const res = createImage(html, { width: 1200, height: 630, format: "png" });
const url = URL.createObjectURL(await res.blob());
```

`createImage` (and the `ImageResponse` class, which is the same thing as a `Response` subclass) returns a standard `Response`; read it with `.blob()`, `.arrayBuffer()` or `.text()`.

### Choosing an engine

```ts
// Satori + ReSVG: png or svg
createImage(html, { engine: "satori", format: "svg", width: 1200, height: 630 });

// Takumi: png, jpeg, webp, ico, raw, svg
createImage(html, { engine: "takumi", format: "webp", quality: 80, width: 1200, height: 630 });
```

The options are a discriminated union on `engine`, so TypeScript narrows the remaining fields (formats, fonts, emoji) to the engine you picked. Satori raster formats other than `png` are rendered as `png`.

**Only the engine you use is downloaded.** Each engine is a separate chunk; Takumi users never fetch Satori's WebAssembly and vice versa. Rough first-render downloads: Takumi ≈ 3.7 MB wasm; Satori ≈ 2.6 MB wasm (yoga + resvg) plus ≈ 1.2 MB of default fonts unless you pass your own.

### Svelte components

```ts
import Card from "./Card.svelte";

createImage(Card, { width: 1200, height: 630 }, { title: "Hello", subtitle: "from the browser" });
```

Components are mounted in a detached shadow root and their HTML is captured, so page CSS can't leak into the image. **Only inline styles are captured** — scoped `<style>` blocks and `css="injected"` are not. Use inline styles, or the `stylesheets` / Tailwind options.

## Web workers

Rendering works inside a module worker: `new Worker(new URL("./worker.ts", import.meta.url), { type: "module" })`.

The client entry code-splits (one chunk per engine), and Vite's default worker format (`iife`) can't code-split, so set the ES format in `vite.config`:

```js title="vite.config.js"
export default defineConfig({
plugins: [sveltekit()],
worker: { format: "es" },
});
```

Without it, `vite dev` works but `vite build` fails with `Invalid value "iife" for option "output.format"`.

Workers have no DOM, so **only HTML strings can be rendered there**. Passing a Svelte component rejects with an error whose `code` is `COMPONENT_IN_WORKER`:

```ts
try {
await createImage(Card, { width: 1200, height: 630 }, props).arrayBuffer();
} catch (err) {
if ((err as { code?: string }).code === "COMPONENT_IN_WORKER") {
// render the component to HTML on the main thread first, then post the string
}
}
```

## Errors

Render failures reject from `.blob()` / `.arrayBuffer()` / `.text()` with an `ImageResponseError` carrying a `code` (`COMPONENT_IN_WORKER`, `FONT_LOAD_FAILED`, `SATORI_RENDER_FAILED`, `TAKUMI_RENDER_FAILED`, …). Read the response through one of those three methods. Reading `res.body` with your own reader gives you a wrapper error whose `originalError` holds the coded one; any other `Response` helper (`clone()`, `bytes()`) surfaces a browser-generic `TypeError: Failed to fetch` instead.

## Fonts

- **Takumi** has a built-in sans-serif: text renders with no setup.
- **Satori** needs font data. If you pass no `fonts`, the package uses a bundled **Noto Sans** (regular + bold) served from your own site — no cross-origin requests, works offline.

To use your own fonts, supply the bytes with `CustomFont` (put the file in `static/` or import it with `?url`). Satori wants resolved data, so pass it through `resolveFonts`; Takumi accepts `CustomFont` instances directly:

```ts
import { createImage, CustomFont, resolveFonts } from "@ethercorps/sveltekit-og/client";

const inter = new CustomFont("Inter", () => fetch("/fonts/Inter-Bold.ttf").then((r) => r.arrayBuffer()), { weight: 700 });

// Satori
createImage(html, { engine: "satori", width: 1200, height: 630, fonts: await resolveFonts([inter]) });

// Takumi
createImage(html, { engine: "takumi", width: 1200, height: 630, fonts: [inter] });
```

`GoogleFont` is **not** available on the client entry: browsers can't change their User-Agent, so Google Fonts serves `woff2`, which neither engine's loader accepts. Download the TTF and use `CustomFont`.

## Limitations

- Vite-only (`?url` asset imports).
- Components: inline styles only; HTML strings only inside workers.
- The Satori path parses HTML with `satori-html` in your bundle. Vite's default minifier is fine; if you minify with terser `ascii_only`, selectors like `:not()` break in the parser — a known upstream `ultrahtml` issue.
- Takumi options beyond `width`, `height`, `format`, `quality`, `stylesheets`, `emoji` and `fonts` are not passed through yet.
2 changes: 1 addition & 1 deletion apps/docs/src/lib/components/add-ons/installer-tabs.svelte
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<script lang="ts">
import { Tabs, TabItem } from '@svecodocs/kit';
const managers = ['pnpm', 'npm', 'yarn', 'deno'];
const managers = ['pnpm', 'npm', 'yarn', 'bun', 'deno'];
import type { Component } from 'svelte';

type Props = {
Expand Down
Loading
Loading