How to Structure FAQs in a Headless CMS So Both Humans and AI Can Read Them
Your FAQ page ranks fine for humans. Then a support assistant built on the same content confidently tells a customer the wrong return window, or an answer engine quotes a policy you retired two quarters ago.
Your FAQ page ranks fine for humans. Then a support assistant built on the same content confidently tells a customer the wrong return window, or an answer engine quotes a policy you retired two quarters ago. The FAQ looked correct on the rendered page, but the machine reading it got a wall of HTML, a stale copy pasted into a vector store, or a retrieval layer that matched on the wrong keyword. One source, three consumers, and only one of them was ever considered.
The fix is not a second FAQ built for robots. Sanity is the Content Operating System for the AI era, an intelligent backend that treats an FAQ as what it actually is: structured content with a question, an answer, a category, and references, modeled once and serialized to every consumer that needs it. When the structure lives in the content model rather than in the markup, the human page, the FAQPage JSON-LD, and the assistant's retrieval query all draw from the same governed source.
This guide walks through modeling FAQs as schema, serializing a single answer to both HTML and machine-readable markup, and wiring the hybrid retrieval that lets an assistant answer from them without hallucinating. The goal is one FAQ that reads correctly whether the reader is a person, a crawler, or an agent.
Why a rendered FAQ page fails machines
An FAQ page that satisfies a human reader is often a dead end for everything else that needs the same answers. The page is HTML: heading tags, paragraphs, maybe an accordion widget with divs that toggle visibility. A person parses that visually in a second. A crawler, an answer engine, or a retrieval-augmented assistant has to guess where each question ends and each answer begins, which category a question belongs to, and which answers are still current. The structure that was obvious to the human eye was never encoded anywhere a machine could read it.
The usual workarounds make things worse. Teams paste answers into a separate knowledge base for their chatbot, then forget to update it when the policy changes, so the assistant and the website drift apart. Or they dump the whole page into a vector store and hope semantic search retrieves the right chunk, which fails the moment a customer asks about "the cozy one" or phrases a literal policy question in words that do not appear in the answer. The failure mode is not cosmetic. A support assistant that retrieves a stale or wrong answer states it with total confidence, because paraphrasing is where facts go to die.
The root cause is that an FAQ's structure lived in its presentation instead of in a content model. A question, an answer, a category, and a set of related references are data. When you treat them as data, every consumer, the page, the crawler, and the agent, reads from the same shape. When you treat them as markup, each consumer improvises, and they improvise differently. Reframing the FAQ as structured content is the move that makes it legible to humans and machines at once, and it is the foundation every later step in this guide builds on.
Model the FAQ as schema, not as a page
Start by naming the fields an FAQ actually has. A question. An answer. A category. References to related documents or products. Optional metadata like a last-reviewed date or an owning team for governance. None of that is presentation; all of it is structure, and structure belongs in a content model you can version and query.
In Sanity this maps directly to the Model-your-business pillar. You define the FAQ as a portable `defineType` schema in code, checked into your repository alongside the application that consumes it. That schema is versioned in git, reviewed in pull requests like any other code, and codegen'd to TypeScript via TypeGen, so the shape of an FAQ answer is a type your frontend and your retrieval code both compile against. Change the model, regenerate the types, and every consumer that drifts out of sync fails at build time instead of in production in front of a customer.
This is where the contrast with GUI-first platforms gets concrete. In Contentful, an FAQ content type is defined through the web UI or the CLI, and that definition lives inside the platform tied to stored content; you adapt to their editor and their storage model. Sanity adapts to yours: the schema is code in `sanity.config.ts`, the editor is Sanity Studio, a React application you customize and ship, and Structure Builder lets you organize FAQs by category exactly the way your editors think about them. The answer field itself is Portable Text, structured JSON rather than an HTML blob, which is the detail that makes the next step possible. Because the model is code and the rich text is structured, one FAQ entry is a queryable, typed, governed object, not a rendered artifact you have to reverse-engineer later.
One answer, two outputs: HTML and JSON-LD
The reason to store the answer as Portable Text rather than an HTML string is that Portable Text is JSON, and JSON serializes in more than one direction. From a single FAQ answer in the Content Lake, you render two things without keeping two copies. To HTML for the human page, using a serializer that maps your Portable Text blocks and marks to your design system's components. And to plain text for the `answerText` field of a FAQPage JSON-LD block, which is the machine-readable surface answer engines ingest.
This matters more in 2026 than the SERP history suggests. Google deprecated the FAQ rich result in general search, so the old motivation, the expandable Q&A snippet, is mostly gone. But ChatGPT, Perplexity, and Google's AI Overviews still parse FAQPage schema.org JSON-LD when deciding what your page says and whether to cite it. The structured markup did not stop being useful; the audience for it shifted from the ten blue links to the answer engines that now sit in front of them. Emitting clean JSON-LD from the same source that renders your page is the Power-anything pillar in practice: one source of truth, any channel, no second copy of every answer maintained by hand for the machines.
The governance win is the part teams underrate. Because both outputs derive from one Portable Text value, they cannot disagree. When an editor updates an answer in the Studio and publishes it, the rendered page and the JSON-LD change together in the same publish. There is no separate export step, no stale machine copy, no drift between what a human reads and what an assistant retrieves. The single source is not just less work; it is the thing that keeps your answers honest across every reader.
Retrieval: why one search layer is never enough
Serializing the answer correctly only helps if the assistant can find the right FAQ in the first place. This is a retrieval problem, and it is where most FAQ chatbots quietly break. There are three ways to search, and each fails on its own.
Pure structured query, GROQ or SQL or GraphQL, means you write a predicate and get exactly what you asked for. It is precise and fast, and it falls over the moment a user says "something like X" or "the cozy one" or anything that lives in vibes rather than fields. Keyword search, BM25, catches literal matches: the customer who types your exact policy wording gets the exact answer. It misses paraphrases and synonyms. Semantic search over embeddings catches intent, the paraphrase and the synonym, but it drifts on precise filters and cannot guarantee that a hard constraint like category or region actually holds.
The discipline that works is hybrid retrieval: keyword search for literal matches, embeddings for semantic ranking, and structured predicates for the filters that have to hold. Anthropic's contextual retrieval research measured the payoff directly. Contextual embeddings alone cut top-20 retrieval failures by 35 percent. Adding contextual BM25 took that to 49 percent. Adding reranking on top brought it to 67 percent. You do not have to read the paper closely to catch the shape of the result: no single layer was enough. As the field guidance puts it bluntly, "We have embeddings" is not a retrieval strategy. It is one ingredient. For an FAQ assistant, that means combining a structured filter that pins the right category, a keyword match that catches literal policy language, and a semantic score that rescues the paraphrased question, rather than betting the whole experience on any one of them.
Hybrid retrieval over FAQs in one GROQ query
Hybrid retrieval sounds like three systems to operate. In GROQ it is one query. Using the text search operators documented in the Sanity docs, you filter, score, and order in a single round trip against the Content Lake, and the projection returns only the fields the assistant needs.
The filter comes first: `*[_type == "faq" && category == $category]` pins the constraint that must hold, so a billing question never returns a shipping answer no matter how the semantics score. Then the score pipeline blends the fuzzy layers: `| score(boost([question] match text::query($queryText), 2), text::semanticSimilarity($queryText))`. The `match text::query()` term is the BM25 keyword hit, boosted 2x on the question field because a literal hit on the question itself matters more than a scattered match in the body. The `text::semanticSimilarity()` term adds the embedding-based semantic score that catches the paraphrase. Then `| order(_score desc)` ranks by the blended result, and a tight projection returns the question, the serialized answer, and the category. Structured predicate, weighted keyword, and semantic similarity, expressed together, not stitched from three services.
There is a scaling detail worth knowing before you reach for embeddings. Sanity's production data on how agents actually call Context MCP shows structured retrieval dominates: the heavy majority of calls are GROQ queries and schema lookups, and semantic search is a small slice. Embeddings are opt-in and off by default, and most projects shipping on Context MCP never turn them on. For FAQs specifically, that means a well-modeled category filter plus a keyword match on the question gets you further than you would guess before semantic ranking becomes the bottleneck. Start with the structured and keyword layers, which cost nothing extra, and add `text::semanticSimilarity()` when your logs show real paraphrase misses, not on reflex.
Freshness and the shape of the agent's answer
Two production problems sink FAQ assistants after launch, and both have structural answers. The first is staleness. Any retrieval setup that lives outside your content store, a bolt-on vector database, a separate search service, inherits a pipeline you now own: incremental indexing when an answer changes, re-embedding on edit, deletion handling when an FAQ is retired, backfill when the schema changes, and eventual-consistency reasoning about when the index catches up. That is a real project and a class of bug all its own. With Content Lake, the index is wired into the content backend. When an answer updates, a record publishes, or a document is deleted, the index knows. Freshness stops being something you maintain and becomes a property of where the content lives.
The second problem is what the retrieval hands back. A tool or endpoint that answers from your FAQs should return schema-shaped objects, not prose. The field observation is direct: a tool that returns prose forces the model to paraphrase, and paraphrasing is where facts go to die. When agents were built against Context MCP, the ones that worked returned schema-shaped responses the model could pass straight through; the ones that struggled got a wall of text back and re-narrated it, badly. For an FAQ assistant, the retrieval layer should return the FAQ answer object, question, answer, category, source reference, not a paragraph the model rewrites into a plausible-sounding hallucination.
This is where the pieces close the loop. The schema you modeled in code is the shape the retrieval returns. The Portable Text answer serializes to a clean string in that object, the same string that feeds your JSON-LD. Sanity Context is the product surface here, a way to give agents structured, governed access to your content, with Context MCP as a hosted read-only endpoint any agent loop can connect to. The FAQ modeled once as structure is legible end to end: the page renders it, the answer engine ingests it, and the assistant returns it without a chance to invent.
Structuring FAQs for humans and machines: where the work lives
| Feature | Sanity | Contentful | Strapi | Payload |
|---|---|---|---|---|
| Where the FAQ schema lives | Portable defineType schema as code in sanity.config.ts, versioned in git and reviewed in pull requests like any other code. | Content type defined via web UI or CLI; definition lives inside the platform and is tied to stored content. | Content type built in the admin UI or via code; open-source and self-hostable, config lives with your app. | Code-first TypeScript config checked into your repo, a strong DX story close to Sanity's on this axis. |
| Rich answer format | Portable Text (structured JSON), so one answer serializes to HTML and to JSON-LD answerText from a single source. | Rich Text field stored as a structured document you serialize yourself; a common and workable pattern. | Rich text or blocks field; serialization to multiple targets is code you write per output. | Rich text via editor field; serializable, but the dual HTML plus JSON-LD mapping is yours to build. |
| Type safety for consumers | TypeGen codegen's the FAQ shape to TypeScript, so frontend and retrieval code fail at build time when the model drifts. | TypeScript types available via generators and SDKs; typically an added step in the toolchain. | Types generated from the schema; coverage depends on plugins and setup you assemble. | Types come from the TypeScript config natively, one of Payload's genuine strengths. |
| Hybrid retrieval over the FAQ | Native: one GROQ query blends a structured filter, boosted match() keyword hit, and text::semanticSimilarity() scoring in one round trip. | Search over the delivery API plus an external vector DB for semantics; blending the layers is your integration. | Bring-your-own: official tutorials wire Strapi to LangChain and a vector DB you operate separately. | payload-ai plugin adds embeddings and completions; hybrid blending and predicates are assembled around it. |
| Index freshness on edit or delete | Content Lake keeps the index fresh automatically on publish and delete, so incremental indexing and re-embedding are not your project. | External search or vector index needs its own sync pipeline for updates and deletions that you maintain. | You own the indexing pipeline end to end: re-embed on change, handle deletions, backfill on schema changes. | Plugin plus external service means you still maintain sync between content edits and the retrieval index. |
| What the agent tool returns | Schema-shaped FAQ answer objects via Context MCP that the model passes through, rather than prose it re-narrates. | API returns structured JSON; shaping it into a clean agent tool response is application code you write. | REST or GraphQL returns structured data; the agent tool contract and shape are yours to design. | Structured API responses available; the tool-shaping and MCP-style surface are things you build. |