PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 2.0.0
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v2.0.0
2.7.0 2.6.0 2.5.0 2.4.0 2.3.0 2.2.0 2.1.1 2.1.0 2.0.2 2.0.1 2.0.0 1.32.0 1.31.0 1.30.0 1.29.0 1.28.0 1.27.0 1.26.0 1.25.0 trunk 1.0.0 1.0.1 1.0.2 1.1.0 1.10.0 All 48 releases
thinkrank / includes / editor / elementor / class-faq-widget.php

class-faq-widget.php in ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO 2.0.0, at includes/editor/elementor/class-faq-widget.php

203 lines 6.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * Elementor FAQ Widget
5 *
6 * The Elementor counterpart of the thinkrank/faq Gutenberg block: an inline
7 * Q&A accordion (native <details>, no JS) emitting FAQPage JSON-LD.
8 *
9 * @package ThinkRank
10 * @subpackage Editor\Elementor
11 * @since 1.18.0
12 */
13
14 declare(strict_types=1);
15
16 namespace ThinkRank\Editor\Elementor;
17
18 // Prevent direct access
19 if (!defined('ABSPATH')) {
20 exit;
21 }
22
23 /**
24 * FAQ Widget.
25 *
26 * @since 1.18.0
27 */
28 class FAQ_Widget extends \Elementor\Widget_Base {
29
30 public function get_name(): string {
31 return 'thinkrank-faq';
32 }
33
34 public function get_title(): string {
35 return __('FAQ (ThinkRank)', 'thinkrank');
36 }
37
38 public function get_icon(): string {
39 return 'eicon-help-o';
40 }
41
42 public function get_categories(): array {
43 return ['thinkrank'];
44 }
45
46 public function get_keywords(): array {
47 return ['faq', 'questions', 'accordion', 'schema', 'thinkrank'];
48 }
49
50 public function get_style_depends(): array {
51 return ['thinkrank-faq-block'];
52 }
53
54 protected function register_controls(): void {
55 $this->start_controls_section('section_content', [
56 'label' => __('FAQ', 'thinkrank'),
57 ]);
58
59 $this->add_control('heading', [
60 'label' => __('Section heading', 'thinkrank'),
61 'type' => \Elementor\Controls_Manager::TEXT,
62 'default' => __('Frequently asked questions', 'thinkrank'),
63 'label_block' => true,
64 ]);
65
66 $this->add_control('heading_tag', [
67 'label' => __('Heading tag', 'thinkrank'),
68 'type' => \Elementor\Controls_Manager::SELECT,
69 'default' => 'h2',
70 'options' => ['h2' => 'H2', 'h3' => 'H3', 'h4' => 'H4', 'p' => __('Paragraph', 'thinkrank')],
71 ]);
72
73 $repeater = new \Elementor\Repeater();
74 $repeater->add_control('question', [
75 'label' => __('Question', 'thinkrank'),
76 'type' => \Elementor\Controls_Manager::TEXT,
77 'label_block' => true,
78 ]);
79 $repeater->add_control('answer', [
80 'label' => __('Answer', 'thinkrank'),
81 'type' => \Elementor\Controls_Manager::WYSIWYG,
82 ]);
83
84 $this->add_control('faqs', [
85 'label' => __('Questions', 'thinkrank'),
86 'type' => \Elementor\Controls_Manager::REPEATER,
87 'fields' => $repeater->get_controls(),
88 'default' => [['question' => '', 'answer' => '']],
89 'title_field' => '{{{ question }}}',
90 ]);
91
92 $this->add_control('first_open', [
93 'label' => __('Open first item by default', 'thinkrank'),
94 'type' => \Elementor\Controls_Manager::SWITCHER,
95 'default' => 'yes',
96 ]);
97
98 $this->add_control('output_schema', [
99 'label' => __('Output FAQ schema (JSON-LD)', 'thinkrank'),
100 'description' => __('Adds FAQPage structured data for rich results.', 'thinkrank'),
101 'type' => \Elementor\Controls_Manager::SWITCHER,
102 'default' => 'yes',
103 ]);
104
105 $this->end_controls_section();
106 }
107
108 protected function render(): void {
109 $settings = $this->get_settings_for_display();
110 $faqs = is_array($settings['faqs'] ?? null) ? $settings['faqs'] : [];
111 $items = array_values(array_filter($faqs, static function ($faq) {
112 return !empty($faq['question']) || !empty($faq['answer']);
113 }));
114
115 if (empty($items)) {
116 return;
117 }
118
119 $heading_tag = in_array($settings['heading_tag'] ?? 'h2', ['h2', 'h3', 'h4', 'p'], true)
120 ? $settings['heading_tag']
121 : 'h2';
122
123 echo '<div class="thinkrank-faq">';
124
125 if (!empty($settings['heading'])) {
126 printf(
127 '<%1$s class="thinkrank-faq__heading">%2$s</%1$s>',
128 esc_html($heading_tag), // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
129 esc_html($settings['heading'])
130 );
131 }
132
133 foreach ($items as $index => $faq) {
134 $open = ('yes' === ($settings['first_open'] ?? '')) && 0 === $index ? ' open' : '';
135 echo '<details class="thinkrank-faq__item"' . $open . '>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
136 echo '<summary class="thinkrank-faq__question">' . esc_html((string) ($faq['question'] ?? '')) . '</summary>';
137 echo '<div class="thinkrank-faq__answer">' . wp_kses_post((string) ($faq['answer'] ?? '')) . '</div>';
138 echo '</details>';
139 }
140
141 echo '</div>';
142
143 $this->maybe_render_schema($settings, $items);
144 }
145
146 /**
147 * Emit FAQPage JSON-LD on the front end (never in the editor preview).
148 *
149 * @param array $settings Widget settings.
150 * @param array $items Non-empty FAQ items.
151 * @return void
152 */
153 private function maybe_render_schema(array $settings, array $items): void {
154 if ('yes' !== ($settings['output_schema'] ?? 'yes')) {
155 return;
156 }
157
158 if (\Elementor\Plugin::$instance->editor->is_edit_mode()) {
159 return;
160 }
161
162 // The request's schema graph already merged this widget's questions into
163 // its single FAQPage; emitting again would duplicate it (#355).
164 if (
165 class_exists('ThinkRank\\Frontend\\Schema_Graph')
166 && \ThinkRank\Frontend\Schema_Graph::instance()->absorbed_content_faq()
167 ) {
168 return;
169 }
170
171 $entities = [];
172 foreach ($items as $faq) {
173 $question = trim(wp_strip_all_tags((string) ($faq['question'] ?? '')));
174 $answer = trim((string) ($faq['answer'] ?? ''));
175 if ($question === '' || $answer === '') {
176 continue;
177 }
178 $entities[] = [
179 '@type' => 'Question',
180 'name' => $question,
181 'acceptedAnswer' => [
182 '@type' => 'Answer',
183 'text' => wp_kses_post($answer),
184 ],
185 ];
186 }
187
188 if (empty($entities)) {
189 return;
190 }
191
192 $json = wp_json_encode([
193 '@context' => 'https://schema.org',
194 '@type' => 'FAQPage',
195 'mainEntity' => $entities,
196 ], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT);
197
198 if (false !== $json) {
199 echo '<script type="application/ld+json">' . $json . '</script>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
200 }
201 }
202 }
203