The contact form could not be made to work by configuration. Two reasons,
both fixed here.
The endpoint never reached the browser. k8s/configmap.yaml carried
PUBLIC_CONTACT_FORM_ENDPOINT, but the Deployment has no env/envFrom pointing
at that ConfigMap, so it was dead config. Wiring it up would not have helped
either: this is a static Astro build, so import.meta.env.PUBLIC_* is inlined
when the image is built and the pod only serves pre-built HTML. Anything the
browser needs has to arrive as a build arg. Add ARG/ENV to the Dockerfile and
pass --build-arg from CI, sourced from a CONTACT_FORM_ENDPOINT Actions
secret. Rewrite the ConfigMap as documentation of what the build expects,
with an explicit warning not to add envFrom.
Submission behaviour was also wrong for visitors. A native POST would have
navigated them to the form provider's page; post in the background with
fetch() and send them to our own /thank-you/ instead. On failure the form now
stays put, re-enables the button and offers the phone and email fallback,
rather than silently losing the inquiry. Add a honeypot (_gotcha, which
Formspree treats as spam) positioned off-screen rather than display:none, so
bots that skip hidden fields still fill it.
Read data-endpoint at submit time rather than caching it at init, keeping the
attribute the single source of truth.
Tests: replace the old contact assertion, which faked a native GET
navigation, with one that mocks the endpoint and exercises the real fetch
path including the posted payload. Add a case for endpoint failure.
typecheck 0/0/0, build 0/0, seo:lint clean on 20 pages, e2e 44 passed.
The 4 remaining failures are the pre-existing stale nav assertions in
docs/launch-checklist.md section 2.1, untouched here.
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.
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.
There was no way to view the rebuild in a browser: the ingress answered only
on websecure for the production hostnames, which have no cert and still
belong to the live WordPress site. Review meant a port-forward.
Add bcs.int.7tl-homelab.com as a staging route. The *.int zone resolves to
the internal traefik LB (10.66.15.30) and is not reachable from the internet,
so this exposes nothing publicly and leaves the real domain untouched. It
follows the same pattern as the existing argocd/lidarr/tautulli ingresses.
Enable cert-manager on the ingress, but scope the tls block to the staging
host only. The production hostnames stay out of it deliberately: requesting a
cert for names the live site still serves is a cutover step, not a
prerequisite.
Fix the issuer name while here. The commented-out placeholder said
"letsencrypt-production"; the ClusterIssuer is actually "letsencrypt-prod",
so the old hint would have silently failed to issue.
Note for cutover: letsencrypt-prod solves via DNS-01 against Cloudflare, so
the production cert can be issued before DNS moves, avoiding the HTTP-01
ordering problem. Conditional on bluecapstrategies.com being reachable by the
cert-manager Cloudflare token, which is not yet verified.
CI has been failing silently since 2026-08-03. The run for 9f1cbca did
execute (Gitea action_run #6, status=failure, 13m) but died before pushing an
image, so no package version exists for that commit and ArgoCD has been
serving the 2026-07-20 build ever since.
Cause: the "Ensure docker + kustomize are available" step ran
`apt-get install -y docker.io`. The runner's job image was node:18-bullseye
and Debian 11 went EOL, so debian-security stopped serving those package
versions. `apt-get update` still succeeds, so the failure surfaces only at
install time as 404s on docker.io, containerd, systemd and libcap2.
Fetch both tools as pinned static binaries instead. These have no distro
archive dependency, so the same rot cannot recur when bookworm ages out.
Verified in the real job environment: the static client installs and reaches
the host daemon through the mounted socket.
The runner job image has separately been moved to node:22-bookworm, which
also resolves the immediate breakage, but the static-binary approach is kept
as the durable fix.
Also refresh the prerequisites header: items 3 and 4 still described the
pre-ADR-0008 registry design (k3s now pulls over public HTTPS, and the
private package's pull auth comes from External Secrets).
Cutover tasks were scattered across three sources that disagreed with each
other and with reality: planning/parity-checklist.md (git-ignored, content
only), docs/seo-aeo-optimization.md (SEO only, listed the Zod schema,
seo-lint and breadcrumbs as open when all three are done, and called for
Uptime Kuma which ADR-0004 replaced with monylog), and session notes.
Add docs/launch-checklist.md as the single source of truth, verified against
the working tree, the cluster and ArgoCD rather than inherited from the older
docs. Point the SEO doc's two checkbox blocks at it and keep only reference
detail there.
Also records blockers that were not tracked anywhere: TLS never wired on the
ingress, developer-facing copy rendered on the contact page, the contact form
having no endpoint, and the draft insight post building into the sitemap
because the draft flag is declared in Zod but never filtered on.