QAPage or FAQPage, and why the difference is not cosmetic
QAPage describes a page where visitors submit competing answers. A forum thread. A community Q&A board. A support site where users reply to each other and one reply gets marked as accepted. The defining feature is that the answers come from people who are not you.
FAQPage describes a page where the site owner asks and answers. Your pricing FAQ. The 5 questions at the bottom of a product page. One voice, no competition, no accepted-answer semantics.
The 2 are not interchangeable. Google's structured data guidelines treat markup that misrepresents a page as a spam issue, and a QAPage block on a page nobody can answer says "here are competing community answers" about content that has none.
The quick test: can a stranger add an answer to this page? If no, it is a FAQPage.
What a valid QAPage schema block looks like
The shape is a single Question under mainEntity:
mainEntityis oneQuestionobject, never an array.Question.nameis the question itself, as a headline.Question.textis the full body of the question, the paragraph underneath the title.Question.authoris the person who asked.Question.dateCreatedis when the question was posted.Question.answerCountis the number of answers on the page.Question.acceptedAnsweris oneAnswer, the accepted or top-voted one.Question.suggestedAnsweris an array of the remainingAnswerobjects.
answerCount should reflect what is on the page. If a thread has fourteen answers and you mark up 3, do not claim fourteen unless all fourteen are visible.
Every field, and where it lands
Q&A items does the structural work. One line per entry, question | answer. The first line's left-hand side becomes Question.name. Every right-hand side becomes an answer, and any line after the first with no pipe is treated as another answer. The first answer becomes acceptedAnswer; everything after it becomes suggestedAnswer[].
The parser accepts tabs instead of pipes, strips leading bullets and 1. numbering, and understands Q: and A: prefixes, because that is how people paste from a forum export.
Paste several distinct question-and-answer pairs and the generator folds the extras in as answers and warns you, with a pointer to the FAQ generator. That warning is telling you that you picked the wrong schema type.
Description becomes Question.text, the full body of the question. Fill it in: a bare one-line question gives a parser nothing beyond the headline.
Author name becomes Question.author, emitted as a Person. This is the asker. Not the site, not the person who answered, not you. If the author URL field is filled it becomes author.url.
Published date becomes Question.dateCreated, and must be ISO 8601: 2024-03-14 or 2024-03-14T09:00:00+01:00. 14/03/2024 and March 14, 2024 are the 2 formats people paste, and neither is valid.
Name and page URL are shared form fields and are not emitted in a QAPage block, which carries mainEntity and nothing else. Leaving them blank changes nothing.
Author and dateCreated describe the asker, not you
This trips up almost everyone migrating from Article markup, where author means the person who wrote the page. On a QAPage, author and dateCreated sit on the Question, and the question was written by whoever posted it.
If your forum stores an author on each answer too, Answer.author and Answer.dateCreated are valid schema.org properties worth adding by hand once you have the block. This generator emits the question-level ones, which are the ones Google documents as recommended.
Putting your own byline on the Question is the version of this mistake that survives review, because nothing errors. It just describes the page incorrectly.
A worked Q&A schema example
2 lines in the Q&A items box:
Why does my Postgres connection pool exhaust under pgbouncer? | Transaction pooling does not preserve session state, so your ORM re-registers prepared statements on every checkout and never releases them. Disable statement caching in the ORM, or move to session pooling.
Check max_client_conn on pgbouncer before touching the ORM. We had the same symptom with the default of 100 and twelve app pods.
With Description set to the body of the post, Author name dmitri-k, and Published date 2024-03-14:
{
"@context": "https://schema.org",
"@type": "QAPage",
"mainEntity": {
"@type": "Question",
"name": "Why does my Postgres connection pool exhaust under pgbouncer?",
"text": "Running pgbouncer in transaction mode in front of Postgres 15. The pool hits its ceiling after about twenty minutes of normal traffic.",
"author": { "@type": "Person", "name": "dmitri-k" },
"dateCreated": "2024-03-14",
"answerCount": 2,
"acceptedAnswer": {
"@type": "Answer",
"text": "Transaction pooling does not preserve session state, so your ORM re-registers prepared statements on every checkout and never releases them. Disable statement caching in the ORM, or move to session pooling."
},
"suggestedAnswer": [
{
"@type": "Answer",
"text": "Check max_client_conn on pgbouncer before touching the ORM. We had the same symptom with the default of 100 and twelve app pods."
}
]
}
}
The second line had no pipe, so it became the second answer rather than a second question. That is the behavior you want on a thread.
What your schema builder checks before you ship
Required, and flagged if missing: a question, at least one answer, and text on the accepted answer. Recommended and flagged if absent: the asker's name, the question body, and the creation date.
Beyond that, 3 things worth checking yourself:
- Every question and answer must be visible in the page text. Markup describing answers a visitor cannot see is a violation, and it is the fastest way to lose eligibility for everything, not just this feature.
- Do not mark up a thread with zero answers. A
QuestionwithanswerCount: 0and noacceptedAnswerhas nothing to offer a parser. - One QAPage block per page. If the URL hosts several threads, it is an index page, and index pages get
QAPagemarkup on the thread pages they link to, not on themselves.
On rich results: Q&A appearances in Google have narrowed sharply since 2023, so do not build a community page for the SERP treatment. Ship the markup because it describes the page accurately and because non-Google parsers and AI answer engines read it.