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 / admin / utils / schemaValidation.js

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

828 lines 33.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * Schema Form Validation Utilities
3 *
4 * Provides validation functions for schema form data before schema generation.
5 * This is separate from schema markup validation which happens after generation.
6 *
7 * @since 1.0.0
8 */
9
10 import { __ } from '@wordpress/i18n';
11
12 /**
13 * Validate section data against schema standards
14 */
15 export const validateSectionData = async (section, data) => {
16 const validation = {
17 section,
18 isValid: true,
19 hasErrors: false,
20 hasWarnings: false,
21 errors: [],
22 warnings: [],
23 suggestions: [],
24 fieldValidation: {},
25 validationSummary: {
26 validatedFields: 0,
27 totalFields: 0,
28 completionPercentage: 0
29 }
30 };
31
32 switch (section) {
33 case 'organization':
34 validateOrganizationSchema(data, validation);
35 break;
36 case 'website':
37 validateWebsiteSchema(data, validation);
38 break;
39 case 'person':
40 validatePersonSchema(data, validation);
41 break;
42 case 'local-business':
43 // LocalBusiness validation is now handled by source tabs (Business Info, Organization)
44 // This tab is purely informational and doesn't need form validation
45 validation.suggestions.push(__('LocalBusiness schema data is validated in the source tabs: Business Info and Organization.', 'thinkrank'));
46 validation.validationSummary.validatedFields = 0;
47 validation.validationSummary.totalFields = 0;
48 validation.validationSummary.completionPercentage = 0;
49 break;
50 case 'schema-types':
51 validateSchemaTypesConfiguration(data, validation);
52 break;
53 }
54
55 // Set overall validation status
56 validation.isValid = !validation.hasErrors;
57
58 // Calculate completion percentage
59 if (validation.validationSummary.totalFields > 0) {
60 validation.validationSummary.completionPercentage = Math.round(
61 (validation.validationSummary.validatedFields / validation.validationSummary.totalFields) * 100
62 );
63 }
64
65 return validation;
66 };
67
68 /**
69 * Validate organization schema section
70 */
71 const validateOrganizationSchema = (data, validation) => {
72 let validatedFields = 0;
73 let totalFields = 12; // organization_name, organization_url, organization_description, organization_logo, organization_type, 5 social fields, 3 contact fields (removed organization_email and organization_address)
74
75 // Required fields for organization schema
76 if (!data.organization_name?.trim()) {
77 validation.errors.push(__('Organization name is required for organization schema.', 'thinkrank'));
78 validation.hasErrors = true;
79 validation.fieldValidation.organization_name = 'error';
80 } else {
81 validatedFields++;
82 validation.suggestions.push(__('✓ Organization name is properly configured.', 'thinkrank'));
83 }
84
85 if (!data.organization_url?.trim()) {
86 validation.errors.push(__('Organization URL is required for organization schema.', 'thinkrank'));
87 validation.hasErrors = true;
88 validation.fieldValidation.organization_url = 'error';
89 } else if (!isValidUrl(data.organization_url)) {
90 validation.errors.push(__('Organization URL must be a valid URL format.', 'thinkrank'));
91 validation.hasErrors = true;
92 validation.fieldValidation.organization_url = 'error';
93 } else {
94 validatedFields++;
95 validation.suggestions.push(__('✓ Organization URL is valid and properly formatted.', 'thinkrank'));
96 }
97
98 if (!data.organization_description?.trim()) {
99 validation.warnings.push(__('Organization description is recommended for better SEO.', 'thinkrank'));
100 validation.hasWarnings = true;
101 validation.fieldValidation.organization_description = 'warning';
102 } else {
103 validatedFields++;
104 validation.suggestions.push(__('✓ Organization description is provided for better SEO.', 'thinkrank'));
105 }
106
107 if (!data.organization_logo?.trim()) {
108 validation.warnings.push(__('Organization logo is recommended for rich snippets.', 'thinkrank'));
109 validation.hasWarnings = true;
110 validation.fieldValidation.organization_logo = 'warning';
111 } else if (!isValidUrl(data.organization_logo)) {
112 validation.errors.push(__('Organization logo must be a valid URL format.', 'thinkrank'));
113 validation.hasErrors = true;
114 validation.fieldValidation.organization_logo = 'error';
115 } else {
116 validatedFields++;
117 validation.suggestions.push(__('✓ Organization logo is configured for rich snippets.', 'thinkrank'));
118 }
119
120 // Organization email is now managed in Business Info tab
121 validation.suggestions.push(__('ℹ Business contact information (email, phone, address) is now managed in Site Identity > Business Info tab for consistency across all local SEO features.', 'thinkrank'));
122
123 if (!data.organization_type?.trim()) {
124 validation.warnings.push(__('Organization type helps search engines understand your business.', 'thinkrank'));
125 validation.hasWarnings = true;
126 validation.fieldValidation.organization_type = 'warning';
127 } else {
128 validatedFields++;
129 validation.suggestions.push(__('✓ Organization type is properly configured.', 'thinkrank'));
130 }
131
132 // Check for sameAs (social media profiles) - this is what's missing in deployment
133 const socialFields = [
134 { key: 'organization_social_facebook', name: 'Facebook' },
135 { key: 'organization_social_twitter', name: 'Twitter' },
136 { key: 'organization_social_linkedin', name: 'LinkedIn' },
137 { key: 'organization_social_instagram', name: 'Instagram' },
138 { key: 'organization_social_youtube', name: 'YouTube' }
139 ];
140
141 let hasSocialProfiles = false;
142 socialFields.forEach(field => {
143 if (data[field.key]?.trim()) {
144 if (!isValidUrl(data[field.key])) {
145 validation.errors.push(__(`${field.name} URL must be a valid URL format.`, 'thinkrank'));
146 validation.hasErrors = true;
147 validation.fieldValidation[field.key] = 'error';
148 } else {
149 validatedFields++;
150 hasSocialProfiles = true;
151 validation.suggestions.push(__(`${field.name} social profile is configured.`, 'thinkrank'));
152 }
153 }
154 });
155
156 if (!hasSocialProfiles) {
157 validation.suggestions.push(__('ℹ Social media profiles are managed in Site Identity > Social Media for consistency across all schema types.', 'thinkrank'));
158 }
159
160 // Contact information is now managed in Business Info tab
161 // Note: Contact data comes from Site Identity > Business Info, not organization form fields
162 validation.suggestions.push(__('ℹ Contact information (phone, email, type) is managed in Site Identity > Business Info for consistency across all local SEO features.', 'thinkrank'));
163
164 validation.validationSummary.validatedFields = validatedFields;
165 validation.validationSummary.totalFields = totalFields;
166 };
167
168 /**
169 * Validate website schema section
170 */
171 const validateWebsiteSchema = (data, validation) => {
172 let validatedFields = 0;
173 let totalFields = 4; // website_name, website_url, website_description, search config (logo managed elsewhere)
174
175 // Website name validation (optional with fallback)
176 if (!data.website_name?.trim()) {
177 validation.suggestions.push(__('✓ Website name will use site title as fallback.', 'thinkrank'));
178 validatedFields++; // This is acceptable as we have a fallback
179 } else {
180 validatedFields++;
181 validation.suggestions.push(__('✓ Website name is properly configured.', 'thinkrank'));
182 }
183
184 // Website URL validation (optional with fallback)
185 if (!data.website_url?.trim()) {
186 validation.suggestions.push(__('✓ Website URL will use home URL as fallback.', 'thinkrank'));
187 validatedFields++; // This is acceptable as we have a fallback
188 } else if (!isValidUrl(data.website_url)) {
189 validation.errors.push(__('Website URL format is invalid. Please enter a valid URL.', 'thinkrank'));
190 validation.hasErrors = true;
191 validation.fieldValidation.website_url = 'error';
192 } else {
193 validatedFields++;
194 validation.suggestions.push(__('✓ Website URL is valid and properly formatted.', 'thinkrank'));
195 }
196
197 // Website description validation (optional with fallback)
198 if (!data.website_description?.trim()) {
199 validation.suggestions.push(__('✓ Website description will use site tagline as fallback.', 'thinkrank'));
200 validatedFields++; // This is acceptable as we have a fallback
201 } else {
202 if (data.website_description.length > 160) {
203 validation.warnings.push(__('Website description is longer than 160 characters and may be truncated.', 'thinkrank'));
204 validation.hasWarnings = true;
205 validation.fieldValidation.website_description = 'warning';
206 }
207 validatedFields++;
208 validation.suggestions.push(__('✓ Website description is provided for better SEO.', 'thinkrank'));
209 }
210
211 // Search configuration validation
212 if (data.website_enable_search !== false) {
213 if (data.website_search_url && data.website_search_url.trim()) {
214 if (!data.website_search_url.includes('{search_term_string}')) {
215 validation.warnings.push(__('Custom search URL should include {search_term_string} placeholder.', 'thinkrank'));
216 validation.hasWarnings = true;
217 validation.fieldValidation.website_search_url = 'warning';
218 } else {
219 validation.suggestions.push(__('✓ Custom search URL is properly configured.', 'thinkrank'));
220 }
221 } else {
222 validation.suggestions.push(__('✓ Search functionality will use default WordPress search.', 'thinkrank'));
223 }
224 validatedFields++;
225 } else {
226 validation.warnings.push(__('Sitelinks search box is disabled. Consider enabling for better user experience.', 'thinkrank'));
227 validation.hasWarnings = true;
228 validation.fieldValidation.website_enable_search = 'warning';
229 }
230
231 // Logo is managed in Site Identity tab - not validated here
232
233 validation.validationSummary.validatedFields = validatedFields;
234 validation.validationSummary.totalFields = totalFields;
235 };
236
237 /**
238 * Validate person schema section
239 */
240 const validatePersonSchema = (data, validation) => {
241 let validatedFields = 0;
242 let totalFields = 11; // person_name, person_image, person_job_title, person_nationality, person_biography, person_url, person_email, person_telephone, person_birth_date, person_address, person_works_for
243
244 // Required fields for person schema
245 if (!data.person_name?.trim()) {
246 validation.errors.push(__('Person name is required for person schema.', 'thinkrank'));
247 validation.hasErrors = true;
248 validation.fieldValidation.person_name = 'error';
249 } else {
250 validatedFields++;
251 validation.suggestions.push(__('✓ Person name is properly configured.', 'thinkrank'));
252 }
253
254 // Profile image validation (recommended)
255 if (!data.person_image?.trim()) {
256 validation.warnings.push(__('Profile image is recommended for better person schema visibility.', 'thinkrank'));
257 validation.hasWarnings = true;
258 validation.fieldValidation.person_image = 'warning';
259 } else {
260 validatedFields++;
261 validation.suggestions.push(__('✓ Profile image is configured.', 'thinkrank'));
262 }
263
264 // Job title validation (recommended)
265 if (!data.person_job_title?.trim()) {
266 validation.warnings.push(__('Job title is recommended for professional person schema.', 'thinkrank'));
267 validation.hasWarnings = true;
268 validation.fieldValidation.person_job_title = 'warning';
269 } else {
270 validatedFields++;
271 validation.suggestions.push(__('✓ Job title is properly configured.', 'thinkrank'));
272 }
273
274 // Nationality validation (optional)
275 if (data.person_nationality?.trim()) {
276 validatedFields++;
277 validation.suggestions.push(__('✓ Nationality information is included.', 'thinkrank'));
278 }
279
280 // Biography validation (recommended)
281 if (!data.person_biography?.trim()) {
282 validation.warnings.push(__('Biography is recommended for comprehensive person schema.', 'thinkrank'));
283 validation.hasWarnings = true;
284 validation.fieldValidation.person_biography = 'warning';
285 } else if (data.person_biography.trim().length < 50) {
286 validation.warnings.push(__('Biography is quite short. Consider adding more details for better SEO.', 'thinkrank'));
287 validation.hasWarnings = true;
288 validation.fieldValidation.person_biography = 'warning';
289 validatedFields++;
290 } else {
291 validatedFields++;
292 validation.suggestions.push(__('✓ Biography is comprehensive and well-detailed.', 'thinkrank'));
293 }
294
295 // Website URL validation (optional)
296 if (data.person_url?.trim()) {
297 if (!isValidUrl(data.person_url)) {
298 validation.errors.push(__('Person website URL must be a valid URL format.', 'thinkrank'));
299 validation.hasErrors = true;
300 validation.fieldValidation.person_url = 'error';
301 } else {
302 validatedFields++;
303 validation.suggestions.push(__('✓ Person website URL is valid and properly formatted.', 'thinkrank'));
304 }
305 }
306
307 // Email validation (optional)
308 if (data.person_email?.trim()) {
309 if (!isValidEmail(data.person_email)) {
310 validation.errors.push(__('Person email must be a valid email format.', 'thinkrank'));
311 validation.hasErrors = true;
312 validation.fieldValidation.person_email = 'error';
313 } else {
314 validatedFields++;
315 validation.suggestions.push(__('✓ Person email is valid and properly formatted.', 'thinkrank'));
316 }
317 }
318
319 // Phone validation (optional)
320 if (data.person_telephone?.trim()) {
321 validatedFields++;
322 validation.suggestions.push(__('✓ Phone number is included.', 'thinkrank'));
323 }
324
325 // Birth date validation (optional)
326 if (data.person_birth_date?.trim()) {
327 validatedFields++;
328 validation.suggestions.push(__('✓ Birth date information is included.', 'thinkrank'));
329 }
330
331 // Address validation (optional)
332 if (data.person_address?.trim()) {
333 validatedFields++;
334 validation.suggestions.push(__('✓ Address information is included.', 'thinkrank'));
335 }
336
337 // Works for validation (optional)
338 if (data.person_works_for?.trim()) {
339 validatedFields++;
340 validation.suggestions.push(__('✓ Organization affiliation is included.', 'thinkrank'));
341 }
342
343 validation.validationSummary.validatedFields = validatedFields;
344 validation.validationSummary.totalFields = totalFields;
345 };
346
347 /**
348 * Validate schema configuration settings
349 */
350 const validateSchemaConfigurationSettings = (data, validation) => {
351 let validatedFields = 0;
352 let totalFields = 6; // Schema configuration settings
353
354 // Check if schema types are enabled
355 const enabledSchemaTypes = data.enabled_schema_types || [];
356
357 if (enabledSchemaTypes.length === 0) {
358 validation.warnings.push(__('No schema types are enabled. Enable at least one schema type for better SEO.', 'thinkrank'));
359 validation.hasWarnings = true;
360 validation.fieldValidation.enabled_schema_types = 'warning';
361 } else {
362 validatedFields++;
363 validation.suggestions.push(__(`${enabledSchemaTypes.length} schema type(s) enabled: ${enabledSchemaTypes.join(', ')}.`, 'thinkrank'));
364 }
365
366 // Check validation level
367 if (!data.validation_level?.trim()) {
368 validation.warnings.push(__('Validation level should be set for proper schema validation.', 'thinkrank'));
369 validation.hasWarnings = true;
370 validation.fieldValidation.validation_level = 'warning';
371 } else {
372 validatedFields++;
373 validation.suggestions.push(__(`✓ Validation level set to ${data.validation_level}.`, 'thinkrank'));
374 }
375
376 // Check if auto-generation is enabled
377 if (data.auto_generate_schema === false) {
378 validation.warnings.push(__('Auto-generate schema is disabled. Enable for automatic schema generation.', 'thinkrank'));
379 validation.hasWarnings = true;
380 validation.fieldValidation.auto_generate_schema = 'warning';
381 } else {
382 validatedFields++;
383 validation.suggestions.push(__('✓ Auto-generate schema is enabled.', 'thinkrank'));
384 }
385
386 // Check if rich snippets optimization is enabled
387 if (data.rich_snippets_optimization === false) {
388 validation.warnings.push(__('Rich snippets optimization is disabled. Enable for better search results.', 'thinkrank'));
389 validation.hasWarnings = true;
390 validation.fieldValidation.rich_snippets_optimization = 'warning';
391 } else {
392 validatedFields++;
393 validation.suggestions.push(__('✓ Rich snippets optimization is enabled.', 'thinkrank'));
394 }
395
396 // Check cache duration
397 if (!data.cache_duration || data.cache_duration < 300) {
398 validation.warnings.push(__('Cache duration should be at least 300 seconds for optimal performance.', 'thinkrank'));
399 validation.hasWarnings = true;
400 validation.fieldValidation.cache_duration = 'warning';
401 } else {
402 validatedFields++;
403 validation.suggestions.push(__(`✓ Cache duration set to ${data.cache_duration} seconds.`, 'thinkrank'));
404 }
405
406 // Check if knowledge graph is enabled
407 if (data.knowledge_graph === false) {
408 validation.warnings.push(__('Knowledge graph is disabled. Enable for better search engine understanding.', 'thinkrank'));
409 validation.hasWarnings = true;
410 validation.fieldValidation.knowledge_graph = 'warning';
411 } else {
412 validatedFields++;
413 validation.suggestions.push(__('✓ Knowledge graph is enabled.', 'thinkrank'));
414 }
415
416 validation.validationSummary.validatedFields = validatedFields;
417 validation.validationSummary.totalFields = totalFields;
418 };
419
420
421
422 /**
423 * Validation helper functions
424 */
425 const isValidUrl = (url) => {
426 try {
427 new URL(url);
428 return true;
429 } catch {
430 return false;
431 }
432 };
433
434 const isValidEmail = (email) => {
435 const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
436 return emailRegex.test(email);
437 };
438
439 const isValidLatitude = (lat) => {
440 const num = parseFloat(lat);
441 return !isNaN(num) && num >= -90 && num <= 90;
442 };
443
444 const isValidLongitude = (lng) => {
445 const num = parseFloat(lng);
446 return !isNaN(num) && num >= -180 && num <= 180;
447 };
448
449 /**
450 * Validate schema types configuration section
451 */
452 const validateSchemaTypesConfiguration = (data, validation) => {
453 let validatedFields = 0;
454 let totalFields = 0;
455
456 // Get enabled schema types
457 const enabledTypes = data.enabled_schema_types || [];
458
459 // Validate Product schema configuration
460 if (enabledTypes.includes('Product')) {
461 totalFields += 7; // product_name, product_price, product_currency, product_brand, product_sku, product_availability, product_description
462
463 if (!data.product_name?.trim()) {
464 validation.warnings.push(__('Product name is recommended for Product schema.', 'thinkrank'));
465 validation.hasWarnings = true;
466 validation.fieldValidation.product_name = 'warning';
467 } else {
468 validatedFields++;
469 validation.suggestions.push(__('✓ Product name is configured.', 'thinkrank'));
470 }
471
472 if (!data.product_description?.trim()) {
473 validation.warnings.push(__('Product description is recommended for Product schema.', 'thinkrank'));
474 validation.hasWarnings = true;
475 validation.fieldValidation.product_description = 'warning';
476 } else {
477 validatedFields++;
478 validation.suggestions.push(__('✓ Product description is configured.', 'thinkrank'));
479 }
480
481 if (data.product_price?.trim()) {
482 validatedFields++;
483 validation.suggestions.push(__('✓ Product price is configured.', 'thinkrank'));
484 }
485
486 if (data.product_brand?.trim()) {
487 validatedFields++;
488 validation.suggestions.push(__('✓ Product brand is configured.', 'thinkrank'));
489 }
490
491 if (data.product_sku?.trim()) {
492 validatedFields++;
493 validation.suggestions.push(__('✓ Product SKU is configured.', 'thinkrank'));
494 }
495
496 if (data.product_currency?.trim()) {
497 validatedFields++;
498 validation.suggestions.push(__('✓ Product currency is configured.', 'thinkrank'));
499 }
500
501 if (data.product_availability?.trim()) {
502 validatedFields++;
503 validation.suggestions.push(__('✓ Product availability is configured.', 'thinkrank'));
504 }
505 }
506
507 // Validate Event schema configuration
508 if (enabledTypes.includes('Event')) {
509 totalFields += 12; // event_name, event_type, event_start_date, event_end_date, event_status, event_location, event_attendance_mode, event_organizer, event_performer, event_image, event_price, event_description
510
511 if (!data.event_name?.trim()) {
512 validation.warnings.push(__('Event name is recommended for Event schema.', 'thinkrank'));
513 validation.hasWarnings = true;
514 validation.fieldValidation.event_name = 'warning';
515 } else {
516 validatedFields++;
517 validation.suggestions.push(__('✓ Event name is configured.', 'thinkrank'));
518 }
519
520 if (!data.event_start_date?.trim()) {
521 validation.warnings.push(__('Event start date is recommended for Event schema.', 'thinkrank'));
522 validation.hasWarnings = true;
523 validation.fieldValidation.event_start_date = 'warning';
524 } else {
525 validatedFields++;
526 validation.suggestions.push(__('✓ Event start date is configured.', 'thinkrank'));
527 }
528
529 if (data.event_location?.trim()) {
530 validatedFields++;
531 validation.suggestions.push(__('✓ Event location is configured.', 'thinkrank'));
532 }
533
534 if (data.event_organizer?.trim()) {
535 validatedFields++;
536 validation.suggestions.push(__('✓ Event organizer is configured.', 'thinkrank'));
537 }
538
539 if (data.event_performer?.trim()) {
540 validatedFields++;
541 validation.suggestions.push(__('✓ Event performer is configured.', 'thinkrank'));
542 }
543
544 if (data.event_image?.trim()) {
545 validatedFields++;
546 validation.suggestions.push(__('✓ Event image is configured.', 'thinkrank'));
547 }
548
549 if (data.event_price?.trim()) {
550 validatedFields++;
551 validation.suggestions.push(__('✓ Event pricing is configured.', 'thinkrank'));
552 }
553
554 if (data.event_description?.trim()) {
555 validatedFields++;
556 validation.suggestions.push(__('✓ Event description is configured.', 'thinkrank'));
557 }
558
559 if (data.event_type?.trim()) {
560 validatedFields++;
561 validation.suggestions.push(__('✓ Event type is configured.', 'thinkrank'));
562 }
563
564 if (data.event_end_date?.trim()) {
565 validatedFields++;
566 validation.suggestions.push(__('✓ Event end date is configured.', 'thinkrank'));
567 }
568
569 if (data.event_status?.trim()) {
570 validatedFields++;
571 validation.suggestions.push(__('✓ Event status is configured.', 'thinkrank'));
572 }
573
574 if (data.event_attendance_mode?.trim()) {
575 validatedFields++;
576 validation.suggestions.push(__('✓ Event attendance mode is configured.', 'thinkrank'));
577 }
578 }
579
580 // Validate Article schema configuration
581 if (enabledTypes.includes('Article')) {
582 totalFields += 9; // article_headline, article_type, article_author, article_publisher, article_date_published, article_date_modified, article_section, article_keywords, article_summary
583
584 if (!data.article_headline?.trim()) {
585 validation.warnings.push(__('Article headline is recommended for Article schema.', 'thinkrank'));
586 validation.hasWarnings = true;
587 validation.fieldValidation.article_headline = 'warning';
588 } else {
589 validatedFields++;
590 validation.suggestions.push(__('✓ Article headline is configured.', 'thinkrank'));
591 }
592
593 if (!data.article_author?.trim()) {
594 validation.warnings.push(__('Article author is recommended for Article schema.', 'thinkrank'));
595 validation.hasWarnings = true;
596 validation.fieldValidation.article_author = 'warning';
597 } else {
598 validatedFields++;
599 validation.suggestions.push(__('✓ Article author is configured.', 'thinkrank'));
600 }
601
602 if (data.article_summary?.trim()) {
603 validatedFields++;
604 validation.suggestions.push(__('✓ Article summary is configured.', 'thinkrank'));
605 }
606
607 if (data.article_publisher?.trim()) {
608 validatedFields++;
609 validation.suggestions.push(__('✓ Article publisher is configured.', 'thinkrank'));
610 }
611
612 if (data.article_date_published?.trim()) {
613 validatedFields++;
614 validation.suggestions.push(__('✓ Article publication date is configured.', 'thinkrank'));
615 }
616
617 if (data.article_date_modified?.trim()) {
618 validatedFields++;
619 validation.suggestions.push(__('✓ Article modification date is configured.', 'thinkrank'));
620 }
621
622 if (data.article_section?.trim()) {
623 validatedFields++;
624 validation.suggestions.push(__('✓ Article section is configured.', 'thinkrank'));
625 }
626
627 if (data.article_keywords?.trim()) {
628 validatedFields++;
629 validation.suggestions.push(__('✓ Article keywords are configured.', 'thinkrank'));
630 }
631
632 if (data.article_type?.trim()) {
633 validatedFields++;
634 validation.suggestions.push(__('✓ Article type is configured.', 'thinkrank'));
635 }
636 }
637
638 // Validate SoftwareApplication schema configuration
639 if (enabledTypes.includes('SoftwareApplication')) {
640 totalFields += 12; // software_name, software_alternate_name, software_version, software_url, software_creator, software_description, software_operating_systems, software_category, software_features, software_price, software_currency, software_availability
641
642 if (!data.software_name?.trim()) {
643 validation.warnings.push(__('Software name is recommended for SoftwareApplication schema.', 'thinkrank'));
644 validation.hasWarnings = true;
645 validation.fieldValidation.software_name = 'warning';
646 } else {
647 validatedFields++;
648 validation.suggestions.push(__('✓ Software name is configured.', 'thinkrank'));
649 }
650
651 if (!data.software_description?.trim()) {
652 validation.warnings.push(__('Software description is recommended for SoftwareApplication schema.', 'thinkrank'));
653 validation.hasWarnings = true;
654 validation.fieldValidation.software_description = 'warning';
655 } else {
656 validatedFields++;
657 validation.suggestions.push(__('✓ Software description is configured.', 'thinkrank'));
658 }
659
660 if (data.software_alternate_name?.trim()) {
661 validatedFields++;
662 validation.suggestions.push(__('✓ Software tagline/alternate name is configured.', 'thinkrank'));
663 }
664
665 if (data.software_version?.trim()) {
666 validatedFields++;
667 validation.suggestions.push(__('✓ Software version is configured.', 'thinkrank'));
668 }
669
670 if (data.software_url?.trim()) {
671 if (!isValidUrl(data.software_url)) {
672 validation.errors.push(__('Software URL must be a valid URL format.', 'thinkrank'));
673 validation.hasErrors = true;
674 validation.fieldValidation.software_url = 'error';
675 } else {
676 validatedFields++;
677 validation.suggestions.push(__('✓ Software URL is configured.', 'thinkrank'));
678 }
679 }
680
681 if (data.software_creator?.trim()) {
682 validatedFields++;
683 validation.suggestions.push(__('✓ Software creator is configured.', 'thinkrank'));
684 }
685
686 if (data.software_operating_systems && data.software_operating_systems.length > 0) {
687 validatedFields++;
688 validation.suggestions.push(__(`✓ Operating systems configured (${data.software_operating_systems.length} systems).`, 'thinkrank'));
689 }
690
691 if (data.software_category?.trim()) {
692 validatedFields++;
693 validation.suggestions.push(__('✓ Software category is configured.', 'thinkrank'));
694 }
695
696 if (data.software_features && data.software_features.length > 0) {
697 validatedFields++;
698 validation.suggestions.push(__(`✓ Software features configured (${data.software_features.length} features).`, 'thinkrank'));
699 }
700
701 if (data.software_price?.trim()) {
702 validatedFields++;
703 validation.suggestions.push(__('✓ Software price is configured.', 'thinkrank'));
704 }
705
706 if (data.software_currency?.trim()) {
707 validatedFields++;
708 validation.suggestions.push(__('✓ Software currency is configured.', 'thinkrank'));
709 }
710
711 if (data.software_availability?.trim()) {
712 validatedFields++;
713 validation.suggestions.push(__('✓ Software availability is configured.', 'thinkrank'));
714 }
715 }
716
717 // Validate HowTo schema configuration
718 if (enabledTypes.includes('HowTo')) {
719 totalFields += 9; // howto_name, howto_description, howto_total_time, howto_prep_time, howto_difficulty, howto_estimated_cost, howto_supply, howto_url, howto_yield
720
721 if (!data.howto_name?.trim()) {
722 validation.warnings.push(__('HowTo title is recommended for HowTo schema.', 'thinkrank'));
723 validation.hasWarnings = true;
724 validation.fieldValidation.howto_name = 'warning';
725 } else {
726 validatedFields++;
727 validation.suggestions.push(__('✓ HowTo title is configured.', 'thinkrank'));
728 }
729
730 if (!data.howto_description?.trim()) {
731 validation.warnings.push(__('HowTo description is recommended for HowTo schema.', 'thinkrank'));
732 validation.hasWarnings = true;
733 validation.fieldValidation.howto_description = 'warning';
734 } else {
735 validatedFields++;
736 validation.suggestions.push(__('✓ HowTo description is configured.', 'thinkrank'));
737 }
738
739 if (data.howto_total_time?.trim()) {
740 validatedFields++;
741 validation.suggestions.push(__('✓ HowTo total time is configured.', 'thinkrank'));
742 }
743
744 if (data.howto_prep_time?.trim()) {
745 validatedFields++;
746 validation.suggestions.push(__('✓ HowTo prep time is configured.', 'thinkrank'));
747 }
748
749 if (data.howto_difficulty?.trim()) {
750 validatedFields++;
751 validation.suggestions.push(__('✓ HowTo difficulty is configured.', 'thinkrank'));
752 }
753
754 if (data.howto_estimated_cost?.trim()) {
755 validatedFields++;
756 validation.suggestions.push(__('✓ HowTo estimated cost is configured.', 'thinkrank'));
757 }
758
759 if (data.howto_supply && data.howto_supply.length > 0) {
760 validatedFields++;
761 validation.suggestions.push(__(`✓ HowTo tools/materials configured (${data.howto_supply.length} items).`, 'thinkrank'));
762 }
763
764 if (data.howto_url?.trim()) {
765 validatedFields++;
766 validation.suggestions.push(__('✓ HowTo URL is configured.', 'thinkrank'));
767 }
768
769 if (data.howto_yield?.trim()) {
770 validatedFields++;
771 validation.suggestions.push(__('✓ HowTo yield/result is configured.', 'thinkrank'));
772 }
773 }
774
775 // Set validation summary
776 validation.validationSummary.validatedFields = validatedFields;
777 validation.validationSummary.totalFields = totalFields;
778 validation.validationSummary.completionPercentage = totalFields > 0 ? Math.round((validatedFields / totalFields) * 100) : 100;
779
780 // Overall validation status
781 validation.isValid = !validation.hasErrors;
782 };
783
784 /**
785 * Main validation function
786 */
787 export const validateSchemaData = (data) => {
788 const validation = {
789 hasErrors: false,
790 hasWarnings: false,
791 errors: [],
792 warnings: [],
793 suggestions: [],
794 fieldValidation: {},
795 validationSummary: {
796 validatedFields: 0,
797 totalFields: 0
798 }
799 };
800
801 // Validate each schema section that has actual form fields
802 validateOrganizationSchema(data, validation);
803 validateWebsiteSchema(data, validation);
804 validatePersonSchema(data, validation);
805 // validateLocalBusinessSchema(data, validation); // Removed - LocalBusiness is now informational only
806 validateSchemaConfigurationSettings(data, validation);
807
808 // Add note about deployment requirements
809 if (data.enabled_schema_types && data.enabled_schema_types.length > 0) {
810 const enabledTypes = data.enabled_schema_types;
811 const missingFormTypes = [];
812
813 if (enabledTypes.includes('SoftwareApplication')) {
814 missingFormTypes.push('SoftwareApplication');
815 }
816 if (enabledTypes.includes('BreadcrumbList')) {
817 missingFormTypes.push('BreadcrumbList');
818 }
819
820 if (missingFormTypes.length > 0) {
821 validation.warnings.push(__(`Note: ${missingFormTypes.join(', ')} schema types are enabled but don't have dedicated form fields. These will be auto-generated from site content during deployment.`, 'thinkrank'));
822 validation.hasWarnings = true;
823 }
824 }
825
826 return validation;
827 };
828