How We Built FAQPage JSON-LD for AI Control to Get Cited by AI
Why a Good-Looking FAQ Accordion Guarantees Nothing to an AI System
Open almost any pricing page or product page and scroll to the bottom — there's a good chance you'll find a tidy FAQ accordion. Click a question, it expands, the answer appears underneath. Someone picked the spacing and the expand animation, bolded the question, left the answer in regular text. To a human reader it's unambiguous: here's the question, here's the answer, one starts where the other ends.
Now picture the same page being read not by a person but by a system preparing an answer for ChatGPT, Perplexity, or Google AI Overviews. It has no intuitive sense of "this is an accordion." What it sees is markup: maybe a details/summary pair, maybe a div with class faq-item wrapping two more divs, maybe a component that only renders the answer text after a click — meaning the answer isn't even in the initial HTML at all. To figure out where a question ends and an answer begins, the system has to infer it — from layout, from visual proximity, from patterns it has seen across thousands of other sites built by thousands of other teams who never agreed on a convention.
Most of the time the inference works fine — modern parsers are decent at pulling text out of markup. But "decent" isn't "guaranteed." Every inference is a place things can go sideways: a question and answer get merged into one blob, an answer gets cut off mid-sentence, a "related questions" block gets mistaken for part of the answer. None of that bothers a human — they'll just keep reading. It matters a lot more to a system that needs to produce an exact quote tied to a specific question, at the exact moment precision counts the most.
It's a bit like handing the same recipe to a line cook versus a random passerby. The cook instantly knows which lines are ingredients and which are steps, because they've internalized the format of a recipe card. The passerby has to read carefully and figure out what's going on. FAQPage JSON-LD is that recipe card in its purest form: no glossy photo of the finished dish, just explicit "question" and "answer" fields that need no guessing from layout.
The fix isn't to rebuild the accordion — it already does its job for human visitors. The fix is adding a second, parallel layer underneath the same visible markup: explicit, unambiguous data describing exactly which string is the question and which is the answer, with nothing left to infer. That's what FAQPage JSON-LD is for.
If a page has one question, the cost of a bad guess is small — worst case, the system quotes a paragraph with slightly less confidence. But once a page has fifteen questions, and there are dozens of pages like it across the site, the odds that a parser misjudges the boundary somewhere stop being theoretical. Explicit markup doesn't eliminate the parsing step entirely, but it moves it from "guess from the layout" to "read the field" — a different order of reliability altogether, especially once the system's answers get reused across thousands of similar pages on the web.
What FAQPage JSON-LD Actually Is, in Plain Terms
schema.org is a shared vocabulary the major search engines proposed jointly, specifically so sites would have a standard way to describe page content in a machine-readable form — independent of how that content happens to look. The vocabulary has hundreds of types: Product, Article, Organization — and, among them, FAQPage.
JSON-LD is one of the formats you can use to embed that data on a page. Technically, it's just a script tag with type application/ld+json sitting in the document's head: a block the browser never shows a visitor, but that any crawler or parser can read directly as a data structure, instead of as prose it first has to interpret.
Applied to an FAQ, it looks like this: the FAQPage type says "this page contains a list of questions and answers." Inside it sits an array of Question entries, each with a name field (the question text) and an acceptedAnswer field — a nested Answer object whose text field holds the answer. No ambiguity: not "this is probably a question because it's bold and sits above a paragraph," but literally a field called name holding the question string.
One thing that's easy to miss: this JSON-LD doesn't replace the visible accordion — it sits alongside it. The same content on the page ends up described twice: once in HTML and CSS for a person who clicks and reads, and once in JSON for a parser that reads directly, with no click and no layout to interpret. The markup and the visible UI aren't competing — they serve two different readers of the same page.
Nothing about the mechanism is new: Google was using FAQPage JSON-LD for rich snippets in ordinary search results long before AI search became a mainstream topic. The stakes used to be lower, though — the markup just changed how a snippet looked on a results page. Now the same mechanism partly decides whether your exact wording ends up inside an answer the user reads in full, possibly never clicking through to the page at all. The cost of getting the markup right went up, even though the markup itself didn't change a single line.
How We Actually Built It: One Function, Two Representations
AI Control has its own FAQ page, and it has JSON-LD too. The only reliable way to guarantee the visible text and whatever sits in that script tag never drift apart is to stop writing them separately. In the codebase there's a single source of truth: an array of { question, answer } objects, which renders the visible accordion and also generates the schema. Here's the actual builder function:
export function buildFaqPageSchema({ questions }) {
return {
'@type': 'FAQPage',
mainEntity: questions.map(({ question, answer }) => ({
'@type': 'Question',
name: question,
acceptedAnswer: { '@type': 'Answer', text: answer },
})),
};
}The function doesn't do anything clever — it doesn't parse HTML, and it doesn't try to guess what counts as a question. It just takes the same array already used to render the UI and reshapes it into the structure schema.org expects: @type FAQPage, with a mainEntity array of Question nodes, each carrying a name (the question) and a nested acceptedAnswer (the answer). Change a question at the source and it changes automatically in both the visible UI and the markup, because both come from the same array. There's no physical path for them to diverge.
To make that concrete, here's what it actually outputs for two real questions about how AI Control tracks brand visibility in AI answers:
{
"@context": "https://schema.org",
"@type": "FAQPage",
"mainEntity": [
{
"@type": "Question",
"name": "What is AI Control?",
"acceptedAnswer": {
"@type": "Answer",
"text": "AI Control is the SEO Control module that tracks how a brand and its competitors are mentioned in answers from ChatGPT, Perplexity, Copilot, Gemini, and Google AI, using real prompts run against the actual systems rather than a simulation."
}
},
{
"@type": "Question",
"name": "What does a \"real\" check mean in AI Control?",
"acceptedAnswer": {
"@type": "Answer",
"text": "It means the prompt is actually sent to the selected AI system and a genuine response comes back, rather than an estimate based on assumptions about how that system might answer."
}
}
]
}The function itself returns the object without an @context — that field gets added a level up, where this fragment is merged into the rest of the page's structured data, so @context isn't duplicated on every single piece. But the substance doesn't change: name and acceptedAnswer.text above are literally the same strings a visitor sees in the expanded accordion, just without the HTML around them.
In practice, FAQPage rarely lives alone on a page: there's usually a WebPage entry too, sometimes an Organization entry with its own set of fields. That's why the builder returns only a fragment — an object with @type and mainEntity — rather than a complete, ready-to-ship document: the page itself decides what to merge it with and where the shared @context goes. That's a common pattern for generating structured data in general: small functions assemble fragments instead of hand-building one monolithic JSON blob per page.
Curious how AI Control's engineering looks beyond markup? There's a separate write-up on how we built AI Control's capture job queue.
Two Layers of the Same Page
Zoom out and a page like this turns out to have two parallel layers describing the same content for two different readers:
The top layer is what a human sees and touches: a clickable accordion, an expand animation, a visual hierarchy of question-above-answer. The bottom layer is the exact same content with no visual presentation at all — pure data, invisible to the eye but fully readable by machine. There's no version conflict between them, because both are generated from the same array at build time. A person clicks, the accordion opens. A parser requests the page, finds the script tag, and reads the structure directly — without ever touching the visible DOM.
Without that single source, it's easy to end up with drift in practice: someone edits a question's wording inside a component, without noticing that a separate copy of the same text lives hardcoded elsewhere for the markup. Months later a stale phrasing surfaces in a search result or an AI answer, long after the page itself moved on. A single source array makes that mistake structurally impossible, not just "unlikely if people are careful."
What Makes a Good FAQ Question for AEO (and What Doesn't)
Markup can't save a badly written question. FAQPage JSON-LD faithfully passes along exactly what you put into it — if an answer is vague or depends on the surrounding paragraphs, the markup will neatly package that vague answer and hand it over as-is. The gap between a question that actually works for AEO and one that only works visually comes down to how self-contained the text is:
Here's an actual rewrite. "Why does this matter?" says nothing on its own — pulled out of the page, it's unclear what it's even about. The same underlying point, made self-contained: "Why track brand mentions in AI answers if they don't always link back to the source?" — now the question stands on its own, even shown in isolation from the rest of the page. A human scrolling top to bottom barely notices the difference, because the surrounding context is right there. For a system extracting a single question-answer pair with none of that context, the difference is decisive.
| Criterion | Good example | Bad example |
|---|---|---|
| Self-contained answer | The answer makes sense on its own, without the rest of the page | "As described above, it works like this" — depends on surrounding paragraphs |
| Specific question | "What does AI Control track?" — one clear subject | "Why does this matter?" — vague, unclear what it even refers to |
| One idea per pair | One question, one answer tied to it | A question that's secretly three different sub-questions |
| Fact over marketing | "How does AI Control get answers from AI systems?" — a concrete mechanism | "We do this better than anyone else" — a claim with no substance |
| Phrased as an actual question | Reads the way a real user would actually ask it | A section header dressed up with a question mark |
Notice none of the right-hand criteria are technical. They're ordinary good-writing rules that mattered before AI search existed too — the cost of ignoring them is just higher now, because a system will either quote a self-contained answer wholesale or skip a vague one, with no way to ask a follow-up the way a human conversation partner would.
One more thing worth saying about length: self-contained doesn't mean long. It means complete within a sentence or two. A three-paragraph answer full of hedges and qualifiers is actually harder for a system to quote than a short, precise one — it has to decide which part to keep and which to cut, and that's the exact same guessing problem the markup was supposed to remove in the first place.
Markup Doesn't Guarantee You Get Cited
Let's be honest here: FAQPage JSON-LD doesn't buy a spot in ChatGPT's answer, and it doesn't guarantee Perplexity will pull your exact wording. It isn't a ranking factor in the "add the markup, climb the results" sense. What it actually does is remove one specific source of uncertainty: instead of inferring structure from layout, the system gets a ready-made, unambiguous structure handed to it.
If the content isn't relevant to the query, if the answer is factually wrong or stale, or if the page simply isn't crawled and the system doesn't know it exists, no amount of markup fixes that. Far more factors decide whether a page becomes a source for an answer than its markup format: relevance, authority, how completely the answer actually resolves the question, whether the brand is on the system's radar at all. Markup does its work at the stage where the system has already decided to look at your page — it makes pulling a correct quote easier, but it doesn't decide whether the page gets looked at in the first place.
Keep the concepts straight, too: an AI system quoting text from your page once isn't the same thing as a link, and it isn't the same as a plain brand mention with no attribution either. The difference between mentions, citations, and backlinks matters here specifically because FAQPage markup only directly addresses one of those three scenarios — the one where a system uses your exact wording, word for word or close to it.
There's an earlier link in this chain, too: getting the page noticed at all. If crawlers don't know the page exists, if nothing in the site's internal linking points to it, or if it's blocked from indexing, no one ever reads the JSON-LD on it, no matter how carefully it's marked up. Markup does its job after a page has already been found and judged relevant enough to look at — it isn't a way to make that discovery happen in the first place.
The practical takeaway is simple: given a choice between a polished, general-sounding line and a precise, less flashy one, precise wins for AEO. The markup will faithfully carry whatever text you give it, inaccurate or outdated included. It isn't a quality filter — it's a transparent pipe. Whatever goes in comes out the other side unchanged.
FAQ: Questions About FAQPage JSON-LD Itself
Do I need to hand-write JSON-LD for an FAQ?
No — you can generate it with the same function that renders the visible list of questions and answers, from the same underlying array, so the text people read and the text machines read have no way to drift apart.
Does FAQPage JSON-LD guarantee a brand gets cited in ChatGPT or Perplexity?
No. The markup makes it easier for a system to extract a question and answer correctly, but it has no influence over whether the system considers the page relevant or authoritative enough to use as a source in the first place.
How is FAQPage JSON-LD different from a regular visible FAQ block on a page?
A visible block is HTML and CSS built for a person to click and read; where the question ends and the answer begins is never explicitly labeled and has to be inferred from layout. FAQPage JSON-LD is a separate data block in a script tag with type application/ld+json, where the question and answer are explicitly labeled with name and acceptedAnswer fields, with nothing left to interpret.
Should I add FAQPage markup to every page just to get the markup?
Markup belongs only on pages that genuinely have substantive questions and answers for users — not as a way to quietly stuff extra keywords into a page's code.
Can FAQPage JSON-LD coexist with other structured data types on the same page?
Yes, that's the normal case: FAQPage sits alongside other types on a page, such as one describing the page itself or the organization publishing it, and each type independently describes its own slice of the content.
Checklist: Adding FAQPage JSON-LD to Your Own Pages
If you want to do the same thing on your own site, the sequence is simple and doesn't need any special infrastructure:
- Write the real content first: self-contained questions and complete answers that make sense without the rest of the page around them.
- Use the same question/answer array for both the visible UI and the schema generation — don't maintain two separate pieces of text.
- Assemble the JSON-LD with a small builder function instead of hand-typing the structure — less manual work, fewer chances to get the shape wrong.
- Insert the result into a script tag with type application/ld+json inside the page's head.
- Validate the output with a structured data validator before shipping it.
- When content changes, edit only the source array — both the UI and the markup update themselves, with no risk of drifting apart.
- Revisit the actual questions and answers periodically: a practical AEO checklist goes well beyond markup — wording and facts matter more than syntax in the end.
- Don't add the markup to pages without a real FAQ — marking up nothing reads as an attempt to game search results, not as a way to help the reader.
Want to check this in your market?
AI Control regularly collects AI responses, brand positions, competitors and cited sources for your prompt library.
Is your own content set up for stories like these?
Use 50 welcome credits for an AI Readiness check — retrieval, extractability, schema.org signals, and a prioritized rewrite brief, scored the way an AI assistant actually reads your page.