PluginProbe
Search Atlas SEO – OTTO AI SEO Automation for WordPress / 2.7.1
Search Atlas SEO – OTTO AI SEO Automation for WordPress v2.7.1
2.7.1 2.7.2 2.7.0 2.6.26 2.6.25 2.6.24 2.6.23 2.6.22 2.6.21 2.6.20 2.6.19 2.6.18 2.6.17 2.6.16 2.6.15 2.6.14 2.6.13 2.6.12 2.6.11 2.6.10 2.6.9 2.6.8 2.6.7 2.6.6 2.6.5 All 141 releases
metasync / includes / class-metasync-seo-suite.php

class-metasync-seo-suite.php in Search Atlas SEO – OTTO AI SEO Automation for WordPress 2.7.1, at includes/class-metasync-seo-suite.php

1,005 lines 69.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Unified "SEO Suite" meta box.
4 *
5 * Consolidates MetaSync's separate Classic-editor meta boxes (SEO, Robots,
6 * Canonical, Redirection, Social & Open Graph, Schema, Video Sitemap) into a
7 * single tabbed box. Classic editor only — the block editor keeps its own SEO
8 * sidebar and is left untouched.
9 *
10 * Presentation-only merge: this class does NOT reimplement any save logic. It
11 * runs after MetaSync registers its own boxes, captures their render callbacks,
12 * removes them from display, and renders the same fields (identical name/nonce
13 * attributes) inside one tabbed container. The plugin's existing save_post
14 * handlers persist everything unchanged. Schema is rendered via MetaSync's own
15 * builder callback. Custom HTML settings stay in their standalone box.
16 *
17 * On LPS / custom-HTML pages (metasync_is_custom_or_lps_page) the box shows a
18 * read-only notice and emits no nonces, so nothing here overwrites the
19 * externally-managed SEO for those pages.
20 *
21 * @package Metasync
22 */
23
24 if (!defined('ABSPATH')) {
25 exit;
26 }
27
28 class Metasync_Seo_Suite
29 {
30 /** Original MetaSync meta-box IDs this box absorbs (Custom HTML is intentionally excluded). */
31 private $target_ids = array(
32 'metasync-seo-meta',
33 'metasync-seo-lps-notice',
34 'common-robots-meta',
35 'advance-robots-meta',
36 'post-canonical-meta',
37 'post-redirection-meta',
38 'metasync_opengraph_meta_box',
39 'metasync-schema-markup',
40 'metasync-video-sitemap-meta',
41 );
42
43 /** @var array<string,bool> Which target boxes were present on the current screen. */
44 private $present = array();
45
46 /** @var array|null Captured Schema meta-box definition (rendered inside the Schema tab). */
47 private $schema_box = null;
48
49 public function __construct()
50 {
51 add_action('add_meta_boxes', array($this, 'unify'), 9999, 2);
52 add_action('admin_enqueue_scripts', array($this, 'enqueue_assets'));
53 }
54
55 /** Load the media library for the OG/Twitter image pickers. */
56 public function enqueue_assets($hook)
57 {
58 if (in_array($hook, array('post.php', 'post-new.php'), true)) {
59 wp_enqueue_media();
60 }
61 }
62
63 /**
64 * True for any request originating from the block editor.
65 *
66 * The block editor renders its meta-box area during the main page load
67 * (is_block_editor() is true there) and re-posts that same form to
68 * post.php?meta-box-loader=1 when saving. Both are treated as block-editor
69 * context so this box never engages in either.
70 */
71 private function is_block_editor_request()
72 {
73 if (!empty($_REQUEST['meta-box-loader'])) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only request flag for meta box registration
74 return true;
75 }
76 $screen = get_current_screen();
77 return $screen && $screen->is_block_editor();
78 }
79
80 /**
81 * Runs after MetaSync registers its boxes (default priority). Captures the
82 * target boxes, removes them from display, and registers the unified box.
83 */
84 public function unify($post_type, $post)
85 {
86 /** Allow sites to opt out of the unified box and keep the classic separate boxes. */
87 if (!apply_filters('metasync_enable_seo_suite', true, $post_type, $post)) {
88 return;
89 }
90
91 // Classic editor only. In the block editor the plugin's own SEO sidebar owns
92 // these fields, and the SEO box is registered __back_compat_meta_box so it
93 // never renders there at all. Returning before any remove_meta_box() call
94 // leaves the block editor byte-for-byte as it is without this class.
95 if ($this->is_block_editor_request()) {
96 return;
97 }
98
99 global $wp_meta_boxes;
100 if (empty($wp_meta_boxes[$post_type])) {
101 return;
102 }
103
104 $this->present = array();
105 $this->schema_box = null;
106
107 foreach ($wp_meta_boxes[$post_type] as $context => $priorities) {
108 foreach ((array) $priorities as $boxes) {
109 if (!is_array($boxes)) {
110 continue;
111 }
112 foreach ($boxes as $box_id => $box) {
113 if (in_array($box_id, $this->target_ids, true) && $box && !empty($box['callback'])) {
114 $this->present[$box_id] = true;
115 if ($box_id === 'metasync-schema-markup') {
116 $this->schema_box = $box;
117 }
118 }
119 }
120 }
121 foreach ($this->target_ids as $id) {
122 remove_meta_box($id, $post_type, $context);
123 }
124 }
125
126 if (empty($this->present)) {
127 return;
128 }
129
130 if (empty($wp_meta_boxes[$post_type]['normal']['high']['metasync_seo_suite'])) {
131 $plugin_name = class_exists('Metasync') ? Metasync::get_effective_plugin_name() : 'MetaSync';
132 add_meta_box(
133 'metasync_seo_suite',
134 sprintf('SEO Suite by %s', $plugin_name),
135 array($this, 'render'),
136 $post_type,
137 'normal',
138 'high'
139 );
140 }
141 }
142
143 /** True when the page's front-end output bypasses WordPress SEO (LPS / custom-HTML). */
144 private function is_managed_page($post_id)
145 {
146 if (function_exists('metasync_is_custom_or_lps_page') && $post_id > 0 && metasync_is_custom_or_lps_page($post_id)) {
147 return true;
148 }
149 // Fallbacks if the helper isn't loaded on this request.
150 if (get_post_meta($post_id, '_metasync_lps_import', true)) {
151 return true;
152 }
153 if (get_post_meta($post_id, '_metasync_is_custom_html_page', true) === '1'
154 && get_post_meta($post_id, '_metasync_raw_html_enabled', true) === '1') {
155 return true;
156 }
157 return false;
158 }
159
160 private static function v($post_id, $key)
161 {
162 $value = get_post_meta($post_id, $key, true);
163 if (is_array($value)) {
164 return '';
165 }
166 // Collapse the "Auto Draft" placeholder the meta box pre-fill can persist into
167 // the social title/description keys, so the unified box shows the real pre-fill
168 // instead and re-saving the post clears the stored placeholder.
169 // @phpstan-ignore-next-line function.alreadyNarrowedType
170 if (method_exists('Metasync_OpenGraph', 'strip_auto_draft_title')
171 && defined('Metasync_OpenGraph::AUTO_DRAFT_PRONE_KEYS')
172 && in_array($key, Metasync_OpenGraph::AUTO_DRAFT_PRONE_KEYS, true)
173 ) {
174 $value = Metasync_OpenGraph::strip_auto_draft_title($value);
175 }
176 // A social title that merely mirrors the post title is the old pre-fill's
177 // snapshot — stale the moment the post is renamed — not a customization.
178 // Collapsed so the field renders empty (the default stays visible as the
179 // placeholder) and the shared save handler stores nothing for it.
180 // @phpstan-ignore-next-line function.alreadyNarrowedType
181 if (method_exists('Metasync_OpenGraph', 'strip_title_snapshot')
182 && defined('Metasync_OpenGraph::TITLE_DEFAULTED_KEYS')
183 && in_array($key, Metasync_OpenGraph::TITLE_DEFAULTED_KEYS, true)
184 && is_string($value)
185 ) {
186 $value = Metasync_OpenGraph::strip_title_snapshot($post_id, $key, $value);
187 }
188 // A social description that merely mirrors the resolved excerpt is the
189 // same kind of snapshot — stale the moment the excerpt or content
190 // changes — collapsed for the same reason as the title above.
191 // @phpstan-ignore-next-line function.alreadyNarrowedType
192 if (method_exists('Metasync_OpenGraph', 'strip_description_snapshot')
193 && defined('Metasync_OpenGraph::DESCRIPTION_DEFAULTED_KEYS')
194 && in_array($key, Metasync_OpenGraph::DESCRIPTION_DEFAULTED_KEYS, true)
195 && is_string($value)
196 ) {
197 $value = Metasync_OpenGraph::strip_description_snapshot($post_id, $key, $value);
198 }
199 return (string) $value;
200 }
201
202 private static function checked_attr($cond)
203 {
204 return $cond ? ' checked' : '';
205 }
206
207 /** A clean, "pretty" permalink for placeholders/preview (avoids ?p=ID on drafts). */
208 private static function pretty_permalink($post)
209 {
210 $link = get_permalink($post->ID);
211 if ($link
212 && strpos($link, '?p=') === false
213 && strpos($link, '?page_id=') === false
214 && strpos($link, 'preview=') === false) {
215 return $link;
216 }
217 $slug = $post->post_name ? $post->post_name : sanitize_title(get_the_title($post->ID));
218 if (!$slug) {
219 $slug = 'your-post';
220 }
221 return user_trailingslashit(home_url('/' . $slug . '/'));
222 }
223
224 /**
225 * Render the unified box.
226 */
227 public function render($post)
228 {
229 $P = $this->present;
230 $id = $post->ID;
231
232 $managed = $this->is_managed_page($id);
233
234 $has_seo = isset($P['metasync-seo-meta']) || isset($P['metasync-seo-lps-notice']);
235 $has_crob = isset($P['common-robots-meta']);
236 $has_arob = isset($P['advance-robots-meta']);
237 $has_robots = $has_crob || $has_arob;
238 $has_canon = isset($P['post-canonical-meta']);
239 $has_redir = isset($P['post-redirection-meta']);
240 $has_social = isset($P['metasync_opengraph_meta_box']);
241 $has_schema = isset($P['metasync-schema-markup']);
242 $has_video = isset($P['metasync-video-sitemap-meta']);
243
244 // Current values.
245 $seo_title = self::v($id, '_metasync_seo_title');
246 $seo_desc = self::v($id, '_metasync_seo_desc');
247 $otto_kw = self::v($id, '_metasync_otto_keywords');
248
249 // The greyed-out value each field shows when left blank: OTTO's
250 // suggestion, or the imported value where OTTO is silent. Which tier
251 // that is — and whether OTTO counts at all, since the per-post "Disable
252 // OTTO" toggle makes its stored suggestion dead data — is
253 // Metasync_Seo_Precedence's call, not this screen's. Without a ghost the
254 // fields look completely empty after an import and the customer
255 // concludes it did nothing.
256 $title_fallback = Metasync_Seo_Precedence::fallback($id, Metasync_Seo_Precedence::FIELD_TITLE);
257 $desc_fallback = Metasync_Seo_Precedence::fallback($id, Metasync_Seo_Precedence::FIELD_DESCRIPTION);
258
259 $ghost_title = $title_fallback['value'];
260 $ghost_desc = $desc_fallback['value'];
261
262 $show_imported_note = $title_fallback['source'] === Metasync_Seo_Precedence::SOURCE_IMPORTED
263 || $desc_fallback['source'] === Metasync_Seo_Precedence::SOURCE_IMPORTED;
264
265 // Robots: post meta first, falling back to the site-wide defaults exactly as
266 // the standalone Common/Advance Robots boxes do (new option spelling first,
267 // then the legacy "mata" spelling).
268 $cr = get_post_meta($id, 'metasync_common_robots', true);
269 if (!$cr && class_exists('Metasync')) {
270 $cr = Metasync::get_option('common_robots_meta') ?? Metasync::get_option('common_robots_mata') ?? '';
271 }
272 if (!is_array($cr)) {
273 $cr = array();
274 }
275 $ar = get_post_meta($id, 'metasync_advance_robots', true);
276 if (!$ar && class_exists('Metasync')) {
277 $ar = Metasync::get_option('advance_robots_meta') ?? Metasync::get_option('advance_robots_mata') ?? '';
278 }
279 if (!is_array($ar)) {
280 $ar = array();
281 }
282 $ar_snip_e = isset($ar['max-snippet']['enable']) ? $ar['max-snippet']['enable'] : '';
283 $ar_snip_l = isset($ar['max-snippet']['length']) ? $ar['max-snippet']['length'] : '';
284 $ar_vid_e = isset($ar['max-video-preview']['enable']) ? $ar['max-video-preview']['enable'] : '';
285 $ar_vid_l = isset($ar['max-video-preview']['length']) ? $ar['max-video-preview']['length'] : '';
286 $ar_img_e = isset($ar['max-image-preview']['enable']) ? $ar['max-image-preview']['enable'] : '';
287 $ar_img_l = isset($ar['max-image-preview']['length']) ? $ar['max-image-preview']['length'] : '';
288
289 // Canonical: same sanitise-and-repair path as the standalone Canonical box.
290 // A bare reset() here would re-corrupt nested-array rows to the string "Array".
291 $raw_canonical = get_post_meta($id, 'meta_canonical', true);
292 if (class_exists('Metasync_Canonical_Sanitizer')) {
293 $canon = Metasync_Canonical_Sanitizer::sanitize($raw_canonical);
294 if (is_array($raw_canonical) || Metasync_Canonical_Sanitizer::is_corrupted($raw_canonical)) {
295 if ($canon !== '') {
296 update_post_meta($id, 'meta_canonical', $canon);
297 } else {
298 delete_post_meta($id, 'meta_canonical');
299 }
300 } elseif ($canon === '' && is_string($raw_canonical) && trim($raw_canonical) !== '') {
301 // Stored value the validator doesn't recognise: show it as-is so a
302 // routine post save doesn't silently wipe it.
303 $canon = trim($raw_canonical);
304 }
305 } else {
306 $canon = is_array($raw_canonical) ? '' : (string) $raw_canonical;
307 }
308 $pretty = self::pretty_permalink($post);
309
310 $rd = get_post_meta($id, 'metasync_post_redirection_meta', true);
311 if (!is_array($rd)) {
312 $rd = array();
313 }
314 $rd_en = (isset($rd['enable']) ? $rd['enable'] : '') === 'true';
315 $rd_type = isset($rd['type']) ? $rd['type'] : '301';
316 $rd_url = isset($rd['url']) ? $rd['url'] : '';
317
318 // The standalone Open Graph box defaults this setting to enabled when
319 // no value has been saved yet. Preserve that behaviour: only an
320 // explicit '0' represents an editor opting out.
321 $og_en_value = get_post_meta($id, '_metasync_og_enabled', true);
322 $og_en = ($og_en_value !== '0');
323 // Match the standalone Open Graph box exactly: its computed defaults are
324 // real form values (and are therefore persisted on save), not placeholders.
325 $og_defaults = array('title' => '', 'description' => '', 'image' => '');
326 $opengraph = class_exists('Metasync_OpenGraph') ? Metasync_OpenGraph::get_instance() : null;
327 if ($opengraph) {
328 $og_defaults = $opengraph->get_default_og_values($id);
329 }
330 // Social titles and descriptions render placeholder-only: the default
331 // (the post title, the resolved excerpt) stays visible in the
332 // placeholder but is never a form value, so submitting the page cannot
333 // persist a snapshot of it. Images keep their computed default as a
334 // real value, matching the standalone OG box.
335 $og_title = self::v($id, '_metasync_og_title');
336 $og_desc = self::v($id, '_metasync_og_description');
337 $og_image = self::v($id, '_metasync_og_image') ?: $og_defaults['image'];
338 $og_type = self::v($id, '_metasync_og_type') ?: 'article';
339 $og_url = self::v($id, '_metasync_og_url');
340 if ($og_url === '') {
341 $og_url = $opengraph ? $opengraph->get_canonical_url($post) : $pretty;
342 }
343 $tw_card = self::v($id, '_metasync_twitter_card') ?: 'summary_large_image';
344 $tw_site = self::v($id, '_metasync_twitter_site');
345 $tw_title = self::v($id, '_metasync_twitter_title');
346 // Placeholder-only like the standalone box: when no twitter title is
347 // stored the placeholder shows the og title if one is set, else the
348 // default (post) title — the exact chain the standalone OG box uses.
349 $tw_title_placeholder = ($og_title !== '') ? $og_title : $og_defaults['title'];
350 // Same chain for the twitter description's placeholder: the og
351 // description if one is set, else the resolved excerpt default.
352 $tw_desc_placeholder = ($og_desc !== '') ? $og_desc : $og_defaults['description'];
353 $tw_desc = self::v($id, '_metasync_twitter_description');
354 $tw_image = self::v($id, '_metasync_twitter_image') ?: $og_image;
355 $tw_alt = self::v($id, '_metasync_twitter_image_alt');
356 $tw_app_iphone = self::v($id, '_metasync_twitter_app_id_iphone');
357 $tw_app_ipad = self::v($id, '_metasync_twitter_app_id_ipad');
358 $tw_app_gp = self::v($id, '_metasync_twitter_app_id_googleplay');
359 $tw_app_url_iphone = self::v($id, '_metasync_twitter_app_url_iphone');
360 $tw_app_url_ipad = self::v($id, '_metasync_twitter_app_url_ipad');
361 $tw_app_url_gp = self::v($id, '_metasync_twitter_app_url_googleplay');
362 $tw_app_country = self::v($id, '_metasync_twitter_app_country');
363 $tw_player = self::v($id, '_metasync_twitter_player');
364 $tw_player_w = self::v($id, '_metasync_twitter_player_width');
365 $tw_player_h = self::v($id, '_metasync_twitter_player_height');
366 $tw_app_disabled = $tw_card === 'app' ? '' : ' disabled';
367 $tw_player_disabled = $tw_card === 'player' ? '' : ' disabled';
368
369 $v_url = self::v($id, '_metasync_video_url');
370 $v_thumb = self::v($id, '_metasync_video_thumbnail');
371 $v_title = self::v($id, '_metasync_video_title');
372 $v_desc = self::v($id, '_metasync_video_description');
373 $v_dur = self::v($id, '_metasync_video_duration');
374
375 $host = strtoupper((string) wp_parse_url(home_url(), PHP_URL_HOST));
376 $host_lc = strtolower($host);
377 $prev_url_disp = preg_replace('#^https?://#', '', (string) $pretty);
378 $site = get_bloginfo('name');
379 if ($site === '') {
380 $site = $host;
381 }
382 $avatar = strtoupper(function_exists('mb_substr') ? mb_substr($site, 0, 1) : substr($site, 0, 1));
383 $handle = sanitize_title($site) ?: 'site';
384
385 $this->styles();
386 ?>
387 <div id="mss-root" data-theme="light"<?php echo $managed ? ' class="lps"' : ''; ?>>
388 <?php
389 // On managed pages (LPS / custom-HTML) we emit NO nonces, so nothing here can
390 // overwrite the externally-managed SEO for those pages.
391 if (!$managed) {
392 if ($has_seo) { wp_nonce_field('metasync_seo_meta_nonce', 'metasync_seo_meta_nonce'); }
393 if ($has_crob) { wp_nonce_field('metasync_common_robots_nonce', 'metasync_common_robots_nonce'); }
394 if ($has_arob) { wp_nonce_field('metasync_advance_robots_nonce', 'metasync_advance_robots_nonce'); }
395 if ($has_canon) { wp_nonce_field('metasync_post_canonical_nonce', 'metasync_post_canonical_nonce'); }
396 if ($has_redir) { wp_nonce_field('metasync_post_redirection_nonce', 'metasync_post_redirection_nonce'); }
397 if ($has_social) { wp_nonce_field('metasync_opengraph_nonce', 'metasync_opengraph_nonce'); }
398 if ($has_video) { wp_nonce_field('metasync_video_sitemap_meta_nonce', 'metasync_video_sitemap_meta_nonce'); }
399 }
400 ?>
401 <div class="metabox">
402 <div class="mb-head">
403 <div class="badge">M</div>
404 <div class="t"><b>SEO Suite</b><span>On-page SEO for this post</span></div>
405 <div class="mss-actions">
406 <button type="button" class="theme-toggle" onclick="mssTheme(this)"><span class="ti">&#127769;</span> <span class="tl">Dark</span></button>
407 </div>
408 </div>
409
410 <div class="mss-lockable">
411 <div class="mb-body"<?php echo $managed ? ' inert' : ''; ?>>
412 <div class="rail">
413 <?php
414 $first = true;
415 $tab = function ($key, $icon, $label) use (&$first) {
416 echo '<div class="nav' . ($first ? ' active' : '') . '" role="button" tabindex="0" onclick="mssGo(this,\'' . esc_attr($key) . '\')" onkeydown="mssKey(event,this)"><span class="ic">' . esc_html($icon) . '</span> ' . esc_html($label) . '</div>';
417 $first = false;
418 };
419 if ($has_seo) { $tab('seo', '&#128269;', 'SEO'); }
420 if ($has_robots) { $tab('robots', '&#129302;', 'Robots'); }
421 if ($has_canon) { $tab('canonical', '&#128279;', 'Canonical'); }
422 if ($has_redir) { $tab('redirect', '&#8618;&#65039;', 'Redirection'); }
423 if ($has_social) { $tab('social', '&#128226;', 'Social &amp; OG'); }
424 if ($has_schema) { $tab('schema', '&#128208;', 'Schema'); }
425 if ($has_video) { $tab('video', '&#127916;', 'Video'); }
426 ?>
427 </div>
428
429 <div class="panel-wrap">
430 <?php
431 $pfirst = true;
432 $pcls = function () use (&$pfirst) {
433 $c = 'panel' . ($pfirst ? ' active' : '');
434 $pfirst = false;
435 return $c;
436 };
437 ?>
438
439 <?php if ($has_seo): ?>
440 <div class="<?php echo esc_attr($pcls()); ?>" data-p="seo">
441 <h2 class="p-title">&#128269; Search Appearance</h2>
442 <p class="p-sub">Set the SEO Title and Meta Description. Leave blank to use the OTTO suggestion.</p>
443 <?php if ($show_imported_note): ?>
444 <p class="p-sub">The greyed-out value below was imported from another SEO plugin. It is used only until OTTO has a suggestion for this page.</p>
445 <?php endif; ?>
446 <div class="field">
447 <div class="lbl"><span>SEO Title</span><span class="cc"><span class="tC">0</span>/60</span></div>
448 <input class="ctrl seoT" name="metasync_seo_title" value="<?php echo esc_attr($seo_title); ?>" placeholder="<?php echo esc_attr($ghost_title); ?>" oninput="mssRc()">
449 </div>
450 <div class="field">
451 <div class="lbl"><span>Meta Description</span><span class="cc"><span class="dC">0</span>/160</span></div>
452 <textarea class="ctrl seoD" name="metasync_seo_desc" placeholder="<?php echo esc_attr($ghost_desc); ?>" oninput="mssRc()"><?php echo esc_textarea($seo_desc); ?></textarea>
453 </div>
454 <?php if ($otto_kw !== ''): ?>
455 <div class="field">
456 <div class="lbl"><span>Focus Keyword</span><span class="ro-badge">Managed by OTTO</span></div>
457 <div class="mss-ro"><span class="lock">&#128274;</span> <?php echo esc_html($otto_kw); ?></div>
458 <div class="hint">Set in Search Atlas &mdash; shown here for reference. Read-only.</div>
459 </div>
460 <?php endif; ?>
461 <div class="sec">Preview</div>
462 <div class="gprev">
463 <div class="u"><?php echo esc_html($prev_url_disp); ?></div>
464 <div class="ti gT"><?php echo esc_html($seo_title ?: ($ghost_title ?: get_the_title($id))); ?></div>
465 <div class="de gD"><?php echo esc_html($seo_desc ?: ($ghost_desc ?: 'No description set.')); ?></div>
466 </div>
467 </div>
468 <?php endif; ?>
469
470 <?php if ($has_robots): ?>
471 <div class="<?php echo esc_attr($pcls()); ?>" data-p="robots">
472 <h2 class="p-title">&#129302; Robots Meta</h2>
473 <p class="p-sub">Crawler directives for this post. These are independent flags &mdash; combine as needed.</p>
474 <?php if ($has_crob): ?>
475 <div class="sec">Common directives</div>
476 <div class="chips">
477 <?php
478 $common = array(
479 'index' => array('Index', 'Allow search engines to index this page'),
480 'noindex' => array('No Index', 'Keep this page out of results'),
481 'nofollow' => array('No Follow', "Don't follow links on this page"),
482 'noarchive' => array('No Archive', 'No cached copy'),
483 'noimageindex' => array('No Image Index', 'Exclude images from image search'),
484 'nosnippet' => array('No Snippet', 'No text snippet in results'),
485 );
486 foreach ($common as $key => $c): ?>
487 <label class="chip">
488 <input type="checkbox" name="common_robots_meta[<?php echo esc_attr($key); ?>]" value="<?php echo esc_attr($key); ?>"<?php // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- checked_attr() returns the validated checked attribute.
489 echo isset($cr[$key]) ? esc_attr(self::checked_attr($cr[$key] === $key)) : ''; ?>>
490 <span class="box">&#10003;</span><div><?php echo esc_html($c[0]); ?><small><?php echo esc_html($c[1]); ?></small></div>
491 </label>
492 <?php endforeach; ?>
493 </div>
494 <?php endif; ?>
495 <?php if ($has_arob): ?>
496 <div class="sec">Advanced limits <span class="sec-note">&mdash; tick a limit to apply it</span></div>
497 <div class="row">
498 <div class="field">
499 <label class="chip mss-advchip"><input type="checkbox" name="advanced_robots_meta[max-snippet][enable]" value="1"<?php checked('1', $ar_snip_e); ?>><span class="box">&#10003;</span><div>Max Snippet</div></label>
500 <input class="ctrl" type="number" name="advanced_robots_meta[max-snippet][length]" value="<?php echo esc_attr($ar_snip_l); ?>" min="-1" placeholder="e.g. 160">
501 <div class="hint">Max snippet length in characters.</div></div>
502 <div class="field">
503 <label class="chip mss-advchip"><input type="checkbox" name="advanced_robots_meta[max-video-preview][enable]" value="1"<?php checked('1', $ar_vid_e); ?>><span class="box">&#10003;</span><div>Max Video Preview</div></label>
504 <input class="ctrl" type="number" name="advanced_robots_meta[max-video-preview][length]" value="<?php echo esc_attr($ar_vid_l); ?>" min="-1" placeholder="e.g. -1">
505 <div class="hint">Max preview duration in seconds.</div></div>
506 </div>
507 <div class="field">
508 <label class="chip mss-advchip"><input type="checkbox" name="advanced_robots_meta[max-image-preview][enable]" value="1"<?php checked('1', $ar_img_e); ?>><span class="box">&#10003;</span><div>Max Image Preview</div></label>
509 <select class="ctrl" name="advanced_robots_meta[max-image-preview][length]">
510 <option value="large"<?php selected($ar_img_l, 'large'); ?>>Large</option>
511 <option value="standard"<?php selected($ar_img_l, 'standard'); ?>>Standard</option>
512 <option value="none"<?php selected($ar_img_l, 'none'); ?>>None</option>
513 </select></div>
514 <?php endif; ?>
515 </div>
516 <?php endif; ?>
517
518 <?php if ($has_canon): ?>
519 <div class="<?php echo esc_attr($pcls()); ?>" data-p="canonical">
520 <h2 class="p-title">&#128279; Canonical URL</h2>
521 <p class="p-sub">Point search engines to the preferred version of this content.</p>
522 <div class="field">
523 <div class="lbl"><span>Canonical URL</span></div>
524 <input class="ctrl" type="text" name="post_canonical_url_meta" value="<?php echo esc_attr($canon); ?>" placeholder="<?php echo esc_attr($pretty); ?>">
525 <div class="hint">Leave blank to use this post's own URL.</div>
526 </div>
527 </div>
528 <?php endif; ?>
529
530 <?php if ($has_redir): ?>
531 <div class="<?php echo esc_attr($pcls()); ?>" data-p="redirect">
532 <h2 class="p-title">&#8618;&#65039; Redirection</h2>
533 <p class="p-sub">Send visitors of this post somewhere else.</p>
534 <div class="switch-card">
535 <label class="switch"><input type="checkbox" name="post_redirect_meta[enable]" value="true"<?php // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- checked_attr() returns the validated checked attribute.
536 echo esc_attr(self::checked_attr($rd_en)); ?>></label>
537 <div class="m"><b>Enable redirection</b><span>Send visitors of this post to another URL</span></div>
538 </div>
539 <div class="row">
540 <div class="field"><div class="lbl"><span>Type</span></div>
541 <select class="ctrl" name="post_redirect_meta[type]">
542 <option value="301"<?php selected($rd_type, '301'); ?>>301 Permanent Move</option>
543 <option value="302"<?php selected($rd_type, '302'); ?>>302 Temporary Move</option>
544 <option value="307"<?php selected($rd_type, '307'); ?>>307 Temporary Redirect</option>
545 <option value="410"<?php selected($rd_type, '410'); ?>>410 Content Deleted</option>
546 <option value="451"<?php selected($rd_type, '451'); ?>>451 Content Unavailable</option>
547 </select></div>
548 <div class="field"><div class="lbl"><span>Destination URL</span></div>
549 <input class="ctrl" type="text" name="post_redirect_meta[url]" value="<?php echo esc_attr($rd_url); ?>" placeholder="https://example.com/new-url/"></div>
550 </div>
551 </div>
552 <?php endif; ?>
553
554 <?php if ($has_social): ?>
555 <div class="<?php echo esc_attr($pcls()); ?>" data-p="social">
556 <h2 class="p-title">&#128226; Social &amp; Open Graph</h2>
557 <p class="p-sub">How this post looks when shared on social platforms.</p>
558 <div class="switch-card">
559 <label class="switch"><input type="checkbox" name="_metasync_og_enabled" value="1"<?php // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- checked_attr() returns the validated checked attribute.
560 echo esc_attr(self::checked_attr($og_en)); ?>></label>
561 <div class="m"><b>Enable Open Graph &amp; Social Media Tags</b><span>Adds Open Graph + Twitter Card tags for better sharing</span></div>
562 </div>
563
564 <div class="subhead"><span class="sq"></span> Open Graph</div>
565 <div class="field"><div class="lbl"><span>Title (og:title)</span></div><input class="ctrl" name="_metasync_og_title" maxlength="60" value="<?php echo esc_attr($og_title); ?>" placeholder="<?php echo esc_attr($og_defaults['title']); ?>" oninput="mssSocial()"></div>
566 <div class="field"><div class="lbl"><span>Description (og:description)</span></div><textarea class="ctrl" name="_metasync_og_description" maxlength="155" placeholder="<?php echo esc_attr($og_defaults['description']); ?>" oninput="mssSocial()"><?php echo esc_textarea($og_desc); ?></textarea></div>
567 <div class="field"><div class="lbl"><span>Image (og:image)</span></div>
568 <div class="mss-imgrow"><input class="ctrl ogimg" type="url" name="_metasync_og_image" value="<?php echo esc_attr($og_image); ?>" placeholder="https://example.com/image.jpg" oninput="mssSocial()"><button class="btn ghost sm mss-pick" data-target=".ogimg" type="button">Select</button></div>
569 <div class="hint">Recommended 1200&times;630px.</div></div>
570 <div class="row">
571 <div class="field"><div class="lbl"><span>Type (og:type)</span></div>
572 <select class="ctrl" name="_metasync_og_type">
573 <?php foreach (array('article', 'website', 'blog', 'product', 'video', 'music') as $t): ?>
574 <option value="<?php echo esc_attr($t); ?>"<?php selected($og_type, $t); ?>><?php echo esc_html(ucfirst($t)); ?></option>
575 <?php endforeach; ?>
576 </select></div>
577 <div class="field"><div class="lbl"><span>URL (og:url)</span></div><input class="ctrl" type="url" name="_metasync_og_url" value="<?php echo esc_attr($og_url); ?>" placeholder="<?php echo esc_attr($pretty); ?>"<?php echo $post->post_status === 'auto-draft' ? ' disabled' : ''; ?>></div>
578 </div>
579
580 <div class="subhead"><span class="sq"></span> Twitter Card</div>
581 <div class="field"><div class="lbl"><span>Card Type</span></div>
582 <select class="ctrl mssTw" name="_metasync_twitter_card" onchange="mssTwT(this)">
583 <option value="summary"<?php selected($tw_card, 'summary'); ?>>Summary</option>
584 <option value="summary_large_image"<?php selected($tw_card, 'summary_large_image'); ?>>Summary Large Image</option>
585 <option value="app"<?php selected($tw_card, 'app'); ?>>App</option>
586 <option value="player"<?php selected($tw_card, 'player'); ?>>Player</option>
587 </select></div>
588 <div class="row">
589 <div class="field"><div class="lbl"><span>Twitter Site</span></div><input class="ctrl" name="_metasync_twitter_site" value="<?php echo esc_attr($tw_site); ?>" placeholder="@yoursite"></div>
590 <div class="field"><div class="lbl"><span>Twitter Title</span></div><input class="ctrl" name="_metasync_twitter_title" maxlength="70" value="<?php echo esc_attr($tw_title); ?>" placeholder="<?php echo esc_attr($tw_title_placeholder); ?>" oninput="mssSocial()"></div>
591 </div>
592 <div class="field"><div class="lbl"><span>Twitter Description</span></div><textarea class="ctrl" name="_metasync_twitter_description" maxlength="200" placeholder="<?php echo esc_attr($tw_desc_placeholder); ?>" oninput="mssSocial()"><?php echo esc_textarea($tw_desc); ?></textarea></div>
593 <div class="row">
594 <div class="field"><div class="lbl"><span>Twitter Image</span></div>
595 <div class="mss-imgrow"><input class="ctrl twimg" type="url" name="_metasync_twitter_image" value="<?php echo esc_attr($tw_image); ?>" placeholder="Falls back to OG image" oninput="mssSocial()"><button class="btn ghost sm mss-pick" data-target=".twimg" type="button">Select</button></div></div>
596 <div class="field"><div class="lbl"><span>Image Alt</span></div><input class="ctrl" name="_metasync_twitter_image_alt" maxlength="420" value="<?php echo esc_attr($tw_alt); ?>" placeholder="Accessibility description"></div>
597 </div>
598
599 <div class="mssTwApp" style="display:<?php echo $tw_card === 'app' ? 'block' : 'none'; ?>">
600 <div class="subhead"><span class="sq"></span> App Card</div>
601 <div class="row">
602 <div class="field"><div class="lbl"><span>iPhone App ID</span></div><input class="ctrl" name="_metasync_twitter_app_id_iphone" value="<?php echo esc_attr($tw_app_iphone); ?>" placeholder="307234931"<?php echo esc_attr($tw_app_disabled); ?>></div>
603 <div class="field"><div class="lbl"><span>iPad App ID</span></div><input class="ctrl" name="_metasync_twitter_app_id_ipad" value="<?php echo esc_attr($tw_app_ipad); ?>" placeholder="307234931"<?php echo esc_attr($tw_app_disabled); ?>></div>
604 </div>
605 <div class="field"><div class="lbl"><span>Google Play App ID</span></div><input class="ctrl" name="_metasync_twitter_app_id_googleplay" value="<?php echo esc_attr($tw_app_gp); ?>" placeholder="com.android.app"<?php echo esc_attr($tw_app_disabled); ?>></div>
606 <div class="row">
607 <div class="field"><div class="lbl"><span>iPhone Custom URL</span></div><input class="ctrl" type="url" name="_metasync_twitter_app_url_iphone" value="<?php echo esc_attr($tw_app_url_iphone); ?>" placeholder="myapp://"<?php echo esc_attr($tw_app_disabled); ?>></div>
608 <div class="field"><div class="lbl"><span>iPad Custom URL</span></div><input class="ctrl" type="url" name="_metasync_twitter_app_url_ipad" value="<?php echo esc_attr($tw_app_url_ipad); ?>" placeholder="myapp://"<?php echo esc_attr($tw_app_disabled); ?>></div>
609 </div>
610 <div class="row">
611 <div class="field"><div class="lbl"><span>Google Play Custom URL</span></div><input class="ctrl" type="url" name="_metasync_twitter_app_url_googleplay" value="<?php echo esc_attr($tw_app_url_gp); ?>" placeholder="myapp://"<?php echo esc_attr($tw_app_disabled); ?>></div>
612 <div class="field"><div class="lbl"><span>App Store Country</span></div>
613 <select class="ctrl" name="_metasync_twitter_app_country"<?php echo esc_attr($tw_app_disabled); ?>>
614 <option value=""<?php selected($tw_app_country, ''); ?>>US (Default)</option>
615 <?php foreach (array('GB' => 'United Kingdom', 'CA' => 'Canada', 'AU' => 'Australia', 'DE' => 'Germany', 'FR' => 'France', 'JP' => 'Japan', 'IN' => 'India', 'BR' => 'Brazil', 'MX' => 'Mexico') as $code => $country): ?>
616 <option value="<?php echo esc_attr($code); ?>"<?php selected($tw_app_country, $code); ?>><?php echo esc_html($country); ?></option>
617 <?php endforeach; ?>
618 </select>
619 <div class="hint">Required if your app is not available in the US App Store.</div>
620 </div>
621 </div>
622 </div>
623 <div class="mssTwPlayer" style="display:<?php echo $tw_card === 'player' ? 'block' : 'none'; ?>">
624 <div class="subhead"><span class="sq"></span> Player Card</div>
625 <div class="field"><div class="lbl"><span>Player URL</span></div><input class="ctrl" type="url" name="_metasync_twitter_player" value="<?php echo esc_attr($tw_player); ?>" placeholder="https://example.com/player"<?php echo esc_attr($tw_player_disabled); ?>></div>
626 <div class="row">
627 <div class="field"><div class="lbl"><span>Width (px)</span></div><input class="ctrl short" type="number" name="_metasync_twitter_player_width" min="1" value="<?php echo esc_attr($tw_player_w); ?>" placeholder="1280"<?php echo esc_attr($tw_player_disabled); ?>></div>
628 <div class="field"><div class="lbl"><span>Height (px)</span></div><input class="ctrl short" type="number" name="_metasync_twitter_player_height" min="1" value="<?php echo esc_attr($tw_player_h); ?>" placeholder="720"<?php echo esc_attr($tw_player_disabled); ?>></div>
629 </div>
630 </div>
631
632 <div class="subhead"><span class="sq"></span> Social preview</div>
633 <div class="mss-sptabs">
634 <button type="button" class="on" onclick="mssSpTab(this,'facebook')">Facebook</button>
635 <button type="button" onclick="mssSpTab(this,'twitter')">Twitter / X</button>
636 <button type="button" onclick="mssSpTab(this,'linkedin')">LinkedIn</button>
637 </div>
638 <div class="spwrap">
639 <div class="spcard fb on" data-plat="facebook">
640 <div class="sp-head"><span class="sp-av"><?php echo esc_html($avatar); ?></span><div class="sp-meta"><b><?php echo esc_html($site); ?></b><span>2 hours ago &middot; &#127760;</span></div></div>
641 <div class="sp-img"><span class="sp-noimg">No image selected</span></div>
642 <div class="sp-body">
643 <div class="sp-url"><?php echo esc_html($host); ?></div>
644 <div class="sp-title spT"></div>
645 <div class="sp-desc spD"></div>
646 </div>
647 </div>
648 <div class="spcard tw" data-plat="twitter">
649 <div class="sp-head"><span class="sp-av"><?php echo esc_html($avatar); ?></span><div class="sp-meta"><b><?php echo esc_html($site); ?></b><span>@<?php echo esc_html($handle); ?> &middot; now</span></div></div>
650 <div class="tw-card">
651 <div class="sp-img"><span class="sp-noimg">No image selected</span></div>
652 <div class="sp-body">
653 <div class="sp-title spT"></div>
654 <div class="sp-desc spD"></div>
655 <div class="sp-url"><?php echo esc_html($host_lc); ?></div>
656 </div>
657 </div>
658 </div>
659 <div class="spcard li" data-plat="linkedin">
660 <div class="sp-head"><span class="sp-av"><?php echo esc_html($avatar); ?></span><div class="sp-meta"><b><?php echo esc_html($site); ?></b><span>Promoted</span></div></div>
661 <div class="sp-img"><span class="sp-noimg">No image selected</span></div>
662 <div class="sp-body">
663 <div class="sp-title spT"></div>
664 <div class="sp-url"><?php echo esc_html($host_lc); ?> &middot; 1 min read</div>
665 </div>
666 </div>
667 </div>
668 </div>
669 <?php endif; ?>
670
671 <?php if ($has_schema): ?>
672 <div class="<?php echo esc_attr($pcls()); ?>" data-p="schema">
673 <h2 class="p-title">&#128208; Schema Markup</h2>
674 <div class="mss-native">
675 <?php
676 if (!empty($this->schema_box['callback'])) {
677 call_user_func($this->schema_box['callback'], $post, $this->schema_box);
678 } else {
679 echo '<p class="p-sub">Schema builder unavailable on this screen.</p>';
680 }
681 ?>
682 </div>
683 </div>
684 <?php endif; ?>
685
686 <?php if ($has_video): ?>
687 <div class="<?php echo esc_attr($pcls()); ?>" data-p="video">
688 <h2 class="p-title">&#127916; Video Sitemap</h2>
689 <p class="p-sub">Override auto-detected video data. Leave blank to use auto-detection.</p>
690 <div class="field"><div class="lbl"><span>Video URL</span></div><input class="ctrl" type="url" name="metasync_video_url" value="<?php echo esc_attr($v_url); ?>" placeholder="https://www.youtube.com/watch?v=..."></div>
691 <div class="field"><div class="lbl"><span>Thumbnail URL</span></div><input class="ctrl" type="url" name="metasync_video_thumbnail" value="<?php echo esc_attr($v_thumb); ?>" placeholder="https://img.youtube.com/vi/.../hqdefault.jpg"></div>
692 <div class="field"><div class="lbl"><span>Video Title</span></div><input class="ctrl" name="metasync_video_title" value="<?php echo esc_attr($v_title); ?>" placeholder="Defaults to post title"></div>
693 <div class="field"><div class="lbl"><span>Video Description</span></div><textarea class="ctrl" name="metasync_video_description" placeholder="Defaults to post excerpt"><?php echo esc_textarea($v_desc); ?></textarea></div>
694 <div class="field"><div class="lbl"><span>Duration (seconds)</span></div><input class="ctrl short" type="number" name="metasync_video_duration" min="0" step="1" value="<?php echo esc_attr($v_dur); ?>" placeholder="300"></div>
695 </div>
696 <?php endif; ?>
697
698 </div>
699 </div>
700
701 <div class="mb-foot">
702 <span class="mss-foot-hint">Changes are saved when you update the post.</span>
703 </div>
704
705 <?php if ($managed): ?>
706 <div class="mss-lps-overlay">
707 <div class="mss-lps-card">
708 <div class="mss-lps-icon">&#128274;</div>
709 <h3>SEO managed by WebStudio</h3>
710 <p>This page's content is served as pre-built HTML before WordPress renders its <code>&lt;head&gt;</code>, so these SEO Suite settings won't apply. SEO for this page is managed by <b>WebStudio</b> (or OTTO, if enabled) to keep everything in sync.</p>
711 <div class="mss-lps-hint">Editing is disabled here to avoid conflicting values.</div>
712 </div>
713 </div>
714 <?php endif; ?>
715 </div>
716 </div>
717 </div>
718
719 <script>
720 (function(){
721 var root=document.getElementById('mss-root'); if(!root) return;
722 function byName(n){return root.querySelector('[name="'+n+'"]');}
723 function mssFld(n){var e=byName(n);return e?e.value.trim():'';}
724 window.mssKey=function(e,el){ if(e.key==='Enter'||e.key===' '){ e.preventDefault(); el.click(); } };
725 window.mssGo=function(el,p){
726 el.parentElement.querySelectorAll('.nav').forEach(function(n){n.classList.remove('active')});
727 el.classList.add('active');
728 root.querySelectorAll('.panel').forEach(function(pn){pn.classList.remove('active')});
729 var t=root.querySelector('.panel[data-p="'+p+'"]'); if(t)t.classList.add('active');
730 };
731 window.mssRc=function(){
732 var ti=root.querySelector('.seoT'), de=root.querySelector('.seoD'); if(!ti) return;
733 root.querySelector('.tC').textContent=ti.value.length;
734 root.querySelector('.dC').textContent=de.value.length;
735 root.querySelector('.gT').textContent=ti.value||ti.getAttribute('placeholder')||'Untitled';
736 root.querySelector('.gD').textContent=de.value||de.getAttribute('placeholder')||'No description set.';
737 };
738 window.mssTwT=function(sel){
739 var isApp=sel.value==='app', isPlayer=sel.value==='player';
740 var appSection=root.querySelector('.mssTwApp'), playerSection=root.querySelector('.mssTwPlayer');
741 if(appSection){
742 appSection.style.display=isApp?'block':'none';
743 appSection.querySelectorAll('input,select,textarea').forEach(function(field){field.disabled=!isApp;});
744 }
745 if(playerSection){
746 playerSection.style.display=isPlayer?'block':'none';
747 playerSection.querySelectorAll('input,select,textarea').forEach(function(field){field.disabled=!isPlayer;});
748 }
749 };
750 window.mssTheme=function(btn){
751 var dark=root.getAttribute('data-theme')==='dark';
752 root.setAttribute('data-theme',dark?'light':'dark');
753 btn.querySelector('.ti').textContent=dark?'\u{1F319}':'☀️';
754 btn.querySelector('.tl').textContent=dark?'Dark':'Light';
755 };
756 window.mssSpTab=function(btn,p){
757 btn.parentElement.querySelectorAll('button').forEach(function(b){b.classList.remove('on')});
758 btn.classList.add('on');
759 root.querySelectorAll('.spcard').forEach(function(c){c.classList.toggle('on', c.getAttribute('data-plat')===p)});
760 };
761 window.mssSocial=function(){
762 var seoT=root.querySelector('.seoT'), seoD=root.querySelector('.seoD');
763 var ogTitleEl=byName('_metasync_og_title');
764 var baseTitle=mssFld('_metasync_og_title') || (ogTitleEl&&ogTitleEl.getAttribute('placeholder')) || (seoT&&seoT.value.trim()) || 'Untitled';
765 var ogDescEl=byName('_metasync_og_description');
766 var baseDesc =mssFld('_metasync_og_description') || (ogDescEl&&ogDescEl.getAttribute('placeholder')) || (seoD&&seoD.value.trim()) || 'No description set.';
767 var baseImg =mssFld('_metasync_og_image');
768 var twTitle=mssFld('_metasync_twitter_title');
769 var twDesc =mssFld('_metasync_twitter_description');
770 var twImg =mssFld('_metasync_twitter_image');
771 root.querySelectorAll('.spcard').forEach(function(c){
772 var isTw=c.getAttribute('data-plat')==='twitter';
773 var T=isTw?(twTitle||baseTitle):baseTitle;
774 var D=isTw?(twDesc||baseDesc):baseDesc;
775 var I=isTw?(twImg||baseImg):baseImg;
776 var et=c.querySelector('.spT'); if(et)et.textContent=T;
777 var ed=c.querySelector('.spD'); if(ed)ed.textContent=D;
778 var eb=c.querySelector('.sp-img'); if(eb){ if(I){eb.style.backgroundImage='url("'+I.replace(/"/g,'')+'")';eb.classList.add('has');}else{eb.style.backgroundImage='';eb.classList.remove('has');} }
779 });
780 };
781 root.querySelectorAll('.mss-pick').forEach(function(btn){
782 btn.addEventListener('click',function(e){e.preventDefault();if(typeof wp==='undefined'||!wp.media)return;
783 var target=root.querySelector(btn.getAttribute('data-target'));
784 var frame=wp.media({title:'Select image',button:{text:'Use image'},multiple:false});
785 frame.on('select',function(){var img=frame.state().get('selection').first().toJSON();if(target){target.value=img.url;mssSocial();}});
786 frame.open();});
787 });
788 ['_metasync_og_title','_metasync_og_description','_metasync_og_image','_metasync_twitter_title','_metasync_twitter_description','_metasync_twitter_image'].forEach(function(n){
789 var e=byName(n); if(e) e.addEventListener('input',mssSocial);
790 });
791 var twitterCard=byName('_metasync_twitter_card'); if(twitterCard)mssTwT(twitterCard); mssRc(); mssSocial();
792 })();
793 </script>
794 <?php
795 }
796
797 private function styles()
798 {
799 static $done = false;
800 if ($done) {
801 return;
802 }
803 $done = true;
804 ?>
805 <style>
806 #mss-root[data-theme="light"]{--card:#fff;--card-2:#f1f3f5;--rail:#fbfbfd;--text:#1a1f26;--muted:#6b7280;--faint:#9ca3af;--border:#e5e7eb;--border-soft:#eef0f3;--accent:#667eea;--purple:#764ba2;--grad:linear-gradient(135deg,#667eea 0%,#764ba2 100%);--grad-hover:linear-gradient(135deg,#5568d3 0%,#653f8c 100%);--grad-soft:linear-gradient(135deg,rgba(102,126,234,.10) 0%,rgba(118,75,162,.06) 100%);--success:#10b981;--warning:#f59e0b;--error:#ef4444;--field-bg:#fff;--shadow:0 4px 24px rgba(17,24,39,.08);--shadow-sm:0 1px 3px rgba(17,24,39,.06)}
807 #mss-root[data-theme="dark"]{--card:#1a1f26;--card-2:#222831;--rail:#171c23;--text:#fff;--muted:#9ca3af;--faint:#8b93a3;--border:#374151;--border-soft:#283242;--accent:#8ea2f0;--purple:#764ba2;--grad:linear-gradient(135deg,#667eea 0%,#764ba2 100%);--grad-hover:linear-gradient(135deg,#5568d3 0%,#653f8c 100%);--grad-soft:linear-gradient(135deg,rgba(102,126,234,.16) 0%,rgba(118,75,162,.10) 100%);--success:#10b981;--warning:#f59e0b;--error:#ef4444;--field-bg:#11161d;--shadow:0 8px 30px rgba(0,0,0,.45);--shadow-sm:0 1px 3px rgba(0,0,0,.4)}
808 #mss-root,#mss-root *{box-sizing:border-box}
809 #mss-root{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Inter,Oxygen,Ubuntu,sans-serif;color:var(--text);font-size:13.5px;line-height:1.55;margin:-6px -12px -12px;background:var(--card);overflow:hidden}
810 #mss-root .mb-head{display:flex;align-items:center;gap:12px;padding:16px 20px;border-bottom:1px solid var(--border-soft)}
811 #mss-root .mb-head .badge{width:30px;height:30px;border-radius:9px;background:var(--grad);display:flex;align-items:center;justify-content:center;color:#fff;font-weight:800;font-size:15px}
812 #mss-root .mb-head .t b{font-size:14.5px;color:var(--text)}
813 #mss-root .mb-head .t span{display:block;color:var(--muted);font-size:11.5px}
814 #mss-root .mss-actions{margin-left:auto;display:flex;gap:8px}
815 #mss-root .theme-toggle{display:flex;align-items:center;gap:7px;background:var(--grad);border:0;color:#fff;border-radius:20px;padding:6px 13px;cursor:pointer;font-size:12px;font-weight:600}
816 #mss-root .mb-body{display:grid;grid-template-columns:212px 1fr;min-height:430px}
817 @media (max-width:900px){#mss-root .mb-body{grid-template-columns:1fr}}
818 #mss-root .rail{background:var(--rail);border-right:1px solid var(--border-soft);padding:12px 10px;display:flex;flex-direction:column;gap:3px}
819 @media (max-width:900px){#mss-root .rail{flex-direction:row;overflow-x:auto;border-right:0;border-bottom:1px solid var(--border-soft)}}
820 #mss-root .nav{display:flex;align-items:center;gap:11px;padding:10px 12px;border-radius:10px;cursor:pointer;color:var(--muted);font-weight:600;font-size:13px;border:1px solid transparent;white-space:nowrap;transition:.15s;position:relative}
821 #mss-root .nav:hover{background:var(--card-2);color:var(--text)}
822 #mss-root .nav.active{background:var(--grad-soft);color:var(--accent);border-color:rgba(102,126,234,.35)}
823 #mss-root .nav.active::before{content:"";position:absolute;left:0;top:8px;bottom:8px;width:3px;border-radius:3px;background:var(--grad)}
824 #mss-root .nav .ic{width:20px;height:20px;flex:none;display:flex;align-items:center;justify-content:center;font-size:14px}
825 #mss-root .panel-wrap{padding:24px 26px;position:relative}
826 #mss-root .panel{display:none;animation:mssfade .22s ease}
827 #mss-root .panel.active{display:block}
828 @keyframes mssfade{from{opacity:0;transform:translateY(4px)}to{opacity:1;transform:none}}
829 #mss-root .p-title{font-size:16px;font-weight:700;margin:0 0 3px;display:flex;align-items:center;gap:9px;color:var(--text)}
830 #mss-root .p-sub{color:var(--muted);font-size:12.5px;margin:0 0 20px}
831 #mss-root .sec{font-size:11px;font-weight:700;letter-spacing:.07em;text-transform:uppercase;color:var(--faint);margin:24px 0 13px;display:flex;align-items:center;gap:8px}
832 #mss-root .sec::after{content:"";flex:1;height:1px;background:var(--border-soft)}
833 #mss-root .sec-note{text-transform:none;letter-spacing:0;font-weight:500;color:var(--faint)}
834 #mss-root .subhead{font-size:14px;font-weight:700;color:var(--text);margin:24px 0 14px;display:flex;align-items:center;gap:9px;padding-bottom:9px;border-bottom:2px solid var(--border-soft)}
835 #mss-root .subhead .sq{width:9px;height:16px;border-radius:3px;background:var(--grad);flex:none}
836 #mss-root .field{margin:0 0 17px}
837 #mss-root .field:last-child{margin-bottom:0}
838 #mss-root .field .lbl{display:flex;justify-content:space-between;align-items:baseline;font-weight:600;margin-bottom:6px;font-size:12.5px;color:var(--text)}
839 #mss-root .field .lbl .cc{color:var(--faint);font-weight:600;font-size:11px}
840 #mss-root .ctrl{width:100%;padding:10px 12px;border:1.5px solid var(--border);border-radius:9px;font-size:13px;background:var(--field-bg);color:var(--text);transition:.15s;font-family:inherit;line-height:1.4;min-height:0;box-shadow:none}
841 #mss-root .ctrl:focus{outline:none;border-color:var(--accent);box-shadow:0 0 0 3px rgba(102,126,234,.16)}
842 #mss-root textarea.ctrl{min-height:78px;resize:vertical}
843 #mss-root input.short{max-width:140px}
844 #mss-root .mss-imgrow{display:flex;gap:9px}
845 #mss-root .hint{color:var(--faint);font-size:11.5px;margin-top:5px;line-height:1.5}
846 #mss-root .row{display:grid;grid-template-columns:1fr 1fr;gap:15px}
847 @media (max-width:560px){#mss-root .row{grid-template-columns:1fr}}
848 #mss-root .switch-card{display:flex;align-items:center;gap:13px;padding:13px 15px;background:var(--card-2);border:1px solid var(--border-soft);border-radius:11px;margin-bottom:18px}
849 #mss-root .switch{width:42px;height:24px;border-radius:24px;background:var(--border);position:relative;flex:none;cursor:pointer;transition:.18s;display:inline-block}
850 #mss-root .switch input{position:absolute;opacity:0;width:0;height:0}
851 #mss-root .switch::after{content:"";position:absolute;width:18px;height:18px;border-radius:50%;background:#fff;top:3px;left:3px;transition:.18s;box-shadow:0 1px 2px rgba(0,0,0,.3)}
852 #mss-root .switch:has(input:checked){background:var(--grad)}
853 #mss-root .switch:has(input:checked)::after{left:21px}
854 #mss-root .switch-card .m b{display:block;font-size:13px;color:var(--text)}
855 #mss-root .switch-card .m span{color:var(--muted);font-size:11.5px}
856 #mss-root .chips{display:flex;flex-wrap:wrap;gap:9px}
857 #mss-root .chip{display:flex;align-items:center;gap:8px;padding:9px 13px;border:1.5px solid var(--border);border-radius:10px;cursor:pointer;font-size:12.5px;font-weight:600;background:var(--field-bg);transition:.15s;user-select:none;color:var(--text)}
858 #mss-root .chip input{position:absolute;opacity:0;width:0;height:0}
859 #mss-root .chip:hover{border-color:var(--accent)}
860 #mss-root .chip:has(input:checked){background:var(--grad-soft);border-color:var(--accent);color:var(--accent)}
861 #mss-root .chip .box{width:15px;height:15px;border-radius:4px;border:1.5px solid var(--border);display:flex;align-items:center;justify-content:center;font-size:10px;color:transparent;flex:none}
862 #mss-root .chip:has(input:checked) .box{background:var(--accent);border-color:var(--accent);color:#fff}
863 #mss-root .chip small{display:block;font-weight:500;color:var(--muted);font-size:10.5px;margin-top:2px;max-width:160px;line-height:1.35}
864 #mss-root .chip:has(input:checked) small{color:var(--text)}
865 #mss-root .mss-advchip{display:inline-flex;margin-bottom:7px}
866 #mss-root .gprev{border:1px solid var(--border);border-radius:11px;padding:15px 17px;background:var(--field-bg)}
867 #mss-root .gprev .u{color:var(--muted);font-size:12px}
868 #mss-root .gprev .ti{color:#1a0dab;font-size:18px;margin:3px 0;font-weight:400}
869 #mss-root[data-theme="dark"] .gprev .ti{color:#8ab4f8}
870 #mss-root .gprev .de{color:var(--muted);font-size:12.5px}
871 #mss-root .btn{background:var(--grad);color:#fff;border:0;padding:9px 17px;border-radius:9px;cursor:pointer;font-size:13px;font-weight:600;box-shadow:var(--shadow-sm);transition:.15s;height:auto;line-height:1.3}
872 #mss-root .btn:hover{background:var(--grad-hover);color:#fff}
873 #mss-root .btn.ghost{background:transparent;color:var(--accent);border:1.5px solid rgba(102,126,234,.45);box-shadow:none}
874 #mss-root .btn.ghost:hover{background:var(--grad-soft)}
875 #mss-root .btn.sm{padding:6px 12px;font-size:12px}
876 #mss-root .mss-sptabs{display:flex;gap:2px;margin-bottom:13px;border-bottom:1px solid var(--border-soft)}
877 #mss-root .mss-sptabs button{background:none;border:0;padding:8px 13px;font-size:12.5px;font-weight:600;color:var(--muted);cursor:pointer;border-bottom:2px solid transparent;margin-bottom:-1px}
878 #mss-root .mss-sptabs button.on{color:var(--accent);border-bottom-color:var(--accent)}
879 #mss-root .spcard{display:none;border:1px solid var(--border);border-radius:11px;overflow:hidden;background:var(--field-bg);max-width:524px}
880 #mss-root .spcard.on{display:block}
881 #mss-root .sp-head{display:flex;align-items:center;gap:10px;padding:11px 13px}
882 #mss-root .sp-av{width:34px;height:34px;border-radius:50%;background:var(--grad);color:#fff;display:flex;align-items:center;justify-content:center;font-weight:800;font-size:14px;flex:none}
883 #mss-root .sp-meta b{font-size:13px;color:var(--text);display:block;line-height:1.3}
884 #mss-root .sp-meta span{font-size:11px;color:var(--faint)}
885 #mss-root .sp-img{height:210px;background:var(--card-2) center/cover no-repeat;display:flex;align-items:center;justify-content:center}
886 #mss-root .sp-img.has .sp-noimg{display:none}
887 #mss-root .sp-noimg{color:var(--faint);font-size:12px}
888 #mss-root .sp-body{padding:11px 14px;border-top:1px solid var(--border-soft);background:var(--card-2)}
889 #mss-root[data-theme="dark"] .sp-body{background:var(--card)}
890 #mss-root .sp-url{font-size:10.5px;text-transform:uppercase;color:var(--faint);letter-spacing:.03em}
891 #mss-root .sp-title{font-size:15px;font-weight:700;color:var(--text);margin:3px 0}
892 #mss-root .sp-desc{font-size:12.5px;color:var(--muted)}
893 #mss-root .spcard.tw .tw-card{margin:0 13px 14px;border:1px solid var(--border);border-radius:15px;overflow:hidden}
894 #mss-root .spcard.tw .sp-body{border-top:0;background:var(--field-bg);display:flex;flex-direction:column}
895 #mss-root .spcard.tw .sp-title{font-size:14px;margin:0 0 3px;order:1}
896 #mss-root .spcard.tw .sp-desc{order:2}
897 #mss-root .spcard.tw .sp-url{order:0;text-transform:none;margin-bottom:4px}
898 #mss-root .spcard.li .sp-title{font-size:14.5px}
899 #mss-root .mb-foot{display:flex;align-items:center;gap:14px;padding:13px 26px;border-top:1px solid var(--border-soft);background:var(--rail)}
900 #mss-root .mss-foot-hint{font-size:12px;color:var(--muted)}
901 #mss-root .mss-lockable{position:relative}
902 #mss-root .mss-lps-overlay{position:absolute;inset:0;display:none;align-items:center;justify-content:center;padding:26px;z-index:6;background:rgba(248,249,250,.62);backdrop-filter:blur(6px);-webkit-backdrop-filter:blur(6px)}
903 #mss-root[data-theme="dark"] .mss-lps-overlay{background:rgba(15,20,25,.66)}
904 #mss-root.lps .mss-lps-overlay{display:flex}
905 #mss-root .mss-lps-card{max-width:440px;text-align:center;background:var(--card);border:1px solid var(--border);border-radius:18px;box-shadow:var(--shadow);padding:32px 30px}
906 #mss-root .mss-lps-icon{width:58px;height:58px;margin:0 auto 16px;border-radius:16px;background:var(--grad);display:flex;align-items:center;justify-content:center;font-size:27px;box-shadow:var(--shadow-sm)}
907 #mss-root .mss-lps-card h3{margin:0 0 9px;font-size:18px;color:var(--text)}
908 #mss-root .mss-lps-card p{margin:0 0 20px;color:var(--muted);font-size:13px;line-height:1.65}
909 #mss-root .mss-lps-card p b{color:var(--text)}
910 #mss-root .mss-lps-card p code{background:var(--card-2);padding:1px 5px;border-radius:4px;font-size:11px}
911 #mss-root .mss-lps-hint{margin-top:15px;font-size:11.5px;color:var(--faint)}
912 #mss-root .mss-ro{width:100%;padding:10px 12px;border:1.5px dashed var(--border);border-radius:9px;background:var(--card-2);color:var(--text);font-size:13px;font-weight:600;display:flex;align-items:center;gap:8px}
913 #mss-root .ro-badge{font-weight:600;font-size:10.5px;color:var(--accent);background:var(--grad-soft);border:1px solid rgba(102,126,234,.30);border-radius:20px;padding:2px 9px}
914 #mss-root .mss-native input[type=text],#mss-root .mss-native input[type=url],#mss-root .mss-native input[type=number],#mss-root .mss-native textarea,#mss-root .mss-native select{border-radius:8px !important}
915 /* The builder's own `.schema-field input {width:100%}` also hits checkboxes and radios,
916 stretching them into a full-width block. Core styles these with
917 -webkit-appearance:none, so they have NO intrinsic size and depend entirely on an
918 explicit width/height — restate core's 1rem box rather than using width:auto, which
919 would collapse the control to nothing while unchecked (the checked state only stays
920 visible because core paints a ::before glyph over it). */
921 #mss-root .mss-native input[type=checkbox],
922 #mss-root .mss-native input[type=radio]{width:1rem !important;min-width:1rem !important;max-width:1rem !important;height:1rem !important;flex:0 0 auto;padding:0 !important;margin:0 7px 0 0;vertical-align:middle}
923 #mss-root .mss-native input[type=radio]{border-radius:50% !important}
924 #mss-root .mss-native .schema-field label:has(input[type=checkbox]),
925 #mss-root .mss-native .schema-field label:has(input[type=radio]){display:flex;align-items:center;gap:2px}
926 /* Dark mode: theme the embedded native Schema builder. Every surface the builder paints
927 light — whether from its stylesheet or an inline style — is listed here. Text is only
928 forced light on surfaces this block also darkens, so nothing ends up light-on-light. */
929 #mss-root[data-theme="dark"] .mss-native{color:var(--text)}
930 #mss-root[data-theme="dark"] .mss-native h1,#mss-root[data-theme="dark"] .mss-native h2,#mss-root[data-theme="dark"] .mss-native h3,#mss-root[data-theme="dark"] .mss-native h4,#mss-root[data-theme="dark"] .mss-native h5,#mss-root[data-theme="dark"] .mss-native label,#mss-root[data-theme="dark"] .mss-native strong,#mss-root[data-theme="dark"] .mss-native p{color:var(--text) !important}
931 #mss-root[data-theme="dark"] .mss-native .description,#mss-root[data-theme="dark"] .mss-native small{color:var(--muted) !important}
932 /* Light surfaces → dark. Class/ID list mirrors schema-markup-admin.css; the [style*=]
933 matchers catch the builder's inline backgrounds (#f5f5f5, #f9f9f9, #fff, white). */
934 #mss-root[data-theme="dark"] .mss-native .schema-types-container,
935 #mss-root[data-theme="dark"] .mss-native .schema-types-header,
936 #mss-root[data-theme="dark"] .mss-native .schema-type-item,
937 #mss-root[data-theme="dark"] .mss-native .schema-type-header,
938 #mss-root[data-theme="dark"] .mss-native .schema-type-content,
939 #mss-root[data-theme="dark"] .mss-native .schema-fields-container,
940 #mss-root[data-theme="dark"] .mss-native .schema-field,
941 #mss-root[data-theme="dark"] .mss-native .schema-field-group,
942 #mss-root[data-theme="dark"] .mss-native .schema-override-header,
943 #mss-root[data-theme="dark"] .mss-native .schema-override-content,
944 #mss-root[data-theme="dark"] .mss-native .no-schema-types,
945 #mss-root[data-theme="dark"] .mss-native .faq-item,
946 #mss-root[data-theme="dark"] .mss-native .instruction-item,
947 #mss-root[data-theme="dark"] .mss-native .logo-upload-container,
948 #mss-root[data-theme="dark"] .mss-native .logo-preview,
949 #mss-root[data-theme="dark"] .mss-native #schema-preview-output,
950 #mss-root[data-theme="dark"] .mss-native #schema-type-modal > div,
951 #mss-root[data-theme="dark"] .mss-native [style*="background: #fff"],
952 #mss-root[data-theme="dark"] .mss-native [style*="background:#fff"],
953 #mss-root[data-theme="dark"] .mss-native [style*="background: white"],
954 #mss-root[data-theme="dark"] .mss-native [style*="background:white"],
955 #mss-root[data-theme="dark"] .mss-native [style*="background: #f9f9f9"],
956 #mss-root[data-theme="dark"] .mss-native [style*="background:#f9f9f9"],
957 #mss-root[data-theme="dark"] .mss-native [style*="background: #f5f5f5"],
958 #mss-root[data-theme="dark"] .mss-native [style*="background:#f5f5f5"],
959 #mss-root[data-theme="dark"] .mss-native [style*="#ffffff"]{background-color:var(--card-2) !important;border-color:var(--border) !important}
960 /* Notices keep their semantic colour instead of being flattened to a grey surface. */
961 #mss-root[data-theme="dark"] .mss-native .metasync-schema-validation-warning,
962 #mss-root[data-theme="dark"] .mss-native [style*="background: #fff3cd"],
963 #mss-root[data-theme="dark"] .mss-native [style*="background:#fff3cd"]{background-color:rgba(245,158,11,.14) !important;border-color:rgba(245,158,11,.45) !important}
964 #mss-root[data-theme="dark"] .mss-native .metasync-schema-validation-warning,
965 #mss-root[data-theme="dark"] .mss-native .metasync-schema-validation-warning *,
966 #mss-root[data-theme="dark"] .mss-native [style*="#fff3cd"] *,
967 #mss-root[data-theme="dark"] .mss-native [style*="color: #856404"],
968 #mss-root[data-theme="dark"] .mss-native [style*="color:#856404"]{color:#fcd34d !important}
969 #mss-root[data-theme="dark"] .mss-native .metasync-schema-requirements-info,
970 #mss-root[data-theme="dark"] .mss-native [style*="background: #e7f3ff"],
971 #mss-root[data-theme="dark"] .mss-native [style*="background:#e7f3ff"]{background-color:rgba(102,126,234,.14) !important;border-color:rgba(102,126,234,.45) !important}
972 #mss-root[data-theme="dark"] .mss-native .metasync-schema-requirements-info,
973 #mss-root[data-theme="dark"] .mss-native .metasync-schema-requirements-info *{color:#c7d2fe !important}
974 /* Dark inline text on the surfaces darkened above → readable. */
975 #mss-root[data-theme="dark"] .mss-native [style*="color: #333"],
976 #mss-root[data-theme="dark"] .mss-native [style*="color:#333"],
977 #mss-root[data-theme="dark"] .mss-native [style*="color: #000"],
978 #mss-root[data-theme="dark"] .mss-native [style*="color:#000"],
979 #mss-root[data-theme="dark"] .mss-native [style*="color: #666"],
980 #mss-root[data-theme="dark"] .mss-native [style*="color:#666"]{color:var(--text) !important}
981 #mss-root[data-theme="dark"] .mss-native input[type=text],#mss-root[data-theme="dark"] .mss-native input[type=url],#mss-root[data-theme="dark"] .mss-native input[type=number],#mss-root[data-theme="dark"] .mss-native input[type=email],#mss-root[data-theme="dark"] .mss-native textarea,#mss-root[data-theme="dark"] .mss-native select{background:var(--field-bg) !important;color:var(--text) !important;border-color:var(--border) !important}
982 /* Checkboxes/radios: core paints them white-on-dark-border, which glares against this
983 box. Unchecked gets the field surface with a visible border; checked gets the brand
984 accent, which keeps enough contrast for core's white ::before tick. */
985 #mss-root[data-theme="dark"] .mss-native input[type=checkbox],
986 #mss-root[data-theme="dark"] .mss-native input[type=radio]{background:var(--field-bg) !important;border-color:#6b7280 !important}
987 #mss-root[data-theme="dark"] .mss-native input[type=checkbox]:checked,
988 #mss-root[data-theme="dark"] .mss-native input[type=radio]:checked{background:var(--accent) !important;border-color:var(--accent) !important}
989 /* Core's .button is painted light by wp-admin; keep it legible on the dark surface.
990 Destructive buttons (the builder paints them red) are left alone. */
991 #mss-root[data-theme="dark"] .mss-native .button,
992 #mss-root[data-theme="dark"] .mss-native .button-secondary{background:var(--card-2) !important;color:var(--accent) !important;border-color:rgba(102,126,234,.45) !important;text-shadow:none !important}
993 #mss-root[data-theme="dark"] .mss-native .button:hover,
994 #mss-root[data-theme="dark"] .mss-native .button-secondary:hover{background:var(--grad-soft) !important;color:var(--text) !important}
995 #mss-root[data-theme="dark"] .mss-native .button-primary{background:var(--grad) !important;color:#fff !important;border-color:transparent !important}
996 /* .remove-schema-type is a text-style .button (red text, no fill) — keep it red rather
997 than letting the .button rule above recolour it. .remove-faq-item / .remove-item are
998 solid-red fills with white text and are deliberately left untouched. */
999 #mss-root[data-theme="dark"] .mss-native .remove-schema-type,
1000 #mss-root[data-theme="dark"] .mss-native .remove-schema-type .dashicons{color:#f87171 !important;border-color:rgba(248,113,113,.45) !important}
1001 </style>
1002 <?php
1003 }
1004 }
1005