faq.php

I’ll build this as a theme shortcode file you can drop into inc/shortcodes/faq.php, with FAQ accordion output, schema JSON-LD, categories, search/filter UI, icons, accessibility, styling hooks, and safe sanitization.

Below is an advanced inc/shortcodes/faq.php file for your RX Theme. It includes:

FAQ accordion, FAQ schema JSON-LD, manual FAQ items, custom post type support, category filter, live search, expand/collapse all, deep-link anchor support, responsive grid/accordion layout, accessible ARIA controls, copy-link button, clean CSS/JS, and safe sanitization.

Create this file:

<?php
/**
 * RX Theme FAQ Shortcodes
 *
 * File: inc/shortcodes/faq.php
 *
 * Shortcodes:
 * [rx_faq]
 * [rx_faq source="manual"] [rx_faq_item question="What is RX Theme?"]Answer here[/rx_faq_item] [/rx_faq]
 * [rx_faq_item question="Question"]Answer[/rx_faq_item]
 *
 * Recommended include from functions.php:
 * require_once get_template_directory() . '/inc/shortcodes/faq.php';
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

if ( ! class_exists( 'RX_Theme_FAQ_Shortcodes' ) ) :

final class RX_Theme_FAQ_Shortcodes {

	private static $instance = null;
	private static $assets_loaded = false;
	private static $schema_items = array();

	public static function instance() {
		if ( null === self::$instance ) {
			self::$instance = new self();
		}
		return self::$instance;
	}

	private function __construct() {
		add_shortcode( 'rx_faq', array( $this, 'render_faq_shortcode' ) );
		add_shortcode( 'rx_faq_item', array( $this, 'render_faq_item_shortcode' ) );

		add_action( 'wp_footer', array( $this, 'print_schema_json_ld' ), 99 );
	}

	public function render_faq_shortcode( $atts, $content = null ) {
		$atts = shortcode_atts(
			array(
				'source'          => 'manual', // manual, post_type, current_post_meta.
				'title'           => 'Frequently Asked Questions',
				'subtitle'        => '',
				'description'     => '',
				'post_type'       => 'rx_faq',
				'taxonomy'        => 'faq_category',
				'category'        => '',
				'limit'           => 20,
				'orderby'         => 'menu_order',
				'order'           => 'ASC',
				'include'         => '',
				'exclude'         => '',
				'layout'          => 'accordion', // accordion, grid.
				'columns'         => 2,
				'open_first'      => 'yes',
				'multiple_open'   => 'yes',
				'search'          => 'yes',
				'filter'          => 'yes',
				'expand_all'      => 'yes',
				'copy_link'       => 'yes',
				'numbering'       => 'no',
				'icon'            => 'plus',
				'schema'          => 'yes',
				'class'           => '',
				'id'              => '',
				'empty_text'      => 'No FAQs found.',
			),
			$atts,
			'rx_faq'
		);

		self::enqueue_assets();

		$uid = ! empty( $atts['id'] ) ? sanitize_html_class( $atts['id'] ) : 'rx-faq-' . wp_generate_uuid4();

		$items = array();

		if ( 'post_type' === $atts['source'] ) {
			$items = $this->get_faqs_from_post_type( $atts );
		} elseif ( 'current_post_meta' === $atts['source'] ) {
			$items = $this->get_faqs_from_current_post_meta();
		} else {
			$items = $this->get_faqs_from_manual_content( $content );
		}

		$items = apply_filters( 'rx_theme_faq_items', $items, $atts );

		if ( empty( $items ) ) {
			return sprintf(
				'<div class="rx-faq-empty">%s</div>',
				esc_html( $atts['empty_text'] )
			);
		}

		if ( 'yes' === $atts['schema'] ) {
			self::$schema_items = array_merge( self::$schema_items, $items );
		}

		$categories = $this->collect_categories( $items );

		$wrapper_classes = array(
			'rx-faq-wrap',
			'rx-faq-layout-' . sanitize_html_class( $atts['layout'] ),
			'rx-faq-icon-' . sanitize_html_class( $atts['icon'] ),
		);

		if ( ! empty( $atts['class'] ) ) {
			$wrapper_classes[] = sanitize_html_class( $atts['class'] );
		}

		$columns = absint( $atts['columns'] );
		if ( $columns < 1 ) {
			$columns = 1;
		}
		if ( $columns > 4 ) {
			$columns = 4;
		}

		ob_start();
		?>

		<section
			id="<?php echo esc_attr( $uid ); ?>"
			class="<?php echo esc_attr( implode( ' ', $wrapper_classes ) ); ?>"
			data-multiple-open="<?php echo esc_attr( $atts['multiple_open'] ); ?>"
			style="--rx-faq-columns: <?php echo esc_attr( $columns ); ?>;"
		>
			<?php if ( ! empty( $atts['title'] ) || ! empty( $atts['subtitle'] ) || ! empty( $atts['description'] ) ) : ?>
				<div class="rx-faq-header">
					<?php if ( ! empty( $atts['subtitle'] ) ) : ?>
						<p class="rx-faq-subtitle"><?php echo esc_html( $atts['subtitle'] ); ?></p>
					<?php endif; ?>

					<?php if ( ! empty( $atts['title'] ) ) : ?>
						<h2 class="rx-faq-title"><?php echo esc_html( $atts['title'] ); ?></h2>
					<?php endif; ?>

					<?php if ( ! empty( $atts['description'] ) ) : ?>
						<div class="rx-faq-description"><?php echo wp_kses_post( wpautop( $atts['description'] ) ); ?></div>
					<?php endif; ?>
				</div>
			<?php endif; ?>

			<?php if ( 'yes' === $atts['search'] || 'yes' === $atts['filter'] || 'yes' === $atts['expand_all'] ) : ?>
				<div class="rx-faq-toolbar">
					<?php if ( 'yes' === $atts['search'] ) : ?>
						<label class="rx-faq-search-label">
							<span class="screen-reader-text"><?php esc_html_e( 'Search FAQ', 'rx-theme' ); ?></span>
							<input
								type="search"
								class="rx-faq-search"
								placeholder="<?php esc_attr_e( 'Search questions...', 'rx-theme' ); ?>"
								autocomplete="off"
							>
						</label>
					<?php endif; ?>

					<?php if ( 'yes' === $atts['filter'] && count( $categories ) > 1 ) : ?>
						<select class="rx-faq-filter" aria-label="<?php esc_attr_e( 'Filter FAQ category', 'rx-theme' ); ?>">
							<option value=""><?php esc_html_e( 'All Categories', 'rx-theme' ); ?></option>
							<?php foreach ( $categories as $cat_key => $cat_label ) : ?>
								<option value="<?php echo esc_attr( $cat_key ); ?>">
									<?php echo esc_html( $cat_label ); ?>
								</option>
							<?php endforeach; ?>
						</select>
					<?php endif; ?>

					<?php if ( 'yes' === $atts['expand_all'] ) : ?>
						<div class="rx-faq-actions">
							<button type="button" class="rx-faq-expand-all">
								<?php esc_html_e( 'Expand All', 'rx-theme' ); ?>
							</button>
							<button type="button" class="rx-faq-collapse-all">
								<?php esc_html_e( 'Collapse All', 'rx-theme' ); ?>
							</button>
						</div>
					<?php endif; ?>
				</div>
			<?php endif; ?>

			<div class="rx-faq-list">
				<?php
				$count = 0;

				foreach ( $items as $index => $item ) :
					$count++;

					$question = isset( $item['question'] ) ? $item['question'] : '';
					$answer   = isset( $item['answer'] ) ? $item['answer'] : '';
					$category = isset( $item['category'] ) ? $item['category'] : '';
					$cat_key  = sanitize_title( $category );

					if ( empty( $question ) || empty( $answer ) ) {
						continue;
					}

					$item_id   = $uid . '-item-' . $count;
					$panel_id  = $item_id . '-panel';
					$button_id = $item_id . '-button';
					$is_open   = ( 1 === $count && 'yes' === $atts['open_first'] );

					$anchor_slug = sanitize_title( $question );
					?>

					<article
						id="<?php echo esc_attr( $anchor_slug ); ?>"
						class="rx-faq-item <?php echo $is_open ? 'is-open' : ''; ?>"
						data-category="<?php echo esc_attr( $cat_key ); ?>"
						data-search="<?php echo esc_attr( strtolower( wp_strip_all_tags( $question . ' ' . $answer . ' ' . $category ) ) ); ?>"
					>
						<h3 class="rx-faq-question-wrap">
							<button
								id="<?php echo esc_attr( $button_id ); ?>"
								class="rx-faq-question"
								type="button"
								aria-expanded="<?php echo $is_open ? 'true' : 'false'; ?>"
								aria-controls="<?php echo esc_attr( $panel_id ); ?>"
							>
								<span class="rx-faq-question-text">
									<?php if ( 'yes' === $atts['numbering'] ) : ?>
										<span class="rx-faq-number"><?php echo esc_html( str_pad( $count, 2, '0', STR_PAD_LEFT ) ); ?></span>
									<?php endif; ?>

									<?php echo esc_html( $question ); ?>
								</span>

								<span class="rx-faq-toggle-icon" aria-hidden="true"></span>
							</button>
						</h3>

						<div
							id="<?php echo esc_attr( $panel_id ); ?>"
							class="rx-faq-answer"
							role="region"
							aria-labelledby="<?php echo esc_attr( $button_id ); ?>"
							<?php echo $is_open ? '' : 'hidden'; ?>
						>
							<div class="rx-faq-answer-inner">
								<?php if ( ! empty( $category ) ) : ?>
									<span class="rx-faq-category"><?php echo esc_html( $category ); ?></span>
								<?php endif; ?>

								<?php echo wp_kses_post( wpautop( do_shortcode( $answer ) ) ); ?>

								<?php if ( 'yes' === $atts['copy_link'] ) : ?>
									<button type="button" class="rx-faq-copy-link" data-anchor="<?php echo esc_attr( $anchor_slug ); ?>">
										<?php esc_html_e( 'Copy FAQ link', 'rx-theme' ); ?>
									</button>
								<?php endif; ?>
							</div>
						</div>
					</article>

				<?php endforeach; ?>
			</div>

			<div class="rx-faq-no-results" hidden>
				<?php esc_html_e( 'No matching FAQ found.', 'rx-theme' ); ?>
			</div>
		</section>

		<?php
		return ob_get_clean();
	}

	public function render_faq_item_shortcode( $atts, $content = null ) {
		$atts = shortcode_atts(
			array(
				'question' => '',
				'category' => '',
			),
			$atts,
			'rx_faq_item'
		);

		$data = array(
			'question' => sanitize_text_field( $atts['question'] ),
			'category' => sanitize_text_field( $atts['category'] ),
			'answer'   => wp_kses_post( $content ),
		);

		return '<!--RX_FAQ_ITEM:' . esc_html( base64_encode( wp_json_encode( $data ) ) ) . '-->';
	}

	private function get_faqs_from_manual_content( $content ) {
		$items = array();

		if ( empty( $content ) ) {
			return $items;
		}

		$processed = do_shortcode( $content );

		preg_match_all( '/<!--RX_FAQ_ITEM:(.*?)-->/s', $processed, $matches );

		if ( empty( $matches[1] ) ) {
			return $items;
		}

		foreach ( $matches[1] as $encoded ) {
			$json = base64_decode( $encoded );
			$data = json_decode( $json, true );

			if ( ! is_array( $data ) ) {
				continue;
			}

			$items[] = array(
				'question' => isset( $data['question'] ) ? sanitize_text_field( $data['question'] ) : '',
				'answer'   => isset( $data['answer'] ) ? wp_kses_post( $data['answer'] ) : '',
				'category' => isset( $data['category'] ) ? sanitize_text_field( $data['category'] ) : '',
			);
		}

		return $items;
	}

	private function get_faqs_from_post_type( $atts ) {
		$items = array();

		$args = array(
			'post_type'           => sanitize_key( $atts['post_type'] ),
			'post_status'         => 'publish',
			'posts_per_page'      => absint( $atts['limit'] ),
			'orderby'             => sanitize_key( $atts['orderby'] ),
			'order'               => 'DESC' === strtoupper( $atts['order'] ) ? 'DESC' : 'ASC',
			'ignore_sticky_posts' => true,
			'no_found_rows'       => true,
		);

		if ( ! empty( $atts['include'] ) ) {
			$args['post__in'] = array_map( 'absint', explode( ',', $atts['include'] ) );
			$args['orderby']  = 'post__in';
		}

		if ( ! empty( $atts['exclude'] ) ) {
			$args['post__not_in'] = array_map( 'absint', explode( ',', $atts['exclude'] ) );
		}

		if ( ! empty( $atts['category'] ) && taxonomy_exists( $atts['taxonomy'] ) ) {
			$args['tax_query'] = array(
				array(
					'taxonomy' => sanitize_key( $atts['taxonomy'] ),
					'field'    => 'slug',
					'terms'    => array_map( 'sanitize_title', explode( ',', $atts['category'] ) ),
				),
			);
		}

		$query = new WP_Query( $args );

		if ( $query->have_posts() ) {
			while ( $query->have_posts() ) {
				$query->the_post();

				$post_id = get_the_ID();

				$category_name = '';
				if ( taxonomy_exists( $atts['taxonomy'] ) ) {
					$terms = get_the_terms( $post_id, sanitize_key( $atts['taxonomy'] ) );
					if ( ! empty( $terms ) && ! is_wp_error( $terms ) ) {
						$category_name = $terms[0]->name;
					}
				}

				$items[] = array(
					'question' => get_the_title(),
					'answer'   => apply_filters( 'the_content', get_the_content() ),
					'category' => $category_name,
				);
			}
			wp_reset_postdata();
		}

		return $items;
	}

	private function get_faqs_from_current_post_meta() {
		$items = array();

		if ( ! is_singular() ) {
			return $items;
		}

		$post_id = get_the_ID();

		/**
		 * Supported meta format:
		 * rx_faq_items = array(
		 *   array(
		 *     'question' => 'Question?',
		 *     'answer' => 'Answer',
		 *     'category' => 'General'
		 *   )
		 * )
		 */
		$meta_items = get_post_meta( $post_id, 'rx_faq_items', true );

		if ( empty( $meta_items ) || ! is_array( $meta_items ) ) {
			return $items;
		}

		foreach ( $meta_items as $item ) {
			$items[] = array(
				'question' => isset( $item['question'] ) ? sanitize_text_field( $item['question'] ) : '',
				'answer'   => isset( $item['answer'] ) ? wp_kses_post( $item['answer'] ) : '',
				'category' => isset( $item['category'] ) ? sanitize_text_field( $item['category'] ) : '',
			);
		}

		return $items;
	}

	private function collect_categories( $items ) {
		$categories = array();

		foreach ( $items as $item ) {
			if ( empty( $item['category'] ) ) {
				continue;
			}

			$key = sanitize_title( $item['category'] );
			$categories[ $key ] = $item['category'];
		}

		return $categories;
	}

	public static function enqueue_assets() {
		if ( self::$assets_loaded ) {
			return;
		}

		self::$assets_loaded = true;

		wp_register_style( 'rx-theme-faq', false, array(), '1.0.0' );
		wp_enqueue_style( 'rx-theme-faq' );

		wp_add_inline_style(
			'rx-theme-faq',
			'
			.rx-faq-wrap {
				margin: 32px 0;
				--rx-faq-border: #e5e7eb;
				--rx-faq-bg: #ffffff;
				--rx-faq-soft: #f8fafc;
				--rx-faq-text: #111827;
				--rx-faq-muted: #6b7280;
				--rx-faq-primary: #0f766e;
				--rx-faq-radius: 16px;
				--rx-faq-shadow: 0 10px 30px rgba(15, 23, 42, 0.08);
			}

			.rx-faq-header {
				margin-bottom: 24px;
				text-align: center;
			}

			.rx-faq-subtitle {
				margin: 0 0 8px;
				color: var(--rx-faq-primary);
				font-weight: 700;
				letter-spacing: .04em;
				text-transform: uppercase;
				font-size: 13px;
			}

			.rx-faq-title {
				margin: 0;
				color: var(--rx-faq-text);
				font-size: clamp(26px, 4vw, 42px);
				line-height: 1.15;
			}

			.rx-faq-description {
				max-width: 760px;
				margin: 14px auto 0;
				color: var(--rx-faq-muted);
				font-size: 16px;
				line-height: 1.7;
			}

			.rx-faq-toolbar {
				display: flex;
				flex-wrap: wrap;
				align-items: center;
				gap: 12px;
				margin-bottom: 18px;
				padding: 14px;
				background: var(--rx-faq-soft);
				border: 1px solid var(--rx-faq-border);
				border-radius: var(--rx-faq-radius);
			}

			.rx-faq-search-label {
				flex: 1 1 260px;
				margin: 0;
			}

			.rx-faq-search,
			.rx-faq-filter {
				width: 100%;
				min-height: 44px;
				border: 1px solid var(--rx-faq-border);
				border-radius: 12px;
				padding: 10px 14px;
				background: #fff;
				color: var(--rx-faq-text);
				font-size: 15px;
				outline: none;
			}

			.rx-faq-filter {
				flex: 0 1 220px;
			}

			.rx-faq-search:focus,
			.rx-faq-filter:focus {
				border-color: var(--rx-faq-primary);
				box-shadow: 0 0 0 3px rgba(15, 118, 110, .15);
			}

			.rx-faq-actions {
				display: flex;
				gap: 8px;
				flex-wrap: wrap;
			}

			.rx-faq-expand-all,
			.rx-faq-collapse-all,
			.rx-faq-copy-link {
				border: 0;
				border-radius: 999px;
				padding: 10px 14px;
				background: var(--rx-faq-primary);
				color: #fff;
				font-size: 14px;
				font-weight: 700;
				cursor: pointer;
			}

			.rx-faq-collapse-all {
				background: #334155;
			}

			.rx-faq-copy-link {
				margin-top: 12px;
				background: #0f172a;
				font-size: 13px;
				padding: 8px 12px;
			}

			.rx-faq-list {
				display: grid;
				gap: 14px;
			}

			.rx-faq-layout-grid .rx-faq-list {
				grid-template-columns: repeat(var(--rx-faq-columns), minmax(0, 1fr));
			}

			.rx-faq-item {
				background: var(--rx-faq-bg);
				border: 1px solid var(--rx-faq-border);
				border-radius: var(--rx-faq-radius);
				box-shadow: var(--rx-faq-shadow);
				overflow: hidden;
				transition: border-color .2s ease, transform .2s ease;
			}

			.rx-faq-item:hover {
				border-color: rgba(15, 118, 110, .45);
			}

			.rx-faq-question-wrap {
				margin: 0;
				font-size: 18px;
				line-height: 1.4;
			}

			.rx-faq-question {
				width: 100%;
				display: flex;
				align-items: center;
				justify-content: space-between;
				gap: 16px;
				border: 0;
				background: transparent;
				padding: 18px 20px;
				color: var(--rx-faq-text);
				text-align: left;
				font: inherit;
				font-weight: 800;
				cursor: pointer;
			}

			.rx-faq-question:focus {
				outline: 3px solid rgba(15, 118, 110, .25);
				outline-offset: -3px;
			}

			.rx-faq-question-text {
				display: flex;
				align-items: center;
				gap: 10px;
			}

			.rx-faq-number {
				display: inline-flex;
				align-items: center;
				justify-content: center;
				min-width: 34px;
				height: 34px;
				border-radius: 50%;
				background: rgba(15, 118, 110, .12);
				color: var(--rx-faq-primary);
				font-size: 13px;
				font-weight: 900;
			}

			.rx-faq-toggle-icon {
				width: 22px;
				height: 22px;
				position: relative;
				flex: 0 0 22px;
			}

			.rx-faq-toggle-icon::before,
			.rx-faq-toggle-icon::after {
				content: "";
				position: absolute;
				top: 50%;
				left: 50%;
				width: 16px;
				height: 2px;
				background: var(--rx-faq-primary);
				transform: translate(-50%, -50%);
				transition: transform .2s ease;
			}

			.rx-faq-toggle-icon::after {
				transform: translate(-50%, -50%) rotate(90deg);
			}

			.rx-faq-item.is-open .rx-faq-toggle-icon::after {
				transform: translate(-50%, -50%) rotate(0deg);
			}

			.rx-faq-answer {
				border-top: 1px solid var(--rx-faq-border);
				background: linear-gradient(180deg, #fff, var(--rx-faq-soft));
			}

			.rx-faq-answer-inner {
				padding: 18px 20px 22px;
				color: #374151;
				font-size: 16px;
				line-height: 1.8;
			}

			.rx-faq-answer-inner p:first-child {
				margin-top: 0;
			}

			.rx-faq-answer-inner p:last-child {
				margin-bottom: 0;
			}

			.rx-faq-category {
				display: inline-flex;
				margin-bottom: 10px;
				padding: 4px 10px;
				border-radius: 999px;
				background: rgba(15, 118, 110, .1);
				color: var(--rx-faq-primary);
				font-size: 12px;
				font-weight: 800;
			}

			.rx-faq-no-results,
			.rx-faq-empty {
				padding: 20px;
				border: 1px dashed var(--rx-faq-border);
				border-radius: var(--rx-faq-radius);
				color: var(--rx-faq-muted);
				text-align: center;
				background: var(--rx-faq-soft);
			}

			@media (max-width: 768px) {
				.rx-faq-layout-grid .rx-faq-list {
					grid-template-columns: 1fr;
				}

				.rx-faq-toolbar {
					align-items: stretch;
				}

				.rx-faq-actions,
				.rx-faq-expand-all,
				.rx-faq-collapse-all {
					width: 100%;
				}

				.rx-faq-actions button {
					flex: 1;
				}
			}
			'
		);

		wp_register_script( 'rx-theme-faq', false, array(), '1.0.0', true );
		wp_enqueue_script( 'rx-theme-faq' );

		wp_add_inline_script(
			'rx-theme-faq',
			'
			(function(){
				"use strict";

				function openItem(item) {
					var btn = item.querySelector(".rx-faq-question");
					var panel = item.querySelector(".rx-faq-answer");
					if (!btn || !panel) return;

					item.classList.add("is-open");
					btn.setAttribute("aria-expanded", "true");
					panel.hidden = false;
				}

				function closeItem(item) {
					var btn = item.querySelector(".rx-faq-question");
					var panel = item.querySelector(".rx-faq-answer");
					if (!btn || !panel) return;

					item.classList.remove("is-open");
					btn.setAttribute("aria-expanded", "false");
					panel.hidden = true;
				}

				function normalizeText(text) {
					return String(text || "").toLowerCase().trim();
				}

				function filterFaq(wrap) {
					var search = wrap.querySelector(".rx-faq-search");
					var filter = wrap.querySelector(".rx-faq-filter");
					var items = wrap.querySelectorAll(".rx-faq-item");
					var noResults = wrap.querySelector(".rx-faq-no-results");

					var searchValue = search ? normalizeText(search.value) : "";
					var filterValue = filter ? filter.value : "";
					var visible = 0;

					items.forEach(function(item){
						var haystack = normalizeText(item.getAttribute("data-search"));
						var category = item.getAttribute("data-category") || "";

						var matchSearch = !searchValue || haystack.indexOf(searchValue) !== -1;
						var matchFilter = !filterValue || category === filterValue;

						if (matchSearch && matchFilter) {
							item.hidden = false;
							visible++;
						} else {
							item.hidden = true;
						}
					});

					if (noResults) {
						noResults.hidden = visible !== 0;
					}
				}

				function initFaq(wrap) {
					var multipleOpen = wrap.getAttribute("data-multiple-open") === "yes";
					var buttons = wrap.querySelectorAll(".rx-faq-question");

					buttons.forEach(function(button){
						button.addEventListener("click", function(){
							var item = button.closest(".rx-faq-item");
							if (!item) return;

							var isOpen = item.classList.contains("is-open");

							if (!multipleOpen) {
								wrap.querySelectorAll(".rx-faq-item.is-open").forEach(closeItem);
							}

							if (isOpen) {
								closeItem(item);
							} else {
								openItem(item);
							}
						});
					});

					var search = wrap.querySelector(".rx-faq-search");
					var filter = wrap.querySelector(".rx-faq-filter");

					if (search) {
						search.addEventListener("input", function(){
							filterFaq(wrap);
						});
					}

					if (filter) {
						filter.addEventListener("change", function(){
							filterFaq(wrap);
						});
					}

					var expandAll = wrap.querySelector(".rx-faq-expand-all");
					var collapseAll = wrap.querySelector(".rx-faq-collapse-all");

					if (expandAll) {
						expandAll.addEventListener("click", function(){
							wrap.querySelectorAll(".rx-faq-item:not([hidden])").forEach(openItem);
						});
					}

					if (collapseAll) {
						collapseAll.addEventListener("click", function(){
							wrap.querySelectorAll(".rx-faq-item").forEach(closeItem);
						});
					}

					wrap.querySelectorAll(".rx-faq-copy-link").forEach(function(btn){
						btn.addEventListener("click", function(){
							var anchor = btn.getAttribute("data-anchor");
							var url = window.location.origin + window.location.pathname + "#" + anchor;

							if (navigator.clipboard && navigator.clipboard.writeText) {
								navigator.clipboard.writeText(url).then(function(){
									var oldText = btn.textContent;
									btn.textContent = "Copied!";
									setTimeout(function(){
										btn.textContent = oldText;
									}, 1300);
								});
							}
						});
					});
				}

				document.addEventListener("DOMContentLoaded", function(){
					document.querySelectorAll(".rx-faq-wrap").forEach(initFaq);

					if (window.location.hash) {
						var target = document.querySelector(window.location.hash);
						if (target && target.classList.contains("rx-faq-item")) {
							openItem(target);
							setTimeout(function(){
								target.scrollIntoView({ behavior: "smooth", block: "start" });
							}, 200);
						}
					}
				});
			})();
			'
		);
	}

	public function print_schema_json_ld() {
		if ( empty( self::$schema_items ) ) {
			return;
		}

		$main_entity = array();

		foreach ( self::$schema_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_UNICODE | JSON_UNESCAPED_SLASHES ) . '</script>' . "\n";
	}
}

