Skip to content

release: v4.4.0 — client-side rendering - #78

Merged
theetherGit merged 61 commits into
mainfrom
dev
Oct 14, 2026
Merged

theetherGit merged 61 commits into
mainfrom
dev

Conversation

@theetherGit

@theetherGit theetherGit commented Oct 6, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Promotes dev to main for the 4.4.0 release. dev is bumped to a clean 4.4.0; merging publishes it to latest (OIDC). Pre-releases shipped as 4.4.0-next.0 … next.2.

Added

  • @ethercorps/sveltekit-og/client (feat(client): browser/worker rendering via /client entry #74): render OG images in the browser or a web worker with Takumi (default) or Satori + ReSVG — no server request.
    • Engines load lazily via dynamic import(); only the one you pick is downloaded.
    • Satori falls back to bundled Noto Sans served same-origin.
    • Components render on the main thread; in workers they reject with COMPONENT_IN_WORKER, HTML strings work everywhere.
    • Errors reach .blob() / .arrayBuffer() / .text() as ImageResponseError with a code.
    • Requires takumi-js with either engine; workers need worker: { format: "es" }.
  • SvelteKit 3 (Vite 8) alongside Kit 2; the sveltekitOG() plugin uses the Environment API when present. Satori, Takumi and Bun examples run on Kit 3.
  • @sveltejs/adapter-bun example (examples/bun-build, deployed on Render) and a Bun runtime guide.
  • Every example has a live /client page for both engines.
  • Docs playground (/docs/playground): editor | Takumi/Satori render ÷ browser layout, resizable panes, Vanilla CSS / Tailwind variants of every template, formatter.

Fixed

  • Empty elements (<div></div> dots, dividers) made Satori throw the multiple-children error — satori-html emits children: []; dropped before rendering, server and client.
  • Multi-line css="injected" styles (e.g. a multi-line linear-gradient) crashed satori 0.25; whitespace-normalized like HTML strings.
  • sveltekitOG() applies the wasm loader to the SSR build only (it broke the /client entry's Takumi wasm on the client build).
  • ultrahtml 1.7.0 with a repo patch for the pseudo-selector regex (breaks under ascii-escaping minifiers).

Chores

  • Release CI: dev + -next.x → next; main + clean version → latest; beta dropped.
  • CHANGELOG backfilled for 4.2.1 / 4.3.0 and cut for 4.4.0; srvx-build example removed; Node 24 pinned for hosted builds.

Verification

  • pnpm test: 37 passed, 1 skipped. svelte-package + publint clean.
  • Browser checklist (headless Chromium): both engines × formats × HTML/component on the main thread; worker HTML renders, components reject; lazy wasm per engine.
  • Built examples (node, satori Kit 3, takumi Kit 3, bun) render /client; vercel/netlify/cloudflare builds green on Node 24.
  • 4.4.0-next.2 published and in use by the docs site.

After merging

  • Bump apps/docs to ^4.4.0 and drop the @next install hints (docs page + playground).

🤖 Generated with Claude Code

Routes cover png/svg/takumi x default/cog/prerendered, built with
@ethercorps/svelte-adapter-universal for node, bun, deno and workers.
ultrahtml rebuilds its pseudo-class regex by string-matching
"(?<argument>¶*)" inside another regex's .source. Minifiers that escape
non-ASCII (e.g. terser ascii_only emits \xb6) break the match, so
:not() and :nth-child() throw. 1.7.0 still has the bug.

Match the argument group regardless of escaping: /\(\?<argument>[^)]*\)/.
Replaces the 1.6.0 patch, which only covered ¶ and \u00B6.
dev + -next.x -> next, main + clean version -> latest. -next.x no longer
publishes from main, and the beta channel is removed.
New `@ethercorps/sveltekit-og/client` export for generating OG images
in the browser or a worker, no server request. Single surface:
`ImageResponse` (extends Response) + `createImage`, with the engine
chosen in config: `{ engine: 'takumi' | 'satori', ...opts }` (default
takumi), typed as a per-engine discriminated union.

Fully self-contained under src/lib/client/ — own browser wasm providers
(satori/yoga + resvg via bundler `?url` + fetch(new URL), same-origin so
no CORS, worker-safe) and own satori/resvg render path that reuses only
the engine-agnostic shared helpers. Server render code stays untouched.

- Svelte components rendered client-side via mount into a detached
  shadow root (not svelte/server), isolating page CSS from the image
  and the component's styles from the page.
- Playground demo route at /client (ssr=false).
- Free resvg wasm objects after render; clamp satori raster formats to
  png so Content-Type matches the bytes; revoke object URLs only once
  the replacement is ready.
…yBuffer()/text() in browsers

Browsers reject Response.arrayBuffer()/blob()/text() with a bare
"TypeError: Failed to fetch" when the body stream errors, dropping the
ImageResponseError and its code. Read the stream directly instead, and
unwrap the shared builder's UNKNOWN_ERROR wrapper so callers see the
inner code (COMPONENT_IN_WORKER, FONT_LOAD_FAILED, ...).
…e-split chunks)

