{
"$type": "site.standard.document",
"canonicalUrl": "https://rednafi.com/misc/standard-site/",
"coverImage": {
"$type": "blob",
"ref": {
"$link": "bafkreibz4kwi3yba6ehh7omf5irvknstbjgvvq6g72d4hmkpe55hg2mehu"
},
"mimeType": "image/png",
"size": 121470
},
"description": "Mirroring a static Hugo blog onto ATProto with standard.site and Sequoia, plus the GitHub Actions wiring that republishes the records on every push without any manual steps.",
"path": "/misc/standard-site/",
"publishedAt": "2026-06-07T00:00:00.000Z",
"site": "at://did:plc:fgtm2c26vfcj74rfmeggbyqj/site.standard.publication/3mnl6f7ob462z",
"tags": [
"Essay",
"DevOps",
"Web"
],
"textContent": "I put this blog on [standard.site]. Every post now also lives as a record on [ATProto] (the\nprotocol behind Bluesky), and new ones publish themselves whenever I [push to main].\n\nWhat it is\n\nstandard.site is a set of shared [ATProto lexicons]. The two that matter here are\nsite.standard.publication and site.standard.document. The publication record describes\nthe blog: name, URL, icon. Each post becomes a document record that lives in my own [data\nrepository] on a [PDS] and points back at the publication. To prove the records are actually\nmine, there's a [/.well-known/site.standard.publication] file on my domain and a [link-rel\ntag] in every post's HTML pointing at the matching record. The two ends point at each other,\nwith no central registry in between.\n\n\n\n{{< mermaid >}}\nsequenceDiagram\n participant R as Reader\n participant S as rednafi.com\n participant P as PDS\n\n R->>S: GET /zephyr/carry-the-pager/\n S-->>R: HTML + a site.standard.document link tag\n R->>P: resolve that document record\n P-->>R: site = publication URI, path = /zephyr/carry-the-pager/\n R->>S: GET /.well-known/site.standard.publication\n S-->>R: the same publication URI\n Note over R: URIs match, so it's provably rednafi.com's\n{{</ mermaid >}}\n\n\n\nWhy bother\n\nMostly the previews. Share one of my posts on Bluesky and the link turns into a card with\nthe title, description, and image instead of a bare URL. That works because the post is a\nreal record the network can read. Bluesky [shows richer previews] for standard.site links.\n\nIt goes past Bluesky, though. The records sit in my own PDS, so any reader can pick them up\non its own. [docs.surf] already lists my posts, and a search on [pckt] turns them up too:\n\n![Searching pckt for rednafi lists the posts, the pager among them][img_3]\n\nIt's cheap [POSSE] on top of that: rednafi.com stays the canonical copy while a copy\nsyndicates out into the [ATmosphere].\n\nSetting it up with Sequoia\n\nI didn't hand-roll any of the ATProto plumbing. [Sequoia] is a CLI by Steve Simkins that\ndoes the whole thing for static sites. It doesn't much care what built yours (Hugo, Astro,\nEleventy) as long as it's Markdown. If you want to put your own blog on standard.site, it\ngoes roughly like this.\n\nFirst, you need an ATProto identity, and a Bluesky account is the easy way to get one. The\nrecords live in your own PDS. Ownership is checked against a domain, so set your site's\ndomain as your handle (mine is rednafi.com) and mint an app password for the CLI.\n\nThen run sequoia init in the repo. It authenticates against your PDS, creates a\nsite.standard.publication record describing the blog (name, URL, icon), and scaffolds a\nsequoia.json. That config is small: it points at your content directory and maps the\nfrontmatter fields it reads, like the publish date and the slug.\n\nThat publicationUri is the at:// address of the publication record init just made. The\nsame URI also lands in static/.well-known/site.standard.publication, so the domain and the\nrecord name each other and the ownership check holds.\n\nEach post's HTML also needs a <link rel=\"site.standard.document\"> pointing at that post's\nrecord. sequoia inject can patch the tags into your built HTML; I emit them from my Hugo\nhead partial instead.\n\nWith that wired up, sequoia publish walks the content, creates a site.standard.document\nrecord per post, and writes the resulting atUri back into each post's frontmatter. State\nlives in .sequoia-state.json, so reruns only touch what actually changed.\n\nMaking it hands-free\n\nI didn't want to run sequoia publish by hand, so it happens in [CI]. The job does a few\nthings before the Hugo build:\n\n- checks that every post has the same frontmatter keys in the same order\n- sets slug and atprotoPath from the filename\n- stops the build if a post has extra fields, missing fields, or a wrong path\n- copies the posts to a temporary Sequoia directory and adds the shared cover there\n- asks Sequoia to publish only posts whose content changed\n- writes Sequoia's returned atUri values and .sequoia-state.json back\n- commits only those generated metadata changes with [skip ci]\n- builds and deploys the Hugo site\n\nMy routine didn't change: write Markdown and push to main. CI fills in the atUri,\ncommits it back, and lets the deploy continue. This post turned into a\nsite.standard.document the moment the deploy ran.\n\nSeeing it work\n\nThe previews are what I actually wanted. The card is just the record rendered, so I can put\na live, clickable one right here instead of a screenshot:\n\n\n\n{{< bskycard\n url=\"https://rednafi.com/zephyr/carry-the-pager/\"\n title=\"If you won't carry the pager, maybe don't push to mainline\"\n description=\"Drive-by AI changes break the shared model a team builds around its code, and the ICs end up cleaning up the mess. Why pushing to mainline should come with the pager.\"\n date=\"May 30, 2026\"\n reading=\"8m\"\n publication=\"Redowan's Reflections\"\n handle=\"rednafi.com\" >}}\n\n\n\nThat same post also exists as a record on [pdsls]. It has the title, description, path,\ntags, and the full body.\n\n![The same post as a site.standard.document record in pdsls][img_2]\n\nIf you want to copy the setup, it's all in the repo: [config], [script], and the [ci\nworkflow].\n\n\n\n\n[standard.site]:\n https://standard.site\n\n[atproto]:\n https://atproto.com\n\n[atproto lexicons]:\n https://atproto.com/specs/lexicon\n\n[data repository]:\n https://atproto.com/guides/data-repos\n\n[pds]:\n https://atproto.com/guides/glossary#pds-personal-data-server\n\n[/.well-known/site.standard.publication]:\n https://rednafi.com/.well-known/site.standard.publication\n\n[link-rel tag]:\n https://github.com/rednafi/com.rednafi/blob/main/layouts/partials/head.html\n\n[shows richer previews]:\n https://atproto.com/blog/standard-site-bluesky-timeline\n\n[posse]:\n https://indieweb.org/POSSE\n\n[atmosphere]:\n https://atproto.com/blog/indexing-standard-site\n\n[sequoia]:\n https://sequoia.pub\n\n[ci]:\n https://github.com/rednafi/com.rednafi/blob/main/.github/workflows/ci.yml\n\n[docs.surf]:\n https://docs.surf\n\n[pckt]:\n https://pckt.blog/read?search=rednafi\n\n[pdsls]:\n https://pdsls.dev/at://did:plc:fgtm2c26vfcj74rfmeggbyqj/site.standard.document/3mnl6iapia32u\n\n[push to main]:\n https://github.com/rednafi/com.rednafi\n\n[config]:\n https://github.com/rednafi/com.rednafi/blob/main/sequoia.json\n\n[script]:\n https://github.com/rednafi/com.rednafi/blob/main/scripts/frontmatter/main.go\n\n[ci workflow]:\n https://github.com/rednafi/com.rednafi/blob/main/.github/workflows/ci.yml\n\n[small Go script]:\n https://github.com/rednafi/com.rednafi/blob/main/scripts/frontmatter/main.go\n\n[img_2]:\n https://blob.rednafi.com/misc/standard-site/image-02-v2-b87aea40ea0e.png\n\n[img_3]:\n https://blob.rednafi.com/misc/standard-site/image-03-d67bfdc6d73e.png",
"title": "Putting this blog on ATProto with standard.site"
}