Skip to content

SEO Content Architecture

github-actions[bot] edited this page Oct 14, 2026 · 6 revisions

Canonical source: docs/05-frontend/seo-content-architecture.md — this page is rendered from it on every publish; edit the source.

SEO content architecture — audit and plan

Audited: 2026-08-30 · Site: https://claw-ai.co · Scope: every public route under app/(marketing), plus every discovery document derived from the content registry.

Companion to multilingual-discovery.md, which documents the mechanism. This document is the content decision: what exists, what is missing, what gets built, and what was deliberately refused.


1. What already exists, and is load-bearing

The discovery layer is finished and self-maintaining. Do not rebuild any of it, and do not hand-write anything it derives.

PUBLIC_CONTENT_DEFINITIONS (constants/content-registry.constants.ts) is the single authority. From one registry entry, a page automatically reaches:

Surface Route Derived how
Sitemap index /sitemap.xml Chunked per locale, counts pages rather than assuming one chunk
Sitemap documents /sitemaps/{locale}/pages-N.xml getIndexablePagesForLocale
Global feed /rss.xml All 13 locales, one fixed URL, each item language-tagged
Locale feed /feed.xml, /{locale}/feed.xml Locale-negotiated
Topic + chat feeds /feeds/topics.xml, /feeds/chats.xml RssFeedKind
Robots /robots.txt Named crawler groups, sitemap advertised
AI discovery /llms.txt Registry
Footer marketing-footer Published entries only
Canonical + hreflang + OG + Twitter every page buildRequestPublicPageMetadata
Feed advertisement <link rel="alternate"> alternates.types, both locale and global

Four tests fail the build if the chain breaks: sitemap-coverage.test.ts, lighthouse-coverage.test.ts, marketing-footer.test.tsx, content-registry.utility.test.ts. Lighthouse CI asserts accessibility, best-practices and SEO at minScore: 1 — a hard error, not a warning.

Consequence for this work: a new page is a registry entry plus content. It is never a hand-written canonical, sitemap line, feed item or footer link.

Existing public surface (28 pages × 13 locales)

Group Paths
Product /, /about, /features, /how-it-works, /pricing, /use-cases
Models /supported-models
Coding /coding-agent, /coding-agent/install
Private AI /local-first-ai, /architecture, /security-and-privacy
Comparison /compare + 9 rivals (chatgpt, claude, gemini, perplexity, copilot, kimi, qwen, glm, deepseek)
Support /faq, /contact
Legal /terms, /privacy, /cookies, /acceptable-use
Dynamic /share/chat/[publicShareId]

Locales: en ar fr it de es ru pt hi ja th fa zh — 13, two RTL (ar, fa).


2. Findings

F1 — 15 PLANNED registry slugs were never built

