Blog//7 min read
FAQ section with FAQPage JSON-LD in Next.js
Add FAQ schema to a Next.js page: one FAQPage per page, JSON-LD generated from the same array that renders the accordion, tested in Rich Results.
MSMarco Schulz, founder of Facade UI
An FAQ section is the cheapest SEO work on a local business site, and the one I see done wrong most often. The accordion shows six questions, the JSON-LD in the head lists four different ones, and the schema was copied from a generator a year ago. Google reads the structured data, compares it to the visible page, and ignores both. Here is how I build the FAQ section so the schema and the page can never drift apart, and what the FAQPage type can and cannot do for you in 2026.
Why the schema has to match the visible text
Google's structured data guidelines are explicit on one point: the content in your markup must be visible on the page, and it must represent the page's main content. For FAQPage that means every question and answer in the JSON-LD must appear as text a visitor can read, not hidden behind a "show more" that never renders, and not padded with extra questions that only exist in the script tag.
The practical consequence is a rule I apply on every project: the schema and the accordion render from the same array. Not two arrays that happen to match today. One items array, mapped twice, once into HTML and once into JSON. If someone edits a question in the CMS, both change in the same deploy.
The second thing worth knowing is scope. Since 2023 Google shows FAQ rich results mainly for well known government and health sites. For a roofing company or a dentist the expanded FAQ snippet in the search results is unlikely. I still ship FAQPage, for two reasons: it costs nothing when generated from the array, and AI search tools and assistants read it to extract question and answer pairs. When a client asks why I bother, I show them their own FAQ quoted back by an assistant.
One FAQPage per page
FAQPage describes a page, not a section. Google's guidance is to mark up a question only once across the site when the same question and answer appear on several pages, and in practice one FAQPage block per page is the setup that validates cleanly. Two mistakes follow from ignoring this:
- A site wide FAQ in the footer that emits FAQPage on every URL. Now 40 pages claim the same ten questions.
- A long page with two FAQ sections, each with its own script tag.
My setup: one FAQ block per page, with questions specific to that page. The roofing home page gets questions about estimates and insurance claims. The storm damage service page gets questions about tarping and claim timelines. Each page is its own FAQPage, and no question appears twice. The roofer page shows where the block sits in a full page order.
The Facade block has jsonLd on by default. If a page already has a FAQPage from another source, pass jsonLd={false} and let that source own it.
Accordion accessibility
The schema is for machines. The accordion is for people, and most accordions fail the people who use a keyboard or a screen reader. The pattern I follow is the WAI-ARIA accordion pattern:
- Each question is a
<button>inside a heading element, so the questions appear in the document outline and a screen reader user can jump between them. - The button carries
aria-expandedwith the current state andaria-controlspointing at the answer panel. - The panel is a
role="region"labelled by its button, so when focus moves into it the context is announced. - Arrow up and down move focus between questions, Home and End jump to the first and last.
The open and close animation runs on height and opacity through motion's AnimatePresence. When the visitor has reduced motion enabled, the component switches to a plain fade with no height change, which avoids the layout movement that makes some people dizzy.
The standalone Smooth accordion uses the same ARIA pattern for cases where you do not want the two column section layout, for example an FAQ inside a pricing table or a service page sidebar. It takes a headingLevel prop so the question headings fit your page outline instead of all being h3.
Generating JSON-LD from the same array
Here is the whole thing on a Next.js page. The faqs array is the single source. The block renders the accordion and the script tag from it.
import { FaqSection, type FaqItem } from "@/components/facade/faq-section";
const faqs: FaqItem[] = [
{
question: "Does insurance cover storm damage to my roof?",
answer:
"Most homeowners policies cover sudden storm damage such as hail or wind. We document the damage, meet your adjuster on site and provide the itemized estimate your insurer asks for.",
},
{
question: "How fast can you tarp a leaking roof?",
answer:
"Same day in most of the Denver metro. Emergency tarping is scheduled before any inspection or estimate so the inside of your home stays dry.",
},
{
question: "Do you offer financing?",
answer: "Yes, through a third party lender with terms from 12 to 60 months. Approval takes a few minutes and does not affect the estimate.",
},
];
export default function StormDamagePage() {
return (
<FaqSection
eyebrow="Storm damage"
heading="Questions Denver homeowners ask after a storm"
description="Straight answers before you call."
items={faqs}
contactHeading="Still unsure?"
contactText="Call and a roofer, not a call center, picks up."
phone="(303) 555-0142"
email="hello@summitroofing.com"
ctaLabel="Request an inspection"
ctaHref="/inspection"
defaultOpen={0}
multiple={false}
jsonLd
/>
);
}Inside the block, the schema is built with useMemo from items: a FAQPage whose mainEntity is one Question per item, each with its name and an acceptedAnswer carrying the answer text, serialized with JSON.stringify.
Two details matter when you write this yourself. First, escape < in the serialized JSON as \u003c, or an answer containing </script> closes your script tag early. Second, render the script tag inside the component. In the Next.js App Router a <script type="application/ld+json"> anywhere in the body is valid, and keeping it next to the accordion guarantees they ship together.
If answers contain links or lists, the Answer text field accepts a small set of tags (h1 to h6, br, ol, ul, li, a, p, div, b, strong, i, em). Keep visible answer and schema answer identical, markup included.
Testing in Rich Results
Run the deployed URL through Google's Rich Results Test. You want one FAQPage detected with the same number of questions as the accordion shows. Then check three things:
- View the page source and search for
application/ld+json. There should be exactly one FAQPage block. - Expand every accordion item and compare the text to the schema. Same words, same punctuation.
- Test the page with JavaScript disabled or view the server rendered HTML. The script tag and the questions must be in the initial response, not injected after hydration. With a server rendered Next.js page this is automatic, but a client only component wrapped in a dynamic import with
ssr: falsewould lose it.
After deploying, Search Console lists the markup under its FAQ enhancement report once Google recrawls. Errors there usually mean a mismatch between visible and marked up text, which the single array prevents.
Installing
The FAQ section and the Smooth accordion are both free and MIT licensed:
npx shadcn@latest add https://facadeui.com/r/faq-section.json
npx shadcn@latest add https://facadeui.com/r/smooth-accordion.jsonPlace the FAQ block after the proof sections and before the final call to action. Visitors who read down to the FAQ are close to a decision, and the contact column on the right of the block is there to catch them.
FAQ
Should I put FAQPage on my home page?
Only if the home page has an FAQ section visible to visitors. If it does, keep the questions about the business as a whole and move service specific questions to the service pages, each with its own FAQPage.
Can I mark up questions that are collapsed by default?
Yes. Content inside a collapsed accordion counts as visible as long as the user can expand it and the text is in the HTML. Do not mark up content that is only loaded on click from an API.
Does FAQPage help with AI search?
It helps the extraction. Assistants and AI search features pull question and answer pairs from structured data more reliably than from arbitrary HTML, and they often quote the answer text as written.