Vite's default iife worker format can't code-split, and the client entry
lazy-imports its engines, so `vite build` failed for any worker using it.
The demo worker also stops importing a .svelte file (the worker sub-build
has no Svelte plugin); a non-string stand-in exercises the same guard.
…ts, fix docs

Browsers can't set the User-Agent, so Google Fonts serves woff2 and
loadGoogleFont rejects it. Satori needs resolved font data, so the docs
example now goes through resolveFonts with CustomFont. Docs also state
that takumi-js is required with either engine (the takumi chunk is
resolved at build time) and that workers need worker.format "es".
sveltekitOG() added unwasm to every build. On the client build it rewrote
the /client entry's takumi wasm into something WebAssembly.instantiate
can't load. Vite computes env.isSsrBuild before SvelteKit sets build.ssr,
so check the merged config and enforce: post to run after sveltekit().
Shared ClientDemo component (@examples/shared/client) renders one PNG in
the browser with the default engine; satori-only passes engine="satori".
Index pages gain a Client-side section. Examples now use sveltekitOG()
instead of a raw rollupWasm in build.rollupOptions so the plugin can stay
off the client build. satori-only gains takumi-js: the client entry
resolves the takumi chunk at build time regardless of engine.
…/client page layout

New shared ClientImage component renders the PNG on mount; the index
card shows it as a real thumbnail (same height as the other cards —
the old 'open page' placeholder broke the aspect ratio) and /client
reuses it. The /client page's dark background moves to body so no white
gutters appear, and its header matches the index hero.
Index pages show two live client cards (Takumi default, Satori · resvg);
/client picks the engine from ?engine=satori and offers a Takumi/Satori
switch. satori-only and takumi-only keep their single card.
feat(client): browser/worker rendering via /client entry
@netlify

netlify Bot commented Oct 6, 2026 •

Copy link
Copy Markdown

✅ Deploy Preview for sveltekit-og canceled.

Name Link
🔨 Latest commit f77d532
🔍 Latest deploy log https://app.netlify.com/projects/sveltekit-og/deploys/6acdf835dde1b158f1c26106

@vercel

vercel Bot commented Oct 6, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
sveltekit-og-vercel Ready Ready Preview Oct 13, 2026 9:22am UTC

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Oct 6, 2026 •

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
sveltekit-og 79bae83 Oct 13 2026, 09:03 AM

@netlify

netlify Bot commented Oct 6, 2026 •

Copy link
Copy Markdown

✅ Deploy Preview for sveltekit-og-dev canceled.

Name Link
🔨 Latest commit f77d532
🔍 Latest deploy log https://app.netlify.com/projects/sveltekit-og-dev/deploys/6acdf83520a5a9a9d9a93865

…yout

Preview pane gets an engine / browser / both toggle. The browser view lays
the same markup out in a sandboxed iframe (or mounts Card.svelte) at the
OG size and scales it to fit; frames size from the pane height so two
stack without cropping.
… side

