Education Q&A Schema Framework
Use conditional-logic schema generation to keep Education Q&A markup aligned with real page state and avoid stale or invalid static templates.
When To Use It
Google Search CentralEducation Q&A documentationUse on education flashcard pages: each question has one accepted answer and every question is visible on the page. For a single question with user-submitted answers, use QAPage markup instead.
Key Implementation Documentation Highlights
- Availability is limited: the Education Q&A carousel shows only for education-related searches, on desktop and mobile, in English (all regions), Portuguese (all regions), Spanish (Mexico only) and Vietnamese (all regions). It applies to flashcard pages marked up with Quiz.
- Required on Quiz: hasPart, one nested Question per flashcard. Repeat hasPart for multiple flashcards.
- Required on each Question: eduQuestionType with the fixed value
Flashcard, text (the full question) and acceptedAnswer, exactly one Answer carrying the full answer text. - Recommended on Quiz: about, a Thing whose name is the underlying concept (for example Cell transport; several names are allowed), and educationalAlignment, an AlignmentObject with alignmentType (
educationalSubjectoreducationalLevel) and targetName (for example Grade 7: Cell Structure). Repeat it for multiple alignments. - Add markup to the most detailed leaf page possible and never to pages without questions. The page needs at least one accurate education question and answer pair, and the Q&A content guidelines apply: violations can lead to a manual action.
- Use the Rich Results Test and URL Inspection to validate rendering, eligibility, and crawlability.
Rules the framework enforces before it emits markup:
- Every question must use
Flashcardfor eduQuestionType. A multiple choice or checkbox question in the set suppresses the whole Quiz instead of emitting an ineligible one. - Each Question gets exactly one acceptedAnswer. A card with several answers, or with no question or answer text, suppresses the markup.
- Questions must be immediately visible on the page, not only inside a PDF or other file; the framework emits nothing when they are not.
- The page must be indexable and self-canonical; alignment entries with an unknown alignmentType or an empty targetName are dropped.
Template Approach
const pageData = {
concept: "Cell transport",
question: "What is the process by which molecules move from high to low concentration?",
answer: "Diffusion"
};
const staticTemplate = {
"@context": "https://schema.org",
"@type": "Quiz",
"about": { "@type": "Thing", "name": pageData.concept },
"hasPart": [
{
"@type": "Question",
"eduQuestionType": "Flashcard",
"text": pageData.question,
"acceptedAnswer": { "@type": "Answer", "text": pageData.answer }
}
]
};
{
"@context": "https://schema.org",
"@type": "Quiz",
"about": {
"@type": "Thing",
"name": "Cell transport"
},
"hasPart": [
{
"@type": "Question",
"eduQuestionType": "Flashcard",
"text": "What is the process by which molecules move from high to low concentration?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Diffusion"
}
}
]
}
Conditional-logic Framework
function isAbsoluteUrl(value) {
if (typeof value !== "string") return false;
try {
const url = new URL(value);
return url.protocol === "https:" || url.protocol === "http:";
} catch {
return false;
}
}
function cleanText(value) {
return typeof value === "string" ? value.replace(/\s+/g, " ").trim() : "";
}
function compact(value) {
if (Array.isArray(value)) {
const items = value.map(compact).filter((item) => item !== undefined);
return items.length ? items : undefined;
}
if (value && typeof value === "object") {
const out = {};
for (const [key, item] of Object.entries(value)) {
const kept = compact(item);
if (kept !== undefined) out[key] = kept;
}
return Object.keys(out).length ? out : undefined;
}
if (value === null || value === undefined || value === "") return undefined;
return value;
}
const ALIGNMENT_TYPES = ["educationalSubject", "educationalLevel"];
function buildFlashcard(card) {
const question = cleanText(card.question);
// exactly one accepted answer per Question: arrays of several are not eligible
const answer = Array.isArray(card.answer) ? null : cleanText(card.answer);
if (!question || !answer) return null;
return {
"@type": "Question",
"eduQuestionType": "Flashcard",
"text": question,
"acceptedAnswer": { "@type": "Answer", "text": answer }
};
}
function buildQuizSchema(source) {
// page gates: indexable, self-canonical, questions visible on the page (not only in a PDF)
if (source.indexable === false) return null;
if (!isAbsoluteUrl(source.url)) return null;
if (source.canonicalUrl && source.canonicalUrl !== source.url) return null;
if (source.questionsVisible !== true) return null;
const cards = source.flashcards || [];
if (!cards.length) return null;
// every question must be a Flashcard; multiple choice or checkbox questions make the Quiz ineligible
if (cards.some((card) => card.format && card.format !== "flashcard")) return null;
const hasPart = cards.map(buildFlashcard);
if (hasPart.some((question) => question === null)) return null;
const schema = {
"@context": "https://schema.org",
"@type": "Quiz",
"about": (source.concepts || []).length
? { "@type": "Thing", "name": source.concepts.length === 1 ? source.concepts[0] : source.concepts }
: null,
"educationalAlignment": (source.alignments || [])
.filter((a) => ALIGNMENT_TYPES.includes(a.type) && cleanText(a.targetName))
.map((a) => ({
"@type": "AlignmentObject",
"alignmentType": a.type,
"targetName": cleanText(a.targetName)
})),
"hasPart": hasPart
};
return compact(schema);
}
// Usage:
// buildQuizSchema({
// url: "https://www.example.com/biology/cell-transport-flashcards",
// canonicalUrl: "https://www.example.com/biology/cell-transport-flashcards",
// indexable: true,
// questionsVisible: true,
// concepts: ["Cell transport"],
// alignments: [
// { type: "educationalSubject", targetName: "Biology" },
// { type: "educationalLevel", targetName: "Grade 7: Cell Structure" }
// ],
// flashcards: [
// { format: "flashcard", question: "What is the process by which molecules move from high to low concentration?", answer: "Diffusion" },
// { format: "flashcard", question: "Which organelle controls what enters and leaves the cell?", answer: "The cell membrane" }
// ]
// });
import re
from urllib.parse import urlparse
def is_absolute_url(value):
if not isinstance(value, str):
return False
parsed = urlparse(value)
return parsed.scheme in ("http", "https") and bool(parsed.netloc)
def clean_text(value):
return re.sub(r"\s+", " ", value).strip() if isinstance(value, str) else ""
def compact(value):
if isinstance(value, list):
items = [kept for kept in (compact(item) for item in value) if kept is not None]
return items or None
if isinstance(value, dict):
out = {}
for key, item in value.items():
kept = compact(item)
if kept is not None:
out[key] = kept
return out or None
if value is None or value == "":
return None
return value
ALIGNMENT_TYPES = ("educationalSubject", "educationalLevel")
def build_flashcard(card):
question = clean_text(card.get("question"))
# exactly one accepted answer per Question: lists of several are not eligible
answer = None if isinstance(card.get("answer"), list) else clean_text(card.get("answer"))
if not question or not answer:
return None
return {
"@type": "Question",
"eduQuestionType": "Flashcard",
"text": question,
"acceptedAnswer": {"@type": "Answer", "text": answer},
}
def build_quiz_schema(source):
# page gates: indexable, self-canonical, questions visible on the page (not only in a PDF)
if source.get("indexable") is False:
return None
if not is_absolute_url(source.get("url")):
return None
if source.get("canonicalUrl") and source["canonicalUrl"] != source["url"]:
return None
if source.get("questionsVisible") is not True:
return None
cards = source.get("flashcards") or []
if not cards:
return None
# every question must be a Flashcard; multiple choice or checkbox questions make the Quiz ineligible
if any(card.get("format") and card["format"] != "flashcard" for card in cards):
return None
has_part = [build_flashcard(card) for card in cards]
if any(question is None for question in has_part):
return None
concepts = source.get("concepts") or []
schema = {
"@context": "https://schema.org",
"@type": "Quiz",
"about": {"@type": "Thing", "name": concepts[0] if len(concepts) == 1 else concepts} if concepts else None,
"educationalAlignment": [
{
"@type": "AlignmentObject",
"alignmentType": a.get("type"),
"targetName": clean_text(a.get("targetName")),
}
for a in source.get("alignments") or []
if a.get("type") in ALIGNMENT_TYPES and clean_text(a.get("targetName"))
],
"hasPart": has_part,
}
return compact(schema)
# Usage:
# build_quiz_schema({
# "url": "https://www.example.com/biology/cell-transport-flashcards",
# "canonicalUrl": "https://www.example.com/biology/cell-transport-flashcards",
# "indexable": True,
# "questionsVisible": True,
# "concepts": ["Cell transport"],
# "alignments": [
# {"type": "educationalSubject", "targetName": "Biology"},
# {"type": "educationalLevel", "targetName": "Grade 7: Cell Structure"},
# ],
# "flashcards": [
# {"format": "flashcard", "question": "What is the process by which molecules move from high to low concentration?", "answer": "Diffusion"},
# {"format": "flashcard", "question": "Which organelle controls what enters and leaves the cell?", "answer": "The cell membrane"},
# ],
# })
{
"@context": "https://schema.org",
"@type": "Quiz",
"about": {
"@type": "Thing",
"name": "Cell transport"
},
"educationalAlignment": [
{
"@type": "AlignmentObject",
"alignmentType": "educationalSubject",
"targetName": "Biology"
},
{
"@type": "AlignmentObject",
"alignmentType": "educationalLevel",
"targetName": "Grade 7: Cell Structure"
}
],
"hasPart": [
{
"@type": "Question",
"eduQuestionType": "Flashcard",
"text": "What is the process by which molecules move from high to low concentration?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Diffusion"
}
},
{
"@type": "Question",
"eduQuestionType": "Flashcard",
"text": "Which organelle controls what enters and leaves the cell?",
"acceptedAnswer": {
"@type": "Answer",
"text": "The cell membrane"
}
}
]
}
The Output As An Entity Graph
The conditional-logic output above, parsed in your browser and drawn as the graph a crawler builds from it. Every nested object is a node and the property that holds it is the edge. Tap a node to inspect its properties, or drag to rearrange.
Why Conditional Logic Is Better Than Static Templates
- Builds every flashcard from the same records that render the visible cards, so question and answer text cannot drift from the page.
- Stamps
Flashcardin code and refuses mixed sets, so a stray multiple choice item never makes the page ineligible. - Adds about and educationalAlignment only when the content model has them, instead of shipping empty or invented values.
- Suppresses markup on PDF-only, noindex, or canonicalized pages without a separate template.
Property Reference
What Google's Education Q&A documentation requires and recommends, property by property. These are the same rules the SchemaCDN validator checks. Each name links to its schema.org definition.
Availability: Education Q&A carousel shows only for education-related searches, on desktop and mobile.
| Property | Expects | Notes | |
|---|---|---|---|
| hasPart | Required | Question, can repeat | Quiz requires hasPart with at least one flashcard Question. |
| hasPart.acceptedAnswer | Required | Answer | Question requires exactly one acceptedAnswer. |
| hasPart.eduQuestionType | Required | Text | EduQuestionType must be 'Flashcard'. One of: Flashcard. |
| hasPart.text | Required | Text | Question requires text: the full flashcard question. |
| about | Recommended | Thing | The underlying concept of the quiz. |
| about.name | Recommended | Text, can repeat | The name of the underlying concept behind the Quiz, e.g. 'Cell transport'. Multiple entries allowed. |
| educationalAlignment | Recommended | AlignmentObject, can repeat | EducationalSubject and/or educationalLevel. |
| educationalAlignment.alignmentType | Recommended | Text | AlignmentType should be educationalSubject or educationalLevel. One of: educationalSubject, educationalLevel. |
| educationalAlignment.targetName | Recommended | Text | For the educational framework node. |