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:
Brad Rodgers 2026-07-15 17:40:46 -04:00
commit cd91c4b97c
92 changed files with 10246 additions and 0 deletions

10
.dockerignore Normal file
View File

@ -0,0 +1,10 @@
node_modules
dist
.astro
.git
npm-debug.log
Dockerfile
README.md
docs
k8s
.DS_Store

10
.env.example Normal file
View File

@ -0,0 +1,10 @@
PUBLIC_SITE_URL=https://www.bluecapstrategies.com
PUBLIC_CONTACT_FORM_ENDPOINT=
# Analytics — side-by-side trial (ADR-0005). Leave blank to disable each.
# PostHog (Cloud free tier recommended for the trial):
PUBLIC_POSTHOG_KEY=
PUBLIC_POSTHOG_HOST=https://us.i.posthog.com
# Umami (self-hosted):
PUBLIC_UMAMI_SRC=
PUBLIC_UMAMI_WEBSITE_ID=

21
.gitignore vendored Normal file
View File

@ -0,0 +1,21 @@
# Dependencies & build output (CI rebuilds these)
node_modules/
dist/
.astro/
# Secrets — only .env.example is committed
.env
.env.*
!.env.example
# Local tool/editor state
.claude/settings.local.json
npm-debug.log*
.DS_Store
# Test artifacts
test-results/
playwright-report/
# Working/planning docs — kept on disk, not version-controlled (see docs/ for committed records)
planning/

18
Dockerfile Normal file
View File

@ -0,0 +1,18 @@
# syntax=docker/dockerfile:1
FROM node:22-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json* ./
RUN npm ci
FROM node:22-alpine AS build
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build
FROM nginx:1.27-alpine AS runtime
COPY deploy/nginx.conf /etc/nginx/conf.d/default.conf
COPY --from=build /app/dist /usr/share/nginx/html
EXPOSE 80
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD wget -qO- http://127.0.0.1/healthz || exit 1

61
README.md Normal file
View File

