docs: record contact/draft fixes, staging URL, and DNS-01 cutover finding
CI / ci (push) Successful in 1m29s

Marks sections 1.3 and 1.5 fixed, adds section 2.6 for the staging URL, and
records that letsencrypt-prod solves via DNS-01 against Cloudflare - which
removes the HTTP-01 ordering problem from section 1.1, leaving only the
question of whether bluecapstrategies.com is reachable by the cert-manager
Cloudflare token.

Adds section 2.5 for developer-facing copy found on the /insights/ index
while fixing the draft leak. Left unfixed on purpose: what that page should
say depends on the owner decision in section 1.5.
This commit is contained in:
bmr_bluecap committed 2026-09-23 09:48:40 -04:00
1 parent 4e354d2721
commit 5bfc5981cd
1 file changed
+54 -18
+54 -18
View File
@@ -26,6 +26,7 @@ Status legend: `[ ]` open · `[x]` done · `[?]` needs verification · 🛑 owne
| `npm run seo:lint` | green, 21 pages clean | | `npm run seo:lint` | green, 21 pages clean |
| `npm run test:e2e` | **42 passed / 4 failed** (stale assertions, see §2.1) | | `npm run test:e2e` | **42 passed / 4 failed** (stale assertions, see §2.1) |
| CI root cause | **found + fixed in-repo 2026-09-22** — see §1.2; needs a green run to confirm | | CI root cause | **found + fixed in-repo 2026-09-22** — see §1.2; needs a green run to confirm |
| Staging URL | **https://bcs.int.7tl-homelab.com** — LAN-only, valid cert (§2.6) |
| Old WordPress site | untouched and serving — must stay that way until cutover | | Old WordPress site | untouched and serving — must stay that way until cutover |
| Gitea host | KVM VM `gitea-1` (10.66.15.22), **Debian 12**, docker compose — *not* in k3s | | Gitea host | KVM VM `gitea-1` (10.66.15.22), **Debian 12**, docker compose — *not* in k3s |
| Gitea / act_runner | 1.27.3 / 3.4.2 — both healthy | | Gitea / act_runner | 1.27.3 / 3.4.2 — both healthy |
@@ -44,10 +45,14 @@ Nothing below is optional. Cutover is unsafe until all of §1 is closed.
- [ ] Uncomment the `tls:` block and `cert-manager.io/cluster-issuer` annotation in - [ ] Uncomment the `tls:` block and `cert-manager.io/cluster-issuer` annotation in
`k8s/ingress.yaml`; set `secretName: bluecap-website-tls`. `k8s/ingress.yaml`; set `secretName: bluecap-website-tls`.
- [ ] Use the existing `letsencrypt-prod` ClusterIssuer (present in-cluster, 472d old, Ready). - [ ] Use the existing `letsencrypt-prod` ClusterIssuer (present in-cluster, 472d old, Ready).
- [ ] **Resolve the ordering problem:** HTTP-01 validation requires DNS to already point at the - [x] **Ordering problem SOLVED.** `letsencrypt-prod` solves via **DNS-01 against Cloudflare**,
cluster, but we don't want to move DNS before a cert exists. Pick one: not HTTP-01, so a cert can be issued *before* any DNS points at the cluster — no insecure
- DNS-01 issuer (preferred — issues the cert *before* cutover, zero insecure window), or window. Proven end to end on 2026-09-23: the staging host issued a valid LE cert in ~75s
- accept a short window: cut DNS, then let HTTP-01 solve, serving plain HTTP for ~1–2 min. with no public reachability (§2.6).
- [ ] 🛑 **Verify the one remaining dependency:** `bluecapstrategies.com` must be in the
Cloudflare account that cert-manager's API token can reach. The domain is at Bluehost
today, so this is probably **not** yet true. It is the single fact separating a clean
zero-downtime cutover from one with a gap — check it early, not on launch day.
- [ ] Verify `https://` serves a valid chain and HTTP→HTTPS redirects before announcing. - [ ] Verify `https://` serves a valid chain and HTTP→HTTPS redirects before announcing.
### 1.2 CI/CD is stalled — no deploy since 2026-07-20 🛑 hard blocker ### 1.2 CI/CD is stalled — no deploy since 2026-07-20 🛑 hard blocker
@@ -111,11 +116,13 @@ Remaining:
uses apt at all, and the job image is no longer EOL (bookworm still serves `docker.io` fine). The uses apt at all, and the job image is no longer EOL (bookworm still serves `docker.io` fine). The
static-binary approach remains the more durable of the two; keep it regardless. static-binary approach remains the more durable of the two; keep it regardless.
### 1.3 Developer-facing copy is visible on the live contact page 🛑 ### 1.3 Developer-facing copy on the contact page ✅ FIXED 2026-09-23
`src/pages/contact.astro:37-41` renders, to real visitors: Two notes written for developers, not visitors, were being rendered:
> "The form is static-first and posts to the configured endpoint when - the form note explaining `PUBLIC_CONTACT_FORM_ENDPOINT`, and
> `PUBLIC_CONTACT_FORM_ENDPOINT` is set. Without an endpoint, it opens an email…" - a "Discovery note" about the WordPress page holding placeholder contact values.
- [ ] Delete that `.form-note`. Replace with visitor-appropriate copy or nothing.
Replaced the first with reader-facing copy and dropped the second (the deviation it described is
already recorded in `docs/content-deviations.md`). Commit `4e354d2`.
### 1.4 Contact form has no endpoint 🛑 ### 1.4 Contact form has no endpoint 🛑
`PUBLIC_CONTACT_FORM_ENDPOINT` is `""` in `k8s/configmap.yaml`, so the form falls back to a `PUBLIC_CONTACT_FORM_ENDPOINT` is `""` in `k8s/configmap.yaml`, so the form falls back to a
@@ -124,15 +131,15 @@ mailto handoff. Inbound leads will be lost or awkward on day one.
- [ ] Test a real submission end-to-end and confirm `/thank-you/` is reached. - [ ] Test a real submission end-to-end and confirm `/thank-you/` is reached.
- [ ] Confirm the delivery address is monitored. - [ ] Confirm the delivery address is monitored.
### 1.5 Placeholder blog post would go live and be indexed 🛑 ### 1.5 Draft insight was being published and indexed ✅ FIXED 2026-09-23
`src/content/insights/example-draft.md` has `draft: true`, but the flag is **declared in the Zod `src/pages/insights/[slug].astro` called `getCollection("insights")` unfiltered, so the
schema and never enforced** — no `filter` on `getCollection("insights")` in `draft: true` placeholder built to `/insights/example-draft/` **and** appeared in
`src/pages/insights/[slug].astro` or the index. Result: `/insights/example-draft/` is built *and* `sitemap-0.xml`. The schema declared the flag and `rss.xml.ts` already honoured it — only the page
listed in `dist/sitemap-0.xml`, so "Example draft insight" ships to Google. route ignored it. Filtered in `getStaticPaths`, matching the RSS pattern. Build is 21 → 20 pages
- [ ] Either filter drafts out of `getCollection` (both the index and `getStaticPaths`) — the and the URL is gone from the sitemap. Commit `4e354d2`.
durable fix — or delete the file.
- [ ] Re-run `npm run build` and confirm the URL is gone from `sitemap-0.xml`. - [ ] 🛑 **Still open (owner):** whether `/insights/` should ship at all with zero real articles.
- [ ] Decide whether `/insights/` should ship at all with zero real articles (see §3). It is currently in the primary nav. See also §2.5.
### 1.6 Canonical host is undecided 🛑 owner ### 1.6 Canonical host is undecided 🛑 owner
The ingress answers on **both** apex and `www`; `astro.config.mjs`, `robots.txt`, and the The ingress answers on **both** apex and `www`; `astro.config.mjs`, `robots.txt`, and the
@@ -146,6 +153,35 @@ hosts with no redirect splits SEO signal from day one.
## 2. Should-fix before cutover ## 2. Should-fix before cutover
### 2.5 `/insights/` index still renders developer-facing copy
Found while fixing §1.5. `src/pages/insights/index.astro` shows visitors two cards written for a
developer, not a reader:
- *"Publishing path"* — "Future posts can be added as Markdown or MDX in
`src/content/insights` with typed frontmatter for title, description, publish date…"
- *"RSS support"* — "The feed is ready for future article imports even though no public posts are
published yet."
The hero also announces the section as a "Future publishing home", i.e. it tells visitors the
section is empty. Same defect class as §1.3, but left alone deliberately: what the page *should*
say depends on the owner's call in §1.5 on whether `/insights/` ships at all.
- [ ] If `/insights/` ships: delete both note-cards and rewrite the hero as reader-facing copy.
- [ ] If it does not: drop it from the primary nav and exclude the route from the sitemap.
### 2.6 Staging URL ✅ LIVE 2026-09-23
**https://bcs.int.7tl-homelab.com** — review the rebuild in a browser, valid cert, no warning.
- LAN-only: `*.int.7tl-homelab.com` resolves to the internal traefik LB (10.66.15.30). Not
reachable from the internet; the real domain is untouched and WordPress still serves it.
- Cert issued by Let's Encrypt via cert-manager DNS-01 in ~75s, `CN=bcs.int.7tl-homelab.com`,
valid to 2026-12-21, auto-renewing.
- All 9 public routes verified 200.
- Commit `171c8c9`. Note it also corrected the issuer name in `k8s/ingress.yaml`: the commented
placeholder said `letsencrypt-production`, but the ClusterIssuer is **`letsencrypt-prod`** —
uncommenting it as written at cutover would have silently failed to issue.
- ArgoCD may take a few minutes to poll a push; force it with
`kubectl annotate application bcs-website -n argocd argocd.argoproj.io/refresh=hard --overwrite`.
### 2.1 e2e suite is red — stale assertions, not a regression ### 2.1 e2e suite is red — stale assertions, not a regression
4 of 46 tests fail. Both failures assert **invented copy that the exact-copy work correctly 4 of 46 tests fail. Both failures assert **invented copy that the exact-copy work correctly
removed**: removed**: