Q&A
On this page. QAPage with one Question, its accepted answer and suggested answers.
One community thread: a question with answers becomes a QAPage, anything else a DiscussionForumPosting. Posters keep their profile @id, and a thread about a product points at it.
Use on a page that shows one user-generated thread from a forum or community. The builder decides the type from the thread: a question that collects answers is Q&A, a trip report or open discussion is a forum posting.
Each poster is the same Person @id as their member profile, and a thread about a product points at that product's @id, so the question is tied to the product it asks about.
Each card opens the framework for that feature, with its own rules and property reference. Cards marked "Linked by @id" are defined on the homepage and only referenced here.
On this page. QAPage with one Question, its accepted answer and suggested answers.
On this page. Used instead of Q&A when the thread is not a question.
On this page. Home, Community, the board, the thread.
Linked by @id. Each poster's Person @id comes from their member profile page.
Linked by @id. The question is about the Ridgeline shell, by its ProductGroup @id.
Every node has a stable @id. A node is written out once, where it belongs, and everywhere else it is referenced by that @id alone. This is the map for this page, generated from the output below.
| @id | Defined on | Type | Referenced by |
|---|---|---|---|
/community/threads/ridgeline-shell-winter-layering/#webpage | This page | QAPage | Top-level node |
/community/members/jkrause/#person | This page | Person | Nested in QAPage.mainEntity.author |
/community/members/maya-okafor/#person | This page | Person | Nested in QAPage.mainEntity.acceptedAnswer.author |
/community/members/trailmix/#person | This page | Person | Nested in QAPage.mainEntity.suggestedAnswer.author |
/community/threads/ridgeline-shell-winter-layering/#breadcrumb | This page | BreadcrumbList | QAPage.breadcrumb |
/#website | Homepage | WebSite | QAPage.isPartOf |
/jackets/ridgeline-rain-shell/#product-group | Product page | ProductGroup | QAPage.mainEntity.about |
One function turns the page's data into the whole graph. It drops anything Google would reject instead of shipping it half built. The input below includes rows it is meant to drop, so you can see the logic work.
// Forum thread page schema: one thread from the store's community forum.
// A question that collects answers becomes a QAPage; any other thread (a
// trip report, a discussion) becomes a DiscussionForumPosting. Posters are
// the same Person @id as their forum profile page, and a thread about a
// product points at that product's @id.
const ISO_DATE_TIME = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(:\d{2})?(Z|[+-]\d{2}:\d{2})$/;
const clean = (v) => (typeof v === 'string' ? v.trim() : v);
const isUrl = (v) => typeof v === 'string' && /^https:\/\/[^\s]+$/.test(v);
const compact = (obj) => {
if (Array.isArray(obj)) {
const out = obj.map(compact).filter((v) => v !== undefined);
return out.length ? out : undefined;
}
if (obj && typeof obj === 'object') {
const out = {};
for (const [k, v] of Object.entries(obj)) {
const c = compact(v);
if (c !== undefined) out[k] = c;
}
return Object.keys(out).length ? out : undefined;
}
return obj === null || obj === '' ? undefined : obj;
};
function breadcrumb(trail, id) {
const items = (trail || []).filter((c) => c.name && isUrl(c.url));
if (items.length < 2) return undefined;
return {
'@type': 'BreadcrumbList',
'@id': id,
itemListElement: items.map((c, i) => ({ '@type': 'ListItem', position: i + 1, name: clean(c.name), item: c.url })),
};
}
function person(u) {
if (!u || !u.name) return undefined;
return compact({
'@type': 'Person',
'@id': isUrl(u.profileUrl) ? `${u.profileUrl}#person` : undefined,
name: clean(u.name),
url: isUrl(u.profileUrl) ? u.profileUrl : undefined,
});
}
// A post is only kept when it has visible text, an author and a timestamp
// with a timezone; deleted and moderated posts are skipped.
const usable = (p) => p && !p.removed && p.text && p.text.trim() && p.author && ISO_DATE_TIME.test(p.date || '');
function buildForumThreadPageSchema(source) {
const site = source.site || {};
const page = source.page || {};
const t = source.thread || {};
if (!isUrl(site.url) || !isUrl(page.url) || !t.title || !usable(t.opening)) return null;
const root = site.url.replace(/\/+$/, '');
const pageUrl = page.url;
const replies = (t.replies || []).filter(usable);
const about = isUrl(t.aboutUrl) ? { '@id': t.aboutUrl } : undefined;
// Q&A only fits a thread that asks one question and collects answers. A
// site-written FAQ or a thread with no replies is not a QAPage.
const isQuestion = t.kind === 'question' && replies.length > 0;
let main;
if (isQuestion) {
const answer = (r) => compact({
'@type': 'Answer',
text: clean(r.text),
author: person(r.author),
datePublished: r.date,
upvoteCount: Number.isInteger(r.upvotes) ? r.upvotes : undefined,
url: `${pageUrl}#post-${r.id}`,
});
const accepted = replies.find((r) => r.accepted);
main = compact({
'@type': 'QAPage',
'@id': `${pageUrl}#webpage`,
url: pageUrl,
name: clean(page.title) || clean(t.title),
isPartOf: { '@id': `${root}/#website` },
mainEntity: {
'@type': 'Question',
name: clean(t.title),
text: clean(t.opening.text),
author: person(t.opening.author),
datePublished: t.opening.date,
answerCount: replies.length,
upvoteCount: Number.isInteger(t.opening.upvotes) ? t.opening.upvotes : undefined,
about,
acceptedAnswer: accepted ? answer(accepted) : undefined,
suggestedAnswer: replies.filter((r) => r !== accepted).map(answer),
},
});
} else {
main = compact({
'@type': 'DiscussionForumPosting',
'@id': `${pageUrl}#post-${t.opening.id}`,
headline: clean(t.title),
text: clean(t.opening.text),
author: person(t.opening.author),
datePublished: t.opening.date,
url: pageUrl,
about,
mainEntityOfPage: { '@id': `${pageUrl}#webpage` },
commentCount: replies.length,
comment: replies.map((r) => compact({
'@type': 'Comment',
text: clean(r.text),
author: person(r.author),
datePublished: r.date,
url: `${pageUrl}#post-${r.id}`,
})),
});
}
const crumbs = breadcrumb(page.breadcrumbs, `${pageUrl}#breadcrumb`);
if (isQuestion) {
if (crumbs) main.breadcrumb = { '@id': crumbs['@id'] };
return { '@context': 'https://schema.org', '@graph': [main, crumbs].filter(Boolean) };
}
const webpage = compact({
'@type': 'WebPage',
'@id': `${pageUrl}#webpage`,
url: pageUrl,
name: clean(page.title) || clean(t.title),
isPartOf: { '@id': `${root}/#website` },
breadcrumb: crumbs ? { '@id': crumbs['@id'] } : undefined,
mainEntity: { '@id': main['@id'] },
});
return { '@context': 'https://schema.org', '@graph': [webpage, crumbs, main].filter(Boolean) };
}
{
"site": {
"url": "https://www.example.com"
},
"page": {
"url": "https://www.example.com/community/threads/ridgeline-shell-winter-layering/",
"title": "Is the Ridgeline shell warm enough for winter hiking? | Trailhead Community",
"breadcrumbs": [
{
"name": "Home",
"url": "https://www.example.com/"
},
{
"name": "Community",
"url": "https://www.example.com/community/"
},
{
"name": "Gear questions",
"url": "https://www.example.com/community/gear-questions/"
},
{
"name": "Is the Ridgeline shell warm enough for winter hiking?",
"url": "https://www.example.com/community/threads/ridgeline-shell-winter-layering/"
}
]
},
"thread": {
"kind": "question",
"title": "Is the Ridgeline shell warm enough for winter hiking?",
"aboutUrl": "https://www.example.com/jackets/ridgeline-rain-shell/#product-group",
"opening": {
"id": "5101",
"text": "Planning day hikes in the White Mountains in January. Is the Ridgeline shell enough on its own, or what should I layer under it?",
"author": {
"name": "jkrause",
"profileUrl": "https://www.example.com/community/members/jkrause/"
},
"date": "2026-09-30T19:12:00-04:00",
"upvotes": 14
},
"replies": [
{
"id": "5102",
"text": "On its own, no. It is a shell, so it blocks wind and rain but has no insulation. I wear a merino base layer and a light fleece under it and stay warm down to about 10F while moving.",
"author": {
"name": "Maya Okafor",
"profileUrl": "https://www.example.com/community/members/maya-okafor/"
},
"date": "2026-09-30T20:05:00-04:00",
"upvotes": 31,
"accepted": true
},
{
"id": "5103",
"text": "Add a puffy for breaks. You cool down fast the moment you stop, and the shell traps the warmth well over a down layer.",
"author": {
"name": "trailmix",
"profileUrl": "https://www.example.com/community/members/trailmix/"
},
"date": "2026-10-01T07:40:00-04:00",
"upvotes": 9
},
{
"id": "5104",
"text": "[removed by moderator]",
"removed": true,
"author": {
"name": "spam99"
},
"date": "2026-10-01T08:00:00-04:00"
}
]
}
}
{
"@context": "https://schema.org",
"@graph": [
{
"@type": "QAPage",
"@id": "https://www.example.com/community/threads/ridgeline-shell-winter-layering/#webpage",
"url": "https://www.example.com/community/threads/ridgeline-shell-winter-layering/",
"name": "Is the Ridgeline shell warm enough for winter hiking? | Trailhead Community",
"isPartOf": {
"@id": "https://www.example.com/#website"
},
"mainEntity": {
"@type": "Question",
"name": "Is the Ridgeline shell warm enough for winter hiking?",
"text": "Planning day hikes in the White Mountains in January. Is the Ridgeline shell enough on its own, or what should I layer under it?",
"author": {
"@type": "Person",
"@id": "https://www.example.com/community/members/jkrause/#person",
"name": "jkrause",
"url": "https://www.example.com/community/members/jkrause/"
},
"datePublished": "2026-09-30T19:12:00-04:00",
"answerCount": 2,
"upvoteCount": 14,
"about": {
"@id": "https://www.example.com/jackets/ridgeline-rain-shell/#product-group"
},
"acceptedAnswer": {
"@type": "Answer",
"text": "On its own, no. It is a shell, so it blocks wind and rain but has no insulation. I wear a merino base layer and a light fleece under it and stay warm down to about 10F while moving.",
"author": {
"@type": "Person",
"@id": "https://www.example.com/community/members/maya-okafor/#person",
"name": "Maya Okafor",
"url": "https://www.example.com/community/members/maya-okafor/"
},
"datePublished": "2026-09-30T20:05:00-04:00",
"upvoteCount": 31,
"url": "https://www.example.com/community/threads/ridgeline-shell-winter-layering/#post-5102"
},
"suggestedAnswer": [
{
"@type": "Answer",
"text": "Add a puffy for breaks. You cool down fast the moment you stop, and the shell traps the warmth well over a down layer.",
"author": {
"@type": "Person",
"@id": "https://www.example.com/community/members/trailmix/#person",
"name": "trailmix",
"url": "https://www.example.com/community/members/trailmix/"
},
"datePublished": "2026-10-01T07:40:00-04:00",
"upvoteCount": 9,
"url": "https://www.example.com/community/threads/ridgeline-shell-winter-layering/#post-5103"
}
]
},
"breadcrumb": {
"@id": "https://www.example.com/community/threads/ridgeline-shell-winter-layering/#breadcrumb"
}
},
{
"@type": "BreadcrumbList",
"@id": "https://www.example.com/community/threads/ridgeline-shell-winter-layering/#breadcrumb",
"itemListElement": [
{
"@type": "ListItem",
"position": 1,
"name": "Home",
"item": "https://www.example.com/"
},
{
"@type": "ListItem",
"position": 2,
"name": "Community",
"item": "https://www.example.com/community/"
},
{
"@type": "ListItem",
"position": 3,
"name": "Gear questions",
"item": "https://www.example.com/community/gear-questions/"
},
{
"@type": "ListItem",
"position": 4,
"name": "Is the Ridgeline shell warm enough for winter hiking?",
"item": "https://www.example.com/community/threads/ridgeline-shell-winter-layering/"
}
]
}
]
}
The output above was run through the SchemaCDN validator, which checks every rule in Google's documentation for each feature. It was validated together with the homepage graph, so the @id links resolve. Result: no errors, and eligible for:
The anchor every other page type points at: Organization, site name and store-wide policies.
ProductGroup with a priced Offer per variant, member pricing and links to store-wide policies.
Article with a real Person author, the Organization as publisher, video and a paywalled section.
One store location with address, geo and hours, linked to its parent brand.
Recipe with timed steps, nutrition, ratings and a video whose key moments match the steps.
One open role with pay, closing date and work location, linked to the employer and the store.
An in-store workshop with times, a rescheduled date, the store as venue and a free ticket.