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:
commit
cd91c4b97c
92 files changed
+10246
No files matched your search
@@ -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).
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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 |
|
||||
Reference in new issue
Block a user