Skip to content
thefaqappthefaqapp
Browse docs

Next.js App Router

Render thefaqapp content in a Next.js 15+ app with SSR, ISR, and useFaq hooks.

Updated 2026-05-19

The most common integration: a Next.js app rendering FAQ content on a public marketing or help surface.

bun add @faqapp/core @faqapp/react

Server-side render a list

// app/help/page.tsx
import { createFAQClient } from "@faqapp/core";

export const revalidate = 300; // ISR — refresh every 5 minutes

export default async function HelpPage() {
  const faq = createFAQClient({
    apiKey: process.env.FAQAPP_API_KEY!,
    organizationSlug: "acme"
  });

  const { data: questions } = await faq.questions.list({ limit: 50 });

  return (
    <main>
      <h1>Help</h1>
      {questions.map((q) => (
        <article key={q.id}>
          <h2>{q.title}</h2>
          <p>{q.answer}</p>
        </article>
      ))}
    </main>
  );
}

The SDK is dependency-free, so it ships fine to RSC. FAQAPP_API_KEY is a server key with read scope. It stays server-side: never prefix it with NEXT_PUBLIC_.

Per-question dynamic route

// app/help/[slug]/page.tsx
import { createFAQClient } from "@faqapp/core";
import { notFound } from "next/navigation";

export async function generateStaticParams() {
  const faq = createFAQClient({
    apiKey: process.env.FAQAPP_API_KEY!,
    organizationSlug: "acme"
  });
  const { data } = await faq.questions.list({ limit: 100 });
  return data.map((q) => ({ slug: q.slug }));
}

export default async function QuestionPage({ params }) {
  const faq = createFAQClient({
    apiKey: process.env.FAQAPP_API_KEY!,
    organizationSlug: "acme"
  });

  try {
    const { data: q } = await faq.questions.get(params.slug);
    return (
      <article>
        <h1>{q.title}</h1>
        <p>{q.answer}</p>
      </article>
    );
  } catch (err) {
    if (err.code === "question_not_found") notFound();
    throw err;
  }
}

Client-side hooks (interactive surfaces)

For client-side search, expanding rows, AI ask, or user-specific filtering:

// app/help/SearchableList.tsx
"use client";
import { createFAQClient, FAQClientProvider, useSearch } from "@faqapp/react";
import { useState } from "react";

const faq = createFAQClient({
  organizationSlug: "acme",
  apiKey: process.env.NEXT_PUBLIC_FAQAPP_PUBLISHABLE_KEY!
});

export function SearchableList() {
  return (
    <FAQClientProvider client={faq}>
      <Inner />
    </FAQClientProvider>
  );
}

function Inner() {
  const [q, setQ] = useState("");
  const { results } = useSearch(q);
  return (
    <div>
      <input value={q} onChange={(e) => setQ(e.target.value)} />
      {results?.results.map((x) => (
        <article key={x.id}>
          <h3>{x.question}</h3>
          <p>{x.answer}</p>
        </article>
      ))}
    </div>
  );
}

A client component ships its key to every visitor, so use a publishable key here. It reads published FAQs and search only. A read key is not safe in the browser: it reads drafts and private product facts. Keep server keys in variables without the NEXT_PUBLIC_ prefix.

Revalidate on publish

Wire a webhook from thefaqapp to a Next.js Route Handler that calls revalidatePath:

// app/api/faqapp-webhook/route.ts
import { revalidatePath } from "next/cache";

export async function POST(req: Request) {
  const sig = req.headers.get("X-FAQApp-Signature");
  // Verify signature against your webhook secret
  // ...
  const event = await req.json();
  if (event.type === "question.published") {
    revalidatePath("/help");
    revalidatePath(`/help/${event.question.slug}`);
  }
  return new Response("ok");
}

Add the webhook URL in your dashboard under Settings → Webhooks. The signing secret lives in your env as FAQAPP_WEBHOOK_SECRET.