FAQs and FAQPage schema
FAQs are reusable question/answer entries editors attach to service pages or any
page via a block. Wherever FAQ markup renders, the theme also emits a single
FAQPage JSON-LD block for the page.
Why it exists
Section titled “Why it exists”The same questions (“how long does probate take?”) recur across several service
pages, so answers live once in a faq post type and are selected per page rather
than retyped. The schema output was added so those Q&As are machine-readable.
The code itself notes the caveat: Google restricted FAQ rich results to
government and health sites in 2023 (inc/faq-schema.php:16-17), so the JSON-LD
is emitted for correctness and other consumers, not for an expected rich result.
Entry points
Section titled “Entry points”| Path | What it does |
|---|---|
wp-content/themes/bpcollins/inc/cpt.php:441 |
faq post type — non-public, admin-only content source |
wp-content/themes/bpcollins/blocks/faq/block-faq.php |
The FAQ accordion block, usable on any page |
wp-content/themes/bpcollins/single-service.php:70-109 |
The service-page FAQ accordion (separate implementation, same markup) |
wp-content/themes/bpcollins/inc/faq-schema.php:25 |
bpcollins_collect_faq_schema_items() — registers Q&As for this request |
wp-content/themes/bpcollins/inc/faq-schema.php:117 |
bpcollins_output_faq_schema() — hooked to wp_footer priority 5 |
How it works
Section titled “How it works”faq is registered 'public' => false, 'show_in_rest' => false, supporting
only title and editor (inc/cpt.php:441-474). The title is the question, the
content is the answer. There is no front-end template for a single FAQ, and no
archive — entries only ever appear embedded in another page.
Two renderers exist:
- The block (
blocks/faq/block-faq.php) reads its own ACF fieldsfaq_titleandfaq_select(a post-object list offaqposts) and loops them into.block-faq-accordion__itemelements. - Service pages (
single-service.php:70-109) read the same-named fields off the service post and render the same markup inline, gated ondisplay_faqs.
Schema collection is a two-phase, request-scoped process:
- A renderer calls
bpcollins_collect_faq_schema_items($faq_posts), which normalises each entry to aWP_Post, skips anything notpublish, strips the question to plain text, and flattens the answer withbpcollins_format_faq_schema_answer()—do_shortcode(), thenwp_strip_all_tags(), then entity decode, then whitespace collapse. Items are stored in the global$bpcollins_faq_schema_itemskeyed bymd5($question), which de-duplicates repeated questions. - On
wp_footer(priority 5),bpcollins_output_faq_schema()builds a singleFAQPageobject with oneQuestion/acceptedAnswerpair per item and echoes it asapplication/ld+json, encoded withJSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT.
If nothing was collected, nothing is output — pages without FAQ markup get no FAQ
schema (inc/faq-schema.php:124).
flowchart TD A[faq CPT entries: title = question, content = answer] --> B[block-faq.php OR single-service.php] B --> C[accordion markup in the page] B --> D[bpcollins_collect_faq_schema_items] D --> E[global $bpcollins_faq_schema_items keyed by md5 question] E --> F[wp_footer priority 5] F --> G[single FAQPage JSON-LD in the body]Configuration
Section titled “Configuration”No settings or environment variables. Editor-facing fields only: faq_title,
faq_select on the block, and display_faqs + faq_title + faq_select on a
service post.
Invariants and gotchas
Section titled “Invariants and gotchas”- Only the service template calls the collector.
single-service.php:108callsbpcollins_collect_faq_schema_items();blocks/faq/block-faq.phpdoes not. So a page using the FAQ block renders the accordion but emits no FAQPage schema. If someone reports “schema missing on a non-service page”, this is why — add the collector call to the block. - Schema is output in the body, not the head, because it is hooked to
wp_footer. That is valid JSON-LD placement, but it means the schema is absent from anything that only parses<head>. - Unpublished FAQs are skipped silently (
inc/faq-schema.php:39) — an editor selecting a draft sees it in the accordion markup (the renderers do not check status) but not in the schema. - Answers are flattened to plain text for the schema, and shortcodes are
executed during flattening (
do_shortcode()atinc/faq-schema.php:64). A shortcode with side effects would fire an extra time. $faq_titleis reused as the loop variable in both renderers (block-faq.php:35,single-service.php:91), overwriting the heading value after the first iteration. Both currently render the heading before the loop, so it is latent, not broken.$contentis echoed in both renderers (block-faq.php:23,single-service.php:80). In the block it is the ACF InnerBlocks/$contentvariable available to ACF block render templates; in the service template it is never set and outputs nothing.- Duplicate questions across the two renderers on one page collapse into one schema entry (md5 keying) but render twice visually.
Changing it safely
Section titled “Changing it safely”- To give the FAQ block schema output, call
bpcollins_collect_faq_schema_items($faq_select)insideblocks/faq/block-faq.phpafter the loop. Collection is idempotent per question, so calling it from more renderers is safe. - New FAQ placement → render the accordion markup, then call the collector.
Never build the JSON-LD yourself; a second
FAQPageblock on one page is worse than none. - Changing accordion markup → change both
blocks/faq/block-faq.phpand the inline copy insingle-service.php, plus the accordion JS inblocks/faq/block-faq.js, then rebuild with gulp. - Deliberately not abstracted: the accordion markup exists twice because the service template pre-dates the block. Unifying it is safe and welcome, but it touches every service page — verify at all three service depths.
- Verify with Google’s Rich Results Test or by searching the page source for
"@type":"FAQPage".
None. No automated tests exist in this repository.