geo-checker 0.3.0 → 0.3.2
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/dist/cli.cjs +297 -71
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +297 -71
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +289 -67
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +6 -1
- package/dist/index.d.ts +6 -1
- package/dist/index.js +289 -67
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -305,10 +305,12 @@ async function buildContext(url, opts = {}) {
|
|
|
305
305
|
"Site appears to be JS-rendered (sparse body + SPA root element). Re-run with --render for accurate results."
|
|
306
306
|
);
|
|
307
307
|
}
|
|
308
|
-
const [robotsRaw, llmsRaw, llmsFullRaw] = await Promise.all([
|
|
308
|
+
const [robotsRaw, llmsRaw, llmsFullRaw, skillMdRaw, agentPermissionsRaw] = await Promise.all([
|
|
309
309
|
fetchText(`${origin}/robots.txt`, opts),
|
|
310
310
|
fetchText(`${origin}/llms.txt`, opts),
|
|
311
|
-
fetchText(`${origin}/llms-full.txt`, opts)
|
|
311
|
+
fetchText(`${origin}/llms-full.txt`, opts),
|
|
312
|
+
fetchText(`${origin}/skill.md`, opts),
|
|
313
|
+
fetchText(`${origin}/agent-permissions.json`, opts)
|
|
312
314
|
]);
|
|
313
315
|
let sitemapUrl = null;
|
|
314
316
|
const robots = robotsRaw ? parseRobots(robotsRaw) : null;
|
|
@@ -316,6 +318,13 @@ async function buildContext(url, opts = {}) {
|
|
|
316
318
|
if (!sitemapUrl) sitemapUrl = `${origin}/sitemap.xml`;
|
|
317
319
|
const sitemapRaw = await fetchText(sitemapUrl, opts);
|
|
318
320
|
const sitemap = sitemapRaw ? parseSitemap(sitemapRaw) : null;
|
|
321
|
+
let agentPermissions = null;
|
|
322
|
+
if (agentPermissionsRaw && agentPermissionsRaw.trim().length > 0) {
|
|
323
|
+
try {
|
|
324
|
+
agentPermissions = JSON.parse(agentPermissionsRaw);
|
|
325
|
+
} catch {
|
|
326
|
+
}
|
|
327
|
+
}
|
|
319
328
|
return {
|
|
320
329
|
url,
|
|
321
330
|
finalUrl,
|
|
@@ -330,7 +339,9 @@ async function buildContext(url, opts = {}) {
|
|
|
330
339
|
jsonLd: extractJsonLd($),
|
|
331
340
|
renderMode,
|
|
332
341
|
fetchedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
333
|
-
warnings
|
|
342
|
+
warnings,
|
|
343
|
+
skillMd: skillMdRaw && skillMdRaw.trim().length > 0 ? skillMdRaw : null,
|
|
344
|
+
agentPermissions
|
|
334
345
|
};
|
|
335
346
|
}
|
|
336
347
|
|
|
@@ -339,10 +350,11 @@ function defineRule(rule) {
|
|
|
339
350
|
return rule;
|
|
340
351
|
}
|
|
341
352
|
var CATEGORY_WEIGHTS = {
|
|
342
|
-
crawler:
|
|
343
|
-
"structured-data":
|
|
344
|
-
citation:
|
|
345
|
-
content:
|
|
353
|
+
crawler: 20,
|
|
354
|
+
"structured-data": 25,
|
|
355
|
+
citation: 20,
|
|
356
|
+
content: 15,
|
|
357
|
+
aeo: 20
|
|
346
358
|
};
|
|
347
359
|
|
|
348
360
|
// src/engine.ts
|
|
@@ -356,7 +368,8 @@ async function runRules(ctx, rules, opts = {}) {
|
|
|
356
368
|
crawler: { score: 0, weight: weights.crawler, results: [] },
|
|
357
369
|
"structured-data": { score: 0, weight: weights["structured-data"], results: [] },
|
|
358
370
|
citation: { score: 0, weight: weights.citation, results: [] },
|
|
359
|
-
content: { score: 0, weight: weights.content, results: [] }
|
|
371
|
+
content: { score: 0, weight: weights.content, results: [] },
|
|
372
|
+
aeo: { score: 0, weight: weights.aeo, results: [] }
|
|
360
373
|
};
|
|
361
374
|
for (const rule of rules) {
|
|
362
375
|
if (onlySet && !onlySet.has(rule.id) && (!rule.stableId || !onlySet.has(rule.stableId))) continue;
|
|
@@ -381,6 +394,7 @@ async function runRules(ctx, rules, opts = {}) {
|
|
|
381
394
|
durationMs
|
|
382
395
|
};
|
|
383
396
|
if (rule.stableId !== void 0) entry.stableId = rule.stableId;
|
|
397
|
+
if (rule.title_ko !== void 0) entry.title_ko = rule.title_ko;
|
|
384
398
|
if (rule.group !== void 0) entry.group = rule.group;
|
|
385
399
|
if (rule.impact !== void 0) entry.impact = rule.impact;
|
|
386
400
|
if (rule.effort !== void 0) entry.effort = rule.effort;
|
|
@@ -443,13 +457,15 @@ var httpsRule = defineRule({
|
|
|
443
457
|
effort: "medium",
|
|
444
458
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#crawlerhttps",
|
|
445
459
|
title: "Site is served over HTTPS",
|
|
460
|
+
title_ko: "\uC0AC\uC774\uD2B8\uAC00 HTTPS\uB85C \uC81C\uACF5\uB428",
|
|
446
461
|
description: "AI crawlers treat HTTPS pages as more trustworthy and some skip plain HTTP entirely.",
|
|
447
462
|
run(ctx) {
|
|
448
463
|
const isHttps = ctx.finalUrl.startsWith("https://");
|
|
449
|
-
return isHttps ? { status: "pass", score: 1, rationale: "Final URL uses HTTPS." } : {
|
|
464
|
+
return isHttps ? { status: "pass", score: 1, rationale: "Final URL uses HTTPS.", rationale_ko: "\uCD5C\uC885 URL\uC774 HTTPS\uB97C \uC0AC\uC6A9\uD569\uB2C8\uB2E4." } : {
|
|
450
465
|
status: "fail",
|
|
451
466
|
score: 0,
|
|
452
467
|
rationale: "Final URL does not use HTTPS. Redirect HTTP \u2192 HTTPS site-wide.",
|
|
468
|
+
rationale_ko: "\uCD5C\uC885 URL\uC774 HTTPS\uB97C \uC0AC\uC6A9\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4. \uC0AC\uC774\uD2B8 \uC804\uCCB4\uB97C HTTP \u2192 HTTPS\uB85C \uB9AC\uB2E4\uC774\uB809\uD2B8\uD558\uC138\uC694.",
|
|
453
469
|
fixUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md"
|
|
454
470
|
};
|
|
455
471
|
}
|
|
@@ -466,15 +482,17 @@ var robotsReachableRule = defineRule({
|
|
|
466
482
|
effort: "low",
|
|
467
483
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#crawlerrobots-reachable",
|
|
468
484
|
title: "robots.txt is reachable",
|
|
485
|
+
title_ko: "robots.txt \uC811\uADFC \uAC00\uB2A5 \uC5EC\uBD80",
|
|
469
486
|
description: "A reachable robots.txt lets crawlers confirm their permissions; missing file is treated as allow-all but blocks explicit signalling.",
|
|
470
487
|
run(ctx) {
|
|
471
488
|
if (ctx.robots) {
|
|
472
|
-
return { status: "pass", score: 1, rationale: "robots.txt returned successfully." };
|
|
489
|
+
return { status: "pass", score: 1, rationale: "robots.txt returned successfully.", rationale_ko: "robots.txt\uAC00 \uC815\uC0C1\uC801\uC73C\uB85C \uC751\uB2F5\uD569\uB2C8\uB2E4." };
|
|
473
490
|
}
|
|
474
491
|
return {
|
|
475
492
|
status: "warn",
|
|
476
493
|
score: 0.3,
|
|
477
494
|
rationale: "robots.txt is missing. Add one even if empty to explicitly signal crawl policy.",
|
|
495
|
+
rationale_ko: "robots.txt\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4. \uD06C\uB864 \uC815\uCC45\uC744 \uBA85\uC2DC\uC801\uC73C\uB85C \uC54C\uB9AC\uB824\uBA74 \uBE44\uC5B4 \uC788\uB354\uB77C\uB3C4 \uCD94\uAC00\uD558\uC138\uC694.",
|
|
478
496
|
fixUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md"
|
|
479
497
|
};
|
|
480
498
|
}
|
|
@@ -522,13 +540,15 @@ var robotsAiAllowRule = defineRule({
|
|
|
522
540
|
effort: "low",
|
|
523
541
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#crawlerrobots-ai-allow",
|
|
524
542
|
title: "AI crawlers are allowed",
|
|
543
|
+
title_ko: "AI \uD06C\uB864\uB7EC \uC811\uADFC \uD5C8\uC6A9 \uC5EC\uBD80",
|
|
525
544
|
description: "Major AI search and training crawlers (17 bots incl. GPTBot, OAI-SearchBot, Google-Extended, ClaudeBot, PerplexityBot, Applebot-Extended, Meta-ExternalAgent, Bytespider, DuckAssistBot, YouBot) must be allowed to index the homepage.",
|
|
526
545
|
run(ctx) {
|
|
527
546
|
if (!ctx.robots) {
|
|
528
547
|
return {
|
|
529
548
|
status: "warn",
|
|
530
549
|
score: 0.5,
|
|
531
|
-
rationale: "robots.txt missing; AI crawlers default to allow, but explicit allow is recommended."
|
|
550
|
+
rationale: "robots.txt missing; AI crawlers default to allow, but explicit allow is recommended.",
|
|
551
|
+
rationale_ko: "robots.txt\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4. AI \uD06C\uB864\uB7EC\uB294 \uAE30\uBCF8\uC801\uC73C\uB85C \uD5C8\uC6A9\uB418\uC9C0\uB9CC, \uBA85\uC2DC\uC801 \uD5C8\uC6A9\uC744 \uAD8C\uC7A5\uD569\uB2C8\uB2E4."
|
|
532
552
|
};
|
|
533
553
|
}
|
|
534
554
|
const path = new URL(ctx.finalUrl).pathname || "/";
|
|
@@ -546,6 +566,7 @@ var robotsAiAllowRule = defineRule({
|
|
|
546
566
|
status: "fail",
|
|
547
567
|
score: Math.max(0, 1 - blocked.length / AI_BOTS.length),
|
|
548
568
|
rationale: `Blocked: ${blocked.join(", ")}. Remove the Disallow or add an explicit Allow for these user-agents.`,
|
|
569
|
+
rationale_ko: `\uCC28\uB2E8\uB428: ${blocked.join(", ")}. \uD574\uB2F9 User-agent\uC758 Disallow\uB97C \uC81C\uAC70\uD558\uAC70\uB098 \uBA85\uC2DC\uC801 Allow\uB97C \uCD94\uAC00\uD558\uC138\uC694.`,
|
|
549
570
|
evidence: { blocked, mentioned, totalBots: AI_BOTS.length },
|
|
550
571
|
fixUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md"
|
|
551
572
|
};
|
|
@@ -554,13 +575,15 @@ var robotsAiAllowRule = defineRule({
|
|
|
554
575
|
return {
|
|
555
576
|
status: "warn",
|
|
556
577
|
score: 0.6,
|
|
557
|
-
rationale: `All ${AI_BOTS.length} AI crawlers reach the page via default rules, but none are explicitly listed. Consider explicit Allow entries
|
|
578
|
+
rationale: `All ${AI_BOTS.length} AI crawlers reach the page via default rules, but none are explicitly listed. Consider explicit Allow entries.`,
|
|
579
|
+
rationale_ko: `${AI_BOTS.length}\uAC1C AI \uD06C\uB864\uB7EC \uBAA8\uB450 \uAE30\uBCF8 \uADDC\uCE59\uC73C\uB85C \uC811\uADFC \uAC00\uB2A5\uD558\uC9C0\uB9CC, \uBA85\uC2DC\uC801\uC73C\uB85C \uD5C8\uC6A9\uB41C \uBD07\uC774 \uC5C6\uC2B5\uB2C8\uB2E4. \uBA85\uC2DC\uC801 Allow \uD56D\uBAA9 \uCD94\uAC00\uB97C \uAD8C\uC7A5\uD569\uB2C8\uB2E4.`
|
|
558
580
|
};
|
|
559
581
|
}
|
|
560
582
|
return {
|
|
561
583
|
status: "pass",
|
|
562
584
|
score: 1,
|
|
563
585
|
rationale: `All ${AI_BOTS.length} AI crawlers can reach the page; ${mentioned.length} explicitly listed.`,
|
|
586
|
+
rationale_ko: `${AI_BOTS.length}\uAC1C AI \uD06C\uB864\uB7EC \uBAA8\uB450 \uC811\uADFC \uAC00\uB2A5\uD558\uBA70, ${mentioned.length}\uAC1C\uAC00 \uBA85\uC2DC\uC801\uC73C\uB85C \uD5C8\uC6A9\uB418\uC5B4 \uC788\uC2B5\uB2C8\uB2E4.`,
|
|
564
587
|
evidence: { mentioned, totalBots: AI_BOTS.length }
|
|
565
588
|
};
|
|
566
589
|
}
|
|
@@ -577,15 +600,17 @@ var llmsTxtPresentRule = defineRule({
|
|
|
577
600
|
effort: "medium",
|
|
578
601
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#crawlerllms-txt-present",
|
|
579
602
|
title: "llms.txt is present",
|
|
603
|
+
title_ko: "llms.txt \uD30C\uC77C \uC874\uC7AC \uC5EC\uBD80",
|
|
580
604
|
description: "An /llms.txt file at the site root gives AI assistants a curated map of the most citation-worthy pages.",
|
|
581
605
|
run(ctx) {
|
|
582
606
|
if (ctx.llmsTxt) {
|
|
583
|
-
return { status: "pass", score: 1, rationale: "llms.txt found at site root." };
|
|
607
|
+
return { status: "pass", score: 1, rationale: "llms.txt found at site root.", rationale_ko: "llms.txt\uAC00 \uC0AC\uC774\uD2B8 \uB8E8\uD2B8\uC5D0 \uC874\uC7AC\uD569\uB2C8\uB2E4." };
|
|
584
608
|
}
|
|
585
609
|
return {
|
|
586
610
|
status: "warn",
|
|
587
611
|
score: 0,
|
|
588
612
|
rationale: "No /llms.txt found. Add one to curate the pages AI assistants should read.",
|
|
613
|
+
rationale_ko: "/llms.txt\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4. AI \uC5B4\uC2DC\uC2A4\uD134\uD2B8\uAC00 \uC77D\uC5B4\uC57C \uD560 \uD398\uC774\uC9C0\uB97C \uD050\uB808\uC774\uC158\uD558\uB824\uBA74 \uCD94\uAC00\uD558\uC138\uC694.",
|
|
589
614
|
fixUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md"
|
|
590
615
|
};
|
|
591
616
|
}
|
|
@@ -602,10 +627,11 @@ var llmsTxtWellformedRule = defineRule({
|
|
|
602
627
|
effort: "low",
|
|
603
628
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#crawlerllms-txt-wellformed",
|
|
604
629
|
title: "llms.txt follows the spec",
|
|
630
|
+
title_ko: "llms.txt \uC2A4\uD399 \uC900\uC218 \uC5EC\uBD80",
|
|
605
631
|
description: "Must start with an H1 project title, then a brief summary, then at least one H2 section containing link items.",
|
|
606
632
|
run(ctx) {
|
|
607
633
|
if (!ctx.llmsTxt) {
|
|
608
|
-
return { status: "skip", score: 0, rationale: "No llms.txt to validate." };
|
|
634
|
+
return { status: "skip", score: 0, rationale: "No llms.txt to validate.", rationale_ko: "\uAC80\uC99D\uD560 llms.txt\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4." };
|
|
609
635
|
}
|
|
610
636
|
const check = isLlmsTxtWellFormed(ctx.llmsTxt);
|
|
611
637
|
if (check.ok) {
|
|
@@ -613,13 +639,15 @@ var llmsTxtWellformedRule = defineRule({
|
|
|
613
639
|
return {
|
|
614
640
|
status: "pass",
|
|
615
641
|
score: 1,
|
|
616
|
-
rationale: `Well-formed with ${ctx.llmsTxt.sections.length} section(s) and ${totalLinks} link(s)
|
|
642
|
+
rationale: `Well-formed with ${ctx.llmsTxt.sections.length} section(s) and ${totalLinks} link(s).`,
|
|
643
|
+
rationale_ko: `\uC2A4\uD399\uC5D0 \uB9DE\uAC8C \uC791\uC131\uB428 (\uC139\uC158 ${ctx.llmsTxt.sections.length}\uAC1C, \uB9C1\uD06C ${totalLinks}\uAC1C).`
|
|
617
644
|
};
|
|
618
645
|
}
|
|
619
646
|
return {
|
|
620
647
|
status: "warn",
|
|
621
648
|
score: 0.3,
|
|
622
649
|
rationale: `llms.txt does not fully match the spec: ${check.reason}.`,
|
|
650
|
+
rationale_ko: `llms.txt\uAC00 \uC2A4\uD399\uC744 \uC644\uC804\uD788 \uB530\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4: ${check.reason}.`,
|
|
623
651
|
fixUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md"
|
|
624
652
|
};
|
|
625
653
|
}
|
|
@@ -636,13 +664,15 @@ var llmsFullTxtRule = defineRule({
|
|
|
636
664
|
effort: "medium",
|
|
637
665
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#crawlerllms-full-txt",
|
|
638
666
|
title: "llms-full.txt provides full-content mirror",
|
|
667
|
+
title_ko: "llms-full.txt \uC804\uCCB4 \uCF58\uD150\uCE20 \uBBF8\uB7EC \uC81C\uACF5 \uC5EC\uBD80",
|
|
639
668
|
description: "Complement /llms.txt with /llms-full.txt containing the full body of every cited page. AI assistants can ingest it in one request instead of crawling every URL.",
|
|
640
669
|
run(ctx) {
|
|
641
670
|
if (ctx.llmsFullTxt && ctx.llmsFullTxt.length > 200) {
|
|
642
671
|
return {
|
|
643
672
|
status: "pass",
|
|
644
673
|
score: 1,
|
|
645
|
-
rationale: `/llms-full.txt found (${ctx.llmsFullTxt.length.toLocaleString()} chars)
|
|
674
|
+
rationale: `/llms-full.txt found (${ctx.llmsFullTxt.length.toLocaleString()} chars).`,
|
|
675
|
+
rationale_ko: `/llms-full.txt\uAC00 \uC874\uC7AC\uD569\uB2C8\uB2E4 (${ctx.llmsFullTxt.length.toLocaleString()}\uC790).`
|
|
646
676
|
};
|
|
647
677
|
}
|
|
648
678
|
if (ctx.llmsFullTxt) {
|
|
@@ -650,6 +680,7 @@ var llmsFullTxtRule = defineRule({
|
|
|
650
680
|
status: "warn",
|
|
651
681
|
score: 0.5,
|
|
652
682
|
rationale: `/llms-full.txt found but very short (${ctx.llmsFullTxt.length} chars). Consider expanding with page bodies.`,
|
|
683
|
+
rationale_ko: `/llms-full.txt\uAC00 \uC788\uC9C0\uB9CC \uB108\uBB34 \uC9E7\uC2B5\uB2C8\uB2E4 (${ctx.llmsFullTxt.length}\uC790). \uD398\uC774\uC9C0 \uBCF8\uBB38\uC73C\uB85C \uB0B4\uC6A9\uC744 \uBCF4\uAC15\uD558\uC138\uC694.`,
|
|
653
684
|
fixHint: "Mirror full article bodies into /llms-full.txt so AI assistants can quote without re-crawling."
|
|
654
685
|
};
|
|
655
686
|
}
|
|
@@ -657,6 +688,7 @@ var llmsFullTxtRule = defineRule({
|
|
|
657
688
|
status: "warn",
|
|
658
689
|
score: 0,
|
|
659
690
|
rationale: "No /llms-full.txt found. Adding one lets AI assistants ingest the full corpus in a single request.",
|
|
691
|
+
rationale_ko: "/llms-full.txt\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4. \uCD94\uAC00\uD558\uBA74 AI \uC5B4\uC2DC\uC2A4\uD134\uD2B8\uAC00 \uC804\uCCB4 \uCF58\uD150\uCE20\uB97C \uD55C \uBC88\uC758 \uC694\uCCAD\uC73C\uB85C \uC218\uC9D1\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4.",
|
|
660
692
|
fixHint: "Publish /llms-full.txt alongside /llms.txt with the full body text of your top pages.",
|
|
661
693
|
estimatedImpact: 1
|
|
662
694
|
};
|
|
@@ -674,19 +706,22 @@ var sitemapPresentRule = defineRule({
|
|
|
674
706
|
effort: "low",
|
|
675
707
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#crawlersitemap-present",
|
|
676
708
|
title: "sitemap.xml is present",
|
|
709
|
+
title_ko: "sitemap.xml \uC874\uC7AC \uC5EC\uBD80",
|
|
677
710
|
description: "A sitemap helps AI crawlers discover and prioritise pages; many crawlers short-circuit discovery without one.",
|
|
678
711
|
run(ctx) {
|
|
679
712
|
if (ctx.sitemap && ctx.sitemap.urls.length > 0) {
|
|
680
713
|
return {
|
|
681
714
|
status: "pass",
|
|
682
715
|
score: 1,
|
|
683
|
-
rationale: `Sitemap found with ${ctx.sitemap.urls.length} URL(s)
|
|
716
|
+
rationale: `Sitemap found with ${ctx.sitemap.urls.length} URL(s).`,
|
|
717
|
+
rationale_ko: `\uC0AC\uC774\uD2B8\uB9F5\uC5D0 URL\uC774 ${ctx.sitemap.urls.length}\uAC1C \uC788\uC2B5\uB2C8\uB2E4.`
|
|
684
718
|
};
|
|
685
719
|
}
|
|
686
720
|
return {
|
|
687
721
|
status: "warn",
|
|
688
722
|
score: 0.2,
|
|
689
723
|
rationale: "No sitemap.xml found (checked /sitemap.xml and Sitemap: directive in robots.txt).",
|
|
724
|
+
rationale_ko: "sitemap.xml\uC774 \uC5C6\uC2B5\uB2C8\uB2E4 (/sitemap.xml \uBC0F robots.txt\uC758 Sitemap: \uC9C0\uC2DC\uC5B4\uB97C \uD655\uC778\uD588\uC2B5\uB2C8\uB2E4).",
|
|
690
725
|
fixUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md"
|
|
691
726
|
};
|
|
692
727
|
}
|
|
@@ -714,15 +749,17 @@ var jsonLdPresentRule = defineRule({
|
|
|
714
749
|
effort: "medium",
|
|
715
750
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#sdjsonld-present",
|
|
716
751
|
title: "JSON-LD structured data is present",
|
|
752
|
+
title_ko: "JSON-LD \uAD6C\uC870\uD654 \uB370\uC774\uD130 \uC874\uC7AC \uC5EC\uBD80",
|
|
717
753
|
description: 'At least one <script type="application/ld+json"> block is the primary way AI engines map your page to an entity.',
|
|
718
754
|
run(ctx) {
|
|
719
755
|
if (ctx.jsonLd.length > 0) {
|
|
720
|
-
return { status: "pass", score: 1, rationale: `Found ${ctx.jsonLd.length} JSON-LD block(s).` };
|
|
756
|
+
return { status: "pass", score: 1, rationale: `Found ${ctx.jsonLd.length} JSON-LD block(s).`, rationale_ko: `JSON-LD \uBE14\uB85D\uC774 ${ctx.jsonLd.length}\uAC1C \uC788\uC2B5\uB2C8\uB2E4.` };
|
|
721
757
|
}
|
|
722
758
|
return {
|
|
723
759
|
status: "fail",
|
|
724
760
|
score: 0,
|
|
725
761
|
rationale: "No JSON-LD blocks found. Add schema.org structured data.",
|
|
762
|
+
rationale_ko: "JSON-LD \uBE14\uB85D\uC774 \uC5C6\uC2B5\uB2C8\uB2E4. schema.org \uAD6C\uC870\uD654 \uB370\uC774\uD130\uB97C \uCD94\uAC00\uD558\uC138\uC694.",
|
|
726
763
|
fixUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md"
|
|
727
764
|
};
|
|
728
765
|
}
|
|
@@ -798,20 +835,22 @@ var jsonLdValidJsonRule = defineRule({
|
|
|
798
835
|
effort: "low",
|
|
799
836
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#sdjsonld-valid-json",
|
|
800
837
|
title: "JSON-LD blocks parse as valid JSON",
|
|
838
|
+
title_ko: "JSON-LD \uBE14\uB85D\uC758 JSON \uC720\uD6A8\uC131",
|
|
801
839
|
description: "Malformed JSON in an ld+json block is silently ignored by most consumers \u2014 a costly silent failure.",
|
|
802
840
|
run(ctx) {
|
|
803
841
|
if (ctx.jsonLd.length === 0) {
|
|
804
|
-
return { status: "skip", score: 0, rationale: "No JSON-LD to validate." };
|
|
842
|
+
return { status: "skip", score: 0, rationale: "No JSON-LD to validate.", rationale_ko: "\uAC80\uC99D\uD560 JSON-LD\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4." };
|
|
805
843
|
}
|
|
806
844
|
if (hasParseError(ctx.jsonLd)) {
|
|
807
845
|
return {
|
|
808
846
|
status: "fail",
|
|
809
847
|
score: 0,
|
|
810
848
|
rationale: "One or more JSON-LD blocks failed to parse.",
|
|
849
|
+
rationale_ko: "JSON-LD \uBE14\uB85D \uD558\uB098 \uC774\uC0C1\uC744 \uD30C\uC2F1\uD558\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4.",
|
|
811
850
|
fixUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md"
|
|
812
851
|
};
|
|
813
852
|
}
|
|
814
|
-
return { status: "pass", score: 1, rationale: "All JSON-LD blocks parse cleanly." };
|
|
853
|
+
return { status: "pass", score: 1, rationale: "All JSON-LD blocks parse cleanly.", rationale_ko: "\uBAA8\uB4E0 JSON-LD \uBE14\uB85D\uC774 \uC62C\uBC14\uB974\uAC8C \uD30C\uC2F1\uB429\uB2C8\uB2E4." };
|
|
815
854
|
}
|
|
816
855
|
});
|
|
817
856
|
|
|
@@ -826,10 +865,11 @@ var schemaTypeRecognizedRule = defineRule({
|
|
|
826
865
|
effort: "low",
|
|
827
866
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#sdschema-type-recognized",
|
|
828
867
|
title: "Schema.org @type is a recognised kind",
|
|
868
|
+
title_ko: "Schema.org @type \uC778\uC2DD \uAC00\uB2A5 \uC5EC\uBD80",
|
|
829
869
|
description: "AI engines match pages against well-known types (Article, Product, FAQPage...). Obscure types weaken the signal.",
|
|
830
870
|
run(ctx) {
|
|
831
871
|
if (ctx.jsonLd.length === 0) {
|
|
832
|
-
return { status: "skip", score: 0, rationale: "No JSON-LD to analyse." };
|
|
872
|
+
return { status: "skip", score: 0, rationale: "No JSON-LD to analyse.", rationale_ko: "\uBD84\uC11D\uD560 JSON-LD\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4." };
|
|
833
873
|
}
|
|
834
874
|
const nodes = flattenJsonLd(ctx.jsonLd);
|
|
835
875
|
const recognized = /* @__PURE__ */ new Set();
|
|
@@ -845,6 +885,7 @@ var schemaTypeRecognizedRule = defineRule({
|
|
|
845
885
|
status: "pass",
|
|
846
886
|
score: 1,
|
|
847
887
|
rationale: `Recognised: ${[...recognized].join(", ")}.`,
|
|
888
|
+
rationale_ko: `\uC778\uC2DD\uB41C \uD0C0\uC785: ${[...recognized].join(", ")}.`,
|
|
848
889
|
evidence: { recognized: [...recognized], all: [...seenTypes] }
|
|
849
890
|
};
|
|
850
891
|
}
|
|
@@ -852,6 +893,7 @@ var schemaTypeRecognizedRule = defineRule({
|
|
|
852
893
|
status: "warn",
|
|
853
894
|
score: 0.3,
|
|
854
895
|
rationale: `No recognised schema.org types. Saw: ${[...seenTypes].join(", ") || "(none)"}.`,
|
|
896
|
+
rationale_ko: `\uC778\uC2DD \uAC00\uB2A5\uD55C schema.org \uD0C0\uC785\uC774 \uC5C6\uC2B5\uB2C8\uB2E4. \uBC1C\uACAC\uB41C \uD0C0\uC785: ${[...seenTypes].join(", ") || "(\uC5C6\uC74C)"}.`,
|
|
855
897
|
fixUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md"
|
|
856
898
|
};
|
|
857
899
|
}
|
|
@@ -868,10 +910,11 @@ var requiredFieldsRule = defineRule({
|
|
|
868
910
|
effort: "medium",
|
|
869
911
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#sdrequired-fields",
|
|
870
912
|
title: "Required fields for recognised types are set",
|
|
913
|
+
title_ko: "\uC778\uC2DD\uB41C \uD0C0\uC785\uC758 \uD544\uC218 \uD544\uB4DC \uCDA9\uC871 \uC5EC\uBD80",
|
|
871
914
|
description: "Article needs headline/author/datePublished, FAQPage needs mainEntity, Product needs offers, etc.",
|
|
872
915
|
run(ctx) {
|
|
873
916
|
if (ctx.jsonLd.length === 0) {
|
|
874
|
-
return { status: "skip", score: 0, rationale: "No JSON-LD to analyse." };
|
|
917
|
+
return { status: "skip", score: 0, rationale: "No JSON-LD to analyse.", rationale_ko: "\uBD84\uC11D\uD560 JSON-LD\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4." };
|
|
875
918
|
}
|
|
876
919
|
const nodes = flattenJsonLd(ctx.jsonLd);
|
|
877
920
|
const missing = [];
|
|
@@ -890,14 +933,16 @@ var requiredFieldsRule = defineRule({
|
|
|
890
933
|
return {
|
|
891
934
|
status: "skip",
|
|
892
935
|
score: 0,
|
|
893
|
-
rationale: "No types with known required fields were found."
|
|
936
|
+
rationale: "No types with known required fields were found.",
|
|
937
|
+
rationale_ko: "\uD544\uC218 \uD544\uB4DC\uAC00 \uC815\uC758\uB41C \uD0C0\uC785\uC774 \uC5C6\uC2B5\uB2C8\uB2E4."
|
|
894
938
|
};
|
|
895
939
|
}
|
|
896
940
|
if (missing.length === 0) {
|
|
897
941
|
return {
|
|
898
942
|
status: "pass",
|
|
899
943
|
score: 1,
|
|
900
|
-
rationale: `Required fields set on ${checked.length} node(s)
|
|
944
|
+
rationale: `Required fields set on ${checked.length} node(s).`,
|
|
945
|
+
rationale_ko: `${checked.length}\uAC1C \uB178\uB4DC\uC758 \uD544\uC218 \uD544\uB4DC\uAC00 \uBAA8\uB450 \uCDA9\uC871\uB429\uB2C8\uB2E4.`
|
|
901
946
|
};
|
|
902
947
|
}
|
|
903
948
|
const msg = missing.map((m) => `${m.type}.${m.field}`).join(", ");
|
|
@@ -905,6 +950,7 @@ var requiredFieldsRule = defineRule({
|
|
|
905
950
|
status: "fail",
|
|
906
951
|
score: Math.max(0, 1 - missing.length / (checked.length * 2)),
|
|
907
952
|
rationale: `Missing required fields: ${msg}.`,
|
|
953
|
+
rationale_ko: `\uB204\uB77D\uB41C \uD544\uC218 \uD544\uB4DC: ${msg}.`,
|
|
908
954
|
evidence: missing,
|
|
909
955
|
fixUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md"
|
|
910
956
|
};
|
|
@@ -922,10 +968,11 @@ var microdataFallbackRule = defineRule({
|
|
|
922
968
|
effort: "medium",
|
|
923
969
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#sdmicrodata-fallback",
|
|
924
970
|
title: "Microdata or RDFa fallback when JSON-LD is missing",
|
|
971
|
+
title_ko: "JSON-LD \uC5C6\uC744 \uB54C Microdata/RDFa \uB300\uCCB4 \uC5EC\uBD80",
|
|
925
972
|
description: "If JSON-LD is absent, inline microdata (itemscope/itemtype) or RDFa still gives some structured signal.",
|
|
926
973
|
run(ctx) {
|
|
927
974
|
if (ctx.jsonLd.length > 0) {
|
|
928
|
-
return { status: "skip", score: 0, rationale: "JSON-LD is present; fallback not needed." };
|
|
975
|
+
return { status: "skip", score: 0, rationale: "JSON-LD is present; fallback not needed.", rationale_ko: "JSON-LD\uAC00 \uC788\uC73C\uBBC0\uB85C \uB300\uCCB4 \uC218\uB2E8\uC774 \uD544\uC694\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4." };
|
|
929
976
|
}
|
|
930
977
|
const microdata = ctx.$("[itemscope][itemtype]").length;
|
|
931
978
|
const rdfa = ctx.$("[typeof][vocab], [typeof][property]").length;
|
|
@@ -933,13 +980,15 @@ var microdataFallbackRule = defineRule({
|
|
|
933
980
|
return {
|
|
934
981
|
status: "pass",
|
|
935
982
|
score: 1,
|
|
936
|
-
rationale: `Found ${microdata} microdata and ${rdfa} RDFa nodes
|
|
983
|
+
rationale: `Found ${microdata} microdata and ${rdfa} RDFa nodes.`,
|
|
984
|
+
rationale_ko: `Microdata ${microdata}\uAC1C, RDFa ${rdfa}\uAC1C \uBC1C\uACAC\uB429\uB2C8\uB2E4.`
|
|
937
985
|
};
|
|
938
986
|
}
|
|
939
987
|
return {
|
|
940
988
|
status: "fail",
|
|
941
989
|
score: 0,
|
|
942
990
|
rationale: "No structured data at all (no JSON-LD, no microdata, no RDFa).",
|
|
991
|
+
rationale_ko: "\uAD6C\uC870\uD654 \uB370\uC774\uD130\uAC00 \uC804\uD600 \uC5C6\uC2B5\uB2C8\uB2E4 (JSON-LD, Microdata, RDFa \uBAA8\uB450 \uC5C6\uC74C).",
|
|
943
992
|
fixUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md"
|
|
944
993
|
};
|
|
945
994
|
}
|
|
@@ -957,10 +1006,11 @@ var noDuplicateTypesRule = defineRule({
|
|
|
957
1006
|
effort: "low",
|
|
958
1007
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#sdno-duplicate-types",
|
|
959
1008
|
title: "No conflicting duplicate @types",
|
|
1009
|
+
title_ko: "@type \uC911\uBCF5 \uCDA9\uB3CC \uC5C6\uC74C",
|
|
960
1010
|
description: "Multiple competing entities of the same primary type (e.g. two Articles) confuse the engine about which one represents the page.",
|
|
961
1011
|
run(ctx) {
|
|
962
1012
|
if (ctx.jsonLd.length === 0) {
|
|
963
|
-
return { status: "skip", score: 0, rationale: "No JSON-LD to analyse." };
|
|
1013
|
+
return { status: "skip", score: 0, rationale: "No JSON-LD to analyse.", rationale_ko: "\uBD84\uC11D\uD560 JSON-LD\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4." };
|
|
964
1014
|
}
|
|
965
1015
|
const counts = /* @__PURE__ */ new Map();
|
|
966
1016
|
for (const node of flattenJsonLd(ctx.jsonLd)) {
|
|
@@ -970,12 +1020,13 @@ var noDuplicateTypesRule = defineRule({
|
|
|
970
1020
|
}
|
|
971
1021
|
const dupes = [...counts.entries()].filter(([, n]) => n > 1);
|
|
972
1022
|
if (dupes.length === 0) {
|
|
973
|
-
return { status: "pass", score: 1, rationale: "No duplicate primary types." };
|
|
1023
|
+
return { status: "pass", score: 1, rationale: "No duplicate primary types.", rationale_ko: "\uC911\uBCF5\uB41C \uAE30\uBCF8 \uD0C0\uC785\uC774 \uC5C6\uC2B5\uB2C8\uB2E4." };
|
|
974
1024
|
}
|
|
975
1025
|
return {
|
|
976
1026
|
status: "warn",
|
|
977
1027
|
score: 0.4,
|
|
978
1028
|
rationale: `Duplicate primary types: ${dupes.map(([t, n]) => `${t}\xD7${n}`).join(", ")}.`,
|
|
1029
|
+
rationale_ko: `\uC911\uBCF5\uB41C \uAE30\uBCF8 \uD0C0\uC785: ${dupes.map(([t, n]) => `${t}\xD7${n}`).join(", ")}.`,
|
|
979
1030
|
fixUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md"
|
|
980
1031
|
};
|
|
981
1032
|
}
|
|
@@ -1023,10 +1074,11 @@ var sameAsEntityRule = defineRule({
|
|
|
1023
1074
|
effort: "medium",
|
|
1024
1075
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#sdsameas-entity",
|
|
1025
1076
|
title: "Entity nodes link the knowledge graph via sameAs",
|
|
1077
|
+
title_ko: "sameAs\uB85C \uC9C0\uC2DD \uADF8\uB798\uD504 \uC5F0\uACB0 \uC5EC\uBD80",
|
|
1026
1078
|
description: "Organization/Person nodes should declare a sameAs[] array linking to Wikipedia/Wikidata/LinkedIn so AI engines can resolve the entity in their knowledge graph (E-E-A-T signal).",
|
|
1027
1079
|
run(ctx) {
|
|
1028
1080
|
if (ctx.jsonLd.length === 0) {
|
|
1029
|
-
return { status: "skip", score: 0, rationale: "No JSON-LD to analyse." };
|
|
1081
|
+
return { status: "skip", score: 0, rationale: "No JSON-LD to analyse.", rationale_ko: "\uBD84\uC11D\uD560 JSON-LD\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4." };
|
|
1030
1082
|
}
|
|
1031
1083
|
const nodes = flattenJsonLd(ctx.jsonLd);
|
|
1032
1084
|
const entities = nodes.filter((n) => getTypes(n).some((t) => ENTITY_TYPES.includes(t)));
|
|
@@ -1034,7 +1086,8 @@ var sameAsEntityRule = defineRule({
|
|
|
1034
1086
|
return {
|
|
1035
1087
|
status: "skip",
|
|
1036
1088
|
score: 0,
|
|
1037
|
-
rationale: "No Organization/Person/LocalBusiness/Brand entity to link."
|
|
1089
|
+
rationale: "No Organization/Person/LocalBusiness/Brand entity to link.",
|
|
1090
|
+
rationale_ko: "\uC5F0\uACB0\uD560 Organization/Person/LocalBusiness/Brand \uC5D4\uD2F0\uD2F0\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4."
|
|
1038
1091
|
};
|
|
1039
1092
|
}
|
|
1040
1093
|
let bestScore = 0;
|
|
@@ -1058,6 +1111,7 @@ var sameAsEntityRule = defineRule({
|
|
|
1058
1111
|
status: "pass",
|
|
1059
1112
|
score: 1,
|
|
1060
1113
|
rationale: `Entity links ${bestEvidence.trusted} trusted knowledge-graph hosts via sameAs.`,
|
|
1114
|
+
rationale_ko: `\uC5D4\uD2F0\uD2F0\uAC00 sameAs\uB85C \uC2E0\uB8B0\uD560 \uC218 \uC788\uB294 \uC9C0\uC2DD \uADF8\uB798\uD504 \uC0AC\uC774\uD2B8 ${bestEvidence.trusted}\uAC1C\uC5D0 \uC5F0\uACB0\uB418\uC5B4 \uC788\uC2B5\uB2C8\uB2E4.`,
|
|
1061
1115
|
evidence: bestEvidence
|
|
1062
1116
|
};
|
|
1063
1117
|
}
|
|
@@ -1066,6 +1120,7 @@ var sameAsEntityRule = defineRule({
|
|
|
1066
1120
|
status: "pass",
|
|
1067
1121
|
score: bestScore,
|
|
1068
1122
|
rationale: `Entity has 1 trusted sameAs link. Add Wikipedia/Wikidata for stronger E-E-A-T.`,
|
|
1123
|
+
rationale_ko: "\uC2E0\uB8B0\uD560 \uC218 \uC788\uB294 sameAs \uB9C1\uD06C\uAC00 1\uAC1C \uC788\uC2B5\uB2C8\uB2E4. E-E-A-T \uAC15\uD654\uB97C \uC704\uD574 Wikipedia/Wikidata\uB97C \uCD94\uAC00\uD558\uC138\uC694.",
|
|
1069
1124
|
evidence: bestEvidence,
|
|
1070
1125
|
estimatedImpact: 1
|
|
1071
1126
|
};
|
|
@@ -1075,6 +1130,7 @@ var sameAsEntityRule = defineRule({
|
|
|
1075
1130
|
status: "warn",
|
|
1076
1131
|
score: bestScore,
|
|
1077
1132
|
rationale: "Entity declares sameAs but no trusted knowledge-graph hosts (Wikipedia/Wikidata/LinkedIn).",
|
|
1133
|
+
rationale_ko: "sameAs\uAC00 \uC120\uC5B8\uB418\uC5B4 \uC788\uC9C0\uB9CC \uC2E0\uB8B0\uD560 \uC218 \uC788\uB294 \uC9C0\uC2DD \uADF8\uB798\uD504 \uD638\uC2A4\uD2B8(Wikipedia/Wikidata/LinkedIn)\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.",
|
|
1078
1134
|
evidence: bestEvidence,
|
|
1079
1135
|
fixHint: "Add Wikipedia/Wikidata/LinkedIn URLs to your Organization sameAs[].",
|
|
1080
1136
|
estimatedImpact: 2
|
|
@@ -1084,6 +1140,7 @@ var sameAsEntityRule = defineRule({
|
|
|
1084
1140
|
status: "warn",
|
|
1085
1141
|
score: 0,
|
|
1086
1142
|
rationale: `${entities.length} entity node(s) found but none declare sameAs links.`,
|
|
1143
|
+
rationale_ko: `\uC5D4\uD2F0\uD2F0 \uB178\uB4DC\uAC00 ${entities.length}\uAC1C \uC788\uC9C0\uB9CC sameAs \uB9C1\uD06C\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.`,
|
|
1087
1144
|
fixHint: 'Add sameAs:["https://en.wikipedia.org/wiki/...", "https://www.linkedin.com/company/..."] to your Organization JSON-LD.',
|
|
1088
1145
|
estimatedImpact: 3
|
|
1089
1146
|
};
|
|
@@ -1115,15 +1172,16 @@ var breadcrumbValidRule = defineRule({
|
|
|
1115
1172
|
effort: "medium",
|
|
1116
1173
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#sdbreadcrumb-valid",
|
|
1117
1174
|
title: "BreadcrumbList items declare position, name, and item",
|
|
1175
|
+
title_ko: "BreadcrumbList \uD56D\uBAA9\uC758 \uD544\uC218 \uD544\uB4DC \uCDA9\uC871 \uC5EC\uBD80",
|
|
1118
1176
|
description: "When BreadcrumbList JSON-LD is present, every itemListElement should set position (1-indexed), name, and item (URL) \u2014 otherwise AI engines cannot reconstruct the path.",
|
|
1119
1177
|
run(ctx) {
|
|
1120
1178
|
if (ctx.jsonLd.length === 0) {
|
|
1121
|
-
return { status: "skip", score: 0, rationale: "No JSON-LD to analyse." };
|
|
1179
|
+
return { status: "skip", score: 0, rationale: "No JSON-LD to analyse.", rationale_ko: "\uBD84\uC11D\uD560 JSON-LD\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4." };
|
|
1122
1180
|
}
|
|
1123
1181
|
const nodes = flattenJsonLd(ctx.jsonLd);
|
|
1124
1182
|
const breadcrumbs = nodes.filter((n) => getTypes(n).includes("BreadcrumbList"));
|
|
1125
1183
|
if (breadcrumbs.length === 0) {
|
|
1126
|
-
return { status: "skip", score: 0, rationale: "No BreadcrumbList present." };
|
|
1184
|
+
return { status: "skip", score: 0, rationale: "No BreadcrumbList present.", rationale_ko: "BreadcrumbList\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4." };
|
|
1127
1185
|
}
|
|
1128
1186
|
const allIssues = [];
|
|
1129
1187
|
let totalItems = 0;
|
|
@@ -1136,7 +1194,8 @@ var breadcrumbValidRule = defineRule({
|
|
|
1136
1194
|
return {
|
|
1137
1195
|
status: "pass",
|
|
1138
1196
|
score: 1,
|
|
1139
|
-
rationale: `BreadcrumbList(s) valid (${totalItems} items)
|
|
1197
|
+
rationale: `BreadcrumbList(s) valid (${totalItems} items).`,
|
|
1198
|
+
rationale_ko: `BreadcrumbList\uAC00 \uC720\uD6A8\uD569\uB2C8\uB2E4 (\uD56D\uBAA9 ${totalItems}\uAC1C).`
|
|
1140
1199
|
};
|
|
1141
1200
|
}
|
|
1142
1201
|
const fatalCount = allIssues.length;
|
|
@@ -1146,6 +1205,7 @@ var breadcrumbValidRule = defineRule({
|
|
|
1146
1205
|
status: score < 0.5 ? "fail" : "warn",
|
|
1147
1206
|
score,
|
|
1148
1207
|
rationale: `${fatalCount} breadcrumb item(s) missing required fields.`,
|
|
1208
|
+
rationale_ko: `breadcrumb \uD56D\uBAA9 ${fatalCount}\uAC1C\uC5D0 \uD544\uC218 \uD544\uB4DC\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.`,
|
|
1149
1209
|
evidence: allIssues.slice(0, 5),
|
|
1150
1210
|
fixHint: 'Each itemListElement needs { "@type": "ListItem", position: N, name, item }.',
|
|
1151
1211
|
estimatedImpact: Math.round(2 * (1 - score))
|
|
@@ -1176,6 +1236,7 @@ var titleRule = defineRule({
|
|
|
1176
1236
|
effort: "low",
|
|
1177
1237
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#cittitle",
|
|
1178
1238
|
title: "<title> is set with a reasonable length",
|
|
1239
|
+
title_ko: "<title> \uD0DC\uADF8 \uC801\uC815 \uAE38\uC774 \uC124\uC815 \uC5EC\uBD80",
|
|
1179
1240
|
description: "The document title is the single most-cited piece of text and should be 10\u201370 characters.",
|
|
1180
1241
|
run(ctx) {
|
|
1181
1242
|
const title = ctx.$("head > title").first().text().trim();
|
|
@@ -1184,6 +1245,7 @@ var titleRule = defineRule({
|
|
|
1184
1245
|
status: "fail",
|
|
1185
1246
|
score: 0,
|
|
1186
1247
|
rationale: "Page has no <title>.",
|
|
1248
|
+
rationale_ko: "\uD398\uC774\uC9C0\uC5D0 <title>\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.",
|
|
1187
1249
|
fixUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md"
|
|
1188
1250
|
};
|
|
1189
1251
|
}
|
|
@@ -1191,17 +1253,19 @@ var titleRule = defineRule({
|
|
|
1191
1253
|
return {
|
|
1192
1254
|
status: "warn",
|
|
1193
1255
|
score: 0.4,
|
|
1194
|
-
rationale: `Title is only ${title.length} chars; consider a more descriptive one
|
|
1256
|
+
rationale: `Title is only ${title.length} chars; consider a more descriptive one.`,
|
|
1257
|
+
rationale_ko: `\uC81C\uBAA9\uC774 ${title.length}\uC790\uBC16\uC5D0 \uC548 \uB429\uB2C8\uB2E4. \uB354 \uAD6C\uCCB4\uC801\uC73C\uB85C \uC791\uC131\uD558\uC138\uC694.`
|
|
1195
1258
|
};
|
|
1196
1259
|
}
|
|
1197
1260
|
if (title.length > 70) {
|
|
1198
1261
|
return {
|
|
1199
1262
|
status: "warn",
|
|
1200
1263
|
score: 0.6,
|
|
1201
|
-
rationale: `Title is ${title.length} chars; search UIs commonly truncate after ~70
|
|
1264
|
+
rationale: `Title is ${title.length} chars; search UIs commonly truncate after ~70.`,
|
|
1265
|
+
rationale_ko: `\uC81C\uBAA9\uC774 ${title.length}\uC790\uC785\uB2C8\uB2E4. \uAC80\uC0C9 UI\uC5D0\uC11C 70\uC790 \uC774\uD6C4\uB294 \uC798\uB9B4 \uC218 \uC788\uC2B5\uB2C8\uB2E4.`
|
|
1202
1266
|
};
|
|
1203
1267
|
}
|
|
1204
|
-
return { status: "pass", score: 1, rationale: `Title length ${title.length} is within range.` };
|
|
1268
|
+
return { status: "pass", score: 1, rationale: `Title length ${title.length} is within range.`, rationale_ko: `\uC81C\uBAA9 \uAE38\uC774 ${title.length}\uC790\uB85C \uC801\uC808\uD569\uB2C8\uB2E4.` };
|
|
1205
1269
|
}
|
|
1206
1270
|
});
|
|
1207
1271
|
|
|
@@ -1216,6 +1280,7 @@ var metaDescriptionRule = defineRule({
|
|
|
1216
1280
|
effort: "low",
|
|
1217
1281
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#citmeta-description",
|
|
1218
1282
|
title: "meta description is set (50\u2013160 chars)",
|
|
1283
|
+
title_ko: "meta description \uC124\uC815 \uC5EC\uBD80 (50\u2013160\uC790)",
|
|
1219
1284
|
description: "AI snippets often quote the meta description verbatim; aim for 50\u2013160 chars.",
|
|
1220
1285
|
run(ctx) {
|
|
1221
1286
|
const desc = ctx.$('head meta[name="description"]').attr("content")?.trim() ?? "";
|
|
@@ -1224,16 +1289,17 @@ var metaDescriptionRule = defineRule({
|
|
|
1224
1289
|
status: "warn",
|
|
1225
1290
|
score: 0,
|
|
1226
1291
|
rationale: "No meta description set.",
|
|
1292
|
+
rationale_ko: "meta description\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.",
|
|
1227
1293
|
fixUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md"
|
|
1228
1294
|
};
|
|
1229
1295
|
}
|
|
1230
1296
|
if (desc.length < 50) {
|
|
1231
|
-
return { status: "warn", score: 0.5, rationale: `Only ${desc.length} chars; aim for 50
|
|
1297
|
+
return { status: "warn", score: 0.5, rationale: `Only ${desc.length} chars; aim for 50+.`, rationale_ko: `${desc.length}\uC790\uBC16\uC5D0 \uC548 \uB429\uB2C8\uB2E4. 50\uC790 \uC774\uC0C1\uC744 \uBAA9\uD45C\uB85C \uD558\uC138\uC694.` };
|
|
1232
1298
|
}
|
|
1233
1299
|
if (desc.length > 160) {
|
|
1234
|
-
return { status: "warn", score: 0.7, rationale: `${desc.length} chars; may be truncated after 160.` };
|
|
1300
|
+
return { status: "warn", score: 0.7, rationale: `${desc.length} chars; may be truncated after 160.`, rationale_ko: `${desc.length}\uC790\uC785\uB2C8\uB2E4. 160\uC790 \uC774\uD6C4\uB294 \uC798\uB9B4 \uC218 \uC788\uC2B5\uB2C8\uB2E4.` };
|
|
1235
1301
|
}
|
|
1236
|
-
return { status: "pass", score: 1, rationale: `Description length ${desc.length} is within range.` };
|
|
1302
|
+
return { status: "pass", score: 1, rationale: `Description length ${desc.length} is within range.`, rationale_ko: `\uC124\uBA85 \uAE38\uC774 ${desc.length}\uC790\uB85C \uC801\uC808\uD569\uB2C8\uB2E4.` };
|
|
1237
1303
|
}
|
|
1238
1304
|
});
|
|
1239
1305
|
|
|
@@ -1248,6 +1314,7 @@ var canonicalRule = defineRule({
|
|
|
1248
1314
|
effort: "low",
|
|
1249
1315
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#citcanonical",
|
|
1250
1316
|
title: "Canonical URL is declared",
|
|
1317
|
+
title_ko: "Canonical URL \uC120\uC5B8 \uC5EC\uBD80",
|
|
1251
1318
|
description: 'rel="canonical" tells crawlers which URL is the source of truth, preventing duplicate-citation confusion.',
|
|
1252
1319
|
run(ctx) {
|
|
1253
1320
|
const href = ctx.$('head link[rel="canonical"]').attr("href")?.trim();
|
|
@@ -1256,14 +1323,15 @@ var canonicalRule = defineRule({
|
|
|
1256
1323
|
status: "warn",
|
|
1257
1324
|
score: 0,
|
|
1258
1325
|
rationale: 'No <link rel="canonical"> found.',
|
|
1326
|
+
rationale_ko: '<link rel="canonical">\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.',
|
|
1259
1327
|
fixUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md"
|
|
1260
1328
|
};
|
|
1261
1329
|
}
|
|
1262
1330
|
try {
|
|
1263
1331
|
const abs = new URL(href, ctx.finalUrl).toString();
|
|
1264
|
-
return { status: "pass", score: 1, rationale: `Canonical URL: ${abs}.` };
|
|
1332
|
+
return { status: "pass", score: 1, rationale: `Canonical URL: ${abs}.`, rationale_ko: `Canonical URL: ${abs}.` };
|
|
1265
1333
|
} catch {
|
|
1266
|
-
return { status: "fail", score: 0, rationale: `Canonical href is not a valid URL: ${href}` };
|
|
1334
|
+
return { status: "fail", score: 0, rationale: `Canonical href is not a valid URL: ${href}`, rationale_ko: `Canonical href\uAC00 \uC720\uD6A8\uD55C URL\uC774 \uC544\uB2D9\uB2C8\uB2E4: ${href}` };
|
|
1267
1335
|
}
|
|
1268
1336
|
}
|
|
1269
1337
|
});
|
|
@@ -1280,6 +1348,7 @@ var ogTagsRule = defineRule({
|
|
|
1280
1348
|
effort: "low",
|
|
1281
1349
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#citog-tags",
|
|
1282
1350
|
title: "Open Graph tags are set",
|
|
1351
|
+
title_ko: "Open Graph \uD0DC\uADF8 \uC124\uC815 \uC5EC\uBD80",
|
|
1283
1352
|
description: "og:title/type/url/image power rich previews on AI chat, social, and messaging.",
|
|
1284
1353
|
run(ctx) {
|
|
1285
1354
|
const missing = [];
|
|
@@ -1288,13 +1357,14 @@ var ogTagsRule = defineRule({
|
|
|
1288
1357
|
if (!val) missing.push(prop);
|
|
1289
1358
|
}
|
|
1290
1359
|
if (missing.length === 0) {
|
|
1291
|
-
return { status: "pass", score: 1, rationale: "All required OG tags present." };
|
|
1360
|
+
return { status: "pass", score: 1, rationale: "All required OG tags present.", rationale_ko: "\uD544\uC218 OG \uD0DC\uADF8\uAC00 \uBAA8\uB450 \uC788\uC2B5\uB2C8\uB2E4." };
|
|
1292
1361
|
}
|
|
1293
1362
|
const ratio = 1 - missing.length / REQUIRED.length;
|
|
1294
1363
|
return {
|
|
1295
1364
|
status: missing.length === REQUIRED.length ? "fail" : "warn",
|
|
1296
1365
|
score: ratio,
|
|
1297
1366
|
rationale: `Missing: ${missing.join(", ")}.`,
|
|
1367
|
+
rationale_ko: `\uB204\uB77D\uB41C \uD0DC\uADF8: ${missing.join(", ")}.`,
|
|
1298
1368
|
fixUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md"
|
|
1299
1369
|
};
|
|
1300
1370
|
}
|
|
@@ -1311,20 +1381,22 @@ var twitterCardRule = defineRule({
|
|
|
1311
1381
|
effort: "low",
|
|
1312
1382
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#cittwitter-card",
|
|
1313
1383
|
title: "Twitter Card metadata is set",
|
|
1384
|
+
title_ko: "Twitter Card \uBA54\uD0C0\uB370\uC774\uD130 \uC124\uC815 \uC5EC\uBD80",
|
|
1314
1385
|
description: "twitter:card + twitter:title give better previews on X/Twitter and some AI surfaces that reuse the tags.",
|
|
1315
1386
|
run(ctx) {
|
|
1316
1387
|
const card = ctx.$('head meta[name="twitter:card"]').attr("content")?.trim();
|
|
1317
1388
|
const title = ctx.$('head meta[name="twitter:title"]').attr("content")?.trim();
|
|
1318
1389
|
if (card && title) {
|
|
1319
|
-
return { status: "pass", score: 1, rationale: `Card type: ${card}.` };
|
|
1390
|
+
return { status: "pass", score: 1, rationale: `Card type: ${card}.`, rationale_ko: `\uCE74\uB4DC \uC720\uD615: ${card}.` };
|
|
1320
1391
|
}
|
|
1321
1392
|
if (card || title) {
|
|
1322
|
-
return { status: "warn", score: 0.5, rationale: "Partial twitter:* metadata; add the missing tag." };
|
|
1393
|
+
return { status: "warn", score: 0.5, rationale: "Partial twitter:* metadata; add the missing tag.", rationale_ko: "twitter:* \uBA54\uD0C0\uB370\uC774\uD130\uAC00 \uC77C\uBD80\uB9CC \uC788\uC2B5\uB2C8\uB2E4. \uB204\uB77D\uB41C \uD0DC\uADF8\uB97C \uCD94\uAC00\uD558\uC138\uC694." };
|
|
1323
1394
|
}
|
|
1324
1395
|
return {
|
|
1325
1396
|
status: "warn",
|
|
1326
1397
|
score: 0,
|
|
1327
1398
|
rationale: "No twitter:card metadata.",
|
|
1399
|
+
rationale_ko: "twitter:card \uBA54\uD0C0\uB370\uC774\uD130\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.",
|
|
1328
1400
|
fixUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md"
|
|
1329
1401
|
};
|
|
1330
1402
|
}
|
|
@@ -1341,6 +1413,7 @@ var langAttrRule = defineRule({
|
|
|
1341
1413
|
effort: "low",
|
|
1342
1414
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#citlang-attr",
|
|
1343
1415
|
title: "<html lang> is set",
|
|
1416
|
+
title_ko: "<html lang> \uC18D\uC131 \uC124\uC815 \uC5EC\uBD80",
|
|
1344
1417
|
description: "A lang attribute helps AI engines route the page to the right-language search surface (and helps screen readers).",
|
|
1345
1418
|
run(ctx) {
|
|
1346
1419
|
const lang = ctx.$("html").attr("lang")?.trim();
|
|
@@ -1349,10 +1422,11 @@ var langAttrRule = defineRule({
|
|
|
1349
1422
|
status: "warn",
|
|
1350
1423
|
score: 0,
|
|
1351
1424
|
rationale: "No lang attribute on <html>.",
|
|
1425
|
+
rationale_ko: "<html>\uC5D0 lang \uC18D\uC131\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.",
|
|
1352
1426
|
fixUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md"
|
|
1353
1427
|
};
|
|
1354
1428
|
}
|
|
1355
|
-
return { status: "pass", score: 1, rationale: `lang="${lang}".` };
|
|
1429
|
+
return { status: "pass", score: 1, rationale: `lang="${lang}".`, rationale_ko: `lang="${lang}".` };
|
|
1356
1430
|
}
|
|
1357
1431
|
});
|
|
1358
1432
|
|
|
@@ -1367,25 +1441,27 @@ var authorVisibleRule = defineRule({
|
|
|
1367
1441
|
effort: "medium",
|
|
1368
1442
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#citauthor-visible",
|
|
1369
1443
|
title: "Author is declared",
|
|
1444
|
+
title_ko: "\uC791\uC131\uC790 \uC815\uBCF4 \uC120\uC5B8 \uC5EC\uBD80",
|
|
1370
1445
|
description: "AI engines prefer citing content with an identifiable author; expose one via JSON-LD, meta[name=author], rel=author, or a .author class.",
|
|
1371
1446
|
run(ctx) {
|
|
1372
1447
|
for (const node of flattenJsonLd(ctx.jsonLd)) {
|
|
1373
1448
|
if (hasField(node, "author")) {
|
|
1374
|
-
return { status: "pass", score: 1, rationale: "Author found in JSON-LD." };
|
|
1449
|
+
return { status: "pass", score: 1, rationale: "Author found in JSON-LD.", rationale_ko: "JSON-LD\uC5D0\uC11C \uC791\uC131\uC790 \uC815\uBCF4\uB97C \uCC3E\uC558\uC2B5\uB2C8\uB2E4." };
|
|
1375
1450
|
}
|
|
1376
1451
|
}
|
|
1377
1452
|
const metaAuthor = ctx.$('head meta[name="author"]').attr("content")?.trim();
|
|
1378
|
-
if (metaAuthor) return { status: "pass", score: 1, rationale: `meta[name=author] = "${metaAuthor}".` };
|
|
1453
|
+
if (metaAuthor) return { status: "pass", score: 1, rationale: `meta[name=author] = "${metaAuthor}".`, rationale_ko: `meta[name=author] = "${metaAuthor}".` };
|
|
1379
1454
|
if (ctx.$('[rel="author"]').length > 0) {
|
|
1380
|
-
return { status: "pass", score: 1, rationale: 'rel="author" link found.' };
|
|
1455
|
+
return { status: "pass", score: 1, rationale: 'rel="author" link found.', rationale_ko: 'rel="author" \uB9C1\uD06C\uB97C \uCC3E\uC558\uC2B5\uB2C8\uB2E4.' };
|
|
1381
1456
|
}
|
|
1382
1457
|
if (ctx.$('.author, [class*="author"], [itemprop="author"]').length > 0) {
|
|
1383
|
-
return { status: "pass", score: 0.8, rationale: "Author-ish DOM selector found (weaker signal)." };
|
|
1458
|
+
return { status: "pass", score: 0.8, rationale: "Author-ish DOM selector found (weaker signal).", rationale_ko: "\uC791\uC131\uC790 \uAD00\uB828 DOM \uC120\uD0DD\uC790\uAC00 \uC788\uC2B5\uB2C8\uB2E4 (\uC2E0\uD638 \uAC15\uB3C4 \uB0AE\uC74C)." };
|
|
1384
1459
|
}
|
|
1385
1460
|
return {
|
|
1386
1461
|
status: "warn",
|
|
1387
1462
|
score: 0,
|
|
1388
1463
|
rationale: "No author signal found (JSON-LD, meta, rel, or .author).",
|
|
1464
|
+
rationale_ko: "\uC791\uC131\uC790 \uC815\uBCF4\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4 (JSON-LD, meta, rel, .author \uBAA8\uB450 \uC5C6\uC74C).",
|
|
1389
1465
|
fixUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md"
|
|
1390
1466
|
};
|
|
1391
1467
|
}
|
|
@@ -1402,25 +1478,27 @@ var datesRule = defineRule({
|
|
|
1402
1478
|
effort: "low",
|
|
1403
1479
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#citdates",
|
|
1404
1480
|
title: "Publish / modified date is present",
|
|
1481
|
+
title_ko: "\uBC1C\uD589\uC77C / \uC218\uC815\uC77C \uC874\uC7AC \uC5EC\uBD80",
|
|
1405
1482
|
description: "AI engines rank recent pages higher; expose datePublished via JSON-LD, <time datetime>, or article:published_time meta.",
|
|
1406
1483
|
run(ctx) {
|
|
1407
1484
|
for (const node of flattenJsonLd(ctx.jsonLd)) {
|
|
1408
1485
|
if (hasField(node, "datePublished")) {
|
|
1409
|
-
return { status: "pass", score: 1, rationale: "datePublished found in JSON-LD." };
|
|
1486
|
+
return { status: "pass", score: 1, rationale: "datePublished found in JSON-LD.", rationale_ko: "JSON-LD\uC5D0\uC11C datePublished\uB97C \uCC3E\uC558\uC2B5\uB2C8\uB2E4." };
|
|
1410
1487
|
}
|
|
1411
1488
|
}
|
|
1412
1489
|
const articleTime = ctx.$('head meta[property="article:published_time"]').attr("content")?.trim();
|
|
1413
1490
|
if (articleTime) {
|
|
1414
|
-
return { status: "pass", score: 1, rationale: `article:published_time = ${articleTime}.` };
|
|
1491
|
+
return { status: "pass", score: 1, rationale: `article:published_time = ${articleTime}.`, rationale_ko: `article:published_time = ${articleTime}.` };
|
|
1415
1492
|
}
|
|
1416
1493
|
const timeEl = ctx.$("time[datetime]").first().attr("datetime")?.trim();
|
|
1417
1494
|
if (timeEl) {
|
|
1418
|
-
return { status: "pass", score: 0.8, rationale: `<time datetime="${timeEl}"> found.` };
|
|
1495
|
+
return { status: "pass", score: 0.8, rationale: `<time datetime="${timeEl}"> found.`, rationale_ko: `<time datetime="${timeEl}">\uB97C \uCC3E\uC558\uC2B5\uB2C8\uB2E4.` };
|
|
1419
1496
|
}
|
|
1420
1497
|
return {
|
|
1421
1498
|
status: "warn",
|
|
1422
1499
|
score: 0,
|
|
1423
1500
|
rationale: "No publish date found (JSON-LD, meta article:published_time, or <time datetime>).",
|
|
1501
|
+
rationale_ko: "\uBC1C\uD589\uC77C\uC774 \uC5C6\uC2B5\uB2C8\uB2E4 (JSON-LD, meta article:published_time, <time datetime> \uBAA8\uB450 \uC5C6\uC74C).",
|
|
1424
1502
|
fixUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md"
|
|
1425
1503
|
};
|
|
1426
1504
|
}
|
|
@@ -1448,6 +1526,7 @@ var contentFreshnessRule = defineRule({
|
|
|
1448
1526
|
effort: "low",
|
|
1449
1527
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#citcontent-freshness",
|
|
1450
1528
|
title: "Article content is fresh (dateModified within 1 year)",
|
|
1529
|
+
title_ko: "\uCF58\uD150\uCE20 \uCD5C\uC2E0\uC131 (dateModified 1\uB144 \uC774\uB0B4)",
|
|
1451
1530
|
description: "AI engines down-rank stale content. Surface a recent dateModified (\u2264365 days) on Article-like pages so retrieval rankings stay strong.",
|
|
1452
1531
|
run(ctx) {
|
|
1453
1532
|
const nodes = flattenJsonLd(ctx.jsonLd);
|
|
@@ -1456,7 +1535,8 @@ var contentFreshnessRule = defineRule({
|
|
|
1456
1535
|
return {
|
|
1457
1536
|
status: "skip",
|
|
1458
1537
|
score: 0,
|
|
1459
|
-
rationale: "No Article/BlogPosting/NewsArticle JSON-LD; freshness signal not applicable."
|
|
1538
|
+
rationale: "No Article/BlogPosting/NewsArticle JSON-LD; freshness signal not applicable.",
|
|
1539
|
+
rationale_ko: "Article/BlogPosting/NewsArticle JSON-LD\uAC00 \uC5C6\uC5B4 \uCD5C\uC2E0\uC131 \uC2E0\uD638\uB97C \uD655\uC778\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4."
|
|
1460
1540
|
};
|
|
1461
1541
|
}
|
|
1462
1542
|
let bestMs = null;
|
|
@@ -1478,6 +1558,7 @@ var contentFreshnessRule = defineRule({
|
|
|
1478
1558
|
status: "warn",
|
|
1479
1559
|
score: 0,
|
|
1480
1560
|
rationale: "Article has no parseable dateModified or datePublished.",
|
|
1561
|
+
rationale_ko: "Article JSON-LD\uC5D0 \uD30C\uC2F1 \uAC00\uB2A5\uD55C dateModified \uB610\uB294 datePublished\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.",
|
|
1481
1562
|
fixHint: "Add ISO-8601 dateModified and datePublished to your Article JSON-LD.",
|
|
1482
1563
|
estimatedImpact: 3
|
|
1483
1564
|
};
|
|
@@ -1488,6 +1569,7 @@ var contentFreshnessRule = defineRule({
|
|
|
1488
1569
|
status: "pass",
|
|
1489
1570
|
score: 1,
|
|
1490
1571
|
rationale: `${usedField} within the last year (~${ageDays} day${ageDays === 1 ? "" : "s"} ago).`,
|
|
1572
|
+
rationale_ko: `${usedField}\uC774 1\uB144 \uC774\uB0B4\uC785\uB2C8\uB2E4 (\uC57D ${ageDays}\uC77C \uC804).`,
|
|
1491
1573
|
evidence: { ageDays, field: usedField }
|
|
1492
1574
|
};
|
|
1493
1575
|
}
|
|
@@ -1496,6 +1578,7 @@ var contentFreshnessRule = defineRule({
|
|
|
1496
1578
|
status: "warn",
|
|
1497
1579
|
score: 0.6,
|
|
1498
1580
|
rationale: `${usedField} is ${ageDays} days old. Refresh within a year for best AI ranking.`,
|
|
1581
|
+
rationale_ko: `${usedField}\uC774 ${ageDays}\uC77C \uB410\uC2B5\uB2C8\uB2E4. AI \uC21C\uC704\uB97C \uC720\uC9C0\uD558\uB824\uBA74 1\uB144 \uC774\uB0B4\uB85C \uAC31\uC2E0\uD558\uC138\uC694.`,
|
|
1499
1582
|
evidence: { ageDays, field: usedField },
|
|
1500
1583
|
estimatedImpact: 2
|
|
1501
1584
|
};
|
|
@@ -1504,6 +1587,7 @@ var contentFreshnessRule = defineRule({
|
|
|
1504
1587
|
status: "warn",
|
|
1505
1588
|
score: 0.2,
|
|
1506
1589
|
rationale: `${usedField} is ${ageDays} days old (>2 years). AI engines treat this as stale.`,
|
|
1590
|
+
rationale_ko: `${usedField}\uC774 ${ageDays}\uC77C \uB410\uC2B5\uB2C8\uB2E4 (2\uB144 \uCD08\uACFC). AI \uC5D4\uC9C4\uC774 \uC624\uB798\uB41C \uCF58\uD150\uCE20\uB85C \uAC04\uC8FC\uD569\uB2C8\uB2E4.`,
|
|
1507
1591
|
evidence: { ageDays, field: usedField },
|
|
1508
1592
|
fixHint: "Update content and bump dateModified to today's date.",
|
|
1509
1593
|
estimatedImpact: 3
|
|
@@ -1535,22 +1619,25 @@ var singleH1Rule = defineRule({
|
|
|
1535
1619
|
effort: "low",
|
|
1536
1620
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#cntsingle-h1",
|
|
1537
1621
|
title: "Exactly one <h1>",
|
|
1622
|
+
title_ko: "<h1> \uD0DC\uADF8 1\uAC1C \uC5EC\uBD80",
|
|
1538
1623
|
description: "A single H1 tells AI engines the primary topic of the page without ambiguity.",
|
|
1539
1624
|
run(ctx) {
|
|
1540
1625
|
const n = ctx.$("h1").length;
|
|
1541
|
-
if (n === 1) return { status: "pass", score: 1, rationale: "Exactly one <h1>." };
|
|
1626
|
+
if (n === 1) return { status: "pass", score: 1, rationale: "Exactly one <h1>.", rationale_ko: "<h1>\uC774 \uC815\uD655\uD788 1\uAC1C\uC785\uB2C8\uB2E4." };
|
|
1542
1627
|
if (n === 0) {
|
|
1543
1628
|
return {
|
|
1544
1629
|
status: "fail",
|
|
1545
1630
|
score: 0,
|
|
1546
1631
|
rationale: "No <h1> on the page.",
|
|
1632
|
+
rationale_ko: "\uD398\uC774\uC9C0\uC5D0 <h1>\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.",
|
|
1547
1633
|
fixUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules/cnt.single-h1.md"
|
|
1548
1634
|
};
|
|
1549
1635
|
}
|
|
1550
1636
|
return {
|
|
1551
1637
|
status: "warn",
|
|
1552
1638
|
score: Math.max(0.3, 1 / n),
|
|
1553
|
-
rationale: `Found ${n} <h1> tags; prefer one primary heading
|
|
1639
|
+
rationale: `Found ${n} <h1> tags; prefer one primary heading.`,
|
|
1640
|
+
rationale_ko: `<h1>\uC774 ${n}\uAC1C \uC788\uC2B5\uB2C8\uB2E4. \uB300\uD45C \uC81C\uBAA9 1\uAC1C\uB9CC \uC0AC\uC6A9\uD558\uC138\uC694.`
|
|
1554
1641
|
};
|
|
1555
1642
|
}
|
|
1556
1643
|
});
|
|
@@ -1566,6 +1653,7 @@ var headingHierarchyRule = defineRule({
|
|
|
1566
1653
|
effort: "medium",
|
|
1567
1654
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#cntheading-hierarchy",
|
|
1568
1655
|
title: "Heading levels do not skip",
|
|
1656
|
+
title_ko: "\uC81C\uBAA9 \uB2E8\uACC4 \uC21C\uC11C \uC900\uC218 \uC5EC\uBD80",
|
|
1569
1657
|
description: "Going from H2 directly to H4 breaks the outline AI engines use to segment content.",
|
|
1570
1658
|
run(ctx) {
|
|
1571
1659
|
const levels = [];
|
|
@@ -1575,7 +1663,7 @@ var headingHierarchyRule = defineRule({
|
|
|
1575
1663
|
if (m?.[1]) levels.push(parseInt(m[1], 10));
|
|
1576
1664
|
});
|
|
1577
1665
|
if (levels.length === 0) {
|
|
1578
|
-
return { status: "skip", score: 0, rationale: "No headings found." };
|
|
1666
|
+
return { status: "skip", score: 0, rationale: "No headings found.", rationale_ko: "\uC81C\uBAA9 \uD0DC\uADF8\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4." };
|
|
1579
1667
|
}
|
|
1580
1668
|
const skips = [];
|
|
1581
1669
|
for (let i = 1; i < levels.length; i++) {
|
|
@@ -1584,12 +1672,13 @@ var headingHierarchyRule = defineRule({
|
|
|
1584
1672
|
if (curr > prev + 1) skips.push({ from: prev, to: curr });
|
|
1585
1673
|
}
|
|
1586
1674
|
if (skips.length === 0) {
|
|
1587
|
-
return { status: "pass", score: 1, rationale: "No heading-level skips." };
|
|
1675
|
+
return { status: "pass", score: 1, rationale: "No heading-level skips.", rationale_ko: "\uC81C\uBAA9 \uB2E8\uACC4\uAC00 \uC21C\uC11C\uB300\uB85C \uC0AC\uC6A9\uB429\uB2C8\uB2E4." };
|
|
1588
1676
|
}
|
|
1589
1677
|
return {
|
|
1590
1678
|
status: "warn",
|
|
1591
1679
|
score: Math.max(0.3, 1 - skips.length / levels.length),
|
|
1592
1680
|
rationale: `${skips.length} heading skip(s) detected (e.g. h${skips[0].from}\u2192h${skips[0].to}).`,
|
|
1681
|
+
rationale_ko: `\uC81C\uBAA9 \uB2E8\uACC4 \uAC74\uB108\uB700\uC774 ${skips.length}\uAC1C \uAC10\uC9C0\uB429\uB2C8\uB2E4 (\uC608: h${skips[0].from}\u2192h${skips[0].to}).`,
|
|
1593
1682
|
fixUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md"
|
|
1594
1683
|
};
|
|
1595
1684
|
}
|
|
@@ -1606,11 +1695,12 @@ var imageAltRule = defineRule({
|
|
|
1606
1695
|
effort: "medium",
|
|
1607
1696
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#cntimage-alt",
|
|
1608
1697
|
title: "\u226580% of <img> have alt text",
|
|
1698
|
+
title_ko: "<img>\uC758 80% \uC774\uC0C1 alt \uD14D\uC2A4\uD2B8 \uBCF4\uC720 \uC5EC\uBD80",
|
|
1609
1699
|
description: "Alt text gives AI engines a textual anchor for visual content and improves accessibility.",
|
|
1610
1700
|
run(ctx) {
|
|
1611
1701
|
const imgs = ctx.$("img");
|
|
1612
1702
|
const total = imgs.length;
|
|
1613
|
-
if (total === 0) return { status: "skip", score: 0, rationale: "No <img> on the page." };
|
|
1703
|
+
if (total === 0) return { status: "skip", score: 0, rationale: "No <img> on the page.", rationale_ko: "\uD398\uC774\uC9C0\uC5D0 <img>\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4." };
|
|
1614
1704
|
let withAlt = 0;
|
|
1615
1705
|
imgs.each((_i, el) => {
|
|
1616
1706
|
const alt = ctx.$(el).attr("alt");
|
|
@@ -1618,12 +1708,13 @@ var imageAltRule = defineRule({
|
|
|
1618
1708
|
});
|
|
1619
1709
|
const ratio = withAlt / total;
|
|
1620
1710
|
if (ratio >= 0.8) {
|
|
1621
|
-
return { status: "pass", score: 1, rationale: `${withAlt}/${total} images have alt (${Math.round(ratio * 100)}%).` };
|
|
1711
|
+
return { status: "pass", score: 1, rationale: `${withAlt}/${total} images have alt (${Math.round(ratio * 100)}%).`, rationale_ko: `\uC774\uBBF8\uC9C0 ${total}\uAC1C \uC911 ${withAlt}\uAC1C\uC5D0 alt\uAC00 \uC788\uC2B5\uB2C8\uB2E4 (${Math.round(ratio * 100)}%).` };
|
|
1622
1712
|
}
|
|
1623
1713
|
return {
|
|
1624
1714
|
status: "warn",
|
|
1625
1715
|
score: ratio,
|
|
1626
1716
|
rationale: `Only ${withAlt}/${total} images have alt text (${Math.round(ratio * 100)}%). Aim for \u226580%.`,
|
|
1717
|
+
rationale_ko: `\uC774\uBBF8\uC9C0 ${total}\uAC1C \uC911 ${withAlt}\uAC1C\uB9CC alt \uD14D\uC2A4\uD2B8\uAC00 \uC788\uC2B5\uB2C8\uB2E4 (${Math.round(ratio * 100)}%). 80% \uC774\uC0C1\uC744 \uBAA9\uD45C\uB85C \uD558\uC138\uC694.`,
|
|
1627
1718
|
fixUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md"
|
|
1628
1719
|
};
|
|
1629
1720
|
}
|
|
@@ -1640,11 +1731,12 @@ var tldrOrFaqRule = defineRule({
|
|
|
1640
1731
|
effort: "medium",
|
|
1641
1732
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#cnttldr-or-faq",
|
|
1642
1733
|
title: "TL;DR summary or FAQ block",
|
|
1734
|
+
title_ko: "TL;DR \uC694\uC57D \uB610\uB294 FAQ \uBE14\uB85D \uC874\uC7AC \uC5EC\uBD80",
|
|
1643
1735
|
description: 'AI engines strongly prefer content with a quotable summary or FAQ \u2014 it makes the page "citation-ready".',
|
|
1644
1736
|
run(ctx) {
|
|
1645
1737
|
for (const node of flattenJsonLd(ctx.jsonLd)) {
|
|
1646
1738
|
if (getTypes(node).includes("FAQPage")) {
|
|
1647
|
-
return { status: "pass", score: 1, rationale: "FAQPage schema present." };
|
|
1739
|
+
return { status: "pass", score: 1, rationale: "FAQPage schema present.", rationale_ko: "FAQPage \uC2A4\uD0A4\uB9C8\uAC00 \uC788\uC2B5\uB2C8\uB2E4." };
|
|
1648
1740
|
}
|
|
1649
1741
|
}
|
|
1650
1742
|
const sel = [
|
|
@@ -1657,12 +1749,13 @@ var tldrOrFaqRule = defineRule({
|
|
|
1657
1749
|
"[data-tldr]"
|
|
1658
1750
|
].join(", ");
|
|
1659
1751
|
if (ctx.$(sel).length > 0) {
|
|
1660
|
-
return { status: "pass", score: 0.85, rationale: "TL;DR / summary / FAQ region detected by selector." };
|
|
1752
|
+
return { status: "pass", score: 0.85, rationale: "TL;DR / summary / FAQ region detected by selector.", rationale_ko: "TL;DR / \uC694\uC57D / FAQ \uC601\uC5ED\uC774 \uAC10\uC9C0\uB429\uB2C8\uB2E4." };
|
|
1661
1753
|
}
|
|
1662
1754
|
return {
|
|
1663
1755
|
status: "warn",
|
|
1664
1756
|
score: 0,
|
|
1665
1757
|
rationale: "No TL;DR / summary / FAQ found; add one to boost AI citation odds.",
|
|
1758
|
+
rationale_ko: "TL;DR / \uC694\uC57D / FAQ\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4. AI \uC778\uC6A9 \uAC00\uB2A5\uC131\uC744 \uB192\uC774\uB824\uBA74 \uCD94\uAC00\uD558\uC138\uC694.",
|
|
1666
1759
|
fixUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md"
|
|
1667
1760
|
};
|
|
1668
1761
|
}
|
|
@@ -1679,6 +1772,7 @@ var wordCountRule = defineRule({
|
|
|
1679
1772
|
effort: "high",
|
|
1680
1773
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#cntword-count",
|
|
1681
1774
|
title: "Page has enough body text",
|
|
1775
|
+
title_ko: "\uCDA9\uBD84\uD55C \uBCF8\uBB38 \uD14D\uC2A4\uD2B8 \uC5EC\uBD80",
|
|
1682
1776
|
description: "Thin pages (under ~100 words) are rarely cited by AI engines. Aim for \u2265300 words of meaningful body copy.",
|
|
1683
1777
|
run(ctx) {
|
|
1684
1778
|
const $ = ctx.$;
|
|
@@ -1686,12 +1780,13 @@ var wordCountRule = defineRule({
|
|
|
1686
1780
|
clone.find("script, style, noscript, nav, header, footer, aside").remove();
|
|
1687
1781
|
const text = clone.text().replace(/\s+/g, " ").trim();
|
|
1688
1782
|
const words = text ? text.split(" ").length : 0;
|
|
1689
|
-
if (words >= 300) return { status: "pass", score: 1, rationale: `${words} words of body text.` };
|
|
1690
|
-
if (words >= 100) return { status: "warn", score: 0.5, rationale: `Only ${words} words; aim for 300
|
|
1783
|
+
if (words >= 300) return { status: "pass", score: 1, rationale: `${words} words of body text.`, rationale_ko: `\uBCF8\uBB38 \uD14D\uC2A4\uD2B8\uAC00 ${words}\uB2E8\uC5B4 \uC788\uC2B5\uB2C8\uB2E4.` };
|
|
1784
|
+
if (words >= 100) return { status: "warn", score: 0.5, rationale: `Only ${words} words; aim for 300+.`, rationale_ko: `${words}\uB2E8\uC5B4\uBC16\uC5D0 \uC5C6\uC2B5\uB2C8\uB2E4. 300\uB2E8\uC5B4 \uC774\uC0C1\uC744 \uBAA9\uD45C\uB85C \uD558\uC138\uC694.` };
|
|
1691
1785
|
return {
|
|
1692
1786
|
status: "fail",
|
|
1693
1787
|
score: 0,
|
|
1694
1788
|
rationale: `Only ${words} words; too thin to be cited.`,
|
|
1789
|
+
rationale_ko: `${words}\uB2E8\uC5B4\uBC16\uC5D0 \uC5C6\uC2B5\uB2C8\uB2E4. AI \uC5D4\uC9C4\uC774 \uC778\uC6A9\uD558\uAE30\uC5D4 \uB108\uBB34 \uC801\uC2B5\uB2C8\uB2E4.`,
|
|
1695
1790
|
fixUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md"
|
|
1696
1791
|
};
|
|
1697
1792
|
}
|
|
@@ -1742,11 +1837,12 @@ var qaStructureRule = defineRule({
|
|
|
1742
1837
|
effort: "medium",
|
|
1743
1838
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#cntqa-structure",
|
|
1744
1839
|
title: "Content uses Q&A structure for answer extraction",
|
|
1840
|
+
title_ko: "\uB2F5\uBCC0 \uCD94\uCD9C\uC744 \uC704\uD55C Q&A \uAD6C\uC870 \uC0AC\uC6A9 \uC5EC\uBD80",
|
|
1745
1841
|
description: "Question-style H2/H3 headings (or FAQPage JSON-LD) help AI engines extract direct answers. Pages with \u22652 question headings are far more likely to be cited.",
|
|
1746
1842
|
run(ctx) {
|
|
1747
1843
|
for (const node of flattenJsonLd(ctx.jsonLd)) {
|
|
1748
1844
|
if (getTypes(node).includes("FAQPage")) {
|
|
1749
|
-
return { status: "pass", score: 1, rationale: "FAQPage JSON-LD provides explicit Q&A." };
|
|
1845
|
+
return { status: "pass", score: 1, rationale: "FAQPage JSON-LD provides explicit Q&A.", rationale_ko: "FAQPage JSON-LD\uB85C \uBA85\uC2DC\uC801\uC778 Q&A \uAD6C\uC870\uAC00 \uC788\uC2B5\uB2C8\uB2E4." };
|
|
1750
1846
|
}
|
|
1751
1847
|
}
|
|
1752
1848
|
const questionHeadings = [];
|
|
@@ -1759,6 +1855,7 @@ var qaStructureRule = defineRule({
|
|
|
1759
1855
|
status: "pass",
|
|
1760
1856
|
score: 1,
|
|
1761
1857
|
rationale: `${questionHeadings.length} question-style headings detected.`,
|
|
1858
|
+
rationale_ko: `\uC9C8\uBB38\uD615 \uC81C\uBAA9\uC774 ${questionHeadings.length}\uAC1C \uAC10\uC9C0\uB429\uB2C8\uB2E4.`,
|
|
1762
1859
|
evidence: { headings: questionHeadings.slice(0, 5) }
|
|
1763
1860
|
};
|
|
1764
1861
|
}
|
|
@@ -1767,6 +1864,7 @@ var qaStructureRule = defineRule({
|
|
|
1767
1864
|
status: "warn",
|
|
1768
1865
|
score: 0.6,
|
|
1769
1866
|
rationale: "1 question-style heading. Add a second to strengthen answer extraction.",
|
|
1867
|
+
rationale_ko: "\uC9C8\uBB38\uD615 \uC81C\uBAA9\uC774 1\uAC1C\uC785\uB2C8\uB2E4. \uB2F5\uBCC0 \uCD94\uCD9C \uAC15\uD654\uB97C \uC704\uD574 \uD558\uB098 \uB354 \uCD94\uAC00\uD558\uC138\uC694.",
|
|
1770
1868
|
evidence: { headings: questionHeadings },
|
|
1771
1869
|
estimatedImpact: 1
|
|
1772
1870
|
};
|
|
@@ -1775,6 +1873,7 @@ var qaStructureRule = defineRule({
|
|
|
1775
1873
|
status: "warn",
|
|
1776
1874
|
score: 0,
|
|
1777
1875
|
rationale: "No question-style H2/H3 headings or FAQPage JSON-LD found.",
|
|
1876
|
+
rationale_ko: "\uC9C8\uBB38\uD615 H2/H3 \uC81C\uBAA9\uC774\uB098 FAQPage JSON-LD\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.",
|
|
1778
1877
|
fixHint: 'Reframe at least 2 H2 headings as questions ("How do I\u2026?", "What is\u2026?") or add FAQPage JSON-LD.',
|
|
1779
1878
|
estimatedImpact: 3
|
|
1780
1879
|
};
|
|
@@ -1792,13 +1891,14 @@ var externalCitationsRule = defineRule({
|
|
|
1792
1891
|
effort: "medium",
|
|
1793
1892
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#cntexternal-citations",
|
|
1794
1893
|
title: "Content cites external sources",
|
|
1894
|
+
title_ko: "\uC678\uBD80 \uCD9C\uCC98 \uC778\uC6A9 \uC5EC\uBD80",
|
|
1795
1895
|
description: "Outbound links to authoritative external sources are an E-E-A-T trust signal. AI engines treat well-cited pages as more credible.",
|
|
1796
1896
|
run(ctx) {
|
|
1797
1897
|
let host;
|
|
1798
1898
|
try {
|
|
1799
1899
|
host = new URL(ctx.finalUrl).hostname.toLowerCase();
|
|
1800
1900
|
} catch {
|
|
1801
|
-
return { status: "skip", score: 0, rationale: "Invalid finalUrl." };
|
|
1901
|
+
return { status: "skip", score: 0, rationale: "Invalid finalUrl.", rationale_ko: "\uC720\uD6A8\uD558\uC9C0 \uC54A\uC740 finalUrl\uC785\uB2C8\uB2E4." };
|
|
1802
1902
|
}
|
|
1803
1903
|
const seen = /* @__PURE__ */ new Set();
|
|
1804
1904
|
ctx.$("main a[href], article a[href], body a[href]").each((_i, el) => {
|
|
@@ -1823,6 +1923,7 @@ var externalCitationsRule = defineRule({
|
|
|
1823
1923
|
status: "pass",
|
|
1824
1924
|
score: 1,
|
|
1825
1925
|
rationale: `${count} distinct external host(s) cited (excluding nofollow).`,
|
|
1926
|
+
rationale_ko: `\uC678\uBD80 \uC0AC\uC774\uD2B8 ${count}\uAC1C\uB97C \uC778\uC6A9\uD569\uB2C8\uB2E4 (nofollow \uC81C\uC678).`,
|
|
1826
1927
|
evidence: { hosts: [...seen].slice(0, 8) }
|
|
1827
1928
|
};
|
|
1828
1929
|
}
|
|
@@ -1831,6 +1932,7 @@ var externalCitationsRule = defineRule({
|
|
|
1831
1932
|
status: "pass",
|
|
1832
1933
|
score: 0.7,
|
|
1833
1934
|
rationale: `${count} external host(s) cited. Aim for \u22653 for stronger E-E-A-T.`,
|
|
1935
|
+
rationale_ko: `\uC678\uBD80 \uC0AC\uC774\uD2B8 ${count}\uAC1C\uB97C \uC778\uC6A9\uD569\uB2C8\uB2E4. E-E-A-T \uAC15\uD654\uB97C \uC704\uD574 3\uAC1C \uC774\uC0C1\uC744 \uBAA9\uD45C\uB85C \uD558\uC138\uC694.`,
|
|
1834
1936
|
evidence: { hosts: [...seen] },
|
|
1835
1937
|
estimatedImpact: 1
|
|
1836
1938
|
};
|
|
@@ -1839,6 +1941,7 @@ var externalCitationsRule = defineRule({
|
|
|
1839
1941
|
status: "warn",
|
|
1840
1942
|
score: 0,
|
|
1841
1943
|
rationale: "No external follow citations found in main content.",
|
|
1944
|
+
rationale_ko: "\uBCF8\uBB38\uC5D0 \uC678\uBD80 \uCD9C\uCC98 \uB9C1\uD06C\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.",
|
|
1842
1945
|
fixHint: "Cite at least one authoritative external source (research paper, official docs, news outlet).",
|
|
1843
1946
|
estimatedImpact: 2
|
|
1844
1947
|
};
|
|
@@ -1856,12 +1959,130 @@ var contentRules = [
|
|
|
1856
1959
|
externalCitationsRule
|
|
1857
1960
|
];
|
|
1858
1961
|
|
|
1962
|
+
// src/rules/aeo/skill-md.ts
|
|
1963
|
+
var aeoSkillMdRule = defineRule({
|
|
1964
|
+
id: "aeo.skill-md",
|
|
1965
|
+
stableId: "aeo.skill-md",
|
|
1966
|
+
category: "aeo",
|
|
1967
|
+
group: "opportunity",
|
|
1968
|
+
weight: 3,
|
|
1969
|
+
impact: "high",
|
|
1970
|
+
effort: "low",
|
|
1971
|
+
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#aeoskill-md",
|
|
1972
|
+
title: "skill.md is present",
|
|
1973
|
+
title_ko: "skill.md \uD30C\uC77C \uC874\uC7AC \uC5EC\uBD80",
|
|
1974
|
+
description: "A /skill.md file describes site capabilities so AI agents know what this site can do for them.",
|
|
1975
|
+
run(ctx) {
|
|
1976
|
+
if (ctx.skillMd !== null) {
|
|
1977
|
+
return {
|
|
1978
|
+
status: "pass",
|
|
1979
|
+
score: 1,
|
|
1980
|
+
rationale: "skill.md found at site root.",
|
|
1981
|
+
rationale_ko: "skill.md\uAC00 \uC0AC\uC774\uD2B8 \uB8E8\uD2B8\uC5D0 \uC874\uC7AC\uD569\uB2C8\uB2E4."
|
|
1982
|
+
};
|
|
1983
|
+
}
|
|
1984
|
+
return {
|
|
1985
|
+
status: "warn",
|
|
1986
|
+
score: 0,
|
|
1987
|
+
rationale: "No /skill.md found. Add one to describe your site capabilities to AI agents.",
|
|
1988
|
+
rationale_ko: "/skill.md\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4. AI \uC5D0\uC774\uC804\uD2B8\uAC00 \uC0AC\uC774\uD2B8 \uAE30\uB2A5\uC744 \uD30C\uC545\uD560 \uC218 \uC788\uB3C4\uB85D \uCD94\uAC00\uD558\uC138\uC694.",
|
|
1989
|
+
fixHint: "Create /skill.md listing what services, products, and capabilities this site offers."
|
|
1990
|
+
};
|
|
1991
|
+
}
|
|
1992
|
+
});
|
|
1993
|
+
|
|
1994
|
+
// src/rules/aeo/agent-permissions.ts
|
|
1995
|
+
var aeoAgentPermissionsRule = defineRule({
|
|
1996
|
+
id: "aeo.agent-permissions",
|
|
1997
|
+
stableId: "aeo.agent-permissions",
|
|
1998
|
+
category: "aeo",
|
|
1999
|
+
group: "opportunity",
|
|
2000
|
+
weight: 3,
|
|
2001
|
+
impact: "medium",
|
|
2002
|
+
effort: "low",
|
|
2003
|
+
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#aeoagent-permissions",
|
|
2004
|
+
title: "agent-permissions.json is present",
|
|
2005
|
+
title_ko: "agent-permissions.json \uD30C\uC77C \uC874\uC7AC \uC5EC\uBD80",
|
|
2006
|
+
description: "Declares explicit read/summarize/cite/train permissions for AI agents.",
|
|
2007
|
+
run(ctx) {
|
|
2008
|
+
if (ctx.agentPermissions !== null) {
|
|
2009
|
+
return {
|
|
2010
|
+
status: "pass",
|
|
2011
|
+
score: 1,
|
|
2012
|
+
rationale: "agent-permissions.json found at site root.",
|
|
2013
|
+
rationale_ko: "agent-permissions.json\uC774 \uC0AC\uC774\uD2B8 \uB8E8\uD2B8\uC5D0 \uC874\uC7AC\uD569\uB2C8\uB2E4.",
|
|
2014
|
+
evidence: ctx.agentPermissions
|
|
2015
|
+
};
|
|
2016
|
+
}
|
|
2017
|
+
return {
|
|
2018
|
+
status: "warn",
|
|
2019
|
+
score: 0,
|
|
2020
|
+
rationale: "No /agent-permissions.json found. Add one to declare AI agent access policy.",
|
|
2021
|
+
rationale_ko: "/agent-permissions.json\uC774 \uC5C6\uC2B5\uB2C8\uB2E4. AI \uC5D0\uC774\uC804\uD2B8 \uC811\uADFC \uC815\uCC45\uC744 \uBA85\uC2DC\uD558\uB824\uBA74 \uCD94\uAC00\uD558\uC138\uC694.",
|
|
2022
|
+
fixHint: "Create /agent-permissions.json with read, summarize, cite, and train permission flags."
|
|
2023
|
+
};
|
|
2024
|
+
}
|
|
2025
|
+
});
|
|
2026
|
+
|
|
2027
|
+
// src/rules/aeo/token-length.ts
|
|
2028
|
+
var THRESHOLD_OPTIMAL = 15e3;
|
|
2029
|
+
var THRESHOLD_MAX = 25e3;
|
|
2030
|
+
var aeoTokenLengthRule = defineRule({
|
|
2031
|
+
id: "aeo.token-length",
|
|
2032
|
+
stableId: "aeo.token-length",
|
|
2033
|
+
category: "aeo",
|
|
2034
|
+
group: "diagnostic",
|
|
2035
|
+
weight: 4,
|
|
2036
|
+
impact: "medium",
|
|
2037
|
+
effort: "medium",
|
|
2038
|
+
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#aeotoken-length",
|
|
2039
|
+
title: "Content token length within AI agent limits",
|
|
2040
|
+
title_ko: "\uCF58\uD150\uCE20 \uD1A0\uD070 \uC218 AI \uC5D0\uC774\uC804\uD2B8 \uAD8C\uC7A5 \uBC94\uC704",
|
|
2041
|
+
description: "Pages under 15K tokens are optimal for AI agents (per Addy Osmani's AEO guidance).",
|
|
2042
|
+
run(ctx) {
|
|
2043
|
+
const text = ctx.$("body").text();
|
|
2044
|
+
const tokenEstimate = Math.round(text.length / 3);
|
|
2045
|
+
const evidence = { tokenEstimate, thresholds: { optimal: THRESHOLD_OPTIMAL, max: THRESHOLD_MAX } };
|
|
2046
|
+
if (tokenEstimate <= THRESHOLD_OPTIMAL) {
|
|
2047
|
+
return {
|
|
2048
|
+
status: "pass",
|
|
2049
|
+
score: 1,
|
|
2050
|
+
rationale: `Estimated ~${tokenEstimate.toLocaleString()} tokens \u2014 within optimal range.`,
|
|
2051
|
+
rationale_ko: `\uC608\uC0C1 \uD1A0\uD070 \uC218 ~${tokenEstimate.toLocaleString()} \u2014 \uAD8C\uC7A5 \uBC94\uC704(15K) \uC774\uB0B4\uC785\uB2C8\uB2E4.`,
|
|
2052
|
+
evidence
|
|
2053
|
+
};
|
|
2054
|
+
}
|
|
2055
|
+
if (tokenEstimate <= THRESHOLD_MAX) {
|
|
2056
|
+
return {
|
|
2057
|
+
status: "warn",
|
|
2058
|
+
score: 0.5,
|
|
2059
|
+
rationale: `Estimated ~${tokenEstimate.toLocaleString()} tokens \u2014 exceeds 15K recommendation.`,
|
|
2060
|
+
rationale_ko: `\uC608\uC0C1 \uD1A0\uD070 \uC218 ~${tokenEstimate.toLocaleString()} \u2014 15K \uAD8C\uC7A5\uCE58\uB97C \uCD08\uACFC\uD569\uB2C8\uB2E4.`,
|
|
2061
|
+
fixHint: "Consider splitting content into shorter, focused pages.",
|
|
2062
|
+
evidence
|
|
2063
|
+
};
|
|
2064
|
+
}
|
|
2065
|
+
return {
|
|
2066
|
+
status: "fail",
|
|
2067
|
+
score: 0,
|
|
2068
|
+
rationale: `Estimated ~${tokenEstimate.toLocaleString()} tokens \u2014 exceeds 25K agent processing limit.`,
|
|
2069
|
+
rationale_ko: `\uC608\uC0C1 \uD1A0\uD070 \uC218 ~${tokenEstimate.toLocaleString()} \u2014 AI \uC5D0\uC774\uC804\uD2B8 \uCC98\uB9AC \uD55C\uACC4(25K)\uB97C \uCD08\uACFC\uD569\uB2C8\uB2E4.`,
|
|
2070
|
+
fixHint: "Split this page into multiple focused pages under 15K tokens.",
|
|
2071
|
+
evidence
|
|
2072
|
+
};
|
|
2073
|
+
}
|
|
2074
|
+
});
|
|
2075
|
+
|
|
2076
|
+
// src/rules/aeo/index.ts
|
|
2077
|
+
var aeoRules = [aeoSkillMdRule, aeoAgentPermissionsRule, aeoTokenLengthRule];
|
|
2078
|
+
|
|
1859
2079
|
// src/rules/index.ts
|
|
1860
2080
|
var defaultRules = [
|
|
1861
2081
|
...crawlerRules,
|
|
1862
2082
|
...structuredDataRules,
|
|
1863
2083
|
...citationRules,
|
|
1864
|
-
...contentRules
|
|
2084
|
+
...contentRules,
|
|
2085
|
+
...aeoRules
|
|
1865
2086
|
];
|
|
1866
2087
|
|
|
1867
2088
|
// src/config.ts
|
|
@@ -2057,7 +2278,8 @@ var CATEGORY_LABELS = {
|
|
|
2057
2278
|
crawler: "AI Crawler Access",
|
|
2058
2279
|
"structured-data": "Structured Data",
|
|
2059
2280
|
citation: "Citation Signals",
|
|
2060
|
-
content: "Content Structure"
|
|
2281
|
+
content: "Content Structure",
|
|
2282
|
+
aeo: "AEO Stack"
|
|
2061
2283
|
};
|
|
2062
2284
|
function scoreBadge(score) {
|
|
2063
2285
|
const color = score >= 85 ? "brightgreen" : score >= 60 ? "yellow" : "red";
|