@nebulacomponents/citable 1.15.1 → 1.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +25 -0
- package/README.md +3 -3
- package/dist/universal/.agents/skills/citable/SKILL.md +5 -1
- package/dist/universal/.agents/skills/citable/VERSION +1 -1
- package/dist/universal/.agents/skills/citable/commands/README.md +3 -1
- package/dist/universal/.agents/skills/citable/commands/governance.md +12 -0
- package/dist/universal/.agents/skills/citable/commands/measurement.md +20 -0
- package/dist/universal/.agents/skills/citable/commands/reporting.md +16 -0
- package/dist/universal/.agents/skills/citable/manifest.json +14 -11
- package/dist/universal/.agents/skills/citable/schemas/experiment.schema.json +1 -1
- package/dist/universal/.agents/skills/citable/schemas/fleet.schema.json +93 -0
- package/dist/universal/.agents/skills/citable/schemas/funnel.schema.json +116 -0
- package/dist/universal/.agents/skills/citable/schemas/mcp-transport-envelope.schema.json +244 -0
- package/dist/universal/.agents/skills/citable/schemas/observation.schema.json +1 -1
- package/dist/universal/.claude/agents/citable/manifest.json +2 -2
- package/dist/universal/.claude/skills/citable/SKILL.md +5 -1
- package/dist/universal/.claude/skills/citable/VERSION +1 -1
- package/dist/universal/.claude/skills/citable/commands/README.md +3 -1
- package/dist/universal/.claude/skills/citable/commands/governance.md +12 -0
- package/dist/universal/.claude/skills/citable/commands/measurement.md +20 -0
- package/dist/universal/.claude/skills/citable/commands/reporting.md +16 -0
- package/dist/universal/.claude/skills/citable/manifest.json +14 -11
- package/dist/universal/.claude/skills/citable/schemas/experiment.schema.json +1 -1
- package/dist/universal/.claude/skills/citable/schemas/fleet.schema.json +93 -0
- package/dist/universal/.claude/skills/citable/schemas/funnel.schema.json +116 -0
- package/dist/universal/.claude/skills/citable/schemas/mcp-transport-envelope.schema.json +244 -0
- package/dist/universal/.claude/skills/citable/schemas/observation.schema.json +1 -1
- package/dist/universal/.cursor/skills/citable/SKILL.md +5 -1
- package/dist/universal/.cursor/skills/citable/VERSION +1 -1
- package/dist/universal/.cursor/skills/citable/commands/README.md +3 -1
- package/dist/universal/.cursor/skills/citable/commands/governance.md +12 -0
- package/dist/universal/.cursor/skills/citable/commands/measurement.md +20 -0
- package/dist/universal/.cursor/skills/citable/commands/reporting.md +16 -0
- package/dist/universal/.cursor/skills/citable/manifest.json +14 -11
- package/dist/universal/.cursor/skills/citable/schemas/experiment.schema.json +1 -1
- package/dist/universal/.cursor/skills/citable/schemas/fleet.schema.json +93 -0
- package/dist/universal/.cursor/skills/citable/schemas/funnel.schema.json +116 -0
- package/dist/universal/.cursor/skills/citable/schemas/mcp-transport-envelope.schema.json +244 -0
- package/dist/universal/.cursor/skills/citable/schemas/observation.schema.json +1 -1
- package/dist/universal/.gemini/skills/citable/SKILL.md +5 -1
- package/dist/universal/.gemini/skills/citable/VERSION +1 -1
- package/dist/universal/.gemini/skills/citable/commands/README.md +3 -1
- package/dist/universal/.gemini/skills/citable/commands/governance.md +12 -0
- package/dist/universal/.gemini/skills/citable/commands/measurement.md +20 -0
- package/dist/universal/.gemini/skills/citable/commands/reporting.md +16 -0
- package/dist/universal/.gemini/skills/citable/manifest.json +14 -11
- package/dist/universal/.gemini/skills/citable/schemas/experiment.schema.json +1 -1
- package/dist/universal/.gemini/skills/citable/schemas/fleet.schema.json +93 -0
- package/dist/universal/.gemini/skills/citable/schemas/funnel.schema.json +116 -0
- package/dist/universal/.gemini/skills/citable/schemas/mcp-transport-envelope.schema.json +244 -0
- package/dist/universal/.gemini/skills/citable/schemas/observation.schema.json +1 -1
- package/dist/universal/.github/skills/citable/SKILL.md +5 -1
- package/dist/universal/.github/skills/citable/VERSION +1 -1
- package/dist/universal/.github/skills/citable/commands/README.md +3 -1
- package/dist/universal/.github/skills/citable/commands/governance.md +12 -0
- package/dist/universal/.github/skills/citable/commands/measurement.md +20 -0
- package/dist/universal/.github/skills/citable/commands/reporting.md +16 -0
- package/dist/universal/.github/skills/citable/manifest.json +14 -11
- package/dist/universal/.github/skills/citable/schemas/experiment.schema.json +1 -1
- package/dist/universal/.github/skills/citable/schemas/fleet.schema.json +93 -0
- package/dist/universal/.github/skills/citable/schemas/funnel.schema.json +116 -0
- package/dist/universal/.github/skills/citable/schemas/mcp-transport-envelope.schema.json +244 -0
- package/dist/universal/.github/skills/citable/schemas/observation.schema.json +1 -1
- package/dist/universal/.kiro/skills/citable/SKILL.md +5 -1
- package/dist/universal/.kiro/skills/citable/VERSION +1 -1
- package/dist/universal/.kiro/skills/citable/commands/README.md +3 -1
- package/dist/universal/.kiro/skills/citable/commands/governance.md +12 -0
- package/dist/universal/.kiro/skills/citable/commands/measurement.md +20 -0
- package/dist/universal/.kiro/skills/citable/commands/reporting.md +16 -0
- package/dist/universal/.kiro/skills/citable/manifest.json +14 -11
- package/dist/universal/.kiro/skills/citable/schemas/experiment.schema.json +1 -1
- package/dist/universal/.kiro/skills/citable/schemas/fleet.schema.json +93 -0
- package/dist/universal/.kiro/skills/citable/schemas/funnel.schema.json +116 -0
- package/dist/universal/.kiro/skills/citable/schemas/mcp-transport-envelope.schema.json +244 -0
- package/dist/universal/.kiro/skills/citable/schemas/observation.schema.json +1 -1
- package/dist/universal/.opencode/skills/citable/SKILL.md +5 -1
- package/dist/universal/.opencode/skills/citable/VERSION +1 -1
- package/dist/universal/.opencode/skills/citable/commands/README.md +3 -1
- package/dist/universal/.opencode/skills/citable/commands/governance.md +12 -0
- package/dist/universal/.opencode/skills/citable/commands/measurement.md +20 -0
- package/dist/universal/.opencode/skills/citable/commands/reporting.md +16 -0
- package/dist/universal/.opencode/skills/citable/manifest.json +14 -11
- package/dist/universal/.opencode/skills/citable/schemas/experiment.schema.json +1 -1
- package/dist/universal/.opencode/skills/citable/schemas/fleet.schema.json +93 -0
- package/dist/universal/.opencode/skills/citable/schemas/funnel.schema.json +116 -0
- package/dist/universal/.opencode/skills/citable/schemas/mcp-transport-envelope.schema.json +244 -0
- package/dist/universal/.opencode/skills/citable/schemas/observation.schema.json +1 -1
- package/dist/universal/.pi/agent/skills/citable/SKILL.md +5 -1
- package/dist/universal/.pi/agent/skills/citable/VERSION +1 -1
- package/dist/universal/.pi/agent/skills/citable/commands/README.md +3 -1
- package/dist/universal/.pi/agent/skills/citable/commands/governance.md +12 -0
- package/dist/universal/.pi/agent/skills/citable/commands/measurement.md +20 -0
- package/dist/universal/.pi/agent/skills/citable/commands/reporting.md +16 -0
- package/dist/universal/.pi/agent/skills/citable/manifest.json +14 -11
- package/dist/universal/.pi/agent/skills/citable/schemas/experiment.schema.json +1 -1
- package/dist/universal/.pi/agent/skills/citable/schemas/fleet.schema.json +93 -0
- package/dist/universal/.pi/agent/skills/citable/schemas/funnel.schema.json +116 -0
- package/dist/universal/.pi/agent/skills/citable/schemas/mcp-transport-envelope.schema.json +244 -0
- package/dist/universal/.pi/agent/skills/citable/schemas/observation.schema.json +1 -1
- package/dist/universal/.qoder/skills/citable/SKILL.md +5 -1
- package/dist/universal/.qoder/skills/citable/VERSION +1 -1
- package/dist/universal/.qoder/skills/citable/commands/README.md +3 -1
- package/dist/universal/.qoder/skills/citable/commands/governance.md +12 -0
- package/dist/universal/.qoder/skills/citable/commands/measurement.md +20 -0
- package/dist/universal/.qoder/skills/citable/commands/reporting.md +16 -0
- package/dist/universal/.qoder/skills/citable/manifest.json +14 -11
- package/dist/universal/.qoder/skills/citable/schemas/experiment.schema.json +1 -1
- package/dist/universal/.qoder/skills/citable/schemas/fleet.schema.json +93 -0
- package/dist/universal/.qoder/skills/citable/schemas/funnel.schema.json +116 -0
- package/dist/universal/.qoder/skills/citable/schemas/mcp-transport-envelope.schema.json +244 -0
- package/dist/universal/.qoder/skills/citable/schemas/observation.schema.json +1 -1
- package/dist/universal/.rovodev/skills/citable/SKILL.md +5 -1
- package/dist/universal/.rovodev/skills/citable/VERSION +1 -1
- package/dist/universal/.rovodev/skills/citable/commands/README.md +3 -1
- package/dist/universal/.rovodev/skills/citable/commands/governance.md +12 -0
- package/dist/universal/.rovodev/skills/citable/commands/measurement.md +20 -0
- package/dist/universal/.rovodev/skills/citable/commands/reporting.md +16 -0
- package/dist/universal/.rovodev/skills/citable/manifest.json +14 -11
- package/dist/universal/.rovodev/skills/citable/schemas/experiment.schema.json +1 -1
- package/dist/universal/.rovodev/skills/citable/schemas/fleet.schema.json +93 -0
- package/dist/universal/.rovodev/skills/citable/schemas/funnel.schema.json +116 -0
- package/dist/universal/.rovodev/skills/citable/schemas/mcp-transport-envelope.schema.json +244 -0
- package/dist/universal/.rovodev/skills/citable/schemas/observation.schema.json +1 -1
- package/dist/universal/.trae/skills/citable/SKILL.md +5 -1
- package/dist/universal/.trae/skills/citable/VERSION +1 -1
- package/dist/universal/.trae/skills/citable/commands/README.md +3 -1
- package/dist/universal/.trae/skills/citable/commands/governance.md +12 -0
- package/dist/universal/.trae/skills/citable/commands/measurement.md +20 -0
- package/dist/universal/.trae/skills/citable/commands/reporting.md +16 -0
- package/dist/universal/.trae/skills/citable/manifest.json +14 -11
- package/dist/universal/.trae/skills/citable/schemas/experiment.schema.json +1 -1
- package/dist/universal/.trae/skills/citable/schemas/fleet.schema.json +93 -0
- package/dist/universal/.trae/skills/citable/schemas/funnel.schema.json +116 -0
- package/dist/universal/.trae/skills/citable/schemas/mcp-transport-envelope.schema.json +244 -0
- package/dist/universal/.trae/skills/citable/schemas/observation.schema.json +1 -1
- package/dist/universal/.trae-cn/skills/citable/SKILL.md +5 -1
- package/dist/universal/.trae-cn/skills/citable/VERSION +1 -1
- package/dist/universal/.trae-cn/skills/citable/commands/README.md +3 -1
- package/dist/universal/.trae-cn/skills/citable/commands/governance.md +12 -0
- package/dist/universal/.trae-cn/skills/citable/commands/measurement.md +20 -0
- package/dist/universal/.trae-cn/skills/citable/commands/reporting.md +16 -0
- package/dist/universal/.trae-cn/skills/citable/manifest.json +14 -11
- package/dist/universal/.trae-cn/skills/citable/schemas/experiment.schema.json +1 -1
- package/dist/universal/.trae-cn/skills/citable/schemas/fleet.schema.json +93 -0
- package/dist/universal/.trae-cn/skills/citable/schemas/funnel.schema.json +116 -0
- package/dist/universal/.trae-cn/skills/citable/schemas/mcp-transport-envelope.schema.json +244 -0
- package/dist/universal/.trae-cn/skills/citable/schemas/observation.schema.json +1 -1
- package/dist/universal/manifest.json +172 -136
- package/package.json +1 -1
- package/schemas/experiment.schema.json +1 -1
- package/schemas/fleet.schema.json +93 -0
- package/schemas/funnel.schema.json +116 -0
- package/schemas/mcp-transport-envelope.schema.json +244 -0
- package/schemas/observation.schema.json +1 -1
- package/skill/SKILL.md +5 -1
- package/skill/commands/README.md +3 -1
- package/skill/commands/governance.md +12 -0
- package/skill/commands/measurement.md +20 -0
- package/skill/commands/reporting.md +16 -0
- package/src/cli/index.js +383 -8
- package/src/commands/attributeImpact.js +54 -0
- package/src/commands/connect.js +3 -0
- package/src/commands/edgeRules.js +120 -0
- package/src/commands/exceptions.js +241 -0
- package/src/commands/fleet.js +104 -0
- package/src/commands/generateLlmsTxt.js +111 -0
- package/src/commands/inspectAeo.js +74 -0
- package/src/commands/inspectCro.js +82 -0
- package/src/commands/inspectGeo.js +52 -0
- package/src/commands/inspectSerp.js +317 -0
- package/src/commands/lintComponents.js +132 -0
- package/src/commands/monitor.js +138 -0
- package/src/commands/observe.js +6 -1
- package/src/commands/planExperiment.js +68 -0
- package/src/commands/probe.js +70 -0
- package/src/commands/reportConsensus.js +48 -0
- package/src/commands/testFunnel.js +107 -0
- package/src/connectors/index.js +6 -1
- package/src/connectors/indexnow.js +431 -0
- package/src/connectors/mcp/allowlist.js +107 -0
- package/src/connectors/mcp/client.js +239 -0
- package/src/connectors/mcp/diagnostics.js +214 -0
- package/src/connectors/mcp/gscServer.js +155 -0
- package/src/connectors/mcp/http.js +130 -0
- package/src/connectors/mcp/index.js +7 -0
- package/src/connectors/mcp/pilot.js +138 -0
- package/src/connectors/mcp/pilotServer.js +80 -0
- package/src/connectors/mcp/sanitizer.js +38 -0
- package/src/connectors/mcp/stdio.js +178 -0
- package/src/crawler/ipRanges.js +161 -0
- package/src/detectors/agent.js +127 -0
- package/src/detectors/ans.js +282 -0
- package/src/detectors/crawl.js +46 -0
- package/src/detectors/cro.js +777 -0
- package/src/detectors/cwv.js +38 -1
- package/src/detectors/evd.js +101 -0
- package/src/detectors/framework.js +1 -1
- package/src/detectors/geoReco.js +408 -0
- package/src/detectors/index.js +4 -2
- package/src/detectors/link.js +112 -1
- package/src/detectors/page.js +101 -0
- package/src/detectors/schemaData.js +529 -0
- package/src/detectors/tech.js +306 -0
- package/src/extractor/page.js +186 -2
- package/src/extractor/site.js +9 -2
- package/src/installer/index.js +45 -2
- package/src/monitoring/alertDelivery.js +101 -7
- package/src/observations/attribution.js +217 -0
- package/src/registries/index.js +14 -0
- package/src/reporting/consensus.js +416 -0
- package/src/reporting/executiveExport.js +132 -0
|
@@ -246,4 +246,533 @@ D.push(defineDetector({
|
|
|
246
246
|
},
|
|
247
247
|
}));
|
|
248
248
|
|
|
249
|
+
D.push(defineDetector({
|
|
250
|
+
id: 'SCHEMA-009', name: 'Structured data entity facts unsupported by visible content', namespace: 'SCHEMA',
|
|
251
|
+
description: 'Material entity facts in JSON-LD (such as contact phone, address, pricing, or founding year) are absent from visible page content.',
|
|
252
|
+
discipline: ['seo', 'aeo', 'geo'], severity: 'high', deterministic: true, requires: ['site'],
|
|
253
|
+
impact: { representation: 'high', legal: 'medium' },
|
|
254
|
+
applicable_requirement: 'Premise 3.4: structured data must match visible content; Google Structured Data General Guidelines: no invisible marked-up facts',
|
|
255
|
+
remediation: 'Ensure all facts declared in Schema.org JSON-LD are visibly presented to users in the rendered page text, or remove the ungrounded markup.',
|
|
256
|
+
verification: 'Confirm every material schema property has matching visible text on the page.',
|
|
257
|
+
check(ctx) {
|
|
258
|
+
const hits = [];
|
|
259
|
+
for (const p of ctx.site.pages) {
|
|
260
|
+
const pageText = (p.text || '').toLowerCase();
|
|
261
|
+
const pageDigits = (p.text || '').replace(/\D/g, '');
|
|
262
|
+
for (const j of p.jsonLd) {
|
|
263
|
+
for (const b of j.blocks) {
|
|
264
|
+
if (!b || typeof b !== 'object') continue;
|
|
265
|
+
// Check telephone
|
|
266
|
+
if (b.telephone && typeof b.telephone === 'string') {
|
|
267
|
+
const telDigits = b.telephone.replace(/\D/g, '');
|
|
268
|
+
if (telDigits.length >= 7 && !pageDigits.includes(telDigits)) {
|
|
269
|
+
hits.push({
|
|
270
|
+
subject: { type: 'schema_block', identifier: `${p.url}#telephone`, url: p.url },
|
|
271
|
+
summary: `Schema telephone "${b.telephone}" not found in visible page content`,
|
|
272
|
+
evidence: [`schema declares telephone: ${b.telephone}`, 'visible text contains no matching phone number'],
|
|
273
|
+
captured: b.telephone,
|
|
274
|
+
expected: 'matching visible phone number',
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
// Check email
|
|
279
|
+
if (b.email && typeof b.email === 'string') {
|
|
280
|
+
const email = b.email.trim().toLowerCase();
|
|
281
|
+
if (email.includes('@') && !pageText.includes(email)) {
|
|
282
|
+
hits.push({
|
|
283
|
+
subject: { type: 'schema_block', identifier: `${p.url}#email`, url: p.url },
|
|
284
|
+
summary: `Schema email "${b.email}" not found in visible page content`,
|
|
285
|
+
evidence: [`schema declares email: ${b.email}`, 'visible text contains no matching email address'],
|
|
286
|
+
captured: b.email,
|
|
287
|
+
expected: 'matching visible email address',
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
// Check postalAddress
|
|
292
|
+
const address = b.address;
|
|
293
|
+
if (address && typeof address === 'object') {
|
|
294
|
+
if (address.postalCode && typeof address.postalCode === 'string') {
|
|
295
|
+
const zip = address.postalCode.trim().toLowerCase();
|
|
296
|
+
if (zip.length >= 3 && !pageText.includes(zip)) {
|
|
297
|
+
hits.push({
|
|
298
|
+
subject: { type: 'schema_block', identifier: `${p.url}#postalCode`, url: p.url },
|
|
299
|
+
summary: `Schema postalCode "${address.postalCode}" not found in visible page content`,
|
|
300
|
+
evidence: [`schema declares postalCode: ${address.postalCode}`, 'visible text contains no matching postal code'],
|
|
301
|
+
captured: address.postalCode,
|
|
302
|
+
expected: 'matching visible postal code',
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
// Check price in offers
|
|
308
|
+
if (b.offers) {
|
|
309
|
+
const offers = [].concat(b.offers);
|
|
310
|
+
for (const off of offers) {
|
|
311
|
+
if (off && off.price !== undefined && off.price !== null) {
|
|
312
|
+
const priceStr = String(off.price);
|
|
313
|
+
if (priceStr && !pageText.includes(priceStr)) {
|
|
314
|
+
hits.push({
|
|
315
|
+
subject: { type: 'schema_block', identifier: `${p.url}#price`, url: p.url },
|
|
316
|
+
summary: `Schema offer price "${off.price}" not found in visible page content`,
|
|
317
|
+
evidence: [`schema declares price: ${off.price}`, 'visible text contains no matching price figure'],
|
|
318
|
+
captured: off.price,
|
|
319
|
+
expected: 'matching visible price',
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
return hits;
|
|
329
|
+
},
|
|
330
|
+
}));
|
|
331
|
+
|
|
332
|
+
D.push(defineDetector({
|
|
333
|
+
id: 'SCHEMA-010', name: 'Dangling or circular @id reference in entity graph', namespace: 'SCHEMA',
|
|
334
|
+
description: 'A JSON-LD node references an @id URI that is not defined on the site or creates a circular self-reference.',
|
|
335
|
+
discipline: ['geo', 'seo'], severity: 'high', deterministic: true, requires: ['site'],
|
|
336
|
+
impact: { representation: 'high' },
|
|
337
|
+
applicable_requirement: 'GEO §9 graph consistency and entity resolution; Schema.org graph reference integrity',
|
|
338
|
+
remediation: 'Ensure all @id references point to defined entity nodes on the site, and resolve circular reference loops.',
|
|
339
|
+
verification: 'All referenced @id targets exist in the graph and graph references are acyclic.',
|
|
340
|
+
check(ctx) {
|
|
341
|
+
const hits = [];
|
|
342
|
+
const siteDefinedIds = new Set();
|
|
343
|
+
|
|
344
|
+
// Collect all defined @id values across all blocks on the site
|
|
345
|
+
for (const p of ctx.site.pages) {
|
|
346
|
+
for (const j of p.jsonLd) {
|
|
347
|
+
for (const b of j.blocks) {
|
|
348
|
+
if (b && b['@id'] && typeof b['@id'] === 'string') {
|
|
349
|
+
siteDefinedIds.add(b['@id']);
|
|
350
|
+
try {
|
|
351
|
+
const u = new URL(b['@id'], p.url);
|
|
352
|
+
siteDefinedIds.add(ctx.site.normalize(u.origin + u.pathname + u.search) + u.hash);
|
|
353
|
+
} catch {}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Inspect references within each block
|
|
360
|
+
for (const p of ctx.site.pages) {
|
|
361
|
+
for (const j of p.jsonLd) {
|
|
362
|
+
for (const b of j.blocks) {
|
|
363
|
+
if (!b || typeof b !== 'object') continue;
|
|
364
|
+
const currentId = b['@id'];
|
|
365
|
+
|
|
366
|
+
const inspectObj = (obj, propPath = '') => {
|
|
367
|
+
if (!obj || typeof obj !== 'object') return;
|
|
368
|
+
for (const [key, val] of Object.entries(obj)) {
|
|
369
|
+
if (key === '@id') continue;
|
|
370
|
+
if (val && typeof val === 'object') {
|
|
371
|
+
if (val['@id'] && typeof val['@id'] === 'string') {
|
|
372
|
+
const targetId = val['@id'];
|
|
373
|
+
// 1. Check circular self-reference
|
|
374
|
+
if (currentId && targetId === currentId) {
|
|
375
|
+
hits.push({
|
|
376
|
+
subject: { type: 'schema_block', identifier: `${p.url}#${propPath ? propPath + '.' : ''}${key}`, url: p.url },
|
|
377
|
+
summary: `Circular @id self-reference detected on ${currentId}`,
|
|
378
|
+
evidence: [`node ${currentId} references itself in property "${key}"`],
|
|
379
|
+
captured: targetId,
|
|
380
|
+
expected: 'acyclic reference to distinct entity',
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
// 2. Check dangling reference if it is an internal URI or fragment
|
|
384
|
+
const isInternal = targetId.startsWith('#') || (ctx.site.base_url && targetId.startsWith(ctx.site.base_url)) || (ctx.site.baseUrl && targetId.startsWith(ctx.site.baseUrl)) || targetId.startsWith('http://') || targetId.startsWith('https://');
|
|
385
|
+
if (isInternal) {
|
|
386
|
+
let normTarget = targetId;
|
|
387
|
+
let isSchemaOrg = false;
|
|
388
|
+
try {
|
|
389
|
+
const u = new URL(targetId, p.url);
|
|
390
|
+
normTarget = ctx.site.normalize(u.origin + u.pathname + u.search) + u.hash;
|
|
391
|
+
isSchemaOrg = u.hostname === 'schema.org' || u.hostname === 'www.schema.org';
|
|
392
|
+
} catch {}
|
|
393
|
+
if (!siteDefinedIds.has(targetId) && !siteDefinedIds.has(normTarget)) {
|
|
394
|
+
if (!isSchemaOrg) {
|
|
395
|
+
hits.push({
|
|
396
|
+
subject: { type: 'schema_block', identifier: `${p.url}#${propPath ? propPath + '.' : ''}${key}`, url: p.url },
|
|
397
|
+
summary: `Dangling @id reference "${targetId}" not defined in site graph`,
|
|
398
|
+
evidence: [`property "${key}" references @id: ${targetId}`, 'no entity node with this @id is defined on the site'],
|
|
399
|
+
captured: targetId,
|
|
400
|
+
expected: 'defined entity @id in graph',
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
inspectObj(val, propPath ? `${propPath}.${key}` : key);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
};
|
|
410
|
+
|
|
411
|
+
inspectObj(b);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
return hits;
|
|
416
|
+
},
|
|
417
|
+
}));
|
|
418
|
+
|
|
419
|
+
D.push(defineDetector({
|
|
420
|
+
id: 'SCHEMA-011', name: 'BreadcrumbList schema broken or unanchored', namespace: 'SCHEMA',
|
|
421
|
+
description: 'BreadcrumbList structured data has invalid sequential positions, missing or empty item URLs/names, or broken hierarchical continuity.',
|
|
422
|
+
discipline: ['seo', 'aeo'], severity: 'medium', deterministic: true, requires: ['site'],
|
|
423
|
+
impact: { representation: 'medium', ranking: 'low' },
|
|
424
|
+
applicable_requirement: 'SEO §7 schema validation; Google Search Central Breadcrumb structured data specification',
|
|
425
|
+
remediation: 'Ensure BreadcrumbList itemListElement contains sequential 1-based positions, each item has a non-empty name and item target URL, and matches the hierarchical navigation.',
|
|
426
|
+
verification: 'Verify BreadcrumbList elements have position 1..N with valid names and target URLs.',
|
|
427
|
+
check(ctx) {
|
|
428
|
+
const hits = [];
|
|
429
|
+
for (const p of indexTargets(ctx)) {
|
|
430
|
+
if (!p.jsonLd) continue;
|
|
431
|
+
for (const j of p.jsonLd) {
|
|
432
|
+
if (!j.blocks) continue;
|
|
433
|
+
for (const b of j.blocks) {
|
|
434
|
+
if (!b || typeof b !== 'object') continue;
|
|
435
|
+
const type = [].concat(b['@type'] || []).join(',');
|
|
436
|
+
if (!/BreadcrumbList/i.test(type)) continue;
|
|
437
|
+
|
|
438
|
+
const items = Array.isArray(b.itemListElement)
|
|
439
|
+
? b.itemListElement
|
|
440
|
+
: (b.itemListElement ? [b.itemListElement] : []);
|
|
441
|
+
|
|
442
|
+
if (items.length === 0) {
|
|
443
|
+
hits.push({
|
|
444
|
+
subject: { type: 'schema_block', identifier: `${p.url}#BreadcrumbList`, url: p.url },
|
|
445
|
+
summary: 'BreadcrumbList schema has empty or missing itemListElement',
|
|
446
|
+
evidence: ['itemListElement is empty or not an array'],
|
|
447
|
+
});
|
|
448
|
+
continue;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
let expectedPos = 1;
|
|
452
|
+
let posBroken = false;
|
|
453
|
+
let missingField = false;
|
|
454
|
+
|
|
455
|
+
for (let i = 0; i < items.length; i++) {
|
|
456
|
+
const el = items[i];
|
|
457
|
+
if (!el || typeof el !== 'object') {
|
|
458
|
+
missingField = true;
|
|
459
|
+
continue;
|
|
460
|
+
}
|
|
461
|
+
const pos = Number(el.position);
|
|
462
|
+
if (Number.isNaN(pos) || pos !== expectedPos) {
|
|
463
|
+
posBroken = true;
|
|
464
|
+
}
|
|
465
|
+
expectedPos++;
|
|
466
|
+
|
|
467
|
+
const name = el.name || (typeof el.item === 'object' ? el.item?.name : null);
|
|
468
|
+
const itemUrl = typeof el.item === 'string'
|
|
469
|
+
? el.item
|
|
470
|
+
: (typeof el.item === 'object' ? (el.item?.['@id'] || el.item?.url) : null) || el.url;
|
|
471
|
+
|
|
472
|
+
const isLast = i === items.length - 1;
|
|
473
|
+
if (!name || (typeof name === 'string' && !name.trim())) {
|
|
474
|
+
missingField = true;
|
|
475
|
+
}
|
|
476
|
+
if (!isLast && (!itemUrl || (typeof itemUrl === 'string' && !itemUrl.trim()))) {
|
|
477
|
+
missingField = true;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
if (posBroken) {
|
|
482
|
+
hits.push({
|
|
483
|
+
subject: { type: 'schema_block', identifier: `${p.url}#BreadcrumbList`, url: p.url },
|
|
484
|
+
summary: 'BreadcrumbList positions are not sequential 1-based integers',
|
|
485
|
+
evidence: [`declared positions: ${items.map((it) => it?.position).join(', ')}`],
|
|
486
|
+
captured: items.map((it) => it?.position),
|
|
487
|
+
expected: items.map((_, i) => i + 1),
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
if (missingField) {
|
|
492
|
+
hits.push({
|
|
493
|
+
subject: { type: 'schema_block', identifier: `${p.url}#BreadcrumbList`, url: p.url },
|
|
494
|
+
summary: 'BreadcrumbList item missing required name or item URL target',
|
|
495
|
+
evidence: ['one or more ListItem entries lack a non-empty name or navigation item URL'],
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
return hits;
|
|
502
|
+
},
|
|
503
|
+
}));
|
|
504
|
+
|
|
505
|
+
D.push(defineDetector({
|
|
506
|
+
id: 'SCHEMA-012', name: 'Temporal contradiction between schema dates, visible text, and HTTP headers', namespace: 'SCHEMA',
|
|
507
|
+
description: 'Structured data dates contradict visible page text timestamps or HTTP Last-Modified headers, undermining search engine freshness consensus.',
|
|
508
|
+
discipline: ['seo', 'aeo', 'geo'], severity: 'medium', deterministic: true, requires: ['site'],
|
|
509
|
+
impact: { representation: 'medium', citation: 'low' },
|
|
510
|
+
applicable_requirement: 'Premise 3.4: structured data must match visible content; SEO §7 freshness consensus; AEO §8 version evidence',
|
|
511
|
+
remediation: 'Synchronize JSON-LD dateModified/datePublished with visible page timestamps and HTTP Last-Modified headers.',
|
|
512
|
+
verification: 'Confirm structured data dates match visible page dates and do not contradict server headers.',
|
|
513
|
+
check(ctx) {
|
|
514
|
+
const hits = [];
|
|
515
|
+
const DATE_TEXT_RX = /\b(?:last\s+updated|updated\s+on|modified\s+on|published\s+on|date):\s*([A-Za-z]+\s+\d{1,2},?\s+\d{4}|\d{4}-\d{2}-\d{2})\b/i;
|
|
516
|
+
|
|
517
|
+
for (const p of indexTargets(ctx)) {
|
|
518
|
+
if (!p.jsonLd) continue;
|
|
519
|
+
|
|
520
|
+
let visibleDate = null;
|
|
521
|
+
if (p.rawHtml || p.paragraphs) {
|
|
522
|
+
const textSources = [...(p.paragraphs || []), ...(p.headings || []).map((h) => h.text || '')];
|
|
523
|
+
for (const s of textSources) {
|
|
524
|
+
const m = DATE_TEXT_RX.exec(s);
|
|
525
|
+
if (m) {
|
|
526
|
+
const d = new Date(m[1]);
|
|
527
|
+
if (!Number.isNaN(d.getTime())) {
|
|
528
|
+
visibleDate = { str: m[1], date: d };
|
|
529
|
+
break;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
const lastModHeader = p.headers?.['last-modified'] || p.headers?.['Last-Modified'];
|
|
536
|
+
const headerDate = lastModHeader ? new Date(lastModHeader) : null;
|
|
537
|
+
const validHeaderDate = headerDate && !Number.isNaN(headerDate.getTime()) ? headerDate : null;
|
|
538
|
+
|
|
539
|
+
for (const j of p.jsonLd) {
|
|
540
|
+
if (!j.blocks) continue;
|
|
541
|
+
for (const b of j.blocks) {
|
|
542
|
+
if (!b || typeof b !== 'object') continue;
|
|
543
|
+
const sPub = b.datePublished ? new Date(b.datePublished) : null;
|
|
544
|
+
const sMod = b.dateModified ? new Date(b.dateModified) : null;
|
|
545
|
+
const validPub = sPub && !Number.isNaN(sPub.getTime()) ? sPub : null;
|
|
546
|
+
const validMod = sMod && !Number.isNaN(sMod.getTime()) ? sMod : null;
|
|
547
|
+
const primarySchemaDate = validMod || validPub;
|
|
548
|
+
const primaryDateStr = b.dateModified || b.datePublished;
|
|
549
|
+
|
|
550
|
+
if (!primarySchemaDate) continue;
|
|
551
|
+
|
|
552
|
+
// 1. Check against visible text date
|
|
553
|
+
if (visibleDate) {
|
|
554
|
+
const diffDays = Math.abs((primarySchemaDate.getTime() - visibleDate.date.getTime()) / (1000 * 60 * 60 * 24));
|
|
555
|
+
if (diffDays > 90) {
|
|
556
|
+
hits.push({
|
|
557
|
+
subject: { type: 'schema_block', identifier: `${p.url}#dateConsensus`, url: p.url },
|
|
558
|
+
summary: `Schema date (${primaryDateStr.slice(0, 10)}) contradicts visible date (${visibleDate.str}) by ${Math.round(diffDays)} days`,
|
|
559
|
+
evidence: [
|
|
560
|
+
`schema date: ${primaryDateStr}`,
|
|
561
|
+
`visible page date: ${visibleDate.str}`,
|
|
562
|
+
`difference: ${Math.round(diffDays)} days`,
|
|
563
|
+
],
|
|
564
|
+
captured: primaryDateStr.slice(0, 10),
|
|
565
|
+
expected: visibleDate.date.toISOString().slice(0, 10),
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// 2. Check against HTTP Last-Modified header
|
|
571
|
+
if (validHeaderDate && validMod) {
|
|
572
|
+
const diffDaysHeader = Math.abs((validMod.getTime() - validHeaderDate.getTime()) / (1000 * 60 * 60 * 24));
|
|
573
|
+
if (diffDaysHeader > 180) {
|
|
574
|
+
hits.push({
|
|
575
|
+
subject: { type: 'schema_block', identifier: `${p.url}#headerConsensus`, url: p.url },
|
|
576
|
+
summary: `Schema dateModified (${b.dateModified.slice(0, 10)}) contradicts HTTP Last-Modified header (${validHeaderDate.toISOString().slice(0, 10)}) by ${Math.round(diffDaysHeader)} days`,
|
|
577
|
+
evidence: [
|
|
578
|
+
`schema dateModified: ${b.dateModified}`,
|
|
579
|
+
`HTTP Last-Modified: ${lastModHeader}`,
|
|
580
|
+
`difference: ${Math.round(diffDaysHeader)} days`,
|
|
581
|
+
],
|
|
582
|
+
captured: b.dateModified.slice(0, 10),
|
|
583
|
+
expected: validHeaderDate.toISOString().slice(0, 10),
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
return hits;
|
|
591
|
+
},
|
|
592
|
+
}));
|
|
593
|
+
|
|
594
|
+
D.push(defineDetector({
|
|
595
|
+
id: 'SCHEMA-013', name: 'VideoObject schema missing critical SERP playback prerequisites', namespace: 'SCHEMA',
|
|
596
|
+
description: 'A VideoObject JSON-LD block is missing required attributes for Google SERP rich video display (name, description, uploadDate, thumbnailUrl, contentUrl or embedUrl).',
|
|
597
|
+
discipline: ['seo', 'aeo'], severity: 'medium', deterministic: true, requires: ['site'],
|
|
598
|
+
impact: { representation: 'medium', ranking: 'low' },
|
|
599
|
+
applicable_requirement: 'Google Search Central Video structured data guidelines; SEO §7 schema validation',
|
|
600
|
+
remediation: 'Add name, description, uploadDate, thumbnailUrl, and either contentUrl or embedUrl to the VideoObject markup.',
|
|
601
|
+
verification: 'Verify VideoObject schema includes all Google Search required playback fields.',
|
|
602
|
+
check(ctx) {
|
|
603
|
+
const hits = [];
|
|
604
|
+
for (const p of indexTargets(ctx)) {
|
|
605
|
+
for (const j of p.jsonLd || []) {
|
|
606
|
+
for (const b of j.blocks || []) {
|
|
607
|
+
if (!b || typeof b !== 'object') continue;
|
|
608
|
+
const type = [].concat(b['@type'] || []).join(',');
|
|
609
|
+
if (!/VideoObject/i.test(type)) continue;
|
|
610
|
+
|
|
611
|
+
const missing = [];
|
|
612
|
+
if (!b.name) missing.push('name');
|
|
613
|
+
if (!b.description) missing.push('description');
|
|
614
|
+
if (!b.uploadDate) missing.push('uploadDate');
|
|
615
|
+
if (!b.thumbnailUrl && !b.thumbnail) missing.push('thumbnailUrl');
|
|
616
|
+
if (!b.contentUrl && !b.embedUrl) missing.push('contentUrl or embedUrl');
|
|
617
|
+
|
|
618
|
+
if (missing.length > 0) {
|
|
619
|
+
hits.push({
|
|
620
|
+
subject: { type: 'schema_block', identifier: `${p.url}#VideoObject`, url: p.url },
|
|
621
|
+
summary: `VideoObject schema missing required Google SERP playback fields: ${missing.join(', ')}`,
|
|
622
|
+
evidence: [
|
|
623
|
+
`video name: ${b.name || 'missing'}`,
|
|
624
|
+
`missing required fields: ${missing.join(', ')}`,
|
|
625
|
+
],
|
|
626
|
+
captured: missing,
|
|
627
|
+
expected: [],
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
return hits;
|
|
634
|
+
},
|
|
635
|
+
}));
|
|
636
|
+
|
|
637
|
+
D.push(defineDetector({
|
|
638
|
+
id: 'SCHEMA-014', name: 'Organization or LocalBusiness schema missing authoritative identity attributes', namespace: 'SCHEMA',
|
|
639
|
+
description: 'An Organization or LocalBusiness JSON-LD block lacks essential identity signals for Google Knowledge Graph or SERP inclusion (name, url, logo, or contactPoint/address).',
|
|
640
|
+
discipline: ['seo', 'aeo', 'geo'], severity: 'low', deterministic: true, requires: ['site'],
|
|
641
|
+
impact: { representation: 'medium', ranking: 'none' },
|
|
642
|
+
applicable_requirement: 'Google Search Central Organization structured data; GEO §9 knowledge graph entity resolution',
|
|
643
|
+
remediation: 'Include name, url, logo, and at least one contactPoint or address in Organization structured data.',
|
|
644
|
+
verification: 'Ensure Organization and LocalBusiness blocks declare logo, url, and contact anchors.',
|
|
645
|
+
check(ctx) {
|
|
646
|
+
const hits = [];
|
|
647
|
+
for (const p of indexTargets(ctx)) {
|
|
648
|
+
for (const j of p.jsonLd || []) {
|
|
649
|
+
for (const b of j.blocks || []) {
|
|
650
|
+
if (!b || typeof b !== 'object') continue;
|
|
651
|
+
const type = [].concat(b['@type'] || []).join(',');
|
|
652
|
+
if (!/Organization|LocalBusiness|Corporation/i.test(type)) continue;
|
|
653
|
+
|
|
654
|
+
const missing = [];
|
|
655
|
+
if (!b.name) missing.push('name');
|
|
656
|
+
if (!b.url) missing.push('url');
|
|
657
|
+
if (!b.logo && !b.image) missing.push('logo/image');
|
|
658
|
+
const hasAnchor = b.contactPoint || b.address || (Array.isArray(b.sameAs) && b.sameAs.length > 0);
|
|
659
|
+
if (!hasAnchor) missing.push('contactPoint, address, or sameAs');
|
|
660
|
+
|
|
661
|
+
// Flag when missing multiple essential identity anchors
|
|
662
|
+
if (missing.length >= 2) {
|
|
663
|
+
hits.push({
|
|
664
|
+
subject: { type: 'schema_block', identifier: `${p.url}#${type}`, url: p.url },
|
|
665
|
+
summary: `${type} schema missing authoritative identity fields: ${missing.join(', ')}`,
|
|
666
|
+
evidence: [
|
|
667
|
+
`entity name: ${b.name || 'missing'}`,
|
|
668
|
+
`missing identity signals: ${missing.join(', ')}`,
|
|
669
|
+
],
|
|
670
|
+
captured: missing,
|
|
671
|
+
expected: [],
|
|
672
|
+
});
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
return hits;
|
|
678
|
+
},
|
|
679
|
+
}));
|
|
680
|
+
|
|
681
|
+
D.push(defineDetector({
|
|
682
|
+
id: 'SCHEMA-015', name: 'Circular or self-referential JSON-LD @id node reference', namespace: 'SCHEMA',
|
|
683
|
+
description: 'A JSON-LD structured data graph contains an @id node that points to itself or forms a direct circular reference cycle, breaking search engine graph crawlers and parsers.',
|
|
684
|
+
discipline: ['seo', 'aeo'], severity: 'high', deterministic: true, requires: ['site'],
|
|
685
|
+
impact: { retrieval: 'high', representation: 'high' },
|
|
686
|
+
applicable_requirement: 'Google Search Central structured data syntax; Knowledge Graph acyclic graph integrity',
|
|
687
|
+
remediation: 'Remove self-referential @id links and ensure parent/child entity relationships form a directed acyclic graph.',
|
|
688
|
+
verification: 'Validate JSON-LD graph to ensure no entity points to its own @id.',
|
|
689
|
+
check(ctx) {
|
|
690
|
+
const hits = [];
|
|
691
|
+
for (const p of indexTargets(ctx)) {
|
|
692
|
+
for (const j of p.jsonLd || []) {
|
|
693
|
+
for (const b of j.blocks || []) {
|
|
694
|
+
if (!b || typeof b !== 'object') continue;
|
|
695
|
+
const rootId = b['@id'];
|
|
696
|
+
if (!rootId) continue;
|
|
697
|
+
|
|
698
|
+
const checkCycle = (obj, path = '') => {
|
|
699
|
+
if (!obj || typeof obj !== 'object') return;
|
|
700
|
+
for (const [key, val] of Object.entries(obj)) {
|
|
701
|
+
if (key === '@id') continue;
|
|
702
|
+
if (val && typeof val === 'object') {
|
|
703
|
+
if (val['@id'] && val['@id'] === rootId) {
|
|
704
|
+
hits.push({
|
|
705
|
+
subject: { type: 'schema_block', identifier: `${p.url}#${rootId}`, url: p.url },
|
|
706
|
+
summary: `Circular self-reference detected in schema graph: property "${path ? `${path}.${key}` : key}" references parent @id (${rootId})`,
|
|
707
|
+
evidence: [
|
|
708
|
+
`node @id: ${rootId}`,
|
|
709
|
+
`cyclic property: ${path ? `${path}.${key}` : key}`,
|
|
710
|
+
'search engines fail graph parsing when structured data nodes reference themselves circularly',
|
|
711
|
+
],
|
|
712
|
+
captured: { rootId, cyclicProperty: path ? `${path}.${key}` : key },
|
|
713
|
+
expected: 'acyclic entity references',
|
|
714
|
+
});
|
|
715
|
+
} else {
|
|
716
|
+
checkCycle(val, path ? `${path}.${key}` : key);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
};
|
|
721
|
+
|
|
722
|
+
checkCycle(b);
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
return hits;
|
|
727
|
+
},
|
|
728
|
+
}));
|
|
729
|
+
|
|
730
|
+
D.push(defineDetector({
|
|
731
|
+
id: 'SCHEMA-016', name: 'Author Person entity lacks external disambiguation URL or sameAs', namespace: 'SCHEMA',
|
|
732
|
+
description: 'An Article, BlogPosting, NewsArticle, or Review defines an author Person entity without an external profile URL or sameAs link, preventing search engine Knowledge Graph author disambiguation and weakening E-E-A-T trust signals.',
|
|
733
|
+
discipline: ['seo', 'aeo', 'geo'], severity: 'medium', deterministic: true, requires: ['site'],
|
|
734
|
+
impact: { representation: 'medium', ranking: 'low' },
|
|
735
|
+
applicable_requirement: 'Google Search Central Article author structured data; E-E-A-T author identity disambiguation',
|
|
736
|
+
remediation: 'Add a "url" or "sameAs" link pointing to the author\'s authoritative personal profile, Wikidata entry, or professional bio.',
|
|
737
|
+
verification: 'Confirm all author Person entities have at least one valid external disambiguation URL or sameAs reference.',
|
|
738
|
+
check(ctx) {
|
|
739
|
+
const hits = [];
|
|
740
|
+
for (const p of indexTargets(ctx)) {
|
|
741
|
+
for (const j of p.jsonLd || []) {
|
|
742
|
+
for (const b of j.blocks || []) {
|
|
743
|
+
if (!b || typeof b !== 'object') continue;
|
|
744
|
+
const type = [].concat(b['@type'] || []).join(',');
|
|
745
|
+
if (!/Article|BlogPosting|NewsArticle|Review/i.test(type)) continue;
|
|
746
|
+
|
|
747
|
+
const authors = [].concat(b.author || []).filter(Boolean);
|
|
748
|
+
for (const author of authors) {
|
|
749
|
+
if (typeof author !== 'object') continue;
|
|
750
|
+
const authorType = author['@type'] || 'Person';
|
|
751
|
+
if (/Person/i.test(authorType)) {
|
|
752
|
+
const hasUrl = Boolean(author.url);
|
|
753
|
+
const hasSameAs = Array.isArray(author.sameAs) ? author.sameAs.length > 0 : Boolean(author.sameAs);
|
|
754
|
+
if (!hasUrl && !hasSameAs) {
|
|
755
|
+
hits.push({
|
|
756
|
+
subject: { type: 'schema_block', identifier: `${p.url}#${type}-author`, url: p.url },
|
|
757
|
+
summary: `Author Person "${author.name || 'unnamed'}" in ${type} schema lacks disambiguating url or sameAs links`,
|
|
758
|
+
evidence: [
|
|
759
|
+
`article type: ${type}`,
|
|
760
|
+
`author: ${author.name || 'unnamed'}`,
|
|
761
|
+
'Google Knowledge Graph and E-E-A-T guidelines require author identity anchors for AI and rich search features',
|
|
762
|
+
],
|
|
763
|
+
captured: { author: author.name || 'unnamed', hasUrl, hasSameAs },
|
|
764
|
+
expected: 'author Person entity includes url or sameAs profile link',
|
|
765
|
+
});
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
return hits;
|
|
773
|
+
},
|
|
774
|
+
}));
|
|
775
|
+
|
|
249
776
|
export default D;
|
|
777
|
+
|
|
778
|
+
|