Add a promotion flag driving post listing, robots, and syndication #97

Closed
opened 2026-08-10 20:07:56 +00:00 by coilyco-ops · 2 comments
Collaborator

Context

Posts are currently dark by design. src/pages/pages.11tydata.js computes robots: "noindex, nofollow" for every page under src/pages/ except the resume, /writing/ carries the same directive, and src/index.njk links to /writing/ from nowhere. All nine posts remain reachable by URL.

Kai has selected three posts to promote:

  • src/pages/posts/stochastic-design-iteration.md
  • src/pages/posts/on-permissions-models-for-cloud-platform-providers.md
  • src/pages/posts/azure-openai-terraform.md

src/pages/posts/code-janitor.md was considered and explicitly excluded. The remaining five stay dark.

Problem

Two things block the syndication work that depends on this.

  • Syndicating a post whose canonical URL points at a noindex page is self-defeating. The remote copy becomes the de facto indexed version and coilysiren.me ranks nowhere.
  • robots is set in eleventyComputed, which wins over front matter. Adding robots: follow, index to a post does nothing. The computed function has to read a different key.

Required outcome

One front matter key on a post drives three behaviors, so curation lives in git next to the prose rather than in three separate lists.

  • Listing - src/writing.njk filters collections.posts to flagged posts.
  • Robots - pages.11tydata.js computes follow, index for flagged posts and keeps noindex, nofollow otherwise.
  • Syndication - the future syndication script reads the same flag as its allowlist.

Key naming is the implementer's call. Something like promoted: true reads clearly and does not collide with the existing template-key, path, date, title, description vocabulary.

Acceptance criteria

  • The three named posts render <meta name="robots" content="follow, index">.
  • The other six posts, including code-janitor.md, still render noindex, nofollow and keep their current URLs. Nothing 404s.
  • /writing/ lists exactly the three promoted posts and is itself indexable.
  • The resume keeps its existing follow, index behavior.
  • Promoting or demoting a post is a one-line front matter diff with no template change.
  • ward exec build succeeds and ward exec pre-commit-all passes.

Non-goals

  • Deleting or unpublishing the five meh posts. They stay live and dark.
  • Any change to post content.
  • Linking /writing/ from the homepage. That is tracked separately.
## Context Posts are currently dark by design. `src/pages/pages.11tydata.js` computes `robots: "noindex, nofollow"` for every page under `src/pages/` except the resume, `/writing/` carries the same directive, and `src/index.njk` links to `/writing/` from nowhere. All nine posts remain reachable by URL. Kai has selected three posts to promote: * `src/pages/posts/stochastic-design-iteration.md` * `src/pages/posts/on-permissions-models-for-cloud-platform-providers.md` * `src/pages/posts/azure-openai-terraform.md` `src/pages/posts/code-janitor.md` was considered and explicitly excluded. The remaining five stay dark. ## Problem Two things block the syndication work that depends on this. * Syndicating a post whose canonical URL points at a `noindex` page is self-defeating. The remote copy becomes the de facto indexed version and coilysiren.me ranks nowhere. * `robots` is set in `eleventyComputed`, which wins over front matter. Adding `robots: follow, index` to a post does nothing. The computed function has to read a different key. ## Required outcome One front matter key on a post drives three behaviors, so curation lives in git next to the prose rather than in three separate lists. * **Listing** - `src/writing.njk` filters `collections.posts` to flagged posts. * **Robots** - `pages.11tydata.js` computes `follow, index` for flagged posts and keeps `noindex, nofollow` otherwise. * **Syndication** - the future syndication script reads the same flag as its allowlist. Key naming is the implementer's call. Something like `promoted: true` reads clearly and does not collide with the existing `template-key`, `path`, `date`, `title`, `description` vocabulary. ## Acceptance criteria * The three named posts render `<meta name="robots" content="follow, index">`. * The other six posts, including `code-janitor.md`, still render `noindex, nofollow` and keep their current URLs. Nothing 404s. * `/writing/` lists exactly the three promoted posts and is itself indexable. * The resume keeps its existing `follow, index` behavior. * Promoting or demoting a post is a one-line front matter diff with no template change. * `ward exec build` succeeds and `ward exec pre-commit-all` passes. ## Non-goals * Deleting or unpublishing the five meh posts. They stay live and dark. * Any change to post content. * Linking `/writing/` from the homepage. That is tracked separately.
Author
Collaborator

