diff --git a/docs/launch-checklist.md b/docs/launch-checklist.md index bc5a598..4f78525 100644 --- a/docs/launch-checklist.md +++ b/docs/launch-checklist.md @@ -26,6 +26,7 @@ Status legend: `[ ]` open Β· `[x]` done Β· `[?]` needs verification Β· πŸ›‘ owne | `npm run seo:lint` | green, 21 pages clean | | `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 | +| 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 | | 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 | @@ -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 `k8s/ingress.yaml`; set `secretName: bluecap-website-tls`. - [ ] 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 - cluster, but we don't want to move DNS before a cert exists. Pick one: - - DNS-01 issuer (preferred β€” issues the cert *before* cutover, zero insecure window), or - - accept a short window: cut DNS, then let HTTP-01 solve, serving plain HTTP for ~1–2 min. +- [x] **Ordering problem SOLVED.** `letsencrypt-prod` solves via **DNS-01 against Cloudflare**, + not HTTP-01, so a cert can be issued *before* any DNS points at the cluster β€” no insecure + window. Proven end to end on 2026-09-23: the staging host issued a valid LE cert in ~75s + 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. ### 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 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 πŸ›‘ -`src/pages/contact.astro:37-41` renders, to real visitors: -> "The form is static-first and posts to the configured endpoint when -> `PUBLIC_CONTACT_FORM_ENDPOINT` is set. Without an endpoint, it opens an email…" -- [ ] Delete that `.form-note`. Replace with visitor-appropriate copy or nothing. +### 1.3 Developer-facing copy on the contact page βœ… FIXED 2026-09-23 +Two notes written for developers, not visitors, were being rendered: +- the form note explaining `PUBLIC_CONTACT_FORM_ENDPOINT`, and +- a "Discovery note" about the WordPress page holding placeholder contact values. + +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 πŸ›‘ `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. - [ ] Confirm the delivery address is monitored. -### 1.5 Placeholder blog post would go live and be indexed πŸ›‘ -`src/content/insights/example-draft.md` has `draft: true`, but the flag is **declared in the Zod -schema and never enforced** β€” no `filter` on `getCollection("insights")` in -`src/pages/insights/[slug].astro` or the index. Result: `/insights/example-draft/` is built *and* -listed in `dist/sitemap-0.xml`, so "Example draft insight" ships to Google. -- [ ] Either filter drafts out of `getCollection` (both the index and `getStaticPaths`) β€” the - durable fix β€” or delete the file. -- [ ] Re-run `npm run build` and confirm the URL is gone from `sitemap-0.xml`. -- [ ] Decide whether `/insights/` should ship at all with zero real articles (see Β§3). +### 1.5 Draft insight was being published and indexed βœ… FIXED 2026-09-23 +`src/pages/insights/[slug].astro` called `getCollection("insights")` unfiltered, so the +`draft: true` placeholder built to `/insights/example-draft/` **and** appeared in +`sitemap-0.xml`. The schema declared the flag and `rss.xml.ts` already honoured it β€” only the page +route ignored it. Filtered in `getStaticPaths`, matching the RSS pattern. Build is 21 β†’ 20 pages +and the URL is gone from the sitemap. Commit `4e354d2`. + +- [ ] πŸ›‘ **Still open (owner):** whether `/insights/` should ship at all with zero real articles. + It is currently in the primary nav. See also Β§2.5. ### 1.6 Canonical host is undecided πŸ›‘ owner 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.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 4 of 46 tests fail. Both failures assert **invented copy that the exact-copy work correctly removed**: