{
  "$type": "site.standard.document",
  "canonicalUrl": "https://rednafi.com/misc/standard-site/",
  "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]. Two pieces make that\nwork.\n\nA [small Go script] fills in one frontmatter field before Sequoia runs. standard.site gives\neach document a stable path, and Sequoia reads that from an atprotoPath field in the\nfrontmatter. Rather than type it into every post, I derive it from the file's location and\nslug, so content/zephyr/carry_the_pager.md with slug: carry-the-pager gets\natprotoPath: /zephyr/carry-the-pager/ written in. It also fails the build if a post has no\nslug to derive one from.\n\nThen GitHub Actions does the rest on every push to main. It runs the sync script, then\nsequoia publish with my handle and app password from repo secrets. It prettier-formats the\nmetadata Sequoia generated, then commits the new atUris, .sequoia-state.json, and the\n.well-known file back with a [skip ci] tag. Finally Hugo builds and deploys.\n\nSo my actual routine didn't change at all. Write Markdown, push to main, walk away. The\nATProto side catches up by itself. This very post turned into a site.standard.document the\nmoment 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    image=\"https://blob.rednafi.com/static/images/home/cover.png\"\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#L52-L78\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/stdsitesync/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/stdsitesync/main.go\n\n[img_2]:\n    https://blob.rednafi.com/static/images/standard_site/img_2_v2.png\n\n[img_3]:\n    https://blob.rednafi.com/static/images/standard_site/img_3.png",
  "title": "Putting this blog on ATProto with standard.site"
}