Family link-in-bio site for the De Luisa family — one page per person, content in the repo as JSON, edited through a small custom admin, with per-person SEO, OpenGraph images and favicons.
Vue 3 + Vite 8 + Tailwind v4 + Pug + SCSS + i18next, prerendered with vite-ssg, deployed to GitHub Pages, with a Cloudflare Worker admin API.
- Content lives in
content/bios/<slug>.json(one per person), typed bysrc/content/bio.ts(IBio). It is loaded at build time (src/composables/use-bios.ts). - Routing — each bio is
deluisa.bio/<slug>(canonical).BioView.vuerenders the resolvedIBio; the subdomain<slug>.deluisa.bio301-redirects to the path (Cloudflare, see DNS below). - Prerendering —
vite-ssgemits one static HTML per bio (vite.config.tsincludedRoutes) so each/<slug>ships its own<title>/OG/<meta>(via@unhead/vue) for social scrapers. - OG images + favicons — generated post-build into
dist/og/<slug>.pnganddist/favicons/<slug>.svg(scripts/generate-og.ts,scripts/generate-favicons.ts). The OG card is a split layout: the person's photo on the left, a panel in their primary colour on the right. - Theme per bio —
IBio.theme(primary/secondary colours, font, card radius, avatar radius/border) is applied via CSS variables inBioView.vueand configurable in the admin.
deluisa.bio/admin is a custom, client-only SPA (src/views/AdminView.vue). It talks only to the
Cloudflare Worker (worker/, served at https://api.deluisa.bio), which is the trust boundary
holding every secret. The Worker must be a subdomain of deluisa.bio (not its *.workers.dev
URL): the SPA and API then share a registrable domain, so the httpOnly session cookie is first-party
and Safari sends it — a cross-site *.workers.dev API gets its cookie blocked and every call 401s.
- Custom basic auth — users are defined in the Worker's
ADMIN_USERSsecret. Each person logs in and is scoped to their own bio. - Stats home — the Worker proxies the PostHog query API, scoped to that person's path, so the admin home shows their visits, unique visitors, views-per-day and clicks-by-link.
- Editor — edit your own bio (profile, appearance/theme, content, links, socials) with a live
preview. Saving commits
content/bios/<slug>.jsonto GitHub via the Worker, which triggers a rebuild. A person can only ever save their own slug (enforced server-side).
Generate an ADMIN_USERS entry:
bun worker/hash-password.ts <user> <slug> <password>
# -> {"user":"…","slug":"…","salt":"…","passHash":"…"}Both run, behind one track() in src/composables/use-analytics.ts:
- Google Tag Manager — container
GTM-MCT4XSDM, lazy-loaded;<noscript>fallback inindex.html. Events:page_view,link_click({ link_id, link_url, location, bio }),share_open,share_native. - PostHog —
VITE_POSTHOG_KEY(public). Autocapture + the same custom events. Powers the admin's per-person dashboard via the Worker (which holds the secret read key).
bun install
cp .env.example .env.local # public VITE_* values for local dev
cp worker/.dev.vars.example worker/.dev.vars # Worker secrets for local devbun dev # public site + admin SPA
bun worker/dev-server.ts # admin API on :8787 (Bun — no wrangler needed locally)Set VITE_ADMIN_API=http://localhost:8787 in .env.local, then log into /admin with a
user from worker/.dev.vars. (Production deploys the Worker via wrangler — see below.)
bun run build # type-check + vite-ssg + OG + favicons
bun run type-check
bun lintTwo GitHub Actions workflows:
.github/workflows/deploy-site.yml— builds and deploysdist/to GitHub Pages on push tomaster. Public values come from repo Variables (VITE_POSTHOG_KEY,VITE_POSTHOG_HOST,VITE_ADMIN_API=https://api.deluisa.bio).public/CNAMEpinsdeluisa.bio; a404.htmlSPA fallback is added..github/workflows/deploy-worker.yml—wrangler deployforworker/on changes, pushing secrets from repo Secrets:CLOUDFLARE_API_TOKEN,CLOUDFLARE_ACCOUNT_ID,SESSION_SECRET,ADMIN_GITHUB_TOKEN(→ WorkerGITHUB_TOKEN),POSTHOG_READ_KEY,ADMIN_USERS. Non-secret config is inworker/wrangler.toml(GITHUB_REPO,POSTHOG_HOST,POSTHOG_PROJECT_ID,ALLOWED_ORIGIN).
-
Apex
deluisa.bio→ GitHub Pages (DNS-only / grey cloud, so GitHub issues the Let's Encrypt cert):A→185.199.108.153,.109.153,.110.153,.111.153;CAA 0 issue "letsencrypt.org". Set + verify the custom domain in the repo's Pages settings. -
Wildcard
*.deluisa.bio→ redirect (proxied / orange cloud, so the redirect rule fires and Universal SSL covers it):*.deluisa.bioCNAME →deluisa.bio, plus one Cloudflare Dynamic Redirect rule using a Wildcard pattern (the Free plan can't useregex_replace):- When → custom expression:
(http.request.full_uri wildcard "https://*.deluisa.bio/*" and http.host ne "api.deluisa.bio") - Then → Dynamic →
https://deluisa.bio/${1}, status 301, preserve query string.
${1}is the subdomain; do not append${2}/the path or a trailing/— vite-ssg emits flatdist/<slug>.html, so GitHub Pages serves/massimo(200) but/massimo/404s. Thehttp.host ne "api.deluisa.bio"guard stops the admin API host from being redirected (redirect rules run before Worker routes). One rule covers every person. - When → custom expression:
-
Admin API
api.deluisa.bio→ the Worker — add it as a Custom Domain on the Worker (Workers & Pages →de-luisa-bio-admin→ Settings → Domains & Routes), which creates the proxied DNS record + route. Same registrable domain as the site, so the session cookie is first-party.
A member needs two things: an admin login and a bio. The home grid and routes include any
content/bios/*.json automatically — no code changes.
Generate a salted PBKDF2 entry (username = slug):
bun worker/hash-password.ts <slug> <slug> <password>
# e.g. bun worker/hash-password.ts marco marco a-strong-password
# -> {"user":"marco","slug":"marco","salt":"…","passHash":"…"}Add that object to the ADMIN_USERS JSON array:
- Local: in
worker/.dev.vars(insert,{…}before the closing]), then restartbun run dev. - Production: update the GitHub Secret
ADMIN_USERSwith the full array (all users).
Rules: the slug is the URL (deluisa.bio/<slug>) and subdomain — lowercase letters, digits and
hyphens only. Each user can edit only their own bio (enforced by the Worker).
- Via the admin (easiest): the person signs in at
/admin, fills everything (name, colours, content, links) and uploads an avatar, then Save bio. This createscontent/bios/<slug>.jsonautomatically (locally it writes to the working tree; in production it commits to GitHub). - Pre-seed (so they appear immediately): copy an existing
content/bios/<slug>.json, changeslug/name/themecolours, leaveavatar: ""(a coloured letter-glyph shows until a photo is uploaded). They then complete it from the admin.
Avatars are uploaded as square WebP derivatives (<slug>-{original,2000,600,250}.webp under
public/media/) and served responsively; the bio's avatar stores the base path /media/<slug>.
Push — the site rebuilds; deluisa.bio/<slug> and <slug>.deluisa.bio both work.
Vue 3 (beta) · Vite 8 · vite-ssg · Tailwind CSS v4 · Pug + SCSS · i18next · @unhead/vue · VueUse · satori + resvg (OG) · PostHog + GTM · Cloudflare Workers · TypeScript · Bun.