PubTrivia publishes free quiz questions as ordinary content pages: fourteen categories, every question and its answer rendered as plain text, no sign-in, no toggle to open. Today that is 462 question and answer pairs.

If you have ever shipped structured data, you already know what the obvious markup for that is, and you can check in one command whether we used it.

curl -s https://pub-trivia.app/quiz-questions/music | grep -o '"@type":"[A-Za-z]*"' | sort | uniq -c 
 6 "@type":"Organization" 3 "@type":"ListItem" 1 "@type":"WebPage" 1 "@type":"BreadcrumbList" 1 "@type":"Article" 

Thirty-four visible questions, each with its answer, and zero Question nodes. Run the same command against all fourteen category pages and the count of FAQPage nodes is zero every time.

Now run it against a different page on the same site:

curl -s https://pub-trivia.app/faq | grep -o '"@type":"Question"' | wc -l # 16 

So the site is perfectly willing to emit FAQPage. It just will not do it for the quiz questions, and the reason is worth more than the schema.

FAQPage is a claim about who asked

FAQPage does not mean "this page has a list of questions on it". It means the page is a frequently asked questions page: questions people put to the publisher about the subject, answered by the publisher.

A trivia question is not that. Nobody asked us which Liverpool band released Abbey Road, we wrote it down as a question for a pub to read out loud. Marking those up as FAQ entries would be describing the page as something it is not, in the one part of the page that exists specifically to tell a machine what the page is. That is the category of mistake that collects a manual action, and the fact that it would probably work for a while is the trap rather than the defence.

The comment above the component that renders them says it in one paragraph, which is in the repository so that the next person to touch the file has to read the decision before changing it:

/** * A published set of quiz questions with their answers. * * Answers are rendered in the open rather than behind a `<details>` toggle. The * reader is a host planning a round, not a player trying not to spoil it, and a * page whose substance is inside collapsed elements is a page a crawler has to * guess about. * * There is deliberately no per-question structured data. `FAQPage` is the * obvious-looking fit and it would be a misrepresentation: these are not * questions people asked us about the product, and marking them up as if they * were is exactly the kind of thing that earns a manual action. The page carries * `Article` markup for what it is, a written piece with a list in it. */ 

What the pages emit instead

Article, because that is what they are. Each category page opens with a few hundred words on how to use that round, and then the list. The Article node names the publisher rather than a person:

export function articleLd(node: ContentNode) { const iso = `${node.updated}T00:00:00Z` return { '@context': 'https://schema.org', '@type': 'Article', headline: node.heading, description: node.description, mainEntityOfPage: { '@type': 'WebPage', '@id': toAbsolute(node.path) }, author: { '@id': organizationIdFor(SELF_APP) }, publisher: { '@id': organizationIdFor(SELF_APP) }, datePublished: iso, dateModified: iso, inLanguage: 'en-GB', } } 

Two things in there are deliberate and both are the same decision as the FAQPage one.

A named human author is the stronger signal, and we do not have a human to name. Inventing "Sarah, Head of Quizzes" to fill a schema field is precisely what structured data is supposed to stop, so the author is the Organization node, which is a real entity that really does publish the page. If a person ever puts their name to these, that line is where it goes.

And datePublished equals dateModified because these are evergreen pages revised in place rather than dated posts. The date a guide was first written is not a fact anybody needs. The date it was last checked is, and it is on the page as a visible "Last reviewed" line, which is the only reason it is allowed in the markup at all.

The rule that decides all of it

Every builder in lib/content/structured-data.ts takes the same data the page renders from. Not a copy of it, the same array:

/** Only ever emitted by a page that renders the same questions and answers. */ export function faqLd(entries: readonly FaqEntry[]) { return { '@context': 'https://schema.org', '@type': 'FAQPage', mainEntity: entries.map((entry) => ({ '@type': 'Question', name: entry.question, acceptedAnswer: { '@type': 'Answer', text: entry.answer }, })), } } 

/faq renders sixteen questions and passes those sixteen to faqLd. There is no path through the code where the markup and the page disagree, because there is only one array. The same shape holds for the breadcrumb trail, the hub lists and the comparison tables.

That is also why whether a page is an Article is a single boolean on the shared page shell, and why the boolean does two jobs:

{article && <JsonLd data={articleLd(node)} />} 
{article && ( <p className="text-muted-foreground/70 mb-12 text-sm"> Last reviewed <time dateTime={node.updated}>...</time> </p> )} 

Turn it on and the page gets both the Article node and the visible review date. Turn it off and it gets neither. You cannot ship the markup without the line it is describing, because one flag controls both. Twenty-eight of our sixty-seven content pages set it, the fourteen guides and the fourteen quiz question pages, and the product pages do not, because a pricing table marked up as an article is a claim about the page that is not true.

Check that too, on two pages fifteen seconds apart:

curl -s https://pub-trivia.app/guides/pub-quiz-format | grep -c "Last reviewed" # 1 curl -s https://pub-trivia.app/features/quiz-builder | grep -c "Last reviewed" # 0 

And the JSON-LD on those two pages differs in exactly the one node you would predict.

Where the questions are

All fourteen category pages are free and open, no account:

Take the questions and use them, no attribution needed. If you would rather not read answers out over a pub sound system, the free tier runs the night from the phones in the room and needs no card.