PluginProbe
MarqueeX – Smooth Marquee Slider, News Ticker & Post Marquee for Block Editor & Elementor / 0.6.0
MarqueeX – Smooth Marquee Slider, News Ticker & Post Marquee for Block Editor & Elementor v0.6.0
0.6.0 0.5.4 0.5.3 0.5.2 0.5.1 0.5.0 0.4.0 0.3.3 0.3.2 trunk 0.0.1 0.0.2 0.0.3 0.0.4 0.0.5 0.0.6 0.1.0 0.1.1 0.1.2 0.2.0 0.2.1 0.2.2 0.2.3 0.3.0 0.3.1
marqueex / includes / Utils / MetaDescription.php

MetaDescription.php in MarqueeX – Smooth Marquee Slider, News Ticker & Post Marquee for Block Editor & Elementor 0.6.0, at includes/Utils/MetaDescription.php

250 lines 8.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace Wpxero\Marqueex\Utils;
4
5 use Wpxero\Marqueex\Traits\Singleton;
6 use Wpxero\Marqueex\Core\Settings;
7
8 /**
9 * Optional meta description generation for pages using MarqueeX blocks.
10 *
11 * OFF by default. Rewriting a site's meta description is an SEO decision that
12 * belongs to the site owner (or their SEO plugin), not to a marquee plugin —
13 * doing it silently was surprising behaviour with no way to turn it off.
14 *
15 * Enable with marqueex_settings.compatibility.meta_description.
16 *
17 * @package marqueex
18 */
19
20 // Exit if accessed directly.
21 if (!defined('ABSPATH')) {
22 exit;
23 }
24 /**
25 * Class MetaDescription
26 */
27 class MetaDescription {
28 use Singleton;
29 /**
30 * Constructor
31 */
32 public function __construct() {
33 if (! Settings::get('compatibility.meta_description', false)) {
34 return;
35 }
36
37 add_filter('wpseo_metadesc', array($this, 'modify_yoast_meta_description'), 10, 1);
38 add_filter('rank_math/frontend/description', array($this, 'modify_rankmath_description'), 10, 1);
39 add_action('wp_head', array($this, 'add_default_meta_description'), 1);
40 }
41
42 /**
43 * Modify Yoast SEO meta description
44 *
45 * @param string $description The current meta description
46 * @return string Modified description
47 */
48 public function modify_yoast_meta_description($description) {
49 // Only modify if Yoast is active and we have an empty description
50 if (empty($description) && $this->should_modify_description()) {
51 return $this->get_marqueex_meta_description();
52 }
53 return $description;
54 }
55
56 /**
57 * Modify Rank Math meta description
58 *
59 * @param string $description The current meta description
60 * @return string Modified description
61 */
62 public function modify_rankmath_description($description) {
63 // Only modify if Rank Math is active and we have an empty description
64 if (empty($description) && $this->should_modify_description()) {
65 return $this->get_marqueex_meta_description();
66 }
67 return $description;
68 }
69
70 /**
71 * Add default meta description if no SEO plugin is active
72 */
73 public function add_default_meta_description() {
74 // Only add if no SEO plugin is active
75 if ($this->is_seo_plugin_active() || !$this->should_modify_description()) {
76 return;
77 }
78
79 $description = $this->get_marqueex_meta_description();
80 if ('' === $description) {
81 return;
82 }
83
84 echo '<meta name="description" content="' . esc_attr($description) . '" />';
85 }
86
87 /**
88 * Check if we should modify the description
89 *
90 * @return bool Whether description should be modified
91 */
92 private function should_modify_description() {
93 global $post;
94
95 // Only on singular pages with blocks
96 if (!is_singular() || !is_object($post) || !has_blocks($post->post_content)) {
97 return false;
98 }
99
100 // Only if page contains MarqueeX blocks. A substring test covers every
101 // block in the namespace; the previous has_block() pair matched only the
102 // legacy infinite-slider.
103 return strpos($post->post_content, '<!-- wp:marqueex/') !== false;
104 }
105
106 /**
107 * Build a meta description from the text a page's marquees actually carry.
108 *
109 * @return string Generated meta description, or '' when there is nothing to say.
110 */
111 private function get_marqueex_meta_description() {
112 global $post;
113
114 if (!is_object($post)) {
115 return '';
116 }
117
118 $content = $this->extract_marqueex_content(parse_blocks($post->post_content));
119
120 if (empty($content)) {
121 // Nothing to describe. Return nothing rather than inventing a sentence
122 // about the plugin — that is the site's meta description, not ours.
123 return '';
124 }
125
126 // De-duplicate: a ticker and a text marquee on one page often repeat a
127 // headline, and a description that stutters is worse than a short one.
128 $content = array_values(array_unique($content));
129
130 $description = trim(implode('', $content));
131 $description = preg_replace('/\s+/u', ' ', $description);
132
133 // Search engines truncate around 155–160 characters.
134 if (function_exists('mb_strlen') && mb_strlen($description) > 155) {
135 $description = trim(mb_substr($description, 0, 152)) . '';
136 } elseif (!function_exists('mb_strlen') && strlen($description) > 155) {
137 $description = trim(substr($description, 0, 152)) . '';
138 }
139
140 return $description;
141 }
142
143 /**
144 * Collect the human-readable text out of MarqueeX blocks.
145 *
146 * Four of the five blocks are dynamic — their `save()` returns null and they
147 * render through render.php — so they have no `innerContent` at all and their
148 * text lives in block attributes. Reading only `innerContent`, as this used
149 * to, meant the description was never generated for anything except the
150 * legacy Infinite Slider.
151 *
152 * @param array $blocks Parsed blocks.
153 * @return string[] Text fragments, in document order.
154 */
155 private function extract_marqueex_content($blocks) {
156 $content = array();
157
158 foreach ($blocks as $block) {
159 $name = $block['blockName'] ?? '';
160
161 if ($name && strpos($name, 'marqueex/') === 0) {
162 $content = array_merge($content, $this->extract_block_text($block));
163 }
164
165 // Inner blocks regardless of the parent — an Infinite Slider's items
166 // are separate blocks, and a marquee can sit inside a group.
167 if (!empty($block['innerBlocks'])) {
168 $content = array_merge(
169 $content,
170 $this->extract_marqueex_content($block['innerBlocks'])
171 );
172 }
173 }
174
175 return $content;
176 }
177
178 /**
179 * Text carried by a single MarqueeX block.
180 *
181 * @param array $block Parsed block.
182 * @return string[]
183 */
184 private function extract_block_text($block) {
185 $text = array();
186 $attrs = $block['attrs'] ?? array();
187
188 // Text Marquee and News Ticker keep their entries in `items`, each a
189 // { text, url, … } record. When `contentType` is 'posts' the entries are
190 // fetched at render time and whatever sits in `items` is stale sample
191 // data, so it must not describe the page.
192 //
193 // Note these are the attributes as SAVED, not as rendered: parse_blocks()
194 // does not merge block.json defaults, and deliberately we do not either.
195 // A marquee the author never edited still carries placeholder copy
196 // ("Breaking news: Your first headline here…"), and that must never end
197 // up as a site's meta description. No authored text, no description.
198 $source = $attrs['contentType'] ?? 'custom';
199
200 if ('posts' !== $source && !empty($attrs['items']) && is_array($attrs['items'])) {
201 foreach ($attrs['items'] as $item) {
202 if (!is_array($item) || empty($item['text'])) {
203 continue;
204 }
205 $clean = $this->clean_text($item['text']);
206 if ('' !== $clean) {
207 $text[] = $clean;
208 }
209 }
210 }
211
212 // Static blocks (the Infinite Slider and its items) do have innerContent.
213 if (!empty($block['innerContent'])) {
214 foreach ($block['innerContent'] as $inner) {
215 if (!is_string($inner) || '' === trim($inner)) {
216 continue;
217 }
218 $clean = $this->clean_text($inner);
219 if ('' !== $clean) {
220 $text[] = $clean;
221 }
222 }
223 }
224
225 return $text;
226 }
227
228 /**
229 * Strip markup and collapse whitespace on one fragment.
230 *
231 * @param string $value
232 * @return string
233 */
234 private function clean_text($value) {
235 $value = wp_strip_all_tags((string) $value);
236 $value = html_entity_decode($value, ENT_QUOTES, get_bloginfo('charset') ?: 'UTF-8');
237
238 return trim(preg_replace('/\s+/u', ' ', $value));
239 }
240
241 /**
242 * Check if a SEO plugin is active
243 *
244 * @return bool Whether a SEO plugin is active
245 */
246 private function is_seo_plugin_active() {
247 return defined('WPSEO_VERSION') || class_exists('RankMath');
248 }
249 }
250