RX_Theme_FAQ_Shortcodes::instance();

endif;

Then include it in your theme functions.php:

require_once get_template_directory() . '/inc/shortcodes/faq.php';

Manual FAQ usage

[rx_faq title="Medical FAQ" subtitle="RX HARUN" search="yes" filter="yes" numbering="yes" schema="yes"]
	[rx_faq_item question="What is RX Theme?" category="General"]
		RX Theme is a custom WordPress theme for medical, health, blog, and professional websites.
	[/rx_faq_item]

	[rx_faq_item question="Does this FAQ support Google FAQ schema?" category="SEO"]
		Yes. It automatically outputs FAQPage JSON-LD schema when schema="yes".
	[/rx_faq_item]
[/rx_faq]

Custom post type FAQ usage

[rx_faq source="post_type" post_type="rx_faq" taxonomy="faq_category" limit="50" search="yes" filter="yes" layout="accordion"]

Grid layout usage

[rx_faq title="Website FAQ" layout="grid" columns="2" numbering="yes" search="yes"]
	[rx_faq_item question="Is RX Theme responsive?" category="Design"]
		Yes. It works on desktop, tablet, and mobile devices.
	[/rx_faq_item]
[/rx_faq]

This file is safe for theme use and does not require a plugin.

Leave a Reply

Your email address will not be published. Required fields are marked *