Initial commit: BlueCap Strategies Astro site

Static Astro 5 site for BlueCap Strategies with exact live-site content
parity, deployed to k3s via GitOps (ArgoCD).

- Page copy in Markdown content collections (services, focus-areas, pages,
  insights) with a Zod SEO schema enforced at build time
- Homepage + About restored to exact live copy; real live-site imagery
- Build-time SEO linter (scripts/seo-lint.mjs) and Playwright e2e suite
- Multi-stage Dockerfile (nginx serves dist/) and Kustomize manifests (k8s/)
- Per-agent robots.txt; config-driven PostHog + Umami analytics scaffold
- ADRs and engineering docs under docs/

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
gitea_adminandClaude Opus 4.8 committed 2026-07-15 17:40:46 -04:00
commit cd91c4b97c
92 files changed
+10246

No files matched your search

+25
View File
@@ -0,0 +1,25 @@
# ADR-0001: GitOps deployment via ArgoCD (not Flux)
**Status:** Accepted — 2026-07-15
## Context
The site deploys to the homelab k3s cluster. GitHub must never receive kubeconfig or direct
cluster access. An earlier plan specified Flux, but Flux was never implemented, and the homelab
**already runs ArgoCD** (namespace `argocd`, active 21d+) managing other workloads.
## Decision
Use **ArgoCD** as the GitOps reconciler. Model: local edit → GitHub Actions builds + pushes image
to GHCR (`<git-sha>` immutable tags) → CI commits the tag into `k8s/` → ArgoCD auto-syncs the
Kustomize manifests → k3s rolls the Deployment → Traefik serves. Manifests stay as plain Kustomize
under `k8s/` (ArgoCD consumes `k8s/kustomization.yaml` natively; no Helm chart).
## Consequences
- No second GitOps controller to run; reuses existing, understood infra.
- Cluster pulls from Git + GHCR; GitHub never touches the cluster.
- Requires: `git init` + GitHub remote (not yet done), a GHCR image path, and an ArgoCD
`Application` CR (direct or via the homelab app-of-apps).
- Rollback = `git revert` + push, or ArgoCD history rollback.
- Full detail in `docs/gitops-deployment-strategy.md`.
@@ -0,0 +1,30 @@
# ADR-0002: Page copy in Astro Content Collections (Markdown)
**Status:** Accepted — 2026-07-15
## Context
Body copy currently lives inside `.ts` files (`src/data/services.ts`, `src/data/focusAreas.ts`)
as quoted string properties — painful to edit, no spellcheck/preview, and prose is trapped in
code. Separately, the rebuild paraphrased much of the live-site copy, and Phase 2 requires
restoring exact copy (see `docs/parity-checklist.md`). Doing both at once avoids a double
migration.
## Decision
Move prose-heavy page copy **out of `.ts` and into Astro Content Collections as Markdown**, with
typed frontmatter for structured/SEO fields. Split of responsibility:
- **Prose** (service/focus/about body, insights) → Markdown body rendered through a shared layout.
- **Structured/visual data** (metrics grid, nav, service icon+blurb cards, CTAs) → typed
frontmatter or kept in `.ts` where it drives layout/components.
Exact-copy restoration (Phase 2) lands directly in this format, not into the old `.ts` shape.
## Consequences
- Editing copy no longer means editing code; enables an eventual owner-editing workflow.
- Consistent rendering/flow via one styled layout (reduces, not increases, formatting risk).
- Requires content collections for `services`, `focusAreas`, and standalone `pages`, plus
refactoring the `[slug]` routes to read from collections.
- Pairs with ADR-0003 (the collection schema encodes SEO rules).
+30
View File
@@ -0,0 +1,30 @@
# ADR-0003: Enforce SEO rules with Zod + build linter (not Pydantic)
**Status:** Accepted — 2026-07-15
## Context
We want SEO/AEO rules (title/description length, canonical, OG image, structured data, single H1,
alt text) to be *enforced*, not aspirational. The question of a Pydantic-style validator came up,
but this is a TypeScript/Astro build — Pydantic is Python and would mean bolting a foreign runtime
onto a JS pipeline.
## Decision
Enforce in two layers, both native to the stack:
1. **Zod schema** on content-collection frontmatter (Astro Content Collections already run on Zod).
Encodes: title ≤ 55 chars, description 120–158, valid canonical, required OG image, `noindex`
flag, `primaryTopic`. Violations **fail `npm run build`**.
2. **Build-time SEO linter** (`scripts/seo-lint.mjs`) over `dist/**/*.html` for rules Zod can't see
in frontmatter: exactly one `<h1>`, heading order, canonical/title/description present, non-empty
`alt` on every `<img>`, every JSON-LD block parses. Runs in CI (and locally via `npm run seo:lint`).
Do **not** introduce Python/Pydantic.
## Consequences
- Every commit is validated before ArgoCD deploys; "the rules are followed" is guaranteed by CI.
- Rules live with the content schema and the build, in one language.
- Requires the content collections from ADR-0002 to carry the shared `seo` schema.
- Detailed rules in `docs/seo-aeo-optimization.md` §1.
@@ -0,0 +1,30 @@
# ADR-0004: Reuse the monylog stack for ops monitoring (not Uptime Kuma)
**Status:** Accepted — 2026-07-15
## Context
An earlier note proposed deploying Uptime Kuma in-cluster. Inspection showed the homelab already
runs **monylog** (`10.66.15.21`, Ansible role `pglta`): a full self-hosted LGTM + Alertmanager
stack (Prometheus, Grafana, Loki, Tempo, Alertmanager → Pushover/ntfy → phone), deliberately
**external to k8s** so it alerts even when the cluster is down. Grafana Alloy (k3s DaemonSet)
already remote-writes cluster metrics (incl. kube-state-metrics) and ships all pod logs to
monylog Loki.
## Decision
**Reuse monylog; do not deploy Uptime Kuma or any new alerting channel.** BlueCap pod/deployment
metrics and nginx logs already flow in. Add only:
1. `blackbox_exporter` to the monylog compose + a Prometheus scrape job probing the in-cluster
Service (app health) and the public URL (edge-path uptime + TLS expiry).
2. A `web-bluecap.yml` alert rules file (SiteDown, CertExpiringSoon <14d, High5xx,
DeploymentDegraded, PodRestarting).
3. A `service: bluecap` label so Alertmanager routes to existing Pushover; a Grafana dashboard.
## Consequences
- No duplicate monitoring/alerting infra; one pane of glass, alerts already reach the phone.
- Changes land in the Homelab Ansible repo (`roles/monylog`), not this repo.
- monylog being off-cluster gives a genuine external vantage for public-URL probing.
- Detail in `docs/seo-aeo-optimization.md` §6a and `docs/gitops-deployment-strategy.md` §4.3.
@@ -0,0 +1,31 @@
# ADR-0005: Visitor analytics — PostHog + Umami side-by-side trial
**Status:** Accepted — 2026-07-15
## Context
The owner leans toward richer data (PostHog: autocapture, funnels, session replay) but the site is
a low-traffic B2B advisory brochure with a Digital-ESG / low-energy positioning, and self-hosting
PostHog is resource-heavy. Rather than decide in the abstract, the owner chose to **trial both**
and compare. This is product/visitor analytics — distinct from the monylog ops telemetry (ADR-0004).
## Decision
Run **PostHog and Umami side-by-side initially** to evaluate which fits. Recommended trial setup:
- **PostHog — Cloud free tier** (1M events/mo) rather than self-host, to avoid loading the homelab
during evaluation; reverse-proxy ingest through our domain to reduce ad-blocker loss.
- **Umami — self-hosted** in k3s (lightweight, cookieless, no consent banner).
Both are wired through a single config-driven `Analytics.astro` component in `BaseLayout`, gated by
env vars, so either can be toggled and a winner kept without code churn. Track the same event set
in both (consultation-CTA clicks, per-service/focus CTA source, insights reads, contact submits).
## Consequences
- Temporary dual instrumentation (slightly more script weight during the trial).
- **Privacy/consent must cover the superset:** PostHog (esp. session replay) likely triggers
cookie-consent + privacy-policy obligations; add these before enabling PostHog in production.
- Decision criteria: insight value vs. weight/consent/ESG fit. Revisit with a follow-up ADR naming
the winner and the retirement of the other.
- Detail in `docs/seo-aeo-optimization.md` §6b.
+26
View File
@@ -0,0 +1,26 @@
# ADR-0006: Launch a blog at /insights/
**Status:** Accepted — 2026-07-15
## Context
The live WordPress site has **no published blog** (0 posts, 0 `project` entries) but does have an
**unpublished, empty "Insights & Resources" draft** (`page_id=303`) — a blog scaffolded but never
launched. The rebuild already has an `/insights/` section (routes, RSS, content collection, one
example draft). The parity plan and SEO/AEO plan both identify thought leadership as a core
authority/GEO asset.
## Decision
**Launch the blog at `/insights/`** in the rebuild. Keep the existing Astro content-collection
scaffold (Markdown/MDX, RSS, tags, canonical for cross-posts). Publish only **real,
owner-reviewed** content — no fabricated or unreviewed AI-generated articles. Candidate first
pieces: PFAS remediation as a capital-formation problem; scaling marine conservation finance;
making island/coastal infrastructure investable.
## Consequences
- Gives AEO/GEO a surface to be cited from; `Article`/`BlogPosting` schema per SEO plan §3.
- Content is owner-gated — the section can ship empty or with 1–2 real posts; do not backfill with
placeholder articles.
- Insights carry `publishDate`/`modifiedDate`; RSS and sitemap `lastmod` derive from these.
+16
View File
@@ -0,0 +1,16 @@
# Architecture Decision Records
Short, immutable records of significant decisions on the BlueCap Strategies rebuild. Each ADR
captures the context, the decision, and its consequences at a point in time. Supersede rather than
edit: to change a decision, add a new ADR and mark the old one `Superseded by ADR-XXXX`.
Format: Status · Context · Decision · Consequences. Statuses: Proposed · Accepted · Superseded.
| ADR | Title | Status |
|-----|-------|--------|
| [0001](0001-gitops-with-argocd.md) | GitOps deployment via ArgoCD (not Flux) | Accepted |
| [0002](0002-page-copy-in-content-collections.md) | Page copy in Astro Content Collections (Markdown) | Accepted |
| [0003](0003-seo-enforcement-with-zod.md) | Enforce SEO rules with Zod + build linter (not Pydantic) | Accepted |
| [0004](0004-reuse-monylog-for-monitoring.md) | Reuse the monylog stack for ops monitoring (not Uptime Kuma) | Accepted |
| [0005](0005-analytics-posthog-and-umami-trial.md) | Visitor analytics: PostHog + Umami side-by-side trial | Accepted |
| [0006](0006-launch-insights-blog.md) | Launch a blog at /insights/ | Accepted |