PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 1.0.0
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v1.0.0
2.7.0 2.6.0 2.5.0 2.4.0 2.3.0 2.2.0 2.1.1 2.1.0 2.0.2 2.0.1 2.0.0 1.32.0 1.31.0 1.30.0 1.29.0 1.28.0 1.27.0 1.26.0 1.25.0 trunk 1.0.0 1.0.1 1.0.2 1.1.0 1.10.0 All 48 releases
thinkrank / src / metabox / components / MetaboxSchemaTab.js

MetaboxSchemaTab.js in ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO 1.0.0, at src/metabox/components/MetaboxSchemaTab.js

775 lines 31.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * ThinkRank Metabox Schema Tab Component
3 *
4 * Handles schema markup generation and deployment for individual posts/pages.
5 * Reuses existing schema API endpoints and follows the deployment tab patterns.
6 *
7 * @package ThinkRank
8 * @since 1.0.0
9 */
10
11 import { useState, useEffect } from '@wordpress/element';
12 import { __ } from '@wordpress/i18n';
13 import apiFetch from '@wordpress/api-fetch';
14 import { calculateWordCount } from '../../admin/components/seo/utils/contentAnalysis';
15
16 // Dynamic schema form imports - only load what's needed
17 const getSchemaFormComponent = (schemaType) => {
18 switch (schemaType) {
19 case 'HowTo':
20 return require('../../admin/components/essential-seo/schema/forms/SchemaHowToForm').default;
21 case 'FAQ':
22 return require('../../admin/components/essential-seo/schema/forms/SchemaFAQForm').default;
23 case 'Event':
24 return require('../../admin/components/essential-seo/schema/forms/SchemaEventForm').default;
25 case 'SoftwareApplication':
26 return require('../../admin/components/essential-seo/schema/forms/SchemaSoftwareForm').default;
27 case 'Product':
28 return require('../../admin/components/essential-seo/schema/forms/SchemaProductForm').default;
29 default:
30 return null;
31 }
32 };
33
34 /**
35 * Schema Tab Component
36 */
37 const MetaboxSchemaTab = ({
38 postId, // number - WordPress post ID
39 postTitle, // string - WordPress post title
40 contentPreview, // string - Post content for schema generation
41 formData, // object - Metabox form data (contains AI-optimized content)
42 showNotice // function - Notice display callback
43 }) => {
44 // Schema state
45 const [selectedSchemaType, setSelectedSchemaType] = useState('Article');
46 const [generatedSchemas, setGeneratedSchemas] = useState(null);
47 const [deployedSchemas, setDeployedSchemas] = useState(null);
48 const [isGenerating, setIsGenerating] = useState(false);
49 const [isDeploying, setIsDeploying] = useState(false);
50 const [isLoading, setIsLoading] = useState(true);
51 const [isRefreshing, setIsRefreshing] = useState(false);
52
53 // Schema form data state - single schema type per post
54 const [schemaFormData, setSchemaFormData] = useState({});
55
56 // Auto-save backup for schema switching
57 const [schemaBackups, setSchemaBackups] = useState({});
58
59 // Get current post type from localized data
60 const currentPostType = window.thinkrankMetabox?.postType || 'post';
61
62 // Schema types for posts (content-focused)
63 const postSchemaTypes = [
64 { label: __('Article', 'thinkrank'), value: 'Article' },
65 { label: __('Blog Posting', 'thinkrank'), value: 'BlogPosting' },
66 { label: __('Technical Article', 'thinkrank'), value: 'TechnicalArticle' },
67 { label: __('News Article', 'thinkrank'), value: 'NewsArticle' },
68 { label: __('Scholarly Article', 'thinkrank'), value: 'ScholarlyArticle' },
69 { label: __('Report', 'thinkrank'), value: 'Report' },
70 { label: __('How-To Guide', 'thinkrank'), value: 'HowTo' },
71 { label: __('FAQ Page', 'thinkrank'), value: 'FAQPage' }
72 ];
73
74 // Schema types for pages (specific subset)
75 const pageSchemaTypes = [
76 { label: __('Article', 'thinkrank'), value: 'Article' },
77 { label: __('How-To Guide', 'thinkrank'), value: 'HowTo' },
78 { label: __('FAQ Page', 'thinkrank'), value: 'FAQPage' },
79 { label: __('Event', 'thinkrank'), value: 'Event' },
80 { label: __('Software Application', 'thinkrank'), value: 'SoftwareApplication' },
81 { label: __('Product', 'thinkrank'), value: 'Product' }
82 ];
83
84 // Use appropriate schema types based on post type
85 const schemaTypes = currentPostType === 'page' ? pageSchemaTypes : postSchemaTypes;
86
87 /**
88 * Handle schema type change with warning and backup
89 */
90 const handleSchemaTypeChange = (newSchemaType) => {
91 if (newSchemaType === selectedSchemaType) return;
92
93 // Check if current schema has data
94 const hasCurrentData = Object.keys(schemaFormData).length > 0;
95
96 if (hasCurrentData) {
97 // Show warning and get confirmation
98 const confirmMessage = __('Switching schema type will replace your current configuration. Your current data will be backed up in case you want to switch back. Continue?', 'thinkrank');
99
100 if (!window.confirm(confirmMessage)) {
101 // User cancelled, revert the select value
102 const selectElement = document.getElementById('schema-type-select');
103 if (selectElement) {
104 selectElement.value = selectedSchemaType;
105 }
106 return;
107 }
108
109 // Backup current data before switching
110 const updatedBackups = {
111 ...schemaBackups,
112 [selectedSchemaType]: schemaFormData
113 };
114 setSchemaBackups(updatedBackups);
115 }
116
117 // Check if we have backup data for the new schema type
118 const backupData = schemaBackups[newSchemaType] || {};
119
120 // Switch to new schema type
121 setSelectedSchemaType(newSchemaType);
122 setSchemaFormData(backupData);
123
124 // Save the new schema type and any backup data
125 saveSchemaFormData(backupData, newSchemaType);
126
127 // Show success message
128 if (Object.keys(backupData).length > 0) {
129 showNotice?.(__(`Switched to ${newSchemaType} schema. Previous configuration restored from backup.`, 'thinkrank'), 'success');
130 } else {
131 showNotice?.(__(`Switched to ${newSchemaType} schema.`, 'thinkrank'), 'success');
132 }
133 };
134
135 /**
136 * Handle schema form data changes
137 */
138 const handleSchemaFormChange = (field, value) => {
139 const newFormData = {
140 ...schemaFormData,
141 [field]: value
142 };
143 setSchemaFormData(newFormData);
144
145 // Don't auto-save on every keystroke - only save when generating schema or saving post
146 };
147
148 /**
149 * Save schema form data to post meta (single schema type approach)
150 */
151 const saveSchemaFormData = async (formData = schemaFormData, schemaType = selectedSchemaType) => {
152 if (!postId) return;
153
154 // Removed debug logging
155
156 try {
157 // Use correct endpoint based on post type
158 const endpoint = currentPostType === 'page' ? 'pages' : 'posts';
159 const apiPath = `/wp/v2/${endpoint}/${postId}`;
160
161 const requestData = {
162 meta: {
163 _thinkrank_schema_form_data: JSON.stringify(formData),
164 _thinkrank_selected_schema_type: schemaType
165 }
166 };
167
168 // Removed debug logging
169
170 const response = await apiFetch({
171 path: apiPath,
172 method: 'POST',
173 data: requestData
174 });
175
176 // Save completed successfully
177 } catch (error) {
178 // Schema form data save failed - handled silently
179 // Error details are available in browser dev tools if needed for debugging
180 }
181 };
182
183 /**
184 * Extract content from WordPress editor (same method as SEO Analysis tab)
185 */
186 const getPostContent = () => {
187 let content = '';
188 let htmlContent = '';
189
190 // Try to get content from Block Editor first
191 if (typeof wp !== 'undefined' && wp.data && wp.data.select('core/editor')) {
192 try {
193 const blockContent = wp.data.select('core/editor').getEditedPostContent();
194 if (blockContent) {
195 htmlContent = blockContent;
196 }
197 } catch (e) {
198 // Block editor not available, try classic editor
199 }
200 }
201
202 // Fallback to TinyMCE (Classic Editor)
203 if (!htmlContent && typeof tinymce !== 'undefined') {
204 const editor = tinymce.get('content');
205 if (editor && !editor.isHidden()) {
206 htmlContent = editor.getContent();
207 }
208 }
209
210 // Fallback to textarea
211 if (!htmlContent) {
212 const contentTextarea = document.getElementById('content');
213 if (contentTextarea) {
214 htmlContent = contentTextarea.value || '';
215 }
216 }
217
218 // Convert HTML to plain text for analysis
219 if (htmlContent) {
220 const tempDiv = document.createElement('div');
221 tempDiv.innerHTML = htmlContent;
222 content = tempDiv.textContent || tempDiv.innerText || '';
223 }
224
225 return { content: content.trim(), htmlContent };
226 };
227
228 /**
229 * Get schema content data with priority logic
230 * Priority: SEO Metadata form fields (AI-optimized) → Post data
231 * Note: Uses same content source and word count method as SEO Analysis tab
232 */
233 const getSchemaContentData = () => {
234 // Get the post permalink from localized data
235 const postPermalink = window.thinkrankMetabox?.postPermalink || window.location.origin + '/?p=' + postId;
236
237 // Get content using same method as SEO Analysis tab
238 const { content } = getPostContent();
239
240 // Calculate word count using same method as SEO Analysis tab
241 const wordCount = calculateWordCount(content);
242
243 return {
244 // Priority 1: SEO Metadata form fields (contains AI-optimized content)
245 title: formData?.thinkrank_seo_title ||
246 // Priority 2: WordPress post title
247 postTitle || '',
248
249 description: formData?.thinkrank_meta_description ||
250 '',
251
252 content: content || contentPreview || '',
253 word_count: wordCount, // Send calculated word count to backend
254 focus_keyword: formData?.thinkrank_focus_keyword || '',
255 post_type: 'post',
256 post_url: postPermalink
257 };
258 };
259
260 /**
261 * Load deployed schemas and schema form data on component mount
262 */
263 useEffect(() => {
264 loadDeployedSchemas(true); // Initial load
265 loadSchemaFormData(); // Load saved form data
266 }, [postId]);
267
268 /**
269 * Save schema data when WordPress saves the post
270 */
271 useEffect(() => {
272 const handleBeforeUnload = () => {
273 // Save form data before page unload
274 if (Object.keys(schemaFormData).length > 0) {
275 saveSchemaFormData(schemaFormData, selectedSchemaType);
276 }
277 };
278
279 window.addEventListener('beforeunload', handleBeforeUnload);
280
281 // Also listen for WordPress save events
282 const handleWordPressSave = () => {
283 if (Object.keys(schemaFormData).length > 0) {
284 saveSchemaFormData(schemaFormData, selectedSchemaType);
285 }
286 };
287
288 // Listen for WordPress post save
289 document.addEventListener('submit', (e) => {
290 if (e.target.id === 'post') {
291 handleWordPressSave();
292 }
293 });
294
295 return () => {
296 window.removeEventListener('beforeunload', handleBeforeUnload);
297 };
298 }, [schemaFormData, selectedSchemaType]);
299
300 // Removed debug comparison function
301
302 /**
303 * Load saved schema form data from post meta (single schema type approach)
304 */
305 const loadSchemaFormData = async () => {
306 if (!postId) return;
307
308 // Removed debug comparison call
309
310 try {
311 // Use correct endpoint based on post type
312 const endpoint = currentPostType === 'page' ? 'pages' : 'posts';
313 const apiPath = `/wp/v2/${endpoint}/${postId}?context=edit`;
314
315 const response = await apiFetch({
316 path: apiPath,
317 method: 'GET'
318 });
319
320 // Removed debug logging
321
322 if (response && response.meta) {
323 // Load the selected schema type first
324 const savedSchemaType = response.meta._thinkrank_selected_schema_type;
325 if (savedSchemaType) {
326 // Check if the saved schema type is valid for current post type
327 const allValidTypes = [...postSchemaTypes, ...pageSchemaTypes];
328 if (allValidTypes.some(type => type.value === savedSchemaType)) {
329 setSelectedSchemaType(savedSchemaType);
330 }
331 }
332
333 // Load the form data for the selected schema type
334 const savedFormData = response.meta._thinkrank_schema_form_data;
335
336 if (savedFormData) {
337 try {
338 const parsedData = JSON.parse(savedFormData);
339 setSchemaFormData(parsedData);
340 } catch (e) {
341 // Failed to parse saved schema form data - reset to empty
342 setSchemaFormData({});
343 }
344 } else {
345 // No saved data, start with empty form
346 setSchemaFormData({});
347 }
348 }
349 } catch (error) {
350 // Failed to load schema form data - handled silently
351 }
352 };
353
354 /**
355 * Load deployed schemas from database
356 */
357 const loadDeployedSchemas = async (isInitialLoad = false) => {
358 if (!postId) return;
359
360 try {
361 // Use main loading state only for initial load, refresh state for subsequent loads
362 if (isInitialLoad) {
363 setIsLoading(true);
364 } else {
365 setIsRefreshing(true);
366 }
367
368 const response = await apiFetch({
369 path: `/thinkrank/v1/schema/deployed?context_type=${currentPostType}&context_id=${postId}`
370 });
371
372 if (response.success) {
373 setDeployedSchemas(response.data);
374 }
375 } catch (error) {
376 showNotice?.(__('Failed to load deployed schemas', 'thinkrank'), 'error');
377 } finally {
378 if (isInitialLoad) {
379 setIsLoading(false);
380 } else {
381 setIsRefreshing(false);
382 }
383 }
384 };
385
386 /**
387 * Generate schema markup
388 */
389 const generateSchema = async () => {
390 if (!postId) {
391 showNotice?.(__('Post ID is required for schema generation', 'thinkrank'), 'error');
392 return;
393 }
394
395 setIsGenerating(true);
396
397 try {
398 const requestData = {
399 context_type: currentPostType,
400 context_id: postId,
401 schema_types: [selectedSchemaType],
402 content_data: getSchemaContentData(),
403 schema_form_data: schemaFormData, // Include current schema type's form data
404 options: {
405 validation_level: 'moderate',
406 rich_snippets_optimization: true
407 }
408 };
409
410 // Save form data before generating schema
411 await saveSchemaFormData(schemaFormData, selectedSchemaType);
412
413 const response = await apiFetch({
414 path: '/thinkrank/v1/schema/generate',
415 method: 'POST',
416 data: requestData
417 });
418
419 if (response.success) {
420 setGeneratedSchemas(response.data);
421 showNotice?.(__('Schema markup generated successfully!', 'thinkrank'), 'success');
422 } else {
423 showNotice?.(response.message || __('Schema generation failed', 'thinkrank'), 'error');
424 }
425 } catch (error) {
426 showNotice?.(__('Schema generation failed. Please try again.', 'thinkrank'), 'error');
427 } finally {
428 setIsGenerating(false);
429 }
430 };
431
432 /**
433 * Deploy schema markup
434 */
435 const deploySchema = async () => {
436 if (!postId) {
437 showNotice?.(__('Post ID is required for schema deployment', 'thinkrank'), 'error');
438 return;
439 }
440
441 // Use generated schemas if available, otherwise use deployed schemas for redeployment
442 let schemasToDeploy;
443
444 if (generatedSchemas?.generated_schemas) {
445 schemasToDeploy = generatedSchemas.generated_schemas;
446 } else if (deployedSchemas) {
447 schemasToDeploy = deployedSchemas;
448 }
449
450 // Normalize schema data structure - ensure we have the raw schema objects, not wrapped format
451 if (schemasToDeploy) {
452 const normalizedSchemas = {};
453
454 Object.entries(schemasToDeploy).forEach(([schemaType, schemaData]) => {
455 // Check if schema is in wrapper format {data: {...}, method: "json_ld", type: "Article"}
456 if (schemaData && typeof schemaData === 'object' && schemaData.data && schemaData.method && schemaData.type) {
457 // Extract the actual schema from the wrapper
458 normalizedSchemas[schemaType] = schemaData.data;
459 } else {
460 // Schema is already in correct format
461 normalizedSchemas[schemaType] = schemaData;
462 }
463 });
464
465 schemasToDeploy = normalizedSchemas;
466 }
467
468 if (!schemasToDeploy) {
469 showNotice?.(__('No schema markup to deploy. Please generate schema first.', 'thinkrank'), 'error');
470 return;
471 }
472
473 setIsDeploying(true);
474
475 try {
476 const requestData = {
477 context_type: currentPostType,
478 context_id: postId,
479 schema_data: schemasToDeploy,
480 options: {
481 deployment_method: 'json_ld'
482 }
483 };
484
485 const response = await apiFetch({
486 path: '/thinkrank/v1/schema/deploy',
487 method: 'POST',
488 data: requestData
489 });
490
491 if (response.success) {
492 showNotice?.(__('Schema markup deployed successfully!', 'thinkrank'), 'success');
493
494 // Update state in a way that prevents jarring UI changes
495 // First, refresh deployed schemas (this will show refresh indicator)
496 await loadDeployedSchemas(false); // Not initial load
497
498 // Then clear generated schemas after deployed schemas are loaded
499 setGeneratedSchemas(null);
500 } else {
501 showNotice?.(response.message || __('Schema deployment failed', 'thinkrank'), 'error');
502 }
503 } catch (error) {
504 showNotice?.(__('Schema deployment failed. Please try again.', 'thinkrank'), 'error');
505 } finally {
506 setIsDeploying(false);
507 }
508 };
509
510 /**
511 * Get deployment status for UI logic
512 */
513 const getDeploymentStatus = () => {
514 if (!generatedSchemas && !deployedSchemas) return 'not_generated';
515 if (generatedSchemas && !deployedSchemas) return 'ready_to_deploy';
516 if (deployedSchemas && !generatedSchemas) return 'deployed';
517 if (generatedSchemas && deployedSchemas) return 'ready_to_redeploy';
518 return 'not_generated';
519 };
520
521 const deploymentStatus = getDeploymentStatus();
522
523 if (isLoading) {
524 return (
525 <div className="thinkrank-metabox-schema-tab">
526 <div className="thinkrank-loading">
527 <span className="spinner is-active"></span>
528 <p>{__('Loading schema data...', 'thinkrank')}</p>
529 </div>
530 </div>
531 );
532 }
533
534 return (
535 <div className={`thinkrank-metabox-schema-tab ${isRefreshing ? 'is-refreshing' : ''}`}>
536 {/* Refresh indicator */}
537 {isRefreshing && (
538 <div className="thinkrank-refresh-indicator">
539 <span className="spinner is-active"></span>
540 <span>{__('Updating schema data...', 'thinkrank')}</span>
541 </div>
542 )}
543
544 {/* Schema Type Selection */}
545 <div className="thinkrank-card">
546 <div className="thinkrank-card-header">
547 <h4>{__('Schema Type', 'thinkrank')}</h4>
548 <p className="description">
549 {__('Select the type of structured data markup for this content.', 'thinkrank')}
550 </p>
551 </div>
552 <div className="thinkrank-card-body">
553 <div className="thinkrank-field">
554 <label htmlFor="schema-type-select">
555 {__('Schema Type', 'thinkrank')}
556 </label>
557 <select
558 id="schema-type-select"
559 value={selectedSchemaType}
560 onChange={(e) => {
561 const newSchemaType = e.target.value;
562 handleSchemaTypeChange(newSchemaType);
563 }}
564 className="widefat"
565 >
566 {schemaTypes.map(type => (
567 <option key={type.value} value={type.value}>
568 {type.label}
569 </option>
570 ))}
571 </select>
572 <p className="description">
573 {__('Article is recommended for most blog posts and pages.', 'thinkrank')}
574 </p>
575 </div>
576
577 {/* Dynamic Schema Configuration Forms */}
578 {(() => {
579 const FormComponent = getSchemaFormComponent(selectedSchemaType);
580 if (!FormComponent) return null;
581
582 return (
583 <div className="schema-form-container">
584 <FormComponent
585 settings={{ ...schemaFormData, enabled: true }}
586 onSettingChange={handleSchemaFormChange}
587 isLoading={isGenerating}
588 />
589 </div>
590 );
591 })()}
592
593 <div className="schema-actions">
594 <button
595 type="button"
596 className="button button-primary"
597 onClick={generateSchema}
598 disabled={isGenerating}
599 >
600 <span className={`dashicons ${isGenerating ? 'dashicons-update spin' : 'dashicons-admin-generic'}`}></span>
601 {isGenerating ? __('Generating...', 'thinkrank') : __('Generate Schema', 'thinkrank')}
602 </button>
603 </div>
604 </div>
605 </div>
606
607 {/* Deployment Status and Actions */}
608 {deploymentStatus === 'not_generated' && (
609 <div className="thinkrank-card">
610 <div className="thinkrank-card-header">
611 <h4>{__('Schema Status', 'thinkrank')}</h4>
612 </div>
613 <div className="thinkrank-card-body">
614 <div className="schema-status-notice">
615 <span className="dashicons dashicons-info"></span>
616 <p>{__('No schema markup generated yet. Select a schema type and click "Generate Schema" to get started.', 'thinkrank')}</p>
617 </div>
618 </div>
619 </div>
620 )}
621
622 {deploymentStatus === 'ready_to_deploy' && (
623 <div className="thinkrank-card">
624 <div className="thinkrank-card-header">
625 <h4>{__('Deploy Schema', 'thinkrank')}</h4>
626 </div>
627 <div className="thinkrank-card-body">
628 <div className="schema-status-notice success">
629 <span className="dashicons dashicons-yes-alt"></span>
630 <p>{__('Schema markup generated successfully! Ready to deploy.', 'thinkrank')}</p>
631 </div>
632
633 <div className="schema-actions">
634 <button
635 type="button"
636 className="button button-primary"
637 onClick={deploySchema}
638 disabled={isDeploying}
639 >
640 <span className={`dashicons ${isDeploying ? 'dashicons-update spin' : 'dashicons-upload'}`}></span>
641 {isDeploying ? __('Deploying...', 'thinkrank') : __('Deploy Schema', 'thinkrank')}
642 </button>
643 </div>
644
645 <p className="description">
646 {__('Schema markup will be deployed as JSON-LD format in the document head.', 'thinkrank')}
647 </p>
648 </div>
649 </div>
650 )}
651
652 {deploymentStatus === 'deployed' && (
653 <div className="thinkrank-card">
654 <div className="thinkrank-card-header">
655 <h4>{__('Schema Actions', 'thinkrank')}</h4>
656 </div>
657 <div className="thinkrank-card-body">
658 <div className="schema-status-notice deployed">
659 <span className="dashicons dashicons-saved"></span>
660 <p>{__('Schema markup is deployed and active on this post.', 'thinkrank')}</p>
661 </div>
662
663 <div className="schema-actions">
664 <button
665 type="button"
666 className="button button-primary"
667 onClick={generateSchema}
668 disabled={isGenerating}
669 >
670 <span className={`dashicons ${isGenerating ? 'dashicons-update spin' : 'dashicons-admin-generic'}`}></span>
671 {isGenerating ? __('Regenerating...', 'thinkrank') : __('Regenerate Schema', 'thinkrank')}
672 </button>
673
674 <button
675 type="button"
676 className="button"
677 onClick={deploySchema}
678 disabled={isDeploying}
679 >
680 <span className={`dashicons ${isDeploying ? 'dashicons-update spin' : 'dashicons-upload'}`}></span>
681 {isDeploying ? __('Redeploying...', 'thinkrank') : __('Redeploy Current', 'thinkrank')}
682 </button>
683 </div>
684 </div>
685 </div>
686 )}
687
688 {deploymentStatus === 'ready_to_redeploy' && (
689 <div className="thinkrank-card">
690 <div className="thinkrank-card-header">
691 <h4>{__('Update Schema', 'thinkrank')}</h4>
692 </div>
693 <div className="thinkrank-card-body">
694 <div className="schema-status-notice warning">
695 <span className="dashicons dashicons-warning"></span>
696 <p>{__('New schema markup generated. Deploy to update the existing schema.', 'thinkrank')}</p>
697 </div>
698
699 <div className="schema-actions">
700 <button
701 type="button"
702 className="button button-primary"
703 onClick={deploySchema}
704 disabled={isDeploying}
705 >
706 <span className={`dashicons ${isDeploying ? 'dashicons-update spin' : 'dashicons-upload'}`}></span>
707 {isDeploying ? __('Updating...', 'thinkrank') : __('Update Schema', 'thinkrank')}
708 </button>
709 </div>
710
711 <p className="description">
712 {__('This will update your existing schema markup with the new version.', 'thinkrank')}
713 </p>
714 </div>
715 </div>
716 )}
717
718 {/* Schema Preview */}
719 {(generatedSchemas?.generated_schemas || deployedSchemas) && (
720 <div className="thinkrank-card">
721 <div className="thinkrank-card-header">
722 <h4>{__('Schema Preview', 'thinkrank')}</h4>
723 <p className="description">
724 {__('Preview of the generated schema markup in JSON-LD format.', 'thinkrank')}
725 </p>
726 </div>
727 <div className="thinkrank-card-body">
728 {/* Schema Types Generated */}
729 <div className="schema-types-generated">
730 <h5>{__('Schema Types:', 'thinkrank')}</h5>
731 <div className="schema-type-badges">
732 {Object.keys(generatedSchemas?.generated_schemas || deployedSchemas || {}).map((schemaType) => (
733 <span key={schemaType} className="schema-type-badge">
734 {schemaType}
735 </span>
736 ))}
737 </div>
738 </div>
739
740 {/* Schema Code Preview */}
741 <div className="schema-code-preview">
742 <h5>{__('JSON-LD Code:', 'thinkrank')}</h5>
743 {Object.entries(generatedSchemas?.generated_schemas || deployedSchemas || {}).map(([schemaType, schemaData]) => (
744 <div key={schemaType} className="schema-code-block">
745 <h6>{schemaType} Schema:</h6>
746 <pre className="schema-json-preview">
747 {JSON.stringify(schemaData, null, 2)}
748 </pre>
749 </div>
750 ))}
751 </div>
752
753 {/* Data Source Information */}
754 <div className="schema-data-source">
755 <h5>{__('Data Sources Used:', 'thinkrank')}</h5>
756 <ul>
757 {formData?.thinkrank_seo_title && <li>{__('✓ SEO Metadata title (AI-optimized)', 'thinkrank')}</li>}
758 {formData?.thinkrank_meta_description && <li>{__('✓ SEO Metadata description (AI-optimized)', 'thinkrank')}</li>}
759 {formData?.thinkrank_focus_keyword && <li>{__('✓ Focus keyword', 'thinkrank')}</li>}
760 {postTitle && !formData?.thinkrank_seo_title && <li>{__('✓ Post title (fallback)', 'thinkrank')}</li>}
761 {contentPreview && <li>{__('✓ Post content', 'thinkrank')}</li>}
762 </ul>
763 <p className="description">
764 {__('Schema markup uses SEO Metadata from the SEO Analysis tab. Use "Generate with AI" to optimize title and description.', 'thinkrank')}
765 </p>
766 </div>
767 </div>
768 </div>
769 )}
770 </div>
771 );
772 };
773
774 export default MetaboxSchemaTab;
775