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

242 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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:
> **Tracked in [`launch-checklist.md`](./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`](./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)
- [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)