The old entry assumed the cluster could be pointed at directly and that DNS-01 removed the certificate ordering problem. Both were wrong once the real traffic path was checked. Public traffic has to transit the Pangolin VPS, so two certificates are needed from two different systems: the edge cert from Pangolin's own Traefik resolver, and the internal-hop cert from cert-manager, because every k3s-backed Pangolin resource proxies to https:443 (the pattern Authentik, paperless, immich and dav all follow). Pangolin's resolver uses HTTP-01 on entrypoint web, not DNS-01, so the edge cert cannot be pre-issued and a short window at cutover is unavoidable. cert-manager can still pre-issue the internal cert. There is also only one Cloudflare token, not two: Pangolin holds no Cloudflare credentials at all. Records the proxied-vs-DNS-only decision, which earlier advice got backwards (this zone is already orange-clouded while the homelab zones are not), the Flexible-SSL redirect-loop risk, the ~5 minute rollback from ttl=auto, and that mail is unaffected because the MX records are not proxied. Cloudflare token now covers bluecapstrategies.com.
BlueCap Strategies Website
Static marketing site for BlueCap Strategies, built with Astro and deployed to a k3s homelab cluster via GitOps (ArgoCD).
Stack
- Astro 5 — static output (
output: "static"), no runtime JS framework. - Content Collections (Markdown) — page copy lives in
src/content/(services, focus-areas, pages, insights), validated by a Zod SEO schema at build time. - nginx (Alpine) serves the built
dist/from a multi-stage Docker image. - ArgoCD + Kustomize (
k8s/) reconcile the deployment; images are published to GHCR by CI.
Local development
npm install
npm run dev # dev server at http://localhost:4321
npm run build # astro check (typecheck) + astro build → dist/
npm run preview # serve the production build
npm run seo:lint # structural SEO checks over dist/ (run after build)
npm run test:e2e # Playwright end-to-end tests
Copy .env.example to .env and fill in values as needed (site URL, contact form endpoint,
analytics keys). Never commit .env.
Editing content
Page copy is Markdown with typed frontmatter — no code changes needed to edit words:
src/content/services/*.md— service detail pagessrc/content/focus-areas/*.md— focus-area detail pagessrc/content/pages/*.md— standalone long-form pages (e.g. About)src/content/insights/*.md— blog/insights posts
Frontmatter is enforced by the Zod schema in src/content.config.ts (e.g. description must be
120–158 chars), so the build fails fast on SEO violations. The .md files are build-time
source: Astro compiles them to static HTML in dist/. The Markdown itself is never served.
Project layout
src/ Astro pages, layouts, components, content collections, styles
public/ Static assets served as-is (images, robots.txt, healthz)
scripts/ Build tooling (seo-lint.mjs)
tests/ Playwright e2e specs
deploy/ nginx.conf for the runtime image
k8s/ Kustomize manifests (namespace, deployment, service, ingress, configmap)
docs/ Committed engineering records — ADRs, deployment, SEO/AEO, deviations
Dockerfile Multi-stage build → nginx image containing only dist/
planning/ (git-ignored) holds working docs — content inventories, parity checklists, and the
original rebuild brief. See docs/adr/ for the decision record.
Deployment
Push to main → GitHub Actions runs the quality gates, builds and pushes a <git-sha>-tagged
image to GHCR, and updates the image tag in k8s/. ArgoCD reconciles the change to k3s. GitHub
never receives cluster credentials. See docs/gitops-deployment-strategy.md.