PLANNED_CONTENT_CONFIGS declares multi-provider-ai, model-routing, advanced-orchestration, memory-and-context, rag-and-files, workspace-connectors, desktop-agent, self-hosting, ai-safety, observability, supported-providers, and four guides/* slugs. None has a route. They are NOINDEX with empty locales, so they leak into nothing — but they encode a different information architecture from the one being built. Four guides/* slugs collide with the /learn GUIDE hub decided in §4; a fifth, workspace-connectors, was added to this list once the boardroom review (§8) put /integrations in scope — it collides with that WORKSPACE hub the same way.

Resolved: the hub scheme wins (see §4). Colliding PLANNED slugs are retired rather than left as a second, contradictory roadmap.

F2 — /supported-models deliberately publishes no model names

The page states its own policy:

"A static list of fashionable model versions becomes inaccurate quickly and can imply credentials that are not configured."

It names implemented provider families and defers to the live catalog. It also records that AWS Bedrock is connector scaffolding whose model synchronisation is not implemented, and is therefore excluded on purpose.

This is correct and stays. It does, however, mean there is no existing source for model-level content, and no public catalog API — the live registry sits in routing-service behind auth and varies per deployment.

Resolved (2026-09-09): a curated, dated MODEL_FACTS module (§3) becomes the single source for every model-level page, explicitly framed as a reviewed editorial snapshot rather than the live catalog.

Superseded (2026-09-12). The premise above — "no public catalog API" — was true when written and is not any more: connector-service publishes one, and the model-level pages read it. The editorial snapshot had drifted to naming 16 models where the product served 170, and to Claude Opus 4 where the deployment ran Opus 5. See §3 for what replaced it and why the boundary objection is answered rather than ignored.

Update (2026-09-30). /supported-models now renders the SAME live roster the home page uses (ModelRosterSection: each provider with a real model count and a sample of real model names, plus a dialog with the full list), read from the connector catalog and empty when the catalog is unavailable. It is the public "models available" page and is in the sitemap (/sitemaps/en/pages-1.xml lists /en/supported-models, checked live 2026-09-30; the registry marks it FeedEligibility.PUBLISHABLE and AdEligibility.INELIGIBLE, so the roster changes no ad eligibility). The private /models dashboard stays authenticated. No new route was added: every home section already has its own indexed page (/pricing, /supported-models, /how-it-works, /features, /coding-agent, /features/pay-as-you-go-credit, /features/administration-and-access-control, /local-first-ai), and repeating a section's copy on a second URL is the thin, duplicated content the AdSense eligibility work guards against (adsense-eligibility.md).

Batch 3 deviation, discovered at build time: this document names the cluster /models. That path is already taken — src/app/(portal)/models/** is a private, authenticated route (the model catalog/discovery dashboard linked from the sidebar). A marketing page at the same path would collide with it, and sitemap-google-readability.test.ts catches exactly this (PRIVATE_ROUTE_PREFIXES in constants/private-route-prefixes.constants.ts includes /models). The cluster was built at /model-providers instead — same six children (openai, anthropic, google, deepseek, xai, local-ai), same registry category (ContentCategory.PROVIDERS), same AdEligibility.INELIGIBLE / FeedEligibility.PUBLISHABLE decisions. Every other detail in §3–§8 below is unchanged; read /models there as /model-providers.

F3 — RSS carries pages with no publication semantics

buildGlobalRssResponse maps every indexable registry page into the feed, so /terms, /privacy, /cookies, /acceptable-use and /contact are feed items whose pubDate is the site-wide review date. A subscriber is notified about the cookie policy.

Resolved: the feed filters to categories with genuine publication semantics. The mechanism is a category predicate, so the rule stays in one place and a new cluster opts in deliberately.

F4 — /use-cases and /features are single pages, not hubs

Both rank for one broad head term and cover many intents in one document. Each is the natural parent of a cluster and currently has no children.

Resolved: both become hubs, keeping their existing URL and improving rather than replacing them. No redirect, no lost equity.

/use-cases built 2026-09-09 — hub (unchanged URL, unchanged registry status) + 7 task pages: coding-and-development, research-and-fact-finding, writing-and-editing, comparing-model-answers, workspace-automation, structured-data-extraction, private-and-local-deployment. The plan named only "task pages" with no fixed list for this cluster; these 7 were chosen against real capabilities — PlanFeature (RESEARCH_MODE, COMPARE_MODE, JUDGE_MODE, CRITIC_REVIEW, CONTEXT_PACKS, MEMORY, WORKSPACES, WEB_EXTRACT), PaygSurface (CODING_AGENT, COMPARE, JUDGE, WORKSPACE_ACTION), WorkspaceProvider (14 connectors) and RoutingMode (LOCAL_ONLY, PRIVACY_FIRST) — not invented. writing-and-editing deliberately reuses a ModelFitTask string value: the two live at different route prefixes (/use-cases/* vs. /model-fit/*) so there is no collision, and the two pages answer different questions — /model-fit/* is "which model fits this task" (ad-INELIGIBLE, names and evaluates models), /use-cases/* is "how do I do this job with ClawAI" (ad-ELIGIBLE per §8.2, product-capability-oriented). Every task cross-links to /learn/*, /model-fit/* and /integrations rather than re-explaining a concept already covered by those clusters. The pre-existing legacy grid section (ten shorter jobs, marketing-use-cases.constants.ts) is unchanged; the new cluster is an additional "go deeper" section beneath it.

/features built 2026-09-09 — hub (unchanged URL, unchanged registry status) + 6 capability pages: model-routing-and-orchestration, memory-and-context, workspace-connectors, file-and-document-handling, observability-and-transparency, security-and-data-handling. The plan gave a starting split of 6 as a suggestion, not a fixed list; this split was verified against the actual code rather than applied as-is. It maps directly onto 6 of the 9 existing /features sections — providers is deliberately excluded (already its own /model-providers cluster) and generation is folded into file-and-document-handling rather than kept separate (it is one mechanism — image/document/research output — not a distinct capability area). routing and orchestration are merged into one page because RoutingMode (7 members: AUTO, MANUAL_MODEL, LOCAL_ONLY, PRIVACY_FIRST, LOW_LATENCY, HIGH_REASONING, COST_SAVER) and the PaygSurface.ORCHESTRATION "nine advanced orchestration labs" (Compare, Consensus, Escalation, Best-of-N, Repair, Verify, Role packs, Pipelines, Judge/Critic — confirmed against the marketing.features.orchestration.* and marketing.features.routing.* i18n keys) are one continuous mechanism: which model answers, and what happens when one model is not enough. Every other page is grounded the same way: memory-and-context in PlanFeature.MEMORY / PlanFeature.CONTEXT_PACKS; workspace-connectors in the 14 WorkspaceProvider members and PaygSurface.WORKSPACE_ACTION; file-and-document-handling in PaygSurface.FILE_GENERATION / PaygSurface.IMAGE and the marketing.features.files.* / marketing.features.generation.* i18n copy; security-and-data-handling in the literal mechanisms already shipped and described in marketing.features.security.* (Argon2, AES-256-GCM, TLS, RBAC, per-service database isolation) — explicitly not a compliance claim, mirroring that same i18n block's own "ClawAI holds no compliance certifications today" line. Every page cross-links to /learn/* rather than re-explaining a concept already covered there, and to /use-cases/* for the job-shaped version of the same capability. The pre-existing 9 sections on /features (FeaturesHeroSection, FeaturesProvidersSection, FeaturesRoutingSection, FeaturesOrchestrationSection, FeaturesMemorySection, FeaturesFilesSection, FeaturesWorkspaceSection, FeaturesGenerationSection, FeaturesObservabilitySection, FeaturesSecuritySection) are unchanged; the new cluster is an additional "go deeper" section (FeaturesCapabilityCardsSection) beneath them, same pattern as the /use-cases batch above.

Flagship pages added 2026-09-26 (full-AI-workspace repositioning, slogan "Every AI, one workspace") — 11 more capability pages on the same route and component: multimodal-ai, files-from-chat, smart-attachments, narrated-research-and-web-crawling, orchestration-labs, conversation-power-tools, read-aloud, image-generation, reliability, pay-as-you-go-credit, administration-and-access-control. Copy lives in constants/features-flagship-content/<locale>.constants.ts and is merged into FEATURES_CLUSTER_CONTENT_BY_LOCALE by mergeFeaturesClusterDictionary, so the original six pages' files are untouched. The flagship set has its own review date (FEATURES_FLAGSHIP_REVIEW_DATE, via getFeatureCapabilityReviewDate). Every claim was audited against shipped code first; the audit's "do not claim" list is binding on edits:

  • no public status page (none exists — observability stays admin-side);
  • no signed/expiring upload URLs — only generated files expire (1 hour, then a free rebuild);
  • no OCR, captcha solving or scraper sidecars as standard (off by default);
  • the provider breaker covers exhausted-credit accounts only, not outages;
  • no team workspaces, seats, invitations, SSO/SAML or SCIM — the administration page says "not yet" in every locale, and marketing.home.enterprise.point3Body no longer mentions SSO;
  • no plan-tier promises for gated features (image generation, read aloud, vision helper, labs, file-generation allowances, video length are DB rows), and no prices or top-up amounts (versioned price rows only);
  • pay-as-you-go credit is off until an operator enables it; gateways are PayPal and Paymob, not Stripe.

"15+ providers & smart routing", Coding Agent and local-first/privacy were NOT given new pages: they are already covered by /model-providers, /features/model-routing-and-orchestration, /coding-agent, /local-first-ai and /security-and-privacy, and a second page on the same topic would be thin duplicate content (rule 38). The homepage gained two positioning bands (HomeValueBandSection with MARKETING_HOME_PAYG_BAND and MARKETING_HOME_TEAMS_BAND) and the organisations band was reframed as local-first; the footer gained a Features column (MARKETING_FOOTER_FEATURE_PATHS, resolved server-side). Adding the pages raised RSS_GLOBAL_MAX_ITEMS to 4,000. Guarded by features-flagship-content.test.ts (discovery, real translations, audited omissions) and repositioning-slogan-keys.test.ts (slogan keys × 13 locales).

Video sections (2026-10-13). Twelve placements show video, all named in MarketingVideoPlacement and driven by ONE registry, MARKETING_VIDEOS (constants/marketing-videos.constants.ts): the "ClawAI Threads in 30 seconds" video on /features/threads, /threads/discover and the home Threads band; a neutral "Watch the ClawAI Threads video in three parts" strip on /features/threads; "See ClawAI in action" on the home page (three Shorts); the two "free AI" Shorts on /pricing, /supported-models and /model-providers; one open-source Short each on /local-first-ai, /how-it-works, /architecture and /integrations. A page adds <MarketingVideoSection placement={...} />.

What the copy may claim: we do not know what each clip narrates, so the self-hosted clips are described neutrally ("ClawAI Threads video, part 1 of 3") with no step story, and the Shorts show their YouTube title VERBATIM from the registry (never translated or paraphrased; the four same-title Shorts get #1 to #4 only to tell them apart). There are no spoken-language badges, because the audio language is unconfirmed; the Arabic locale puts Arabic-titled Shorts first by the title's script, not by a claim about the audio.

Self-hosted mp4s (MarketingVideo) are a click-to-load facade like the Shorts: a lazy poster and a play button in a reserved 16:9 box, so no <video> and no mp4 request exists before the click; click, Enter or Space swaps in <video controls autoplay preload="metadata"> and moves focus to it. They get VideoObject JSON-LD. The clips carry an audio track; muted stays only for the media-has-caption lint rule, and the owner supplies .vtt captions if the clips contain speech. YouTube Shorts (YouTubeShortFacade) are the same pattern over a local thumbnail, so there is no YouTube request before the click, and the CSP frame-src names only https://www.youtube-nocookie.com. next.config.mjs serves /marketing/videos/:path* with Cache-Control: public, max-age=31536000, immutable: files are replaced by a new name, never edited in place (no test covers next.config headers, so this is documented here). Copy is marketing.videos.* in marketing-videos-translations.ts (13 locales). Each section adds a heading and a line of text, so a page never becomes video-only (rule 38).

F5 — no topical clusters exist

Nothing covers the informational half of the funnel: what model routing is, what orchestration is, which model suits which task, what each connector actually does. Every existing page is bottom-of-funnel. This is the entire opportunity.

F6 — three proposed provider pages would be fabrications

/models/qwen, /models/kimi and /models/glm were proposed. ConnectorProvider has no such members; Qwen, Kimi and GLM exist only as /compare rivals (competitor products), and as open-weight models a user may run locally through Ollama. A provider page would claim a connector that does not exist.

Resolved: not built as provider pages. Their open-weight local story is covered honestly by /models/local-ai.

F7 — IndexNow absent built 2026-09-07

Deferred twice with a written reason (needs a hosted key file, a new env var, and the full 18-item infra propagation; Bing Webmaster manual submission covers the same ground), reaffirmed 2026-08-30, and reversed on 2026-09-07 once the operator generated a key in Bing Webmaster Tools.

Two of the three original costs turned out not to apply:

  • No env var, so no 18-item propagation. The key is a committed constant (constants/indexnow.constants.ts). It cannot be a secret — the protocol verifies ownership by fetching the same value back from a public URL — so hiding it in .env would have spread a published string across seven compose files and both installers, and introduced a way for the served file and the submitted key to drift per environment. That drift is answered with a flat 403 that names nothing.
  • The key file is a route, not hosted infrastructure. app/<key>.txt/route.ts serves the bare key and 404s when shouldNoIndexEverything() — a staging host must not claim ownership of the canonical origin, or its URLs could be submitted as the real site's.

What remains true is that manual submission covers the same ground. The reason to automate it anyway is timing: tools/indexnow/index.mjs runs after a successful production deploy, so a page edited at 14:00 is submitted at 14:00 rather than whenever someone next opens the console.

It submits changes, not the site. IndexNow answers repeated submission of unchanged URLs with 429 and reads it as spam, and the sitemap already publishes lastmod. The submitter walks the sitemap index, follows each child, and takes only entries whose lastmod falls inside a window (default 7 days). Undated and foreign-host entries are dropped — one foreign URL makes the endpoint reject the whole batch with 422.

Consequences worth knowing before reading the logs:

  • Most deploys will submit nothing, and that is correct. Marketing pages carry a hand-maintained lastReviewed, so they only qualify when their content genuinely changed. As measured on 2026-09-07: 0 URLs inside the 7-day window, 806 with the window opened to ten years.

  • A first-time seed is a deliberate, separate action. To submit everything once, run the submitter with the window widened:

    SITE_URL=https://claw-ai.co \
    INDEXNOW_KEY=<key> \
    INDEXNOW_WINDOW_DAYS=3650 \
    node tools/indexnow/index.mjs
  • A failure never fails the deploy. The step is continue-on-error and the script swallows its own errors: search-engine notification is not part of the deploy's contract, and a third party's 429 must not turn a healthy release red. Read the step's log for 403 (key file not served), 422 (a URL outside the host) or 429 (submitted too often).

F9 — the live site already contradicts the product (must fix)

Found while verifying the capability inventory. Three counts are written as prose into public-comparison-content/<locale>.constants.ts — thirteen copies each — and two are now wrong:

Claim on the site Reality in code Verdict
"Twelve workspace connectors" WorkspaceProvider has 14, all with adapters Wrong. Google Calendar and Outlook Calendar were added and never propagated
"Five routing modes" RoutingMode has 7, and ROUTING_MODE_LABELS renders all 7 in the product Wrong. The homepage collapses three router biases into one bullet; the count then under-reports what a user actually sees
"Nine frontier model families" ConnectorProvider has 5 cloud providers + 2 local runtimes Unverifiable as written. Nothing in the code produces the number nine

This is the drift the brief warns about, and it happened in the ordinary way: someone added a connector and had no reason to think a comparison page in Thai depended on the count.

Resolved: counts are derived, never typed. Content strings take a placeholder — '{connectorCount} workspace connectors' — and the count comes from the enum at render time, through the same interpolation the comparison pages already use for {rival} (utilities/public-comparison.utility.ts). A count can then only be wrong if the enum is wrong, and adding a connector updates 13 locales for free.

PRODUCT_COUNTS in the shared facts module owns every derived number.

F8 — no cannibalisation in the existing surface

Every current page owns a distinct intent. /compare/* (rival products) and the new /compare/models/* (model-vs-model) are different intents and must stay visibly different, which is the one real cannibalisation risk introduced here.


3. Source of truth for model facts

Superseded 2026-09-12. The source is now the live connector catalog, not a frontend constant.

GET /api/v1/internal/connectors/public-catalog, fetched server-side and read through selectModelsForProviderPage. It is built from the SAME query that fills the in-app model picker (findExposedForCatalog), so a page cannot advertise a model nobody can select, nor omit one users have.

Why the constant was deleted

constants/model-facts.constants.ts held 16 models across 5 providers, copied by hand from model-cost-seed.constants.ts and carrying a manual review date. The design below argued that was safer than crossing a service boundary. In practice it failed in the way hand-copied data always does:

  • It drifted from the product. The seed it mirrored is a PRICING list, not an availability list — this deployment actually exposes 170 models across 4 providers, and the page named 16.
  • It was not even the only copy. subscription-marketing.constants.ts held a SECOND roster for the home page that disagreed with it — naming Moonshot Kimi, Zhipu GLM, Alibaba Qwen and Amazon Bedrock (none of which can serve a model here), plus MiniMax and NVIDIA models that appear nowhere in this codebase. Its test asserted those names, so the fiction was load-bearing.
  • Nothing could catch either. Both were internally consistent and fully typed. The drift detector D1 below describes a manual check that was never run.

The boundary concern the original decision raised was real, and is answered rather than ignored: the endpoint is service-token guarded, the token never reaches a browser, and the payload carries no price, no rate and no connector identity (rule 37). The qualitative-cost rule survives — the backend simply does not send a number for a page to leak.

What still holds

MODEL_PAGE_CONNECTOR_PROVIDERS maps each /model-providers/<page> to the connector providers behind it, because the two vocabularies are different: ConnectorProvider names an adapter, ModelProviderPage names a page, and local-ai is deliberately one page over two runtimes.

The editorial copy per provider — positioning, strengths by task, FAQ — is unchanged and still lives in constants/models-content/<locale>.constants.ts in 13 locales. Only the MODEL LIST moved. A failed catalog fetch renders that copy with an "unavailable" note rather than an invented list.

The original design, for the record

Rules:

  • Dated. MODEL_FACTS_REVIEW_DATE renders on every consuming page and drives dateModified. Moving it without re-checking the claims is prohibited.
  • Qualitative, never benchmarked. No invented scores, no fabricated latency, no leaderboard positions.
  • One copy. A page that needs a model fact imports it. A second hardcoded list anywhere is a defect.
  • Provider families are the honest unit. Where a claim would need a version number to be true, it belongs to the family, not the page.

4. URL architecture

Existing URLs are unchanged. Everything below is additive.

Hub Children Intent
/learn concept pages (what-is-*, cloud-ai-vs-local-ai, ollama-vs-llamacpp, …) Informational
/model-providers openai, anthropic, google, deepseek, xai, local-ai (built at /model-providers, not /models — see note below) Commercial investigation
/compare/models model-vs-model pairs Comparison
/model-fit task pages (coding, complex-reasoning, writing-and-editing, research-with-sources, private-local-workloads) — built at /model-fit, not /best-ai-model//models/for — see note below Commercial investigation
/integrations 14 verified connectors Commercial investigation
/use-cases (exists) task pages Commercial investigation
/features (exists) capability pages Commercial investigation
/solutions role pages Commercial investigation
/industries private-deployment verticals Commercial investigation
/prompts prompt guides Informational
/tools model selector Transactional-adjacent

Cannibalisation rules, decided once:

  • /best-ai-model/coding is the single canonical page for "best AI/LLM/model for coding/programming". /learn/best-ai-model-for-coding is not built.
  • /use-cases/* targets the task; /solutions/* targets the role. A role page that would only restate a task page is not built.
  • /compare/* is product vs product; /compare/models/* is model vs model. Different H1 shapes, different tables, cross-linked.
  • /learn/* defines a concept; /features/* documents ClawAI's implementation of it. Every learn page links to its feature page and vice versa; neither duplicates the other's job.

5. Implementation batches

Each batch is independently gated, committed and pushed.

Batch Contents Priority
0 This document, retire the four guides/* PLANNED slugs, shared SEO components, cluster scaffolding, RSS publication-semantics fix P0
1 /learn hub + concept pages P0
2 /integrations hub + 14 connector pages, retire the workspace-connectors PLANNED slug P0
3 Built 2026-09-09 — /model-providers hub (not /models, see note below) + 6 provider pages, MODEL_FACTS P0
4 Built 2026-09-09 — /use-cases hub (unchanged URL) + 7 task pages, see F4 note above P1
5 /model-fit half built 2026-09-09 — hub + 5 task pages (was /best-ai-model, not /models/for, see note below). /compare/models still outstanding P1
6 /features hub + capability pages P1
7 /solutions + /industries P2
8 /prompts + /tools P2
9 Internal-link sweep, nav/footer clusters, final verification P0

Definition of done for every page, enforced by the four coverage tests plus Lighthouse: route resolves under /{locale}/; registry entry PUBLISHED + REVIEWED + INDEXABLE; 13 locales of SEO metadata and body copy; URL in lighthouserc.json; reachable from footer or hub; exactly one h1; breadcrumbs with BreadcrumbList; no fabricated claim.


6. Refused deliberately

Recording these so they are not re-proposed as oversights.

Refused Why
/model-providers/qwen, /model-providers/kimi, /model-providers/glm No such connector exists (F6)
/model-providers/amazon-bedrock Scaffolding only; already excluded by /supported-models (F2)
/learn/best-ai-model-for-coding Same intent as /best-ai-model/coding (§4)
Benchmark tables No trustworthy first-party data; fabrication is forbidden
Compliance claims (SOC 2, ISO 27001, HIPAA, FedRAMP, GDPR certification) Not held. Industry pages say "designed for private deployment"
/tools/local-ai-hardware-calculator Requires hardware data the repo does not have; a guessed recommendation is worse than none
IndexNow Built 2026-09-07 — the deferral in F7 was reversed once a key existed
Indexable tool permutation URLs Combinatorial thin content; the tool is one canonical page

7. Verified capability inventory

Every factual claim on a new page must trace to a row here. Each was read out of the code on 2026-08-30, with the file that proves it. Nothing on a public page may assert a capability absent from this table.

Cloud model providers — ConnectorProvider

packages/shared-types/src/enums/connector-provider.enum.ts

Provider Adapter Seeded models Public page
OpenAI yes gpt-5, gpt-5-mini, gpt-4o, gpt-4o-mini, o3, o4-mini /models/openai
Anthropic yes claude-opus-4, claude-sonnet-4, claude-haiku-4-5 /models/anthropic
Google Gemini yes gemini-2.5-pro, gemini-2.5-flash, gemini-2.5-flash-lite /models/google
DeepSeek yes deepseek-chat, deepseek-reasoner /models/deepseek
xAI Grok yes grok-4, grok-3-mini /models/xai
Ollama yes open-weight, operator-chosen /models/local-ai
llama.cpp yes open-weight, operator-chosen /models/local-ai
AWS Bedrock scaffolding only none no page

Model list from apps/claw-routing-service/src/modules/router-models/constants/model-cost-seed.constants.ts — the 16 models the product actually prices.

Routing modes — RoutingMode (7)

packages/shared-types/src/enums/routing-mode.enum.ts

AUTO · MANUAL_MODEL · LOCAL_ONLY · PRIVACY_FIRST · LOW_LATENCY · HIGH_REASONING · COST_SAVER

The nine orchestration labs — TokenLedgerContext → PaygSurface.ORCHESTRATION

apps/claw-chat-service/src/modules/chat-messages/constants/payg.constants.ts

REPAIR · VERIFY · CONSENSUS · ESCALATION_CHAIN · BEST_OF_N · COST_ENSEMBLE · ROLE_PACK · PIPELINE · TASK_DECOMPOSITION

Judge and Compare are separate surfaces (PaygSurface.JUDGE, .COMPARE), not labs. COST_ENSEMBLE, ROLE_PACK and TASK_DECOMPOSITION were not in the original brief and are genuine uncovered opportunities.

Plan-gated features — PlanFeature

packages/shared-types/src/enums/plan-feature.enum.ts

COMPARE_MODE · JUDGE_MODE · RESEARCH_MODE · WEB_SEARCH · WEB_FETCH · WEB_EXTRACT · CRITIC_REVIEW · WORKSPACES · MEMORY · CONTEXT_PACKS

Workspace connectors — WorkspaceProvider (14, all with adapters)

packages/shared-types/src/enums/workspace-provider.enum.ts, adapters verified in apps/claw-workspace-service/src/**/adapters/

