This component renders an FAQ section and its Astro FAQ schema from one array of questions and answers. The visible part uses <details> and <summary>, so it needs no JavaScript, and the structured data is a FAQPage JSON-LD block generated from the same data, so the markup and schema can never drift apart. Drop it on any page that answers real questions.
Key Takeaways
- One data source feeds both the visible FAQ and the JSON-LD.
- Use
set:htmlwithJSON.stringify()to output the schema.- Answers in the schema must match what’s on the page.
- Write questions the way people actually ask them; they also suit AI answer engines.
The component
---
// src/components/FAQ.astro
interface QA { question: string; answer: string }
interface Props { items: QA[]; heading?: string }
const { items, heading = 'Frequently asked questions' } = Astro.props;
const schema = {
'@context': 'https://schema.org',
'@type': 'FAQPage',
mainEntity: items.map(({ question, answer }) => ({
'@type': 'Question',
name: question,
acceptedAnswer: { '@type': 'Answer', text: answer },
})),
};
---
<section class="faq" aria-labelledby="faq-heading">
<h2 id="faq-heading">{heading}</h2>
{items.map(({ question, answer }) => (
<details class="faq-item">
<summary>{question}</summary>
<p>{answer}</p>
</details>
))}
</section>
<script type="application/ld+json" set:html={JSON.stringify(schema)} />
<style>
.faq-item { border-bottom: 1px solid var(--color-border, #e5e4df); padding: 0.25rem 0; }
.faq-item summary { padding: 0.9rem 0; font-weight: 600; cursor: pointer; }
.faq-item p { margin: 0 0 1rem; }
</style>
Using it
---
import FAQ from '../components/FAQ.astro';
const faqs = [
{
question: 'How do I get the theme after buying?',
answer: 'You receive an invitation to a private GitHub repository at your order email.',
},
{
question: 'Can I use a theme for client projects?',
answer: 'Yes. The licence allows unlimited sites for yourself or paying clients.',
},
];
---
<FAQ items={faqs} />
Keep FAQs in a data file or a content collection when several pages share them.
Answers with links or formatting
The schema’s text field accepts plain text (and some basic HTML). If you want links in the visible answer, store an HTML answer and render it with set:html on the page; strip tags for the schema:
const plain = (html: string) => html.replace(/<[^>]+>/g, '');
// acceptedAnswer: { '@type': 'Answer', text: plain(answer) }
<p set:html={answer} />
Only do this with HTML you wrote; never with user input.
Rules worth following
- Visible and marked up must match. Don’t add questions to the schema that aren’t on the page.
- Real questions. Phrase them as readers would type or say them.
- One FAQPage per page. If a page has several FAQ blocks, combine them into one schema object.
- Don’t expect guaranteed rich results. Search engines decide when to show FAQ rich results; the markup still helps them understand the page.
Validating
Paste the page URL into Google’s Rich Results Test, or check the JSON in the page source: view-source: and search for FAQPage.
The structured data approach is covered in the Astro SEO checklist, set:html in Fragment and set:html, and the collapsible pattern in the accordion component. Google’s FAQ structured data documentation lists the requirements.
FAQ
Do I need FAQPage schema for an FAQ section?
No, but it helps search engines interpret question-and-answer content, and costs nothing when generated from the same data.
Can I use this in MDX posts?
Yes. Import the component in the MDX file and pass the questions as a prop.