I’ll make this as a reusable advanced FAQ block template for RX Theme, with safe escaping, schema JSON-LD, accordion behavior, block attributes, ACF fallback, accessibility, and theme-friendly class naming.
Yes dear friend. Create this file:
/template-parts/blocks/block-faq.php
This is an advanced reusable FAQ block for RX Theme. It supports:
Advanced features included: $args support, ACF repeater support, post meta fallback, FAQ schema JSON-LD, accessible accordion, search/filter inside FAQ, expand/collapse all, copy answer button, deep-link support, RTL-friendly markup, safe escaping, unique block IDs, lazy JS, no jQuery dependency.
<?php
/**
* Template Part: Advanced FAQ Block
*
* File: /template-parts/blocks/block-faq.php
*
* Usage examples:
*
* 1. Basic:
* get_template_part( 'template-parts/blocks/block', 'faq' );
*
* 2. With custom args:
* get_template_part(
* 'template-parts/blocks/block',
* 'faq',
* array(
* 'title' => 'Frequently Asked Questions',
* 'subtitle' => 'Helpful answers for readers.',
* 'description' => 'Find common questions and simple answers.',
* 'faqs' => array(
* array(
* 'question' => 'What is RX Theme?',
* 'answer' => 'RX Theme is a custom WordPress theme.',
* ),
* ),
* )
* );
*
* 3. ACF supported field names:
* - rx_faq_title
* - rx_faq_subtitle
* - rx_faq_description
* - rx_faq_items repeater
* - question
* - answer
*
* @package RX_Theme
*/
defined( 'ABSPATH' ) || exit;
/**
* ---------------------------------------------------------
* 1. Safe default args
* ---------------------------------------------------------
*/
$rx_faq_defaults = array(
'id' => '',
'title' => '',
'subtitle' => '',
'description' => '',
'faqs' => array(),
'layout' => 'accordion', // accordion, list.
'style' => 'default', // default, card, minimal.
'columns' => 1,
'show_search' => true,
'show_expand_all' => true,
'show_copy_button' => false,
'open_first' => false,
'allow_multiple_open' => true,
'enable_schema' => true,
'enable_deep_link' => true,
'enable_count' => true,
'heading_level' => 2,
'item_heading_level' => 3,
'empty_message' => __( 'No frequently asked questions found.', 'rx-theme' ),
'search_placeholder' => __( 'Search questions...', 'rx-theme' ),
'expand_all_text' => __( 'Expand all', 'rx-theme' ),
'collapse_all_text' => __( 'Collapse all', 'rx-theme' ),
'copy_text' => __( 'Copy', 'rx-theme' ),
'copied_text' => __( 'Copied!', 'rx-theme' ),
'no_result_text' => __( 'No matching questions found.', 'rx-theme' ),
'custom_class' => '',
'anchor' => '',
'background' => '',
'text_align' => '',
'container' => true,
'aria_label' => __( 'Frequently asked questions', 'rx-theme' ),
);
$rx_faq_args = isset( $args ) && is_array( $args ) ? wp_parse_args( $args, $rx_faq_defaults ) : $rx_faq_defaults;
/**
* ---------------------------------------------------------
* 2. Gutenberg block support when passed by render callback
* ---------------------------------------------------------
*/
if ( isset( $rx_faq_args['block'] ) && is_array( $rx_faq_args['block'] ) ) {
$rx_block = $rx_faq_args['block'];
if ( empty( $rx_faq_args['id'] ) && ! empty( $rx_block['id'] ) ) {
$rx_faq_args['id'] = 'rx-faq-' . sanitize_html_class( $rx_block['id'] );
}
if ( empty( $rx_faq_args['anchor'] ) && ! empty( $rx_block['anchor'] ) ) {
$rx_faq_args['anchor'] = sanitize_title( $rx_block['anchor'] );
}
if ( ! empty( $rx_block['className'] ) ) {
$rx_faq_args['custom_class'] .= ' ' . sanitize_html_class( $rx_block['className'] );
}
if ( ! empty( $rx_block['align'] ) ) {
$rx_faq_args['custom_class'] .= ' align' . sanitize_html_class( $rx_block['align'] );
}
}
/**
* ---------------------------------------------------------
* 3. ACF field fallback
* ---------------------------------------------------------
*/
if ( function_exists( 'get_field' ) ) {
$rx_acf_title = get_field( 'rx_faq_title' );
$rx_acf_subtitle = get_field( 'rx_faq_subtitle' );
$rx_acf_description = get_field( 'rx_faq_description' );
$rx_acf_items = get_field( 'rx_faq_items' );
if ( empty( $rx_faq_args['title'] ) && ! empty( $rx_acf_title ) ) {
$rx_faq_args['title'] = $rx_acf_title;
}
if ( empty( $rx_faq_args['subtitle'] ) && ! empty( $rx_acf_subtitle ) ) {
$rx_faq_args['subtitle'] = $rx_acf_subtitle;
}
if ( empty( $rx_faq_args['description'] ) && ! empty( $rx_acf_description ) ) {
$rx_faq_args['description'] = $rx_acf_description;
}
if ( empty( $rx_faq_args['faqs'] ) && is_array( $rx_acf_items ) ) {
$rx_faq_args['faqs'] = $rx_acf_items;
}
}
/**
* ---------------------------------------------------------
* 4. Post meta fallback
* ---------------------------------------------------------
*
* Supported meta:
* - rx_faq_items as array
* - rx_faq_items as JSON string
*/
if ( empty( $rx_faq_args['faqs'] ) ) {
$rx_meta_faqs = get_post_meta( get_the_ID(), 'rx_faq_items', true );
if ( is_string( $rx_meta_faqs ) ) {
$rx_decoded = json_decode( $rx_meta_faqs, true );
if ( json_last_error() === JSON_ERROR_NONE && is_array( $rx_decoded ) ) {
$rx_meta_faqs = $rx_decoded;
}
}
if ( is_array( $rx_meta_faqs ) && ! empty( $rx_meta_faqs ) ) {
$rx_faq_args['faqs'] = $rx_meta_faqs;
}
}
/**
* ---------------------------------------------------------
* 5. Demo fallback only for Customizer preview/admin preview
* ---------------------------------------------------------
*/
if ( empty( $rx_faq_args['faqs'] ) && ( is_customize_preview() || is_admin() ) ) {
$rx_faq_args['faqs'] = array(
array(
'question' => __( 'What is this FAQ block?', 'rx-theme' ),
'answer' => __( 'This is an advanced reusable FAQ block for RX Theme. You can pass FAQ items by args, ACF, or post meta.', 'rx-theme' ),
),
array(
'question' => __( 'Does this block support FAQ schema?', 'rx-theme' ),
'answer' => __( 'Yes. It can automatically print FAQPage JSON-LD schema for better structured data support.', 'rx-theme' ),
),
);
}
/**
* ---------------------------------------------------------
* 6. Normalize FAQ data
* ---------------------------------------------------------
*/
$rx_normalized_faqs = array();
if ( is_array( $rx_faq_args['faqs'] ) ) {
foreach ( $rx_faq_args['faqs'] as $rx_index => $rx_item ) {
if ( ! is_array( $rx_item ) ) {
continue;
}
$rx_question = '';
if ( isset( $rx_item['question'] ) ) {
$rx_question = $rx_item['question'];
} elseif ( isset( $rx_item['title'] ) ) {
$rx_question = $rx_item['title'];
} elseif ( isset( $rx_item['q'] ) ) {
$rx_question = $rx_item['q'];
}
$rx_answer = '';
if ( isset( $rx_item['answer'] ) ) {
$rx_answer = $rx_item['answer'];
} elseif ( isset( $rx_item['content'] ) ) {
$rx_answer = $rx_item['content'];
} elseif ( isset( $rx_item['a'] ) ) {
$rx_answer = $rx_item['a'];
}
$rx_category = isset( $rx_item['category'] ) ? $rx_item['category'] : '';
$rx_icon = isset( $rx_item['icon'] ) ? $rx_item['icon'] : '';
$rx_open = isset( $rx_item['open'] ) ? (bool) $rx_item['open'] : false;
$rx_question = wp_strip_all_tags( $rx_question );
$rx_answer = trim( (string) $rx_answer );
if ( '' === $rx_question || '' === $rx_answer ) {
continue;
}
$rx_normalized_faqs[] = array(
'question' => $rx_question,
'answer' => $rx_answer,
'category' => sanitize_text_field( $rx_category ),
'icon' => sanitize_text_field( $rx_icon ),
'open' => $rx_open,
);
}
}
$rx_faq_args['faqs'] = $rx_normalized_faqs;
/**
* ---------------------------------------------------------
* 7. Helper values
* ---------------------------------------------------------
*/
$rx_block_id = ! empty( $rx_faq_args['id'] )
? sanitize_html_class( $rx_faq_args['id'] )
: 'rx-faq-' . wp_unique_id();
$rx_anchor_id = ! empty( $rx_faq_args['anchor'] )
? sanitize_title( $rx_faq_args['anchor'] )
: $rx_block_id;
$rx_heading_level = absint( $rx_faq_args['heading_level'] );
$rx_heading_level = $rx_heading_level >= 1 && $rx_heading_level <= 6 ? $rx_heading_level : 2;
$rx_item_heading_level = absint( $rx_faq_args['item_heading_level'] );
$rx_item_heading_level = $rx_item_heading_level >= 2 && $rx_item_heading_level <= 6 ? $rx_item_heading_level : 3;
$rx_faq_count = count( $rx_faq_args['faqs'] );
$rx_classes = array(
'rx-block',
'rx-block-faq',
'rx-faq',
'rx-faq--layout-' . sanitize_html_class( $rx_faq_args['layout'] ),
'rx-faq--style-' . sanitize_html_class( $rx_faq_args['style'] ),
'rx-faq--columns-' . absint( $rx_faq_args['columns'] ),
);
if ( ! empty( $rx_faq_args['custom_class'] ) ) {
$rx_classes[] = sanitize_html_class( trim( $rx_faq_args['custom_class'] ) );
}
if ( ! empty( $rx_faq_args['background'] ) ) {
$rx_classes[] = 'has-background';
$rx_classes[] = 'has-' . sanitize_html_class( $rx_faq_args['background'] ) . '-background';
}
if ( ! empty( $rx_faq_args['text_align'] ) ) {
$rx_classes[] = 'has-text-align-' . sanitize_html_class( $rx_faq_args['text_align'] );
}
$rx_wrapper_classes = implode( ' ', array_filter( $rx_classes ) );
$rx_data_settings = array(
'allowMultipleOpen' => (bool) $rx_faq_args['allow_multiple_open'],
'enableDeepLink' => (bool) $rx_faq_args['enable_deep_link'],
'copyText' => (string) $rx_faq_args['copy_text'],
'copiedText' => (string) $rx_faq_args['copied_text'],
'noResultText' => (string) $rx_faq_args['no_result_text'],
);
/**
* ---------------------------------------------------------
* 8. Schema JSON-LD
* ---------------------------------------------------------
*/
$rx_schema = array();
if ( ! empty( $rx_faq_args['enable_schema'] ) && ! empty( $rx_faq_args['faqs'] ) ) {
$rx_schema = array(
'@context' => 'https://schema.org',
'@type' => 'FAQPage',
'mainEntity' => array(),
);
foreach ( $rx_faq_args['faqs'] as $rx_schema_item ) {
$rx_schema['mainEntity'][] = array(
'@type' => 'Question',
'name' => wp_strip_all_tags( $rx_schema_item['question'] ),
'acceptedAnswer' => array(
'@type' => 'Answer',
'text' => wp_strip_all_tags( $rx_schema_item['answer'] ),
),
);
}
}
?>
<section
id="<?php echo esc_attr( $rx_anchor_id ); ?>"
class="<?php echo esc_attr( $rx_wrapper_classes ); ?>"
aria-label="<?php echo esc_attr( $rx_faq_args['aria_label'] ); ?>"
data-rx-faq="<?php echo esc_attr( wp_json_encode( $rx_data_settings ) ); ?>"
>
<?php if ( ! empty( $rx_faq_args['container'] ) ) : ?>
<div class="rx-container rx-faq__container">
<?php endif; ?>
<?php if ( ! empty( $rx_faq_args['subtitle'] ) || ! empty( $rx_faq_args['title'] ) || ! empty( $rx_faq_args['description'] ) ) : ?>
<header class="rx-faq__header">
<?php if ( ! empty( $rx_faq_args['subtitle'] ) ) : ?>
<p class="rx-faq__subtitle">
<?php echo esc_html( $rx_faq_args['subtitle'] ); ?>
</p>
<?php endif; ?>
<?php if ( ! empty( $rx_faq_args['title'] ) ) : ?>
<<?php echo esc_attr( 'h' . $rx_heading_level ); ?> class="rx-faq__title">
<?php echo esc_html( $rx_faq_args['title'] ); ?>
<?php if ( ! empty( $rx_faq_args['enable_count'] ) && $rx_faq_count > 0 ) : ?>
<span class="rx-faq__count" aria-label="<?php echo esc_attr( sprintf( _n( '%s question', '%s questions', $rx_faq_count, 'rx-theme' ), number_format_i18n( $rx_faq_count ) ) ); ?>">
<?php echo esc_html( number_format_i18n( $rx_faq_count ) ); ?>
</span>
<?php endif; ?>
</<?php echo esc_attr( 'h' . $rx_heading_level ); ?>>
<?php endif; ?>
<?php if ( ! empty( $rx_faq_args['description'] ) ) : ?>
<div class="rx-faq__description">
<?php echo wp_kses_post( wpautop( $rx_faq_args['description'] ) ); ?>
</div>
<?php endif; ?>
</header>
<?php endif; ?>
<?php if ( ! empty( $rx_faq_args['faqs'] ) ) : ?>
<?php if ( ! empty( $rx_faq_args['show_search'] ) || ! empty( $rx_faq_args['show_expand_all'] ) ) : ?>
<div class="rx-faq__toolbar">
<?php if ( ! empty( $rx_faq_args['show_search'] ) ) : ?>
<div class="rx-faq__search-wrap">
<label class="screen-reader-text" for="<?php echo esc_attr( $rx_block_id ); ?>-search">
<?php esc_html_e( 'Search FAQ questions', 'rx-theme' ); ?>
</label>
<input
id="<?php echo esc_attr( $rx_block_id ); ?>-search"
class="rx-faq__search"
type="search"
placeholder="<?php echo esc_attr( $rx_faq_args['search_placeholder'] ); ?>"
autocomplete="off"
data-rx-faq-search
/>
</div>
<?php endif; ?>
<?php if ( ! empty( $rx_faq_args['show_expand_all'] ) ) : ?>
<div class="rx-faq__actions">
<button
type="button"
class="rx-faq__action rx-faq__expand-all"
data-rx-faq-expand-all
>
<?php echo esc_html( $rx_faq_args['expand_all_text'] ); ?>
</button>
<button
type="button"
class="rx-faq__action rx-faq__collapse-all"
data-rx-faq-collapse-all
>
<?php echo esc_html( $rx_faq_args['collapse_all_text'] ); ?>
</button>
</div>
<?php endif; ?>
</div>
<?php endif; ?>
<div class="rx-faq__items" data-rx-faq-items>
<?php foreach ( $rx_faq_args['faqs'] as $rx_index => $rx_faq ) : ?>
<?php
$rx_item_number = $rx_index + 1;
$rx_item_id = $rx_block_id . '-item-' . $rx_item_number;
$rx_button_id = $rx_item_id . '-button';
$rx_panel_id = $rx_item_id . '-panel';
$rx_is_open = false;
if ( ! empty( $rx_faq['open'] ) ) {
$rx_is_open = true;
}
if ( ! empty( $rx_faq_args['open_first'] ) && 0 === $rx_index ) {
$rx_is_open = true;
}
$rx_question_plain = wp_strip_all_tags( $rx_faq['question'] );
$rx_answer_plain = wp_strip_all_tags( $rx_faq['answer'] );
$rx_search_text = strtolower( $rx_question_plain . ' ' . $rx_answer_plain . ' ' . $rx_faq['category'] );
?>
<article
id="<?php echo esc_attr( $rx_item_id ); ?>"
class="rx-faq__item<?php echo $rx_is_open ? ' is-open' : ''; ?>"
data-rx-faq-item
data-rx-faq-text="<?php echo esc_attr( $rx_search_text ); ?>"
<?php if ( ! empty( $rx_faq['category'] ) ) : ?>
data-rx-faq-category="<?php echo esc_attr( $rx_faq['category'] ); ?>"
<?php endif; ?>
>
<<?php echo esc_attr( 'h' . $rx_item_heading_level ); ?> class="rx-faq__question-heading">
<button
id="<?php echo esc_attr( $rx_button_id ); ?>"
class="rx-faq__question"
type="button"
aria-expanded="<?php echo $rx_is_open ? 'true' : 'false'; ?>"
aria-controls="<?php echo esc_attr( $rx_panel_id ); ?>"
data-rx-faq-button
>
<span class="rx-faq__question-inner">
<?php if ( ! empty( $rx_faq['icon'] ) ) : ?>
<span class="rx-faq__icon" aria-hidden="true">
<?php echo esc_html( $rx_faq['icon'] ); ?>
</span>
<?php endif; ?>
<span class="rx-faq__number" aria-hidden="true">
<?php echo esc_html( sprintf( '%02d', $rx_item_number ) ); ?>
</span>
<span class="rx-faq__question-text">
<?php echo esc_html( $rx_faq['question'] ); ?>
</span>
<?php if ( ! empty( $rx_faq['category'] ) ) : ?>
<span class="rx-faq__category">
<?php echo esc_html( $rx_faq['category'] ); ?>
</span>
<?php endif; ?>
</span>
<span class="rx-faq__toggle-icon" aria-hidden="true">
<span class="rx-faq__toggle-line rx-faq__toggle-line--horizontal"></span>
<span class="rx-faq__toggle-line rx-faq__toggle-line--vertical"></span>
</span>
</button>
</<?php echo esc_attr( 'h' . $rx_item_heading_level ); ?>>
<div
id="<?php echo esc_attr( $rx_panel_id ); ?>"
class="rx-faq__answer"
role="region"
aria-labelledby="<?php echo esc_attr( $rx_button_id ); ?>"
<?php echo $rx_is_open ? '' : 'hidden'; ?>
data-rx-faq-panel
>
<div class="rx-faq__answer-inner">
<?php echo wp_kses_post( wpautop( do_shortcode( $rx_faq['answer'] ) ) ); ?>
<?php if ( ! empty( $rx_faq_args['show_copy_button'] ) ) : ?>
<button
type="button"
class="rx-faq__copy"
data-rx-faq-copy
data-rx-faq-copy-text="<?php echo esc_attr( $rx_answer_plain ); ?>"
>
<?php echo esc_html( $rx_faq_args['copy_text'] ); ?>
</button>
<?php endif; ?>
</div>
</div>
</article>
<?php endforeach; ?>
</div>
<p class="rx-faq__no-results" hidden data-rx-faq-no-results>
<?php echo esc_html( $rx_faq_args['no_result_text'] ); ?>
</p>
<?php else : ?>
<p class="rx-faq__empty">
<?php echo esc_html( $rx_faq_args['empty_message'] ); ?>
</p>
<?php endif; ?>
<?php if ( ! empty( $rx_faq_args['container'] ) ) : ?>
</div>
<?php endif; ?>
</section>
<?php if ( ! empty( $rx_schema ) ) : ?>
<script type="application/ld+json">
<?php echo wp_json_encode( $rx_schema, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT ); ?>
</script>
<?php endif; ?>
<?php
/**
* ---------------------------------------------------------
* 9. Inline script printed once
* ---------------------------------------------------------
*/
if ( ! defined( 'RX_THEME_FAQ_BLOCK_SCRIPT_PRINTED' ) ) :
define( 'RX_THEME_FAQ_BLOCK_SCRIPT_PRINTED', true );
?>
<script>
(function () {
'use strict';
const faqBlocks = document.querySelectorAll('[data-rx-faq]');
if (!faqBlocks.length) {
return;
}
const safeJSON = function (value) {
try {
return JSON.parse(value || '{}');
} catch (error) {
return {};
}
};
const normalize = function (value) {
return String(value || '').toLowerCase().trim();
};
const openItem = function (item) {
const button = item.querySelector('[data-rx-faq-button]');
const panel = item.querySelector('[data-rx-faq-panel]');
if (!button || !panel) {
return;
}
item.classList.add('is-open');
button.setAttribute('aria-expanded', 'true');
panel.hidden = false;
};
const closeItem = function (item) {
const button = item.querySelector('[data-rx-faq-button]');
const panel = item.querySelector('[data-rx-faq-panel]');
if (!button || !panel) {
return;
}
item.classList.remove('is-open');
button.setAttribute('aria-expanded', 'false');
panel.hidden = true;
};
const copyText = async function (text) {
if (!navigator.clipboard || !text) {
return false;
}
try {
await navigator.clipboard.writeText(text);
return true;
} catch (error) {
return false;
}
};
faqBlocks.forEach(function (block) {
const settings = safeJSON(block.getAttribute('data-rx-faq'));
const items = Array.from(block.querySelectorAll('[data-rx-faq-item]'));
const searchInput = block.querySelector('[data-rx-faq-search]');
const expandAllButton = block.querySelector('[data-rx-faq-expand-all]');
const collapseAllButton = block.querySelector('[data-rx-faq-collapse-all]');
const noResults = block.querySelector('[data-rx-faq-no-results]');
items.forEach(function (item) {
const button = item.querySelector('[data-rx-faq-button]');
if (!button) {
return;
}
button.addEventListener('click', function () {
const isOpen = button.getAttribute('aria-expanded') === 'true';
if (!settings.allowMultipleOpen) {
items.forEach(function (otherItem) {
if (otherItem !== item) {
closeItem(otherItem);
}
});
}
if (isOpen) {
closeItem(item);
} else {
openItem(item);
if (settings.enableDeepLink && item.id) {
const newUrl = window.location.pathname + window.location.search + '#' + item.id;
window.history.replaceState(null, '', newUrl);
}
}
});
});
if (searchInput) {
searchInput.addEventListener('input', function () {
const query = normalize(searchInput.value);
let visibleCount = 0;
items.forEach(function (item) {
const text = normalize(item.getAttribute('data-rx-faq-text'));
const isMatch = !query || text.indexOf(query) !== -1;
item.hidden = !isMatch;
if (isMatch) {
visibleCount++;
}
});
if (noResults) {
noResults.hidden = visibleCount !== 0;
}
});
}
if (expandAllButton) {
expandAllButton.addEventListener('click', function () {
items.forEach(function (item) {
if (!item.hidden) {
openItem(item);
}
});
});
}
if (collapseAllButton) {
collapseAllButton.addEventListener('click', function () {
items.forEach(function (item) {
closeItem(item);
});
});
}
block.addEventListener('click', function (event) {
const copyButton = event.target.closest('[data-rx-faq-copy]');
if (!copyButton) {
return;
}
const originalText = copyButton.textContent;
const text = copyButton.getAttribute('data-rx-faq-copy-text');
copyText(text).then(function (success) {
if (!success) {
return;
}
copyButton.textContent = settings.copiedText || 'Copied!';
window.setTimeout(function () {
copyButton.textContent = originalText || settings.copyText || 'Copy';
}, 1600);
});
});
if (settings.enableDeepLink && window.location.hash) {
const target = block.querySelector(window.location.hash);
if (target && target.matches('[data-rx-faq-item]')) {
openItem(target);
window.setTimeout(function () {
target.scrollIntoView({
behavior: 'smooth',
block: 'start'
});
}, 100);
}
}
});
})();
</script>
<?php endif; ?>
Use it like this in any template:
<?php
get_template_part(
'template-parts/blocks/block',
'faq',
array(
'title' => 'Frequently Asked Questions',
'subtitle' => 'RX Theme Help Center',
'description' => 'Common questions and answers for visitors.',
'show_search' => true,
'show_expand_all' => true,
'show_copy_button' => true,
'open_first' => true,
'enable_schema' => true,
'faqs' => array(
array(
'question' => 'What is RX Theme?',
'answer' => 'RX Theme is a custom WordPress theme built for medical content, performance, SEO, and advanced design.',
'category' => 'Theme',
'icon' => '❓',
),
array(
'question' => 'Does RX Theme support FAQ schema?',
'answer' => 'Yes. This FAQ block automatically creates FAQPage JSON-LD schema when schema is enabled.',
'category' => 'SEO',
'icon' => '✅',
),
),
)
);
?>
Recommended CSS file name for this block:
/assets/css/blocks/block-faq.css
Recommended JS file name if you later separate inline JavaScript:
/assets/js/blocks/block-faq.js