GitHub · GitLab · Bitbucket · Slack · Jira · Confluence · Figma · ClickUp · Google Drive · Gmail · SharePoint · OneDrive · Google Calendar · Outlook Calendar

Google Calendar and Outlook Calendar were absent from the brief; both have real adapters and get pages.

Other paid surfaces — PaygSurface

CHAT · COMPARE · JUDGE · ORCHESTRATION · IMAGE · FILE_GENERATION · CODING_AGENT · WORKSPACE_ACTION · ROUTING

Research is deliberately not a paid model surface — it reaches search providers and is metered as WEB_SEARCH / WEB_FETCH / WEB_EXTRACT. A page claiming "research uses your model credit" would be wrong.


8. Plan-time boardroom review — three vetoes, and what they changed

Reviewed 2026-08-30 before any page was written. Business-owner, CTO and operations lenses each returned VETO. Every finding below is resolved in the plan rather than deferred, because a veto at plan time is cheap and the same veto after eight batches is not.

8.1 Claim liability — the plan would have published unsourced claims

MODEL_FACTS may not carry speed. The only in-repo source for latency is apps/claw-routing-service/src/modules/router-models/constants/cloud-model-intelligence.constants.ts, whose own header attributes latency classes to "ClawAI's router benchmark suite (see qa/routing-latency-baseline/)". That directory does not exist. Publishing a speed band in 13 languages citing a benchmark suite that is not in the repository is a fabricated claim with a fabricated citation.

