Skip to content

Render the og card via og-svg instead of node-canvas - #9

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

Render the og card via og-svg instead of node-canvas#9
alcor wants to merge 4 commits into
mainfrom
og-svg-renderer

Conversation

@alcor

@alcor alcor commented Aug 1, 2026

Copy link
Copy Markdown
Member

Replaces OG card generation with a call to the shared og-svg renderer.

Both previous paths were dead

netlify/functions/og.js used node-canvas, a native binding that cannot run on the current runtime.

netlify/edge-functions/og.tsx used Deno's og_edge and was switched off by pointing build.edge_functions at ./netlify/edge-functions-disabled — a directory that does not exist in this repo.

So /og has had no working implementation.

What replaces them

netlify/zones-svg.js builds the card as an SVG string: one column per zone, filled with the same per-hour gradient from colors that the canvas version used, zone name above centre and local time below, white text on night columns and black on day. og.js then POSTs it to the renderer.

Sent as a request body rather than a query payload — these SVGs carry a gradient definition plus two text runs per zone, and would otherwise eat into Cloudflare's 16KB URL cap.

One trap worth flagging

og.tsx and the edge_functions override had to be removed together. Removing the override alone would have let Netlify fall back to the default netlify/edge-functions/ directory and pick up the dead og.tsx, whose own config declares path: "/og" — resurrecting the broken implementation and taking the route from the working one.

Verified

Rendered against the live renderer across zone counts and both empty cases:

200 PNG 1200x630  svg= 1576b  three zones, mixed day/night
200 PNG 1200x630  svg=  568b  single zone
200 PNG 1200x630  svg= 2485b  five zones
200 PNG 1200x630  svg=  229b  empty zones (fallback card)
200 PNG 1200x630  svg=  229b  undefined info

Zone objects were stubbed to the shape common.js produces (zoneStart.hour(), niceZoneName, startString, night).

Known issue, pre-existing and not addressed here

getZoneInfo could not be exercised with plain node. common.js:2 does import tzdata from "tzdata", and that package's main is timezone-data.json, so under Node ≥20 it needs a with { type: "json" } import attribute and otherwise fails with ERR_IMPORT_ATTRIBUTE_MISSING.

This is unchanged by this PR — it sits in the import chain of the old and new og.js identically. Netlify bundles functions with esbuild, which inlines JSON imports, so the deployed function is likely fine. Worth confirming separately, since it also makes the module impossible to unit-test locally.

Removed

netlify/edge-functions/og.tsx, the build.edge_functions override, and the canvas dependency.

🤖 Generated with Claude Code

Both previous paths were dead: netlify/functions/og.js used node-canvas,
and netlify/edge-functions/og.tsx used Deno og_edge but was switched off
by pointing build.edge_functions at a directory that does not exist.

netlify/zones-svg.js now builds the card as an SVG — one column per zone
with the same per-hour gradient, white text on night columns — and og.js
POSTs it to the shared renderer. Sent as a body rather than a query
payload because these SVGs would otherwise eat into the 16KB URL cap.

Deletes og.tsx and the edge_functions override together: removing the
override alone would have let the dead og.tsx claim /og, since its own
config declares that path. Drops the canvas dependency.

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

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@netlify

netlify Bot commented Aug 1, 2026

Copy link
Copy Markdown

Deploy Preview for timezone-fyi failed.

Name Link
🔨 Latest commit d3537cd
🔍 Latest deploy log https://app.netlify.com/projects/timezone-fyi/deploys/6a72484b41eba5000821f46e

@alcor alcor self-assigned this Aug 1, 2026
alcor and others added 3 commits August 4, 2026 02:33
Two problems found by rendering real zone data rather than stubs.

common.js could not be imported outside the bundler: tzdata's package
main is timezone-data.json, so Node rejected the import with
ERR_IMPORT_ATTRIBUTE_MISSING. Adding `with { type: "json" }` makes the
module — and everything importing it — loadable and testable. The import
exists only to force the bundler to include tzdata, per its comment.

With that fixed, real data showed the card was broken. common.js formats
times as `10:30ᴀᴍ` using Latin small capitals (U+1D00, U+1D0D, U+1D18).
That renders fine in a browser with a web font, but essentially no font
ships the Phonetic Extensions block, so resvg drew "10:30" followed by two
missing-glyph boxes. Small capitals are now folded to ordinary capitals in
the card template only, leaving the HTML page's typography alone.

Adds vitest and 13 tests, exercising the real getZoneInfo rather than
fabricated zone objects. The repo had none.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Both deployments share one code path. The Netlify functions are already
plain Request -> Response handlers with no Netlify-specific APIs, so the
Worker delegates to them unchanged; Netlify's behaviour is untouched.
Asset precedence matches too — static/ is served first, including / itself.

Two things had to be fixed for the bundled runtime:

timezonecomplete locates its data with require(tzDataName), a deliberately
dynamic require its source comments as avoiding browserify problems. Node
resolves it at runtime, so Netlify worked by accident; a bundled runtime
with no runtime require cannot, and every zone lookup failed with
"Timezonecomplete needs time zone data". The data is now handed over
explicitly via TzDatabase.init, the library's documented entry point.

/og reaches og-svg through a service binding rather than over HTTP. A
Worker fetching another Worker on the same workers.dev zone is refused
with Cloudflare error 1042. og.js takes an optional env and uses the
binding when present, falling back to fetch on Netlify.

Also surfaces the upstream status when a render fails, instead of a bare
"Could not render card".

Verified against the live Netlify deployment: titles match on every path
tried, all assets serve, and /og returns a 1200x630 PNG in ~120ms.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The if/else that normalizes a zone name or converts a numeric offset sat
under a braceless `if (z.startsWith("GMT+"))`, so it ran for essentially
nothing — and called .startsWith on the numbers that `overrides` yields,
throwing "z.startsWith is not a function".

The surrounding code makes the intent clear: a bare number is an offset in
hours and becomes minutes, which is what timezonecomplete expects; anything
else is a name to look up. Restored both.

Name lookup now tries the value as-is, uppercased, and title-cased per
path segment, so `utc` and `asia/tokyo` resolve rather than failing with
"non-existing time zone name". Unknown names still surface that error.

Zone tokens are also percent-decoded: an IANA name has to encode its slash
to survive the comma-separated path, and nothing decoded it. Labels are
derived from the decoded token too, so an encoded zone no longer reads
"ASIA%2FTOKYO" — while abbreviations stay "JST" and numeric-offset zones
do not label as a number.

  /2pm,utc,jst             0 zones -> UTC 2pm, JST 11pm
  /2pm,Asia%2FTokyo        error   -> TOKYO 2pm
  /2pm,america%2Fnew_york  error   -> NEW YORK 2pm
  /10:30am,pst,est         unchanged
  /2pm,london              still an error (bare cities are not IANA zones)

27 tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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