From 072872eb4846bfd2b0e1a3d2a83a4652f3ceaa58 Mon Sep 17 00:00:00 2001 From: Cansin Yildiz Date: Thu, 10 Sep 2026 00:08:28 +0200 Subject: [PATCH 1/3] Move the apex to yellowpine.com and make .dev the redirect yellowpine.com is finally in the Porkbun account, so the site takes the name people actually guess. yellowpine.dev becomes the redirect domain. This also closes a live defect. The page has advertised hello@yellowpine.com since 2026-09-08, but .com was not ours then, and when it arrived it came with the previous owner's Mailgun MX records and none of our forwards. Every message sent to the address on the homepage went to servers we do not control. No test related the contact address to the domain the site is served from, so nothing caught it. tests/site.test.mjs now asserts that relationship. It was written against the live page first, where it failed with "contact address is not on yellowpine.dev: hello@yellowpine.com", and only then was the domain flipped. The Deploying section is rewritten rather than retitled: its reasoning rested on .dev being HSTS-preloaded, which made HTTPS free. .com is not, so Enforce HTTPS is now something to set rather than assume. The ordering footgun that cost a morning last time -- DNS must point at GitHub before the domain lands in CNAME, or validation fails and GitHub never retries -- is written down there too. The 2026-09-08 spec is left as written; the new one supersedes it rather than editing what was true then. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Q5nJ8eKEb1wm5tCzxipXHS --- CNAME | 2 +- README.md | 22 ++++-- .../2026-09-10-yellowpine-com-apex-design.md | 67 +++++++++++++++++++ index.html | 6 +- package.json | 2 +- tests/site.test.mjs | 33 +++++++-- 6 files changed, 114 insertions(+), 18 deletions(-) create mode 100644 docs/superpowers/specs/2026-09-10-yellowpine-com-apex-design.md diff --git a/CNAME b/CNAME index 2a97b99..a0b08ba 100644 --- a/CNAME +++ b/CNAME @@ -1 +1 @@ -yellowpine.dev \ No newline at end of file +yellowpine.com diff --git a/README.md b/README.md index 002f3bc..0cee00e 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,14 @@ -# yellow-pine.github.io — yellowpine.dev +# yellow-pine.github.io — yellowpine.com The Yellow Pine website. One hand-written `index.html`, served by GitHub Pages at -[yellowpine.dev](https://yellowpine.dev). +[yellowpine.com](https://yellowpine.com). It is a **hook page**, not a brochure: who we are, what we have shipped, how to reach us. Detail lives on [github.com/yellow-pine](https://github.com/yellow-pine), the way [cansin.dev](https://cansin.dev) defers to its own profile. -`yellowpine.com` is unrelated to this repo and still redirects to the GitHub org. +`yellowpine.dev` is the redirect domain: it 301s here, apex and `www` alike. The apex moved +from `.dev` to `.com` on 2026-09-10, once `yellowpine.com` was finally ours. ## Contents @@ -59,6 +60,15 @@ SKIP_NETWORK=1 npm test # offline: skip link-liveness ## Deploying -Pages serves the default branch root. `CNAME` sets the domain. `.dev` is HSTS-preloaded, so -the site is HTTPS-only by construction — there is no HTTP fallback to configure. After the -DNS records point at GitHub, enable **Enforce HTTPS** once the certificate issues. +Pages serves the default branch root. `CNAME` sets the domain. + +**Point DNS at GitHub _before_ the domain lands in `CNAME`.** If Pages registers a custom +domain whose DNS still resolves elsewhere, certificate validation fails and GitHub never +retries on its own: Pages keeps serving over HTTP, `https_certificate` is absent entirely, +and the site is one `gh api -X PUT .../pages -f cname=""` and re-set away from working. Pass +`https_enforced` in its own call afterwards — inline it 404s with "certificate does not exist +yet". + +Unlike the old `.dev` apex, `.com` is not HSTS-preloaded, so HTTPS is not free here. **Enforce +HTTPS** is what redirects HTTP to HTTPS, and it has to be switched on once the certificate +issues rather than assumed. diff --git a/docs/superpowers/specs/2026-09-10-yellowpine-com-apex-design.md b/docs/superpowers/specs/2026-09-10-yellowpine-com-apex-design.md new file mode 100644 index 0000000..550b0be --- /dev/null +++ b/docs/superpowers/specs/2026-09-10-yellowpine-com-apex-design.md @@ -0,0 +1,67 @@ +# yellowpine.com as the apex - design + +**Date:** 2026-09-10 +**Status:** approved, implementing + +## Purpose + +`yellowpine.com` is finally in our Porkbun account. It is the name people guess, the name on +the career record, and the name the org profile already prints. This makes it the apex: the +site moves from `yellowpine.dev` to `yellowpine.com`, `.dev` becomes the redirect domain, and +the mailboxes move with it. + +It reverses the topology chosen on 2026-09-08, which parked the site on `.dev` precisely +because `.com` was not ours to point anywhere. That constraint is gone. + +## The defect this closes + +The page has advertised `hello@yellowpine.com` since 2026-09-08, in the masthead and again +under *Get in touch*. `yellowpine.com` was not ours then, and when it arrived it came with the +previous owner's Mailgun MX records and zero forwards of ours. **Every message sent to the +address on the homepage went to servers we do not control.** Nothing in the suite caught it, +because no test related the contact address to the domain the site is served from. + +`tests/site.test.mjs` now asserts that relationship, and it was written failing against the +live page before anything was changed. + +## Decisions taken + +| Question | Decision | +|---|---| +| Apex | `yellowpine.com`. GitHub Pages, this repo, `CNAME` + 4 A records + `www` CNAME. | +| `yellowpine.dev` | Redirect domain. Porkbun URL forwarding, 301, wildcard, path preserved. Kept, not dropped: it is in print and in the git history. | +| Email | The 16 aliases move to `@yellowpine.com`, same convention (`cansinyildiz+.yellowpine@gmail.com`). `.dev` forwarding is removed, so `.dev` is redirect-only. | +| Previous owner's records | Mailgun MX pair, Mailgun SPF, and the two `_acme-challenge` TXT records deleted. The TXT pair validated the wildcard certificate for the URL forwarding we are removing. | +| The 2026-09-08 spec | Left as written. It records what was true then; this supersedes rather than edits it. | + +## Non-goals + +- No redesign. Identity v2.2 and the page itself are untouched apart from three URLs. +- No change to the free-trial hosted inbox pending setup on `.dev` (expires 2026-09-22). +- No new aliases. The 16 are copied across exactly. + +## Order of operations + +Pages serves exactly one custom domain, so the two domains cannot both be correct at once. +The sequence puts the unavoidable gap on the domain being retired: + +1. `yellowpine.com` DNS -> GitHub Pages (A records, `www`), Porkbun mail (MX, SPF). Previous + owner's records deleted. `.dev` is untouched and fully live throughout. +2. Porkbun URL forward removed from `.com`. +3. The 16 forwards created on `.com`, in the UI - Porkbun's public API has no email endpoints. +4. Confirm `.com` resolves to GitHub before the domain lands in `CNAME`. Registering a custom + domain whose DNS still points elsewhere fails validation, and GitHub never retries. +5. Merge -> Pages re-registers on `.com`. Verify the certificate reaches `approved`, then + enable `https_enforced` in its own call. +6. Immediately after: `.dev` A records, `www`, MX, SPF and forwards deleted; URL forward added. + +**Known transitional risk.** Between 5 and Porkbun issuing a certificate for `yellowpine.dev` +in 6, `.dev` is dark. `.dev` is HSTS-preloaded at the TLD level, so it hard-fails in browsers +with no HTTP fallback to degrade to. This is why `.dev` is flipped last and only once `.com` +is confirmed serving: the outage lands on the domain being retired, never on the live one. + +## Verification + +`yellowpine.com` serves 200 over valid HTTPS; `www.yellowpine.com` reaches it; `yellowpine.dev` +and `www.yellowpine.dev` 301 to it over valid HTTPS; mail to an alias at `@yellowpine.com` +arrives. The suite's own gate is `npm test` with the network checks enabled. diff --git a/index.html b/index.html index c7c02ed..3fa869b 100644 --- a/index.html +++ b/index.html @@ -6,11 +6,11 @@ Yellow Pine | Thoughtful tools, built with care - + - - + + diff --git a/package.json b/package.json index 7dfea97..6121b01 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@yellow-pine/website", "private": true, - "description": "The Yellow Pine website (yellowpine.dev) - one hand-written page, served by GitHub Pages.", + "description": "The Yellow Pine website (yellowpine.com) - one hand-written page, served by GitHub Pages.", "type": "module", "scripts": { "test": "node --test tests/site.test.mjs" diff --git a/tests/site.test.mjs b/tests/site.test.mjs index aae2c17..e39c0b7 100644 --- a/tests/site.test.mjs +++ b/tests/site.test.mjs @@ -1,16 +1,19 @@ -// Invariants for the Yellow Pine website (index.html), served at yellowpine.dev. +// Invariants for the Yellow Pine website (index.html), served at yellowpine.com. // // The site is a single hand-written file with no build step, so there is no compiler to // catch mistakes. These tests are the gate instead: // -// 1. CNAME says exactly yellowpine.dev, and the page's canonical URL agrees. -// 2. Every github.com/yellow-pine/* link is reachable WITHOUT auth — the publish rule +// 1. CNAME says exactly yellowpine.com, and the page's canonical URL agrees. +// 2. The contact address is on that same domain. These are one fact in two places, and +// they did drift: the page advertised hello@yellowpine.com for two days while the +// site served yellowpine.dev and the .com MX belonged to the previous owner. +// 3. Every github.com/yellow-pine/* link is reachable WITHOUT auth — the publish rule // expressed without naming any repo: a link to a private repo 404s for the anonymous // public and fails here, so nothing private can leak onto the site. -// 3. Every product link is live (a dead product link is worse than no link). -// 4. The page has no external asset dependencies beyond Google Fonts — the mark and +// 4. Every product link is live (a dead product link is worse than no link). +// 5. The page has no external asset dependencies beyond Google Fonts — the mark and // favicon must stay inline, and the wordmark is live type rather than outlines. -// 5. Brand v2.2 guardrails hold: the identity yellow appears only inside the inline mark +// 6. Brand v2.2 guardrails hold: the identity yellow appears only inside the inline mark // and the selection wash, and focus rings are azure, never yellow. // // Zero dependencies: node:test + global fetch (Node >= 20). Network checks honor @@ -25,7 +28,7 @@ import { fileURLToPath } from 'node:url'; const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..'); const read = (p) => readFileSync(join(repoRoot, p), 'utf8'); -const DOMAIN = 'yellowpine.dev'; +const DOMAIN = 'yellowpine.com'; const html = read('index.html'); const skipNetwork = process.env.SKIP_NETWORK === '1'; @@ -83,6 +86,22 @@ test('page declares the canonical URL and matches CNAME', () => { assert.equal(new URL(canonical).hostname, DOMAIN); }); +test('the contact address is on the domain the site serves', () => { + // A mailto on some other domain is a dead address the moment that domain stops being + // ours to route: the reader sees an invitation to write, and the mail lands nowhere. + // Contact domain and served domain are one fact, so they are asserted as one. + const mailtos = allHrefs.filter((h) => h.startsWith('mailto:')); + assert.ok(mailtos.length > 0, 'the page should offer a way to reach us'); + for (const href of mailtos) { + const address = href.slice('mailto:'.length); + assert.equal( + address.split('@')[1], + DOMAIN, + `contact address is not on ${DOMAIN}: ${address}`, + ); + } +}); + test('page has the metadata a shared link needs', () => { for (const needle of [ '', From 282e48641633404b1365249b5429f04c38af2e55 Mon Sep 17 00:00:00 2001 From: Cansin Yildiz Date: Thu, 10 Sep 2026 00:23:30 +0200 Subject: [PATCH 2/3] Ignore a mailto's query tail when checking its domain mailto: hrefs may carry ?subject= or ?body=, and splitting on @ alone folds that tail into the host: "yellowpine.com?subject=Hello" never equals DOMAIN, so the invariant would fail on a link that is perfectly correct. Found reviewing the diff, not in the wild -- neither address on the page carries a query today. Fixing it now keeps the next person from hitting a false failure and "fixing" it by loosening the assertion. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Q5nJ8eKEb1wm5tCzxipXHS --- tests/site.test.mjs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/site.test.mjs b/tests/site.test.mjs index e39c0b7..ad1e2b8 100644 --- a/tests/site.test.mjs +++ b/tests/site.test.mjs @@ -93,7 +93,8 @@ test('the contact address is on the domain the site serves', () => { const mailtos = allHrefs.filter((h) => h.startsWith('mailto:')); assert.ok(mailtos.length > 0, 'the page should offer a way to reach us'); for (const href of mailtos) { - const address = href.slice('mailto:'.length); + // Strip any ?subject=/?body= tail, or the domain check reads it as part of the host. + const address = href.slice('mailto:'.length).split('?')[0]; assert.equal( address.split('@')[1], DOMAIN, From c219732998e8f06af6b77c92ce427ea1b79c4ecd Mon Sep 17 00:00:00 2001 From: Cansin Yildiz Date: Thu, 10 Sep 2026 00:26:32 +0200 Subject: [PATCH 3/3] Close the gaps a review found in the domain guard and the docs Five findings, all real: The domain guard missed og:url and og:site_name entirely. hrefsIn() matches only href=/src=, so the og:* tags never reached the mailto or canonical checks, and the metadata test asserted those tags EXIST without ever reading what they say. A future move could update CNAME and canonical, miss og:*, and go green while every shared link advertised the old domain. Proven by reverting og:url alone: CNAME and canonical both still passed, and only the new guard failed. The address is also visible anchor text, not just an href. Editing one without the other left the suite green while anyone copying the address by eye got the dead domain. Also proven by mutation. README documented `-f cname=""` as the certificate recovery. The Pages API removes a custom domain only on a JSON null, and gh's -f sends raw strings, so the one command written down for the emergency would fail at the moment it was needed. Now `-F cname=null`, with the reason next to it. The spec claimed `npm test` was the gate for four things it cannot check: the apex is excluded from the liveness test by isSelf, and nothing fetches .dev or tests mail. A certificate that never issued would have kept CI green forever. The verification section now says plainly that those checks are manual. The spec's non-goal about the .dev trial inbox contradicted step 6, which deletes the MX and SPF that inbox would need. Reworded: it is neither set up nor cancelled, it lapses on 2026-09-22, and nothing reaches it meanwhile -- which is what redirect-only means. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Q5nJ8eKEb1wm5tCzxipXHS --- README.md | 15 ++++++++--- .../2026-09-10-yellowpine-com-apex-design.md | 13 ++++++++-- tests/site.test.mjs | 25 +++++++++++++++++++ 3 files changed, 48 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 0cee00e..c0fbbb2 100644 --- a/README.md +++ b/README.md @@ -64,9 +64,18 @@ Pages serves the default branch root. `CNAME` sets the domain. **Point DNS at GitHub _before_ the domain lands in `CNAME`.** If Pages registers a custom domain whose DNS still resolves elsewhere, certificate validation fails and GitHub never -retries on its own: Pages keeps serving over HTTP, `https_certificate` is absent entirely, -and the site is one `gh api -X PUT .../pages -f cname=""` and re-set away from working. Pass -`https_enforced` in its own call afterwards — inline it 404s with "certificate does not exist +retries on its own: Pages keeps serving over HTTP and `https_certificate` is absent entirely. + +To recover, unset the custom domain and set it again, which re-runs validation: + +```sh +gh api -X PUT repos/yellow-pine/yellow-pine.github.io/pages -F cname=null +gh api -X PUT repos/yellow-pine/yellow-pine.github.io/pages -f cname=yellowpine.com +``` + +Note `-F`, not `-f`: the API removes the domain only on a JSON `null`, and `-f` would send +the literal string `"null"`. Enable **Enforce HTTPS** in a separate call once the certificate +issues — passing `https_enforced` alongside `cname` 404s with "certificate does not exist yet". Unlike the old `.dev` apex, `.com` is not HSTS-preloaded, so HTTPS is not free here. **Enforce diff --git a/docs/superpowers/specs/2026-09-10-yellowpine-com-apex-design.md b/docs/superpowers/specs/2026-09-10-yellowpine-com-apex-design.md index 550b0be..1e44c9a 100644 --- a/docs/superpowers/specs/2026-09-10-yellowpine-com-apex-design.md +++ b/docs/superpowers/specs/2026-09-10-yellowpine-com-apex-design.md @@ -37,7 +37,9 @@ live page before anything was changed. ## Non-goals - No redesign. Identity v2.2 and the page itself are untouched apart from three URLs. -- No change to the free-trial hosted inbox pending setup on `.dev` (expires 2026-09-22). +- The free-trial hosted inbox pending setup on `.dev` is neither set up nor cancelled; it + lapses on its own on 2026-09-22. Removing `.dev`'s MX and SPF does mean nothing reaches it + in the meantime, which is the point of making `.dev` redirect-only. - No new aliases. The 16 are copied across exactly. ## Order of operations @@ -64,4 +66,11 @@ is confirmed serving: the outage lands on the domain being retired, never on the `yellowpine.com` serves 200 over valid HTTPS; `www.yellowpine.com` reaches it; `yellowpine.dev` and `www.yellowpine.dev` 301 to it over valid HTTPS; mail to an alias at `@yellowpine.com` -arrives. The suite's own gate is `npm test` with the network checks enabled. +arrives. + +**None of that is covered by `npm test`.** The suite asserts the repo's own invariants — +`CNAME`, canonical, `og:*`, the contact address, the brand guardrails — and its only network +checks are the `github.com/yellow-pine/*` links and the product links. The apex is explicitly +excluded from the liveness test by `isSelf`, so a certificate that never issues, or a `.dev` +forward that leaves the domain dark, would keep CI green indefinitely. The four checks above +are manual, by `curl`, and are the real gate for this change. diff --git a/tests/site.test.mjs b/tests/site.test.mjs index ad1e2b8..cc13b6a 100644 --- a/tests/site.test.mjs +++ b/tests/site.test.mjs @@ -103,6 +103,31 @@ test('the contact address is on the domain the site serves', () => { } }); +test('the social metadata points at the same domain', () => { + // hrefsIn() only sees href=/src=, so og:url never reaches the mailto and canonical + // checks above, and the metadata test below only asserts these tags EXIST, never what + // they say. Without this, a domain move can update CNAME and canonical, miss og:*, and + // still go green - while every shared link and social preview advertises the old domain. + const ogUrl = html.match(/ { + // The address is a link target AND visible text. Edit one without the other and the + // suite stays green while readers who copy it by eye, and crawlers reading the text, + // get the dead domain. + const links = [...html.matchAll(/]*>([^<]+)<\/a>/g)]; + assert.ok(links.length > 0, 'expected at least one mailto link'); + for (const [, target, text] of links) { + assert.equal(text.trim(), target.split('?')[0], 'anchor text must match the mailto target'); + } +}); + test('page has the metadata a shared link needs', () => { for (const needle of [ '',