Operations runbook

Day-to-day tasks: creating links, editing them, adding logos, and getting them live.

Every link is a file in git. Publishing or changing a link requires a commit and a redeploy. There is no way to edit content in production.


The quick route: /builder

Steps 1 and 2 below are what the builder replaces. Open /builder, pick a type, fill in the form, and watch the real page render beside it. The link id is generated for you, images can be uploaded straight from your machine, and Download gives you a zip:

<uuid>.zip
├── <uuid>.json          → data/links/
└── logos/*.webp         → assets/logos/

The record's image paths are already rewritten to match the files beside it, so unzip both into the repository and pick up at step 3. It previews only — it cannot publish. See BUILDER.md.

The manual steps remain below, and remain the reference.

Step 1 — generate a UUID

64 alphanumeric characters. Use this and copy the output:

deno eval 'const c="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";console.log(Array.from(crypto.getRandomValues(new Uint8Array(64))).map(b=>c[b%62]).join(""))'

Do not hand-write one, do not use a sequential or guessable value, and do not reuse a UUID from a deleted link. The slug is the only access control on the page — see ARCHITECTURE.md §9.

Step 2 — create the file

data/links/<uuid>.json, where <uuid> is exactly the value from step 1. Copy a template from DATA-MODEL.md and set the uuid field to the same value as the filename. If they differ, the page renders but the vCard download 404s.

Type-specific checklists:

business-card — all seven data fields are required. phone is E.164 with a leading +. Set hasWhatsapp to reflect reality; false hides the green pill. Avoid , ; \ in any value (they are not escaped in the generated .vcf).

links-page — pick icon values from the 14 supported names; anything else silently becomes a globe. Tiles render two per row, so an even block count looks best. Keep title to 1–2 words and remember description is clamped to two lines. For a whatsapp action use digits only, no +; for a phone action use E.164 with +.

showcase-page — each block needs logo, title, description; features, link and linkText are optional. linkText defaults to "Visit Website". socialLinks supports only instagram, linkedin, twitter, facebook, snapchat, and each platform must be unique in the array.

Step 3 — add any new logos

Drop images into assets/logos/ and reference them as /logos/<file>:

"logo": "/logos/acme-logo.png"

serveLogos (lib/assets.ts) serves png, svg, jpg, jpeg, webp, gif, avif and ico with the correct MIME type. Anything else is sent as application/octet-stream, which browsers will not render as an image — add the extension to CONTENT_TYPES if you need it.

Prefer .svg, or .png with a transparent background. Logos display at h-16 md:h-24 (cards) or h-12 md:h-16 (showcase-page header) on a white plate, so transparent-background PNGs work best and a white logo will be invisible.

Step 4 — verify locally

deno task dev

Open http://localhost:5173/o/<uuid> and check:

  • The page renders — a "Link Unavailable" page means the record failed validation, and the server log lists exactly which fields
  • The logo loads; header colour looks right and text is legible against it
  • Every link works: tel:, mailto:, wa.me, and external URLs
  • For business-card: the QR code appears, and Save Contact downloads a .vcf that imports cleanly into Contacts
  • Mobile layout — use device emulation at ~390 px wide, not just a narrow window
  • Then run deno task check and deno task test

Note that the QR encodes https://onnne.link/o/<uuid> by default, so scanning it from a local dev server takes you to production — which is usually what you want when checking a real link. Set SITE_ORIGIN to point it at the deployment you are running.

Step 5 — commit and deploy

deno fmt .
deno task check
git add data/links/<uuid>.json assets/logos/
git commit -m "feat(links): add <name> business card"
git push

Then deploy — see DEPLOYMENT.md.


Find the file by content rather than by name — filenames are opaque:

# by person
grep -rl "Rivera" data/links/

# list every link with its type and title/name
for f in data/links/*.json; do
  echo "$(basename "$f" .json)  $(jq -r '.type' "$f")  $(jq -r '.config.title // (.data.firstName + " " + .data.lastName)' "$f")"
done

Edit, deno fmt ., verify locally, commit, deploy. Never change the uuid of a live link — printed QR codes and programmed NFC tags point at the old one.


Delete data/links/<uuid>.json and deploy. The URL then renders the 404 page.

Two caveats: any QR code or NFC tag already in the wild is now dead, and the 404 page is served with a 200 status (see ROUTING.md). If the link may need to come back, prefer moving the file out of data/links/ in a branch over deleting it outright.


QR codes and NFC tags

For a business card

The page renders its own QR code — nothing to generate or store. It always encodes https://onnne.link/o/<uuid>, produced server-side at 160 px, ECL M, foreground #1a1a1a.

For printing, or for the other two types

links-page and showcase-page render no QR code. Generate one from the URL with any tool; the URL is all you need:

https://onnne.link/o/<uuid>

Practical notes for print: a 64-character URL is a fairly dense QR, so print at 2 cm or larger and keep the quiet zone clear. Test-scan the actual printed artefact — not the screen proof — with both iOS Camera and an Android scanner before ordering a run.

NFC tags

Program the tag with an NDEF URI record containing https://onnne.link/o/<uuid>. Any NFC writer app (NFC Tools, TagWriter) does this. Lock the tag once verified if it is going onto a physical card. NTAG213 (144 bytes) is more than enough for a URL this size.


Common problems

Symptom Cause
404 on a URL you just added Filename does not match the uuid field, or the file is not in data/links/
"Link Unavailable" (500) instead of the page The record fails validation — the server log lists every problem; deno task test finds it
Save Contact 404s uuid field inside the file does not match the filename
Logo is a broken image Path must start /logos/, file must exist in assets/logos/, extension must be in CONTENT_TYPES
Tile shows a globe instead of the right icon icon name not in the supported list — check spelling
Social icon shows Instagram unexpectedly platform not one of the five supported by showcase-page
Sticky bars overlap on showcase-page Header height changed without updating top-[72px] md:top-[88px]
A snapshot test fails after a component edit Intended? deno task test:update. Not intended? You changed the markup
deno task check fails on formatting Run deno fmt .

Bulk operations

Everything is JSON on disk, so jq works well.

# count links by type
jq -r '.type' data/links/*.json | sort | uniq -c

# every distinct logo in use
jq -r '.config.logo' data/links/*.json | sort -u

# find links pointing at a logo you want to replace
grep -l "old-logo.png" data/links/*.json

# rebrand: change primaryColor across every business card
for f in data/links/*.json; do
  [ "$(jq -r '.type' "$f")" = "business-card" ] || continue
  jq '.config.primaryColor = "#0f172a"' "$f" > "$f.tmp" && mv "$f.tmp" "$f"
done
deno fmt .

Always deno fmt . afterwards — jq output is 2-space indented but will not match Deno's formatter exactly, and deno task check will fail.