Files
bcs-website/docs/seo-aeo-optimization.md
T
bmr_bluecap 79e7fd4b2b docs: add maintained launch checklist; de-duplicate stale task lists
Cutover tasks were scattered across three sources that disagreed with each
other and with reality: planning/parity-checklist.md (git-ignored, content
only), docs/seo-aeo-optimization.md (SEO only, listed the Zod schema,
seo-lint and breadcrumbs as open when all three are done, and called for
Uptime Kuma which ADR-0004 replaced with monylog), and session notes.

Add docs/launch-checklist.md as the single source of truth, verified against
the working tree, the cluster and ArgoCD rather than inherited from the older
docs. Point the SEO doc's two checkbox blocks at it and keep only reference
detail there.

Also records blockers that were not tracked anywhere: TLS never wired on the
ingress, developer-facing copy rendered on the contact page, the contact form
having no endpoint, and the draft insight post building into the sitemap
because the draft flag is declared in Zod but never filtered on.
2026-09-22 15:45:48 -04:00

15 KiB
Raw Blame History

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:

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.parses.
  • 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:

Tracked in launch-checklist.md §4. The list that used to live here went stale (breadcrumbs are done). This section keeps the reference detail below; the live task state is in the launch checklist only.

  • Per-page OG images — every page currently falls back to one shared image (BaseLayout.astro emits a single og:image). Generate topic-specific 1200×630 images for services, focus areas, and insights.
  • robots meta wired to a noindex frontmatter flag (for /thank-you/, staging). Not wired today — /thank-you/ is sitemap-excluded but emits no noindex.
  • article:published_time / modified_time OG tags on insights posts.
  • Breadcrumbs — DONE. BreadcrumbList in src/utils/schema.ts, applied on detail pages.
  • Sitemap — via @astrojs/sitemap, excludes /thank-you/. Still to 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

Moved. This list is no longer maintained here — it duplicated, and disagreed with, reality (it listed the Zod schema, seo-lint, and breadcrumbs as open when all three are done, and called for Uptime Kuma, which ADR-0004 replaced with monylog).

The maintained checklist is launch-checklist.md. SEO items are in §3 (owner decisions: canonical host, robots policy) and §4 (post-cutover: OG images, noindex meta, article times, lastmod, Lighthouse, trailing slashes). What is already complete is recorded in §5.

Sources (verified 2026-07-15)