I’ll build this as a full advanced inc/post-types/faq.php module for your RX theme: FAQ custom post type, taxonomy, admin columns, meta boxes, schema JSON-LD, shortcode, REST support, rewrite rules, and theme-safe loading hooks.
Below is an advanced inc/post-types/faq.php file for your RX Theme. It includes:
FAQ custom post type, FAQ category taxonomy, FAQ tag taxonomy, custom meta fields, admin columns, shortcode, schema JSON-LD, REST API support, search integration, archive sorting, FAQ accordion output, and reusable helper functions.
WordPress recommends registering custom post types on the init hook, and show_in_rest => true makes the post type available in the block editor and REST API. Shortcodes are registered with add_shortcode(), and JSON output can safely use wp_json_encode().
Create this file:
<?php
/**
* RX Theme FAQ Post Type
*
* File: inc/post-types/faq.php
*
* Features:
* - FAQ custom post type
* - FAQ category taxonomy
* - FAQ tag taxonomy
* - FAQ meta fields
* - Admin columns
* - FAQ shortcode
* - FAQ schema JSON-LD
* - Archive query optimization
* - Search integration
* - REST API support
* - Theme helper functions
*
* @package RX_Theme
*/
defined( 'ABSPATH' ) || exit;
if ( ! class_exists( 'RX_Theme_FAQ_Post_Type' ) ) :
final class RX_Theme_FAQ_Post_Type {
/**
* Post type key.
*/
public const POST_TYPE = 'rx_faq';
/**
* Category taxonomy key.
*/
public const TAX_CATEGORY = 'rx_faq_category';
/**
* Tag taxonomy key.
*/
public const TAX_TAG = 'rx_faq_tag';
/**
* Meta keys.
*/
public const META_FEATURED = '_rx_faq_featured';
public const META_ORDER = '_rx_faq_order';
public const META_SHORT_ANSWER = '_rx_faq_short_answer';
public const META_REVIEWED_BY = '_rx_faq_reviewed_by';
public const META_REVIEWED_DATE = '_rx_faq_reviewed_date';
public const META_READING_LEVEL = '_rx_faq_reading_level';
public const META_SOURCE_URL = '_rx_faq_source_url';
public const META_SCHEMA_ENABLED = '_rx_faq_schema_enabled';
/**
* Singleton instance.
*
* @var self|null
*/
private static ?self $instance = null;
/**
* Get instance.
*/
public static function instance() : self {
if ( null === self::$instance ) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Constructor.
*/
private function __construct() {
add_action( 'init', array( $this, 'register_post_type' ), 5 );
add_action( 'init', array( $this, 'register_taxonomies' ), 6 );
add_action( 'init', array( $this, 'register_meta' ), 7 );
add_action( 'add_meta_boxes', array( $this, 'add_meta_boxes' ) );
add_action( 'save_post_' . self::POST_TYPE, array( $this, 'save_meta_boxes' ), 10, 2 );
add_filter( 'manage_' . self::POST_TYPE . '_posts_columns', array( $this, 'admin_columns' ) );
add_action( 'manage_' . self::POST_TYPE . '_posts_custom_column', array( $this, 'admin_column_content' ), 10, 2 );
add_filter( 'manage_edit-' . self::POST_TYPE . '_sortable_columns', array( $this, 'sortable_columns' ) );
add_action( 'pre_get_posts', array( $this, 'admin_orderby_columns' ) );
add_filter( 'post_updated_messages', array( $this, 'updated_messages' ) );
add_filter( 'enter_title_here', array( $this, 'title_placeholder' ) );
add_shortcode( 'rx_faq', array( $this, 'faq_shortcode' ) );
add_action( 'wp_head', array( $this, 'output_single_faq_schema' ), 20 );
add_action( 'wp_head', array( $this, 'output_archive_faq_schema' ), 21 );
add_filter( 'template_include', array( $this, 'template_loader' ) );
add_filter( 'the_content', array( $this, 'append_single_faq_extra_content' ) );
add_action( 'restrict_manage_posts', array( $this, 'admin_taxonomy_filter' ) );
add_filter( 'parse_query', array( $this, 'admin_taxonomy_filter_query' ) );
add_filter( 'wp_sitemaps_post_types', array( $this, 'add_to_wp_sitemap' ) );
}
/**
* Register FAQ post type.
*/
public function register_post_type() : void {
$labels = array(
'name' => esc_html__( 'FAQs', 'rx-theme' ),
'singular_name' => esc_html__( 'FAQ', 'rx-theme' ),
'add_new' => esc_html__( 'Add New', 'rx-theme' ),
'add_new_item' => esc_html__( 'Add New FAQ', 'rx-theme' ),
'edit_item' => esc_html__( 'Edit FAQ', 'rx-theme' ),
'new_item' => esc_html__( 'New FAQ', 'rx-theme' ),
'view_item' => esc_html__( 'View FAQ', 'rx-theme' ),
'view_items' => esc_html__( 'View FAQs', 'rx-theme' ),
'search_items' => esc_html__( 'Search FAQs', 'rx-theme' ),
'not_found' => esc_html__( 'No FAQs found.', 'rx-theme' ),
'not_found_in_trash' => esc_html__( 'No FAQs found in Trash.', 'rx-theme' ),
'all_items' => esc_html__( 'All FAQs', 'rx-theme' ),
'archives' => esc_html__( 'FAQ Archives', 'rx-theme' ),
'attributes' => esc_html__( 'FAQ Attributes', 'rx-theme' ),
'insert_into_item' => esc_html__( 'Insert into FAQ', 'rx-theme' ),
'uploaded_to_this_item' => esc_html__( 'Uploaded to this FAQ', 'rx-theme' ),
'featured_image' => esc_html__( 'FAQ Image', 'rx-theme' ),
'set_featured_image' => esc_html__( 'Set FAQ Image', 'rx-theme' ),
'remove_featured_image' => esc_html__( 'Remove FAQ Image', 'rx-theme' ),
'use_featured_image' => esc_html__( 'Use as FAQ Image', 'rx-theme' ),
'menu_name' => esc_html__( 'FAQs', 'rx-theme' ),
'filter_items_list' => esc_html__( 'Filter FAQs list', 'rx-theme' ),
'items_list_navigation' => esc_html__( 'FAQs list navigation', 'rx-theme' ),
'items_list' => esc_html__( 'FAQs list', 'rx-theme' ),
'item_published' => esc_html__( 'FAQ published.', 'rx-theme' ),
'item_published_privately' => esc_html__( 'FAQ published privately.', 'rx-theme' ),
'item_reverted_to_draft' => esc_html__( 'FAQ reverted to draft.', 'rx-theme' ),
'item_scheduled' => esc_html__( 'FAQ scheduled.', 'rx-theme' ),
'item_updated' => esc_html__( 'FAQ updated.', 'rx-theme' ),
);
$args = array(
'labels' => $labels,
'description' => esc_html__( 'Frequently asked questions for RX Theme.', 'rx-theme' ),
'public' => true,
'publicly_queryable' => true,
'exclude_from_search' => false,
'show_ui' => true,
'show_in_menu' => true,
'show_in_nav_menus' => true,
'show_in_admin_bar' => true,
'show_in_rest' => true,
'rest_base' => 'rx-faqs',
'rest_namespace' => 'wp/v2',
'menu_position' => 21,
'menu_icon' => 'dashicons-editor-help',
'capability_type' => 'post',
'map_meta_cap' => true,
'hierarchical' => false,
'supports' => array(
'title',
'editor',
'excerpt',
'thumbnail',
'author',
'revisions',
'custom-fields',
'page-attributes',
),
'taxonomies' => array(
self::TAX_CATEGORY,
self::TAX_TAG,
),
'has_archive' => 'faqs',
'rewrite' => array(
'slug' => 'faq',
'with_front' => false,
'feeds' => true,
'pages' => true,
),
'query_var' => 'rx_faq',
'can_export' => true,
'delete_with_user' => false,
);
register_post_type( self::POST_TYPE, apply_filters( 'rx_theme_faq_post_type_args', $args ) );
}
/**
* Register FAQ taxonomies.
*/
public function register_taxonomies() : void {
$category_labels = array(
'name' => esc_html__( 'FAQ Categories', 'rx-theme' ),
'singular_name' => esc_html__( 'FAQ Category', 'rx-theme' ),
'search_items' => esc_html__( 'Search FAQ Categories', 'rx-theme' ),
'all_items' => esc_html__( 'All FAQ Categories', 'rx-theme' ),
'parent_item' => esc_html__( 'Parent FAQ Category', 'rx-theme' ),
'parent_item_colon' => esc_html__( 'Parent FAQ Category:', 'rx-theme' ),
'edit_item' => esc_html__( 'Edit FAQ Category', 'rx-theme' ),
'update_item' => esc_html__( 'Update FAQ Category', 'rx-theme' ),
'add_new_item' => esc_html__( 'Add New FAQ Category', 'rx-theme' ),
'new_item_name' => esc_html__( 'New FAQ Category Name', 'rx-theme' ),
'menu_name' => esc_html__( 'FAQ Categories', 'rx-theme' ),
);
register_taxonomy(
self::TAX_CATEGORY,
array( self::POST_TYPE ),
apply_filters(
'rx_theme_faq_category_taxonomy_args',
array(
'hierarchical' => true,
'labels' => $category_labels,
'public' => true,
'show_ui' => true,
'show_admin_column' => true,
'show_in_nav_menus' => true,
'show_tagcloud' => false,
'show_in_quick_edit' => true,
'show_in_rest' => true,
'rest_base' => 'rx-faq-categories',
'rewrite' => array(
'slug' => 'faq-category',
'with_front' => false,
'hierarchical' => true,
),
'query_var' => true,
'update_count_callback' => '_update_post_term_count',
)
)
);
$tag_labels = array(
'name' => esc_html__( 'FAQ Tags', 'rx-theme' ),
'singular_name' => esc_html__( 'FAQ Tag', 'rx-theme' ),
'search_items' => esc_html__( 'Search FAQ Tags', 'rx-theme' ),
'popular_items' => esc_html__( 'Popular FAQ Tags', 'rx-theme' ),
'all_items' => esc_html__( 'All FAQ Tags', 'rx-theme' ),
'edit_item' => esc_html__( 'Edit FAQ Tag', 'rx-theme' ),
'update_item' => esc_html__( 'Update FAQ Tag', 'rx-theme' ),
'add_new_item' => esc_html__( 'Add New FAQ Tag', 'rx-theme' ),
'new_item_name' => esc_html__( 'New FAQ Tag Name', 'rx-theme' ),
'separate_items_with_commas' => esc_html__( 'Separate FAQ tags with commas', 'rx-theme' ),
'add_or_remove_items' => esc_html__( 'Add or remove FAQ tags', 'rx-theme' ),
'choose_from_most_used' => esc_html__( 'Choose from the most used FAQ tags', 'rx-theme' ),
'menu_name' => esc_html__( 'FAQ Tags', 'rx-theme' ),
);
register_taxonomy(
self::TAX_TAG,
array( self::POST_TYPE ),
apply_filters(
'rx_theme_faq_tag_taxonomy_args',
array(
'hierarchical' => false,
'labels' => $tag_labels,
'public' => true,
'show_ui' => true,
'show_admin_column' => true,
'show_in_nav_menus' => true,
'show_tagcloud' => true,
'show_in_quick_edit' => true,
'show_in_rest' => true,
'rest_base' => 'rx-faq-tags',
'rewrite' => array(
'slug' => 'faq-tag',
'with_front' => false,
),
'query_var' => true,
)
)
);
}
/**
* Register post meta for REST and security.
*/
public function register_meta() : void {
$meta_fields = array(
self::META_FEATURED => array(
'type' => 'boolean',
'single' => true,
'default' => false,
'sanitize_callback' => 'rest_sanitize_boolean',
),
self::META_ORDER => array(
'type' => 'integer',
'single' => true,
'default' => 0,
'sanitize_callback' => 'absint',
),
self::META_SHORT_ANSWER => array(
'type' => 'string',
'single' => true,
'default' => '',
'sanitize_callback' => 'sanitize_textarea_field',
),
self::META_REVIEWED_BY => array(
'type' => 'string',
'single' => true,
'default' => '',
'sanitize_callback' => 'sanitize_text_field',
),
self::META_REVIEWED_DATE => array(
'type' => 'string',
'single' => true,
'default' => '',
'sanitize_callback' => array( $this, 'sanitize_date' ),
),
self::META_READING_LEVEL => array(
'type' => 'string',
'single' => true,
'default' => 'simple',
'sanitize_callback' => 'sanitize_key',
),
self::META_SOURCE_URL => array(
'type' => 'string',
'single' => true,
'default' => '',
'sanitize_callback' => 'esc_url_raw',
),
self::META_SCHEMA_ENABLED => array(
'type' => 'boolean',
'single' => true,
'default' => true,
'sanitize_callback' => 'rest_sanitize_boolean',
),
);
foreach ( $meta_fields as $key => $args ) {
register_post_meta(
self::POST_TYPE,
$key,
array_merge(
array(
'show_in_rest' => true,
'auth_callback' => array( $this, 'meta_auth_callback' ),
),
$args
)
);
}
}
/**
* Meta auth callback.
*/
public function meta_auth_callback() : bool {
return current_user_can( 'edit_posts' );
}
/**
* Sanitize date.
*/
public function sanitize_date( $date ) : string {
$date = sanitize_text_field( (string) $date );
if ( empty( $date ) ) {
return '';
}
$timestamp = strtotime( $date );
if ( false === $timestamp ) {
return '';
}
return gmdate( 'Y-m-d', $timestamp );
}
/**
* Add FAQ meta boxes.
*/
public function add_meta_boxes() : void {
add_meta_box(
'rx_faq_settings',
esc_html__( 'FAQ Settings', 'rx-theme' ),
array( $this, 'render_settings_meta_box' ),
self::POST_TYPE,
'side',
'high'
);
add_meta_box(
'rx_faq_extra',
esc_html__( 'FAQ Extra Information', 'rx-theme' ),
array( $this, 'render_extra_meta_box' ),
self::POST_TYPE,
'normal',
'default'
);
add_meta_box(
'rx_faq_schema',
esc_html__( 'FAQ Schema / SEO', 'rx-theme' ),
array( $this, 'render_schema_meta_box' ),
self::POST_TYPE,
'normal',
'default'
);
}
/**
* Render settings meta box.
*/
public function render_settings_meta_box( WP_Post $post ) : void {
wp_nonce_field( 'rx_faq_meta_save', 'rx_faq_meta_nonce' );
$featured = (bool) get_post_meta( $post->ID, self::META_FEATURED, true );
$order = absint( get_post_meta( $post->ID, self::META_ORDER, true ) );
$level = get_post_meta( $post->ID, self::META_READING_LEVEL, true );
$level = $level ? $level : 'simple';
?>
<p>
<label>
<input type="checkbox" name="rx_faq_featured" value="1" <?php checked( $featured ); ?>>
<?php esc_html_e( 'Featured FAQ', 'rx-theme' ); ?>
</label>
</p>
<p>
<label for="rx_faq_order">
<strong><?php esc_html_e( 'Custom Order', 'rx-theme' ); ?></strong>
</label>
<input type="number" id="rx_faq_order" name="rx_faq_order" value="<?php echo esc_attr( $order ); ?>" class="widefat" min="0" step="1">
<small><?php esc_html_e( 'Lower number appears first.', 'rx-theme' ); ?></small>
</p>
<p>
<label for="rx_faq_reading_level">
<strong><?php esc_html_e( 'Reading Level', 'rx-theme' ); ?></strong>
</label>
<select id="rx_faq_reading_level" name="rx_faq_reading_level" class="widefat">
<option value="simple" <?php selected( $level, 'simple' ); ?>><?php esc_html_e( 'Simple', 'rx-theme' ); ?></option>
<option value="intermediate" <?php selected( $level, 'intermediate' ); ?>><?php esc_html_e( 'Intermediate', 'rx-theme' ); ?></option>
<option value="advanced" <?php selected( $level, 'advanced' ); ?>><?php esc_html_e( 'Advanced', 'rx-theme' ); ?></option>
<option value="medical" <?php selected( $level, 'medical' ); ?>><?php esc_html_e( 'Medical', 'rx-theme' ); ?></option>
</select>
</p>
<?php
}
/**
* Render extra meta box.
*/
public function render_extra_meta_box( WP_Post $post ) : void {
$short_answer = get_post_meta( $post->ID, self::META_SHORT_ANSWER, true );
$reviewed_by = get_post_meta( $post->ID, self::META_REVIEWED_BY, true );
$reviewed_date = get_post_meta( $post->ID, self::META_REVIEWED_DATE, true );
$source_url = get_post_meta( $post->ID, self::META_SOURCE_URL, true );
?>
<p>
<label for="rx_faq_short_answer">
<strong><?php esc_html_e( 'Short Answer', 'rx-theme' ); ?></strong>
</label>
<textarea id="rx_faq_short_answer" name="rx_faq_short_answer" class="widefat" rows="4"><?php echo esc_textarea( $short_answer ); ?></textarea>
<small><?php esc_html_e( 'Useful for archive cards, accordion preview, and schema summary.', 'rx-theme' ); ?></small>
</p>
<p>
<label for="rx_faq_reviewed_by">
<strong><?php esc_html_e( 'Reviewed By', 'rx-theme' ); ?></strong>
</label>
<input type="text" id="rx_faq_reviewed_by" name="rx_faq_reviewed_by" value="<?php echo esc_attr( $reviewed_by ); ?>" class="widefat">
</p>
<p>
<label for="rx_faq_reviewed_date">
<strong><?php esc_html_e( 'Reviewed Date', 'rx-theme' ); ?></strong>
</label>
<input type="date" id="rx_faq_reviewed_date" name="rx_faq_reviewed_date" value="<?php echo esc_attr( $reviewed_date ); ?>" class="widefat">
</p>
<p>
<label for="rx_faq_source_url">
<strong><?php esc_html_e( 'Source URL', 'rx-theme' ); ?></strong>
</label>
<input type="url" id="rx_faq_source_url" name="rx_faq_source_url" value="<?php echo esc_url( $source_url ); ?>" class="widefat" placeholder="https://example.com/source">
</p>
<?php
}
/**
* Render schema meta box.
*/
public function render_schema_meta_box( WP_Post $post ) : void {
$schema_enabled = get_post_meta( $post->ID, self::META_SCHEMA_ENABLED, true );
if ( '' === $schema_enabled ) {
$schema_enabled = true;
}
?>
<p>
<label>
<input type="checkbox" name="rx_faq_schema_enabled" value="1" <?php checked( (bool) $schema_enabled ); ?>>
<?php esc_html_e( 'Enable FAQPage JSON-LD schema for this FAQ.', 'rx-theme' ); ?>
</label>
</p>
<p>
<small>
<?php esc_html_e( 'The FAQ title is used as the question. The answer comes from Short Answer first; if empty, the main content is used.', 'rx-theme' ); ?>
</small>
</p>
<?php
}
/**
* Save meta boxes.
*/
public function save_meta_boxes( int $post_id, WP_Post $post ) : void {
if ( ! isset( $_POST['rx_faq_meta_nonce'] ) ) {
return;
}
if ( ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['rx_faq_meta_nonce'] ) ), 'rx_faq_meta_save' ) ) {
return;
}
if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
return;
}
if ( wp_is_post_revision( $post_id ) ) {
return;
}
if ( self::POST_TYPE !== $post->post_type ) {
return;
}
if ( ! current_user_can( 'edit_post', $post_id ) ) {
return;
}
$featured = isset( $_POST['rx_faq_featured'] ) ? 1 : 0;
$schema_enabled = isset( $_POST['rx_faq_schema_enabled'] ) ? 1 : 0;
$order = isset( $_POST['rx_faq_order'] )
? absint( wp_unslash( $_POST['rx_faq_order'] ) )
: 0;
$short_answer = isset( $_POST['rx_faq_short_answer'] )
? sanitize_textarea_field( wp_unslash( $_POST['rx_faq_short_answer'] ) )
: '';
$reviewed_by = isset( $_POST['rx_faq_reviewed_by'] )
? sanitize_text_field( wp_unslash( $_POST['rx_faq_reviewed_by'] ) )
: '';
$reviewed_date = isset( $_POST['rx_faq_reviewed_date'] )
? $this->sanitize_date( wp_unslash( $_POST['rx_faq_reviewed_date'] ) )
: '';
$reading_level = isset( $_POST['rx_faq_reading_level'] )
? sanitize_key( wp_unslash( $_POST['rx_faq_reading_level'] ) )
: 'simple';
$source_url = isset( $_POST['rx_faq_source_url'] )
? esc_url_raw( wp_unslash( $_POST['rx_faq_source_url'] ) )
: '';
$allowed_levels = array( 'simple', 'intermediate', 'advanced', 'medical' );
if ( ! in_array( $reading_level, $allowed_levels, true ) ) {
$reading_level = 'simple';
}
update_post_meta( $post_id, self::META_FEATURED, $featured );
update_post_meta( $post_id, self::META_ORDER, $order );
update_post_meta( $post_id, self::META_SHORT_ANSWER, $short_answer );
update_post_meta( $post_id, self::META_REVIEWED_BY, $reviewed_by );
update_post_meta( $post_id, self::META_REVIEWED_DATE, $reviewed_date );
update_post_meta( $post_id, self::META_READING_LEVEL, $reading_level );
update_post_meta( $post_id, self::META_SOURCE_URL, $source_url );
update_post_meta( $post_id, self::META_SCHEMA_ENABLED, $schema_enabled );
}
/**
* Admin columns.
*/
public function admin_columns( array $columns ) : array {
$new_columns = array();
foreach ( $columns as $key => $label ) {
$new_columns[ $key ] = $label;
if ( 'title' === $key ) {
$new_columns['rx_faq_featured'] = esc_html__( 'Featured', 'rx-theme' );
$new_columns['rx_faq_order'] = esc_html__( 'Order', 'rx-theme' );
$new_columns['rx_faq_reading_level'] = esc_html__( 'Level', 'rx-theme' );
$new_columns['rx_faq_reviewed'] = esc_html__( 'Reviewed', 'rx-theme' );
$new_columns['rx_faq_schema'] = esc_html__( 'Schema', 'rx-theme' );
}
}
return $new_columns;
}
/**
* Admin column content.
*/
public function admin_column_content( string $column, int $post_id ) : void {
switch ( $column ) {
case 'rx_faq_featured':
echo get_post_meta( $post_id, self::META_FEATURED, true )
? '<span aria-label="Featured">⭐</span>'
: '<span aria-label="Not featured">—</span>';
break;
case 'rx_faq_order':
echo esc_html( (string) absint( get_post_meta( $post_id, self::META_ORDER, true ) ) );
break;
case 'rx_faq_reading_level':
echo esc_html( ucfirst( (string) get_post_meta( $post_id, self::META_READING_LEVEL, true ) ) );
break;
case 'rx_faq_reviewed':
$by = get_post_meta( $post_id, self::META_REVIEWED_BY, true );
$date = get_post_meta( $post_id, self::META_REVIEWED_DATE, true );
if ( $by || $date ) {
echo esc_html( trim( $by . ' ' . $date ) );
} else {
echo '—';
}
break;
case 'rx_faq_schema':
echo get_post_meta( $post_id, self::META_SCHEMA_ENABLED, true )
? '<span style="color:green;">' . esc_html__( 'Enabled', 'rx-theme' ) . '</span>'
: '<span style="color:#777;">' . esc_html__( 'Disabled', 'rx-theme' ) . '</span>';
break;
}
}
/**
* Sortable admin columns.
*/
public function sortable_columns( array $columns ) : array {
$columns['rx_faq_order'] = 'rx_faq_order';
$columns['rx_faq_featured'] = 'rx_faq_featured';
$columns['rx_faq_reading_level'] = 'rx_faq_reading_level';
return $columns;
}
/**
* Admin orderby columns.
*/
public function admin_orderby_columns( WP_Query $query ) : void {
if ( ! is_admin() || ! $query->is_main_query() ) {
return;
}
if ( self::POST_TYPE !== $query->get( 'post_type' ) ) {
return;
}
$orderby = $query->get( 'orderby' );
if ( 'rx_faq_order' === $orderby ) {
$query->set( 'meta_key', self::META_ORDER );
$query->set( 'orderby', 'meta_value_num' );
}
if ( 'rx_faq_featured' === $orderby ) {
$query->set( 'meta_key', self::META_FEATURED );
$query->set( 'orderby', 'meta_value_num' );
}
if ( 'rx_faq_reading_level' === $orderby ) {
$query->set( 'meta_key', self::META_READING_LEVEL );
$query->set( 'orderby', 'meta_value' );
}
}
/**
* Updated messages.
*/
public function updated_messages( array $messages ) : array {
$messages[ self::POST_TYPE ] = array(
0 => '',
1 => esc_html__( 'FAQ updated.', 'rx-theme' ),
2 => esc_html__( 'Custom field updated.', 'rx-theme' ),
3 => esc_html__( 'Custom field deleted.', 'rx-theme' ),
4 => esc_html__( 'FAQ updated.', 'rx-theme' ),
5 => esc_html__( 'FAQ restored to revision.', 'rx-theme' ),
6 => esc_html__( 'FAQ published.', 'rx-theme' ),
7 => esc_html__( 'FAQ saved.', 'rx-theme' ),
8 => esc_html__( 'FAQ submitted.', 'rx-theme' ),
9 => esc_html__( 'FAQ scheduled.', 'rx-theme' ),
10 => esc_html__( 'FAQ draft updated.', 'rx-theme' ),
);
return $messages;
}
/**
* Title placeholder.
*/
public function title_placeholder( string $title ) : string {
$screen = get_current_screen();
if ( $screen && self::POST_TYPE === $screen->post_type ) {
return esc_html__( 'Enter FAQ question here', 'rx-theme' );
}
return $title;
}
/**
* FAQ shortcode.
*
* Usage:
* [rx_faq]
* [rx_faq limit="10" category="general" featured="yes" accordion="yes"]
*/
public function faq_shortcode( array $atts = array() ) : string {
$atts = shortcode_atts(
array(
'limit' => 10,
'category' => '',
'tag' => '',
'featured' => '',
'orderby' => 'menu_order',
'order' => 'ASC',
'accordion' => 'yes',
'show_excerpt' => 'yes',
'show_schema' => 'no',
'class' => '',
),
$atts,
'rx_faq'
);
$args = array(
'post_type' => self::POST_TYPE,
'post_status' => 'publish',
'posts_per_page' => absint( $atts['limit'] ),
'ignore_sticky_posts' => true,
'no_found_rows' => true,
'update_post_meta_cache' => true,
'update_post_term_cache' => true,
);
if ( 'menu_order' === $atts['orderby'] ) {
$args['meta_key'] = self::META_ORDER;
$args['orderby'] = array(
'meta_value_num' => 'ASC',
'title' => 'ASC',
);
} else {
$args['orderby'] = sanitize_key( $atts['orderby'] );
$args['order'] = 'DESC' === strtoupper( $atts['order'] ) ? 'DESC' : 'ASC';
}
$tax_query = array();
if ( ! empty( $atts['category'] ) ) {
$tax_query[] = array(
'taxonomy' => self::TAX_CATEGORY,
'field' => 'slug',
'terms' => array_map( 'sanitize_title', explode( ',', $atts['category'] ) ),
);
}
if ( ! empty( $atts['tag'] ) ) {
$tax_query[] = array(
'taxonomy' => self::TAX_TAG,
'field' => 'slug',
'terms' => array_map( 'sanitize_title', explode( ',', $atts['tag'] ) ),
);
}
if ( ! empty( $tax_query ) ) {
$args['tax_query'] = $tax_query;
}
if ( 'yes' === strtolower( $atts['featured'] ) ) {
$args['meta_query'] = array(
array(
'key' => self::META_FEATURED,
'value' => '1',
'compare' => '=',
),
);
}
$query = new WP_Query( apply_filters( 'rx_theme_faq_shortcode_query_args', $args, $atts ) );
if ( ! $query->have_posts() ) {
return '';
}
$is_accordion = 'yes' === strtolower( $atts['accordion'] );
$show_excerpt = 'yes' === strtolower( $atts['show_excerpt'] );
$wrapper_cls = 'rx-faq-list';
if ( $is_accordion ) {
$wrapper_cls .= ' rx-faq-accordion';
}
if ( ! empty( $atts['class'] ) ) {
$wrapper_cls .= ' ' . sanitize_html_class( $atts['class'] );
}
ob_start();
echo '<div class="' . esc_attr( $wrapper_cls ) . '">';
$schema_items = array();
while ( $query->have_posts() ) {
$query->the_post();
$post_id = get_the_ID();
$question = get_the_title();
$short_answer = get_post_meta( $post_id, self::META_SHORT_ANSWER, true );
$answer = $short_answer ? $short_answer : wp_strip_all_tags( get_the_content() );
$schema_items[] = array(
'question' => $question,
'answer' => $answer,
);
if ( $is_accordion ) {
$this->render_faq_accordion_item( $post_id, $show_excerpt );
} else {
$this->render_faq_card_item( $post_id, $show_excerpt );
}
}
echo '</div>';
if ( 'yes' === strtolower( $atts['show_schema'] ) ) {
$this->print_faq_schema_from_items( $schema_items );
}
wp_reset_postdata();
return ob_get_clean();
}
/**
* Render accordion item.
*/
private function render_faq_accordion_item( int $post_id, bool $show_excerpt = true ) : void {
$short_answer = get_post_meta( $post_id, self::META_SHORT_ANSWER, true );
$content = apply_filters( 'the_content', get_post_field( 'post_content', $post_id ) );
$answer = $short_answer && $show_excerpt ? wpautop( esc_html( $short_answer ) ) : $content;
$item_id = 'rx-faq-' . $post_id;
?>
<div class="rx-faq-item" id="<?php echo esc_attr( $item_id ); ?>">
<details class="rx-faq-details">
<summary class="rx-faq-question">
<?php echo esc_html( get_the_title( $post_id ) ); ?>
</summary>
<div class="rx-faq-answer">
<?php echo wp_kses_post( $answer ); ?>
<p class="rx-faq-read-more">
<a href="<?php echo esc_url( get_permalink( $post_id ) ); ?>">
<?php esc_html_e( 'Read full answer', 'rx-theme' ); ?>
</a>
</p>
</div>
</details>
</div>
<?php
}
/**
* Render card item.
*/
private function render_faq_card_item( int $post_id, bool $show_excerpt = true ) : void {
$short_answer = get_post_meta( $post_id, self::META_SHORT_ANSWER, true );
?>
<article class="rx-faq-card" id="rx-faq-<?php echo esc_attr( (string) $post_id ); ?>">
<h3 class="rx-faq-card-title">
<a href="<?php echo esc_url( get_permalink( $post_id ) ); ?>">
<?php echo esc_html( get_the_title( $post_id ) ); ?>
</a>
</h3>
<?php if ( $show_excerpt ) : ?>
<div class="rx-faq-card-excerpt">
<?php
if ( $short_answer ) {
echo wpautop( esc_html( $short_answer ) );
} else {
echo wp_kses_post( wpautop( get_the_excerpt( $post_id ) ) );
}
?>
</div>
<?php endif; ?>
</article>
<?php
}
/**
* Single FAQ schema.
*/
public function output_single_faq_schema() : void {
if ( ! is_singular( self::POST_TYPE ) ) {
return;
}
$post_id = get_the_ID();
if ( ! $post_id ) {
return;
}
$enabled = get_post_meta( $post_id, self::META_SCHEMA_ENABLED, true );
if ( '0' === (string) $enabled ) {
return;
}
$question = get_the_title( $post_id );
$answer = get_post_meta( $post_id, self::META_SHORT_ANSWER, true );
if ( ! $answer ) {
$answer = wp_strip_all_tags( get_post_field( 'post_content', $post_id ) );
}
$this->print_faq_schema_from_items(
array(
array(
'question' => $question,
'answer' => $answer,
),
)
);
}
/**
* Archive FAQ schema.
*/
public function output_archive_faq_schema() : void {
if ( ! is_post_type_archive( self::POST_TYPE ) && ! is_tax( array( self::TAX_CATEGORY, self::TAX_TAG ) ) ) {
return;
}
if ( ! have_posts() ) {
return;
}
global $wp_query;
if ( empty( $wp_query->posts ) ) {
return;
}
$items = array();
foreach ( $wp_query->posts as $post ) {
if ( self::POST_TYPE !== get_post_type( $post ) ) {
continue;
}
$enabled = get_post_meta( $post->ID, self::META_SCHEMA_ENABLED, true );
if ( '0' === (string) $enabled ) {
continue;
}
$answer = get_post_meta( $post->ID, self::META_SHORT_ANSWER, true );
if ( ! $answer ) {
$answer = wp_trim_words( wp_strip_all_tags( $post->post_content ), 60 );
}
$items[] = array(
'question' => get_the_title( $post ),
'answer' => $answer,
);
}
if ( ! empty( $items ) ) {
$this->print_faq_schema_from_items( $items );
}
}
/**
* Print FAQPage schema.
*/
private function print_faq_schema_from_items( array $items ) : void {
$main_entity = array();
foreach ( $items as $item ) {
if ( empty( $item['question'] ) || empty( $item['answer'] ) ) {
continue;
}
$main_entity[] = array(
'@type' => 'Question',
'name' => wp_strip_all_tags( $item['question'] ),
'acceptedAnswer' => array(
'@type' => 'Answer',
'text' => wp_strip_all_tags( $item['answer'] ),
),
);
}
if ( empty( $main_entity ) ) {
return;
}
$schema = array(
'@context' => 'https://schema.org',
'@type' => 'FAQPage',
'mainEntity' => $main_entity,
);
echo "\n" . '<script type="application/ld+json" class="rx-faq-schema">' . wp_json_encode( $schema, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE ) . '</script>' . "\n";
}
/**
* Append extra content on single FAQ.
*/
public function append_single_faq_extra_content( string $content ) : string {
if ( ! is_singular( self::POST_TYPE ) || ! in_the_loop() || ! is_main_query() ) {
return $content;
}
$post_id = get_the_ID();
$reviewed_by = get_post_meta( $post_id, self::META_REVIEWED_BY, true );
$reviewed_date = get_post_meta( $post_id, self::META_REVIEWED_DATE, true );
$source_url = get_post_meta( $post_id, self::META_SOURCE_URL, true );
$level = get_post_meta( $post_id, self::META_READING_LEVEL, true );
ob_start();
if ( $reviewed_by || $reviewed_date || $source_url || $level ) {
echo '<div class="rx-faq-extra-info">';
if ( $level ) {
echo '<p><strong>' . esc_html__( 'Reading level:', 'rx-theme' ) . '</strong> ' . esc_html( ucfirst( $level ) ) . '</p>';
}
if ( $reviewed_by ) {
echo '<p><strong>' . esc_html__( 'Reviewed by:', 'rx-theme' ) . '</strong> ' . esc_html( $reviewed_by ) . '</p>';
}
if ( $reviewed_date ) {
echo '<p><strong>' . esc_html__( 'Reviewed date:', 'rx-theme' ) . '</strong> ' . esc_html( $reviewed_date ) . '</p>';
}
if ( $source_url ) {
echo '<p><strong>' . esc_html__( 'Source:', 'rx-theme' ) . '</strong> <a href="' . esc_url( $source_url ) . '" target="_blank" rel="noopener noreferrer nofollow">' . esc_html__( 'View reference', 'rx-theme' ) . '</a></p>';
}
echo '</div>';
}
return $content . ob_get_clean();
}
/**
* Template loader.
*/
public function template_loader( string $template ) : string {
if ( is_singular( self::POST_TYPE ) ) {
$theme_template = locate_template(
array(
'single-rx_faq.php',
'template-parts/faq/single-faq.php',
)
);
if ( $theme_template ) {
return $theme_template;
}
}
if ( is_post_type_archive( self::POST_TYPE ) ) {
$theme_template = locate_template(
array(
'archive-rx_faq.php',
'template-parts/faq/archive-faq.php',
)
);
if ( $theme_template ) {
return $theme_template;
}
}
if ( is_tax( array( self::TAX_CATEGORY, self::TAX_TAG ) ) ) {
$theme_template = locate_template(
array(
'taxonomy-rx_faq_category.php',
'taxonomy-rx_faq_tag.php',
'template-parts/faq/taxonomy-faq.php',
)
);
if ( $theme_template ) {
return $theme_template;
}
}
return $template;
}
/**
* Admin taxonomy filter dropdown.
*/
public function admin_taxonomy_filter() : void {
global $typenow;
if ( self::POST_TYPE !== $typenow ) {
return;
}
$taxonomy = self::TAX_CATEGORY;
$selected = isset( $_GET[ $taxonomy ] ) ? sanitize_text_field( wp_unslash( $_GET[ $taxonomy ] ) ) : '';
wp_dropdown_categories(
array(
'show_option_all' => esc_html__( 'All FAQ Categories', 'rx-theme' ),
'taxonomy' => $taxonomy,
'name' => $taxonomy,
'orderby' => 'name',
'selected' => $selected,
'hierarchical' => true,
'depth' => 3,
'show_count' => true,
'hide_empty' => false,
'value_field' => 'slug',
)
);
}
/**
* Admin taxonomy filter query.
*/
public function admin_taxonomy_filter_query( WP_Query $query ) : void {
global $pagenow;
if ( ! is_admin() || 'edit.php' !== $pagenow ) {
return;
}
$post_type = isset( $_GET['post_type'] ) ? sanitize_key( wp_unslash( $_GET['post_type'] ) ) : '';
if ( self::POST_TYPE !== $post_type ) {
return;
}
$taxonomy = self::TAX_CATEGORY;
if ( empty( $_GET[ $taxonomy ] ) ) {
return;
}
$term_slug = sanitize_title( wp_unslash( $_GET[ $taxonomy ] ) );
$query->query_vars[ $taxonomy ] = $term_slug;
}
/**
* Better archive query.
*/
public function add_to_wp_sitemap( array $post_types ) : array {
$post_types[ self::POST_TYPE ] = get_post_type_object( self::POST_TYPE );
return $post_types;
}
}
endif;
/**
* Initialize FAQ module.
*/
function rx_theme_faq_post_type() : RX_Theme_FAQ_Post_Type {
return RX_Theme_FAQ_Post_Type::instance();
}
rx_theme_faq_post_type();
/**
* Helper: Get FAQ query.
*/
function rx_theme_get_faqs( array $args = array() ) : WP_Query {
$defaults = array(
'post_type' => RX_Theme_FAQ_Post_Type::POST_TYPE,
'post_status' => 'publish',
'posts_per_page' => 10,
'ignore_sticky_posts' => true,
'update_post_meta_cache' => true,
'update_post_term_cache' => true,
'meta_key' => RX_Theme_FAQ_Post_Type::META_ORDER,
'orderby' => array(
'meta_value_num' => 'ASC',
'title' => 'ASC',
),
);
return new WP_Query( wp_parse_args( $args, $defaults ) );
}
/**
* Helper: Get featured FAQs.
*/
function rx_theme_get_featured_faqs( int $limit = 5 ) : WP_Query {
return rx_theme_get_faqs(
array(
'posts_per_page' => absint( $limit ),
'meta_query' => array(
array(
'key' => RX_Theme_FAQ_Post_Type::META_FEATURED,
'value' => '1',
'compare' => '=',
),
),
)
);
}
/**
* Helper: Get FAQ short answer.
*/
function rx_theme_get_faq_short_answer( int $post_id = 0 ) : string {
$post_id = $post_id ? $post_id : get_the_ID();
if ( ! $post_id ) {
return '';
}
return (string) get_post_meta( $post_id, RX_Theme_FAQ_Post_Type::META_SHORT_ANSWER, true );
}
/**
* Helper: Display FAQ short answer.
*/
function rx_theme_the_faq_short_answer( int $post_id = 0 ) : void {
$answer = rx_theme_get_faq_short_answer( $post_id );
if ( $answer ) {
echo wp_kses_post( wpautop( esc_html( $answer ) ) );
}
}
/**
* Helper: Check if FAQ is featured.
*/
function rx_theme_is_featured_faq( int $post_id = 0 ) : bool {
$post_id = $post_id ? $post_id : get_the_ID();
if ( ! $post_id ) {
return false;
}
return (bool) get_post_meta( $post_id, RX_Theme_FAQ_Post_Type::META_FEATURED, true );
}
Now load it from your functions.php:
/**
* Load RX FAQ post type.
*/
require_once get_template_directory() . '/inc/post-types/faq.php';
After adding this file, go to:
WordPress Dashboard → Settings → Permalinks → Save Changes
This refreshes rewrite rules so /faq/ and /faqs/ URLs work correctly.