PluginProbe
Search Atlas SEO – OTTO AI SEO Automation for WordPress / 2.6.25
Search Atlas SEO – OTTO AI SEO Automation for WordPress v2.6.25
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.6.25, at includes/class-metasync-seo-suite.php

967 lines 65.8 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'])) {
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 return Metasync_OpenGraph::strip_auto_draft_title($value);
175 }
176 return (string) $value;
177 }
178
179 private static function checked_attr($cond)
180 {
181 return $cond ? ' checked' : '';
182 }
183
184 /** A clean, "pretty" permalink for placeholders/preview (avoids ?p=ID on drafts). */
185 private static function pretty_permalink($post)
186 {
187 $link = get_permalink($post->ID);
188 if ($link
189 && strpos($link, '?p=') === false
190 && strpos($link, '?page_id=') === false
191 && strpos($link, 'preview=') === false) {
192 return $link;
193 }
194 $slug = $post->post_name ? $post->post_name : sanitize_title(get_the_title($post->ID));
195 if (!$slug) {
196 $slug = 'your-post';
197 }
198 return user_trailingslashit(home_url('/' . $slug . '/'));
199 }
200
201 /**
202 * Render the unified box.
203 */
204 public function render($post)
205 {
206 $P = $this->present;
207 $id = $post->ID;
208 $a = 'esc_attr';
209
210 $managed = $this->is_managed_page($id);
211
212 $has_seo = isset($P['metasync-seo-meta']) || isset($P['metasync-seo-lps-notice']);
213 $has_crob = isset($P['common-robots-meta']);
214 $has_arob = isset($P['advance-robots-meta']);
215 $has_robots = $has_crob || $has_arob;
216 $has_canon = isset($P['post-canonical-meta']);
217 $has_redir = isset($P['post-redirection-meta']);
218 $has_social = isset($P['metasync_opengraph_meta_box']);
219 $has_schema = isset($P['metasync-schema-markup']);
220 $has_video = isset($P['metasync-video-sitemap-meta']);
221
222 // Current values.
223 $seo_title = self::v($id, '_metasync_seo_title');
224 $seo_desc = self::v($id, '_metasync_seo_desc');
225 $otto_kw = self::v($id, '_metasync_otto_keywords');
226
227 // The greyed-out value each field shows when left blank: OTTO's
228 // suggestion, or the imported value where OTTO is silent. Which tier
229 // that is — and whether OTTO counts at all, since the per-post "Disable
230 // OTTO" toggle makes its stored suggestion dead data — is
231 // Metasync_Seo_Precedence's call, not this screen's. Without a ghost the
232 // fields look completely empty after an import and the customer
233 // concludes it did nothing.
234 $title_fallback = Metasync_Seo_Precedence::fallback($id, Metasync_Seo_Precedence::FIELD_TITLE);
235 $desc_fallback = Metasync_Seo_Precedence::fallback($id, Metasync_Seo_Precedence::FIELD_DESCRIPTION);
236
237 $ghost_title = $title_fallback['value'];
238 $ghost_desc = $desc_fallback['value'];
239
240 $show_imported_note = $title_fallback['source'] === Metasync_Seo_Precedence::SOURCE_IMPORTED
241 || $desc_fallback['source'] === Metasync_Seo_Precedence::SOURCE_IMPORTED;
242
243 // Robots: post meta first, falling back to the site-wide defaults exactly as
244 // the standalone Common/Advance Robots boxes do (new option spelling first,
245 // then the legacy "mata" spelling).
246 $cr = get_post_meta($id, 'metasync_common_robots', true);
247 if (!$cr && class_exists('Metasync')) {
248 $cr = Metasync::get_option('common_robots_meta') ?? Metasync::get_option('common_robots_mata') ?? '';
249 }
250 if (!is_array($cr)) {
251 $cr = array();
252 }
253 $ar = get_post_meta($id, 'metasync_advance_robots', true);
254 if (!$ar && class_exists('Metasync')) {
255 $ar = Metasync::get_option('advance_robots_meta') ?? Metasync::get_option('advance_robots_mata') ?? '';
256 }
257 if (!is_array($ar)) {
258 $ar = array();
259 }
260 $ar_snip_e = isset($ar['max-snippet']['enable']) ? $ar['max-snippet']['enable'] : '';
261 $ar_snip_l = isset($ar['max-snippet']['length']) ? $ar['max-snippet']['length'] : '';
262 $ar_vid_e = isset($ar['max-video-preview']['enable']) ? $ar['max-video-preview']['enable'] : '';
263 $ar_vid_l = isset($ar['max-video-preview']['length']) ? $ar['max-video-preview']['length'] : '';
264 $ar_img_e = isset($ar['max-image-preview']['enable']) ? $ar['max-image-preview']['enable'] : '';
265 $ar_img_l = isset($ar['max-image-preview']['length']) ? $ar['max-image-preview']['length'] : '';
266
267 // Canonical: same sanitise-and-repair path as the standalone Canonical box.
268 // A bare reset() here would re-corrupt nested-array rows to the string "Array".
269 $raw_canonical = get_post_meta($id, 'meta_canonical', true);
270 if (class_exists('Metasync_Canonical_Sanitizer')) {
271 $canon = Metasync_Canonical_Sanitizer::sanitize($raw_canonical);
272 if (is_array($raw_canonical) || Metasync_Canonical_Sanitizer::is_corrupted($raw_canonical)) {
273 if ($canon !== '') {
274 update_post_meta($id, 'meta_canonical', $canon);
275 } else {
276 delete_post_meta($id, 'meta_canonical');
277 }
278 } elseif ($canon === '' && is_string($raw_canonical) && trim($raw_canonical) !== '') {
279 // Stored value the validator doesn't recognise: show it as-is so a
280 // routine post save doesn't silently wipe it.
281 $canon = trim($raw_canonical);
282 }
283 } else {
284 $canon = is_array($raw_canonical) ? '' : (string) $raw_canonical;
285 }
286 $pretty = self::pretty_permalink($post);
287
288 $rd = get_post_meta($id, 'metasync_post_redirection_meta', true);
289 if (!is_array($rd)) {
290 $rd = array();
291 }
292 $rd_en = (isset($rd['enable']) ? $rd['enable'] : '') === 'true';
293 $rd_type = isset($rd['type']) ? $rd['type'] : '301';
294 $rd_url = isset($rd['url']) ? $rd['url'] : '';
295
296 // The standalone Open Graph box defaults this setting to enabled when
297 // no value has been saved yet. Preserve that behaviour: only an
298 // explicit '0' represents an editor opting out.
299 $og_en_value = get_post_meta($id, '_metasync_og_enabled', true);
300 $og_en = ($og_en_value !== '0');
301 // Match the standalone Open Graph box exactly: its computed defaults are
302 // real form values (and are therefore persisted on save), not placeholders.
303 $og_defaults = array('title' => '', 'description' => '', 'image' => '');
304 $opengraph = class_exists('Metasync_OpenGraph') ? Metasync_OpenGraph::get_instance() : null;
305 if ($opengraph) {
306 $og_defaults = $opengraph->get_default_og_values($id);
307 }
308 $og_title = self::v($id, '_metasync_og_title') ?: $og_defaults['title'];
309 $og_desc = self::v($id, '_metasync_og_description') ?: $og_defaults['description'];
310 $og_image = self::v($id, '_metasync_og_image') ?: $og_defaults['image'];
311 $og_type = self::v($id, '_metasync_og_type') ?: 'article';
312 $og_url = self::v($id, '_metasync_og_url');
313 if ($og_url === '') {
314 $og_url = $opengraph ? $opengraph->get_canonical_url($post) : $pretty;
315 }
316 $tw_card = self::v($id, '_metasync_twitter_card') ?: 'summary_large_image';
317 $tw_site = self::v($id, '_metasync_twitter_site');
318 $tw_title = self::v($id, '_metasync_twitter_title') ?: $og_title;
319 $tw_desc = self::v($id, '_metasync_twitter_description') ?: $og_desc;
320 $tw_image = self::v($id, '_metasync_twitter_image') ?: $og_image;
321 $tw_alt = self::v($id, '_metasync_twitter_image_alt');
322 $tw_app_iphone = self::v($id, '_metasync_twitter_app_id_iphone');
323 $tw_app_ipad = self::v($id, '_metasync_twitter_app_id_ipad');
324 $tw_app_gp = self::v($id, '_metasync_twitter_app_id_googleplay');
325 $tw_app_url_iphone = self::v($id, '_metasync_twitter_app_url_iphone');
326 $tw_app_url_ipad = self::v($id, '_metasync_twitter_app_url_ipad');
327 $tw_app_url_gp = self::v($id, '_metasync_twitter_app_url_googleplay');
328 $tw_app_country = self::v($id, '_metasync_twitter_app_country');
329 $tw_player = self::v($id, '_metasync_twitter_player');
330 $tw_player_w = self::v($id, '_metasync_twitter_player_width');
331 $tw_player_h = self::v($id, '_metasync_twitter_player_height');
332 $tw_app_disabled = $tw_card === 'app' ? '' : ' disabled';
333 $tw_player_disabled = $tw_card === 'player' ? '' : ' disabled';
334
335 $v_url = self::v($id, '_metasync_video_url');
336 $v_thumb = self::v($id, '_metasync_video_thumbnail');
337 $v_title = self::v($id, '_metasync_video_title');
338 $v_desc = self::v($id, '_metasync_video_description');
339 $v_dur = self::v($id, '_metasync_video_duration');
340
341 $host = strtoupper((string) wp_parse_url(home_url(), PHP_URL_HOST));
342 $host_lc = strtolower($host);
343 $prev_url_disp = preg_replace('#^https?://#', '', (string) $pretty);
344 $site = get_bloginfo('name');
345 if ($site === '') {
346 $site = $host;
347 }
348 $avatar = strtoupper(function_exists('mb_substr') ? mb_substr($site, 0, 1) : substr($site, 0, 1));
349 $handle = sanitize_title($site) ?: 'site';
350
351 $this->styles();
352 ?>
353 <div id="mss-root" data-theme="light"<?php echo $managed ? ' class="lps"' : ''; ?>>
354 <?php
355 // On managed pages (LPS / custom-HTML) we emit NO nonces, so nothing here can
356 // overwrite the externally-managed SEO for those pages.
357 if (!$managed) {
358 if ($has_seo) { wp_nonce_field('metasync_seo_meta_nonce', 'metasync_seo_meta_nonce'); }
359 if ($has_crob) { wp_nonce_field('metasync_common_robots_nonce', 'metasync_common_robots_nonce'); }
360 if ($has_arob) { wp_nonce_field('metasync_advance_robots_nonce', 'metasync_advance_robots_nonce'); }
361 if ($has_canon) { wp_nonce_field('metasync_post_canonical_nonce', 'metasync_post_canonical_nonce'); }
362 if ($has_redir) { wp_nonce_field('metasync_post_redirection_nonce', 'metasync_post_redirection_nonce'); }
363 if ($has_social) { wp_nonce_field('metasync_opengraph_nonce', 'metasync_opengraph_nonce'); }
364 if ($has_video) { wp_nonce_field('metasync_video_sitemap_meta_nonce', 'metasync_video_sitemap_meta_nonce'); }
365 }
366 ?>
367 <div class="metabox">
368 <div class="mb-head">
369 <div class="badge">M</div>
370 <div class="t"><b>SEO Suite</b><span>On-page SEO for this post</span></div>
371 <div class="mss-actions">
372 <button type="button" class="theme-toggle" onclick="mssTheme(this)"><span class="ti">&#127769;</span> <span class="tl">Dark</span></button>
373 </div>
374 </div>
375
376 <div class="mss-lockable">
377 <div class="mb-body"<?php echo $managed ? ' inert' : ''; ?>>
378 <div class="rail">
379 <?php
380 $first = true;
381 $tab = function ($key, $icon, $label) use (&$first) {
382 echo '<div class="nav' . ($first ? ' active' : '') . '" role="button" tabindex="0" onclick="mssGo(this,\'' . esc_attr($key) . '\')" onkeydown="mssKey(event,this)"><span class="ic">' . $icon . '</span> ' . $label . '</div>';
383 $first = false;
384 };
385 if ($has_seo) { $tab('seo', '&#128269;', 'SEO'); }
386 if ($has_robots) { $tab('robots', '&#129302;', 'Robots'); }
387 if ($has_canon) { $tab('canonical', '&#128279;', 'Canonical'); }
388 if ($has_redir) { $tab('redirect', '&#8618;&#65039;', 'Redirection'); }
389 if ($has_social) { $tab('social', '&#128226;', 'Social &amp; OG'); }
390 if ($has_schema) { $tab('schema', '&#128208;', 'Schema'); }
391 if ($has_video) { $tab('video', '&#127916;', 'Video'); }
392 ?>
393 </div>
394
395 <div class="panel-wrap">
396 <?php
397 $pfirst = true;
398 $pcls = function () use (&$pfirst) {
399 $c = 'panel' . ($pfirst ? ' active' : '');
400 $pfirst = false;
401 return $c;
402 };
403 ?>
404
405 <?php if ($has_seo): ?>
406 <div class="<?php echo $pcls(); ?>" data-p="seo">
407 <h2 class="p-title">&#128269; Search Appearance</h2>
408 <p class="p-sub">Set the SEO Title and Meta Description. Leave blank to use the OTTO suggestion.</p>
409 <?php if ($show_imported_note): ?>
410 <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>
411 <?php endif; ?>
412 <div class="field">
413 <div class="lbl"><span>SEO Title</span><span class="cc"><span class="tC">0</span>/60</span></div>
414 <input class="ctrl seoT" name="metasync_seo_title" value="<?php echo $a($seo_title); ?>" placeholder="<?php echo $a($ghost_title); ?>" oninput="mssRc()">
415 </div>
416 <div class="field">
417 <div class="lbl"><span>Meta Description</span><span class="cc"><span class="dC">0</span>/160</span></div>
418 <textarea class="ctrl seoD" name="metasync_seo_desc" placeholder="<?php echo $a($ghost_desc); ?>" oninput="mssRc()"><?php echo esc_textarea($seo_desc); ?></textarea>
419 </div>
420 <?php if ($otto_kw !== ''): ?>
421 <div class="field">
422 <div class="lbl"><span>Focus Keyword</span><span class="ro-badge">Managed by OTTO</span></div>
423 <div class="mss-ro"><span class="lock">&#128274;</span> <?php echo esc_html($otto_kw); ?></div>
424 <div class="hint">Set in Search Atlas &mdash; shown here for reference. Read-only.</div>
425 </div>
426 <?php endif; ?>
427 <div class="sec">Preview</div>
428 <div class="gprev">
429 <div class="u"><?php echo esc_html($prev_url_disp); ?></div>
430 <div class="ti gT"><?php echo esc_html($seo_title ?: ($ghost_title ?: get_the_title($id))); ?></div>
431 <div class="de gD"><?php echo esc_html($seo_desc ?: ($ghost_desc ?: 'No description set.')); ?></div>
432 </div>
433 </div>
434 <?php endif; ?>
435
436 <?php if ($has_robots): ?>
437 <div class="<?php echo $pcls(); ?>" data-p="robots">
438 <h2 class="p-title">&#129302; Robots Meta</h2>
439 <p class="p-sub">Crawler directives for this post. These are independent flags &mdash; combine as needed.</p>
440 <?php if ($has_crob): ?>
441 <div class="sec">Common directives</div>
442 <div class="chips">
443 <?php
444 $common = array(
445 'index' => array('Index', 'Allow search engines to index this page'),
446 'noindex' => array('No Index', 'Keep this page out of results'),
447 'nofollow' => array('No Follow', "Don't follow links on this page"),
448 'noarchive' => array('No Archive', 'No cached copy'),
449 'noimageindex' => array('No Image Index', 'Exclude images from image search'),
450 'nosnippet' => array('No Snippet', 'No text snippet in results'),
451 );
452 foreach ($common as $key => $c): ?>
453 <label class="chip">
454 <input type="checkbox" name="common_robots_meta[<?php echo $a($key); ?>]" value="<?php echo $a($key); ?>"<?php echo isset($cr[$key]) ? self::checked_attr($cr[$key] === $key) : ''; ?>>
455 <span class="box">&#10003;</span><div><?php echo esc_html($c[0]); ?><small><?php echo esc_html($c[1]); ?></small></div>
456 </label>
457 <?php endforeach; ?>
458 </div>
459 <?php endif; ?>
460 <?php if ($has_arob): ?>
461 <div class="sec">Advanced limits <span class="sec-note">&mdash; tick a limit to apply it</span></div>
462 <div class="row">
463 <div class="field">
464 <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>
465 <input class="ctrl" type="number" name="advanced_robots_meta[max-snippet][length]" value="<?php echo $a($ar_snip_l); ?>" min="-1" placeholder="e.g. 160">
466 <div class="hint">Max snippet length in characters.</div></div>
467 <div class="field">
468 <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>
469 <input class="ctrl" type="number" name="advanced_robots_meta[max-video-preview][length]" value="<?php echo $a($ar_vid_l); ?>" min="-1" placeholder="e.g. -1">
470 <div class="hint">Max preview duration in seconds.</div></div>
471 </div>
472 <div class="field">
473 <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>
474 <select class="ctrl" name="advanced_robots_meta[max-image-preview][length]">
475 <option value="large"<?php selected($ar_img_l, 'large'); ?>>Large</option>
476 <option value="standard"<?php selected($ar_img_l, 'standard'); ?>>Standard</option>
477 <option value="none"<?php selected($ar_img_l, 'none'); ?>>None</option>
478 </select></div>
479 <?php endif; ?>
480 </div>
481 <?php endif; ?>
482
483 <?php if ($has_canon): ?>
484 <div class="<?php echo $pcls(); ?>" data-p="canonical">
485 <h2 class="p-title">&#128279; Canonical URL</h2>
486 <p class="p-sub">Point search engines to the preferred version of this content.</p>
487 <div class="field">
488 <div class="lbl"><span>Canonical URL</span></div>
489 <input class="ctrl" type="text" name="post_canonical_url_meta" value="<?php echo $a($canon); ?>" placeholder="<?php echo $a($pretty); ?>">
490 <div class="hint">Leave blank to use this post's own URL.</div>
491 </div>
492 </div>
493 <?php endif; ?>
494
495 <?php if ($has_redir): ?>
496 <div class="<?php echo $pcls(); ?>" data-p="redirect">
497 <h2 class="p-title">&#8618;&#65039; Redirection</h2>
498 <p class="p-sub">Send visitors of this post somewhere else.</p>
499 <div class="switch-card">
500 <label class="switch"><input type="checkbox" name="post_redirect_meta[enable]" value="true"<?php echo self::checked_attr($rd_en); ?>></label>
501 <div class="m"><b>Enable redirection</b><span>Send visitors of this post to another URL</span></div>
502 </div>
503 <div class="row">
504 <div class="field"><div class="lbl"><span>Type</span></div>
505 <select class="ctrl" name="post_redirect_meta[type]">
506 <option value="301"<?php selected($rd_type, '301'); ?>>301 Permanent Move</option>
507 <option value="302"<?php selected($rd_type, '302'); ?>>302 Temporary Move</option>
508 <option value="307"<?php selected($rd_type, '307'); ?>>307 Temporary Redirect</option>
509 <option value="410"<?php selected($rd_type, '410'); ?>>410 Content Deleted</option>
510 <option value="451"<?php selected($rd_type, '451'); ?>>451 Content Unavailable</option>
511 </select></div>
512 <div class="field"><div class="lbl"><span>Destination URL</span></div>
513 <input class="ctrl" type="text" name="post_redirect_meta[url]" value="<?php echo $a($rd_url); ?>" placeholder="https://example.com/new-url/"></div>
514 </div>
515 </div>
516 <?php endif; ?>
517
518 <?php if ($has_social): ?>
519 <div class="<?php echo $pcls(); ?>" data-p="social">
520 <h2 class="p-title">&#128226; Social &amp; Open Graph</h2>
521 <p class="p-sub">How this post looks when shared on social platforms.</p>
522 <div class="switch-card">
523 <label class="switch"><input type="checkbox" name="_metasync_og_enabled" value="1"<?php echo self::checked_attr($og_en); ?>></label>
524 <div class="m"><b>Enable Open Graph &amp; Social Media Tags</b><span>Adds Open Graph + Twitter Card tags for better sharing</span></div>
525 </div>
526
527 <div class="subhead"><span class="sq"></span> Open Graph</div>
528 <div class="field"><div class="lbl"><span>Title (og:title)</span></div><input class="ctrl" name="_metasync_og_title" maxlength="60" value="<?php echo $a($og_title); ?>" placeholder="<?php echo $a(get_the_title($id)); ?>" oninput="mssSocial()"></div>
529 <div class="field"><div class="lbl"><span>Description (og:description)</span></div><textarea class="ctrl" name="_metasync_og_description" maxlength="155" placeholder="Post excerpt&hellip;" oninput="mssSocial()"><?php echo esc_textarea($og_desc); ?></textarea></div>
530 <div class="field"><div class="lbl"><span>Image (og:image)</span></div>
531 <div class="mss-imgrow"><input class="ctrl ogimg" type="url" name="_metasync_og_image" value="<?php echo $a($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>
532 <div class="hint">Recommended 1200&times;630px.</div></div>
533 <div class="row">
534 <div class="field"><div class="lbl"><span>Type (og:type)</span></div>
535 <select class="ctrl" name="_metasync_og_type">
536 <?php foreach (array('article', 'website', 'blog', 'product', 'video', 'music') as $t): ?>
537 <option value="<?php echo $a($t); ?>"<?php selected($og_type, $t); ?>><?php echo esc_html(ucfirst($t)); ?></option>
538 <?php endforeach; ?>
539 </select></div>
540 <div class="field"><div class="lbl"><span>URL (og:url)</span></div><input class="ctrl" type="url" name="_metasync_og_url" value="<?php echo $a($og_url); ?>" placeholder="<?php echo $a($pretty); ?>"<?php echo $post->post_status === 'auto-draft' ? ' disabled' : ''; ?>></div>
541 </div>
542
543 <div class="subhead"><span class="sq"></span> Twitter Card</div>
544 <div class="field"><div class="lbl"><span>Card Type</span></div>
545 <select class="ctrl mssTw" name="_metasync_twitter_card" onchange="mssTwT(this)">
546 <option value="summary"<?php selected($tw_card, 'summary'); ?>>Summary</option>
547 <option value="summary_large_image"<?php selected($tw_card, 'summary_large_image'); ?>>Summary Large Image</option>
548 <option value="app"<?php selected($tw_card, 'app'); ?>>App</option>
549 <option value="player"<?php selected($tw_card, 'player'); ?>>Player</option>
550 </select></div>
551 <div class="row">
552 <div class="field"><div class="lbl"><span>Twitter Site</span></div><input class="ctrl" name="_metasync_twitter_site" value="<?php echo $a($tw_site); ?>" placeholder="@yoursite"></div>
553 <div class="field"><div class="lbl"><span>Twitter Title</span></div><input class="ctrl" name="_metasync_twitter_title" maxlength="70" value="<?php echo $a($tw_title); ?>" placeholder="Falls back to OG title" oninput="mssSocial()"></div>
554 </div>
555 <div class="field"><div class="lbl"><span>Twitter Description</span></div><textarea class="ctrl" name="_metasync_twitter_description" maxlength="200" placeholder="Falls back to OG description" oninput="mssSocial()"><?php echo esc_textarea($tw_desc); ?></textarea></div>
556 <div class="row">
557 <div class="field"><div class="lbl"><span>Twitter Image</span></div>
558 <div class="mss-imgrow"><input class="ctrl twimg" type="url" name="_metasync_twitter_image" value="<?php echo $a($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>
559 <div class="field"><div class="lbl"><span>Image Alt</span></div><input class="ctrl" name="_metasync_twitter_image_alt" maxlength="420" value="<?php echo $a($tw_alt); ?>" placeholder="Accessibility description"></div>
560 </div>
561
562 <div class="mssTwApp" style="display:<?php echo $tw_card === 'app' ? 'block' : 'none'; ?>">
563 <div class="subhead"><span class="sq"></span> App Card</div>
564 <div class="row">
565 <div class="field"><div class="lbl"><span>iPhone App ID</span></div><input class="ctrl" name="_metasync_twitter_app_id_iphone" value="<?php echo $a($tw_app_iphone); ?>" placeholder="307234931"<?php echo $tw_app_disabled; ?>></div>
566 <div class="field"><div class="lbl"><span>iPad App ID</span></div><input class="ctrl" name="_metasync_twitter_app_id_ipad" value="<?php echo $a($tw_app_ipad); ?>" placeholder="307234931"<?php echo $tw_app_disabled; ?>></div>
567 </div>
568 <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 $a($tw_app_gp); ?>" placeholder="com.android.app"<?php echo $tw_app_disabled; ?>></div>
569 <div class="row">
570 <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 $a($tw_app_url_iphone); ?>" placeholder="myapp://"<?php echo $tw_app_disabled; ?>></div>
571 <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 $a($tw_app_url_ipad); ?>" placeholder="myapp://"<?php echo $tw_app_disabled; ?>></div>
572 </div>
573 <div class="row">
574 <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 $a($tw_app_url_gp); ?>" placeholder="myapp://"<?php echo $tw_app_disabled; ?>></div>
575 <div class="field"><div class="lbl"><span>App Store Country</span></div>
576 <select class="ctrl" name="_metasync_twitter_app_country"<?php echo $tw_app_disabled; ?>>
577 <option value=""<?php selected($tw_app_country, ''); ?>>US (Default)</option>
578 <?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): ?>
579 <option value="<?php echo $a($code); ?>"<?php selected($tw_app_country, $code); ?>><?php echo esc_html($country); ?></option>
580 <?php endforeach; ?>
581 </select>
582 <div class="hint">Required if your app is not available in the US App Store.</div>
583 </div>
584 </div>
585 </div>
586 <div class="mssTwPlayer" style="display:<?php echo $tw_card === 'player' ? 'block' : 'none'; ?>">
587 <div class="subhead"><span class="sq"></span> Player Card</div>
588 <div class="field"><div class="lbl"><span>Player URL</span></div><input class="ctrl" type="url" name="_metasync_twitter_player" value="<?php echo $a($tw_player); ?>" placeholder="https://example.com/player"<?php echo $tw_player_disabled; ?>></div>
589 <div class="row">
590 <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 $a($tw_player_w); ?>" placeholder="1280"<?php echo $tw_player_disabled; ?>></div>
591 <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 $a($tw_player_h); ?>" placeholder="720"<?php echo $tw_player_disabled; ?>></div>
592 </div>
593 </div>
594
595 <div class="subhead"><span class="sq"></span> Social preview</div>
596 <div class="mss-sptabs">
597 <button type="button" class="on" onclick="mssSpTab(this,'facebook')">Facebook</button>
598 <button type="button" onclick="mssSpTab(this,'twitter')">Twitter / X</button>
599 <button type="button" onclick="mssSpTab(this,'linkedin')">LinkedIn</button>
600 </div>
601 <div class="spwrap">
602 <div class="spcard fb on" data-plat="facebook">
603 <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>
604 <div class="sp-img"><span class="sp-noimg">No image selected</span></div>
605 <div class="sp-body">
606 <div class="sp-url"><?php echo esc_html($host); ?></div>
607 <div class="sp-title spT"></div>
608 <div class="sp-desc spD"></div>
609 </div>
610 </div>
611 <div class="spcard tw" data-plat="twitter">
612 <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>
613 <div class="tw-card">
614 <div class="sp-img"><span class="sp-noimg">No image selected</span></div>
615 <div class="sp-body">
616 <div class="sp-title spT"></div>
617 <div class="sp-desc spD"></div>
618 <div class="sp-url"><?php echo esc_html($host_lc); ?></div>
619 </div>
620 </div>
621 </div>
622 <div class="spcard li" data-plat="linkedin">
623 <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>
624 <div class="sp-img"><span class="sp-noimg">No image selected</span></div>
625 <div class="sp-body">
626 <div class="sp-title spT"></div>
627 <div class="sp-url"><?php echo esc_html($host_lc); ?> &middot; 1 min read</div>
628 </div>
629 </div>
630 </div>
631 </div>
632 <?php endif; ?>
633
634 <?php if ($has_schema): ?>
635 <div class="<?php echo $pcls(); ?>" data-p="schema">
636 <h2 class="p-title">&#128208; Schema Markup</h2>
637 <div class="mss-native">
638 <?php
639 if (!empty($this->schema_box['callback'])) {
640 call_user_func($this->schema_box['callback'], $post, $this->schema_box);
641 } else {
642 echo '<p class="p-sub">Schema builder unavailable on this screen.</p>';
643 }
644 ?>
645 </div>
646 </div>
647 <?php endif; ?>
648
649 <?php if ($has_video): ?>
650 <div class="<?php echo $pcls(); ?>" data-p="video">
651 <h2 class="p-title">&#127916; Video Sitemap</h2>
652 <p class="p-sub">Override auto-detected video data. Leave blank to use auto-detection.</p>
653 <div class="field"><div class="lbl"><span>Video URL</span></div><input class="ctrl" type="url" name="metasync_video_url" value="<?php echo $a($v_url); ?>" placeholder="https://www.youtube.com/watch?v=..."></div>
654 <div class="field"><div class="lbl"><span>Thumbnail URL</span></div><input class="ctrl" type="url" name="metasync_video_thumbnail" value="<?php echo $a($v_thumb); ?>" placeholder="https://img.youtube.com/vi/.../hqdefault.jpg"></div>
655 <div class="field"><div class="lbl"><span>Video Title</span></div><input class="ctrl" name="metasync_video_title" value="<?php echo $a($v_title); ?>" placeholder="Defaults to post title"></div>
656 <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>
657 <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 $a($v_dur); ?>" placeholder="300"></div>
658 </div>
659 <?php endif; ?>
660
661 </div>
662 </div>
663
664 <div class="mb-foot">
665 <span class="mss-foot-hint">Changes are saved when you update the post.</span>
666 </div>
667
668 <?php if ($managed): ?>
669 <div class="mss-lps-overlay">
670 <div class="mss-lps-card">
671 <div class="mss-lps-icon">&#128274;</div>
672 <h3>SEO managed by WebStudio</h3>
673 <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>
674 <div class="mss-lps-hint">Editing is disabled here to avoid conflicting values.</div>
675 </div>
676 </div>
677 <?php endif; ?>
678 </div>
679 </div>
680 </div>
681
682 <script>
683 (function(){
684 var root=document.getElementById('mss-root'); if(!root) return;
685 function byName(n){return root.querySelector('[name="'+n+'"]');}
686 function mssFld(n){var e=byName(n);return e?e.value.trim():'';}
687 window.mssKey=function(e,el){ if(e.key==='Enter'||e.key===' '){ e.preventDefault(); el.click(); } };
688 window.mssGo=function(el,p){
689 el.parentElement.querySelectorAll('.nav').forEach(function(n){n.classList.remove('active')});
690 el.classList.add('active');
691 root.querySelectorAll('.panel').forEach(function(pn){pn.classList.remove('active')});
692 var t=root.querySelector('.panel[data-p="'+p+'"]'); if(t)t.classList.add('active');
693 };
694 window.mssRc=function(){
695 var ti=root.querySelector('.seoT'), de=root.querySelector('.seoD'); if(!ti) return;
696 root.querySelector('.tC').textContent=ti.value.length;
697 root.querySelector('.dC').textContent=de.value.length;
698 root.querySelector('.gT').textContent=ti.value||ti.getAttribute('placeholder')||'Untitled';
699 root.querySelector('.gD').textContent=de.value||de.getAttribute('placeholder')||'No description set.';
700 };
701 window.mssTwT=function(sel){
702 var isApp=sel.value==='app', isPlayer=sel.value==='player';
703 var appSection=root.querySelector('.mssTwApp'), playerSection=root.querySelector('.mssTwPlayer');
704 if(appSection){
705 appSection.style.display=isApp?'block':'none';
706 appSection.querySelectorAll('input,select,textarea').forEach(function(field){field.disabled=!isApp;});
707 }
708 if(playerSection){
709 playerSection.style.display=isPlayer?'block':'none';
710 playerSection.querySelectorAll('input,select,textarea').forEach(function(field){field.disabled=!isPlayer;});
711 }
712 };
713 window.mssTheme=function(btn){
714 var dark=root.getAttribute('data-theme')==='dark';
715 root.setAttribute('data-theme',dark?'light':'dark');
716 btn.querySelector('.ti').textContent=dark?'\u{1F319}':'☀️';
717 btn.querySelector('.tl').textContent=dark?'Dark':'Light';
718 };
719 window.mssSpTab=function(btn,p){
720 btn.parentElement.querySelectorAll('button').forEach(function(b){b.classList.remove('on')});
721 btn.classList.add('on');
722 root.querySelectorAll('.spcard').forEach(function(c){c.classList.toggle('on', c.getAttribute('data-plat')===p)});
723 };
724 window.mssSocial=function(){
725 var seoT=root.querySelector('.seoT'), seoD=root.querySelector('.seoD');
726 var ogTitleEl=byName('_metasync_og_title');
727 var baseTitle=mssFld('_metasync_og_title') || (ogTitleEl&&ogTitleEl.getAttribute('placeholder')) || (seoT&&seoT.value.trim()) || 'Untitled';
728 var baseDesc =mssFld('_metasync_og_description') || (seoD&&seoD.value.trim()) || 'No description set.';
729 var baseImg =mssFld('_metasync_og_image');
730 var twTitle=mssFld('_metasync_twitter_title');
731 var twDesc =mssFld('_metasync_twitter_description');
732 var twImg =mssFld('_metasync_twitter_image');
733 root.querySelectorAll('.spcard').forEach(function(c){
734 var isTw=c.getAttribute('data-plat')==='twitter';
735 var T=isTw?(twTitle||baseTitle):baseTitle;
736 var D=isTw?(twDesc||baseDesc):baseDesc;
737 var I=isTw?(twImg||baseImg):baseImg;
738 var et=c.querySelector('.spT'); if(et)et.textContent=T;
739 var ed=c.querySelector('.spD'); if(ed)ed.textContent=D;
740 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');} }
741 });
742 };
743 root.querySelectorAll('.mss-pick').forEach(function(btn){
744 btn.addEventListener('click',function(e){e.preventDefault();if(typeof wp==='undefined'||!wp.media)return;
745 var target=root.querySelector(btn.getAttribute('data-target'));
746 var frame=wp.media({title:'Select image',button:{text:'Use image'},multiple:false});
747 frame.on('select',function(){var img=frame.state().get('selection').first().toJSON();if(target){target.value=img.url;mssSocial();}});
748 frame.open();});
749 });
750 ['_metasync_og_title','_metasync_og_description','_metasync_og_image','_metasync_twitter_title','_metasync_twitter_description','_metasync_twitter_image'].forEach(function(n){
751 var e=byName(n); if(e) e.addEventListener('input',mssSocial);
752 });
753 var twitterCard=byName('_metasync_twitter_card'); if(twitterCard)mssTwT(twitterCard); mssRc(); mssSocial();
754 })();
755 </script>
756 <?php
757 }
758
759 private function styles()
760 {
761 static $done = false;
762 if ($done) {
763 return;
764 }
765 $done = true;
766 ?>
767 <style>
768 #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)}
769 #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)}
770 #mss-root,#mss-root *{box-sizing:border-box}
771 #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}
772 #mss-root .mb-head{display:flex;align-items:center;gap:12px;padding:16px 20px;border-bottom:1px solid var(--border-soft)}
773 #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}
774 #mss-root .mb-head .t b{font-size:14.5px;color:var(--text)}
775 #mss-root .mb-head .t span{display:block;color:var(--muted);font-size:11.5px}
776 #mss-root .mss-actions{margin-left:auto;display:flex;gap:8px}
777 #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}
778 #mss-root .mb-body{display:grid;grid-template-columns:212px 1fr;min-height:430px}
779 @media (max-width:900px){#mss-root .mb-body{grid-template-columns:1fr}}
780 #mss-root .rail{background:var(--rail);border-right:1px solid var(--border-soft);padding:12px 10px;display:flex;flex-direction:column;gap:3px}
781 @media (max-width:900px){#mss-root .rail{flex-direction:row;overflow-x:auto;border-right:0;border-bottom:1px solid var(--border-soft)}}
782 #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}
783 #mss-root .nav:hover{background:var(--card-2);color:var(--text)}
784 #mss-root .nav.active{background:var(--grad-soft);color:var(--accent);border-color:rgba(102,126,234,.35)}
785 #mss-root .nav.active::before{content:"";position:absolute;left:0;top:8px;bottom:8px;width:3px;border-radius:3px;background:var(--grad)}
786 #mss-root .nav .ic{width:20px;height:20px;flex:none;display:flex;align-items:center;justify-content:center;font-size:14px}
787 #mss-root .panel-wrap{padding:24px 26px;position:relative}
788 #mss-root .panel{display:none;animation:mssfade .22s ease}
789 #mss-root .panel.active{display:block}
790 @keyframes mssfade{from{opacity:0;transform:translateY(4px)}to{opacity:1;transform:none}}
791 #mss-root .p-title{font-size:16px;font-weight:700;margin:0 0 3px;display:flex;align-items:center;gap:9px;color:var(--text)}
792 #mss-root .p-sub{color:var(--muted);font-size:12.5px;margin:0 0 20px}
793 #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}
794 #mss-root .sec::after{content:"";flex:1;height:1px;background:var(--border-soft)}
795 #mss-root .sec-note{text-transform:none;letter-spacing:0;font-weight:500;color:var(--faint)}
796 #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)}
797 #mss-root .subhead .sq{width:9px;height:16px;border-radius:3px;background:var(--grad);flex:none}
798 #mss-root .field{margin:0 0 17px}
799 #mss-root .field:last-child{margin-bottom:0}
800 #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)}
801 #mss-root .field .lbl .cc{color:var(--faint);font-weight:600;font-size:11px}
802 #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}
803 #mss-root .ctrl:focus{outline:none;border-color:var(--accent);box-shadow:0 0 0 3px rgba(102,126,234,.16)}
804 #mss-root textarea.ctrl{min-height:78px;resize:vertical}
805 #mss-root input.short{max-width:140px}
806 #mss-root .mss-imgrow{display:flex;gap:9px}
807 #mss-root .hint{color:var(--faint);font-size:11.5px;margin-top:5px;line-height:1.5}
808 #mss-root .row{display:grid;grid-template-columns:1fr 1fr;gap:15px}
809 @media (max-width:560px){#mss-root .row{grid-template-columns:1fr}}
810 #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}
811 #mss-root .switch{width:42px;height:24px;border-radius:24px;background:var(--border);position:relative;flex:none;cursor:pointer;transition:.18s;display:inline-block}
812 #mss-root .switch input{position:absolute;opacity:0;width:0;height:0}
813 #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)}
814 #mss-root .switch:has(input:checked){background:var(--grad)}
815 #mss-root .switch:has(input:checked)::after{left:21px}
816 #mss-root .switch-card .m b{display:block;font-size:13px;color:var(--text)}
817 #mss-root .switch-card .m span{color:var(--muted);font-size:11.5px}
818 #mss-root .chips{display:flex;flex-wrap:wrap;gap:9px}
819 #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)}
820 #mss-root .chip input{position:absolute;opacity:0;width:0;height:0}
821 #mss-root .chip:hover{border-color:var(--accent)}
822 #mss-root .chip:has(input:checked){background:var(--grad-soft);border-color:var(--accent);color:var(--accent)}
823 #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}
824 #mss-root .chip:has(input:checked) .box{background:var(--accent);border-color:var(--accent);color:#fff}
825 #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}
826 #mss-root .chip:has(input:checked) small{color:var(--text)}
827 #mss-root .mss-advchip{display:inline-flex;margin-bottom:7px}
828 #mss-root .gprev{border:1px solid var(--border);border-radius:11px;padding:15px 17px;background:var(--field-bg)}
829 #mss-root .gprev .u{color:var(--muted);font-size:12px}
830 #mss-root .gprev .ti{color:#1a0dab;font-size:18px;margin:3px 0;font-weight:400}
831 #mss-root[data-theme="dark"] .gprev .ti{color:#8ab4f8}
832 #mss-root .gprev .de{color:var(--muted);font-size:12.5px}
833 #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}
834 #mss-root .btn:hover{background:var(--grad-hover);color:#fff}
835 #mss-root .btn.ghost{background:transparent;color:var(--accent);border:1.5px solid rgba(102,126,234,.45);box-shadow:none}
836 #mss-root .btn.ghost:hover{background:var(--grad-soft)}
837 #mss-root .btn.sm{padding:6px 12px;font-size:12px}
838 #mss-root .mss-sptabs{display:flex;gap:2px;margin-bottom:13px;border-bottom:1px solid var(--border-soft)}
839 #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}
840 #mss-root .mss-sptabs button.on{color:var(--accent);border-bottom-color:var(--accent)}
841 #mss-root .spcard{display:none;border:1px solid var(--border);border-radius:11px;overflow:hidden;background:var(--field-bg);max-width:524px}
842 #mss-root .spcard.on{display:block}
843 #mss-root .sp-head{display:flex;align-items:center;gap:10px;padding:11px 13px}
844 #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}
845 #mss-root .sp-meta b{font-size:13px;color:var(--text);display:block;line-height:1.3}
846 #mss-root .sp-meta span{font-size:11px;color:var(--faint)}
847 #mss-root .sp-img{height:210px;background:var(--card-2) center/cover no-repeat;display:flex;align-items:center;justify-content:center}
848 #mss-root .sp-img.has .sp-noimg{display:none}
849 #mss-root .sp-noimg{color:var(--faint);font-size:12px}
850 #mss-root .sp-body{padding:11px 14px;border-top:1px solid var(--border-soft);background:var(--card-2)}
851 #mss-root[data-theme="dark"] .sp-body{background:var(--card)}
852 #mss-root .sp-url{font-size:10.5px;text-transform:uppercase;color:var(--faint);letter-spacing:.03em}
853 #mss-root .sp-title{font-size:15px;font-weight:700;color:var(--text);margin:3px 0}
854 #mss-root .sp-desc{font-size:12.5px;color:var(--muted)}
855 #mss-root .spcard.tw .tw-card{margin:0 13px 14px;border:1px solid var(--border);border-radius:15px;overflow:hidden}
856 #mss-root .spcard.tw .sp-body{border-top:0;background:var(--field-bg);display:flex;flex-direction:column}
857 #mss-root .spcard.tw .sp-title{font-size:14px;margin:0 0 3px;order:1}
858 #mss-root .spcard.tw .sp-desc{order:2}
859 #mss-root .spcard.tw .sp-url{order:0;text-transform:none;margin-bottom:4px}
860 #mss-root .spcard.li .sp-title{font-size:14.5px}
861 #mss-root .mb-foot{display:flex;align-items:center;gap:14px;padding:13px 26px;border-top:1px solid var(--border-soft);background:var(--rail)}
862 #mss-root .mss-foot-hint{font-size:12px;color:var(--muted)}
863 #mss-root .mss-lockable{position:relative}
864 #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)}
865 #mss-root[data-theme="dark"] .mss-lps-overlay{background:rgba(15,20,25,.66)}
866 #mss-root.lps .mss-lps-overlay{display:flex}
867 #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}
868 #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)}
869 #mss-root .mss-lps-card h3{margin:0 0 9px;font-size:18px;color:var(--text)}
870 #mss-root .mss-lps-card p{margin:0 0 20px;color:var(--muted);font-size:13px;line-height:1.65}
871 #mss-root .mss-lps-card p b{color:var(--text)}
872 #mss-root .mss-lps-card p code{background:var(--card-2);padding:1px 5px;border-radius:4px;font-size:11px}
873 #mss-root .mss-lps-hint{margin-top:15px;font-size:11.5px;color:var(--faint)}
874 #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}
875 #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}
876 #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}
877 /* The builder's own `.schema-field input {width:100%}` also hits checkboxes and radios,
878 stretching them into a full-width block. Core styles these with
879 -webkit-appearance:none, so they have NO intrinsic size and depend entirely on an
880 explicit width/height — restate core's 1rem box rather than using width:auto, which
881 would collapse the control to nothing while unchecked (the checked state only stays
882 visible because core paints a ::before glyph over it). */
883 #mss-root .mss-native input[type=checkbox],
884 #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}
885 #mss-root .mss-native input[type=radio]{border-radius:50% !important}
886 #mss-root .mss-native .schema-field label:has(input[type=checkbox]),
887 #mss-root .mss-native .schema-field label:has(input[type=radio]){display:flex;align-items:center;gap:2px}
888 /* Dark mode: theme the embedded native Schema builder. Every surface the builder paints
889 light — whether from its stylesheet or an inline style — is listed here. Text is only
890 forced light on surfaces this block also darkens, so nothing ends up light-on-light. */
891 #mss-root[data-theme="dark"] .mss-native{color:var(--text)}
892 #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}
893 #mss-root[data-theme="dark"] .mss-native .description,#mss-root[data-theme="dark"] .mss-native small{color:var(--muted) !important}
894 /* Light surfaces → dark. Class/ID list mirrors schema-markup-admin.css; the [style*=]
895 matchers catch the builder's inline backgrounds (#f5f5f5, #f9f9f9, #fff, white). */
896 #mss-root[data-theme="dark"] .mss-native .schema-types-container,
897 #mss-root[data-theme="dark"] .mss-native .schema-types-header,
898 #mss-root[data-theme="dark"] .mss-native .schema-type-item,
899 #mss-root[data-theme="dark"] .mss-native .schema-type-header,
900 #mss-root[data-theme="dark"] .mss-native .schema-type-content,
901 #mss-root[data-theme="dark"] .mss-native .schema-fields-container,
902 #mss-root[data-theme="dark"] .mss-native .schema-field,
903 #mss-root[data-theme="dark"] .mss-native .schema-field-group,
904 #mss-root[data-theme="dark"] .mss-native .schema-override-header,
905 #mss-root[data-theme="dark"] .mss-native .schema-override-content,
906 #mss-root[data-theme="dark"] .mss-native .no-schema-types,
907 #mss-root[data-theme="dark"] .mss-native .faq-item,
908 #mss-root[data-theme="dark"] .mss-native .instruction-item,
909 #mss-root[data-theme="dark"] .mss-native .logo-upload-container,
910 #mss-root[data-theme="dark"] .mss-native .logo-preview,
911 #mss-root[data-theme="dark"] .mss-native #schema-preview-output,
912 #mss-root[data-theme="dark"] .mss-native #schema-type-modal > div,
913 #mss-root[data-theme="dark"] .mss-native [style*="background: #fff"],
914 #mss-root[data-theme="dark"] .mss-native [style*="background:#fff"],
915 #mss-root[data-theme="dark"] .mss-native [style*="background: white"],
916 #mss-root[data-theme="dark"] .mss-native [style*="background:white"],
917 #mss-root[data-theme="dark"] .mss-native [style*="background: #f9f9f9"],
918 #mss-root[data-theme="dark"] .mss-native [style*="background:#f9f9f9"],
919 #mss-root[data-theme="dark"] .mss-native [style*="background: #f5f5f5"],
920 #mss-root[data-theme="dark"] .mss-native [style*="background:#f5f5f5"],
921 #mss-root[data-theme="dark"] .mss-native [style*="#ffffff"]{background-color:var(--card-2) !important;border-color:var(--border) !important}
922 /* Notices keep their semantic colour instead of being flattened to a grey surface. */
923 #mss-root[data-theme="dark"] .mss-native .metasync-schema-validation-warning,
924 #mss-root[data-theme="dark"] .mss-native [style*="background: #fff3cd"],
925 #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}
926 #mss-root[data-theme="dark"] .mss-native .metasync-schema-validation-warning,
927 #mss-root[data-theme="dark"] .mss-native .metasync-schema-validation-warning *,
928 #mss-root[data-theme="dark"] .mss-native [style*="#fff3cd"] *,
929 #mss-root[data-theme="dark"] .mss-native [style*="color: #856404"],
930 #mss-root[data-theme="dark"] .mss-native [style*="color:#856404"]{color:#fcd34d !important}
931 #mss-root[data-theme="dark"] .mss-native .metasync-schema-requirements-info,
932 #mss-root[data-theme="dark"] .mss-native [style*="background: #e7f3ff"],
933 #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}
934 #mss-root[data-theme="dark"] .mss-native .metasync-schema-requirements-info,
935 #mss-root[data-theme="dark"] .mss-native .metasync-schema-requirements-info *{color:#c7d2fe !important}
936 /* Dark inline text on the surfaces darkened above → readable. */
937 #mss-root[data-theme="dark"] .mss-native [style*="color: #333"],
938 #mss-root[data-theme="dark"] .mss-native [style*="color:#333"],
939 #mss-root[data-theme="dark"] .mss-native [style*="color: #000"],
940 #mss-root[data-theme="dark"] .mss-native [style*="color:#000"],
941 #mss-root[data-theme="dark"] .mss-native [style*="color: #666"],
942 #mss-root[data-theme="dark"] .mss-native [style*="color:#666"]{color:var(--text) !important}
943 #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}
944 /* Checkboxes/radios: core paints them white-on-dark-border, which glares against this
945 box. Unchecked gets the field surface with a visible border; checked gets the brand
946 accent, which keeps enough contrast for core's white ::before tick. */
947 #mss-root[data-theme="dark"] .mss-native input[type=checkbox],
948 #mss-root[data-theme="dark"] .mss-native input[type=radio]{background:var(--field-bg) !important;border-color:#6b7280 !important}
949 #mss-root[data-theme="dark"] .mss-native input[type=checkbox]:checked,
950 #mss-root[data-theme="dark"] .mss-native input[type=radio]:checked{background:var(--accent) !important;border-color:var(--accent) !important}
951 /* Core's .button is painted light by wp-admin; keep it legible on the dark surface.
952 Destructive buttons (the builder paints them red) are left alone. */
953 #mss-root[data-theme="dark"] .mss-native .button,
954 #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}
955 #mss-root[data-theme="dark"] .mss-native .button:hover,
956 #mss-root[data-theme="dark"] .mss-native .button-secondary:hover{background:var(--grad-soft) !important;color:var(--text) !important}
957 #mss-root[data-theme="dark"] .mss-native .button-primary{background:var(--grad) !important;color:#fff !important;border-color:transparent !important}
958 /* .remove-schema-type is a text-style .button (red text, no fill) — keep it red rather
959 than letting the .button rule above recolour it. .remove-faq-item / .remove-item are
960 solid-red fills with white text and are deliberately left untouched. */
961 #mss-root[data-theme="dark"] .mss-native .remove-schema-type,
962 #mss-root[data-theme="dark"] .mss-native .remove-schema-type .dashicons{color:#f87171 !important;border-color:rgba(248,113,113,.45) !important}
963 </style>
964 <?php
965 }
966 }
967