Skip to content

Make og:image work for SVG: pasted markup, svg: payloads, and UTF-8 - #1

Open
alcor wants to merge 3 commits into
mainfrom
og-svg-renderer
Open

Make og:image work for SVG: pasted markup, svg: payloads, and UTF-8#1
alcor wants to merge 3 commits into
mainfrom
og-svg-renderer

Conversation

@alcor

@alcor alcor commented Aug 1, 2026

Copy link
Copy Markdown
Member

Makes og:image work for SVG. Two independent bugs, both of which meant links got no preview image at all.

Bug 1 — the rasterizer was never reachable

metadata.js built its rasterizer URL as a path:

info.i = "/.netlify/functions/rasterize/" + info.i;

This repo has never contained a rasterize.js. And in the repo that does (itty-bitty), that function reads event.rawQuery — never the path — so a path-shaped call could not have worked either. Probed against production using itty-bitty's copy:

URL form Result
/.netlify/functions/rasterize/svg:<b64> — as this code built it 502
/.netlify/functions/rasterize?<b64> — as the function reads it 200 image/jpeg, 8707 bytes

Now points at the shared og-svg renderer with the payload as a query parameter.

Bug 2 — pasted SVG bypassed the renderer entirely

This is the bigger one, because it was the default path rather than an edge case.

The editor invites SVG code — prompt("Choose an emoji, image URL or SVG code") — and then base64s whatever it is given, with no svg: marker:

if (data.image) path.push("i/" + encodeURIComponent(btoa(data.image).replace(/=/g, "")));

metadata.js only routed to the renderer on an explicit svg: prefix. After decodeURL base64-decoded the value, a pasted SVG was raw markup, matched no branch, and fell through to the bare-hostname case:

og:image = "https://<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"600\"…"

resolveImageUrl now detects markup directly, so all three shapes reach the renderer: pasted markup, an explicit svg: payload, and a bare fragment (og-svg wraps fragments in an <svg> root with a 1200×630 viewport).

It is extracted as a pure exported function so the branching is testable — the handler just calls it.

Bug 3 — UTF-8 was mangled

Found only by rendering against the live service; the unit tests were passing.

btoa is Latin-1 only. An accented character encoded as a raw 0xE9 byte that is not valid UTF-8, so resvg rejected the whole document with a 422. Anything above U+00FF — an emoji, a checkmark — made btoa throw outright before the URL was even built.

The editor now uses a UTF-8-safe encoder, and decodeURL recovers UTF-8 with a fallback to the raw bytes, so links written by the old editor still resolve.

Verified

Every shape, through the editor's real encode path, against the live renderer:

Input Before After
Pasted full SVG https://<svg… 200 PNG 600×315
Bare fragment https://<circle… 200 PNG 1200×630
Accented text 422 200 PNG 400×200
Emoji / btoa throws 200 PNG 400×200
svg: prefixed payload 502 200 PNG
Absolute URL, relative path, bare hostname unchanged unchanged

Tests

Adds vitest and 14 tests. The repo had none.

Worth noting the unit tests alone did not catch the UTF-8 bug — it surfaced only when the end-to-end check against the live renderer disagreed with a green suite. The encoding boundaries are where this code breaks, so the tests now pin them explicitly, including a case asserting that Latin-1 payloads from the old editor still work.

Note on output format

og-svg returns PNG; the old rasterizer produced JPEG. Since no caller ever reached it successfully there is no existing behaviour to preserve, and every crawler accepts PNG.

RENDER_ORIGIN is a single exported constant, so moving og-svg to a dedicated domain later is a one-line change.

🤖 Generated with Claude Code

This repo pointed at /.netlify/functions/rasterize, which it never
contained, so the svg: image feature has never worked at all.

The payload is passed through byte-for-byte rather than re-encoded, since
it may be base64 or percent-encoded SVG depending on who wrote the URL;
og-svg tries base64 first and falls back to percent-decoding.

Renderer: https://github.com/arfct/og-svg

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@alcor alcor closed this Aug 1, 2026
@alcor alcor reopened this Aug 1, 2026
@alcor alcor self-assigned this Aug 1, 2026
alcor and others added 2 commits July 31, 2026 23:46
Same pin as main (b83d6e2) so the deploy preview builds with a Node
version that can install netlify-cli 27.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The editor invites SVG code ("Choose an emoji, image URL or SVG code")
and base64s whatever it is given with no marker, so a pasted SVG arrived
in metadata.js as raw markup. Only an explicit svg: prefix was routed to
the renderer, so markup fell through to the bare-hostname branch and
produced og:image="https://<svg xmlns=..." — no preview at all. This was
the default path for anyone using the editor's image field.

resolveImageUrl now detects markup directly, so a pasted SVG, an explicit
svg: payload, and a bare fragment all reach the renderer. Extracted as a
pure function so the branching is testable; the handler just calls it.

Also fixes UTF-8. btoa is Latin-1 only: an accent encoded as a raw 0xE9
byte that is not valid UTF-8, and resvg rejected the document with a 422,
while anything above U+00FF made btoa throw outright. The editor now uses
a UTF-8-safe encoder and decodeURL recovers UTF-8, falling back to the raw
bytes so links written by the old editor still work.

Adds vitest and 14 tests; the repo had none.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@alcor alcor changed the title Make the svg: og:image work by calling og-svg Make og:image work for SVG: pasted markup, svg: payloads, and UTF-8 Aug 4, 2026
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