→ Speed is not published. Not qualitatively, not at all, until a real measurement exists.

That file must not be a source at all. It carries unsourced disparagement of named third-party products — avoidFor: ['high_reasoning', 'legal_drafting'] for GPT-4o-mini, weakDomains for GPT-4o. Those are internal routing heuristics. Published, they are trade-libel exposure in 13 languages. It also lists mistral-large-latest, command-r-plus, qwen-max and moonshot-v1-128k — providers with no ConnectorProvider member — which would silently undo F6.

→ MODEL_FACTS is grounded only in model-cost-seed.constants.ts plus vendor documentation. Every entry carries a source and a vendor URL. A fact with no vendor URL is not published.

Naming a model is an entitlement claim. /pricing already refuses it in as many words: "this page does not promise a specific model on a specific tier. Confirm the live catalog before choosing a plan for one model." docs/business/plan-allowances.md records a prior incident where one page overstated the enforced allowance fifteen-fold. This plan would multiply that claim surface from one page to ~1,300 documents, and docs/business/rollout-and-notice.md discharges allowance-change notice on the pricing page — a mechanism that cannot work once the claim lives in 1,300 places.

→ Every page naming a model carries the same "confirm the live catalog" qualifier, in all 13 locales, and links to /pricing. No page pairs a model name with a signup CTA implying that model is included in a given tier.