@ -0,0 +1,61 @@
# BlueCap Strategies Website
Static marketing site for BlueCap Strategies, built with [Astro](https://astro.build/) 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
```bash
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 pages
- `src/content/focus-areas/*.md` — focus-area detail pages
- `src/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
120158 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`.

15
astro.config.mjs Normal file
View File

@ -0,0 +1,15 @@
import { defineConfig } from "astro/config";
import sitemap from "@astrojs/sitemap";
const site = process.env.PUBLIC_SITE_URL ?? "https://www.bluecapstrategies.com";
export default defineConfig({
site,
output: "static",
trailingSlash: "always",
integrations: [
sitemap({
filter: (page) => !page.includes("/thank-you/"),
}),
],
});

26
deploy/nginx.conf Normal file
View File

@ -0,0 +1,26 @@
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Cache-Control "public, max-age=300";
location = /healthz {
access_log off;
try_files /healthz =200;
}
location / {
try_files $uri $uri/ /404/index.html;
}
error_page 404 /404/index.html;
location ~* \.(?:css|js|jpg|jpeg|png|webp|svg|ico|woff2)$ {
try_files $uri =404;
add_header Cache-Control "public, max-age=31536000, immutable";
}
}

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`.

View File

@ -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).

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 120158, 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.

View File

@ -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.

View File

@ -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.

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 12 real posts; do not backfill with
placeholder articles.
- Insights carry `publishDate`/`modifiedDate`; RSS and sitemap `lastmod` derive from these.

16
docs/adr/README.md Normal file
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 |

View File

@ -0,0 +1,78 @@
# Content Deviations
Intentional differences between the live WordPress site (`docs/live-site-content-inventory.md`)
and the Astro rebuild, logged per the parity plan. Updated **2026-07-15** after the copy
migration to Markdown collections (ADR-0002).
## Service & focus-area detail pages — exact copy restored
- The 8 service detail pages and 4 focus-area detail pages now carry **verbatim live-site copy** in
`src/content/services/*.md` and `src/content/focus-areas/*.md`. Each service page: H1 (service
name) → H2 (exact tagline) → the exact three body paragraphs, matching the live structure.
- **Removed invented content** from the earlier rebuild: the paraphrased `sections[]` sub-headings
and the "Typical outputs" / `outcomes[]` lists were **not present on the live site** and have been
dropped to honor the no-invention rule. They may return later as owner-approved "Typical outputs"
modules (parity plan Phase 5).
- **Focus-area detail pages** use the exact single paragraph from the homepage "Our Core Focus
Areas" section — the only live copy that exists for these (the live `/focus-areas/` page is empty).
No additional copy was invented.
## Metadata (not visible body copy)
- **SEO meta descriptions** (`description` frontmatter, 120158 chars) are newly written marketing
metadata, not live body copy. Paraphrase is acceptable for meta descriptions; they are enforced
by the Zod schema (ADR-0003).
- `summary` frontmatter reuses the **exact** live `/services/` overview paragraph (services) and the
**exact** homepage focus paragraph (focus areas); it drives the overview and homepage cards.
## Typography
- Curly apostrophes/quotes from WordPress were normalized to **straight** apostrophes in the Markdown
bodies (e.g., "client's" not "client's"). Meaning and wording are unchanged.
## Homepage — exact copy restored (2026-07-15)
- `src/pages/index.astro` now carries **exact live copy**: hero paragraph, the full "How We Work"
section (intro + Increasing the Supply of Capital + Bridging Supply and Demand + Cultivating
Capital-Ready Initiatives incl. the "step in as sponsor" paragraph), the About teaser, the four
metrics (27 Years / $10+ billion / $3.5+ billion / 275+ Projects and Initiatives, with exact
supporting text), and the Core Focus Areas intro.
- **Additive (not live copy):** the "Our Services" section keeps a short connective subhead +
intro sentence (the live homepage lists only service titles). No fabricated claims.
- **Default OG / hero fallback images** repointed to real photos (`img_0067`); the contact hero uses
`img_0058`. The four generic placeholder images were **deleted** from `public/assets/`.
- **Omitted defect:** the live "Follow Our Quest" lorem-ipsum block remains removed.
- H1 wording is exact; live renders RESTORATION/PRESERVATION/RESPONSIBLE USE in all-caps, the
rebuild uses sentence case (typographic only).
## About Us — exact copy restored + migrated to Markdown (2026-07-15)
- Migrated to `src/content/pages/about.md` (ADR-0002) with **verbatim** copy for all ten sections
and every paragraph. The sticky in-page nav is derived from the h2 headings (restores the live
anchor nav). The live "andrestore" typo is corrected to "and restore".
- Added hero heading ("Finance, innovation, and stewardship…") — the live About page has no hero
heading; this is an additive structural heading, not body copy.
## Imagery — real live-site assets imported (2026-07-15)
- Real assets downloaded to `public/assets/live-site/` (+ `/icons`). The real logo replaced the
placeholder `public/assets/bluecap-logo.png`. Homepage hero (`img_0067`), homepage About band
(`img_0053`), About hero (`img_0062`), Services hero (`img_0075`), Focus/detail heroes
(`img_0070`) now use real photography. The 8 service cards render their real icons (decorative,
`aria-hidden`).
- `IMG_0075.webp` was JPEG-mislabeled on the live site; saved as `img_0075.jpg`.
## About Us imagery — interleaved (2026-07-15)
- Eight real live-site photos are now interleaved through the About sections (`img1`, `img_0055`,
`img_0054`, `img_0072`, `img_0053`, `img_0058`, `img_0075`, `img_0070`), plus `img_0062` as the
hero — matching the live page's ~9-photo visual rhythm. Alt text is descriptive (live alts were
empty). Order approximates the live placement; exact per-section pairing is best-effort.
## Known placeholders / not-yet-restored (tracked in parity-checklist.md)
- In-body About images are referenced by URL from Markdown, so they ship without intrinsic
width/height (minor CLS); revisit if a Lighthouse CLS budget requires it.
- Per-page **1200×630 OG images** (SEO plan §2) still to be generated; pages currently share the
`img_0067` default.
- Contact/defect items unchanged (see `bcs-website-owner-decisions`).

83
docs/deployment.md Normal file
View File

@ -0,0 +1,83 @@
# Deployment
## Local Commands
```bash
npm install
npm run dev
npm run typecheck
npm run build
npm run preview
```
## Configuration
Set these at build time:
```bash
PUBLIC_SITE_URL=https://www.bluecapstrategies.com
PUBLIC_CONTACT_FORM_ENDPOINT=https://your-form-endpoint.example
```
`PUBLIC_CONTACT_FORM_ENDPOINT` can point to Formspree, a webhook, ntfy, or a self-hosted form
receiver. Without it, the contact form validates fields and opens an email draft to
`info@bluecapstrategies.com`.
## Container
```bash
docker build -t bluecap-strategies-website:latest .
docker run --rm -p 8080:80 bluecap-strategies-website:latest
```
## k3s
Production deployment should use the pull-based GitOps model documented in
`docs/gitops-deployment-strategy.md`.
The intended flow is:
```text
local edit -> git push -> GitHub Actions test/build/publish -> Git manifest image tag update -> ArgoCD reconciles k3s
```
ArgoCD is already running in the homelab k3s cluster, so it is the reconciler (not Flux). GitHub
Actions should not receive kubeconfig or direct access to the homelab cluster.
Manual `kubectl` use should be limited to bootstrapping the ArgoCD Application, read-only
inspection, and emergency operations. If you intentionally choose to apply the current manifests
by hand during a controlled bootstrap test, the command would be:
```bash
kubectl apply -k k8s/
```
Do not treat that as the normal production deployment process.
The manifests assume Traefik ingress is available. Read-only research confirmed Traefik and
cert-manager are installed, with a ready `letsencrypt-prod` ClusterIssuer. However, TLS and ingress
hardening should remain documented rather than applied until the Pangolin/Newt edge path is
confirmed.
Items to confirm before production ingress changes:
- Whether Pangolin/Newt terminates TLS, passes TLS through, or proxies HTTPS to Traefik.
- Whether Pangolin/Newt preserves `Host` headers for the BlueCap hostnames.
- Whether cert-manager's Cloudflare DNS-01 token can issue certificates for `bluecapstrategies.com`.
- Whether the canonical production host should be apex or `www`.
## DNS Cutover
Before cutover:
- Build and push the final container image.
- Confirm the GitHub Actions workflow publishes the GHCR image and updates the desired image tag.
- Confirm ArgoCD reconciles the `bluecap-strategies` workload from Git.
- Confirm the ingress or edge route answers for `www.bluecapstrategies.com` and `bluecapstrategies.com`.
- Lower DNS TTL on the existing records.
- Point BlueCap DNS to the existing Pangolin/Newt public edge, or to the chosen production edge if
that architecture changes.
- Configure apex-to-canonical redirect at the selected layer: Cloudflare, Pangolin/Newt, Traefik,
or nginx.
- Verify `/`, `/services/`, `/about-us/`, `/contact/`, `/sitemap-index.xml`, and `/robots.txt`
after DNS propagation.

View File

@ -0,0 +1,193 @@
# BlueCap Strategies GitOps Deployment Strategy (ArgoCD)
## Goal
Deploy the BlueCap Strategies Astro site to the existing k3s cluster using GitOps, without
giving GitHub Actions direct access to the homelab cluster.
The homelab already runs **ArgoCD** in k3s, so this strategy uses ArgoCD as the reconciler
rather than standing up a second GitOps controller (Flux). ArgoCD pulls from Git and GHCR; the
cluster is never exposed to GitHub.
Desired operating model:
1. Edit the site locally in this repository.
2. Commit changes.
3. Push to `main`.
4. GitHub Actions runs tests and builds a container image.
5. GitHub Actions pushes the image to GHCR, tagged with the immutable `<git-sha>`.
6. Git is updated with the new desired image tag (CI commits the tag into `k8s/`).
7. ArgoCD, running inside k3s, detects the Git change.
8. ArgoCD syncs the Kubernetes manifests (Kustomize).
9. k3s pulls the new image and rolls the website deployment.
10. Traefik serves the updated site publicly.
GitHub never receives kubeconfig or direct network access to the homelab cluster. The cluster
pulls from Git and the image registry.
## Prerequisites (not yet done)
- **This repository is not yet a git repo.** GitOps requires a remote (GitHub) that both CI and
ArgoCD can read. `git init`, push to a GitHub repo, then wire ArgoCD to that repo.
- Decide the GHCR image path (replace the placeholder `ghcr.io/your-org/...` in
`k8s/deployment.yaml`).
## Cluster Findings
Read-only `kubectl` inspection previously showed:
- k3s API reachable at `10.66.15.30:6443`.
- Ingress controller: `traefik`. Ingress class: `traefik`.
- cert-manager installed with a `letsencrypt-prod` ClusterIssuer.
- Existing namespaces for media, nextcloud, monitoring, etc.
- No `bluecap-strategies` namespace yet.
- **ArgoCD is already installed and managing other workloads in the cluster.**
Edge / DNS / TLS (research 2026-06-05):
- Edge routing uses a Pangolin/Newt path (`dav.rodgersweb.com` via Caddy).
- `bluecapstrategies.com` DNS is managed by Cloudflare.
- TLS handled by cert-manager via a Cloudflare DNS-01 token.
## Repository & Image Strategy
- **Mono-repo:** Keep site source and k8s manifests in the same repository.
- **GHCR:** Use GitHub Container Registry for images.
- **Immutable tags:** Deploy via `<git-sha>` tags, never `latest`. `latest` defeats ArgoCD's
ability to detect and record what is actually running.
- **Manifests:** Plain Kustomize under `k8s/` (already present). ArgoCD consumes
`k8s/kustomization.yaml` natively — no Helm chart required.
## Phase 1: ArgoCD Application (Foundational GitOps)
Register the site as an ArgoCD `Application`. Two common patterns; pick one to match how the
homelab already organizes ArgoCD:
- **Direct Application** — a single `Application` CR pointing at this repo's `k8s/` path.
- **App-of-apps** — add this `Application` to the existing root/app-of-apps repo if the homelab
uses that pattern.
Example direct Application (store in the homelab ArgoCD config repo, or apply once to bootstrap):
```yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: bluecap-website
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/<owner>/bcs-website.git
targetRevision: main
path: k8s
destination:
server: https://kubernetes.default.svc
namespace: bluecap-strategies
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
```
`prune: true` removes resources deleted from Git; `selfHeal: true` reverts manual drift;
`CreateNamespace=true` lets ArgoCD create `bluecap-strategies` (or keep `namespace.yaml` in
`k8s/` — both work, don't do both destructively).
## Image Update Strategy
Two options — this strategy recommends **Option A** for simplicity and auditability:
### Option A (recommended): CI commits the image tag
GitHub Actions, after pushing the image, patches the image tag in `k8s/` (via
`kustomize edit set image` or a `newTag` field) and commits back to `main`. ArgoCD auto-syncs the
new tag. Every deploy is a Git commit traceable to a source SHA — clean rollback via `git revert`.
Add to `k8s/kustomization.yaml`:
```yaml
images:
- name: ghcr.io/<owner>/bluecap-strategies-website
newTag: <ci-sets-this>
```
### Option B: ArgoCD Image Updater
Install ArgoCD Image Updater and annotate the Application to watch the GHCR repo and write back
the tag. Removes the CI commit-back step but adds a controller and registry-credential wiring.
Only adopt if the homelab already runs Image Updater for other apps.
## Phase 2: CI Pipeline (GitHub Actions)
Workflow on push to `main`:
1. **Quality gate:** `npm ci`, `npm run typecheck`, `npm run build`.
2. **E2E validation:** `npm run test:e2e` (Playwright) where browser deps are available.
3. **Publish:** build the Docker image, push to GHCR tagged `<git-sha>`.
4. **GitOps sync (Option A):** `kustomize edit set image ...=ghcr.io/<owner>/...:<git-sha>`,
commit and push to `main`. ArgoCD reconciles.
CI never touches the cluster — it only writes to GHCR and Git.
## Phase 3: Staging (Launch Readiness)
Avoid testing in production. Add a `k8s/overlays/staging/` Kustomize overlay and a second ArgoCD
Application tracking it.
- **Target:** `staging.bluecapstrategies.com`
- **Goal:** let the owner review content/insights changes before they hit the live site.
## Phase 4: Elite Authority Infrastructure (9/10 Standard)
### 1. Secret hygiene — Git-managed credentials
Avoid manual `kubectl create secret`. Use **Sealed Secrets** or **External Secrets Operator** so
GHCR pull tokens and contact-form keys are encrypted in Git and the site is reproducible from
source alone. Match whichever the homelab ArgoCD stack already standardizes on.
### 2. Edge authority — Cloudflare hardening
- **WAF & protection:** enable Cloudflare WAF to block automated probes and DDoS.
- **Edge caching:** "Cache Everything" for static Astro assets so most traffic serves from
Cloudflare's edge regardless of homelab bandwidth.
- **Cloudflare Tunnels (optional):** consider `cloudflared` to remove open router ports.
### 3. Uptime intelligence (observability) — reuse the `monylog` stack
The homelab already runs a full LGTM + Alertmanager stack on the `monylog` box (`10.66.15.21`,
Ansible role `pglta`), external to k8s. **Do not deploy Uptime Kuma.** Alloy already remote-writes
BlueCap pod/deployment metrics (kube-state-metrics) and ships nginx pod logs to Loki. To finish:
- **Add `blackbox_exporter`** to the monylog compose + a Prometheus scrape job probing the
in-cluster Service and the public URL (uptime, HTTP status, TLS-expiry, latency).
- **Add a `web-bluecap.yml` alert rules file** (SiteDown, CertExpiringSoon <14d, High5xx,
DeploymentDegraded, PodRestarting) mirroring the existing host-infra rules.
- **Alerting** already flows to **Pushover** (OpenBao creds) with ntfy fallback → phone; add a
`service: bluecap` label. **Grafana** already present for dashboards. ArgoCD sync/health is an
extra deploy-level signal. See `docs/seo-aeo-optimization.md` §6a for detail.
### 4. Digital ESG — verified sustainability
- Verify Green Web status if the homelab uses renewable energy.
- Use Astro's `Image` component to minimize data transfer.
- Document the low-energy architecture in `llms.txt` or a footer "Digital ESG" note.
### 5. Security performance (A+ rating)
- Harden `deploy/nginx.conf` toward an A+ on `securityheaders.com`: robust CSP, HSTS,
X-Frame-Options.
- TLS 1.3 only; cert-manager keeps certs well ahead of expiry.
## Verification & Rollback
### Rollback
GitOps makes rollbacks surgical:
1. `git revert <commit-sha>`
2. `git push origin main`
3. ArgoCD reconciles the previous known-good state automatically. (Or use ArgoCD's
History/Rollback UI to pin an earlier synced revision.)
### Automated checks
- `npm run test:e2e` (pre-deployment, in CI)
- K8s readiness/liveness probes on `/healthz` (during rollout)
- ArgoCD health status + Uptime Kuma (post-deployment)
## Open Items Before Production Ingress
- Confirm whether Pangolin/Newt terminates, passes through, or proxies TLS to Traefik, and
whether it preserves `Host` headers for the BlueCap hostnames.
- Confirm cert-manager's Cloudflare DNS-01 token can issue for `bluecapstrategies.com`.
- Decide canonical host (apex vs `www`) and where the apex→canonical redirect lives (Cloudflare,
Pangolin/Newt, Traefik, or nginx).

View File

@ -0,0 +1,237 @@
# SEO, AEO & AI-Crawler Optimization Plan
Requirements and rules for optimizing the BlueCap Strategies rebuild for classic search (SEO),
answer engines / AI citation (AEO/GEO), and AI-crawler acceptance. Written **2026-07-15** against
mid-2026 best practices (see Sources). Where a rule can be machine-enforced, it is defined as a
**Zod schema constraint** or a **build-time check** so the build fails when copy violates it —
this is the TypeScript/Astro equivalent of Pydantic validation (do **not** add Python/Pydantic to
a JS build; Astro Content Collections already run on Zod).
## 0. Guiding principles (2026 reality check)
1. **HTML is the source of truth, not `llms.txt`.** AI search crawlers (GPTBot, ClaudeBot,
PerplexityBot, OAI-SearchBot, Google-Extended) overwhelmingly **skip `/llms.txt` and crawl
rendered HTML directly**; Google confirmed it does not support llms.txt. Keep our `llms.txt`
(low cost, ~10% of sites have one) but invest first in semantic HTML, structured data, and
crawlable copy. Our Astro output is **fully static** — 100% of meaningful copy is in initial
HTML, which is the single biggest AEO advantage over the old JS-heavy WordPress/Elementor site.
2. **Structured data drives citation.** Schema lets answer engines verify *who we are*, *what a
page covers*, and *which facts are reliable to cite*. FAQ schema has among the highest AI
citation rates (question/answer mirrors how engines present answers).
3. **Answer-ready structure wins.** Clear headings, direct definitional first sentences, and
self-contained paragraphs get extracted; buried or paraphrased marketing prose does not.
4. **E-E-A-T via the principal.** Boutique-advisory credibility is entity-driven — a real
`Person` entity (Brad Rodgers) with `knowsAbout`, linked to the `Organization`, is a core GEO
asset. (Gated on owner-approved bio — see parity plan Phase 5.)
## 1. Programmatic enforcement (Zod + build linter)
### 1a. Content-collection SEO schema (fails the build)
Extend `src/content.config.ts`. Every collection (pages, services, focusAreas, insights) shares
an `seo` shape with enforced limits:
```ts
import { z } from "astro:content";
export const seo = z.object({
// <title> renders as "`title` | BlueCap Strategies" — keep the full SERP title ≤ ~60 chars
title: z.string().min(10).max(55),
// meta description + OG description; 120158 is the safe SERP/AI-snippet window
description: z.string().min(120).max(158),
// canonical is derived from slug, but allow override for cross-posted insights
canonicalUrl: z.string().url().optional(),
// every page must ship a social share image (absolute path under /assets)
ogImage: z.string().startsWith("/assets/").default("/assets/og/default.jpg"),
// primary entity/topic — powers internal linking + schema keywords
primaryTopic: z.string(),
noindex: z.boolean().default(false),
});
```
Rules encoded here (build fails on violation): title length, description length window, OG image
present, canonical is a valid URL. This is the "rules are being followed" guarantee you asked
about — enforced on every `npm run build` in CI before ArgoCD ever sees the image.
### 1b. Build-time SEO linter (what Zod can't see)
Zod validates frontmatter, not rendered HTML. Add a small check (a Node script run in CI, and/or
Playwright assertions in `tests/`) that fails the build if any built page in `dist/` violates:
- Exactly **one `<h1>`** per page.
- Heading hierarchy has no skipped levels (no `<h4>` before an `<h2>`).
- `<title>`, `<meta name="description">`, `<link rel="canonical">` all present and non-empty.
- Every `<img>` has non-empty `alt` (parity plan already requires this).
- At least one JSON-LD block present, and **every** JSON-LD block `JSON.parse`s.
- Internal links resolve (no 404s to our own routes).
- Canonical host matches the chosen production host (apex vs `www` — see §5).
Recommended: a `scripts/seo-lint.mjs` that walks `dist/**/*.html` with a lightweight parser, plus
keep the existing Playwright suite for link/CTA coverage.
## 2. Per-page technical SEO requirements
Already implemented in `src/layouts/BaseLayout.astro` (keep + enforce):
- `<title>`, `<meta name="description">`, `<link rel="canonical">`
- Open Graph (`og:type/title/description/url/image`) + Twitter `summary_large_image`
- JSON-LD injection slot (Organization + WebSite globally, page schema per route)
- `lang="en"`, viewport, skip-link, RSS `<link rel="alternate">`
To add/verify:
- [ ] **Per-page OG images** — currently every page falls back to `/assets/water-strategy.jpg`.
Generate topic-specific OG images (services, focus areas, insights) at 1200×630.
- [ ] **`robots` meta** wired to the `noindex` frontmatter flag (for `/thank-you/`, staging).
- [ ] **`article:published_time` / `modified_time`** OG tags on insights posts.
- [ ] **Breadcrumbs** (visible + `BreadcrumbList` schema) on services/focus/insights detail pages.
- [ ] **Sitemap** already via `@astrojs/sitemap` (excludes `/thank-you/`); add `lastmod` from
content dates for insights.
- [ ] **Trailing-slash consistency** — config uses `trailingSlash: "always"`; ensure canonical +
internal links all match to avoid duplicate-URL dilution.
## 3. Structured data (JSON-LD) matrix
| Page / type | Required schema | Notes |
|-------------|-----------------|-------|
| Global | `Organization`, `WebSite` | Already in `src/utils/schema.ts`. Add `sameAs` (real socials), `logo`, `contactPoint` (phone/email), `areaServed`. |
| Homepage | `Organization` + `WebSite` (+ optional `SearchAction`) | — |
| `/services/` + each service | `Service` (with `provider` → Organization, `serviceType`, `areaServed`) | One per detail page. |
| `/focus-areas/` + each focus | `Service` or `DefinedTerm`/`Article` | Focus areas double as topic-authority hubs. |
| About / Brad Rodgers | `Person` + `AboutPage`; `Person.worksFor` → Organization; `knowsAbout[]` | **Owner-approved facts only.** Core GEO/E-E-A-T asset. |
| Insights posts | `Article`/`BlogPosting` (author=Person, publisher=Organization, dates, `headline`, `image`) | Powers AI citation of thought leadership. |
| Insights index | `Blog` / `CollectionPage` | — |
| Contact | `ContactPage` + `Organization.contactPoint` | Use real values, never the WP placeholders. |
| FAQ sections | `FAQPage`**only where the same Q&A is visibly rendered** | Highest AI-citation rate. Never add invisible FAQ schema. |
| Breadcrumbs | `BreadcrumbList` | Detail pages. |
Rule: **structured data must reflect content that is visibly present on the page.** No hidden-text
schema, no unsupported claims, no fabricated review/rating markup.
## 4. AEO / GEO content rules
Applied during Phase 2 copy restoration and Phase 5+ enhancements (must not violate the
no-paraphrasing parity rule on restored copy):
- **Answer-first**: each section's opening sentence should stand alone as a direct answer /
definition (e.g., "Marine conservation finance is …"). Extractable by answer engines.
- **Semantic headings** phrased as the questions/topics people ask ("What makes PFAS remediation a
capital formation problem?").
- **FAQ modules** on high-intent authority pages (PFAS economics, marine conservation finance) —
visible Q&A + `FAQPage` schema. Owner-approved answers only.
- **Entity consistency**: use canonical terminology everywhere — *catalytic capital*, *aquatic
resources*, *marine & freshwater ecosystems*, *PFAS remediation*, *marine conservation finance*,
*environmental markets*, *coastal resilience*. Feeds `primaryTopic` + internal linking.
- **Internal linking graph**: services ↔ focus areas ↔ insights ↔ principal bio. Dense, relevant
internal links are a top GEO signal.
- **Freshness**: insights carry `publishDate`/`modifiedDate`; surface "Updated" dates.
- **Statistics & specifics**: the homepage metrics (27 years, $10B+, $3.5B+, 275+) are exactly the
kind of concrete, citable facts answer engines favor — keep them prominent and, where true,
attributable.
## 5. AI-crawler policy (`robots.txt`) — **owner decision required**
Current `public/robots.txt` = `Allow: /` for everyone. 2026 best practice is **explicit per-agent
rules**, splitting *search/answer* bots (that can cite and send traffic) from *training* bots
(that ingest content into model weights). Decision matrix:
| Bot | Job | Recommendation |
|-----|-----|----------------|
| `OAI-SearchBot`, `ChatGPT-User` | ChatGPT search index + live citation fetch | **Allow** |
| `Claude-SearchBot` | Claude web-search citations | **Allow** |
| `PerplexityBot` | Perplexity answer citations | **Allow** |
| `Google-Extended` | Gemini + Google AI Overviews | **Allow** (maximizes AI visibility) |
| `Googlebot`, `Bingbot` | Classic search | **Allow** |
| `GPTBot` | OpenAI model *training* | Owner choice — allow = max reach, disallow = opt out of training |
| `ClaudeBot` | Anthropic model *training* | Owner choice (same trade-off) |
| `Bytespider`, stealth scrapers | Known to ignore robots.txt | Consider **edge/WAF block** (Cloudflare) — robots.txt won't stop them |
**Recommended default for an authority-seeking advisory firm: allow all search/answer + classic
bots (maximize citation and discoverability); training bots are a values call.** Because BlueCap
wants to be cited as an authority, allowing everything is the simplest high-visibility posture.
Non-compliant scrapers are handled at the Cloudflare edge (see gitops doc §4.2), not robots.txt.
Keep `llms.txt` (already present, low cost) but treat it as supplemental, not load-bearing.
## 6. Measurement & Monitoring — **recommendation, needs sign-off**
Three distinct layers; today none is decided.
### 6a. Uptime / infrastructure — reuse the existing `monylog` stack (do NOT deploy Uptime Kuma)
The homelab already runs a full self-hosted **LGTM + Alertmanager** stack on the `monylog` box
(`10.66.15.21`, Ansible role `pglta`): Prometheus (15d retention, remote-write receiver), Grafana,
Loki, Tempo, Alertmanager → **Pushover** (creds from OpenBao) with **ntfy** fallback → phone. It
runs **outside k8s on purpose** so it still alerts when the cluster is down. Grafana Alloy runs as
a k3s DaemonSet and remote-writes cluster metrics + ships all pod logs to monylog.
**Already flowing for BlueCap (zero work):**
- **Pod/deployment health** — Alloy scrapes kube-state-metrics + annotated pods → Prometheus.
`kube_deployment_status_replicas_unavailable`, restarts, OOMKills are queryable now.
- **nginx logs** — Alloy ships all pod logs to Loki. 5xx rate / errors available via LogQL now.
**To add (small):**
1. **blackbox_exporter** — the one gap (not in the monylog compose). Add as a compose service +
a Prometheus scrape job. Probe the in-cluster Service (app health) **and** the public URL (full
edge-path uptime + TLS expiry). monylog is off-cluster → probing the public URL is a genuine
external vantage. Metrics: `probe_success`, `probe_http_status_code`,
`probe_ssl_earliest_cert_expiry`, latency.
2. **`web-bluecap.yml` alert rules** (mirror the existing host-infra rules pattern): `SiteDown`
(`probe_success==0` for 2m), `CertExpiringSoon` (<14d), `High5xx` (Loki/blackbox),
`DeploymentDegraded`, `PodRestarting`.
3. **Alertmanager** already routes to Pushover — just add a `service: bluecap` label; no new
channel. **Grafana** already present → build a BlueCap uptime/latency/5xx/pod-health dashboard.
- **ArgoCD** health/sync status as an additional deploy-level signal.
### 6b. Visitor & behavior analytics — PostHog + Umami side-by-side trial (ADR-0005)
Decided: **run PostHog and Umami side-by-side initially** and keep whichever fits best. This is
**product/visitor analytics**, distinct from the `monylog` ops telemetry in §6a. Trial setup:
- **PostHog — Cloud free tier** (1M events/mo) for the trial, not self-host (self-hosting needs
ClickHouse/Kafka/etc.; PostHog's docs steer small users to Cloud). Reverse-proxy ingest through
our domain to reduce ad-blocker loss. Gives autocapture, funnels, session replay.
- **Umami — self-hosted** in k3s: cookieless, ~2 KB, no consent banner, always-on baseline
(pageviews, sources, devices, custom events). Aligns with the Digital-ESG positioning.
- Both wired through one config-driven `Analytics.astro` in `BaseLayout`, gated by env vars, so
either can be toggled and the winner kept without code churn. Same event set in both:
consultation-CTA clicks, per-service/focus CTA source (ties to the segmented-CTA strategy),
insights reads, contact submits.
- **Consent/privacy must cover the superset** — PostHog (esp. session replay) likely triggers
cookie-consent + privacy-policy obligations; add these **before** enabling PostHog in production.
Umami alone needs no banner.
- **Avoid GA4** unless Google Ads attribution is later required.
- Evaluate on insight value vs. weight/consent/ESG fit; record the winner in a follow-up ADR.
### 6c. Search & AI-citation visibility
- **Google Search Console** + **Bing Webmaster Tools** (free, essential) — submit sitemap, monitor
impressions/queries/coverage, Core Web Vitals.
- **AI-citation tracking** (emerging, optional): periodically test brand/topic prompts across
ChatGPT/Perplexity/Google AI Overviews and log whether BlueCap is cited. Lightweight manual
tracker to start; dedicated GEO tools are immature — don't over-invest yet.
- **Performance budget**: Lighthouse in CI; target 95+ SEO/Perf/Best-Practices/Accessibility.
Static Astro should hit this easily; guard against regressions from images (use Astro `Image`).
## 7. Pre-launch SEO checklist
- [ ] Zod SEO schema in place; build fails on title/description/OG/canonical violations.
- [ ] `scripts/seo-lint.mjs` (single H1, heading order, canonical, alt text, JSON-LD parses) green in CI.
- [ ] Every page: unique title, description in window, canonical, OG image, valid JSON-LD.
- [ ] Organization/WebSite/Service/Person/Article/Breadcrumb schema per §3, all visible-content-backed.
- [ ] `robots.txt` per §5 (owner-decided); `sitemap-index.xml` correct; `/thank-you/` + staging `noindex`.
- [ ] Canonical host decided (apex vs `www`) + redirect configured at the edge.
- [ ] Search Console + Bing verified, sitemap submitted.
- [ ] Analytics deployed (Umami) + consultation/CTA events firing.
- [ ] Uptime Kuma + external check live and alerting.
- [ ] Lighthouse ≥ 95 across the board; no CLS from unsized images.
## Sources (verified 2026-07-15)
- [The State of llms.txt in 2026 — aeo.press](https://www.aeo.press/ai/the-state-of-llms-txt-in-2026)
- [llms.txt Explained (May 2026) — Codersera](https://codersera.com/blog/llms-txt-complete-guide-2026/)
- [Structured Data for AEO and GEO: Schema Markup Guide 2026 — Kurieta](https://kurieta.com/schema-for-aeo-geo/)
- [Answer Engine Optimization: Complete AEO Guide [2026] — Frase](https://www.frase.io/blog/what-is-answer-engine-optimization-the-complete-guide-to-getting-cited-by-ai)
- [Are FAQ Schemas Important for AI Search, GEO & AEO? — Frase](https://www.frase.io/blog/faq-schema-ai-search-geo-aeo)
- [The AI User-Agent Landscape in 2026: A Complete Reference — No Hacks](https://nohacks.co/blog/ai-user-agents-landscape-2026)
- [AI Crawlers Explained: GPTBot, ClaudeBot, PerplexityBot (2026) — Anagram](https://www.anagram.ai/blog/ai-crawlers-explained-gptbot-claudebot-perplexitybot-and-how-to-let-them-in-2026)
- [Robots.txt for AI Crawlers in 2026 — Cubitrek](https://cubitrek.com/blog/robots-txt-2026-managing-ai-crawler-budgets)

9
k8s/configmap.yaml Normal file
View File

@ -0,0 +1,9 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: bluecap-website-config
namespace: bluecap-strategies
data:
PUBLIC_SITE_URL: "https://www.bluecapstrategies.com"
# Set to a Formspree, webhook, ntfy, or self-hosted form endpoint before production cutover.
PUBLIC_CONTACT_FORM_ENDPOINT: ""

41
k8s/deployment.yaml Normal file
View File

@ -0,0 +1,41 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: bluecap-website
namespace: bluecap-strategies
spec:
replicas: 2
selector:
matchLabels:
app: bluecap-website
template:
metadata:
labels:
app: bluecap-website
spec:
containers:
- name: website
image: ghcr.io/your-org/bluecap-strategies-website:latest
imagePullPolicy: IfNotPresent
ports:
- name: http
containerPort: 80
readinessProbe:
httpGet:
path: /healthz
port: http
initialDelaySeconds: 5
periodSeconds: 10
livenessProbe:
httpGet:
path: /healthz
port: http
initialDelaySeconds: 15
periodSeconds: 20
resources:
requests:
cpu: 25m
memory: 64Mi
limits:
cpu: 250m
memory: 128Mi

37
k8s/ingress.yaml Normal file
View File

@ -0,0 +1,37 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: bluecap-website
namespace: bluecap-strategies
annotations:
traefik.ingress.kubernetes.io/router.entrypoints: websecure
# If cert-manager is installed, uncomment and set your ClusterIssuer.
# cert-manager.io/cluster-issuer: letsencrypt-production
spec:
# If cert-manager is installed, uncomment tls and set the secret name.
# tls:
# - hosts:
# - www.bluecapstrategies.com
# - bluecapstrategies.com
# secretName: bluecap-website-tls
rules:
- host: www.bluecapstrategies.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: bluecap-website
port:
name: http
- host: bluecapstrategies.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: bluecap-website
port:
name: http

8
k8s/kustomization.yaml Normal file
View File

@ -0,0 +1,8 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- namespace.yaml
- configmap.yaml
- deployment.yaml
- service.yaml
- ingress.yaml

4
k8s/namespace.yaml Normal file
View File

@ -0,0 +1,4 @@
apiVersion: v1
kind: Namespace
metadata:
name: bluecap-strategies

12
k8s/service.yaml Normal file
View File

@ -0,0 +1,12 @@
apiVersion: v1
kind: Service
metadata:
name: bluecap-website
namespace: bluecap-strategies
spec:
selector:
app: bluecap-website
ports:
- name: http
port: 80
targetPort: http

6702
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

27
package.json Normal file
View File

@ -0,0 +1,27 @@
{
"name": "bluecap-strategies-website",
"version": "1.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "ASTRO_TELEMETRY_DISABLED=1 astro dev --host 0.0.0.0",
"build": "ASTRO_TELEMETRY_DISABLED=1 astro check && ASTRO_TELEMETRY_DISABLED=1 astro build",
"preview": "ASTRO_TELEMETRY_DISABLED=1 astro preview --host 0.0.0.0",
"typecheck": "ASTRO_TELEMETRY_DISABLED=1 astro check",
"lint": "ASTRO_TELEMETRY_DISABLED=1 astro check",
"seo:lint": "node scripts/seo-lint.mjs",
"test:e2e": "playwright test",
"format": "prettier --write ."
},
"dependencies": {
"@astrojs/rss": "^4.0.12",
"@astrojs/sitemap": "^3.6.0",
"astro": "^5.9.2"
},
"devDependencies": {
"@astrojs/check": "^0.9.4",
"@playwright/test": "^1.60.0",
"prettier": "^3.5.3",
"typescript": "^5.8.3"
}
}

34
playwright.config.ts Normal file
View File

@ -0,0 +1,34 @@
import { defineConfig, devices } from "@playwright/test";
export default defineConfig({
testDir: "./tests",
timeout: 30_000,
expect: {
timeout: 5_000,
},
use: {
baseURL: "http://localhost:4321",
trace: "retain-on-failure",
},
projects: [
{
name: "chromium-desktop",
use: {
browserName: "chromium",
viewport: { width: 1440, height: 900 },
},
},
{
name: "chromium-mobile",
use: {
browserName: "chromium",
...devices["Pixel 5"],
},
},
],
webServer: {
command: "npm run preview",
url: "http://localhost:4321",
reuseExistingServer: true,
},
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 206 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 209 KiB

1
public/healthz Normal file
View File

@ -0,0 +1 @@
ok

16
public/llms.txt Normal file
View File

@ -0,0 +1,16 @@
# BlueCap Strategies
BlueCap Strategies is a water-sector advisory firm helping capital providers, project sponsors,
communities, and policymakers mobilize capital for aquatic resource restoration, preservation,
and responsible use.
Public sections:
- /: Homepage and firm overview.
- /services/: Services overview.
- /focus-areas/: Core focus areas.
- /about-us/: Firm vision, mission, values, and approach.
- /contact/: Contact form, phone, and email.
- /insights/: Prepared future publishing section.
This file is supplemental. The complete public content is available in crawlable HTML pages with
metadata and structured data.

51
public/robots.txt Normal file
View File

@ -0,0 +1,51 @@
# BlueCap Strategies robots.txt — see docs/seo-aeo-optimization.md §5
# Policy: allow classic search + AI answer/search crawlers for maximum discoverability and
# answer-engine citation. Training-crawler policy (GPTBot, ClaudeBot, Google-Extended) is an
# owner decision — currently ALLOWED for maximum reach. To opt out of model training, uncomment
# the "Disallow: /" lines in the training-crawler block below.
# --- Classic search engines ---
User-agent: Googlebot
Allow: /
User-agent: Bingbot
Allow: /
# --- AI answer / search crawlers (these can cite us and send referral traffic) ---
User-agent: OAI-SearchBot
Allow: /
User-agent: ChatGPT-User
Allow: /
User-agent: Claude-SearchBot
Allow: /
User-agent: Claude-User
Allow: /
User-agent: PerplexityBot
Allow: /
# Google-Extended controls Gemini + Google AI Overviews (this is also a training signal for
# Google — treated as answer-visibility here; move to the training block to opt out).
User-agent: Google-Extended
Allow: /
# --- AI model-training crawlers (owner decision — allowed by default) ---
User-agent: GPTBot
Allow: /
# Disallow: /
User-agent: ClaudeBot
Allow: /
# Disallow: /
# --- Everything else ---
User-agent: *
Allow: /
# Note: non-compliant scrapers (e.g. Bytespider, stealth crawlers) ignore robots.txt; block those
# at the Cloudflare edge, not here (see docs/gitops-deployment-strategy.md §4.2).
Sitemap: https://www.bluecapstrategies.com/sitemap-index.xml

90
scripts/seo-lint.mjs Normal file
View File

@ -0,0 +1,90 @@
#!/usr/bin/env node
// Build-time SEO linter (ADR-0003, docs/seo-aeo-optimization.md §1b).
// Walks dist/**/*.html and fails (exit 1) on structural SEO violations that the Zod frontmatter
// schema cannot see in rendered HTML. Dependency-free.
import { readdirSync, readFileSync, statSync } from "node:fs";
import { join, relative } from "node:path";
const DIST = "dist";
const failures = [];
function walk(dir) {
const out = [];
for (const entry of readdirSync(dir)) {
const full = join(dir, entry);
if (statSync(full).isDirectory()) out.push(...walk(full));
else if (entry.endsWith(".html")) out.push(full);
}
return out;
}
function fail(file, msg) {
failures.push(`${relative(DIST, file)}: ${msg}`);
}
function checkFile(file) {
const html = readFileSync(file, "utf8");
// Exactly one <h1>
const h1s = html.match(/<h1[\s>]/gi) ?? [];
if (h1s.length !== 1) fail(file, `expected exactly 1 <h1>, found ${h1s.length}`);
// <title> present + non-empty
const title = html.match(/<title>([^<]*)<\/title>/i);
if (!title || !title[1].trim()) fail(file, "missing or empty <title>");
// meta description present + non-empty
const desc = html.match(/<meta[^>]*name=["']description["'][^>]*>/i);
if (!desc) fail(file, "missing <meta name=description>");
else {
const content = desc[0].match(/content=["']([^"']*)["']/i);
if (!content || !content[1].trim()) fail(file, "empty meta description");
}
// canonical present
if (!/<link[^>]*rel=["']canonical["'][^>]*>/i.test(html)) fail(file, "missing <link rel=canonical>");
// every <img> has non-empty alt (decorative images must opt out via aria-hidden/role=presentation)
for (const tag of html.match(/<img\b[^>]*>/gi) ?? []) {
const decorative = /\baria-hidden=["']true["']/i.test(tag) || /\brole=["']presentation["']/i.test(tag);
if (decorative) continue;
const alt = tag.match(/\balt=["']([^"']*)["']/i);
if (!alt) fail(file, `<img> without alt: ${tag.slice(0, 80)}`);
else if (!alt[1].trim()) fail(file, `<img> with empty alt: ${tag.slice(0, 80)}`);
}
// every JSON-LD block parses
for (const m of html.matchAll(/<script[^>]*type=["']application\/ld\+json["'][^>]*>([\s\S]*?)<\/script>/gi)) {
try {
JSON.parse(m[1]);
} catch (e) {
fail(file, `invalid JSON-LD: ${e.message}`);
}
}
// heading order: never skip a level going deeper
const levels = [...html.matchAll(/<h([1-6])[\s>]/gi)].map((m) => Number(m[1]));
for (let i = 1; i < levels.length; i++) {
if (levels[i] > levels[i - 1] + 1) {
fail(file, `heading jumps from h${levels[i - 1]} to h${levels[i]}`);
break;
}
}
}
let files = [];
try {
files = walk(DIST);
} catch {
console.error(`seo-lint: no ${DIST}/ directory — run \`npm run build\` first.`);
process.exit(2);
}
for (const f of files) checkFile(f);
if (failures.length) {
console.error(`\nSEO lint FAILED — ${failures.length} issue(s) across ${files.length} page(s):\n`);
for (const f of failures) console.error(`${f}`);
process.exit(1);
}
console.log(`SEO lint passed — ${files.length} page(s) clean.`);