paneforge splits editor | preview horizontally and the preview into
engine / browser shells vertically; every divider drags (keyboard too),
layout persists via autoSaveId. Each render gets its own header with its
own facts. Below lg the shells become Edit / engine / Browser / Code tabs.
satori-html emits children: [] for every empty element and satori treats
any array of children on a non-flex <div> as more than one child, so an
empty decorative <div> (a dot, a rule) always failed with SATORI_RENDER_FAILED.
Drop empty children arrays in createVNode; covers the server and client paths.
…template

- Format pretty-prints the markup: one element per line, long style
  attributes split into one declaration per line (the engines strip
  newlines and trim, so the render is unchanged).
- Tailwind template using the tw attribute, rendered by both engines; the
  browser shell maps tw to class and loads the Tailwind browser build for
  it (sandbox allow-scripts, opaque origin).
- Docs template's dot <div> gets display:flex so it renders on Satori with
  the published package too (the real fix is in createVNode).
Each HTML template now ships three variants: satori (flex only, the subset
both engines share), takumi (CSS grid, box-shadow, gradients — Satori
rejects grid, and the playground says so), tailwind (tw attribute, both
engines). A Variant control sits next to the template; picking takumi
follows with the engine. The standalone Tailwind template folds into the
variants.
…ngine

The three-way satori / takumi / tailwind variant read like a second engine
picker. Now the toolbar has Engine and Styling; the vanilla template is
chosen per engine (grid on Takumi, flex on Satori) and Tailwind is shared.
Switching engine reloads the template unless the markup was edited.
…footer

100dvh minus the sticky header and the footer, and no min-height so short
viewports fit too.
Engines and formats show as Takumi / Satori / PNG / JPEG / WebP / SVG (the
snippet keeps the API values). Below lg the toolbar is Template · Engine ·
Options; styling, format and size live in an Options popover, Download
moves into the render head, Copy into the Code tab, Reset into the editor
head. The workbench is one screen tall on every size, so panes scroll and
the page never does.
The drawer slides open/closed (svelte slide, 180 ms, cubicOut) with the
caret rotating; template and options popovers fade and drop in and out via
@starting-style + allow-discrete. Both collapse to instant under
prefers-reduced-motion.
The passive '⌘↩ renders now' hint becomes a Render button (fires the
immediate render; keycap shows the shortcut on lg) next to Format, both
with icons in the small bordered head-button style shared by Reset, Copy
and Download. Icon-only below sm so the head never clips.
dirty compared raw text, so Format → Satori kept the formatted Takumi grid
markup and Satori rejected it. Compare formatted (the formatter is
idempotent); engine, style and template switches carry the pretty-printed
state over.
The Takumi grid version (title, author row, series card) is now also the
flex version for Satori and the tw version; the Tailwind card is a solid
rose since Satori's tw has no gradients.
…styling

The Takumi grid designs (tiles, shadows, gradients) are rebuilt with flex
for Satori and as tw for Tailwind, so switching engine or styling changes
the markup, not the picture. Tailwind uses solid fills where Satori's tw
has no gradients.
Pages' v2 build reads .node-version from the project root directory
(apps/docs), not the repository root, so it kept picking 22.16 and failed
SvelteKit 3's engines check (>=22.17).
Pages' v2 build ignored the major-only '24' in .node-version (both at the
repo root and in apps/docs) and kept its 22.16 default; its docs only
show a full version, so give it one.
The 'sveltekit-og' Pages project builds from examples/cf-pages-build and
only reads .node-version there; the workspace install resolves the Kit 3
examples too, so it needs >=22.17 even though the example itself is Kit 2.
client.md and the playground pointed at @next; takumi.md still told
people to install 4.3 from a beta tag.
@theetherGit
theetherGit merged commit a693554 into main Oct 14, 2026
14 checks passed

This branch was successfully deployed

1 active deployment
Preview — f77d5327 Deployed Oct 13, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant