From 0f769d7bddeaf3e1639c9726d9be0d29bfcf191f Mon Sep 17 00:00:00 2001
From: Brad Rodgers
Date: Wed, 23 Sep 2026 10:05:55 -0400
Subject: [PATCH] feat(contact): wire the form to a real endpoint at build time
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.
---
.gitea/workflows/ci.yml | 9 ++++-
Dockerfile | 8 +++++
k8s/configmap.yaml | 11 +++++-
src/pages/contact.astro | 74 +++++++++++++++++++++++++++++++++--------
src/styles/global.css | 21 ++++++++++++
tests/site.spec.ts | 40 +++++++++++++++++++---
6 files changed, 143 insertions(+), 20 deletions(-)
diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml
index 7a20c09..98d0d31 100644
--- a/.gitea/workflows/ci.yml
+++ b/.gitea/workflows/ci.yml
@@ -94,7 +94,14 @@ jobs:
- name: Build & push image to Gitea Packages
run: |
echo "${{ secrets.CI_TOKEN }}" | docker login 10.66.15.22:3000 -u "${{ github.repository_owner }}" --password-stdin
- docker build -t "$PUSH_IMAGE:${{ steps.tag.outputs.sha }}" -t "$PUSH_IMAGE:latest" .
+ # PUBLIC_* values are inlined at build time (static Astro) — they cannot be supplied at
+ # deploy time. CONTACT_FORM_ENDPOINT is the Formspree URL; set it as a repo Actions
+ # secret. If it is empty the build still succeeds and the contact form degrades to a
+ # mailto draft, so a missing secret is survivable but should be caught in review.
+ docker build \
+ --build-arg PUBLIC_CONTACT_FORM_ENDPOINT="${{ secrets.CONTACT_FORM_ENDPOINT }}" \
+ --build-arg PUBLIC_SITE_URL="https://www.bluecapstrategies.com" \
+ -t "$PUSH_IMAGE:${{ steps.tag.outputs.sha }}" -t "$PUSH_IMAGE:latest" .
# Registry pushes can time out on a slow blob; docker push is resumable, so retry.
push_retry() {
for i in 1 2 3 4 5; do
diff --git a/Dockerfile b/Dockerfile
index 00b0f59..5f20126 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -6,6 +6,14 @@ RUN npm ci
FROM node:22-alpine AS build
WORKDIR /app
+# Astro is a STATIC build: import.meta.env.PUBLIC_* is inlined into the HTML here, at image build
+# time. A runtime env var on the pod can never reach it (the pod only runs nginx over pre-built
+# files), so anything the browser needs must arrive as a build arg. CI passes these; see
+# .gitea/workflows/ci.yml.
+ARG PUBLIC_CONTACT_FORM_ENDPOINT=""
+ARG PUBLIC_SITE_URL="https://www.bluecapstrategies.com"
+ENV PUBLIC_CONTACT_FORM_ENDPOINT=$PUBLIC_CONTACT_FORM_ENDPOINT \
+ PUBLIC_SITE_URL=$PUBLIC_SITE_URL
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build
diff --git a/k8s/configmap.yaml b/k8s/configmap.yaml
index cd486aa..14b1d61 100644
--- a/k8s/configmap.yaml
+++ b/k8s/configmap.yaml
@@ -1,9 +1,18 @@
+# NOTE: this ConfigMap is NOT consumed by the Deployment, and wiring it up would not help.
+# The site is a STATIC Astro build served by nginx — every PUBLIC_* value is inlined into the HTML
+# when the image is built, so a runtime env var cannot change it. Configure these as build args in
+# .gitea/workflows/ci.yml instead (PUBLIC_CONTACT_FORM_ENDPOINT comes from the CONTACT_FORM_ENDPOINT
+# Actions secret).
+#
+# Kept as documentation of the values the build expects. Do not add envFrom pointing at it — that
+# would imply a runtime knob that does not exist.
apiVersion: v1
kind: ConfigMap
metadata:
name: bluecap-website-config
namespace: bluecap-strategies
data:
+ # Mirrors the build arg default in the Dockerfile.
PUBLIC_SITE_URL: "https://www.bluecapstrategies.com"
- # Set to a Formspree, webhook, ntfy, or self-hosted form endpoint before production cutover.
+ # Real value lives in the CONTACT_FORM_ENDPOINT Actions secret, injected at image build.
PUBLIC_CONTACT_FORM_ENDPOINT: ""
diff --git a/src/pages/contact.astro b/src/pages/contact.astro
index 73f9eab..50f4222 100644
--- a/src/pages/contact.astro
+++ b/src/pages/contact.astro
@@ -35,6 +35,8 @@ const endpoint = import.meta.env.PUBLIC_CONTACT_FORM_ENDPOINT ?? "";