WP Manifestindependent plugin directory
manifest / editor / prc-quiz-builder

PRC Quiz Builder

A block-first, interactive quiz system for the PRC platform. Authors compose quizzes using Gutenberg blocks powered by the WordPress Interactivity API. Supports classic quizzes, typology clustering, embeddable usage, and community group results.

★ 3stars

Install

Download source zip No release zip yet

This is the repository archive. It installs, but the folder name will carry the branch suffix and updates will not flow.

Install straight from GitHub with WP-CLI:

wp plugin install https://github.com/pewresearch/prc-quiz-builder/archive/refs/heads/trunk.zip
No built-in updater. New versions won't appear under Dashboard → Updates. Install new releases yourself, or let Git Updater track the repository.

Canonical docs: docs/plugins/prc-quiz-builder/

An interactive, block-based quiz system for the PRC platform.

Overview

Quiz Builder provides a custom quiz post type and a suite of Gutenberg blocks backed by the WordPress Interactivity API. Authors compose quizzes in the block editor using a Controller → Pages → Page → Question → Answer hierarchy; the Controller block injects all runtime state into the Interactivity API context and coordinates submission, scoring, and results display on the frontend. The plugin supports three quiz modes (scored quiz, typology/clustering, freeform), three display types (paged, scrollable, and fluid), community group results, and iframe-embeddable views.

Firebase Realtime Database is the persistence layer for archetype (result) records and community groups. WordPress post meta tracks submission analytics.

Dependencies

  • Upstream: prc-platform-core (provides \PRC\Platform\Firebase and the prc_api_endpoints, prc_platform_on_post_init, and prc_iframe_content hooks), prc-research-teams (optional — registers team-prefixed quiz URLs via prc_research_teams_rewrite_config), WP Consent API (wp_add_cookie_info)
  • Downstream: Internal analytics tooling queries the _submissions REST field on the quiz post type; any page embedding a quiz via /embed/ or /iframe/ URLs

Architecture

The plugin bootstraps through includes/class-plugin.php, which loads all dependencies, registers the quiz CPT, sets up rewrite rules, and instantiates each block class. Blocks are loaded from build/ (production) or src/ (local/dev), controlled by wp_get_environment_type().

Archetype lookups are cached in the WordPress object cache (prc_quiz_builder_archetypes group, 1-day TTL) and backed by Firebase. Community groups are stored only in Firebase; Groups::get_group() reads from Firebase.

The Controller block's render_callback is the key server/client bridge: it writes all runtime context (quizId, quizType, displayType, configuredDisplayType, allowSubmissions, groupId, archetype, etc.) into data-wp-context and wires up all Interactivity API directives. configuredDisplayType is an immutable copy of the author's displayType setting; displayType may be rewritten at runtime (e.g. fluid → scrollable on narrow viewports). Core/buttons blocks that carry specific CSS classes (prc-quiz-start-button, prc-quiz-next-page-button, prc-quiz-submit-button, etc.) have data-wp-on--click attributes injected server-side via WP_HTML_Tag_Processor.

Key Files

Path Purpose
prc-quiz-builder.php Plugin entry point; defines PRC_QUIZ_FILE, PRC_QUIZ_DIR, PRC_QUIZ_VERSION constants
includes/class-plugin.php Core orchestrator — loads deps, registers CPT, rewrite rules, query vars, cookies, and all blocks
includes/class-archetypes.php Firebase CRUD for archetype (result hash) records; object-cache layer
includes/class-groups.php Firebase CRUD for community groups
includes/class-rest-api.php REST endpoint registration and handlers; contains the $rest_disabled kill switch
includes/class-analytics.php _report post meta schema and submission counter; exposes _submissions REST field
includes/class-ability-categories.php Registers the quiz WP Abilities category for MCP discovery
includes/class-ability.php WP Abilities API prc-quiz-builder/get-analytics tool (submissions + groups; MCP + REST)
includes/class-cli-report.php WP-CLI wp prc quiz report — ad hoc read/update of _report meta
includes/class-cli-build-audience.php WP-CLI wp prc quiz build-group-owners-audience — thin wrapper around Audience_Service
includes/class-audience-service.php Shared build / list / delete for quiz group-owners audiences (CLI + REST)
includes/class-loader.php Hook registration queue
includes/class-block-supports.php CPT-scoped inserter filtering (allowed_block_types_all), Quiz Builder category, editor-support enqueue
includes/editor-support/ Unregisters quiz core block variations outside the quiz CPT editor
includes/inspector-sidebar-panel/ Block editor plugin: quiz analytics, group analytics, and group-creators audience panel
src/controller/class-controller.php Controller block — server render, Interactivity API context injection, button directive patching
src/controller/view.js Controller Interactivity API store — display-type resolution, submission, page visibility, navigation
src/results/class-results.php Results block server render
src/group-results/ Group results block (view script + create-group action)
src/embeddable/ Embeddable block for cross-site reuse
build/ Compiled JS/CSS/asset manifests for all blocks

