Skip to content

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.

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.

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

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 fields faq_title and faq_select (a post-object list of faq posts) and loops them into .block-faq-accordion__item elements.
  • Service pages (single-service.php:70-109) read the same-named fields off the service post and render the same markup inline, gated on display_faqs.

Schema collection is a two-phase, request-scoped process:

  1. A renderer calls bpcollins_collect_faq_schema_items($faq_posts), which normalises each entry to a WP_Post, skips anything not publish, strips the question to plain text, and flattens the answer with bpcollins_format_faq_schema_answer()do_shortcode(), then wp_strip_all_tags(), then entity decode, then whitespace collapse. Items are stored in the global $bpcollins_faq_schema_items keyed by md5($question), which de-duplicates repeated questions.
  2. On wp_footer (priority 5), bpcollins_output_faq_schema() builds a single FAQPage object with one Question/acceptedAnswer pair per item and echoes it as application/ld+json, encoded with JSON_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]

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.

  • Only the service template calls the collector. single-service.php:108 calls bpcollins_collect_faq_schema_items(); blocks/faq/block-faq.php does 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() at inc/faq-schema.php:64). A shortcode with side effects would fire an extra time.
  • $faq_title is 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.
  • $content is echoed in both renderers (block-faq.php:23, single-service.php:80). In the block it is the ACF InnerBlocks/$content variable 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.
  • To give the FAQ block schema output, call bpcollins_collect_faq_schema_items($faq_select) inside blocks/faq/block-faq.php after 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 FAQPage block on one page is worse than none.
  • Changing accordion markup → change both blocks/faq/block-faq.php and the inline copy in single-service.php, plus the accordion JS in blocks/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.