← All work
Shipped Solo · Design & Engineering · 2026

Digital Wedding Invitation — Four Renditions of One Printed Card

My own wedding invitation, rebuilt for the phone. One printed card became four digital renditions — two scrolling pages, a 39-second film for WhatsApp, and a set of shareable image cards — all driven by a single content file and one shared, hand-drawn design system. The interesting part is the constraints: a fact that must never be rendered, an Arabic webfont whose failure is invisible, and a QR that has to survive being re-decoded out of a compressed video.

  • Vanilla JS
  • Hand-authored SVG
  • CSS
  • Node.js
  • Playwright
  • Swift · AVFoundation
  • Cloudflare Pages

A printed wedding card is a fixed object: one sheet, one set of facts, laid out by someone who knew exactly how much space each line deserved. This project took my own, for a 2026 wedding, and asked what it becomes when the thing people actually receive is a link on a phone.

The answer turned out to be four different objects, not one, because the places an invitation travels to want different shapes: a page you scroll, a film that autoplays in a chat thread, and still images sized for a status and a feed.

The design study behind the mark

Before any of it was built, the envelope seal was drawn eight ways and judged side by side — each rendered large enough to read the line quality, and again at true size on the envelope, because a mark that looks beautiful enlarged can dissolve at 77 pixels on a phone:

Eight hand-authored SVG envelope seals — a rose, a calligraphic cipher, an arabesque knot, a crescent and star, a Deccani lotus roundel, twin roses, a woven medallion, and a single rosebud

The monogram got the same treatment — ten variants, each shown enlarged and at true size, before one was picked. The winner genuinely interlaces rather than overlapping: the S passes in front of the F below the midline, the F comes back over the top, and the heart at the crown is two open half-strokes that cross at the cleft. The over-under is real, not implied.

The chosen interlaced F&S monogram — shown enlarged for line quality, at 77px true size on a phone, and sitting on the envelope flap

What it is

Four renditions, one source of truth:

  • A scrolling page carrying the printed card’s own artwork — quatrefoil damask ground, arabesque watermark, double hairline rule frame, lozenge dividers.
  • A second scrolling page built instead from an invented drawn ornament — a mihrab arch, margin vines and an envelope seal, all hand-authored SVG.
  • A 39.4-second film — 1080×1920, H.264, faststart, 13.64 MiB with a nasheed on an AAC track, so it plays inline in a chat thread on both iOS and Android.
  • Sixteen image cards — four subjects × portrait and square × two raster scales, for a status post and a feed post.

Every visible string in all four comes from one data file. A wrong time on a wedding invitation is unrecoverable once it is sent, so no variant is allowed to hardcode content — change the data, rebuild, and all four move together. The drawn ornament and the card artwork live in shared modules for the same reason: hand-copying them into a variant is what once shipped a stale seal in the film and dropped the label gloss from the cards.

The constraint that shaped everything

This is an Indian-Muslim invitation, not a Western one, and the governing rule was written down before any code: the invitation carries the information that is on the printed card, and nothing more.

That excluded a long list of things that are technically easy and visually attractive — no RSVP (guests are invited, not polled), no “our story” timeline, no photographs of the couple, no travel or stay notes, no gift registry, no share buttons or hashtag. Additions were permitted only where they served information already on the card: a countdown to the printed date, a maps link to the printed venue, and a calendar entry for the printed event.

Two text registers exist — one matching the printed card’s formality for elders and relatives, one warmer for friends, selected by a query parameter. Both carry byte-identical facts; a register is only allowed to rephrase two fields.

A fact that must never be rendered

The ceremony’s actual congregation time is needed — the “Add to Calendar” entry is wrong without it — but it must never appear on screen, where the displayed time is always the words “After Asr namaz.”

So it is stored as two separate integers, never a string, used solely to build the calendar file’s start timestamp. And because “nobody will accidentally concatenate those” is not a guarantee, the build refuses to write the page at all if that clock string appears anywhere in the built body. The same build aborts if an expected tag is missing from the template rather than silently shipping a page with no styles.

Failures that are invisible in a screenshot

Three of this project’s real risks all share a property: the broken version looks fine.

  • A failed Arabic webfont renders plausible-looking fallback glyphs with the wrong shaping. You cannot see it in a screenshot, so a console assertion confirms the font actually loaded.
  • The mihrab arch has to contain both names, their qualifications and both lineages, with the side margins clear of the innermost jamb line — checked as a real bounding-box assertion at seven widths from 320px up, along with a no-horizontal-scroll check at each.
  • A QR that does not scan looks identical to one that does. This one is decoded back out of the final rendered PNGs, and back out of frames pulled from the finished MP4, asserting the exact URL.

Writing the QR encoder

The cards and the film each carry a maps link per venue, and a film cannot be tapped — so for the venue beats the QR is the only way to hand a guest an address. Every off-the-shelf option was rejected deliberately: an external QR web API puts a third party in the render path with a silent failure mode; an image CDN bakes a remote URL into a file that has to render offline; and the printed card’s own QR blocks use stylised rounded modules that simply do not decode.

So the encoder is written here — byte mode, versions 1 to 20, all four error-correction levels, and nothing more. It emits a boolean matrix that the renderers turn into crisp SVG rects on the exact module grid. It was fuzzed over 229 payloads, which found a real alignment-pattern bug, and it exists as exactly one shared copy precisely because a second hand-written implementation would very likely reintroduce it.

The film’s venue beats also demonstrate a nice bit of restraint: they display only the locality, while the full street address lives inside the QR payload and nowhere on screen. Those two beats run 9.5 seconds each — the longest in the film — because that is genuinely how long it takes to notice a code, unlock a second phone and hold the camera steady. It is why the film is 39.4 seconds rather than 27.8.

How it’s rendered

There is no framework anywhere in this project — the pages are vanilla JS, CSS and hand-authored SVG, and the author’s build step is one dependency-free Node script that inlines everything into a single self-contained file. The deliverable has no relative dependencies at all, so uploading the folder cannot produce a blank page, and it degrades to a documented serif fallback stack offline.

The film is rendered deterministically rather than recorded: the scene owns a clock, the renderer steps it frame by frame through Playwright and screenshots each one, so the output is reproducible instead of dependent on wall-clock timing. Encoding is done by small Swift tools built on AVFoundation — PNG sequence to H.264, an ffprobe stand-in for codec/chroma/faststart checks, a frame extractor that pulls stills back out of the encoded file, and an audio muxer. The render asserts the finished file has an audio track and comes in under the size a chat app will accept.

The image-card renderer follows the same discipline — it screenshots at two device scales, verifies the result, prints a per-check log rather than a summary claim, and exits non-zero if anything fails.

Small things that mattered

The calendar file is served with an explicit text/calendar content type, because that single header is the whole difference between “Add to Calendar” opening the phone’s calendar app and it downloading an inert file — iOS and Android both route the link by its type. The palette was sampled from a 400 dpi render of the source PDF rather than eyeballed, so the digital and paper invitations read as one family. And both pages ship noindex, nofollow, noarchive with a disallow-all robots file — an invitation is something you hand to people, so it stays out of search results even though the link itself is easy to pass on.

Both scrolling renditions are linked above: one carrying the printed card’s own artwork, the other built entirely from the drawn ornament.