Skip to content

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

Open
theetherGit wants to merge 56 commits into
mainfrom
dev
Open

theetherGit wants to merge 56 commits into
mainfrom
dev

Conversation

@theetherGit

Copy link
Copy Markdown
Collaborator

Summary

Promotes dev to main for the 4.4.0 release. Currently published as 4.4.0-next.0 on the next tag.

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 (the CDN has no CORS headers).
    • 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 (browsers otherwise collapse an errored body into TypeError: Failed to fetch).
    • Exports CustomFont + resolveFonts; GoogleFont is not exported (browsers can't set the UA, Google serves woff2).
    • Docs page: usage/client.md. Requires takumi-js with either engine; workers need worker: { format: "es" }.
  • Every example has a live /client page and index cards for both engines.

Fixed

  • sveltekitOG() Vite plugin now applies the wasm rollup plugin to the SSR build only — on the client build it broke the /client entry's Takumi wasm.
  • ultrahtml upgraded to 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 channel dropped.
  • CHANGELOG backfilled for 4.2.1 / 4.3.0; srvx-build example removed.

Verification

  • pnpm test: 34 passed, 1 skipped (satori render is browser-only).
  • Browser checklist (headless Chromium): both engines × png/svg × HTML/component on the main thread; worker HTML renders, components reject; 0 CDN requests; lazy wasm per engine.
  • node-build and satori-only-build examples: production builds pass and /client renders in the built apps.
  • 4.4.0-next.0 published and smoke-installed from the registry.

Before merging

main only publishes a clean version. Bump on dev first, then merge this PR:

git checkout dev && pnpm release   # pick 4.4.0

Merging as-is (version 4.4.0-next.0) is a no-op publish.

🤖 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 fb15450
🔍 Latest deploy log https://app.netlify.com/projects/sveltekit-og/deploys/6ac734c270cd6900081ba2ec

@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 8, 2026 6:15am 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 f628d1e Oct 08 2026, 06:10 AM

@netlify

netlify Bot commented Oct 6, 2026 •

Copy link
Copy Markdown

✅ Deploy Preview for sveltekit-og-dev canceled.

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

Requirements (Kit 3, Bun >= 1.4, build must run under Bun), vite.config
with sveltekit({ adapter }) + sveltekitOG({ esmImport: false }), build/run
scripts, and the examples/bun-build self-test. The package-manager
installer tabs gain a bun tab, with a bun add line in every adapter
snippet.
Single-stage node:24 + Bun 1.4.2 image built from the repo root (pnpm
workspace install filtered to bun-build), vite build under Bun, bun ./build
at runtime. render.yaml deploys it as a free Docker web service from dev.
Verified locally: all image routes 200, /client renders with both engines.
Components using css="injected" emit their <style> verbatim into head; a
multi-line CSS value (e.g. linear-gradient split across lines) makes
satori 0.25's css-gradient-parser throw "object null is not iterable".
HTML strings were already newline-stripped; the component body now gets
the same, and the injected head has its whitespace collapsed.
Renders OG images in the browser with @ethercorps/sveltekit-og/client:
five templates (docs, blog post, release, profile, Svelte component),
engine/format/size/quality controls, live preview with size and timing,
download, and a copy-ready createImage(...) snippet. Built on the docs'
own svecodocs tokens and components; responsive from 320 px up.

Bumps the docs' package dependency to 4.4.0-next.1 (first version with
the /client entry) and adds a Playground anchor to the sidebar.
…lighting

Pick a template (live thumbnails) → Tune it → Preview (sticky on wide
screens, in sequence on narrow) → Take the code. The createImage snippet
is highlighted with @twinkleplop/typescript and the GitHub theme, which
follows the docs' .dark toggle. Thumbnails render once at the template's
native 1200×630 and scale down.
Stage 02 is now the editor: a transparent textarea over a twinkleplop
HTML paint layer with matching metrics and scroll sync, Tab inserts two
spaces, Reset to template. The preview re-renders as you type. The
component template shows Card.svelte's markup read-only beside its
props. Output controls move to the preview stage.
Toolbar (template picker popover, engine/format segments, size, quality),
editor beside the live render, collapsible code drawer; Edit/Preview/Code
tabs below lg. Cmd/Ctrl+Enter renders immediately. HtmlEditor gains a
fill mode so it can stretch inside a pane.
…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).

This branch was successfully deployed

1 active deployment
Preview — fb15450c Deployed Oct 8, 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