# 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({ // 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)