8.2 Pages that changed shape or will not be built

Was planned Now Why
/industries/healthcare, /industries/financial-services, /industries/government Not built An industry page addressed to a regulated buyer is an implied offer of a contractual instrument. public-launch-content/en.constants.ts records that ClawAI offers no data-processing agreement. HIPAA needs a BAA; "designed for private deployment" does not discharge one. Blocked pending a regulated-vertical claims decision in docs/business/ (not written yet)
/industries/* generally Hub over unregulated verticals only Same reasoning; agencies, software teams, research and education carry no implied instrument
/compare/models/gpt-vs-claude (model vs model) /compare/models/* reframed: "how ClawAI's router chooses between them" Comparative advertising in de fr it es pt must compare verifiable material features objectively (Dir. 2006/114/EC art. 4; German UWG §6 is competitor-enforced). A page comparing two products we sell neither of, with benchmarks refused, substantiates nothing. A claim about our own routing behaviour is substantiable
/best-ai-model/coding /model-fit/coding — "Choosing a model for coding" (see note below — not /models/for/coding, the path this row originally named) "Best" is an unsubstantiated superlative about third-party products, and §6 refuses benchmarks, so it is unsubstantiable by construction. Same intent, same traffic, no superlative
"14 verified connectors" "14 connectors", copy generated from the provider registry 8 of 14 have webhooks: false (three were flipped from true because they were lying); Bitbucket and Jira webhook verifiers are no-op stubs returning signatureValid: true; both calendars support exactly one write action. "Real-time sync" and "signature-verified" are false for specific connectors

/model-fit route note (batch 5, built 2026-09-09), the "see note below" this table points to. The row above originally read /models/for/coding. That collides for the identical reason the model-providers cluster moved off /models in batch 3 (commit efed5669f): PRIVATE_ROUTE_PREFIXES (constants/private-route-prefixes.constants.ts) blocks any path whose string starts with /models — the authenticated model catalog dashboard at src/app/(portal)/models/** — and /models/for/coding starts with /models. /models-for/* was considered next and rejected too: it also starts with the literal string /models, so PRIVATE_ROUTE_PREFIXES's prefix match (path.startsWith(prefix)) blocks it exactly as hard. The cluster ships at /model-fit instead — /model-fit/coding, /model-fit/complex-reasoning, /model-fit/writing-and-editing, /model-fit/research-with-sources, /model-fit/private-local-workloads — which shares no prefix with the private route. Five task pages, not the "coding, reasoning, writing, …" placeholder list in §4's table: the user's approved backlog for this cluster specifies coding, complex reasoning, writing and editing, research with sources, and private/local workloads.

Ad eligibility, decided rather than defaulted. /compare/* is INELIGIBLE on recorded reasoning — "a page whose job is to be a fair, checkable comparison of named competitors does not also carry ad inventory." That reasoning applies verbatim to every new cluster that names a third party.

→ INELIGIBLE: /compare/models/*, /models/*, /model-fit/* (built at this path instead of the originally-planned /models/for/* — see the route note above; the ad-ineligibility reasoning is unchanged), /integrations/*, /industries/*. ELIGIBLE: /learn/*, /use-cases/*, /features/*, /solutions/*. Publishing ~100 pages at once ad-eligible is also an AdSense scaled-content exposure.

/prompts ad-eligibility (batch 6, built 2026-09-09) — the decision this table's row above (/prompts + /tools, P2) did not itself make. §4 lists /prompts with informational intent and this table names the P2 build order, but neither states an AdEligibility value for it the way this section does explicitly for every other cluster. Decided here, applying the same test the rest of this section uses (does the page name and evaluate a third party, or does it explain a technique): /prompts/* explains prompt-writing technique — clarity, examples, step-by-step reasoning, structured output, system vs. user prompts, iterating on a result, and how the approach shifts by task type — and names no third-party AI product anywhere in its copy. That is the /learn/* shape, not the /compare/* or /model-fit/* shape, so /prompts/* is ELIGIBLE, same reasoning and same category (ContentCategory.GUIDE) as /learn/*. FeedEligibility.PUBLISHABLE, also matching /learn/*: evergreen editorial content with genuine publication semantics.

reviewDate is mandatory for every cluster naming a third party, not just /compare/*. A comparison without a visible date is a claim with no expiry.

8.3 Architecture — decisions that were being made implicitly

Fork Decision
Fan-out shape at 100 pages ADR-084. The existing comparison cluster fans out only one of five layers (registry config). Slug enum, SEO copy, route file and Lighthouse URL are all hand-written ×9. At 100 pages that is 100 enum members, 1,300 SEO blocks, 100 route files. Resolved as a dynamic segment per cluster (/learn/[topic] + generateStaticParams off the cluster order array)
PUBLIC_PAGE_SEO_BY_LOCALE Pluggable. 168 KB / 2,391 lines today for 28 slugs; ~770 KB and ~10,900 lines at 128 — the exact shape TypeScript checks superlinearly, and every batch would edit the same file. Each cluster owns its SEO copy beside its body copy; buildLocalizedMetadata resolves through a registry of providers
Feed eligibility A field on the definition, not a category predicate. The registry already answers "which surfaces does this page appear on" per definition (indexability, adEligibility). Keying on ContentCategory would mean two pages in one category can never differ, and adds a second way to express the same kind of fact
Lifecycle RETIRED added. There is currently no way to un-publish. Rolling back a batch deletes the routes and turns every already-crawled URL into a soft 404 across 13 locales at once — worse than the bad page. The point of no return is the first successful production deploy, not the merge

8.4 Operational ceilings — measured, not estimated

Lighthouse CI timings from four consecutive real runs: 13.9 s per audit, ±5 s. 28 URLs × numberOfRuns: 2 = 56 audits = 13 min today.

Cumulative URLs Audits Lighthouse step
28 (today) 56 13 min (measured)
61 122 ~28 min
128 (end state) 256 ~59–77 min

lighthouse-coverage.test.ts asserts the audited set equals the indexable set in both directions, so this growth is mandatory — there is no sampling escape hatch — and categories:best-practices at minScore: 1 means one bad audit in 256 fails the run. The workflow has no concurrency: group and no timeout-minutes.

→ Batch 0 adds both guards, repairs docs/09-testing/lighthouse-ci.md (which already says "eight published public pages" and 0.95, against a reality of 28 and 1), records the minutes ≈ URLs × 0.46 formula, and caps /compare/models explicitly — C(16,2) = 120 pairs are possible and nothing bounded it.

RSS_GLOBAL_MAX_ITEMS = 2000 truncates silently, and the truncation eats pages rather than chats: global-rss.service.ts sorts by publishedAt descending before slicing, page items carry a fixed lastReviewed while chat items carry live timestamps, so every chat sorts ahead of every page. Two documents already compute the ceiling from "13 locales × 16 pages" and are stale at 28.

→ Batch 0 fixes the arithmetic, the comments, and adds a test that fails before the ceiling is reached.

Not affected, asserted rather than assumed: nginx needs no change (the catch-all location / is declared last, and these are not API paths); no new env vars; no compose changes; SITEMAP_URL_CHUNK_SIZE = 40_000 is three orders of magnitude clear. Build time is a non-event (~36 s → ~90–150 s) because every marketing page reads headers() and renders dynamically, so locale is not a build multiplier.

8.5 Debt accepted, with triggers

# Debt Trigger to repay
D1 MODEL_FACTS duplicates a routing-service constant across a workspace boundary with no drift detector A model added to or removed from MODEL_COST_SEED, or MODEL_COST_SEED_VERSION bumped
D2 The footer's explorePages is a deny-list; every new page joins one site-wide column Inverted to opt-in in Batch 0, not Batch 9 — seven batches would otherwise pass Lighthouse on a footer degrading each time
D3 /llms.txt partitions by deny-list too, so all ~100 new pages flatten into one "Product" section Same Batch 0 change
D4 Retiring PLANNED slugs empties PLANNED_CONTENT_CONFIGS, leaving ContentLifecycleStatus.PLANNED with zero instances and its fallback branch untested Decided in Batch 0/2: retire only the colliding slugs (the four guides/* in Batch 0, plus workspace-connectors in Batch 2 once /integrations shipped), keep the rest
D5 CI cannot detect a production SITE_URL misconfiguration — lighthouse.yml sets its own Post-deploy verification step added to §5

8.6 The boundary the CTO explicitly upheld

MODEL_FACTS as a frontend constants module was challenged and confirmed correct. Crossing to routing-service's live catalog would require a new unauthenticated public endpoint exposing precisely what that service refuses to expose (a full provider rate card is a margin input); it would make marketing pages vary per deployment (a self-hoster with no OpenAI key rendering an empty /models/openai that Google has already indexed); and it would put a runtime frontend→routing-service dependency on the SEO critical path, where an outage empties indexed pages.


9. Batch 9 — final status: every cluster, its ACTUAL route, and its commit

Verified 2026-09-09 against the live registry, lighthouserc.json and the codebase — not against the originally-planned URLs in §4. Every deviation recorded in a "Deviation"/"note" callout above is now DONE and reconciled here in one place.

Batch Cluster Planned route (§4) Actual route shipped Hub + children Commit Status
0 Infra (RSS fix, footer opt-in, llms.txt sections, retired 4 guides/* PLANNED slugs) — — — 2bd88f5cc DONE
1 /learn /learn /learn (unchanged) hub + 30 topics (not 12/18 as informally reported elsewhere — verified by reading LEARN_TOPIC_ORDER) 014eec326 DONE
2 /integrations /integrations /integrations (unchanged) hub + 14 connector topics 014eec326 DONE
3 /models /models /model-providers — /models collides with the private, authenticated src/app/(portal)/models/** catalog dashboard; PRIVATE_ROUTE_PREFIXES blocks any path starting with /models (F2, §8.2) hub + 6 providers (openai, anthropic, google, deepseek, xai, local-ai) efed5669f DONE
4 /use-cases /use-cases /use-cases (unchanged URL, converted single page → hub) hub + 7 task pages 502fcc80d DONE
5 /best-ai-model (or /models/for/*) /best-ai-model /model-fit — /models/for/* and /models-for/* were both considered and rejected: both start with the literal string /models, so PRIVATE_ROUTE_PREFIXES's path.startsWith(prefix) match blocks them exactly as hard as /models itself (§8.2 route note) hub + 5 task pages (coding, complex-reasoning, writing-and-editing, research-with-sources, private-local-workloads) 62feb4aeb DONE
6 /features /features /features (unchanged URL, converted single page → hub) hub + 6 capability pages 3350f5c5b DONE
8 /prompts /prompts /prompts (unchanged) — ad/feed eligibility decided explicitly in §8.2 (ELIGIBLE / PUBLISHABLE, same reasoning as /learn/*) hub + 7 prompt-writing guides a9ecd6cd2 DONE
7 /solutions, /industries — Not built — /industries/* blocked pending a regulated-vertical claims decision in docs/business/, not written yet (§8.2); /solutions has no commit in this project's history — — NOT BUILT
8 /tools /tools Not built — no commit in this project's history — — NOT BUILT
9 Internal-link sweep, nav/footer clusters, final verification — — — (this verification pass; no code changes were required) DONE

Lighthouse coverage, counted from lighthouserc.json directly (2026-09-09): 28 pre-existing marketing pages + /learn (31) + /integrations (15) + /model-providers (7) + /model-fit (6) + /prompts (8) + /use-cases children (7, hub already counted in the pre-existing 28) + /features children (6, hub already counted in the pre-existing 28) = 108 URLs total, matching docs/09-testing/lighthouse-ci.md after this batch's correction (it previously read 74, stale since /model-providers, /model-fit, /use-cases, /features and /prompts landed after that count was written).

Footer verification: MARKETING_FOOTER_EXPLORE_PATHS (constants/marketing-footer.constants.ts) is confirmed opt-in (D2, resolved in Batch 0) and alphabetically ordered, carrying all 6 hub roots from this project (/features, /integrations, /learn, /model-fit, /model-providers, /prompts, /use-cases) alongside the pre-existing entries. No footer edit was needed — the /model-fit and /prompts additions made by the orchestrating session were already correct and complete. Every cluster's children are reached from their hub's own internal links (the /learn reference pattern: a hub component calls a build*HubCards / build*Siblings utility off the cluster's *_ORDER array), not from individual footer entries — confirmed consistent across all 6 clusters.

Cross-link and stale-path audit: spot-checked cross-links (/prompts/prompting-for-structured-output → /learn/what-are-structured-ai-outputs, /use-cases/private-and-local-deployment → /model-providers/local-ai, /features/model-routing-and-orchestration → /learn/what-is-llm-orchestration) all resolve to real registry slugs. A repo-wide search for the pre-rename /models/openai-style and /models/for/* paths found none — every reference to /models in src/ is either the private portal route (topbar, sidebar, breadcrumb) or an explanatory comment about the rename decision, not a stale marketing link.

Ad/feed eligibility, verified against the registry (not assumed): /model-providers/* and /model-fit/* are AdEligibility.INELIGIBLE; /use-cases/*, /features/*, /learn/* and /prompts/* are AdEligibility.ELIGIBLE. /prompts was verified against git show a9ecd6cd2 and the registry rather than assumed — it is ELIGIBLE / FeedEligibility.PUBLISHABLE, the /learn/* shape, per the explicit decision in §8.2.

9.1 /compare/models — the reframed model-vs-model cluster, built

Built 2026-09-10, closing the row in §8.2's table ("/compare/models/gpt-vs-claude (model vs model)" → "/compare/models/* reframed"). Ships at /compare/models — a hub plus six pair pages, nested under the existing /compare route rather than a new top-level path, since the reframed claim ("how ClawAI's own router chooses between two families") is still fundamentally a comparison page and belongs with its siblings.

Capping decision. §8.4 warned nothing bounded /compare/models: a full model-level matrix is C(16,2) = 120 pairs; even capped to the 5 cloud provider families in ModelProviderPage it is C(5,2) = 10 pairs, and 11 with local-ai folded in as an eleventh "family". This batch builds 6 pairs, capped at family level (never individual models), chosen editorially rather than exhaustively:

Pair Why it is in the set
openai-vs-anthropic The two most consistently searched cloud families; also the most commonly conflated in support questions
openai-vs-google Completes the three-way "big cloud" triangle with the pair above
anthropic-vs-google Completes the triangle; the two cost-tier shapes differ enough to be worth a page (Anthropic has no CHEAP-class seeded model, Google does)
openai-vs-deepseek The cost-tier contrast pair — DeepSeek's entire seeded catalog sits in STANDARD, worth explaining against OpenAI's wider CHEAP→PREMIUM range
openai-vs-xai Covers the fifth and smallest cloud catalog (XAI, 2 seeded models) against the largest (OPENAI, 6)
cloud-vs-local Not a named-vendor pair — cloud (any of the five families) against Ollama/llama.cpp. Given ClawAI's local-first positioning, this is the single most important pair in the cluster, and it is structurally different from the other five: the question is where a request runs, not which vendor answers it

Deliberately not built in this batch: anthropic-vs-deepseek, google-vs-deepseek, anthropic-vs-xai, google-vs-xai, deepseek-vs-xai — the remaining 5 of the 10 possible cloud-family pairs. No claim-liability reason blocks them; they were simply lower search-intent priority than the six above. Trigger to add one: real query volume evidence, or a support pattern showing users actually confusing two of these specific families.

Grounding. Every cost-class claim (CHEAP/STANDARD/PREMIUM/ULTRA, rendered as "budget"/"standard"/"premium"/"highest") is read directly from constants/model-facts.constants.ts (itself diffed against model-cost-seed.constants.ts — see that file's own grounding rules, reused here rather than re-verified independently). Every routing-mode claim (Auto, Manual Model, High Reasoning, Cost Saver, Local-Only, Privacy-First) is a real member of packages/shared-types/src/enums/routing-mode.enum.ts. No page in this cluster claims a speed/latency difference between named vendors, matching every other cluster's refusal (§6) — the locale-completeness test (compare-models-content-locale-completeness.test.ts) asserts this mechanically in all 13 locales, and separately asserts no locale's translation implies a winner via "best"/"better than"/"superior to"/ "outperforms".

Router-behaviour framing, not a product ranking. Per §8.2, no page declares a winner between the two named families, not implicitly and not by ordering — every page's productNote and FAQ entries were written to state what ClawAI's own router weighs (cost class, routing mode fit, whether a workload must stay local) rather than which vendor is "better". Verified by: (1) the locale-completeness test above running in all 13 locales, (2) a manual re-read of the English source and every FAQ/productNote field before translation, (3) spot-checking two non-English locale files for the same property after each translation agent's self-report.

Not done in this batch: no link was added from the existing /compare hub page to the new /compare/models hub. ComparisonHubContent (a 13-locale content type) would need a new field to carry that link's copy, and this batch judged that scope not worth the risk of touching another cluster's already-shipped locale content. The new hub is fully discoverable today — registered in the content registry, sitemap, llms.txt and Lighthouse CI — and every pair page cross-links out to /model-providers/*, /model-fit, and /learn/*. Trigger to revisit: if search-console data shows /compare/models under-discovered relative to its sibling clusters.

10. Public Threads board (2026-10-14)

The guest-facing Threads board is registry slug threads/board, path /threads/board, in all 13 locales (ADR-178 addendum). It follows the cluster rules above rather than adding a new mechanism:

  • Registry and sitemap. One content-registry.constants.ts entry (category GUIDE, AdEligibility.INELIGIBLE, FeedEligibility.PUBLISHABLE, review date 2026-10-14). The sitemap, hreflang alternates (13 locales plus x-default = English) and llms.txt all read the registry, so no sitemap code changed. Guarded by sitemap-coverage.test.ts (a route on disk for every published path and the reverse) and content-registry.utility.test.ts.
  • Metadata. Title and description come from chat.threadPublicBoardSeoTitle and ...SeoDescription in thread-public-board-translations.ts; threads-public-board-seo.constants.ts derives the registry copy from that single source and is added to CLUSTER_SEO_SOURCES. buildPublicPageMetadata(threads/board, locale) yields the self-canonical, hreflang, OpenGraph and Twitter tags and index,follow. The SEO title carries no brand suffix: the root metadata template (%s | ClawAI) adds it, and a suffix in the copy made the <title> read "... | ClawAI | ClawAI". Thread URLs (canonical, hreflang, sitemap, RSS) use one percent-encoded slug segment (threadCanonicalUrl), so Arabic, Persian, Hindi and Japanese slugs are advertised in the same form the page serves. /sitemaps/<locale>/all-threads-N.xml also lists Threads of every other language under /<locale>/threads/<slug>; the page canonicalises to the content-language URL, so these are discovery aids and not duplicates (ADR-182).
  • Structured data. buildPublicBoardJsonLd (structured-data.utility.ts) emits a CollectionPage whose mainEntity is an ItemList of the first page of Threads (name and canonical /<locale>/threads/<slug> URL only, numberOfItems = total) plus a BreadcrumbList. No rating, author or private field.
  • Rule 38 (low-value content). The page is a listing of content that already has its own indexable page, so the risk is thin content. It carries a heading and real introductory copy (threadPublicBoardIntro, at least 120 characters in every locale, tested) that says what is on the board, how it is vetted and what a guest can and cannot do; every card links to a full Thread; the page is ad-ineligible. The canonical of every Thread stays /threads/<slug>.
  • Member Board stays out. /board is in PRIVATE_ROUTE_PREFIXES (robots disallow for every locale), noindex and absent from the sitemap. robots.test.ts pins that /board is blocked and /threads/board is not.
  • Audit lists. lighthouserc.json, lighthouserc.pr.json and apps/claw-frontend/scripts/responsive-audit/audit.cjs include the page; lighthouse-coverage.test.ts fails if an indexable page is missing from the Lighthouse set.

Clone this wiki locally