View File

@ -0,0 +1,28 @@
---
/**
* Side-by-side visitor analytics (ADR-0005): PostHog + Umami, each gated by env vars so this is a
* no-op until configured. Set the PUBLIC_* vars at build time to enable either or both.
* - PostHog: PUBLIC_POSTHOG_KEY, PUBLIC_POSTHOG_HOST (default https://us.i.posthog.com)
* - Umami: PUBLIC_UMAMI_SRC (script URL), PUBLIC_UMAMI_WEBSITE_ID
* Track the same event set in both to compare (see docs/seo-aeo-optimization.md §6b).
*/
const posthogKey = import.meta.env.PUBLIC_POSTHOG_KEY;
const posthogHost = import.meta.env.PUBLIC_POSTHOG_HOST ?? "https://us.i.posthog.com";
const umamiSrc = import.meta.env.PUBLIC_UMAMI_SRC;
const umamiWebsiteId = import.meta.env.PUBLIC_UMAMI_WEBSITE_ID;
---
{
posthogKey && (
<script is:inline define:vars={{ posthogKey, posthogHost }}>
!function(t,e){var o,n,p,r;e.__SV||(window.posthog=e,e._i=[],e.init=function(i,s,a){function g(t,e){var o=e.split(".");2==o.length&&(t=t[o[0]],e=o[1]),t[e]=function(){t.push([e].concat(Array.prototype.slice.call(arguments,0)))}}(p=t.createElement("script")).type="text/javascript",p.async=!0,p.src=s.api_host+"/static/array.js",(r=t.getElementsByTagName("script")[0]).parentNode.insertBefore(p,r);var u=e;for(void 0!==a?u=e[a]=[]:a="posthog",u.people=u.people||[],u.toString=function(t){var e="posthog";return"posthog"!==a&&(e+="."+a),t||(e+=" (stub)"),e},u.people.toString=function(){return u.toString(1)+".people (stub)"},o="capture identify alias people.set people.set_once set_config register register_once unregister opt_out_capturing has_opted_out_capturing opt_in_capturing reset isFeatureEnabled onFeatureFlags getFeatureFlag getFeatureFlagPayload reloadFeatureFlags group updateEarlyAccessFeatureEnrollment getEarlyAccessFeatures getActiveMatchingSurveys getSurveys getNextSurveyStep".split(" "),n=0;n<o.length;n++)g(u,o[n]);e._i.push([i,s,a])},e.__SV=1)}(document,window.posthog||[]);
posthog.init(posthogKey, { api_host: posthogHost, person_profiles: "identified_only" });
</script>
)
}
{
umamiSrc && umamiWebsiteId && (
<script is:inline defer src={umamiSrc} data-website-id={umamiWebsiteId} />
)
}

View File

