Personal website for Nathan Arthur, a full-stack web developer.
Its job is verification, not discovery: almost everyone who lands here already has a reason to look him up — a hiring manager holding his application, someone who read a link. Design for a 30-second skim by that person. Evidence first, ornament never.
- SvelteKit (Svelte 5) with
@sveltejs/adapter-static - Tailwind CSS
- Deployed to Cloudflare Workers static assets at nathanarthur.com
- Typographic, not card-based. Structure comes from type scale, hairline rules, and whitespace. No boxes, no shadows, no rounded corners.
- Dark only. There is no light theme and no toggle. The full palette lives in
tailwind.config.jsas six tokens:bg,ink,mute,faint,rule,accent. Don't introduce colors outside those. accentresolves to the CSS variable--accent, declared once on:rootin+layout.svelte. Change the accent there, not in the Tailwind config. Anything that needs the accent in plain CSS usesvar(--accent)so it stays in step.- Instrument Sans (400, 500) for prose and headings; the system mono stack for metadata — stack lists, dates, roles, section labels.
- Section labels are small mono, uppercase, wide tracking.
- Links are body-colored and underline on hover in
accent. No hover lifts, no scroll effects, no entrance animations. Genuine controls (the/usestag filter) may take a hover colour change and atransition-colors, because a control that gives no feedback is an accessibility problem, not restraint. - Every link opens in the same tab. No
target="_blank"anywhere, external or not, and therefore norel="noopener noreferrer". "It's external" is not a reason to take the tab away from the reader; nothing here is a form, editor, or checkout worth protecting, and anyone wanting a new tab can middle-click.noreferrerin particular was actively costing us: it strips theRefererheader even on same-tab navigation, so Substack and Pine Peak Digital could not see this site as a traffic source. If a future link genuinely needs a new tab, it getsrel="noopener"— notnoreferrer— plus a visible cue in the label. - Every interactive element gets a visible
:focus-visibleoutline — the palette is custom, so the browser default can't be assumed legible against it. - All text tokens clear WCAG AA (4.5:1) against
bg. Check any new colour before adding it;faintshipped at 3.27:1 once and had to be corrected. - Measure stays readable (
max-w-prose); the page column ismax-w-2xl. - Always set explicit width and height on images to prevent layout shift.
- The positioning line under the name is also the home page's
<meta name="description">—+page.sveltederives one from the other, so editing the constant changes the search result too. - Featured work is tiered: three entries get a role, a stack, a year range, and a real description. Everything else is a one-line "Also built" list.
- No "open to work" banner — current clients read this site.
- No hard-sell CTA. Contact is GitHub, LinkedIn, writing, and email, stated once.
src/
├── app.html # <head>, Supascribe loader script
└── routes/
├── +layout.svelte # page column, footer, global link/focus styles
├── +layout.ts # prerender: true (required for static build)
├── +page.svelte # home: positioning, selected work, also built
├── +error.svelte # 404; emitted as build/404.html by the adapter
├── audioverse/+page.svelte # AudioVerse case study
├── writing/+page.svelte # newsletter + individual Beeminder articles
└── uses/
├── +page.ts # parses uses.yaml at build time
├── +page.svelte # renders it; owns only the tag-filter state
├── filter.ts # tag/category logic, the only tested code
├── filter.spec.ts
└── uses.yaml # the list itself
There is no nav and no components directory. Subpages carry a "← Nathan Arthur" link, and the footer lives in the layout — its only consumer.
+layout.svelte carries a dev-only accent picker (colour input, presets, and a
live contrast readout). It is deliberate, not leftover scaffolding: dev is
false in the build so it tree-shakes away entirely. If you touch it, keep every
part behind dev — including effect bodies, since an ungated $effect is
emitted into the production bundle even when its markup is eliminated.
The favicon is self-hosted: static/favicon.svg is the source and
static/favicon.png is rendered from it as the fallback for browsers that don't
take SVG icons. Keep them in step. It carries a single letterform, not a
monogram — at the 16px browsers actually draw, two letters turn to mush.
Supascribe, loaded via the script tag in app.html and mounted on the
data-supascribe-subscribe div in +layout.svelte. Its default theme is a blue
button; the layout's style block overrides the --csw-* CSS variables to match
the palette. Setting those colors in the Supascribe dashboard would let that
block be deleted.
pnpm dev/pnpm build/pnpm previewpnpm check(svelte-check) andpnpm lint(prettier + eslint) both run in CIexport const prerender = trueinsrc/routes/+layout.tsis required for the static build- Don't use
@applyin Svelte<style>blocks; use Tailwind classes in markup and plain CSS in<style>when needed
Zone-level analytics are in the Cloudflare dashboard. There is deliberately no analytics script on the site.