FAQ sections serve two purposes: they answer common questions for visitors and they can appear as rich results in Google Search. Here is how to build both.
Step 1: Define FAQ Data
// lib/faq.ts
export type FAQItem = {
question: string;
answer: string;
};
export const faqs: FAQItem[] = [
{
question: "How long does it take to build a website?",
answer:
"A typical business website takes 4-8 weeks from kickoff to launch. Complex web applications with custom features may take 3-6 months. We provide a detailed timeline during our initial consultation.",
},
{
question: "How much does a website cost?",
answer:
"Pricing depends on the scope and complexity of your project. Small business websites start around $3,000-$5,000. Custom web applications range from $10,000-$50,000+. Contact us for a detailed quote.",
},
{
question: "Do you offer website maintenance?",
answer:
"Yes. We offer monthly maintenance plans that include security updates, performance monitoring, content updates, and technical support. Plans start at $200/month.",
},
{
question: "Can you redesign my existing website?",
answer:
"Absolutely. We regularly redesign existing websites to improve performance, user experience, and conversions. We can work with your current platform or migrate to a new one.",
},
{
question: "Do you build mobile apps?",
answer:
"Yes. We develop cross-platform mobile applications using React Native, which allows us to build for both iOS and Android from a single codebase, reducing cost and time to market.",
},
];
Step 2: Build the Accordion Component
"use client";
import { useState } from "react";
import type { FAQItem } from "@/lib/faq";
function FAQAccordionItem({ item, isOpen, onToggle }: {
item: FAQItem;
isOpen: boolean;
onToggle: () => void;
}) {
return (
<div className="border-b dark:border-gray-700">
<button
onClick={onToggle}
className="flex w-full items-center justify-between py-5 text-left"
aria-expanded={isOpen}
>
<span className="text-lg font-medium text-gray-900 dark:text-white">
{item.question}
</span>
<svg
className={`h-5 w-5 flex-shrink-0 text-gray-500 transition-transform ${
isOpen ? "rotate-180" : ""
}`}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path strokeLinecap="round" strokeLinejoin="round" d="M19 9l-7 7-7-7" />
</svg>
</button>
<div
className={`overflow-hidden transition-all duration-300 ${
isOpen ? "max-h-96 pb-5" : "max-h-0"
}`}
>
<p className="text-gray-600 dark:text-gray-300">{item.answer}</p>
</div>
</div>
);
}
export function FAQAccordion({ items }: { items: FAQItem[] }) {
const [openIndex, setOpenIndex] = useState<number | null>(0);
return (
<div className="divide-y dark:divide-gray-700">
{items.map((item, index) => (
<FAQAccordionItem
key={index}
item={item}
isOpen={openIndex === index}
onToggle={() => setOpenIndex(openIndex === index ? null : index)}
/>
))}
</div>
);
}
Step 3: Add FAQ Schema Markup
// components/FAQSchema.tsx
import type { FAQItem } from "@/lib/faq";
export function FAQSchema({ items }: { items: FAQItem[] }) {
const schema = {
"@context": "https://schema.org",
"@type": "FAQPage",
mainEntity: items.map((item) => ({
"@type": "Question",
name: item.question,
acceptedAnswer: {
"@type": "Answer",
text: item.answer,
},
})),
};
return (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}
/>
);
}
Step 4: Assemble the FAQ Section
// app/faq/page.tsx (or as a section in any page)
import { faqs } from "@/lib/faq";
import { FAQAccordion } from "@/components/FAQAccordion";
import { FAQSchema } from "@/components/FAQSchema";
export default function FAQPage() {
return (
<>
<FAQSchema items={faqs} />
<section className="py-20">
<div className="mx-auto max-w-3xl px-6">
<h2 className="text-3xl font-bold text-gray-900 dark:text-white">
Frequently Asked Questions
</h2>
<p className="mt-4 text-gray-500 dark:text-gray-400">
Find answers to common questions about our services.
</p>
<div className="mt-10">
<FAQAccordion items={faqs} />
</div>
</div>
</section>
</>
);
}
Step 5: Add to Existing Pages with Metadata
Use Next.js metadata to include the schema on any page:
// app/services/page.tsx
import { faqs } from "@/lib/faq";
import { FAQSchema } from "@/components/FAQSchema";
import { FAQAccordion } from "@/components/FAQAccordion";
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "Our Services",
description: "Web design and development services.",
};
export default function ServicesPage() {
return (
<main>
<FAQSchema items={faqs} />
{/* Other page content */}
<section className="py-20">
<div className="mx-auto max-w-3xl px-6">
<h2 className="text-3xl font-bold">FAQ</h2>
<FAQAccordion items={faqs} />
</div>
</section>
</main>
);
}
Step 6: Test Your Schema
- Go to Google Rich Results Test
- Enter your page URL or paste the HTML
- Verify "FAQ" is detected as an eligible rich result
- Check for warnings or errors
Step 7: Use shadcn/ui Accordion (Alternative)
If you already use shadcn/ui:
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";
export function FAQAccordion({ items }: { items: FAQItem[] }) {
return (
<Accordion type="single" collapsible defaultValue="item-0">
{items.map((item, index) => (
<AccordionItem key={index} value={`item-${index}`}>
<AccordionTrigger>{item.question}</AccordionTrigger>
<AccordionContent>{item.answer}</AccordionContent>
</AccordionItem>
))}
</Accordion>
);
}
SEO Impact
FAQ rich results show expandable questions directly in Google Search results. This can significantly increase your click-through rate by taking up more space in the results page and providing immediate answers.
Note: Google may not always show FAQ rich results. They are more commonly displayed for authoritative, well-established pages.
Need SEO-Optimized Web Pages?
We build websites with structured data and schema markup to maximize search visibility. Contact us for SEO-driven web development.