@ -0,0 +1,28 @@
---
type Item = {
title: string;
description: string;
href: string;
icon?: string;
};
type Props = {
items: Item[];
label?: string;
heading?: string;
};
const { items, label = "Related pages", heading } = Astro.props;
---
{heading && <h2 class="card-grid-heading">{heading}</h2>}
<div class="card-grid" aria-label={label}>
{items.map((item) => (
<article class="link-card">
{item.icon && <img class="card-icon" src={item.icon} alt="" aria-hidden="true" width="48" height="48" loading="lazy" />}
<h3><a href={item.href}>{item.title}</a></h3>
<p>{item.description}</p>
<a class="card-action" href={item.href}>Learn more</a>
</article>
))}
</div>

View File

@ -0,0 +1,18 @@
---
import { site } from "@data/site";
---
<section class="contact-cta" aria-labelledby="contact-cta-title">
<div>
<p class="eyebrow">Start the conversation</p>
<h2 id="contact-cta-title">Bring capital, strategy, and implementation into alignment.</h2>
<p>
BlueCap Strategies works with capital providers, project sponsors, communities, and
policymakers solving complex water-sector challenges.
</p>
</div>
<div class="cta-actions">
<a class="button" href="/contact/">Request a consultation</a>
<a class="text-link" href={site.contactEmailHref}>{site.email}</a>
</div>
</section>

View File

@ -0,0 +1,33 @@
---
import { nav, serviceNav, site } from "@data/site";
---
<footer class="site-footer">
<div class="footer-inner">
<section class="footer-brand" aria-label="BlueCap Strategies summary">
<img src="/assets/bluecap-logo.png" alt="BlueCap Strategies" width="190" height="57" />
<p>
Transforming environmental challenges into investable opportunities through strategy,
capital, and implementation support for aquatic resources.
</p>
</section>
<nav aria-label="Footer navigation">
<h2>Quick Links</h2>
{nav.map((item) => <a href={item.href}>{item.label}</a>)}
</nav>
<nav aria-label="Service navigation">
<h2>Services</h2>
{serviceNav.slice(0, 5).map((item) => <a href={item.href}>{item.label}</a>)}
<a href="/services/">All services</a>
</nav>
<section>
<h2>Contact Info</h2>
<p><strong>Phone</strong><br /><a href={`tel:${site.phone.replaceAll("-", "")}`}>{site.phone}</a></p>
<p><strong>Email</strong><br /><a href={site.contactEmailHref}>{site.email}</a></p>
<a class="footer-cta" href="/contact/">Request a consultation</a>
</section>
</div>
<div class="footer-bottom">
<p>(c) 2026 BlueCap Strategies, LLC. All rights reserved.</p>
</div>
</footer>

View File

@ -0,0 +1,32 @@
---
import { nav, serviceNav } from "@data/site";
---
<header class="site-header">
<div class="header-inner">
<a class="brand" href="/" aria-label="BlueCap Strategies home">
<img src="/assets/bluecap-logo.png" alt="BlueCap Strategies" width="220" height="66" />
</a>
<nav class="desktop-nav" aria-label="Primary navigation">
<a href="/">Home</a>
<div class="nav-menu">
<a href="/services/">Services</a>
<div class="nav-panel" aria-label="Service pages">
{serviceNav.map((item) => <a href={item.href}>{item.label}</a>)}
</div>
</div>
<a href="/focus-areas/">Focus Areas</a>
<a href="/about-us/">About Us</a>
<a href="/insights/">Insights</a>
<a class="nav-cta" href="/contact/">Contact</a>
</nav>
<details class="mobile-nav">
<summary aria-label="Open navigation">Menu</summary>
<nav aria-label="Mobile navigation">
{nav.map((item) => <a href={item.href}>{item.label}</a>)}
<hr />
{serviceNav.map((item) => <a href={item.href}>{item.label}</a>)}
</nav>
</details>
</div>
</header>

View File

@ -0,0 +1,26 @@
---
type Props = {
eyebrow?: string;
title: string;
lead?: string;
image?: string;
imageAlt?: string;
};
const {
eyebrow,
title,
lead,
image = "/assets/live-site/img_0067.jpg",
imageAlt = "Water resources and coastal landscape",
} = Astro.props;
---
<section class="page-hero">
<div class="hero-copy">
{eyebrow && <p class="eyebrow">{eyebrow}</p>}
<h1>{title}</h1>
{lead && <p>{lead}</p>}
</div>
<img src={image} alt={imageAlt} width="1280" height="854" loading="eager" />
</section>

65
src/content.config.ts Normal file
View File

@ -0,0 +1,65 @@
import { defineCollection, z } from "astro:content";
/**
* Shared SEO/AEO frontmatter rules (ADR-0003). Encoded as Zod so violations FAIL the build:
* `astro check` / `astro build` reject any content entry that breaks these constraints.
* See docs/seo-aeo-optimization.md §1.
*/
export const seoSchema = z.object({
// Rendered as "`title` | BlueCap Strategies"; keep the base title tight so the full SERP title
// stays within ~60 chars where practical.
title: z.string().min(3).max(60),
// Serves as <meta name="description"> + OG/Twitter description; 120158 is the safe SERP / AI
// snippet window.
description: z.string().min(120).max(158),
// Canonical is derived from the route by default; override only for cross-posted content.
canonicalUrl: z.string().url().optional(),
// Absolute path to a social share image under /assets (falls back to the layout default).
ogImage: z.string().startsWith("/assets/").optional(),
// Primary entity/topic — powers internal linking and schema keywords.
primaryTopic: z.string().optional(),
noindex: z.boolean().default(false),
});
/**
* Service + focus-area detail pages (ADR-0002). Prose lives in the Markdown body (exact live-site
* copy see docs/live-site-content-inventory.md); structured/SEO fields live in frontmatter.
* `summary` is the exact overview paragraph reused on the /services/, /focus-areas/, and homepage
* cards; `description` is the SEO meta description.
*/
const detailSchema = seoSchema.extend({
summary: z.string().min(1),
eyebrow: z.string(),
lead: z.string().optional(),
order: z.number(),
icon: z.string().optional(),
image: z.string().optional(),
});
const insights = defineCollection({
type: "content",
schema: seoSchema.extend({
publishDate: z.date(),
modifiedDate: z.date().optional(),
tags: z.array(z.string()).default([]),
draft: z.boolean().default(false),
}),
});
const services = defineCollection({ type: "content", schema: detailSchema });
const focusAreas = defineCollection({ type: "content", schema: detailSchema });
// Standalone long-form pages (About, etc.) — prose in the Markdown body (ADR-0002), hero + SEO
// fields in frontmatter. The sticky in-page nav is derived from the body's h2 headings.
const pages = defineCollection({
type: "content",
schema: seoSchema.extend({
eyebrow: z.string().optional(),
heroTitle: z.string(),
heroLead: z.string().optional(),
heroImage: z.string().optional(),
heroImageAlt: z.string().optional(),
}),
});
export const collections = { insights, services, "focus-areas": focusAreas, pages };

View File

@ -0,0 +1,10 @@
---
title: "Economic Development for Island and Coastal Communities"
description: "BlueCap supports island and coastal community development through eco-tourism, resilient infrastructure, sustainable fisheries, and shoreline protection."
summary: "We are committed to supporting the sustainable development of island and coastal communities through eco-friendly tourism, resilient infrastructure, sustainable fisheries, and shoreline protection. Our holistic approach integrates environmental stewardship with economic growth, ensuring these unique communities can prosper while safeguarding their natural ecosystems for future generations."
eyebrow: "Core Focus Area"
order: 4
primaryTopic: "coastal community development"
---
We are committed to supporting the sustainable development of island and coastal communities through eco-friendly tourism, resilient infrastructure, sustainable fisheries, and shoreline protection. Our holistic approach integrates environmental stewardship with economic growth, ensuring these unique communities can prosper while safeguarding their natural ecosystems for future generations.

View File

@ -0,0 +1,10 @@
---
title: "Structuring Marine Conservation Finance"
description: "BlueCap designs financial mechanisms and environmental markets that channel scalable capital into effective, measurable marine conservation projects."
summary: "Our team excels in designing financial mechanisms that channel capital into marine conservation projects. We are experts at measuring impacts, evaluating funding opportunities, and creating scalable financial structures that drive effective marine conservation efforts forward. We also focus on expanding environmental markets that enable the trading of environmental assets, unlocking solutions that benefit both the environment and capital markets."
eyebrow: "Core Focus Area"
order: 2
primaryTopic: "marine conservation finance"
---
Our team excels in designing financial mechanisms that channel capital into marine conservation projects. We are experts at measuring impacts, evaluating funding opportunities, and creating scalable financial structures that drive effective marine conservation efforts forward. We also focus on expanding environmental markets that enable the trading of environmental assets, unlocking solutions that benefit both the environment and capital markets.

View File

@ -0,0 +1,10 @@
---
title: "Sustainable Living Resource Utilization"
description: "BlueCap guides investment into responsible aquaculture, sustainable fisheries, and IUU-fishing solutions that balance ecology with economic development."
summary: "BlueCap Strategies supports the sustainable use of living marine resources by guiding investment into responsible aquaculture, sustainable fisheries, and combating illegal, unregulated, and unreported (IUU) fishing. We also nurture and expand organizations in this sector, helping them grow while maintaining a balance between ecological preservation and economic development. Our focus ensures these vital industries can thrive sustainably."
eyebrow: "Core Focus Area"
order: 3
primaryTopic: "sustainable living resources"
---
BlueCap Strategies supports the sustainable use of living marine resources by guiding investment into responsible aquaculture, sustainable fisheries, and combating illegal, unregulated, and unreported (IUU) fishing. We also nurture and expand organizations in this sector, helping them grow while maintaining a balance between ecological preservation and economic development. Our focus ensures these vital industries can thrive sustainably.

View File

@ -0,0 +1,10 @@
---
title: "The Economics of PFAS Remediation"
description: "BlueCap navigates the finance of PFAS remediation, evaluating cost-effective technologies and building funding pathways for efficient, equitable cleanup."
summary: "BlueCap Strategies offers deep expertise in navigating the financial complexities of PFAS remediation. We identify cost-effective technologies and help grow emerging solutions into viable remediation options. By developing funding sources and pathways, we enable communities to address this issue efficiently and equitably, ensuring both immediate action and lasting financial sustainability."
eyebrow: "Core Focus Area"
order: 1
primaryTopic: "PFAS remediation finance"
---
BlueCap Strategies offers deep expertise in navigating the financial complexities of PFAS remediation. We identify cost-effective technologies and help grow emerging solutions into viable remediation options. By developing funding sources and pathways, we enable communities to address this issue efficiently and equitably, ensuring both immediate action and lasting financial sustainability.

View File

@ -0,0 +1,14 @@
---
title: "Example draft insight"
description: "A draft placeholder that keeps the insights collection active and demonstrates the SEO frontmatter every published BlueCap Strategies insight must supply."
publishDate: 2026-06-04
modifiedDate: 2026-06-04
tags:
- water infrastructure
primaryTopic: "insights publishing"
draft: true
---
This draft placeholder keeps the insights content collection active. Replace it with imported
Substack, Medium, article, essay, case study, market note, or announcement content when publishing
begins.

111
src/content/pages/about.md Normal file
View File

@ -0,0 +1,111 @@
---
title: "About Us"
description: "BlueCap Strategies works at the intersection of finance, innovation, and stewardship, supporting the sustainable management of global water resources."
eyebrow: "About Us"
heroTitle: "Finance, innovation, and stewardship for global water resources"
heroImage: "/assets/live-site/img_0062.jpg"
heroImageAlt: "Coastal aquatic landscape representing BlueCap Strategies work"
primaryTopic: "about BlueCap Strategies"
---
At BlueCap Strategies, we stand at the intersection of finance, innovation, and environmental stewardship, uniquely positioned to address one of the most pressing challenges of our time: the sustainable management of global water resources. We believe that economic growth and environmental sustainability are not mutually exclusive but are, in fact, deeply intertwined. Our mission is to leverage the power of capital markets to create lasting solutions that preserve and restore marine and freshwater ecosystems while delivering measurable financial returns for our clients.
## Our Vision
### A World Where Water and Prosperity Flow Together
Water is the lifeblood of our economies and ecosystems. Yet, as global populations grow and climate change accelerates, our water resources face unprecedented pressures. At BlueCap Strategies, we see these challenges as opportunities for transformation. Our vision is to create a future where water resources are not only safeguarded but also serve as drivers of economic prosperity.
Our work is guided by the belief that we can build systems where thriving aquatic ecosystems and robust economies coexist. We work to ensure that investments in water not only solve immediate issues but also lay the foundation for long-term environmental and economic resilience. By aligning financial strategies with sustainability goals, we empower our clients to be both profitable and responsible stewards of the planet's most vital resource.
![Coastal shoreline where freshwater meets the sea](/assets/live-site/img1.jpg)
## Why We Exist
### The Global Water Crisis
The global water crisis is not a distant threat — it's a reality that impacts billions of people today. From water scarcity affecting entire regions to pollution threatening the health of our oceans, rivers, and lakes, the need for bold, scalable solutions has never been greater. The complexity of the water crisis, compounded by fragmented funding, technological gaps, and limited policy support, calls for a new approach. That's where BlueCap Strategies comes in.
We exist to solve these challenges by unlocking capital and building the organizations and infrastructure needed to tackle these pressing water issues. Whether it's supporting PFAS remediation, helping communities adapt to changing marine ecosystems, or creating markets for water-based environmental assets, our role is to bring innovative financial solutions to the table. We help bridge the gap between those who have the capital and those who have the knowledge and capacity to implement real-world solutions.
![Open water and marine environment](/assets/live-site/img_0055.jpg)
## What Sets Us Apart
### Blending Financial Expertise with Environmental Impact
At BlueCap Strategies, we do more than just advise — we drive real, on-the-ground change. Our ability to combine deep financial expertise with a passion for environmental impact is what sets us apart. While many consultancies offer strategic guidance, we go further, ensuring that the strategies we develop are actionable, scalable, and lead to measurable, tangible results.
### Integrated Approach: Beyond Consulting
Most firms provide either financial consulting or environmental solutions. We've chosen to integrate these two areas into one cohesive offering. Our hybrid business model allows us to function not only as advisors but also as active participants when the need arises. This means we don't just recommend solutions — we build them, sponsor them, and drive them to completion when market gaps demand it.
For instance, in PFAS remediation — one of the most pressing water contamination issues facing communities today — we don't just identify funding options. We work alongside stakeholders to structure projects that secure necessary capital, implement the most cost-effective technologies, and monitor long-term outcomes to ensure lasting impact.
In marine conservation finance, we help develop mechanisms that channel capital into marine preservation efforts, ensuring that these projects are not just financially viable but capable of driving meaningful environmental change. Whether it's through designing market-based solutions or working to unlock new funding streams, we approach every challenge with the goal of creating both economic and environmental value.
![Aquaculture and working waters](/assets/live-site/img_0054.jpg)
## Collaborative Partnerships
### The Heart of Our Approach
At BlueCap Strategies, we know that multi-stakeholder collaboration is the key to creating solutions that last. The complexities of the water sector demand partnerships across the public and private sectors, and between global institutions and local communities. We serve as the bridge that connects these diverse stakeholders, ensuring that the right expertise, resources, and capital are brought together for maximum impact.
Our work with island and coastal communities is a prime example of this collaborative approach. These communities often face unique environmental challenges, such as rising sea levels, overfishing, and limited access to clean water. By working closely with local governments, non-profits, and private investors, we can help develop sustainable tourism projects, aquaculture initiatives, and coastal resilience plans that both protect ecosystems and drive economic growth. These partnerships allow us to tailor solutions to the specific needs of each community while ensuring that the projects are financially sustainable in the long run.
![Island and coastal community waters](/assets/live-site/img_0072.jpg)
## Policy and Market Development
Another critical piece of the puzzle is our work in policy advocacy and market development. Many of the most pressing water challenges — such as pollution and resource depletion — require innovative market-based solutions and strong regulatory frameworks. We work on both fronts, advocating for policies that support sustainable water use and creating new environmental markets where water-related assets can be traded.
By helping to shape these markets and policies, we reduce the friction that often exists between capital providers and project sponsors, ensuring that investment flows smoothly and projects can be executed without unnecessary delays or obstacles. This, in turn, leads to faster, more effective solutions to environmental challenges.
![Freshwater river system](/assets/live-site/img_0053.jpg)
## Data-Driven Innovation
### Our Edge in Creating Sustainable Solutions
Innovation isn't just about new ideas — it's about ensuring that those ideas are grounded in data and designed to deliver measurable results. At BlueCap Strategies, we pride ourselves on our data-driven approach to problem-solving. From financial modeling to environmental impact assessments, we use the latest tools and technologies to ensure that our projects are both financially viable and environmentally sound.
For example, in our work with PFAS remediation, we can not only evaluate the costs and benefits of various remediation technologies but also measure the long-term effectiveness of these solutions through ongoing impact assessments. This data allows us to continuously refine our approach, ensuring that our clients get the best possible return on their investment while also achieving the greatest environmental benefit.
![Coastal waters at the shoreline](/assets/live-site/img_0058.jpg)
## Sustainability and Profitability
### A Dual Mandate
Too often, businesses and investors are asked to choose between profitability and sustainability. At BlueCap Strategies, we reject this notion. We believe that the two can — and should — go hand in hand. Our projects are designed to deliver both financial returns and measurable environmental outcomes, ensuring that our clients can succeed in both realms.
Take our work in sustainable living resource utilization, for example. In this area, we help clients invest in aquaculture and fisheries that are not only profitable but also environmentally responsible. By developing business models that prioritize sustainability from the outset, we ensure that these industries can continue to thrive without depleting the natural resources they depend on. This approach benefits not only the environment but also the communities and investors who rely on these industries for their livelihoods.
![Sustainable fisheries and living marine resources](/assets/live-site/img_0075.jpg)
## Our Core Values
### The Foundation of our Work
At the heart of everything we do are our core values: integrity, innovation, and impact. These values aren't just words — they are the guiding principles that shape our decisions and actions.
**Integrity:** We hold ourselves to the highest ethical standards in all aspects of our work. Our clients trust us to act in their best interests, and we take that responsibility seriously.
**Innovation:** We are constantly pushing the boundaries of what's possible. Whether developing new financial products or helping clients adopt cutting-edge technologies, we are committed to staying at the forefront of innovation.
**Impact:** Our success is measured by the impact we create. We are driven by the desire to make a real difference in the world, and we take pride in the tangible results we deliver for our clients and the planet.
![Marine and freshwater ecosystem](/assets/live-site/img_0070.jpg)
## Looking to the Future
### Scaling Our Impact
As we look to the future, we are excited about the growing role of capital in solving global water challenges. With climate change, urbanization, and industrialization continuing to strain water resources, the need for scalable, impactful solutions is greater than ever. At BlueCap Strategies, we are committed to leading the charge, helping to scale solutions that not only address immediate issues but also lay the groundwork for long-term resilience.
In the coming years, we aim to deepen our focus on innovative financing models that bring more capital to the water sector, expand our policy advocacy efforts to support sustainable water management, and continue building partnerships that enable communities and ecosystems to thrive.
## Join Us in Building a Sustainable Future
If you share our vision of a future where water and prosperity flow together, we invite you to partner with us. Whether you're an investor, project sponsor, or policymaker, BlueCap Strategies can help you achieve your goals while making a meaningful impact on the planet.

