// faq-article-page.jsx → qa-page.jsx (renamed in v1.36.0 for the question-answer CPT)
// Pattern: a single question is answered authoritatively on one page,
// surrounded by structured supporting context (key points, applicability,
// related topics, follow-up Q&A, assessment CTA).
//
// Data flow (v1.36.0+):
// 1. PHP renders the page via templates/template-claude-page.php
// 2. JSX root mounts
// 3. QaSingleApp extracts the post slug from window.location.pathname
// 4. Fetches /wp-json/claude-pages/v1/qa/{slug} (see includes/rest/qa-endpoints.php)
// 5. Passes the returned data object as `data` prop to and
//
// The design's hardcoded FAQ_DATA constant has been removed in favor of REST-driven data.
// The shape returned by the REST endpoint matches what these components expect:
// { category, breadcrumb[], updated, readTime, question, tldr, answer[],
// keyPoints[], whenApplies[], related[{label,href}], followups[{q,a}], assessmentBlurb }
// ============================================================
// FAQ Article Hero — Wise Blue with subtle radial light
// Mirrors the dark-hero pattern from Solutions pages but
// designed for a single question to read clearly as the H1.
// ============================================================
const FaqArticleHero = ({ data }) => (
{/* Subtle radial glow — references illumination, very restrained */}
{/* Hairline grid lines — very faint vertical accents */}
{/* Breadcrumb / category */}
{data.breadcrumb.map((b, i) => (
{b}
{i < data.breadcrumb.length - 1 && (
/
)}
))}
{/* Question — the H1 */}
{data.question}
{/* Short answer / TL;DR */}
{/* Meta strip */}
);
const MetaItem = ({ label, value }) => (
{label}
{value}
);
// ============================================================
// FAQ Article Body — two-column layout
// Left: article (Clear Answer → Key Points → When This Applies → Related Q&A → Assessment)
// Right: sticky On-this-page TOC + Related Topics
// ============================================================
const FaqArticleBody = ({ data }) => {
const [open, setOpen] = React.useState(0);
return (
{/* ============ ARTICLE COLUMN ============ */}
{/* ----- Clear Answer Explained ----- */}
{data.answer.map((p, i) => (
{p}
))}
{/* ----- Key Points ----- */}
{data.keyPoints.map((kp, i) => (
{String(i + 1).padStart(2, '0')}
{kp}
))}
{/* ----- When This Applies ----- */}
{data.whenApplies.map((w, i) => (
{w}
))}
{/* ----- Related Questions (mini accordion) ----- */}
{data.followups.map((it, i) => {
const isOpen = open === i;
return (
setOpen(isOpen ? -1 : i)}
style={{
width: '100%', background: 'transparent', border: 'none',
padding: '26px 0', cursor: 'pointer', textAlign: 'left',
display: 'grid', gridTemplateColumns: '52px 1fr 32px', gap: 20,
alignItems: 'center'
}}>
Q·{String(i + 2).padStart(2, '0')}
{it.q}
+
);
})}
{/* ============ SIDE RAIL ============ */}
);
};
// Sub-section anchor + eyebrow header used inside the article
const SectionAnchor = ({ id, eyebrow, title }) => (
);
const Divider = () => (
);
// ============================================================
// QaSingleApp — REST-driven root wrapper (added v1.36.0)
//
// Reads the post slug from window.location.pathname (URL shape:
// /question-answer/{slug}/) and fetches the matching Q&A from
// /wp-json/claude-pages/v1/qa/{slug}. While loading, renders a
// minimal skeleton hero. On error renders a polite empty state.
// Once resolved, hands the data object to and
// .
//
// Defensive: any of the array/object fields may be missing if the
// admin hasn't populated all ACF fields — the components fall back
// gracefully (see in-component guards mirroring project-single-page).
// ============================================================
const QaSingleApp = () => {
const [state, setState] = React.useState({ loading: true, error: null, data: null });
React.useEffect(() => {
// Slug comes from URL: /question-answer/{slug}/ → extract {slug}
const path = window.location.pathname.replace(/\/$/, '');
const m = path.match(/\/question-answer\/([a-z0-9-]+)$/i);
const slug = m ? m[1] : null;
if (!slug) {
setState({ loading: false, error: 'No Q&A slug in URL', data: null });
return;
}
const base = (window.GI_REST_BASE || '/wp-json/claude-pages/v1/').replace(/\/?$/, '/');
fetch(base + 'qa/' + encodeURIComponent(slug), { headers: { 'Accept': 'application/json' } })
.then(r => {
if (!r.ok) throw new Error('HTTP ' + r.status);
return r.json();
})
.then(data => setState({ loading: false, error: null, data }))
.catch(err => setState({ loading: false, error: String(err), data: null }));
}, []);
// Lucide re-init after content is rendered
React.useEffect(() => {
if (state.data && window.lucide) {
window.lucide.createIcons({ attrs: { 'stroke-width': 1.5 } });
}
}, [state.data]);
if (state.loading) {
return (
);
}
if (state.error || !state.data) {
return (
This Q&A isn't available.
The page you're looking for may have been moved or is being prepared. Return home .
);
}
return (
<>
>
);
};
// Normalize REST response so missing ACF fields don't crash the components.
// Every array field becomes `[]`, every string becomes `''`. The components
// then naturally render nothing for empty sections.
const normalizeQaData = (d) => ({
category: d.category || '',
breadcrumb: Array.isArray(d.breadcrumb) ? d.breadcrumb : ['Resources', 'Q&A'],
updated: d.updated || '',
readTime: d.readTime || '',
question: d.question || '',
tldr: d.tldr || '',
answer: Array.isArray(d.answer) ? d.answer : [],
keyPoints: Array.isArray(d.keyPoints) ? d.keyPoints : [],
whenApplies: Array.isArray(d.whenApplies) ? d.whenApplies : [],
related: Array.isArray(d.related) ? d.related : [],
followups: Array.isArray(d.followups) ? d.followups : [],
assessmentBlurb: d.assessmentBlurb || '',
});
Object.assign(window, { FaqArticleHero, FaqArticleBody, QaSingleApp });