Blocks

Block Namespace Role
Controller prc-quiz/controller Root block; owns all Interactivity API state and submission flow
Pages prc-quiz/pages Wrapper for a multi-page quiz
Page prc-quiz/page A single page; contains questions and arbitrary content
Question prc-quiz/question Single-choice, multiple-choice, or thermometer; supports randomization
Answer prc-quiz/answer Answer choice with optional correctness, points, and label
Results prc-quiz/results Container rendered after quiz completion
Result Score prc-quiz/result-score Displays the participant's score
Result Table prc-quiz/result-table Tabular results view; supports demographic breaks
Result Histogram prc-quiz/result-histogram Score distribution histogram
Group Results prc-quiz/group-results Community group aggregate results; required to enable group creation
Progress Bar prc-quiz/progress-bar Linear bar or per-question circles showing completion and outcomes
Embeddable prc-quiz/embeddable Reuse a quiz across other posts; edits propagate to all embeds

Quiz blocks appear in the block inserter only when editing the quiz post type, grouped under the Quiz Builder category (prc-quiz slug). The Quiz embeddable block (prc-quiz/embeddable) remains available on other post types for synced cross-post reuse.

Knowledge quiz features (type: quiz)

These options apply only when the Controller type is quiz (not typology or freeform).

Live feedback and answer lock

Enable Live Feedback on the Controller block. After a participant selects an answer on a single-choice or thermometer question, the UI shows whether the choice is correct or incorrect and locks further changes on that question. Multiple-choice questions stay open so participants can select every required answer before locking.

Live feedback is implemented in src/answer/view.js via context.liveFeedback and state.isQuestionLocked.

Answer correctness tri-state

Knowledge-quiz answers use a tri-state correct attribute:

Value Meaning Toolbar label
true Correct answer Correct Answer
null Neutral / "Not sure" Not Sure
false Incorrect answer Incorrect Answer

The toolbar control lives in src/answer/correct-toggle.js. Freeform quizzes hide the control.

Question outcome

getQuestionOutcome() (src/controller/question-outcome.js) resolves each question to correct, incorrect, unsure, or unanswered:

  • Correct — selected UUIDs exactly match every answer marked correct: true
  • Unsure — only correct: null answers are selected (no wrong or right answers)
  • Incorrect — any other non-empty selection
  • Unanswered — no selection

The Results block exposes a Correct / Incorrect block bit (prc-quiz-builder/question-outcome-label) that prints the active question outcome from context. Labels come from the Quiz Controller (Correct / Incorrect / Not sure outcome label settings) and can be overridden per bit in the bit insert/edit popover. Empty bit fields inherit the quiz-wide defaults.

Any core/paragraph or core/heading that contains this bit receives the class has-prc-quiz-question-outcome and the interactive class is-awaiting-selection while the question is unanswered, so the whole host block stays hidden until the user has a selection.

Progress Circles use the same outcome logic for per-question marks.

Exclusive score buckets

The Controller Score Buckets panel stores a JSON catalog of named, non-overlapping closed ranges ([min, max] inclusive; shared endpoints count as overlap). Validation runs in the editor via validateExclusiveBuckets() (src/controller/score-buckets.js).

At results time, matchScoreBucket() returns the first bucket containing the participant's score. Insert the Matching Score Bucket block bit (prc-quiz-builder/matching-score-bucket) in the Results block to print the matched label.

Progress bar variations