View File

@ -0,0 +1,17 @@
---
title: "Develop and Deploy Catalytic Capital"
description: "BlueCap structures and deploys catalytic capital into high-impact water, PFAS, and marine conservation projects for financial and environmental returns."
summary: "We strategically develop and deploy catalytic capital to drive investment into high-impact, sustainable, water-oriented projects. Our approach maximizes financial returns while accelerating the adoption of innovative solutions that benefit marine and freshwater ecosystems."
eyebrow: "Service"
order: 2
primaryTopic: "catalytic capital"
icon: "/assets/live-site/icons/capital.png"
---
## Driving Sustainable Investments for Maximum Impact
At the heart of our mission is the belief that capital can be a powerful force for good. Our Develop and Deploy Catalytic Capital service is designed to unlock and direct financial resources towards projects that drive meaningful change in the water sector. We work with a range of investors, including private equity firms, impact investors, and philanthropic organizations, to structure and deploy capital in a way that maximizes both financial returns and environmental impact.
This service involves a comprehensive process of identifying high-impact investment opportunities, designing financial structures that align with investor goals, and managing the deployment of capital to ensure optimal outcomes. We focus on projects that have the potential to be transformative, such as innovative PFAS remediation technologies, large-scale marine conservation initiatives, and the development of sustainable aquaculture practices. By mobilizing catalytic capital, we aim to accelerate the adoption of solutions that can have a significant positive impact on marine and freshwater ecosystems.
Our team brings a wealth of experience in financial analysis, capital structuring, and project management, allowing us to navigate the complexities of funding mechanisms and investor expectations. We work closely with clients to understand their financial objectives and risk tolerance, developing tailored investment strategies that align with their mission and values. Through this collaborative approach, we help investors not only achieve their financial goals but also contribute to the long-term sustainability of our planet's water resources.

View File

@ -0,0 +1,17 @@
---
title: "Impact Assessment & Reporting"
description: "BlueCap delivers rigorous, data-driven impact assessment and reporting, defining KPIs and measuring environmental, social, and economic outcomes."
summary: "We offer comprehensive impact assessment and reporting services, providing clients with transparent and data-driven insights into the effectiveness of their investments. Our evaluations help ensure accountability and demonstrate tangible progress toward environmental and financial goals."
eyebrow: "Service"
order: 7
primaryTopic: "impact assessment"
icon: "/assets/live-site/icons/impact-reporting.png"
---
## Demonstrating Value Through Comprehensive Impact Analysis
In the realm of sustainable initiatives, measuring impact is essential for understanding effectiveness and guiding future efforts. Our Impact Assessment and Reporting service provides clients with the tools and insights needed to evaluate the success of their projects and investments. We offer a rigorous, data-driven approach to impact assessment, ensuring that outcomes are accurately measured, transparently reported, and aligned with the client's objectives.
We begin by working with clients to define key performance indicators (KPIs) and metrics that reflect the project's goals, whether they involve environmental conservation, social benefits, or economic returns. Using a combination of quantitative and qualitative methods, we collect and analyze data to assess the project's impact over time. This includes monitoring environmental indicators, evaluating social and economic benefits, and identifying areas for improvement.
Our reporting process is designed to provide clear, actionable insights that can be used for internal decision-making and external communication. We produce comprehensive reports that not only present the data but also interpret the findings in the context of the client's strategic goals. By demonstrating the value and impact of their initiatives, our clients can build trust with stakeholders, attract further investment, and drive continuous improvement in their sustainability efforts.

View File

@ -0,0 +1,17 @@
---
title: "Market Intelligence & Opportunity Analysis"
description: "Market intelligence and opportunity analysis for investors and sponsors navigating regulatory change and emerging opportunities in the water sector."
summary: "We provide in-depth market intelligence to identify emerging trends and untapped opportunities in the water sector. Our analyses enable clients to make informed decisions, positioning them at the forefront of sustainable innovation and investment."
eyebrow: "Service"
order: 1
primaryTopic: "market intelligence"
icon: "/assets/live-site/icons/market-research.png"
---
## Navigating the Blue Economy with Expert Insights
In the rapidly evolving water sector, staying ahead of market trends is crucial for strategic decision-making. Our Market Intelligence and Opportunity Analysis service provides clients with a comprehensive understanding of the landscape, encompassing regulatory changes, technological advancements, and emerging investment opportunities. We leverage a combination of data analytics, industry research, and expert insights to deliver actionable intelligence. This service is designed to empower clients to make informed choices that align with their strategic objectives, whether they are looking to invest, develop new technologies, or enter new markets.
Our approach involves a deep dive into market trends, including the analysis of current and future demands, competitive landscapes, and the economic and regulatory factors influencing the sector. We also assess the potential risks and rewards associated with different market segments, helping clients identify where their investments can have the greatest impact. By combining qualitative and quantitative data, we provide a nuanced view of the market that goes beyond surface-level analysis.
Additionally, we offer opportunity mapping to pinpoint specific areas where clients can leverage their strengths and resources. This includes identifying emerging trends such as PFAS remediation technologies, marine conservation finance mechanisms, and sustainable business models that have the potential to transform the market. With our insights, clients can confidently navigate the complexities of the Blue Economy and position themselves as leaders in sustainable innovation.

View File

@ -0,0 +1,17 @@
---
title: "Policy Advocacy & Development"
description: "BlueCap shapes and advocates for policy that supports sustainable management of marine and freshwater resources, guiding regulation and market development."
summary: "We advocate for, and contribute to, the development of policies that promote sustainable practices in the marine and freshwater sectors. Our expertise helps guide regulatory frameworks that support innovation, conservation, and responsible resource management."
eyebrow: "Service"
order: 8
primaryTopic: "policy advocacy"
icon: "/assets/live-site/icons/policy-advocacy.png"
---
## Shaping the Future of Sustainable Water Management
Effective policy is a cornerstone of sustainable water management, providing the framework for innovation, conservation, and responsible resource use. Our Policy Advocacy and Development service is dedicated to influencing and shaping policies that support the sustainable management of marine and freshwater resources. We work with clients to navigate the policy landscape, advocate for change, and contribute to the development of regulations that promote sustainable practices.
Our team has extensive experience in policy analysis, development, and advocacy, allowing us to engage with policymakers at all levels. We begin by analyzing existing policies and identifying gaps or areas for improvement that can support the client's objectives. We then work with stakeholders to develop policy recommendations, build coalitions, and advocate for their adoption.
We also assist clients in understanding and complying with regulatory requirements, ensuring that their projects and investments are aligned with current and emerging policies. By staying at the forefront of policy developments, we help clients navigate the complexities of the regulatory environment and capitalize on opportunities to drive systemic change. Through our advocacy efforts, we aim to create a policy framework that fosters innovation, supports sustainable development, and ensures the long-term health of our planet's water resources.

View File

@ -0,0 +1,17 @@
---
title: "Project Management & Implementation"
description: "BlueCap manages and implements water-sector projects from planning to completion, delivering on time, on budget, and aligned with sustainability goals."
summary: "From concept to completion, we manage and implement projects to ensure they meet strategic objectives and deliver measurable impact. Our comprehensive approach ensures seamless execution, aligning with timelines, budgets, and sustainability goals."
eyebrow: "Service"
order: 6
primaryTopic: "project management"
icon: "/assets/live-site/icons/project-management.png"
---
## Turning Vision into Reality with Expert Execution
The success of any project lies in its execution. Our Project Management and Implementation service is designed to ensure that initiatives are carried out efficiently, effectively, and in alignment with strategic objectives. We bring a hands-on approach to project management, overseeing every aspect of the process from planning to completion, and ensuring that projects are delivered on time, within scope, and on budget.
We start by developing a comprehensive project plan that outlines the goals, timelines, resources, and milestones. Our team applies best practices in project management, including risk assessment, quality control, and stakeholder communication, to keep projects on track and address any challenges that arise. We also provide on-the-ground support to manage the logistics and coordination of project activities, ensuring smooth implementation and successful outcomes.
Our expertise covers a wide range of projects, including PFAS remediation, marine conservation efforts, and the development of sustainable aquaculture systems. We understand the complexities involved in these initiatives, from regulatory compliance to technical requirements, and we have the experience and skills needed to navigate these challenges. By managing projects with precision and care, we help clients turn their vision into reality, achieving impactful results that make a difference.

View File

@ -0,0 +1,17 @@
---
title: "Stakeholder Engagement & Partnership Development"
description: "BlueCap builds and manages multi-stakeholder partnerships across governments, NGOs, and communities to make complex water projects successful and scalable."
summary: "We facilitate stakeholder engagement and forge collaborative partnerships to enhance project success and scalability. By aligning diverse interests and resources, we help create a unified approach to solving complex environmental challenges."
eyebrow: "Service"
order: 3
primaryTopic: "stakeholder engagement"
icon: "/assets/live-site/icons/stakeholder.png"
---
## Cultivating Collaboration for Sustainable Solutions
Effective stakeholder engagement and partnership development are essential for driving meaningful change in the water sector. Our service is dedicated to building and nurturing strategic alliances between diverse stakeholders, including governments, NGOs, corporations, and local communities. By fostering collaboration and aligning interests, we create the conditions for successful, scalable projects that address complex environmental challenges.
We begin by identifying key stakeholders and understanding their roles, interests, and potential contributions to the project. This involves mapping out the ecosystem of partners and assessing how each can support the initiative's objectives. We then facilitate dialogue and engagement, creating platforms for stakeholders to collaborate, share knowledge, and co-create solutions. Our team excels in managing multi-stakeholder initiatives, ensuring that all voices are heard and that partnerships are built on trust and mutual benefit.
Our approach is grounded in the belief that sustainable solutions require collective action. We work to create partnerships that are not only effective in the short term but also have the potential to drive long-lasting impact. By bringing together the right mix of expertise, resources, and influence, we help clients overcome barriers to implementation and achieve their sustainability goals. Whether it's developing a marine conservation finance mechanism, implementing a PFAS remediation project, or launching a new environmental market, our stakeholder engagement and partnership development services are designed to ensure success through collaboration.

View File

@ -0,0 +1,17 @@
---
title: "Sustainable Business Model Innovation"
description: "BlueCap helps clients design sustainable business models that integrate environmental responsibility with profitability across marine and freshwater markets."
summary: "We help clients develop sustainable business models that integrate environmental responsibility with profitability. By innovating new approaches to value creation, we ensure that our clients can thrive while making a positive impact on marine and freshwater resources."
eyebrow: "Service"
order: 5
primaryTopic: "business model innovation"
icon: "/assets/live-site/icons/business-model.png"
---
## Redefining Profitability with Sustainability
Achieving sustainability in the water sector requires not only technological innovation but also innovative business models. Our Sustainable Business Model Innovation service helps clients develop strategies that integrate environmental responsibility with financial success. We work with businesses to rethink how they create, deliver, and capture value in a way that is both profitable and sustainable.
Our approach involves a deep dive into the client's existing business model, identifying areas where sustainability can be integrated to enhance value creation. This includes exploring new revenue streams, cost efficiencies, and market opportunities that arise from adopting sustainable practices. We also help clients navigate the complexities of implementing these changes, from securing stakeholder buy-in to aligning operations with sustainability goals.
We focus on creating models that are adaptable and resilient, capable of thriving in an ever-changing market landscape. This includes considering factors such as regulatory changes, consumer preferences, and technological advancements. By developing innovative business models that prioritize sustainability, we enable our clients to lead the way in the Blue Economy, setting a standard for how businesses can succeed while making a positive impact on the planet.

View File

@ -0,0 +1,17 @@
---
title: "Sustainable Technology Incubation"
description: "BlueCap identifies, nurtures, and scales sustainable water technologies with technical support, business development, and capital for market readiness."
summary: "Our technology incubation services focus on fostering sustainable innovations that address critical challenges in the water sector. We support the development and scaling of cutting-edge technologies, accelerating their path to market readiness and impact."
eyebrow: "Service"
order: 4
primaryTopic: "technology incubation"
icon: "/assets/live-site/icons/green-technology.png"
---
## Accelerating Innovation for a Sustainable Future
Innovation is key to addressing the pressing challenges facing our marine and freshwater ecosystems. Our Sustainable Technology Incubation service is dedicated to identifying, nurturing, and scaling groundbreaking technologies that have the potential to revolutionize the water sector. We provide a comprehensive incubation process that includes technical support, business development, and access to capital, helping startups and innovators bring their ideas to market.
Our incubation process begins with a thorough assessment of the technology's potential impact, scalability, and market fit. We work with entrepreneurs and researchers to refine their concepts, develop prototypes, and test their solutions in real-world environments. Our team offers guidance on everything from technical development to regulatory compliance, ensuring that the technologies we incubate are not only innovative but also viable and ready for adoption.
In addition to technical support, we provide business development services to help innovators build sustainable business models. This includes market analysis, go-to-market strategy development, and financial planning. We also connect startups with our network of investors, partners, and industry experts, providing the resources and mentorship needed to accelerate growth. By fostering innovation through this holistic approach, we aim to bring forth solutions that can drive significant improvements in water quality, conservation, and resource management.

