Skip to main content
Back to Blog
Tutorials
3 min read
November 26, 2024

How to Create an FAQ Section with Schema Markup

Build an accessible FAQ accordion component with FAQ schema markup for rich results in Google Search.

Ryel Banfield

Founder & Lead Developer

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

  1. Go to Google Rich Results Test
  2. Enter your page URL or paste the HTML
  3. Verify "FAQ" is detected as an eligible rich result
  4. 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.

FAQschema markupSEOJSON-LDtutorial

Ready to Start Your Project?

RCB Software builds world-class websites and applications for businesses worldwide.

Get in Touch

Related Articles