Compare commits

...
2 Commits
Author SHA1 Message Date
bmr_bluecap 5bfc5981cd 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.
2026-09-23 09:48:40 -04:00
bmr_bluecap 4e354d2721 fix: stop publishing draft insights; remove developer copy from contact page
Two defects that would have shipped to visitors at cutover.

Draft posts were reaching the build. The insights schema declares a `draft`
flag and rss.xml.ts filters on it, but [slug].astro called getCollection()
unfiltered, so /insights/example-draft/ was generated and listed in
sitemap-0.xml. "Example draft insight" would have been crawled and indexed.
Filter in getStaticPaths, matching the existing pattern in rss.xml.ts. Build
drops from 21 to 20 pages and the URL is gone from the sitemap.

The contact page rendered two notes written for developers, not visitors: one
explaining that the form "posts to the configured endpoint when
PUBLIC_CONTACT_FORM_ENDPOINT is set", and a "Discovery note" about the
WordPress page containing placeholder contact values. Replace the first with
a line addressed to the reader and drop the second; the deviation it
described is already recorded in docs/content-deviations.md, which is where
it belongs.

build 0/0, seo:lint clean on 20 pages. e2e unchanged at 42/46 — the 4
failures are the pre-existing stale assertions tracked in
docs/launch-checklist.md section 2.1.
2026-09-23 09:48:07 -04:00
3 changed files with 58 additions and 26 deletions

No files matched your search

+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**:
+1 -7
View File
@@ -27,17 +27,11 @@ const endpoint = import.meta.env.PUBLIC_CONTACT_FORM_ENDPOINT ?? "";
<h2>Contact Info</h2> <h2>Contact Info</h2>
<p><strong>Phone</strong><br /><a href={`tel:${site.phone.replaceAll("-", "")}`}>{site.phone}</a></p> <p><strong>Phone</strong><br /><a href={`tel:${site.phone.replaceAll("-", "")}`}>{site.phone}</a></p>
<p><strong>Email</strong><br /><a href={site.contactEmailHref}>{site.email}</a></p> <p><strong>Email</strong><br /><a href={site.contactEmailHref}>{site.email}</a></p>
<p class="form-note">
Discovery note: the current WordPress contact page includes placeholder phone and email
values. This rebuild uses the verified homepage/footer contact details.
</p>
</aside> </aside>
<div class="form-panel"> <div class="form-panel">
<h2>Request a consultation</h2> <h2>Request a consultation</h2>
<p class="form-note"> <p class="form-note">
The form is static-first and posts to the configured endpoint when Tell us about your project and we will follow up within two business days.
<code>PUBLIC_CONTACT_FORM_ENDPOINT</code> is set. Without an endpoint, it opens an email
draft after validating required fields.
</p> </p>
<form class="contact-form" method="post" action={endpoint || "/thank-you/"} data-contact-form data-endpoint={endpoint}> <form class="contact-form" method="post" action={endpoint || "/thank-you/"} data-contact-form data-endpoint={endpoint}>
<input type="hidden" name="subject" value="BlueCap Strategies website inquiry" /> <input type="hidden" name="subject" value="BlueCap Strategies website inquiry" />
+3 -1
View File
@@ -6,7 +6,9 @@ import ContactCta from "@components/ContactCta.astro";
import { breadcrumbSchema, pageSchema } from "@utils/schema"; import { breadcrumbSchema, pageSchema } from "@utils/schema";
export async function getStaticPaths() { export async function getStaticPaths() {
const posts = await getCollection("insights"); // Drafts must never reach the build: an unfiltered getCollection() published
// /insights/example-draft/ AND listed it in the sitemap. Matches rss.xml.ts.
const posts = await getCollection("insights", ({ data }) => !data.draft);
return posts.map((post) => ({ return posts.map((post) => ({
params: { slug: post.slug }, params: { slug: post.slug },
props: { post }, props: { post },