41
src/data/site.ts Normal file
View File

@ -0,0 +1,41 @@
export const site = {
name: "BlueCap Strategies",
legalName: "BlueCap Strategies, LLC",
description:
"BlueCap Strategies helps investors, project sponsors, communities, and policymakers mobilize capital for aquatic resource restoration, preservation, and responsible use.",
url: "https://www.bluecapstrategies.com",
phone: "804-332-6618",
email: "info@bluecapstrategies.com",
contactEmailHref: "mailto:info@bluecapstrategies.com",
founder: "Brad Rodgers",
addressNote: "Serving water-sector clients across marine and freshwater markets.",
};
export const discoveryNotes = [
"The WordPress contact page contained placeholder contact fields (123-456-8912 and info@gmail.com), while the homepage and footer used 804-332-6618 and info@bluecapstrategies.com.",
"The rebuild uses the homepage/footer contact details as the credible public values.",
"The WordPress sitemap included a placeholder /focus-areas/ page but did not expose individual focus-area pages. This rebuild keeps /focus-areas/ and adds crawlable detail pages for each homepage focus-area summary.",
"The old homepage included placeholder social/follow copy. That filler was removed instead of being reproduced.",
];
export const nav = [
{ label: "Home", href: "/" },
{ label: "Services", href: "/services/" },
{ label: "Focus Areas", href: "/focus-areas/" },
{ label: "About Us", href: "/about-us/" },
{ label: "Insights", href: "/insights/" },
{ label: "Contact", href: "/contact/" },
];
// Services dropdown for the header/footer nav. Kept static (independent of the content collection)
// so nav components stay synchronous; order/titles mirror src/content/services/*.md.
export const serviceNav = [
{ label: "Market Intelligence & Opportunity Analysis", href: "/market-intelligence-opportunity-analysis/" },
{ label: "Develop and Deploy Catalytic Capital", href: "/develop-and-deploy-catalytic-capital/" },
{ label: "Stakeholder Engagement & Partnership Development", href: "/stakeholder-engagement-partnership-development/" },
{ label: "Sustainable Technology Incubation", href: "/sustainable-technology-incubation/" },
{ label: "Sustainable Business Model Innovation", href: "/sustainable-business-model-innovation/" },
{ label: "Project Management & Implementation", href: "/project-management-implementation/" },
{ label: "Impact Assessment & Reporting", href: "/impact-assessment-reporting/" },
{ label: "Policy Advocacy & Development", href: "/policy-advocacy-development/" },
];

10
src/env.d.ts vendored Normal file
View File

@ -0,0 +1,10 @@
/// <reference types="astro/client" />
interface ImportMetaEnv {
readonly PUBLIC_SITE_URL?: string;
readonly PUBLIC_CONTACT_FORM_ENDPOINT?: string;
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}

View File

@ -0,0 +1,62 @@
---
import Header from "@components/Header.astro";
import Footer from "@components/Footer.astro";
import Analytics from "@components/Analytics.astro";
import { site } from "@data/site";
import { assetUrl, canonical } from "@utils/url";
import { organizationSchema, websiteSchema } from "@utils/schema";
import "../styles/global.css";
type Props = {
title: string;
description: string;
pathname: string;
image?: string;
jsonLd?: unknown[];
};
const {
title,
description,
pathname,
image = "/assets/live-site/img_0067.jpg",
jsonLd = [],
} = Astro.props;
const pageTitle = title === site.name ? title : `${title} | ${site.name}`;
const canonicalUrl = canonical(pathname);
const imageUrl = assetUrl(image);
const schemas = [organizationSchema(), websiteSchema(), ...jsonLd];
---
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>{pageTitle}</title>
<meta name="description" content={description} />
<link rel="canonical" href={canonicalUrl} />
<link rel="icon" href="/assets/bluecap-logo.png" />
<link rel="alternate" type="application/rss+xml" title="BlueCap Strategies Insights" href="/rss.xml" />
<meta property="og:type" content="website" />
<meta property="og:title" content={pageTitle} />
<meta property="og:description" content={description} />
<meta property="og:url" content={canonicalUrl} />
<meta property="og:image" content={imageUrl} />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={pageTitle} />
<meta name="twitter:description" content={description} />
<meta name="twitter:image" content={imageUrl} />
{schemas.map((schema) => <script is:inline type="application/ld+json" set:html={JSON.stringify(schema)} />)}
<Analytics />
</head>
<body>
<a class="skip-link" href="#main">Skip to content</a>
<Header />
<main id="main">
<slot />
</main>
<Footer />
</body>
</html>

22
src/pages/404.astro Normal file
View File

@ -0,0 +1,22 @@
---
import BaseLayout from "@layouts/BaseLayout.astro";
const description = "The requested BlueCap Strategies page could not be found.";
---
<BaseLayout title="Page Not Found" description={description} pathname="/404/">
<section class="insights-shell">
<article class="note-card">
<p class="eyebrow">404</p>
<h1>Page not found</h1>
<p>
The page may have moved during the rebuild. Use the navigation to find services, focus
areas, or contact BlueCap Strategies directly.
</p>
<div class="hero-actions">
<a class="button" href="/">Home</a>
<a class="text-link" href="/services/">Services</a>
</div>
</article>
</section>
</BaseLayout>

61
src/pages/[slug].astro Normal file
View File

@ -0,0 +1,61 @@
---
import { getCollection } from "astro:content";
import BaseLayout from "@layouts/BaseLayout.astro";
import PageHero from "@components/PageHero.astro";
import ContactCta from "@components/ContactCta.astro";
import { breadcrumbSchema, pageSchema, serviceSchema } from "@utils/schema";
export async function getStaticPaths() {
const services = await getCollection("services");
const focusAreas = await getCollection("focus-areas");
return [
...services.map((entry) => ({
params: { slug: entry.slug },
props: { entry, kind: "Service", parentTitle: "Services", parentHref: "/services/" },
})),
...focusAreas.map((entry) => ({
params: { slug: entry.slug },
props: { entry, kind: "Core Focus Area", parentTitle: "Focus Areas", parentHref: "/focus-areas/" },
})),
];
}
const { entry, kind, parentTitle, parentHref } = Astro.props;
const { title, description, image } = entry.data;
const pathname = `/${entry.slug}/`;
const isService = kind === "Service";
const { Content } = await entry.render();
const jsonLd = [
pageSchema({ title, description, pathname }),
breadcrumbSchema([
{ name: "Home", href: "/" },
{ name: parentTitle, href: parentHref },
{ name: title, href: pathname },
]),
...(isService ? [serviceSchema({ title, description, pathname })] : []),
];
---
<BaseLayout title={title} description={description} pathname={pathname} jsonLd={jsonLd}>
<PageHero eyebrow={kind} title={title} image={image ?? "/assets/live-site/img_0070.jpg"} />
<section class="section">
<div class="detail-layout">
<div class="article-body">
<p><a class="text-link" href={parentHref}>&larr; {parentTitle} overview</a></p>
<Content />
</div>
<aside class="sidebar" aria-label={`${title} — related`}>
<div class="note-card">
<h2>Related</h2>
<a class="text-link" href={parentHref}>{parentTitle} overview</a>
</div>
<div class="note-card">
<h2>Start a conversation</h2>
<a class="text-link" href="/contact/">Request a consultation</a>
</div>
</aside>
</div>
</section>
<ContactCta />
</BaseLayout>

54
src/pages/about-us.astro Normal file
View File

@ -0,0 +1,54 @@
---
import { getEntry } from "astro:content";
import BaseLayout from "@layouts/BaseLayout.astro";
import PageHero from "@components/PageHero.astro";
import ContactCta from "@components/ContactCta.astro";
import { breadcrumbSchema, pageSchema } from "@utils/schema";
const about = await getEntry("pages", "about");
if (!about) throw new Error("Missing content entry: pages/about");
const { title, description, eyebrow, heroTitle, heroLead, heroImage, heroImageAlt } = about.data;
const { Content, headings } = await about.render();
const sectionNav = headings.filter((h) => h.depth === 2);
const pathname = "/about-us/";
---
<BaseLayout
title={title}
description={description}
pathname={pathname}
image={heroImage}
jsonLd={[
pageSchema({ type: "AboutPage", title, description, pathname }),
breadcrumbSchema([
{ name: "Home", href: "/" },
{ name: "About Us", href: pathname },
]),
]}
>
<PageHero
eyebrow={eyebrow}
title={heroTitle}
lead={heroLead}
image={heroImage ?? "/assets/live-site/img_0062.jpg"}
imageAlt={heroImageAlt}
/>
<section class="section">
<div class="detail-layout">
<div class="article-body">
<Content />
</div>
<aside class="sidebar" aria-label="On this page">
<nav class="note-card anchor-nav" aria-label="Section navigation">
<h2>On this page</h2>
<ul>
{sectionNav.map((h) => (
<li><a href={`#${h.slug}`}>{h.text}</a></li>
))}
</ul>
</nav>
</aside>
</div>
</section>
<ContactCta />
</BaseLayout>

93
src/pages/contact.astro Normal file
View File

@ -0,0 +1,93 @@
---
import BaseLayout from "@layouts/BaseLayout.astro";
import PageHero from "@components/PageHero.astro";
import { site } from "@data/site";
import { breadcrumbSchema, pageSchema } from "@utils/schema";
const description =
"Contact BlueCap Strategies to discuss capital formation, project strategy, partnerships, implementation, or water-sector advisory needs.";
const endpoint = import.meta.env.PUBLIC_CONTACT_FORM_ENDPOINT ?? "";
---
<BaseLayout
title="Contact"
description={description}
pathname="/contact/"
jsonLd={[
pageSchema({ type: "ContactPage", title: "Contact", description, pathname: "/contact/" }),
breadcrumbSchema([
{ name: "Home", href: "/" },
{ name: "Contact", href: "/contact/" },
]),
]}
>
<PageHero
eyebrow="Contact"
title="Get in touch with BlueCap Strategies"
lead="Share the opportunity, project, or water-sector challenge you are working on. BlueCap will respond through the most appropriate contact path."
image="/assets/live-site/img_0058.jpg"
imageAlt="Coastal water and marine resource landscape"
/>
<section class="section contact-layout">
<aside class="contact-panel">
<h2>Contact Info</h2>
<p><strong>Phone</strong><br /><a href={`tel:${site.phone.replaceAll("-", "")}`}>{site.phone}</a></p>
<p><strong>Email</strong><br /><a href={site.contactEmailHref}>{site.email}</a></p>
<p class="form-note">
Discovery note: the current WordPress contact page includes placeholder phone and email
values. This rebuild uses the verified homepage/footer contact details.
</p>
</aside>
<div class="form-panel">
<h2>Request a consultation</h2>
<p class="form-note">
The form is static-first and posts to the configured endpoint when
<code>PUBLIC_CONTACT_FORM_ENDPOINT</code> is set. Without an endpoint, it opens an email
draft after validating required fields.
</p>
<form class="contact-form" method="post" action={endpoint || "/thank-you/"} data-contact-form data-endpoint={endpoint}>
<input type="hidden" name="subject" value="BlueCap Strategies website inquiry" />
<div class="field-grid">
<label>
Name *
<input name="name" autocomplete="name" required minlength="2" />
</label>
<label>
Organization
<input name="organization" autocomplete="organization" />
</label>
</div>
<div class="field-grid">
<label>
Email *
<input name="email" type="email" autocomplete="email" required />
</label>
<label>
Phone
<input name="phone" type="tel" autocomplete="tel" />
</label>
</div>
<label>
Message *
<textarea name="message" required minlength="20"></textarea>
</label>
<button class="button" type="submit">Submit</button>
</form>
</div>
</section>
<script>
const form = document.querySelector("[data-contact-form]");
form?.addEventListener("submit", (event) => {
const endpoint = form.getAttribute("data-endpoint");
if (endpoint) return;
event.preventDefault();
if (!(form instanceof HTMLFormElement) || !form.reportValidity()) return;
const data = new FormData(form);
const subject = encodeURIComponent("BlueCap Strategies website inquiry");
const body = encodeURIComponent(
`Name: ${data.get("name")}\nOrganization: ${data.get("organization") ?? ""}\nEmail: ${data.get("email")}\nPhone: ${data.get("phone") ?? ""}\n\n${data.get("message")}`,
);
window.location.href = `mailto:${"info@bluecapstrategies.com"}?subject=${subject}&body=${body}`;
});
</script>
</BaseLayout>

View File

@ -0,0 +1,47 @@
---
import { getCollection } from "astro:content";
import BaseLayout from "@layouts/BaseLayout.astro";
import PageHero from "@components/PageHero.astro";
import CardGrid from "@components/CardGrid.astro";
import ContactCta from "@components/ContactCta.astro";
import { breadcrumbSchema, pageSchema } from "@utils/schema";
const focusAreas = (await getCollection("focus-areas")).sort((a, b) => a.data.order - b.data.order);
const description =
"BlueCap Strategies focuses on PFAS remediation economics, marine conservation finance, sustainable living resource utilization, and island and coastal community development.";
---
<BaseLayout
title="Focus Areas"
description={description}
pathname="/focus-areas/"
image="/assets/live-site/img_0070.jpg"
jsonLd={[
pageSchema({ title: "Focus Areas", description, pathname: "/focus-areas/" }),
breadcrumbSchema([
{ name: "Home", href: "/" },
{ name: "Focus Areas", href: "/focus-areas/" },
]),
]}
>
<PageHero
eyebrow="Core Focus Areas"
title="Focused expertise for high-stakes aquatic resource challenges"
lead="BlueCap concentrates on areas where finance, policy, innovation, and implementation can unlock measurable progress for aquatic ecosystems and coastal economies."
image="/assets/live-site/img_0070.jpg"
imageAlt="Coastal water resource landscape"
/>
<section class="section">
<CardGrid
heading="Our core focus areas"
items={focusAreas.map((area) => ({
title: area.data.title,
description: area.data.summary,
href: `/${area.slug}/`,
}))}
label="Focus area pages"
/>
</section>
<ContactCta />
</BaseLayout>

182
src/pages/index.astro Normal file
View File

