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.
Create a new link
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.vcfthat imports cleanly into Contacts - Mobile layout — use device emulation at ~390 px wide, not just a narrow window
- Then run
deno task checkanddeno 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.
Edit an existing link
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.
Retire a link
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.
Related
- Field reference → DATA-MODEL.md
- What each type renders → LINK-TYPES.md
- Build and serve → DEPLOYMENT.md