Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/app/answers/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ export default async function AnswerPage({
]),
],
};
const faqLd = buildFaqPageJsonLd(faq, url);
const faqLd = buildFaqPageJsonLd(faq, url, null, ans.question);

return (
<article className="mx-auto max-w-3xl px-4 sm:px-6 py-8 sm:py-12">
Expand Down
7 changes: 6 additions & 1 deletion src/app/benchmarks/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -425,7 +425,12 @@ export default async function BenchmarkPage({
// don't leak into the SERP snippet. The visible FAQ section below
// mirrors every question/answer, satisfying Google's "content visible
// on the page" requirement.
const faqJsonLd = buildFaqPageJsonLd(benchmark.faq, benchmarkUrl);
const faqJsonLd = buildFaqPageJsonLd(
benchmark.faq,
benchmarkUrl,
null,
`${benchmark.title} — frequently asked questions`,
);

return (
<article className="mx-auto max-w-5xl w-full px-4 sm:px-6 pt-10 sm:pt-14 overflow-x-clip min-w-0">
Expand Down
7 changes: 6 additions & 1 deletion src/app/compare/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -772,10 +772,15 @@ export default async function ComparePage({
const faqJsonLd = {
"@context": "https://schema.org",
"@type": "FAQPage",
// Google's Rich Results validator flags missing `name` on the
// FAQPage parent even when Question.name is set. Add here + on
// Answer nodes so the compare page matches the shared FAQ builder
// in src/lib/jsonld.ts.
name: `${a.name} vs ${b.name} — frequently asked questions`,
mainEntity: faqEntries.map((e) => ({
"@type": "Question",
name: e.q,
acceptedAnswer: { "@type": "Answer", text: e.a },
acceptedAnswer: { "@type": "Answer", name: e.q, text: e.a },
})),
};

Expand Down
12 changes: 12 additions & 0 deletions src/lib/jsonld.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ export function buildFaqPageJsonLd(
faq: FaqItem[] | undefined,
pageUrl: string,
chain?: string | null,
pageName?: string,
): Record<string, unknown> | null {
if (!faq || faq.length === 0) return null;
const suffix = chainIdFragment(chain);
Expand All @@ -131,11 +132,22 @@ export function buildFaqPageJsonLd(
"@context": "https://schema.org",
"@type": "FAQPage",
"@id": id,
// FAQPage inherits `name` from CreativeWork. Google's Rich Results
// validator flags missing name as "Item: N/A" on the parent node
// even when Question.name is set on each entry. Callers pass a
// page-specific name; fallback preserves a valid string for any
// caller that predates this arg.
name: pageName ?? "Frequently asked questions",
mainEntity: faq.map((item) => ({
"@type": "Question",
name: stripFaqMarkdown(item.q),
acceptedAnswer: {
"@type": "Answer",
// Answer inherits `name` from CreativeWork too. Reusing the
// question text as the answer name is semantically accurate
// (this Answer is the answer TO this question) and closes the
// same "missing name" gap on the nested node.
name: stripFaqMarkdown(item.q),
text: stripFaqMarkdown(item.a),
},
})),
Expand Down
Loading