prc-quiz/progress-bar has two block variations:

Variation Class Behavior
Progress Bar (default) (none) Linear fill showing percent of questions answered
Progress Circles is-style-circles One circle per question — check, x, or question mark by outcome

Circles reflect live outcomes when Live Feedback is enabled; otherwise they show answered vs unanswered.

Display Types and Frontend Behavior

The Controller block's displayType attribute accepts paged, scrollable, or fluid. Runtime behavior is implemented in src/controller/view.js and src/page/view.js.

Fluid resolution

A fluid quiz resolves to a concrete display mode by viewport width (782px breakpoint, matching the block editor's mobile preview). Resolution runs on init and re-runs on window resize via actions.applyDisplayType (data-wp-on-async-window--resize on the Controller when displayType is fluid):

Viewport Resolved displayType Notes
< 782px scrollable Next-page button wrappers hidden within the quiz container
>= 782px paged Next-page button wrappers shown; one page at a time

configuredDisplayType retains the original fluid value so client logic can distinguish a fluid quiz that resolved to scrollable from a natively scrollable quiz, and so resize handlers can re-resolve without losing the fluid configuration.

Page visibility (displayPages)

The Pages block binds hidden to !state.displayPages (src/pages/class-pages.php). Visibility rules:

Configuration During quiz Results or group-results URL
paged Pages visible Pages hidden
fluid → paged (desktop) Pages visible Pages hidden
scrollable (native) Pages always visible Pages always visible
fluid → scrollable (mobile) Pages visible Pages hidden (matches paged)

Native scrollable quizzes keep pages visible on results URLs so inline, submit-as-you-go results can render below the questions. Fluid quizzes on mobile hide pages when landing on a results URL so users are not dropped at the top of the question stack.

Navigation buttons

Quiz action buttons are core/buttons block variations patched with Interactivity API click handlers:

CSS class Action Behavior
prc-quiz-start-button onStartQuizClick Sets currentPageUuid to the first question page (pages[1]). In non-paged modes, scrolls smoothly to that page via [data-page-uuid].
prc-quiz-next-page-button onNextPageClick Advances to the next page; scrolls the quiz container into view (paged only).
prc-quiz-previous-page-button onPreviousPageClick Returns to the previous page.
prc-quiz-submit-button onSubmitQuizClick Submits the quiz (triggers Mailchimp form when present).
prc-quiz-reset-button onResetQuizClick Resets quiz state or navigates back to the quiz URL from results.

Submission behavior

Submission is controlled by the allowSubmissions Controller attribute and the active display type:

allowSubmissions Display type Submit trigger
true paged / fluid → paged Explicit Submit button on the last page
true scrollable / fluid → scrollable Explicit Submit button on the last page (no auto-submit on answer threshold)
false scrollable / fluid → scrollable Auto-submits via onScrollableSubmit when the user meets threshold
false paged Submit button still required

When submissions are enabled, scrollable and fluid-on-mobile quizzes require a prc-quiz-submit-button on the last page. Quizzes with submissions disabled and a native scrollable display type advance to results automatically once the answer threshold is met.

Results display scroll

When results become visible, onResultsDisplay in src/results/view.js scrolls the results block into view (scrollIntoView with block: 'start'). This applies to all display types, including fluid quizzes on mobile: after submit, question pages hide and the viewport moves to the top of the results block so users are not left scrolled to the middle of the page.

Hooks & Filters

Hook Type Description
allowed_block_types_all filter Hides prc-quiz/* blocks outside the quiz CPT editor (except prc-quiz/embeddable)
block_categories_all filter Appends the prc-quiz block category on the quiz CPT editor only
prc_api_endpoints filter Registers quiz/create-group, quiz/get-group, quiz/submit, and quiz/purge-archetypes REST routes
init action Registers all quiz URL patterns (results, group, embed) via add_rewrite_rule
query_vars filter Registers quizArchetype, quizGroup, quizGroupDomain, quizShowResults, quizShareQuiz, quizEmbed
prc_research_teams_rewrite_config filter Injects quiz URL patterns for research-team-prefixed routes (e.g. /politics/quiz/...)
prc_platform_on_post_init action Creates the Firebase quiz entry when a new quiz post is initialized
prc_iframe_content filter Wraps quiz content with a PRC-branded header in iframe/embed views
render_block_context filter Injects prc-quiz/id into block context for prc-quiz/controller on singular quiz pages
render_block_core/buttons filter Patches Interactivity API data-wp-on--click onto quiz action buttons by CSS class
prc_quiz_log_submission action Fired on quiz submit; consumed internally by Analytics to increment _report post meta
enqueue_block_editor_assets action Enqueues editor-support outside the quiz CPT and the inspector sidebar panel on quiz CPT only

REST API

All endpoints are registered through the platform's prc_api_endpoints filter. Public write endpoints validate the quiz post (exists, quiz post type, published) and apply per-IP rate limiting on submit.

Method Route Auth Description
POST quiz/submit Rate limit + quiz validation Records a submission; creates or increments the archetype in Firebase; updates group if groupId is present
POST quiz/create-group Firebase ID token (X-PRC-User-Token) Creates a community group in Firebase; owner is always the verified token's sub claim — any ownerId in the request body is ignored; returns { group_id, group_url }
GET quiz/get-group Public Returns full group data including typology clusters, answer tallies, and result/group URLs
POST quiz/purge-archetypes manage_options Admin-only; wipes all archetypes for a quiz from Firebase

The quiz REST resource also exposes a _submissions field containing the _report post meta (requires edit_posts capability).

WP Abilities API

Ability ID Input Description
prc-quiz-builder/get-analytics post_id (integer, required) Returns { post_id, title, submissions, groups } — submission report from _report meta plus community group analytics. Requires edit_post on that quiz. If Firebase is unavailable, groups includes an error field while submissions still returns. Exposed via REST and MCP.

WP-CLI

Requires manage_options. All mutation subcommands support --dry-run.

Subcommand Description
wp prc quiz report get Print current _report for a quiz (--format=table\|json)
wp prc quiz report set Set absolute month or day counts (--year, --month, optional --day, --month-count, --total)
wp prc quiz report add Add a delta to month or day counts (same flags as set)
wp prc quiz report set Set absolute month and/or total counts
wp prc quiz report add Add a delta to month and/or total counts
wp prc quiz report sync-firebase Sum Firebase archetype hits and apply to month + total (--mode=delta\|set, default delta)
# Inspect report
wp prc quiz report get --quiz-id=313764

# Backfill from Firebase (delta mode adds firebase_hits - current_total)
wp prc quiz report sync-firebase --quiz-id=313764 --year=2026 --month=06 --dry-run
wp prc quiz report sync-firebase --quiz-id=313764 --year=2026 --month=06

# Manual correction
wp prc quiz report add --quiz-id=313764 --year=2026 --month=06 --month-count=500 --total=500
wp prc quiz report set --quiz-id=313764 --year=2026 --month=06 --month-count=125653 --total=125653

sync-firebase loads the full quiz/{id}/archetypes node; large quizzes may be slow. first_24_hours and first_week are not modified by these commands.

URL Patterns

Pattern Query Vars
/quiz/{slug}/ quiz={slug}
/quiz/{slug}/results/{hash}/ quiz, quizArchetype, quizShowResults=true
/quiz/{slug}/group/{group-id}/ quiz, quizGroup
/quiz/{slug}/group/{group-id}/results/ quiz, quizGroup, quizShowResults=true
/quiz/{slug}/group/{group-id}/results/{hash}/ quiz, quizGroup, quizArchetype, quizShowResults=true
/quiz/{slug}/group/{domain}/{group-id}/results/ quiz, quizGroup, quizGroupDomain, quizShowResults=true
/quiz/{slug}/embed/ or /quiz/{slug}/iframe/ quiz, iframe=true

Research-team-prefixed variants (e.g. /politics/quiz/{slug}/...) follow the same sub-patterns, registered via prc_research_teams_rewrite_config.

Data Storage

Store Key / Path Contents
Firebase quiz/{quiz_id}/archetypes/{hash} { score, submission, hits }
Firebase quiz/{quiz_id}/groups/{group_id} Group metadata, cluster tallies, answer tallies, total
Firebase users/{owner_id}/groups/{group_id} Group index per user
WP post meta _report Submission counts: first 24 hrs, first week, total, by year/month, and (from 2026-08-01) day buckets under _days
WP object cache MD5 of {quiz_id, hash} Cached archetype lookup; group prc_quiz_builder_archetypes; TTL 1 day

Cookies

Both cookies are registered with WP Consent API as functional, 30-day expiry.

Cookie Contents
prc-quiz-builder Quiz progress JSON: answers, scores, archetype hash, completion timestamp
prc-quiz-builder__typology Typology answers and assigned group for personalization

Admin: Quizzes DataViews list

Quizzes → All Quizzes opens a DataViews screen (prc-quiz-builder-library) instead of the classic edit.php list table. The list reads mirrored post meta (_prc_quiz_type, _prc_quiz_display_type, _prc_quiz_groups_enabled, _prc_quiz_question_count) synced on save because quiz configuration lives in block attributes inside post_content.

  • Open analytics for a quiz from the list actions (same modal as the editor inspector sidebar).
  • Escape hatch: append ?classic=1 to edit.php?post_type=quiz to use the classic list table (Trash and other status views redirect with the same filter preserved).

Implementation: includes/class-quiz-list.php + GET quiz/library (via prc_api_endpoints).

Local Development

# From the monorepo root:
npx turbo build --filter=@prc/quiz-builder
npm run start -w @prc/quiz-builder

To run Playwright tests (from the monorepo root — VIP dev-env and Playwright are all centralized at root):

npm run vip:start
npm test -- tests/prc-quiz-builder/e2e/

Firebase availability

Archetype persistence and community groups require Firebase Realtime Database. Archetypes::is_available() and Groups guard all writes when prc-firebase is not configured or the service account is missing.

Submit behavior when Firebase is down

Quiz type quiz/submit behavior
Normal (no groupId) Returns { hash, time, persisted: false } with HTTP 200 — in-session results still work; archetype hits are not stored
Group (groupId present) Returns HTTP 503 — shared group tallies cannot be updated without Firebase

Group creation (quiz/create-group) returns HTTP 503 when Firebase Auth is unavailable.

Local Firebase setup

Service account generation is documented in docs/dependency-auth.md (npm run gen:firebase-sa). prc-firebase reads WPCOM_VIP_PRIVATE_DIR/firebase-service-account.json.

Troubleshooting

Blocks not appearing after a build

Symptom: prc-quiz/controller (or any quiz block) is missing from the editor or throws a "block is invalid" error.
Cause: load_blocks() globs build/*/ for block directories and calls include_block() on each. If a compiled PHP file is missing from build/{block-name}/class-{block-name}.php, the error is logged but swallowed silently — the block just won't register.
Fix: Run npx turbo build --filter=@prc/quiz-builder and confirm PHP files exist under build/. Check PHP error logs for Block missing. entries.

Quiz submissions failing silently

Symptom: Users complete a quiz but results don't persist; the submit endpoint returns a 403, 404, 429, or generic error.
Cause: Rest_API::$rest_disabled may be flipped to true (emergency kill switch in class-rest-api.php), the quiz post may be unpublished or missing, or the per-IP rate limit (100 submissions per quiz per minute) may be exceeded.
Fix: If the kill switch is active, set $rest_disabled = false and redeploy. Confirm the quiz post is published. For 429 responses, wait and retry or investigate abusive traffic.

Group quizzes fail with 503

Symptom: Group submit or create-group returns "results service is temporarily unavailable."
Cause: Firebase Realtime Database or Auth is not configured (private/firebase-service-account.json missing locally, or deploy injection skipped).
Fix: Run npm run gen:firebase-sa locally; confirm prc-firebase loads the service account on the target environment. Normal (non-group) quizzes may still return in-session results with persisted: false.

Inspector sidebar panel not loading

Symptom: No quiz analytics panel appears in the block editor sidebar.
Cause: Inspector_Sidebar_Panel::enqueue_block_plugin_assets() only enqueues on screens where $screen->post_type === 'quiz'. It also checks is_admin(). Opening the editor from a non-quiz post type silently skips enqueue.
Fix: Confirm you are editing a quiz post type, not a page or another CPT that embeds the quiz via prc-quiz/embeddable.