{
  "$type": "site.standard.document",
  "content": "---\ntitle: \"A static site that takes pull requests\"\ndescription: \"Out of Office stores its data in a JSON file in git, not a database. Why,\n  and when this pattern fits.\"\ntags: [astro, svelte, github]\n---\n\n[Out of Office](https://www.outofoffice.cv) catalogues post-parliament gigs\ntaken by Australian politicians. The data is structured: each gig holds a\npollie[^pollie] slug, role description, organisation, dates, source URL, and a\n[zod](https://zod.dev/)-validated shape. The site runs on Astro and GitHub\nPages, with no server or database of my own. Contributions go in via PRs\nagainst `gigs.json` in the public data repo.\n\n[^pollie]: Australian slang for politician. I love it.\n\nOn the face of it, this is a CRUD app: typed records, validated input, and an\naudit trail. The off-the-shelf answer is Postgres plus an admin panel, or a\nheadless CMS like [Decap](https://decapcms.org/),\n[Sveltia](https://sveltiacms.app/), or [TinaCMS](https://tina.io/).\n\nI went the other way for two reasons. The first is that there's no server:\nGitHub Pages handles the site, and any 3am page about a memory leak is\nGitHub's problem. The whole thing costs nothing to run.\n\nThe second is that git is already a perfectly good audit trail. Every gig\nchange is a commit with an author, a timestamp, and a diff. Reviewing a\ncontribution means reading a PR; reverting a bad entry is a one-click revert.\nA CRUD app would require an `audit_log` table and an admin UI you'd have to\nwrite yourself.\n\nThe one real wrinkle is auth: skipping the backend kills the OAuth dance,\nsince there's no proxy to exchange a code for a token. Two options remain for\nhitting the GitHub API from the browser. OAuth needs a stateful callback URL\nhandled by a backend, which is exactly the dependency I'm trying to avoid.\nThe other option is a fine-grained personal access token (PAT), pasted into a\nform and stored in localStorage.\n\nA PAT skips the dance entirely. In their GitHub settings, the user mints one\nscoped to the data repo. They paste it into the form, and the site uses it to\nopen PRs on their behalf.\n\nThe user has to know what a PAT is, and how to scope one (`contents:write` on\nthe data repo, nothing else). They also have to trust the site's JS, which\nlives in a public repo. None of that is a friendly first-time experience. The\naudience for Out of Office filters itself, and that filter doubles as abuse\nprevention.[^filter] The people who would file a PR usually know what a PAT\nis. If not, they're willing to learn.\n\n[^filter]:\n    Bots don't tend to have fine-grained PATs scoped to your specific repo,\n    mostly because there's no easy way to mass-produce them.\n\nWhether this pattern fits your site is mostly a question about the data and\nthe audience. The good cases share a shape: a canonical data file with typed\nfields, contributions infrequent enough to avoid concurrent PRs, and a small\nmotivated audience. When the conditions don't hold, the pattern breaks. A\nsite aimed at the general public can't demand a PAT; the friction is fatal.\nHundreds of submissions per day will hit GitHub's rate limits, and the merge\nqueue will take longer to triage than writing the data conventionally.\n\nIf you want to copy or fork it, the full source is at\n[github.com/out-of-office-cv/out-of-office-cv-website](https://github.com/out-of-office-cv/out-of-office-cv-website).\nThe contribution form is one Svelte component and three small stores (auth,\ndrafts, PR), with under 200 lines of GitHub API code. It runs on free GitHub\nPages, and I built it in an afternoon.\n",
  "createdAt": "2026-05-13T23:14:35.846Z",
  "description": "Out of Office stores its data in a JSON file in git, not a database. Why, and when this pattern fits.",
  "path": "/blog/2026/04/28/a-static-site-that-takes-pull-requests",
  "publishedAt": "2026-04-28T00:00:00.000Z",
  "site": "at://did:plc:tevykrhi4kibtsipzci76d76/site.standard.publication/self",
  "tags": [
    "astro",
    "svelte",
    "github"
  ],
  "textContent": "Out of Office stores its data in a JSON file in git, not a database. Why, and when this pattern fits.",
  "title": "A static site that takes pull requests"
}