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>
10
.dockerignore
Normal 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
@ -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
@ -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
@ -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
@ -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
|
||||
120–158 chars), so the build fails fast on SEO violations. The `.md` files are **build-time
|
||||
source**: Astro compiles them to static HTML in `dist/`. The Markdown itself is never served.
|
||||
|
||||
## Project layout
|
||||
|
||||
```
|
||||
src/ Astro pages, layouts, components, content collections, styles
|
||||
public/ Static assets served as-is (images, robots.txt, healthz)
|
||||
scripts/ Build tooling (seo-lint.mjs)
|
||||
tests/ Playwright e2e specs
|
||||
deploy/ nginx.conf for the runtime image
|
||||
k8s/ Kustomize manifests (namespace, deployment, service, ingress, configmap)
|
||||
docs/ Committed engineering records — ADRs, deployment, SEO/AEO, deviations
|
||||
Dockerfile Multi-stage build → nginx image containing only dist/
|
||||
```
|
||||
|
||||
`planning/` (git-ignored) holds working docs — content inventories, parity checklists, and the
|
||||
original rebuild brief. See `docs/adr/` for the decision record.
|
||||
|
||||
## Deployment
|
||||
|
||||
Push to `main` → GitHub Actions runs the quality gates, builds and pushes a `<git-sha>`-tagged
|
||||
image to GHCR, and updates the image tag in `k8s/`. ArgoCD reconciles the change to k3s. GitHub
|
||||
never receives cluster credentials. See `docs/gitops-deployment-strategy.md`.
|
||||
15
astro.config.mjs
Normal 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
@ -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";
|
||||
}
|
||||
}
|
||||
25
docs/adr/0001-gitops-with-argocd.md
Normal 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`.
|
||||
30
docs/adr/0002-page-copy-in-content-collections.md
Normal 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).
|
||||
30
docs/adr/0003-seo-enforcement-with-zod.md
Normal file
@ -0,0 +1,30 @@
|
||||
# ADR-0003: Enforce SEO rules with Zod + build linter (not Pydantic)
|
||||
|
||||
**Status:** Accepted — 2026-07-15
|
||||
|
||||
## Context
|
||||
|
||||
We want SEO/AEO rules (title/description length, canonical, OG image, structured data, single H1,
|
||||
alt text) to be *enforced*, not aspirational. The question of a Pydantic-style validator came up,
|
||||
but this is a TypeScript/Astro build — Pydantic is Python and would mean bolting a foreign runtime
|
||||
onto a JS pipeline.
|
||||
|
||||
## Decision
|
||||
|
||||
Enforce in two layers, both native to the stack:
|
||||
|
||||
1. **Zod schema** on content-collection frontmatter (Astro Content Collections already run on Zod).
|
||||
Encodes: title ≤ 55 chars, description 120–158, valid canonical, required OG image, `noindex`
|
||||
flag, `primaryTopic`. Violations **fail `npm run build`**.
|
||||
2. **Build-time SEO linter** (`scripts/seo-lint.mjs`) over `dist/**/*.html` for rules Zod can't see
|
||||
in frontmatter: exactly one `<h1>`, heading order, canonical/title/description present, non-empty
|
||||
`alt` on every `<img>`, every JSON-LD block parses. Runs in CI (and locally via `npm run seo:lint`).
|
||||
|
||||
Do **not** introduce Python/Pydantic.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Every commit is validated before ArgoCD deploys; "the rules are followed" is guaranteed by CI.
|
||||
- Rules live with the content schema and the build, in one language.
|
||||
- Requires the content collections from ADR-0002 to carry the shared `seo` schema.
|
||||
- Detailed rules in `docs/seo-aeo-optimization.md` §1.
|
||||
30
docs/adr/0004-reuse-monylog-for-monitoring.md
Normal 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.
|
||||
31
docs/adr/0005-analytics-posthog-and-umami-trial.md
Normal 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.
|
||||
26
docs/adr/0006-launch-insights-blog.md
Normal file
@ -0,0 +1,26 @@
|
||||
# ADR-0006: Launch a blog at /insights/
|
||||
|
||||
**Status:** Accepted — 2026-07-15
|
||||
|
||||
## Context
|
||||
|
||||
The live WordPress site has **no published blog** (0 posts, 0 `project` entries) but does have an
|
||||
**unpublished, empty "Insights & Resources" draft** (`page_id=303`) — a blog scaffolded but never
|
||||
launched. The rebuild already has an `/insights/` section (routes, RSS, content collection, one
|
||||
example draft). The parity plan and SEO/AEO plan both identify thought leadership as a core
|
||||
authority/GEO asset.
|
||||
|
||||
## Decision
|
||||
|
||||
**Launch the blog at `/insights/`** in the rebuild. Keep the existing Astro content-collection
|
||||
scaffold (Markdown/MDX, RSS, tags, canonical for cross-posts). Publish only **real,
|
||||
owner-reviewed** content — no fabricated or unreviewed AI-generated articles. Candidate first
|
||||
pieces: PFAS remediation as a capital-formation problem; scaling marine conservation finance;
|
||||
making island/coastal infrastructure investable.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Gives AEO/GEO a surface to be cited from; `Article`/`BlogPosting` schema per SEO plan §3.
|
||||
- Content is owner-gated — the section can ship empty or with 1–2 real posts; do not backfill with
|
||||
placeholder articles.
|
||||
- Insights carry `publishDate`/`modifiedDate`; RSS and sitemap `lastmod` derive from these.
|
||||
16
docs/adr/README.md
Normal 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 |
|
||||
78
docs/content-deviations.md
Normal 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, 120–158 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
@ -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.
|
||||
193
docs/gitops-deployment-strategy.md
Normal 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).
|
||||
237
docs/seo-aeo-optimization.md
Normal 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; 120–158 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
@ -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
@ -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
@ -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
@ -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
@ -0,0 +1,4 @@
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: bluecap-strategies
|
||||
12
k8s/service.yaml
Normal 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
27
package.json
Normal 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
@ -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,
|
||||
},
|
||||
});
|
||||
BIN
public/assets/bluecap-logo.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
public/assets/live-site/bluecap-logo.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
public/assets/live-site/home-square.jpg
Normal file
|
After Width: | Height: | Size: 154 KiB |
BIN
public/assets/live-site/icons/business-model.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
BIN
public/assets/live-site/icons/capital.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
public/assets/live-site/icons/green-technology.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
BIN
public/assets/live-site/icons/impact-reporting.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
public/assets/live-site/icons/market-research.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
public/assets/live-site/icons/policy-advocacy.png
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
BIN
public/assets/live-site/icons/project-management.png
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
BIN
public/assets/live-site/icons/stakeholder.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
public/assets/live-site/img1.jpg
Normal file
|
After Width: | Height: | Size: 125 KiB |
BIN
public/assets/live-site/img_0053.jpg
Normal file
|
After Width: | Height: | Size: 68 KiB |
BIN
public/assets/live-site/img_0054.jpg
Normal file
|
After Width: | Height: | Size: 113 KiB |
BIN
public/assets/live-site/img_0055.jpg
Normal file
|
After Width: | Height: | Size: 102 KiB |
BIN
public/assets/live-site/img_0058.jpg
Normal file
|
After Width: | Height: | Size: 62 KiB |
BIN
public/assets/live-site/img_0062.jpg
Normal file
|
After Width: | Height: | Size: 170 KiB |
BIN
public/assets/live-site/img_0067.jpg
Normal file
|
After Width: | Height: | Size: 206 KiB |
BIN
public/assets/live-site/img_0070.jpg
Normal file
|
After Width: | Height: | Size: 57 KiB |
BIN
public/assets/live-site/img_0072.jpg
Normal file
|
After Width: | Height: | Size: 79 KiB |
BIN
public/assets/live-site/img_0075.jpg
Normal file
|
After Width: | Height: | Size: 209 KiB |
1
public/healthz
Normal file
@ -0,0 +1 @@
|
||||
ok
|
||||
16
public/llms.txt
Normal 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
@ -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
@ -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.`);
|
||||
28
src/components/Analytics.astro
Normal 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} />
|
||||
)
|
||||
}
|
||||
28
src/components/CardGrid.astro
Normal 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>
|
||||
18
src/components/ContactCta.astro
Normal 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>
|
||||
33
src/components/Footer.astro
Normal 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>
|
||||
32
src/components/Header.astro
Normal 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>
|
||||
26
src/components/PageHero.astro
Normal 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
@ -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; 120–158 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 };
|
||||
@ -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.
|
||||
@ -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.
|
||||
@ -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.
|
||||
10
src/content/focus-areas/the-economics-of-pfas-remediation.md
Normal 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.
|
||||
14
src/content/insights/example-draft.md
Normal 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
@ -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.
|
||||
|
||||

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

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

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

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

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

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

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

|
||||
|
||||
## 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.
|
||||
17
src/content/services/develop-and-deploy-catalytic-capital.md
Normal 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.
|
||||
17
src/content/services/impact-assessment-reporting.md
Normal 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.
|
||||
@ -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.
|
||||
17
src/content/services/policy-advocacy-development.md
Normal 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.
|
||||
17
src/content/services/project-management-implementation.md
Normal 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.
|
||||
@ -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.
|
||||
@ -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.
|
||||
17
src/content/services/sustainable-technology-incubation.md
Normal 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
@ -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
@ -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;
|
||||
}
|
||||
62
src/layouts/BaseLayout.astro
Normal 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
@ -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
@ -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}>← {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
@ -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
@ -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>
|
||||
47
src/pages/focus-areas.astro
Normal 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
@ -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>
|
||||
61
src/pages/insights/[slug].astro
Normal 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>
|
||||
47
src/pages/insights/index.astro
Normal 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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -0,0 +1,12 @@
|
||||
{
|
||||
"extends": "astro/tsconfigs/strict",
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@components/*": ["src/components/*"],
|
||||
"@data/*": ["src/data/*"],
|
||||
"@layouts/*": ["src/layouts/*"],
|
||||
"@utils/*": ["src/utils/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||