Blog — SEO/GEO Content System
Overview
The blog at/blog is powered by Sanity CMS (project htd46ya1, single
dataset production). Content lives only in Sanity: posts are authored as
drafts via the Sanity MCP (or the Studio), a human reviews and publishes
in the Studio, and a Sanity webhook revalidates the site and pings IndexNow.
Non-production deployments read the same dataset with the drafts
perspective, so drafts preview there tagged with a Draft badge.
There is no git-side content pipeline. The former HTML/manifest/CI publishing
system (publish scripts, content/blog/, the publish workflow, Supabase
image staging) was removed deliberately — do not recreate any part of it.
The authoring contract for agents is
.agents/skills/author-blog-posts/SKILL.md (draft-first workflow, schema
shapes, document-id rules, FAQ guidance). Read it before creating or editing
posts.
Content validation (mandatory before a draft is handed to review)
- Humanizer — no banned AI words, no em dashes, no bold-header-colon patterns
- Compliance — no buy/sell/hold advice, no guaranteed-return language
- Formatting — no raw URLs, no exclamation marks
What NOT to touch
studio/schemaTypes/postType.ts— Sanity post schema (schema changes require a Studio redeploy)components/blog/blog-portable-text.tsx— Portable text renderer- Blog route structure (
app/(public)/blog/)
File structure
SEO rules
- Canonical URLs use the www host (
https://www.cfosilvia.com) — the apex redirects to it - Document
_ids must never contain a dot — dotted “path” ids are invisible to unauthenticated reads and silently blank the public blog - JSON-LD: Article + Breadcrumb on every post; FAQPage when
faqis authored (rendered visibly too — Google requires visible FAQ content) - Internal links: 2-3 related articles per post (same category preferred)
- robots.txt: AI crawlers ALLOWED (GPTBot, ClaudeBot, PerplexityBot) — this is
intentional for GEO;
/llms.txtserves a crawler index - Sitemap auto-includes all published posts
- IndexNow: fired automatically by
/api/revalidate/sanityfor indexable slugs
Security rules
- NEVER commit Sanity tokens, API keys, or .env files
- Agents author drafts; production publishing is a human action in the Studio unless the user explicitly authorizes otherwise
- IndexNow endpoint requires Bearer token auth
Environment variables
Modulus design system (second system, in progress)
A second design system is being built alongside the existing one, for the landing and product A/B test. It lives incomponents/modulus/ and is
namespaced Modulus/* in Storybook. Until the test concludes, both systems
must render correctly and independently.
Frozen. Do not edit these files.
app/themes.css— staging-frozen perdocs/THEMING.md. The live dark-theme experiment keys its rules oncontrol/dark-variant-bhere, so an edit can corrupt a running test.app/globals.css— staging-frozen; also holds the nine.sam-*utility classes, which are unscopedtailwind.config.tsundertheme.screensandtheme.fontFamily, both of which replace Tailwind defaults rather than extending them. Existing components rely onxs,desktopand2xl, and onfont-sans.
Being retired. Deleting is allowed, editing still is not.
components/ui/**— the pre-Modulus component library- Any existing
*.stories.tsxoutsidecomponents/modulus/
docs/MODULUS-CLEANUP-PLAN.md: remove a module
that has no importers, or migrate its call sites to the Modulus equivalent and
then remove it. A story is deleted together with the component it covers.
Editing one is still a stop-and-ask. Do not restyle, extend, or add a
variant to anything in here. Measured on 2026-09-18: 19 files under
app/(public) still import components/ui, only 12 of its 64 modules have a
Modulus counterpart at all, and the landing A/B test is still running. So these
components still paint live surfaces, and changing one reaches all of them at
once — which is exactly what the original blanket freeze was protecting.
What changed, and what did not. The freeze read “read-only, do not edit”
because both design systems had to coexist while the product A/B test ran. The
in-app half of that is over: app/layout-with-sidebar-client.tsx stamps
data-theme-variant='modulus' unconditionally, with no flag, so inside
app/(app)/** there is no control arm left to protect. The landing and the
public routes are a different story and are not settled, which is why this is a
narrowing of the rule rather than its removal.
components/modulus/** is the new system and is not part of this cleanup —
including the components that are built but not yet adopted. Do not delete
something there for being unreferenced.
Additions must be scoped, never global
- New CSS variables go under the agreed
data-theme-variantselector in a new stylesheet, never in:rootand never inthemes.css. Bare:rootdeclarations would restyle the existing components, since names such as--surface-card,--surface-hoverand--surface-selectedalready exist. - New Tailwind keys carry a
modulus-prefix. Tailwind keys are global regardless of CSS scoping, andsurface(26 keys) andctaalready exist. - New Storybook stories are titled
Modulus/*.Components/*,Navigation/*andUI/*are taken. - Do not enable autodocs globally. It is off repo-wide, and enabling it would generate docs for all 895 existing story entries across 221 files. Opt in per story instead.
Notes
- Opacity tokens carrying
com.modulus.cssTransform: divideBy100in their$extensionsmust be divided by 100 for CSS. Figma stores opacity as percent. Without the transform, disabled controls render fully opaque. .storybook/preview.tsximportsglobals.cssthenthemes.css, and the order is load-bearing. Any new stylesheet must load after both.