@ -0,0 +1,182 @@
---
import { getCollection } from "astro:content";
import BaseLayout from "@layouts/BaseLayout.astro";
import CardGrid from "@components/CardGrid.astro";
import ContactCta from "@components/ContactCta.astro";
import { pageSchema } from "@utils/schema";
const services = (await getCollection("services")).sort((a, b) => a.data.order - b.data.order);
const focusAreas = (await getCollection("focus-areas")).sort((a, b) => a.data.order - b.data.order);
const description =
"BlueCap Strategies leverages capital, strategy, and data-driven insight to restore, preserve, and responsibly use aquatic resources.";
---
<BaseLayout
title="BlueCap Strategies"
description={description}
pathname="/"
image="/assets/live-site/img_0067.jpg"
jsonLd={[pageSchema({ title: "BlueCap Strategies", description, pathname: "/" })]}
>
<section class="home-hero">
<div class="hero-copy">
<p class="eyebrow">Water-sector strategy and capital formation</p>
<h1>Leveraging capital to drive the restoration, preservation and responsible use of aquatic resources</h1>
<p>
At BlueCap Strategies, we combine financial expertise, innovative strategies, and
data-driven insights to accelerate the restoration and responsible use of aquatic
resources. We empower our clients and partners to shape a sustainable future where
ecosystems and economies thrive together.
</p>
<div class="hero-actions">
<a class="button" href="/contact/">Request a Consultation</a>
<a class="text-link" href="/services/">Explore services</a>
</div>
</div>
<div class="hero-media">
<img
src="/assets/live-site/img_0067.jpg"
alt="Blue water and coastal landscape representing aquatic resource strategy"
width="1280"
height="854"
/>
</div>
</section>
<section class="section alt" aria-labelledby="how-work">
<div class="section-inner">
<div class="section-header">
<p class="eyebrow">How We Work</p>
<h2 id="how-work">Unlocking Capital and Innovation to Solve Global Water Challenges</h2>
<p>
At BlueCap Strategies, we create the conditions for water-related capital and project
demand to intersect seamlessly. Our approach ensures that sustainable water solutions are
capital-ready, capable of delivering measurable impact, and tailored to benefit both
ecosystems and economies.
</p>
</div>
<div class="three-grid">
<article class="work-card">
<h3>Increasing the Supply of Capital</h3>
<p>
We're dedicated to expanding the pool of capital available for water-related projects.
Our team works diligently to educate potential investors on the importance and potential
returns of water-focused initiatives. Through rigorous evaluation and due diligence, we
identify and present high-quality investment and funding opportunities in areas such as
water treatment, conservation, and emerging marine technologies. Our goal is to create a
robust pipeline of well-structured, impactful projects that attract diverse sources of
capital across the spectrum, from traditional investors to impact-focused funds, as well
as grant-making organizations.
</p>
</article>
<article class="work-card">
<h3>Bridging Supply and Demand</h3>
<p>
Our unique value lies in facilitating effective, seamless relationships between capital
providers and project sponsors. Through policy advocacy, market development, stakeholder
engagement and impact assessment, we ensure that both sides of the transaction understand
and meet each other's expectations. We reduce friction, ensuring capital flows
strategically to deliver the greatest environmental and economic impact.
</p>
</article>
<article class="work-card">
<h3>Cultivating Capital-Ready Initiatives</h3>
<p>
BlueCap Strategies is committed to boosting the demand for water-related capital by
preparing projects for investment. We collaborate closely with project sponsors,
providing expertise to transform promising ideas into capital-ready ventures. We help
them structure projects in ways that appeal to investors, focusing on scalability,
sustainability, and measurable impact. By streamlining project design, financial
modeling, and risk management, we ensure that these sponsors can effectively attract the
capital they need to succeed.
</p>
<p>
Our primary goal is to equip project sponsors with the tools and expertise to attract
capital. However, when existing solutions cannot meet the urgency or scale of certain
challenges, we adapt from advisors to active participants. In these situations, BlueCap
Strategies is ready to step in directly as a project sponsor. We take the lead in
assembling teams, sourcing capital, and overseeing implementation to ensure critical
needs are addressed. Whether building partnerships, sourcing capital, or overseeing
execution, we bring the necessary expertise to drive innovation and catalyze change in
underserved areas.
</p>
</article>
</div>
</div>
</section>
<section class="section" aria-labelledby="about-home">
<div class="media-band">
<img src="/assets/live-site/img_0053.jpg" alt="Aquatic ecosystem and coastal water" width="1024" height="679" />
<div class="section-header">
<p class="eyebrow">About Us</p>
<h2 id="about-home">Innovating towards a sustainable future</h2>
<p>
At BlueCap Strategies, we transform environmental challenges into profitable, investable
opportunities. By driving catalytic capital into sustainable projects, we not only ensure
thriving aquatic ecosystems but also deliver substantial financial returns. Led by industry
pioneer Brad Rodgers, our team brings a unique blend of financial acumen, capital
structuring expertise, policy innovation, and strategic partnerships. We collaborate
closely with clients to develop tailored solutions, and when existing options fall short,
we innovate and implement impactful strategies ourselves. Our flexible, hybrid approach
allows us to adapt and solve the most complex challenges with both insight and decisive
action.
</p>
<a class="text-link" href="/about-us/">Learn More</a>
</div>
</div>
<div class="stats-grid" aria-label="BlueCap Strategies experience metrics">
<div class="stat"><strong>27 Years</strong><span>of senior-level experience working with institutional capital providers</span></div>
<div class="stat"><strong>$10+ billion</strong><span>of capital deployed across real-asset, infrastructure and environmental sectors.</span></div>
<div class="stat"><strong>$3.5+ billion</strong><span>of public-private partnerships structured in key ESG and environmental infrastructure sectors.</span></div>
<div class="stat"><strong>275+ Projects and Initiatives</strong><span>advised on across real-asset, environmental, and water resource sectors.</span></div>
</div>
</section>
<section class="section alt" aria-labelledby="services-home">
<div class="section-inner">
<div class="section-header">
<p class="eyebrow">Our Services</p>
<h2 id="services-home">Strategy, capital, partnerships, and implementation</h2>
<p>
BlueCap supports clients across the full lifecycle of water-sector opportunities, from
market intelligence and capital strategy through project delivery and reporting.
</p>
</div>
<CardGrid
items={services.map((service) => ({
title: service.data.title,
description: service.data.summary,
href: `/${service.slug}/`,
icon: service.data.icon,
}))}
label="BlueCap services"
/>
</div>
</section>
<section class="section" aria-labelledby="focus-home">
<div class="section-header">
<p class="eyebrow">Our Core Focus Areas</p>
<h2 id="focus-home">Where strategy, capital, and innovation shape the future of water resources</h2>
<p>
At BlueCap Strategies, we tackle pressing water challenges by combining innovation, capital
mobilization, and data-driven insights. From securing grant funding to guiding investments,
we help clients unlock sustainable opportunities in marine and freshwater sectors, bridging
the gap between capital providers and project sponsors to drive impactful solutions.
</p>
</div>
<div class="focus-grid">
{focusAreas.map((area) => (
<article class="focus-card">
<h3><a href={`/${area.slug}/`}>{area.data.title}</a></h3>
<p>{area.data.summary}</p>
<a class="text-link" href={`/${area.slug}/`}>Learn more</a>
</article>
))}
</div>
</section>
<ContactCta />
</BaseLayout>

View File

@ -0,0 +1,61 @@
---
import { getCollection } from "astro:content";
import BaseLayout from "@layouts/BaseLayout.astro";
import PageHero from "@components/PageHero.astro";
import ContactCta from "@components/ContactCta.astro";
import { breadcrumbSchema, pageSchema } from "@utils/schema";
export async function getStaticPaths() {
const posts = await getCollection("insights");
return posts.map((post) => ({
params: { slug: post.slug },
props: { post },
}));
}
const { post } = Astro.props;
const { Content } = await post.render();
const pathname = `/insights/${post.slug}/`;
const jsonLd = [
pageSchema({ title: post.data.title, description: post.data.description, pathname }),
breadcrumbSchema([
{ name: "Home", href: "/" },
{ name: "Insights", href: "/insights/" },
{ name: post.data.title, href: pathname },
]),
];
---
<BaseLayout title={post.data.title} description={post.data.description} pathname={pathname} jsonLd={jsonLd}>
<PageHero
eyebrow="Insight"
title={post.data.title}
lead={post.data.description}
image="/assets/live-site/img_0067.jpg"
/>
<article class="detail-layout">
<div class="article-body">
<Content />
</div>
<aside class="sidebar" aria-label="Insight details">
<div class="note-card">
<h2>Published</h2>
<p>{post.data.publishDate.toLocaleDateString("en-US", { year: "numeric", month: "long", day: "numeric" })}</p>
</div>
{post.data.tags && post.data.tags.length > 0 && (
<div class="note-card">
<h2>Tags</h2>
<ul class="outcome-grid">
{post.data.tags.map((tag) => <li>{tag}</li>)}
</ul>
</div>
)}
<div class="note-card">
<h2>Related</h2>
<a class="text-link" href="/insights/">Insights overview</a>
</div>
</aside>
</article>
<ContactCta />
</BaseLayout>

View File

@ -0,0 +1,47 @@
---
import BaseLayout from "@layouts/BaseLayout.astro";
import PageHero from "@components/PageHero.astro";
import ContactCta from "@components/ContactCta.astro";
import { breadcrumbSchema, pageSchema } from "@utils/schema";
const description =
"A prepared future publishing section for BlueCap Strategies articles, essays, case studies, market notes, and announcements.";
---
<BaseLayout
title="Insights"
description={description}
pathname="/insights/"
jsonLd={[
pageSchema({ title: "Insights", description, pathname: "/insights/" }),
breadcrumbSchema([
{ name: "Home", href: "/" },
{ name: "Insights", href: "/insights/" },
]),
]}
>
<PageHero
eyebrow="Insights"
title="Future publishing home for BlueCap Strategies"
lead="This section is prepared for articles, essays, case studies, market notes, and announcements when BlueCap migrates thought leadership from Substack, Medium, or other channels onto its own domain."
image="/assets/live-site/img_0067.jpg"
/>
<section class="insights-shell">
<article class="note-card">
<h2>Publishing path</h2>
<p>
Future posts can be added as Markdown or MDX in <code>src/content/insights</code> with
typed frontmatter for title, description, publish date, modified date, tags, and optional
canonical URL.
</p>
</article>
<article class="note-card">
<h2>RSS support</h2>
<p>
RSS is available at <a href="/rss.xml">/rss.xml</a>. The feed is ready for future article
imports even though no public posts are published yet.
</p>
</article>
</section>
<ContactCta />
</BaseLayout>

22
src/pages/rss.xml.ts Normal file
View File

@ -0,0 +1,22 @@
import rss from "@astrojs/rss";
import { getCollection } from "astro:content";
import type { APIContext } from "astro";
import { site } from "@data/site";
export async function GET(context: APIContext) {
const posts = await getCollection("insights", ({ data }) => !data.draft);
return rss({
title: `${site.name} Insights`,
description:
"Articles, market notes, case studies, and announcements from BlueCap Strategies.",
site: context.site ?? "https://www.bluecapstrategies.com",
items: posts.map((post) => ({
title: post.data.title,
description: post.data.description,
pubDate: post.data.publishDate,
link: `/insights/${post.slug}/`,
categories: post.data.tags,
})),
});
}

47
src/pages/services.astro Normal file
View File

@ -0,0 +1,47 @@
---
import { getCollection } from "astro:content";
import BaseLayout from "@layouts/BaseLayout.astro";
import PageHero from "@components/PageHero.astro";
import CardGrid from "@components/CardGrid.astro";
import ContactCta from "@components/ContactCta.astro";
import { breadcrumbSchema, pageSchema } from "@utils/schema";
const services = (await getCollection("services")).sort((a, b) => a.data.order - b.data.order);
const description =
"BlueCap Strategies provides market intelligence, catalytic capital, partnership development, technology incubation, implementation, impact reporting, and policy advisory services.";
---
<BaseLayout
title="Services"
description={description}
pathname="/services/"
jsonLd={[
pageSchema({ title: "Services", description, pathname: "/services/" }),
breadcrumbSchema([
{ name: "Home", href: "/" },
{ name: "Services", href: "/services/" },
]),
]}
>
<PageHero
eyebrow="Services"
title="Advisory services for capital-ready water-sector initiatives"
lead="BlueCap Strategies helps clients identify opportunities, structure capital, build partnerships, implement projects, measure outcomes, and shape supportive policy."
image="/assets/live-site/img_0075.jpg"
imageAlt="Marine and freshwater resources viewed from above"
/>
<section class="section">
<CardGrid
heading="Our services"
items={services.map((service) => ({
title: service.data.title,
description: service.data.summary,
href: `/${service.slug}/`,
icon: service.data.icon,
}))}
label="Service pages"
/>
</section>
<ContactCta />
</BaseLayout>

25
src/pages/thank-you.astro Normal file
View File

@ -0,0 +1,25 @@
---
import BaseLayout from "@layouts/BaseLayout.astro";
import { pageSchema } from "@utils/schema";
const description = "Thank you for contacting BlueCap Strategies.";
---
<BaseLayout
title="Thank You"
description={description}
pathname="/thank-you/"
jsonLd={[pageSchema({ title: "Thank You", description, pathname: "/thank-you/" })]}
>
<section class="insights-shell">
<article class="note-card">
<p class="eyebrow">Contact received</p>
<h1>Thank you</h1>
<p>
Your message has been submitted. BlueCap Strategies will review the inquiry and respond
through the appropriate contact path.
</p>
<a class="button" href="/">Return home</a>
</article>
</section>
</BaseLayout>

718
src/styles/global.css Normal file
View File