Landed in 5e37bb6 on main. CI green on all five jobs, live and verified. Kai reconfirmed the three-post selection today before this was built.

promoted: true on a post now drives its /writing/ listing, its robots, its sitemap entry, and the allowlist #103 will read, from one key in front matter next to the prose.

The trap this issue called was real. robots is computed in pages.11tydata.js and beats front matter, so a post cannot opt in by setting robots itself. The computed function reads data.promoted instead. Anyone who tries the obvious thing first will lose an hour, which is presumably why it was written down.

The sitemap needed no change. It derives from each page's own robots value, so flipping the flag moved three posts into it on its own. Live now:

/                                                    (unchanged)
/about/                                              (unchanged)
/hiring/                                             (unchanged)
/posts/azure-openai-terraform/                       new
/posts/on-permissions-models-for-cloud-platform-providers/   new
/posts/stochastic-design-iteration/                  new
/resume/                                             (unchanged)

/posts/code-janitor/ still serves noindex, nofollow, and /writing/ lists exactly three.

Also landed, from #125 items 2 through 5

All four were verified against the live site before being touched, and all four are correct under any curation policy, so they were safe to do alongside this.

  • Posts rendered zero h1 and started at h2. Each now carries exactly one. resume.md is excluded, because it opens with its own # Kai Siren and a blanket change would have given that page two h1 elements. The title styling followed the tag, so .post-header now matches h1 as well as h2.
  • Post titles were bare. They take the X | Kai Siren pattern, since posts are the pages most likely to reach someone who does not know the name.
  • No machine-readable date anywhere. The post and its /writing/ entry both emit <time datetime="YYYY-MM-DD"> from a new isoDay filter beside the existing date filters.
  • No structured data on posts. Each emits BlogPosting with datePublished and an author pinned to the same url the homepage Person declares, so the two resolve as one entity.

What this unblocks

#98 and #99 were both explicitly blocked on this flag and now are not. #99 should scope its feed to post.data.promoted, which is the same predicate /writing/ filters on.

Two notes for whoever takes #99. Its acceptance says the feed emits absolute URLs on https://coilysiren.me, but the canonical host moved to www in 8871872 because the apex 301s, so that criterion is stale. And it specifies ward exec build, which is retired here in favour of just.

/writing/ itself is still noindex, nofollow and unlinked from the homepage. That is #98's call rather than mine, and I left it alone deliberately.

Suite

Assertions moved with the policy rather than around it. The suite now asserts the derived curated set, one non-empty h1 and the title pattern on every indexed page, and each post's <time> matching its BlogPosting datePublished. Both new invariants were mutation-tested, by un-promoting a post and by reverting the h1, and both fail as they should.

just test-quick, just test-e2e-ci and just pre-commit-all all pass. Eight unit tests, three Cypress specs.

