@@ -4,6 +4,7 @@ import { isInsideDevtools } from './devtools-dom-filter'
44import { useSeoStyles } from './use-seo-styles'
55import { pickSeverityClass , seoHealthTier } from './seo-severity'
66import type { SeoSeverity } from './seo-severity'
7+ import { sectionHealthScore } from './seo-section-summary'
78import type { SeoSectionSummary } from './seo-section-summary'
89
910type JsonLdValue = Record < string , unknown >
@@ -84,8 +85,6 @@ function entryUsesOnlySupportedTypes(entry: JsonLdEntry): boolean {
8485 return entry . types . every ( isSupportedSchemaType )
8586}
8687
87- const RESERVED_KEYS = new Set ( [ '@context' , '@type' , '@id' , '@graph' ] )
88-
8988function isRecord ( value : unknown ) : value is JsonLdValue {
9089 return typeof value === 'object' && value !== null && ! Array . isArray ( value )
9190}
@@ -134,9 +133,32 @@ const VALID_SCHEMA_CONTEXTS = new Set([
134133
135134function validateContext ( entity : JsonLdValue ) : Array < ValidationIssue > {
136135 const context = entity [ '@context' ]
137- if ( ! context ) {
136+ if ( context === undefined ) {
138137 return [ { severity : 'error' , message : 'Missing @context attribute.' } ]
139138 }
139+ if ( context === null || isRecord ( context ) ) {
140+ return [ ]
141+ }
142+ if ( Array . isArray ( context ) ) {
143+ const stringContexts = context . filter (
144+ ( value ) : value is string => typeof value === 'string' ,
145+ )
146+
147+ if (
148+ stringContexts . length > 0 &&
149+ ! stringContexts . some ( ( value ) => VALID_SCHEMA_CONTEXTS . has ( value ) )
150+ ) {
151+ return [
152+ {
153+ severity : 'error' ,
154+ message :
155+ 'Array @context is missing a schema.org context URL in its string entries.' ,
156+ } ,
157+ ]
158+ }
159+
160+ return [ ]
161+ }
140162 if ( typeof context === 'string' ) {
141163 if ( ! VALID_SCHEMA_CONTEXTS . has ( context ) ) {
142164 return [
@@ -151,7 +173,8 @@ function validateContext(entity: JsonLdValue): Array<ValidationIssue> {
151173 return [
152174 {
153175 severity : 'error' ,
154- message : 'Invalid @context type. Expected a string schema.org URL.' ,
176+ message :
177+ 'Invalid @context type. Expected a schema.org URL, object, array, or null.' ,
155178 } ,
156179 ]
157180}
@@ -202,20 +225,6 @@ function validateEntityByType(
202225 } )
203226 }
204227
205- const allowedSet = new Set ( [
206- ...rules . required ,
207- ...rules . recommended ,
208- ...rules . optional ,
209- ...RESERVED_KEYS ,
210- ] )
211- const unknownKeys = Object . keys ( entity ) . filter ( ( key ) => ! allowedSet . has ( key ) )
212- if ( unknownKeys . length > 0 ) {
213- issues . push ( {
214- severity : 'warning' ,
215- message : `Possible invalid attributes for ${ typeName } : ${ unknownKeys . join ( ', ' ) } ` ,
216- } )
217- }
218-
219228 return issues
220229}
221230
@@ -455,20 +464,7 @@ function sumMissingSchemaFieldCounts(entries: Array<JsonLdEntry>): {
455464 * small penalty so optional-field gaps match how the SEO overview weights them.
456465 */
457466function getJsonLdScore ( entries : Array < JsonLdEntry > ) : number {
458- let errors = 0
459- let warnings = 0
460- let infos = 0
461-
462- for ( const entry of entries ) {
463- for ( const issue of entry . issues ) {
464- if ( issue . severity === 'error' ) errors += 1
465- else if ( issue . severity === 'warning' ) warnings += 1
466- else infos += 1
467- }
468- }
469-
470- const penalty = Math . min ( 100 , errors * 20 + warnings * 10 + infos * 2 )
471- return Math . max ( 0 , 100 - penalty )
467+ return sectionHealthScore ( entries . flatMap ( ( entry ) => entry . issues ) )
472468}
473469
474470function JsonLdEntityPreviewCard ( props : { entity : JsonLdValue } ) {
0 commit comments