@ -0,0 +1,718 @@
:root {
--navy: #123047;
--blue: #1e6d91;
--cyan: #3ca2bd;
--green: #4f7f65;
--ink: #18242d;
--muted: #5d6b75;
--line: #d9e2e7;
--wash: #f3f7f8;
--paper: #ffffff;
--accent: #c47f43;
--shadow: 0 18px 45px rgba(18, 48, 71, 0.12);
font-family:
Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}
* {
box-sizing: border-box;
}
html {
color: var(--ink);
background: var(--paper);
scroll-behavior: smooth;
}
body {
margin: 0;
font-size: 17px;
line-height: 1.65;
}
img {
display: block;
max-width: 100%;
height: auto;
}
a {
color: var(--blue);
}
a:hover {
color: var(--navy);
}
h1,
h2,
h3 {
margin: 0;
color: var(--navy);
line-height: 1.15;
letter-spacing: 0;
}
h1 {
max-width: 16ch;
font-size: 3.35rem;
}
h2 {
font-size: 2rem;
}
h3 {
font-size: 1.2rem;
}
p {
margin: 0;
}
.skip-link {
position: absolute;
left: 1rem;
top: 1rem;
z-index: 10;
transform: translateY(-150%);
background: var(--navy);
color: #fff;
padding: 0.7rem 1rem;
}
.skip-link:focus {
transform: translateY(0);
}
.site-header {
position: sticky;
top: 0;
z-index: 5;
background: rgba(255, 255, 255, 0.96);
border-bottom: 1px solid var(--line);
backdrop-filter: blur(12px);
}
.header-inner,
.footer-inner,
.section,
.page-hero,
.home-hero,
.contact-cta,
.detail-layout,
.insights-shell {
width: min(1180px, calc(100% - 40px));
margin-inline: auto;
}
.header-inner {
min-height: 82px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 2rem;
}
.brand img {
width: 190px;
}
.desktop-nav {
display: flex;
align-items: center;
gap: 1.3rem;
font-size: 0.95rem;
font-weight: 700;
}
.desktop-nav a,
.mobile-nav a {
color: var(--ink);
text-decoration: none;
}
.nav-cta,
.button,
.footer-cta {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 44px;
border-radius: 6px;
padding: 0.68rem 1rem;
background: var(--navy);
color: #fff !important;
text-decoration: none;
font-weight: 800;
}
.nav-menu {
position: relative;
padding-block: 1.2rem;
}
.nav-panel {
position: absolute;
right: -1rem;
top: 100%;
width: 340px;
display: none;
grid-template-columns: 1fr;
gap: 0.15rem;
padding: 0.7rem;
background: #fff;
border: 1px solid var(--line);
border-radius: 8px;
box-shadow: var(--shadow);
}
.nav-panel a {
padding: 0.5rem 0.65rem;
border-radius: 6px;
}
.nav-panel a:hover {
background: var(--wash);
}
.nav-menu:hover .nav-panel,
.nav-menu:focus-within .nav-panel {
display: grid;
}
.mobile-nav {
display: none;
}
.mobile-nav summary {
min-height: 44px;
padding: 0.5rem 0.8rem;
border: 1px solid var(--line);
border-radius: 6px;
font-weight: 800;
cursor: pointer;
}
.mobile-nav nav {
position: absolute;
left: 20px;
right: 20px;
top: 76px;
display: grid;
gap: 0.2rem;
padding: 1rem;
background: #fff;
border: 1px solid var(--line);
border-radius: 8px;
box-shadow: var(--shadow);
}
.mobile-nav a {
padding: 0.55rem;
border-radius: 6px;
}
.mobile-nav a:hover {
background: var(--wash);
}
.home-hero {
display: grid;
grid-template-columns: minmax(0, 1.08fr) minmax(320px, 0.92fr);
gap: 3rem;
align-items: center;
padding: 3.25rem 0 3rem;
}
.home-hero .hero-media {
position: relative;
}
.home-hero img,
.page-hero img {
width: 100%;
aspect-ratio: 4 / 3;
object-fit: cover;
border-radius: 8px;
box-shadow: var(--shadow);
}
.hero-copy {
display: grid;
gap: 1.2rem;
}
.hero-copy p:not(.eyebrow) {
max-width: 68ch;
color: var(--muted);
font-size: 1.15rem;
}
.eyebrow {
color: var(--green);
font-size: 0.82rem;
font-weight: 900;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.hero-actions,
.cta-actions {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 1rem;
}
.text-link {
font-weight: 800;
color: var(--blue);
text-decoration-thickness: 2px;
text-underline-offset: 4px;
}
.section {
padding: 4.5rem 0;
}
.section.alt {
width: 100%;
max-width: none;
background: var(--wash);
}
.section.alt > .section-inner {
width: min(1180px, calc(100% - 40px));
margin-inline: auto;
}
.section-header {
max-width: 760px;
display: grid;
gap: 0.7rem;
margin-bottom: 2rem;
}
.section-header p {
color: var(--muted);
}
.card-grid-heading {
margin-bottom: 1.5rem;
}
.card-icon {
width: 48px;
height: 48px;
object-fit: contain;
margin-bottom: 0.75rem;
}
.anchor-nav ul {
list-style: none;
margin: 0.75rem 0 0;
padding: 0;
display: grid;
gap: 0.5rem;
}
.anchor-nav a {
color: var(--muted);
text-decoration: none;
font-size: 0.95rem;
}
.anchor-nav a:hover {
color: var(--ink);
}
.three-grid,
.stats-grid,
.card-grid,
.focus-grid,
.outcome-grid {
display: grid;
gap: 1rem;
}
.three-grid {
grid-template-columns: repeat(3, 1fr);
}
.stats-grid {
grid-template-columns: repeat(4, 1fr);
}
.stat,
.link-card,
.focus-card,
.work-card,
.note-card {
background: #fff;
border: 1px solid var(--line);
border-radius: 8px;
padding: 1.25rem;
}
.work-card {
display: grid;
gap: 0.7rem;
}
.stat strong {
display: block;
color: var(--accent);
font-size: 1.8rem;
line-height: 1.1;
}
.card-grid,
.focus-grid {
grid-template-columns: repeat(2, 1fr);
}
.link-card,
.focus-card {
display: grid;
gap: 0.8rem;
}
.link-card h3 a,
.focus-card h3 a {
color: var(--navy);
text-decoration: none;
}
.link-card .card-action {
color: var(--blue);
font-weight: 800;
text-decoration-thickness: 2px;
text-underline-offset: 4px;
}
.media-band {
display: grid;
grid-template-columns: 0.9fr 1.1fr;
gap: 2rem;
align-items: center;
}
.media-band img {
border-radius: 8px;
box-shadow: var(--shadow);
}
.article-body img {
width: 100%;
height: auto;
border-radius: 8px;
box-shadow: var(--shadow);
margin: 0.5rem 0 1.5rem;
}
.page-hero {
display: grid;
grid-template-columns: 1.05fr 0.95fr;
gap: 3rem;
align-items: center;
padding: 3.5rem 0;
}
.detail-layout {
display: grid;
grid-template-columns: minmax(0, 1fr) 320px;
gap: 3rem;
padding: 0 0 4.5rem;
}
.article-body {
display: grid;
gap: 2rem;
}
.article-body section {
display: grid;
gap: 0.75rem;
}
.article-body p,
.article-body li {
color: var(--muted);
}
.sidebar {
align-self: start;
position: sticky;
top: 104px;
display: grid;
gap: 1rem;
}
.outcome-grid {
list-style: none;
padding: 0;
margin: 0;
}
.outcome-grid li {
padding: 0.75rem 0 0.75rem 1rem;
border-left: 3px solid var(--cyan);
background: var(--wash);
}
.contact-cta {
display: grid;
grid-template-columns: 1fr auto;
gap: 2rem;
align-items: center;
margin-block: 2rem 4.5rem;
padding: 2rem;
background: var(--navy);
color: #fff;
border-radius: 8px;
}
.contact-cta h2,
.contact-cta .eyebrow {
color: #fff;
}
.contact-cta p {
max-width: 720px;
color: rgba(255, 255, 255, 0.82);
}
.contact-cta .button {
background: #fff;
color: var(--navy) !important;
}
.contact-cta .text-link {
color: #fff;
}
.contact-layout {
display: grid;
grid-template-columns: 0.8fr 1.2fr;
gap: 2rem;
align-items: start;
}
.contact-panel,
.form-panel {
border: 1px solid var(--line);
border-radius: 8px;
padding: 1.5rem;
background: #fff;
}
.contact-panel {
display: grid;
gap: 1rem;
}
.contact-form {
display: grid;
gap: 1rem;
}
.field-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 1rem;
}
label {
display: grid;
gap: 0.35rem;
color: var(--navy);
font-weight: 800;
}
input,
textarea {
width: 100%;
min-height: 46px;
border: 1px solid var(--line);
border-radius: 6px;
padding: 0.7rem 0.8rem;
font: inherit;
color: var(--ink);
background: #fff;
}
textarea {
min-height: 160px;
resize: vertical;
}
input:focus,
textarea:focus {
outline: 3px solid rgba(60, 162, 189, 0.25);
border-color: var(--cyan);
}
.form-note {
color: var(--muted);
font-size: 0.95rem;
}
.site-footer {
background: #10293d;
color: rgba(255, 255, 255, 0.78);
}
.footer-inner {
display: grid;
grid-template-columns: 1.25fr 0.7fr 1fr 0.85fr;
gap: 2rem;
padding: 3rem 0;
}
.footer-inner h2 {
margin-bottom: 0.7rem;
color: #fff;
font-size: 1rem;
}
.footer-inner a {
display: block;
margin: 0.35rem 0;
color: rgba(255, 255, 255, 0.9);
text-decoration: none;
}
.footer-brand {
display: grid;
gap: 1rem;
}
.footer-brand img {
width: 175px;
background: #fff;
border-radius: 6px;
padding: 0.4rem;
}
.footer-cta {
width: fit-content;
margin-top: 1rem !important;
background: var(--cyan);
}
.footer-bottom {
border-top: 1px solid rgba(255, 255, 255, 0.14);
padding: 1rem 20px;
text-align: center;
font-size: 0.92rem;
}
.insights-shell {
padding: 4rem 0;
display: grid;
gap: 2rem;
}
@media (max-width: 960px) {
h1 {
max-width: 16ch;
font-size: 3rem;
}
.desktop-nav {
display: none;
}
.mobile-nav {
display: block;
}
.home-hero,
.page-hero,
.media-band,
.detail-layout,
.contact-layout,
.contact-cta {
grid-template-columns: 1fr;
}
.contact-cta .cta-actions {
justify-self: start;
}
.three-grid,
.stats-grid,
.footer-inner {
grid-template-columns: repeat(2, 1fr);
}
.sidebar {
position: static;
}
}
@media (max-width: 640px) {
body {
font-size: 16px;
}
h1 {
max-width: none;
font-size: 2rem;
}
h2 {
font-size: 1.65rem;
}
.header-inner,
.footer-inner,
.section,
.page-hero,
.home-hero,
.contact-cta,
.detail-layout,
.insights-shell {
width: min(100% - 28px, 1180px);
}
.home-hero,
.page-hero {
padding: 1.8rem 0;
gap: 1.6rem;
}
.hero-copy {
gap: 0.9rem;
}
.hero-copy p:not(.eyebrow) {
font-size: 1.05rem;
}
.section {
padding: 3rem 0;
}
.section.alt > .section-inner {
width: min(100% - 28px, 1180px);
}
.three-grid,
.stats-grid,
.card-grid,
.focus-grid,
.field-grid,
.footer-inner {
grid-template-columns: 1fr;
}
.brand img {
width: 160px;
}
.contact-cta {
padding: 1.25rem;
}
}

92
src/utils/schema.ts Normal file
View File

@ -0,0 +1,92 @@
import { site } from "@data/site";
import { assetUrl, canonical, siteUrl } from "./url";
type Crumb = { name: string; href: string };
export function organizationSchema() {
return {
"@context": "https://schema.org",
"@type": "Organization",
"@id": `${siteUrl}/#organization`,
name: site.name,
legalName: site.legalName,
url: siteUrl,
logo: assetUrl("/assets/bluecap-logo.png"),
email: site.email,
telephone: site.phone,
founder: {
"@type": "Person",
name: site.founder,
},
description: site.description,
};
}
export function websiteSchema() {
return {
"@context": "https://schema.org",
"@type": "WebSite",
"@id": `${siteUrl}/#website`,
url: siteUrl,
name: site.name,
publisher: { "@id": `${siteUrl}/#organization` },
};
}
export function pageSchema({
type = "WebPage",
title,
description,
pathname,
}: {
type?: "WebPage" | "AboutPage" | "ContactPage";
title: string;
description: string;
pathname: string;
}) {
return {
"@context": "https://schema.org",
"@type": type,
"@id": `${canonical(pathname)}#webpage`,
url: canonical(pathname),
name: title,
description,
isPartOf: { "@id": `${siteUrl}/#website` },
about: { "@id": `${siteUrl}/#organization` },
};
}
export function serviceSchema({
title,
description,
pathname,
}: {
title: string;
description: string;
pathname: string;
}) {
return {
"@context": "https://schema.org",
"@type": "Service",
"@id": `${canonical(pathname)}#service`,
name: title,
description,
provider: { "@id": `${siteUrl}/#organization` },
areaServed: "United States",
url: canonical(pathname),
serviceType: "Water-sector advisory services",
};
}
export function breadcrumbSchema(crumbs: Crumb[]) {
return {
"@context": "https://schema.org",
"@type": "BreadcrumbList",
itemListElement: crumbs.map((crumb, index) => ({
"@type": "ListItem",
position: index + 1,
name: crumb.name,
item: canonical(crumb.href),
})),
};
}

13
src/utils/url.ts Normal file
View File

@ -0,0 +1,13 @@
const defaultSiteUrl = "https://www.bluecapstrategies.com";
export const siteUrl = (import.meta.env.PUBLIC_SITE_URL ?? defaultSiteUrl).replace(/\/$/, "");
export function canonical(pathname: string) {
const cleanPath = pathname.startsWith("/") ? pathname : `/${pathname}`;
const withSlash = cleanPath.endsWith("/") ? cleanPath : `${cleanPath}/`;
return `${siteUrl}${withSlash}`;
}
export function assetUrl(path: string) {
return `${siteUrl}${path.startsWith("/") ? path : `/${path}`}`;
}

134
tests/site.spec.ts Normal file
View File

@ -0,0 +1,134 @@
import { expect, test } from "@playwright/test";
const expectedPages = [
"/",
"/about-us/",
"/contact/",
"/services/",
"/focus-areas/",
"/insights/",
"/market-intelligence-opportunity-analysis/",
"/develop-and-deploy-catalytic-capital/",
"/stakeholder-engagement-partnership-development/",
"/sustainable-technology-incubation/",
"/sustainable-business-model-innovation/",
"/project-management-implementation/",
"/impact-assessment-reporting/",
"/policy-advocacy-development/",
"/the-economics-of-pfas-remediation/",
"/structuring-marine-conservation-finance/",
"/sustainable-living-resource-utilization/",
"/economic-development-for-island-and-coastal-communities/",
];
test.describe("page rendering", () => {
for (const path of expectedPages) {
test(`${path} renders meaningful HTML without horizontal overflow`, async ({ page }) => {
const response = await page.goto(path);
expect(response?.status(), path).toBeLessThan(400);
await expect(page.locator("h1")).toBeVisible();
await expect(page.locator("main")).toContainText(/BlueCap|water|capital|aquatic|PFAS|marine/i);
const overflow = await page.evaluate(
() => document.documentElement.scrollWidth - document.documentElement.clientWidth,
);
expect(overflow, `${path} has horizontal overflow`).toBeLessThanOrEqual(1);
});
}
});
test("all internal href links resolve", async ({ page, request, baseURL }) => {
const found = new Set<string>();
for (const path of expectedPages) {
await page.goto(path);
const links = await page.locator("a[href]").evaluateAll((anchors) =>
anchors.map((anchor) => (anchor as HTMLAnchorElement).href),
);
for (const href of links) {
if (href.startsWith("mailto:") || href.startsWith("tel:")) continue;
const url = new URL(href);
if (url.origin !== baseURL) continue;
found.add(`${url.pathname}${url.search}`);
}
}
for (const href of found) {
const response = await request.get(href);
expect(response.status(), href).toBeLessThan(400);
}
});
test("desktop navigation and CTA links are clickable", async ({ page }) => {
await page.setViewportSize({ width: 1440, height: 900 });
await page.goto("/");
await page.getByRole("link", { name: "Explore services" }).click();
await expect(page).toHaveURL(/\/services\/$/);
await expect(page.getByRole("heading", { name: /Advisory services/ })).toBeVisible();
await page.goto("/");
await page.getByRole("link", { name: "Request a consultation" }).first().click();
await expect(page).toHaveURL(/\/contact\/$/);
await expect(page.getByRole("heading", { name: /Get in touch/ })).toBeVisible();
});
test("service and focus-area learn more links are wired to detail pages", async ({ page }) => {
for (const path of ["/services/", "/focus-areas/"]) {
await page.goto(path);
const learnMoreLinks = page.getByRole("link", { name: "Learn more" });
const count = await learnMoreLinks.count();
expect(count, `${path} should expose Learn more links`).toBeGreaterThan(0);
for (let index = 0; index < count; index += 1) {
const href = await learnMoreLinks.nth(index).getAttribute("href");
expect(href, `${path} Learn more ${index + 1}`).toMatch(/^\/.+\/$/);
const response = await page.request.get(href!);
expect(response.status(), href ?? "").toBeLessThan(400);
}
}
});
test("mobile menu opens and navigates", async ({ page }) => {
await page.setViewportSize({ width: 390, height: 844 });
await page.goto("/");
await page.locator(".mobile-nav summary").click();
await expect(page.locator(".mobile-nav nav")).toBeVisible();
await page.locator(".mobile-nav").getByRole("link", { name: "Focus Areas" }).click();
await expect(page).toHaveURL(/\/focus-areas\/$/);
await expect(page.getByRole("heading", { name: /Focused expertise/ })).toBeVisible();
});
test("contact form validates required, invalid, and valid paths", async ({ page }) => {
await page.goto("/contact/");
const submit = page.getByRole("button", { name: "Submit" });
await submit.click();
const emptyValidation = await page.locator('input[name="name"]').evaluate((input) => {
return (input as HTMLInputElement).validationMessage.length > 0;
});
expect(emptyValidation).toBe(true);
await expect(page).toHaveURL(/\/contact\/$/);
await page.locator('input[name="name"]').fill("Test User");
await page.locator('input[name="email"]').fill("not-an-email");
await page.locator('textarea[name="message"]').fill("Too short");
await submit.click();
const invalidEmail = await page.locator('input[name="email"]').evaluate((input) => {
return !(input as HTMLInputElement).validity.valid;
});
expect(invalidEmail).toBe(true);
await page.locator('input[name="email"]').fill("test@example.com");
await page.locator('textarea[name="message"]').fill("This is a valid test inquiry for BlueCap Strategies.");
await page.locator("form").evaluate((form) => {
form.setAttribute("data-endpoint", "/thank-you/");
form.setAttribute("action", "/thank-you/");
form.setAttribute("method", "get");
});
await submit.click();
await expect(page).toHaveURL(/\/thank-you\/(?:\?.*)?$/);
await expect(page.getByRole("heading", { name: "Thank you" })).toBeVisible();
});

12
tsconfig.json Normal file
View File

@ -0,0 +1,12 @@
{
"extends": "astro/tsconfigs/strict",
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@components/*": ["src/components/*"],
"@data/*": ["src/data/*"],
"@layouts/*": ["src/layouts/*"],
"@utils/*": ["src/utils/*"]
}
}
}