Landed in `5e37bb6` on `main`. CI green on all five jobs, live and verified. Kai reconfirmed the three-post selection today before this was built. `promoted: true` on a post now drives its `/writing/` listing, its `robots`, its sitemap entry, and the allowlist `#103` will read, from one key in front matter next to the prose. **The trap this issue called was real.** `robots` is computed in `pages.11tydata.js` and beats front matter, so a post cannot opt in by setting `robots` itself. The computed function reads `data.promoted` instead. Anyone who tries the obvious thing first will lose an hour, which is presumably why it was written down. **The sitemap needed no change.** It derives from each page's own `robots` value, so flipping the flag moved three posts into it on its own. Live now: ``` / (unchanged) /about/ (unchanged) /hiring/ (unchanged) /posts/azure-openai-terraform/ new /posts/on-permissions-models-for-cloud-platform-providers/ new /posts/stochastic-design-iteration/ new /resume/ (unchanged) ``` `/posts/code-janitor/` still serves `noindex, nofollow`, and `/writing/` lists exactly three. ## Also landed, from `#125` items 2 through 5 All four were verified against the live site before being touched, and all four are correct under any curation policy, so they were safe to do alongside this. * **Posts rendered zero `h1`** and started at `h2`. Each now carries exactly one. `resume.md` is excluded, because it opens with its own `# Kai Siren` and a blanket change would have given that page two `h1` elements. The title styling followed the tag, so `.post-header` now matches `h1` as well as `h2`. * **Post titles were bare.** They take the `X | Kai Siren` pattern, since posts are the pages most likely to reach someone who does not know the name. * **No machine-readable date anywhere.** The post and its `/writing/` entry both emit `<time datetime="YYYY-MM-DD">` from a new `isoDay` filter beside the existing date filters. * **No structured data on posts.** Each emits `BlogPosting` with `datePublished` and an `author` pinned to the same `url` the homepage `Person` declares, so the two resolve as one entity. ## What this unblocks `#98` and `#99` were both explicitly blocked on this flag and now are not. `#99` should scope its feed to `post.data.promoted`, which is the same predicate `/writing/` filters on. Two notes for whoever takes `#99`. Its acceptance says the feed emits absolute URLs on `https://coilysiren.me`, but the canonical host moved to `www` in `8871872` because the apex 301s, so that criterion is stale. And it specifies `ward exec build`, which is retired here in favour of `just`. `/writing/` itself is still `noindex, nofollow` and unlinked from the homepage. That is `#98`'s call rather than mine, and I left it alone deliberately. ## Suite Assertions moved with the policy rather than around it. The suite now asserts the derived curated set, one non-empty `h1` and the title pattern on every indexed page, and each post's `<time>` matching its `BlogPosting` `datePublished`. Both new invariants were mutation-tested, by un-promoting a post and by reverting the `h1`, and both fail as they should. `just test-quick`, `just test-e2e-ci` and `just pre-commit-all` all pass. Eight unit tests, three Cypress specs.
Author
Collaborator

Correction: I closed this one criterion short

My previous comment said /writing/ was left noindex deliberately, as #98's call. That was wrong. This issue's acceptance criteria say the writing index "lists exactly the three promoted posts and is itself indexable". #98 owns linking it from the homepage, not its robots directive.

Fixed in 8ba05ee. CI green on all five jobs, live and verified:

/writing/   name="robots" content="follow, index"
sitemap     8 entries

The sitemap picked /writing/ up on its own, the same way the promoted posts did, because the derivation reads each page's own robots. llms.txt names it now, since it is part of the public set. The suite's indexed set gains it, which also puts it under the one-h1 and title-pattern assertions, and it already satisfied both.

Every acceptance criterion in this issue is now met:

  • The three named posts render follow, index
  • The other six, code-janitor included, still render noindex, nofollow at their existing URLs, nothing 404s
  • /writing/ lists exactly the three promoted posts and is itself indexable
  • The resume keeps its follow, index behaviour
  • Promoting or demoting is a one-line front matter diff with no template change
  • Build and pre-commit pass, via just rather than the retired ward exec

#98 is unchanged and still owns surfacing this from the homepage. It now has an indexable, correctly-scoped page to link to, which was the end state it was waiting for.

## Correction: I closed this one criterion short My previous comment said `/writing/` was left `noindex` deliberately, as `#98`'s call. That was wrong. This issue's acceptance criteria say the writing index "lists exactly the three promoted posts **and is itself indexable**". `#98` owns linking it from the homepage, not its robots directive. Fixed in `8ba05ee`. CI green on all five jobs, live and verified: ``` /writing/ name="robots" content="follow, index" sitemap 8 entries ``` The sitemap picked `/writing/` up on its own, the same way the promoted posts did, because the derivation reads each page's own `robots`. `llms.txt` names it now, since it is part of the public set. The suite's indexed set gains it, which also puts it under the one-`h1` and title-pattern assertions, and it already satisfied both. Every acceptance criterion in this issue is now met: - [x] The three named posts render `follow, index` - [x] The other six, `code-janitor` included, still render `noindex, nofollow` at their existing URLs, nothing 404s - [x] `/writing/` lists exactly the three promoted posts and is itself indexable - [x] The resume keeps its `follow, index` behaviour - [x] Promoting or demoting is a one-line front matter diff with no template change - [x] Build and pre-commit pass, via `just` rather than the retired `ward exec` `#98` is unchanged and still owns surfacing this from the homepage. It now has an indexable, correctly-scoped page to link to, which was the end state it was waiting for.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
coilysiren/website#97
No description provided.