geo-checker 0.3.0 → 0.3.1
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 +147 -57
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +147 -57
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +147 -57
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +147 -57
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.cjs
CHANGED
|
@@ -362,6 +362,7 @@ async function runRules(ctx, rules, opts = {}) {
|
|
|
362
362
|
durationMs
|
|
363
363
|
};
|
|
364
364
|
if (rule.stableId !== void 0) entry.stableId = rule.stableId;
|
|
365
|
+
if (rule.title_ko !== void 0) entry.title_ko = rule.title_ko;
|
|
365
366
|
if (rule.group !== void 0) entry.group = rule.group;
|
|
366
367
|
if (rule.impact !== void 0) entry.impact = rule.impact;
|
|
367
368
|
if (rule.effort !== void 0) entry.effort = rule.effort;
|
|
@@ -424,13 +425,15 @@ var httpsRule = defineRule({
|
|
|
424
425
|
effort: "medium",
|
|
425
426
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#crawlerhttps",
|
|
426
427
|
title: "Site is served over HTTPS",
|
|
428
|
+
title_ko: "\uC0AC\uC774\uD2B8\uAC00 HTTPS\uB85C \uC81C\uACF5\uB428",
|
|
427
429
|
description: "AI crawlers treat HTTPS pages as more trustworthy and some skip plain HTTP entirely.",
|
|
428
430
|
run(ctx) {
|
|
429
431
|
const isHttps = ctx.finalUrl.startsWith("https://");
|
|
430
|
-
return isHttps ? { status: "pass", score: 1, rationale: "Final URL uses HTTPS." } : {
|
|
432
|
+
return isHttps ? { status: "pass", score: 1, rationale: "Final URL uses HTTPS.", rationale_ko: "\uCD5C\uC885 URL\uC774 HTTPS\uB97C \uC0AC\uC6A9\uD569\uB2C8\uB2E4." } : {
|
|
431
433
|
status: "fail",
|
|
432
434
|
score: 0,
|
|
433
435
|
rationale: "Final URL does not use HTTPS. Redirect HTTP \u2192 HTTPS site-wide.",
|
|
436
|
+
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.",
|
|
434
437
|
fixUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md"
|
|
435
438
|
};
|
|
436
439
|
}
|
|
@@ -447,15 +450,17 @@ var robotsReachableRule = defineRule({
|
|
|
447
450
|
effort: "low",
|
|
448
451
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#crawlerrobots-reachable",
|
|
449
452
|
title: "robots.txt is reachable",
|
|
453
|
+
title_ko: "robots.txt \uC811\uADFC \uAC00\uB2A5 \uC5EC\uBD80",
|
|
450
454
|
description: "A reachable robots.txt lets crawlers confirm their permissions; missing file is treated as allow-all but blocks explicit signalling.",
|
|
451
455
|
run(ctx) {
|
|
452
456
|
if (ctx.robots) {
|
|
453
|
-
return { status: "pass", score: 1, rationale: "robots.txt returned successfully." };
|
|
457
|
+
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." };
|
|
454
458
|
}
|
|
455
459
|
return {
|
|
456
460
|
status: "warn",
|
|
457
461
|
score: 0.3,
|
|
458
462
|
rationale: "robots.txt is missing. Add one even if empty to explicitly signal crawl policy.",
|
|
463
|
+
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.",
|
|
459
464
|
fixUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md"
|
|
460
465
|
};
|
|
461
466
|
}
|
|
@@ -503,13 +508,15 @@ var robotsAiAllowRule = defineRule({
|
|
|
503
508
|
effort: "low",
|
|
504
509
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#crawlerrobots-ai-allow",
|
|
505
510
|
title: "AI crawlers are allowed",
|
|
511
|
+
title_ko: "AI \uD06C\uB864\uB7EC \uC811\uADFC \uD5C8\uC6A9 \uC5EC\uBD80",
|
|
506
512
|
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.",
|
|
507
513
|
run(ctx) {
|
|
508
514
|
if (!ctx.robots) {
|
|
509
515
|
return {
|
|
510
516
|
status: "warn",
|
|
511
517
|
score: 0.5,
|
|
512
|
-
rationale: "robots.txt missing; AI crawlers default to allow, but explicit allow is recommended."
|
|
518
|
+
rationale: "robots.txt missing; AI crawlers default to allow, but explicit allow is recommended.",
|
|
519
|
+
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."
|
|
513
520
|
};
|
|
514
521
|
}
|
|
515
522
|
const path = new URL(ctx.finalUrl).pathname || "/";
|
|
@@ -527,6 +534,7 @@ var robotsAiAllowRule = defineRule({
|
|
|
527
534
|
status: "fail",
|
|
528
535
|
score: Math.max(0, 1 - blocked.length / AI_BOTS.length),
|
|
529
536
|
rationale: `Blocked: ${blocked.join(", ")}. Remove the Disallow or add an explicit Allow for these user-agents.`,
|
|
537
|
+
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.`,
|
|
530
538
|
evidence: { blocked, mentioned, totalBots: AI_BOTS.length },
|
|
531
539
|
fixUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md"
|
|
532
540
|
};
|
|
@@ -535,13 +543,15 @@ var robotsAiAllowRule = defineRule({
|
|
|
535
543
|
return {
|
|
536
544
|
status: "warn",
|
|
537
545
|
score: 0.6,
|
|
538
|
-
rationale: `All ${AI_BOTS.length} AI crawlers reach the page via default rules, but none are explicitly listed. Consider explicit Allow entries
|
|
546
|
+
rationale: `All ${AI_BOTS.length} AI crawlers reach the page via default rules, but none are explicitly listed. Consider explicit Allow entries.`,
|
|
547
|
+
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.`
|
|
539
548
|
};
|
|
540
549
|
}
|
|
541
550
|
return {
|
|
542
551
|
status: "pass",
|
|
543
552
|
score: 1,
|
|
544
553
|
rationale: `All ${AI_BOTS.length} AI crawlers can reach the page; ${mentioned.length} explicitly listed.`,
|
|
554
|
+
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.`,
|
|
545
555
|
evidence: { mentioned, totalBots: AI_BOTS.length }
|
|
546
556
|
};
|
|
547
557
|
}
|
|
@@ -558,15 +568,17 @@ var llmsTxtPresentRule = defineRule({
|
|
|
558
568
|
effort: "medium",
|
|
559
569
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#crawlerllms-txt-present",
|
|
560
570
|
title: "llms.txt is present",
|
|
571
|
+
title_ko: "llms.txt \uD30C\uC77C \uC874\uC7AC \uC5EC\uBD80",
|
|
561
572
|
description: "An /llms.txt file at the site root gives AI assistants a curated map of the most citation-worthy pages.",
|
|
562
573
|
run(ctx) {
|
|
563
574
|
if (ctx.llmsTxt) {
|
|
564
|
-
return { status: "pass", score: 1, rationale: "llms.txt found at site root." };
|
|
575
|
+
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." };
|
|
565
576
|
}
|
|
566
577
|
return {
|
|
567
578
|
status: "warn",
|
|
568
579
|
score: 0,
|
|
569
580
|
rationale: "No /llms.txt found. Add one to curate the pages AI assistants should read.",
|
|
581
|
+
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.",
|
|
570
582
|
fixUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md"
|
|
571
583
|
};
|
|
572
584
|
}
|
|
@@ -583,10 +595,11 @@ var llmsTxtWellformedRule = defineRule({
|
|
|
583
595
|
effort: "low",
|
|
584
596
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#crawlerllms-txt-wellformed",
|
|
585
597
|
title: "llms.txt follows the spec",
|
|
598
|
+
title_ko: "llms.txt \uC2A4\uD399 \uC900\uC218 \uC5EC\uBD80",
|
|
586
599
|
description: "Must start with an H1 project title, then a brief summary, then at least one H2 section containing link items.",
|
|
587
600
|
run(ctx) {
|
|
588
601
|
if (!ctx.llmsTxt) {
|
|
589
|
-
return { status: "skip", score: 0, rationale: "No llms.txt to validate." };
|
|
602
|
+
return { status: "skip", score: 0, rationale: "No llms.txt to validate.", rationale_ko: "\uAC80\uC99D\uD560 llms.txt\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4." };
|
|
590
603
|
}
|
|
591
604
|
const check = isLlmsTxtWellFormed(ctx.llmsTxt);
|
|
592
605
|
if (check.ok) {
|
|
@@ -594,13 +607,15 @@ var llmsTxtWellformedRule = defineRule({
|
|
|
594
607
|
return {
|
|
595
608
|
status: "pass",
|
|
596
609
|
score: 1,
|
|
597
|
-
rationale: `Well-formed with ${ctx.llmsTxt.sections.length} section(s) and ${totalLinks} link(s)
|
|
610
|
+
rationale: `Well-formed with ${ctx.llmsTxt.sections.length} section(s) and ${totalLinks} link(s).`,
|
|
611
|
+
rationale_ko: `\uC2A4\uD399\uC5D0 \uB9DE\uAC8C \uC791\uC131\uB428 (\uC139\uC158 ${ctx.llmsTxt.sections.length}\uAC1C, \uB9C1\uD06C ${totalLinks}\uAC1C).`
|
|
598
612
|
};
|
|
599
613
|
}
|
|
600
614
|
return {
|
|
601
615
|
status: "warn",
|
|
602
616
|
score: 0.3,
|
|
603
617
|
rationale: `llms.txt does not fully match the spec: ${check.reason}.`,
|
|
618
|
+
rationale_ko: `llms.txt\uAC00 \uC2A4\uD399\uC744 \uC644\uC804\uD788 \uB530\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4: ${check.reason}.`,
|
|
604
619
|
fixUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md"
|
|
605
620
|
};
|
|
606
621
|
}
|
|
@@ -617,13 +632,15 @@ var llmsFullTxtRule = defineRule({
|
|
|
617
632
|
effort: "medium",
|
|
618
633
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#crawlerllms-full-txt",
|
|
619
634
|
title: "llms-full.txt provides full-content mirror",
|
|
635
|
+
title_ko: "llms-full.txt \uC804\uCCB4 \uCF58\uD150\uCE20 \uBBF8\uB7EC \uC81C\uACF5 \uC5EC\uBD80",
|
|
620
636
|
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.",
|
|
621
637
|
run(ctx) {
|
|
622
638
|
if (ctx.llmsFullTxt && ctx.llmsFullTxt.length > 200) {
|
|
623
639
|
return {
|
|
624
640
|
status: "pass",
|
|
625
641
|
score: 1,
|
|
626
|
-
rationale: `/llms-full.txt found (${ctx.llmsFullTxt.length.toLocaleString()} chars)
|
|
642
|
+
rationale: `/llms-full.txt found (${ctx.llmsFullTxt.length.toLocaleString()} chars).`,
|
|
643
|
+
rationale_ko: `/llms-full.txt\uAC00 \uC874\uC7AC\uD569\uB2C8\uB2E4 (${ctx.llmsFullTxt.length.toLocaleString()}\uC790).`
|
|
627
644
|
};
|
|
628
645
|
}
|
|
629
646
|
if (ctx.llmsFullTxt) {
|
|
@@ -631,6 +648,7 @@ var llmsFullTxtRule = defineRule({
|
|
|
631
648
|
status: "warn",
|
|
632
649
|
score: 0.5,
|
|
633
650
|
rationale: `/llms-full.txt found but very short (${ctx.llmsFullTxt.length} chars). Consider expanding with page bodies.`,
|
|
651
|
+
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.`,
|
|
634
652
|
fixHint: "Mirror full article bodies into /llms-full.txt so AI assistants can quote without re-crawling."
|
|
635
653
|
};
|
|
636
654
|
}
|
|
@@ -638,6 +656,7 @@ var llmsFullTxtRule = defineRule({
|
|
|
638
656
|
status: "warn",
|
|
639
657
|
score: 0,
|
|
640
658
|
rationale: "No /llms-full.txt found. Adding one lets AI assistants ingest the full corpus in a single request.",
|
|
659
|
+
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.",
|
|
641
660
|
fixHint: "Publish /llms-full.txt alongside /llms.txt with the full body text of your top pages.",
|
|
642
661
|
estimatedImpact: 1
|
|
643
662
|
};
|
|
@@ -655,19 +674,22 @@ var sitemapPresentRule = defineRule({
|
|
|
655
674
|
effort: "low",
|
|
656
675
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#crawlersitemap-present",
|
|
657
676
|
title: "sitemap.xml is present",
|
|
677
|
+
title_ko: "sitemap.xml \uC874\uC7AC \uC5EC\uBD80",
|
|
658
678
|
description: "A sitemap helps AI crawlers discover and prioritise pages; many crawlers short-circuit discovery without one.",
|
|
659
679
|
run(ctx) {
|
|
660
680
|
if (ctx.sitemap && ctx.sitemap.urls.length > 0) {
|
|
661
681
|
return {
|
|
662
682
|
status: "pass",
|
|
663
683
|
score: 1,
|
|
664
|
-
rationale: `Sitemap found with ${ctx.sitemap.urls.length} URL(s)
|
|
684
|
+
rationale: `Sitemap found with ${ctx.sitemap.urls.length} URL(s).`,
|
|
685
|
+
rationale_ko: `\uC0AC\uC774\uD2B8\uB9F5\uC5D0 URL\uC774 ${ctx.sitemap.urls.length}\uAC1C \uC788\uC2B5\uB2C8\uB2E4.`
|
|
665
686
|
};
|
|
666
687
|
}
|
|
667
688
|
return {
|
|
668
689
|
status: "warn",
|
|
669
690
|
score: 0.2,
|
|
670
691
|
rationale: "No sitemap.xml found (checked /sitemap.xml and Sitemap: directive in robots.txt).",
|
|
692
|
+
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).",
|
|
671
693
|
fixUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md"
|
|
672
694
|
};
|
|
673
695
|
}
|
|
@@ -695,15 +717,17 @@ var jsonLdPresentRule = defineRule({
|
|
|
695
717
|
effort: "medium",
|
|
696
718
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#sdjsonld-present",
|
|
697
719
|
title: "JSON-LD structured data is present",
|
|
720
|
+
title_ko: "JSON-LD \uAD6C\uC870\uD654 \uB370\uC774\uD130 \uC874\uC7AC \uC5EC\uBD80",
|
|
698
721
|
description: 'At least one <script type="application/ld+json"> block is the primary way AI engines map your page to an entity.',
|
|
699
722
|
run(ctx) {
|
|
700
723
|
if (ctx.jsonLd.length > 0) {
|
|
701
|
-
return { status: "pass", score: 1, rationale: `Found ${ctx.jsonLd.length} JSON-LD block(s).` };
|
|
724
|
+
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.` };
|
|
702
725
|
}
|
|
703
726
|
return {
|
|
704
727
|
status: "fail",
|
|
705
728
|
score: 0,
|
|
706
729
|
rationale: "No JSON-LD blocks found. Add schema.org structured data.",
|
|
730
|
+
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.",
|
|
707
731
|
fixUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md"
|
|
708
732
|
};
|
|
709
733
|
}
|
|
@@ -779,20 +803,22 @@ var jsonLdValidJsonRule = defineRule({
|
|
|
779
803
|
effort: "low",
|
|
780
804
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#sdjsonld-valid-json",
|
|
781
805
|
title: "JSON-LD blocks parse as valid JSON",
|
|
806
|
+
title_ko: "JSON-LD \uBE14\uB85D\uC758 JSON \uC720\uD6A8\uC131",
|
|
782
807
|
description: "Malformed JSON in an ld+json block is silently ignored by most consumers \u2014 a costly silent failure.",
|
|
783
808
|
run(ctx) {
|
|
784
809
|
if (ctx.jsonLd.length === 0) {
|
|
785
|
-
return { status: "skip", score: 0, rationale: "No JSON-LD to validate." };
|
|
810
|
+
return { status: "skip", score: 0, rationale: "No JSON-LD to validate.", rationale_ko: "\uAC80\uC99D\uD560 JSON-LD\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4." };
|
|
786
811
|
}
|
|
787
812
|
if (hasParseError(ctx.jsonLd)) {
|
|
788
813
|
return {
|
|
789
814
|
status: "fail",
|
|
790
815
|
score: 0,
|
|
791
816
|
rationale: "One or more JSON-LD blocks failed to parse.",
|
|
817
|
+
rationale_ko: "JSON-LD \uBE14\uB85D \uD558\uB098 \uC774\uC0C1\uC744 \uD30C\uC2F1\uD558\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4.",
|
|
792
818
|
fixUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md"
|
|
793
819
|
};
|
|
794
820
|
}
|
|
795
|
-
return { status: "pass", score: 1, rationale: "All JSON-LD blocks parse cleanly." };
|
|
821
|
+
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." };
|
|
796
822
|
}
|
|
797
823
|
});
|
|
798
824
|
|
|
@@ -807,10 +833,11 @@ var schemaTypeRecognizedRule = defineRule({
|
|
|
807
833
|
effort: "low",
|
|
808
834
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#sdschema-type-recognized",
|
|
809
835
|
title: "Schema.org @type is a recognised kind",
|
|
836
|
+
title_ko: "Schema.org @type \uC778\uC2DD \uAC00\uB2A5 \uC5EC\uBD80",
|
|
810
837
|
description: "AI engines match pages against well-known types (Article, Product, FAQPage...). Obscure types weaken the signal.",
|
|
811
838
|
run(ctx) {
|
|
812
839
|
if (ctx.jsonLd.length === 0) {
|
|
813
|
-
return { status: "skip", score: 0, rationale: "No JSON-LD to analyse." };
|
|
840
|
+
return { status: "skip", score: 0, rationale: "No JSON-LD to analyse.", rationale_ko: "\uBD84\uC11D\uD560 JSON-LD\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4." };
|
|
814
841
|
}
|
|
815
842
|
const nodes = flattenJsonLd(ctx.jsonLd);
|
|
816
843
|
const recognized = /* @__PURE__ */ new Set();
|
|
@@ -826,6 +853,7 @@ var schemaTypeRecognizedRule = defineRule({
|
|
|
826
853
|
status: "pass",
|
|
827
854
|
score: 1,
|
|
828
855
|
rationale: `Recognised: ${[...recognized].join(", ")}.`,
|
|
856
|
+
rationale_ko: `\uC778\uC2DD\uB41C \uD0C0\uC785: ${[...recognized].join(", ")}.`,
|
|
829
857
|
evidence: { recognized: [...recognized], all: [...seenTypes] }
|
|
830
858
|
};
|
|
831
859
|
}
|
|
@@ -833,6 +861,7 @@ var schemaTypeRecognizedRule = defineRule({
|
|
|
833
861
|
status: "warn",
|
|
834
862
|
score: 0.3,
|
|
835
863
|
rationale: `No recognised schema.org types. Saw: ${[...seenTypes].join(", ") || "(none)"}.`,
|
|
864
|
+
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)"}.`,
|
|
836
865
|
fixUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md"
|
|
837
866
|
};
|
|
838
867
|
}
|
|
@@ -849,10 +878,11 @@ var requiredFieldsRule = defineRule({
|
|
|
849
878
|
effort: "medium",
|
|
850
879
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#sdrequired-fields",
|
|
851
880
|
title: "Required fields for recognised types are set",
|
|
881
|
+
title_ko: "\uC778\uC2DD\uB41C \uD0C0\uC785\uC758 \uD544\uC218 \uD544\uB4DC \uCDA9\uC871 \uC5EC\uBD80",
|
|
852
882
|
description: "Article needs headline/author/datePublished, FAQPage needs mainEntity, Product needs offers, etc.",
|
|
853
883
|
run(ctx) {
|
|
854
884
|
if (ctx.jsonLd.length === 0) {
|
|
855
|
-
return { status: "skip", score: 0, rationale: "No JSON-LD to analyse." };
|
|
885
|
+
return { status: "skip", score: 0, rationale: "No JSON-LD to analyse.", rationale_ko: "\uBD84\uC11D\uD560 JSON-LD\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4." };
|
|
856
886
|
}
|
|
857
887
|
const nodes = flattenJsonLd(ctx.jsonLd);
|
|
858
888
|
const missing = [];
|
|
@@ -871,14 +901,16 @@ var requiredFieldsRule = defineRule({
|
|
|
871
901
|
return {
|
|
872
902
|
status: "skip",
|
|
873
903
|
score: 0,
|
|
874
|
-
rationale: "No types with known required fields were found."
|
|
904
|
+
rationale: "No types with known required fields were found.",
|
|
905
|
+
rationale_ko: "\uD544\uC218 \uD544\uB4DC\uAC00 \uC815\uC758\uB41C \uD0C0\uC785\uC774 \uC5C6\uC2B5\uB2C8\uB2E4."
|
|
875
906
|
};
|
|
876
907
|
}
|
|
877
908
|
if (missing.length === 0) {
|
|
878
909
|
return {
|
|
879
910
|
status: "pass",
|
|
880
911
|
score: 1,
|
|
881
|
-
rationale: `Required fields set on ${checked.length} node(s)
|
|
912
|
+
rationale: `Required fields set on ${checked.length} node(s).`,
|
|
913
|
+
rationale_ko: `${checked.length}\uAC1C \uB178\uB4DC\uC758 \uD544\uC218 \uD544\uB4DC\uAC00 \uBAA8\uB450 \uCDA9\uC871\uB429\uB2C8\uB2E4.`
|
|
882
914
|
};
|
|
883
915
|
}
|
|
884
916
|
const msg = missing.map((m) => `${m.type}.${m.field}`).join(", ");
|
|
@@ -886,6 +918,7 @@ var requiredFieldsRule = defineRule({
|
|
|
886
918
|
status: "fail",
|
|
887
919
|
score: Math.max(0, 1 - missing.length / (checked.length * 2)),
|
|
888
920
|
rationale: `Missing required fields: ${msg}.`,
|
|
921
|
+
rationale_ko: `\uB204\uB77D\uB41C \uD544\uC218 \uD544\uB4DC: ${msg}.`,
|
|
889
922
|
evidence: missing,
|
|
890
923
|
fixUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md"
|
|
891
924
|
};
|
|
@@ -903,10 +936,11 @@ var microdataFallbackRule = defineRule({
|
|
|
903
936
|
effort: "medium",
|
|
904
937
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#sdmicrodata-fallback",
|
|
905
938
|
title: "Microdata or RDFa fallback when JSON-LD is missing",
|
|
939
|
+
title_ko: "JSON-LD \uC5C6\uC744 \uB54C Microdata/RDFa \uB300\uCCB4 \uC5EC\uBD80",
|
|
906
940
|
description: "If JSON-LD is absent, inline microdata (itemscope/itemtype) or RDFa still gives some structured signal.",
|
|
907
941
|
run(ctx) {
|
|
908
942
|
if (ctx.jsonLd.length > 0) {
|
|
909
|
-
return { status: "skip", score: 0, rationale: "JSON-LD is present; fallback not needed." };
|
|
943
|
+
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." };
|
|
910
944
|
}
|
|
911
945
|
const microdata = ctx.$("[itemscope][itemtype]").length;
|
|
912
946
|
const rdfa = ctx.$("[typeof][vocab], [typeof][property]").length;
|
|
@@ -914,13 +948,15 @@ var microdataFallbackRule = defineRule({
|
|
|
914
948
|
return {
|
|
915
949
|
status: "pass",
|
|
916
950
|
score: 1,
|
|
917
|
-
rationale: `Found ${microdata} microdata and ${rdfa} RDFa nodes
|
|
951
|
+
rationale: `Found ${microdata} microdata and ${rdfa} RDFa nodes.`,
|
|
952
|
+
rationale_ko: `Microdata ${microdata}\uAC1C, RDFa ${rdfa}\uAC1C \uBC1C\uACAC\uB429\uB2C8\uB2E4.`
|
|
918
953
|
};
|
|
919
954
|
}
|
|
920
955
|
return {
|
|
921
956
|
status: "fail",
|
|
922
957
|
score: 0,
|
|
923
958
|
rationale: "No structured data at all (no JSON-LD, no microdata, no RDFa).",
|
|
959
|
+
rationale_ko: "\uAD6C\uC870\uD654 \uB370\uC774\uD130\uAC00 \uC804\uD600 \uC5C6\uC2B5\uB2C8\uB2E4 (JSON-LD, Microdata, RDFa \uBAA8\uB450 \uC5C6\uC74C).",
|
|
924
960
|
fixUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md"
|
|
925
961
|
};
|
|
926
962
|
}
|
|
@@ -938,10 +974,11 @@ var noDuplicateTypesRule = defineRule({
|
|
|
938
974
|
effort: "low",
|
|
939
975
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#sdno-duplicate-types",
|
|
940
976
|
title: "No conflicting duplicate @types",
|
|
977
|
+
title_ko: "@type \uC911\uBCF5 \uCDA9\uB3CC \uC5C6\uC74C",
|
|
941
978
|
description: "Multiple competing entities of the same primary type (e.g. two Articles) confuse the engine about which one represents the page.",
|
|
942
979
|
run(ctx) {
|
|
943
980
|
if (ctx.jsonLd.length === 0) {
|
|
944
|
-
return { status: "skip", score: 0, rationale: "No JSON-LD to analyse." };
|
|
981
|
+
return { status: "skip", score: 0, rationale: "No JSON-LD to analyse.", rationale_ko: "\uBD84\uC11D\uD560 JSON-LD\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4." };
|
|
945
982
|
}
|
|
946
983
|
const counts = /* @__PURE__ */ new Map();
|
|
947
984
|
for (const node of flattenJsonLd(ctx.jsonLd)) {
|
|
@@ -951,12 +988,13 @@ var noDuplicateTypesRule = defineRule({
|
|
|
951
988
|
}
|
|
952
989
|
const dupes = [...counts.entries()].filter(([, n]) => n > 1);
|
|
953
990
|
if (dupes.length === 0) {
|
|
954
|
-
return { status: "pass", score: 1, rationale: "No duplicate primary types." };
|
|
991
|
+
return { status: "pass", score: 1, rationale: "No duplicate primary types.", rationale_ko: "\uC911\uBCF5\uB41C \uAE30\uBCF8 \uD0C0\uC785\uC774 \uC5C6\uC2B5\uB2C8\uB2E4." };
|
|
955
992
|
}
|
|
956
993
|
return {
|
|
957
994
|
status: "warn",
|
|
958
995
|
score: 0.4,
|
|
959
996
|
rationale: `Duplicate primary types: ${dupes.map(([t, n]) => `${t}\xD7${n}`).join(", ")}.`,
|
|
997
|
+
rationale_ko: `\uC911\uBCF5\uB41C \uAE30\uBCF8 \uD0C0\uC785: ${dupes.map(([t, n]) => `${t}\xD7${n}`).join(", ")}.`,
|
|
960
998
|
fixUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md"
|
|
961
999
|
};
|
|
962
1000
|
}
|
|
@@ -1004,10 +1042,11 @@ var sameAsEntityRule = defineRule({
|
|
|
1004
1042
|
effort: "medium",
|
|
1005
1043
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#sdsameas-entity",
|
|
1006
1044
|
title: "Entity nodes link the knowledge graph via sameAs",
|
|
1045
|
+
title_ko: "sameAs\uB85C \uC9C0\uC2DD \uADF8\uB798\uD504 \uC5F0\uACB0 \uC5EC\uBD80",
|
|
1007
1046
|
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).",
|
|
1008
1047
|
run(ctx) {
|
|
1009
1048
|
if (ctx.jsonLd.length === 0) {
|
|
1010
|
-
return { status: "skip", score: 0, rationale: "No JSON-LD to analyse." };
|
|
1049
|
+
return { status: "skip", score: 0, rationale: "No JSON-LD to analyse.", rationale_ko: "\uBD84\uC11D\uD560 JSON-LD\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4." };
|
|
1011
1050
|
}
|
|
1012
1051
|
const nodes = flattenJsonLd(ctx.jsonLd);
|
|
1013
1052
|
const entities = nodes.filter((n) => getTypes(n).some((t) => ENTITY_TYPES.includes(t)));
|
|
@@ -1015,7 +1054,8 @@ var sameAsEntityRule = defineRule({
|
|
|
1015
1054
|
return {
|
|
1016
1055
|
status: "skip",
|
|
1017
1056
|
score: 0,
|
|
1018
|
-
rationale: "No Organization/Person/LocalBusiness/Brand entity to link."
|
|
1057
|
+
rationale: "No Organization/Person/LocalBusiness/Brand entity to link.",
|
|
1058
|
+
rationale_ko: "\uC5F0\uACB0\uD560 Organization/Person/LocalBusiness/Brand \uC5D4\uD2F0\uD2F0\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4."
|
|
1019
1059
|
};
|
|
1020
1060
|
}
|
|
1021
1061
|
let bestScore = 0;
|
|
@@ -1039,6 +1079,7 @@ var sameAsEntityRule = defineRule({
|
|
|
1039
1079
|
status: "pass",
|
|
1040
1080
|
score: 1,
|
|
1041
1081
|
rationale: `Entity links ${bestEvidence.trusted} trusted knowledge-graph hosts via sameAs.`,
|
|
1082
|
+
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.`,
|
|
1042
1083
|
evidence: bestEvidence
|
|
1043
1084
|
};
|
|
1044
1085
|
}
|
|
@@ -1047,6 +1088,7 @@ var sameAsEntityRule = defineRule({
|
|
|
1047
1088
|
status: "pass",
|
|
1048
1089
|
score: bestScore,
|
|
1049
1090
|
rationale: `Entity has 1 trusted sameAs link. Add Wikipedia/Wikidata for stronger E-E-A-T.`,
|
|
1091
|
+
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.",
|
|
1050
1092
|
evidence: bestEvidence,
|
|
1051
1093
|
estimatedImpact: 1
|
|
1052
1094
|
};
|
|
@@ -1056,6 +1098,7 @@ var sameAsEntityRule = defineRule({
|
|
|
1056
1098
|
status: "warn",
|
|
1057
1099
|
score: bestScore,
|
|
1058
1100
|
rationale: "Entity declares sameAs but no trusted knowledge-graph hosts (Wikipedia/Wikidata/LinkedIn).",
|
|
1101
|
+
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.",
|
|
1059
1102
|
evidence: bestEvidence,
|
|
1060
1103
|
fixHint: "Add Wikipedia/Wikidata/LinkedIn URLs to your Organization sameAs[].",
|
|
1061
1104
|
estimatedImpact: 2
|
|
@@ -1065,6 +1108,7 @@ var sameAsEntityRule = defineRule({
|
|
|
1065
1108
|
status: "warn",
|
|
1066
1109
|
score: 0,
|
|
1067
1110
|
rationale: `${entities.length} entity node(s) found but none declare sameAs links.`,
|
|
1111
|
+
rationale_ko: `\uC5D4\uD2F0\uD2F0 \uB178\uB4DC\uAC00 ${entities.length}\uAC1C \uC788\uC9C0\uB9CC sameAs \uB9C1\uD06C\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.`,
|
|
1068
1112
|
fixHint: 'Add sameAs:["https://en.wikipedia.org/wiki/...", "https://www.linkedin.com/company/..."] to your Organization JSON-LD.',
|
|
1069
1113
|
estimatedImpact: 3
|
|
1070
1114
|
};
|
|
@@ -1096,15 +1140,16 @@ var breadcrumbValidRule = defineRule({
|
|
|
1096
1140
|
effort: "medium",
|
|
1097
1141
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#sdbreadcrumb-valid",
|
|
1098
1142
|
title: "BreadcrumbList items declare position, name, and item",
|
|
1143
|
+
title_ko: "BreadcrumbList \uD56D\uBAA9\uC758 \uD544\uC218 \uD544\uB4DC \uCDA9\uC871 \uC5EC\uBD80",
|
|
1099
1144
|
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.",
|
|
1100
1145
|
run(ctx) {
|
|
1101
1146
|
if (ctx.jsonLd.length === 0) {
|
|
1102
|
-
return { status: "skip", score: 0, rationale: "No JSON-LD to analyse." };
|
|
1147
|
+
return { status: "skip", score: 0, rationale: "No JSON-LD to analyse.", rationale_ko: "\uBD84\uC11D\uD560 JSON-LD\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4." };
|
|
1103
1148
|
}
|
|
1104
1149
|
const nodes = flattenJsonLd(ctx.jsonLd);
|
|
1105
1150
|
const breadcrumbs = nodes.filter((n) => getTypes(n).includes("BreadcrumbList"));
|
|
1106
1151
|
if (breadcrumbs.length === 0) {
|
|
1107
|
-
return { status: "skip", score: 0, rationale: "No BreadcrumbList present." };
|
|
1152
|
+
return { status: "skip", score: 0, rationale: "No BreadcrumbList present.", rationale_ko: "BreadcrumbList\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4." };
|
|
1108
1153
|
}
|
|
1109
1154
|
const allIssues = [];
|
|
1110
1155
|
let totalItems = 0;
|
|
@@ -1117,7 +1162,8 @@ var breadcrumbValidRule = defineRule({
|
|
|
1117
1162
|
return {
|
|
1118
1163
|
status: "pass",
|
|
1119
1164
|
score: 1,
|
|
1120
|
-
rationale: `BreadcrumbList(s) valid (${totalItems} items)
|
|
1165
|
+
rationale: `BreadcrumbList(s) valid (${totalItems} items).`,
|
|
1166
|
+
rationale_ko: `BreadcrumbList\uAC00 \uC720\uD6A8\uD569\uB2C8\uB2E4 (\uD56D\uBAA9 ${totalItems}\uAC1C).`
|
|
1121
1167
|
};
|
|
1122
1168
|
}
|
|
1123
1169
|
const fatalCount = allIssues.length;
|
|
@@ -1127,6 +1173,7 @@ var breadcrumbValidRule = defineRule({
|
|
|
1127
1173
|
status: score < 0.5 ? "fail" : "warn",
|
|
1128
1174
|
score,
|
|
1129
1175
|
rationale: `${fatalCount} breadcrumb item(s) missing required fields.`,
|
|
1176
|
+
rationale_ko: `breadcrumb \uD56D\uBAA9 ${fatalCount}\uAC1C\uC5D0 \uD544\uC218 \uD544\uB4DC\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.`,
|
|
1130
1177
|
evidence: allIssues.slice(0, 5),
|
|
1131
1178
|
fixHint: 'Each itemListElement needs { "@type": "ListItem", position: N, name, item }.',
|
|
1132
1179
|
estimatedImpact: Math.round(2 * (1 - score))
|
|
@@ -1157,6 +1204,7 @@ var titleRule = defineRule({
|
|
|
1157
1204
|
effort: "low",
|
|
1158
1205
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#cittitle",
|
|
1159
1206
|
title: "<title> is set with a reasonable length",
|
|
1207
|
+
title_ko: "<title> \uD0DC\uADF8 \uC801\uC815 \uAE38\uC774 \uC124\uC815 \uC5EC\uBD80",
|
|
1160
1208
|
description: "The document title is the single most-cited piece of text and should be 10\u201370 characters.",
|
|
1161
1209
|
run(ctx) {
|
|
1162
1210
|
const title = ctx.$("head > title").first().text().trim();
|
|
@@ -1165,6 +1213,7 @@ var titleRule = defineRule({
|
|
|
1165
1213
|
status: "fail",
|
|
1166
1214
|
score: 0,
|
|
1167
1215
|
rationale: "Page has no <title>.",
|
|
1216
|
+
rationale_ko: "\uD398\uC774\uC9C0\uC5D0 <title>\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.",
|
|
1168
1217
|
fixUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md"
|
|
1169
1218
|
};
|
|
1170
1219
|
}
|
|
@@ -1172,17 +1221,19 @@ var titleRule = defineRule({
|
|
|
1172
1221
|
return {
|
|
1173
1222
|
status: "warn",
|
|
1174
1223
|
score: 0.4,
|
|
1175
|
-
rationale: `Title is only ${title.length} chars; consider a more descriptive one
|
|
1224
|
+
rationale: `Title is only ${title.length} chars; consider a more descriptive one.`,
|
|
1225
|
+
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.`
|
|
1176
1226
|
};
|
|
1177
1227
|
}
|
|
1178
1228
|
if (title.length > 70) {
|
|
1179
1229
|
return {
|
|
1180
1230
|
status: "warn",
|
|
1181
1231
|
score: 0.6,
|
|
1182
|
-
rationale: `Title is ${title.length} chars; search UIs commonly truncate after ~70
|
|
1232
|
+
rationale: `Title is ${title.length} chars; search UIs commonly truncate after ~70.`,
|
|
1233
|
+
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.`
|
|
1183
1234
|
};
|
|
1184
1235
|
}
|
|
1185
|
-
return { status: "pass", score: 1, rationale: `Title length ${title.length} is within range.` };
|
|
1236
|
+
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.` };
|
|
1186
1237
|
}
|
|
1187
1238
|
});
|
|
1188
1239
|
|
|
@@ -1197,6 +1248,7 @@ var metaDescriptionRule = defineRule({
|
|
|
1197
1248
|
effort: "low",
|
|
1198
1249
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#citmeta-description",
|
|
1199
1250
|
title: "meta description is set (50\u2013160 chars)",
|
|
1251
|
+
title_ko: "meta description \uC124\uC815 \uC5EC\uBD80 (50\u2013160\uC790)",
|
|
1200
1252
|
description: "AI snippets often quote the meta description verbatim; aim for 50\u2013160 chars.",
|
|
1201
1253
|
run(ctx) {
|
|
1202
1254
|
const desc = ctx.$('head meta[name="description"]').attr("content")?.trim() ?? "";
|
|
@@ -1205,16 +1257,17 @@ var metaDescriptionRule = defineRule({
|
|
|
1205
1257
|
status: "warn",
|
|
1206
1258
|
score: 0,
|
|
1207
1259
|
rationale: "No meta description set.",
|
|
1260
|
+
rationale_ko: "meta description\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.",
|
|
1208
1261
|
fixUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md"
|
|
1209
1262
|
};
|
|
1210
1263
|
}
|
|
1211
1264
|
if (desc.length < 50) {
|
|
1212
|
-
return { status: "warn", score: 0.5, rationale: `Only ${desc.length} chars; aim for 50
|
|
1265
|
+
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.` };
|
|
1213
1266
|
}
|
|
1214
1267
|
if (desc.length > 160) {
|
|
1215
|
-
return { status: "warn", score: 0.7, rationale: `${desc.length} chars; may be truncated after 160.` };
|
|
1268
|
+
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.` };
|
|
1216
1269
|
}
|
|
1217
|
-
return { status: "pass", score: 1, rationale: `Description length ${desc.length} is within range.` };
|
|
1270
|
+
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.` };
|
|
1218
1271
|
}
|
|
1219
1272
|
});
|
|
1220
1273
|
|
|
@@ -1229,6 +1282,7 @@ var canonicalRule = defineRule({
|
|
|
1229
1282
|
effort: "low",
|
|
1230
1283
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#citcanonical",
|
|
1231
1284
|
title: "Canonical URL is declared",
|
|
1285
|
+
title_ko: "Canonical URL \uC120\uC5B8 \uC5EC\uBD80",
|
|
1232
1286
|
description: 'rel="canonical" tells crawlers which URL is the source of truth, preventing duplicate-citation confusion.',
|
|
1233
1287
|
run(ctx) {
|
|
1234
1288
|
const href = ctx.$('head link[rel="canonical"]').attr("href")?.trim();
|
|
@@ -1237,14 +1291,15 @@ var canonicalRule = defineRule({
|
|
|
1237
1291
|
status: "warn",
|
|
1238
1292
|
score: 0,
|
|
1239
1293
|
rationale: 'No <link rel="canonical"> found.',
|
|
1294
|
+
rationale_ko: '<link rel="canonical">\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.',
|
|
1240
1295
|
fixUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md"
|
|
1241
1296
|
};
|
|
1242
1297
|
}
|
|
1243
1298
|
try {
|
|
1244
1299
|
const abs = new URL(href, ctx.finalUrl).toString();
|
|
1245
|
-
return { status: "pass", score: 1, rationale: `Canonical URL: ${abs}.` };
|
|
1300
|
+
return { status: "pass", score: 1, rationale: `Canonical URL: ${abs}.`, rationale_ko: `Canonical URL: ${abs}.` };
|
|
1246
1301
|
} catch {
|
|
1247
|
-
return { status: "fail", score: 0, rationale: `Canonical href is not a valid URL: ${href}` };
|
|
1302
|
+
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}` };
|
|
1248
1303
|
}
|
|
1249
1304
|
}
|
|
1250
1305
|
});
|
|
@@ -1261,6 +1316,7 @@ var ogTagsRule = defineRule({
|
|
|
1261
1316
|
effort: "low",
|
|
1262
1317
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#citog-tags",
|
|
1263
1318
|
title: "Open Graph tags are set",
|
|
1319
|
+
title_ko: "Open Graph \uD0DC\uADF8 \uC124\uC815 \uC5EC\uBD80",
|
|
1264
1320
|
description: "og:title/type/url/image power rich previews on AI chat, social, and messaging.",
|
|
1265
1321
|
run(ctx) {
|
|
1266
1322
|
const missing = [];
|
|
@@ -1269,13 +1325,14 @@ var ogTagsRule = defineRule({
|
|
|
1269
1325
|
if (!val) missing.push(prop);
|
|
1270
1326
|
}
|
|
1271
1327
|
if (missing.length === 0) {
|
|
1272
|
-
return { status: "pass", score: 1, rationale: "All required OG tags present." };
|
|
1328
|
+
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." };
|
|
1273
1329
|
}
|
|
1274
1330
|
const ratio = 1 - missing.length / REQUIRED.length;
|
|
1275
1331
|
return {
|
|
1276
1332
|
status: missing.length === REQUIRED.length ? "fail" : "warn",
|
|
1277
1333
|
score: ratio,
|
|
1278
1334
|
rationale: `Missing: ${missing.join(", ")}.`,
|
|
1335
|
+
rationale_ko: `\uB204\uB77D\uB41C \uD0DC\uADF8: ${missing.join(", ")}.`,
|
|
1279
1336
|
fixUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md"
|
|
1280
1337
|
};
|
|
1281
1338
|
}
|
|
@@ -1292,20 +1349,22 @@ var twitterCardRule = defineRule({
|
|
|
1292
1349
|
effort: "low",
|
|
1293
1350
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#cittwitter-card",
|
|
1294
1351
|
title: "Twitter Card metadata is set",
|
|
1352
|
+
title_ko: "Twitter Card \uBA54\uD0C0\uB370\uC774\uD130 \uC124\uC815 \uC5EC\uBD80",
|
|
1295
1353
|
description: "twitter:card + twitter:title give better previews on X/Twitter and some AI surfaces that reuse the tags.",
|
|
1296
1354
|
run(ctx) {
|
|
1297
1355
|
const card = ctx.$('head meta[name="twitter:card"]').attr("content")?.trim();
|
|
1298
1356
|
const title = ctx.$('head meta[name="twitter:title"]').attr("content")?.trim();
|
|
1299
1357
|
if (card && title) {
|
|
1300
|
-
return { status: "pass", score: 1, rationale: `Card type: ${card}.` };
|
|
1358
|
+
return { status: "pass", score: 1, rationale: `Card type: ${card}.`, rationale_ko: `\uCE74\uB4DC \uC720\uD615: ${card}.` };
|
|
1301
1359
|
}
|
|
1302
1360
|
if (card || title) {
|
|
1303
|
-
return { status: "warn", score: 0.5, rationale: "Partial twitter:* metadata; add the missing tag." };
|
|
1361
|
+
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." };
|
|
1304
1362
|
}
|
|
1305
1363
|
return {
|
|
1306
1364
|
status: "warn",
|
|
1307
1365
|
score: 0,
|
|
1308
1366
|
rationale: "No twitter:card metadata.",
|
|
1367
|
+
rationale_ko: "twitter:card \uBA54\uD0C0\uB370\uC774\uD130\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.",
|
|
1309
1368
|
fixUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md"
|
|
1310
1369
|
};
|
|
1311
1370
|
}
|
|
@@ -1322,6 +1381,7 @@ var langAttrRule = defineRule({
|
|
|
1322
1381
|
effort: "low",
|
|
1323
1382
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#citlang-attr",
|
|
1324
1383
|
title: "<html lang> is set",
|
|
1384
|
+
title_ko: "<html lang> \uC18D\uC131 \uC124\uC815 \uC5EC\uBD80",
|
|
1325
1385
|
description: "A lang attribute helps AI engines route the page to the right-language search surface (and helps screen readers).",
|
|
1326
1386
|
run(ctx) {
|
|
1327
1387
|
const lang = ctx.$("html").attr("lang")?.trim();
|
|
@@ -1330,10 +1390,11 @@ var langAttrRule = defineRule({
|
|
|
1330
1390
|
status: "warn",
|
|
1331
1391
|
score: 0,
|
|
1332
1392
|
rationale: "No lang attribute on <html>.",
|
|
1393
|
+
rationale_ko: "<html>\uC5D0 lang \uC18D\uC131\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.",
|
|
1333
1394
|
fixUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md"
|
|
1334
1395
|
};
|
|
1335
1396
|
}
|
|
1336
|
-
return { status: "pass", score: 1, rationale: `lang="${lang}".` };
|
|
1397
|
+
return { status: "pass", score: 1, rationale: `lang="${lang}".`, rationale_ko: `lang="${lang}".` };
|
|
1337
1398
|
}
|
|
1338
1399
|
});
|
|
1339
1400
|
|
|
@@ -1348,25 +1409,27 @@ var authorVisibleRule = defineRule({
|
|
|
1348
1409
|
effort: "medium",
|
|
1349
1410
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#citauthor-visible",
|
|
1350
1411
|
title: "Author is declared",
|
|
1412
|
+
title_ko: "\uC791\uC131\uC790 \uC815\uBCF4 \uC120\uC5B8 \uC5EC\uBD80",
|
|
1351
1413
|
description: "AI engines prefer citing content with an identifiable author; expose one via JSON-LD, meta[name=author], rel=author, or a .author class.",
|
|
1352
1414
|
run(ctx) {
|
|
1353
1415
|
for (const node of flattenJsonLd(ctx.jsonLd)) {
|
|
1354
1416
|
if (hasField(node, "author")) {
|
|
1355
|
-
return { status: "pass", score: 1, rationale: "Author found in JSON-LD." };
|
|
1417
|
+
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." };
|
|
1356
1418
|
}
|
|
1357
1419
|
}
|
|
1358
1420
|
const metaAuthor = ctx.$('head meta[name="author"]').attr("content")?.trim();
|
|
1359
|
-
if (metaAuthor) return { status: "pass", score: 1, rationale: `meta[name=author] = "${metaAuthor}".` };
|
|
1421
|
+
if (metaAuthor) return { status: "pass", score: 1, rationale: `meta[name=author] = "${metaAuthor}".`, rationale_ko: `meta[name=author] = "${metaAuthor}".` };
|
|
1360
1422
|
if (ctx.$('[rel="author"]').length > 0) {
|
|
1361
|
-
return { status: "pass", score: 1, rationale: 'rel="author" link found.' };
|
|
1423
|
+
return { status: "pass", score: 1, rationale: 'rel="author" link found.', rationale_ko: 'rel="author" \uB9C1\uD06C\uB97C \uCC3E\uC558\uC2B5\uB2C8\uB2E4.' };
|
|
1362
1424
|
}
|
|
1363
1425
|
if (ctx.$('.author, [class*="author"], [itemprop="author"]').length > 0) {
|
|
1364
|
-
return { status: "pass", score: 0.8, rationale: "Author-ish DOM selector found (weaker signal)." };
|
|
1426
|
+
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)." };
|
|
1365
1427
|
}
|
|
1366
1428
|
return {
|
|
1367
1429
|
status: "warn",
|
|
1368
1430
|
score: 0,
|
|
1369
1431
|
rationale: "No author signal found (JSON-LD, meta, rel, or .author).",
|
|
1432
|
+
rationale_ko: "\uC791\uC131\uC790 \uC815\uBCF4\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4 (JSON-LD, meta, rel, .author \uBAA8\uB450 \uC5C6\uC74C).",
|
|
1370
1433
|
fixUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md"
|
|
1371
1434
|
};
|
|
1372
1435
|
}
|
|
@@ -1383,25 +1446,27 @@ var datesRule = defineRule({
|
|
|
1383
1446
|
effort: "low",
|
|
1384
1447
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#citdates",
|
|
1385
1448
|
title: "Publish / modified date is present",
|
|
1449
|
+
title_ko: "\uBC1C\uD589\uC77C / \uC218\uC815\uC77C \uC874\uC7AC \uC5EC\uBD80",
|
|
1386
1450
|
description: "AI engines rank recent pages higher; expose datePublished via JSON-LD, <time datetime>, or article:published_time meta.",
|
|
1387
1451
|
run(ctx) {
|
|
1388
1452
|
for (const node of flattenJsonLd(ctx.jsonLd)) {
|
|
1389
1453
|
if (hasField(node, "datePublished")) {
|
|
1390
|
-
return { status: "pass", score: 1, rationale: "datePublished found in JSON-LD." };
|
|
1454
|
+
return { status: "pass", score: 1, rationale: "datePublished found in JSON-LD.", rationale_ko: "JSON-LD\uC5D0\uC11C datePublished\uB97C \uCC3E\uC558\uC2B5\uB2C8\uB2E4." };
|
|
1391
1455
|
}
|
|
1392
1456
|
}
|
|
1393
1457
|
const articleTime = ctx.$('head meta[property="article:published_time"]').attr("content")?.trim();
|
|
1394
1458
|
if (articleTime) {
|
|
1395
|
-
return { status: "pass", score: 1, rationale: `article:published_time = ${articleTime}.` };
|
|
1459
|
+
return { status: "pass", score: 1, rationale: `article:published_time = ${articleTime}.`, rationale_ko: `article:published_time = ${articleTime}.` };
|
|
1396
1460
|
}
|
|
1397
1461
|
const timeEl = ctx.$("time[datetime]").first().attr("datetime")?.trim();
|
|
1398
1462
|
if (timeEl) {
|
|
1399
|
-
return { status: "pass", score: 0.8, rationale: `<time datetime="${timeEl}"> found.` };
|
|
1463
|
+
return { status: "pass", score: 0.8, rationale: `<time datetime="${timeEl}"> found.`, rationale_ko: `<time datetime="${timeEl}">\uB97C \uCC3E\uC558\uC2B5\uB2C8\uB2E4.` };
|
|
1400
1464
|
}
|
|
1401
1465
|
return {
|
|
1402
1466
|
status: "warn",
|
|
1403
1467
|
score: 0,
|
|
1404
1468
|
rationale: "No publish date found (JSON-LD, meta article:published_time, or <time datetime>).",
|
|
1469
|
+
rationale_ko: "\uBC1C\uD589\uC77C\uC774 \uC5C6\uC2B5\uB2C8\uB2E4 (JSON-LD, meta article:published_time, <time datetime> \uBAA8\uB450 \uC5C6\uC74C).",
|
|
1405
1470
|
fixUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md"
|
|
1406
1471
|
};
|
|
1407
1472
|
}
|
|
@@ -1429,6 +1494,7 @@ var contentFreshnessRule = defineRule({
|
|
|
1429
1494
|
effort: "low",
|
|
1430
1495
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#citcontent-freshness",
|
|
1431
1496
|
title: "Article content is fresh (dateModified within 1 year)",
|
|
1497
|
+
title_ko: "\uCF58\uD150\uCE20 \uCD5C\uC2E0\uC131 (dateModified 1\uB144 \uC774\uB0B4)",
|
|
1432
1498
|
description: "AI engines down-rank stale content. Surface a recent dateModified (\u2264365 days) on Article-like pages so retrieval rankings stay strong.",
|
|
1433
1499
|
run(ctx) {
|
|
1434
1500
|
const nodes = flattenJsonLd(ctx.jsonLd);
|
|
@@ -1437,7 +1503,8 @@ var contentFreshnessRule = defineRule({
|
|
|
1437
1503
|
return {
|
|
1438
1504
|
status: "skip",
|
|
1439
1505
|
score: 0,
|
|
1440
|
-
rationale: "No Article/BlogPosting/NewsArticle JSON-LD; freshness signal not applicable."
|
|
1506
|
+
rationale: "No Article/BlogPosting/NewsArticle JSON-LD; freshness signal not applicable.",
|
|
1507
|
+
rationale_ko: "Article/BlogPosting/NewsArticle JSON-LD\uAC00 \uC5C6\uC5B4 \uCD5C\uC2E0\uC131 \uC2E0\uD638\uB97C \uD655\uC778\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4."
|
|
1441
1508
|
};
|
|
1442
1509
|
}
|
|
1443
1510
|
let bestMs = null;
|
|
@@ -1459,6 +1526,7 @@ var contentFreshnessRule = defineRule({
|
|
|
1459
1526
|
status: "warn",
|
|
1460
1527
|
score: 0,
|
|
1461
1528
|
rationale: "Article has no parseable dateModified or datePublished.",
|
|
1529
|
+
rationale_ko: "Article JSON-LD\uC5D0 \uD30C\uC2F1 \uAC00\uB2A5\uD55C dateModified \uB610\uB294 datePublished\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.",
|
|
1462
1530
|
fixHint: "Add ISO-8601 dateModified and datePublished to your Article JSON-LD.",
|
|
1463
1531
|
estimatedImpact: 3
|
|
1464
1532
|
};
|
|
@@ -1469,6 +1537,7 @@ var contentFreshnessRule = defineRule({
|
|
|
1469
1537
|
status: "pass",
|
|
1470
1538
|
score: 1,
|
|
1471
1539
|
rationale: `${usedField} within the last year (~${ageDays} day${ageDays === 1 ? "" : "s"} ago).`,
|
|
1540
|
+
rationale_ko: `${usedField}\uC774 1\uB144 \uC774\uB0B4\uC785\uB2C8\uB2E4 (\uC57D ${ageDays}\uC77C \uC804).`,
|
|
1472
1541
|
evidence: { ageDays, field: usedField }
|
|
1473
1542
|
};
|
|
1474
1543
|
}
|
|
@@ -1477,6 +1546,7 @@ var contentFreshnessRule = defineRule({
|
|
|
1477
1546
|
status: "warn",
|
|
1478
1547
|
score: 0.6,
|
|
1479
1548
|
rationale: `${usedField} is ${ageDays} days old. Refresh within a year for best AI ranking.`,
|
|
1549
|
+
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.`,
|
|
1480
1550
|
evidence: { ageDays, field: usedField },
|
|
1481
1551
|
estimatedImpact: 2
|
|
1482
1552
|
};
|
|
@@ -1485,6 +1555,7 @@ var contentFreshnessRule = defineRule({
|
|
|
1485
1555
|
status: "warn",
|
|
1486
1556
|
score: 0.2,
|
|
1487
1557
|
rationale: `${usedField} is ${ageDays} days old (>2 years). AI engines treat this as stale.`,
|
|
1558
|
+
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.`,
|
|
1488
1559
|
evidence: { ageDays, field: usedField },
|
|
1489
1560
|
fixHint: "Update content and bump dateModified to today's date.",
|
|
1490
1561
|
estimatedImpact: 3
|
|
@@ -1516,22 +1587,25 @@ var singleH1Rule = defineRule({
|
|
|
1516
1587
|
effort: "low",
|
|
1517
1588
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#cntsingle-h1",
|
|
1518
1589
|
title: "Exactly one <h1>",
|
|
1590
|
+
title_ko: "<h1> \uD0DC\uADF8 1\uAC1C \uC5EC\uBD80",
|
|
1519
1591
|
description: "A single H1 tells AI engines the primary topic of the page without ambiguity.",
|
|
1520
1592
|
run(ctx) {
|
|
1521
1593
|
const n = ctx.$("h1").length;
|
|
1522
|
-
if (n === 1) return { status: "pass", score: 1, rationale: "Exactly one <h1>." };
|
|
1594
|
+
if (n === 1) return { status: "pass", score: 1, rationale: "Exactly one <h1>.", rationale_ko: "<h1>\uC774 \uC815\uD655\uD788 1\uAC1C\uC785\uB2C8\uB2E4." };
|
|
1523
1595
|
if (n === 0) {
|
|
1524
1596
|
return {
|
|
1525
1597
|
status: "fail",
|
|
1526
1598
|
score: 0,
|
|
1527
1599
|
rationale: "No <h1> on the page.",
|
|
1600
|
+
rationale_ko: "\uD398\uC774\uC9C0\uC5D0 <h1>\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.",
|
|
1528
1601
|
fixUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules/cnt.single-h1.md"
|
|
1529
1602
|
};
|
|
1530
1603
|
}
|
|
1531
1604
|
return {
|
|
1532
1605
|
status: "warn",
|
|
1533
1606
|
score: Math.max(0.3, 1 / n),
|
|
1534
|
-
rationale: `Found ${n} <h1> tags; prefer one primary heading
|
|
1607
|
+
rationale: `Found ${n} <h1> tags; prefer one primary heading.`,
|
|
1608
|
+
rationale_ko: `<h1>\uC774 ${n}\uAC1C \uC788\uC2B5\uB2C8\uB2E4. \uB300\uD45C \uC81C\uBAA9 1\uAC1C\uB9CC \uC0AC\uC6A9\uD558\uC138\uC694.`
|
|
1535
1609
|
};
|
|
1536
1610
|
}
|
|
1537
1611
|
});
|
|
@@ -1547,6 +1621,7 @@ var headingHierarchyRule = defineRule({
|
|
|
1547
1621
|
effort: "medium",
|
|
1548
1622
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#cntheading-hierarchy",
|
|
1549
1623
|
title: "Heading levels do not skip",
|
|
1624
|
+
title_ko: "\uC81C\uBAA9 \uB2E8\uACC4 \uC21C\uC11C \uC900\uC218 \uC5EC\uBD80",
|
|
1550
1625
|
description: "Going from H2 directly to H4 breaks the outline AI engines use to segment content.",
|
|
1551
1626
|
run(ctx) {
|
|
1552
1627
|
const levels = [];
|
|
@@ -1556,7 +1631,7 @@ var headingHierarchyRule = defineRule({
|
|
|
1556
1631
|
if (m?.[1]) levels.push(parseInt(m[1], 10));
|
|
1557
1632
|
});
|
|
1558
1633
|
if (levels.length === 0) {
|
|
1559
|
-
return { status: "skip", score: 0, rationale: "No headings found." };
|
|
1634
|
+
return { status: "skip", score: 0, rationale: "No headings found.", rationale_ko: "\uC81C\uBAA9 \uD0DC\uADF8\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4." };
|
|
1560
1635
|
}
|
|
1561
1636
|
const skips = [];
|
|
1562
1637
|
for (let i = 1; i < levels.length; i++) {
|
|
@@ -1565,12 +1640,13 @@ var headingHierarchyRule = defineRule({
|
|
|
1565
1640
|
if (curr > prev + 1) skips.push({ from: prev, to: curr });
|
|
1566
1641
|
}
|
|
1567
1642
|
if (skips.length === 0) {
|
|
1568
|
-
return { status: "pass", score: 1, rationale: "No heading-level skips." };
|
|
1643
|
+
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." };
|
|
1569
1644
|
}
|
|
1570
1645
|
return {
|
|
1571
1646
|
status: "warn",
|
|
1572
1647
|
score: Math.max(0.3, 1 - skips.length / levels.length),
|
|
1573
1648
|
rationale: `${skips.length} heading skip(s) detected (e.g. h${skips[0].from}\u2192h${skips[0].to}).`,
|
|
1649
|
+
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}).`,
|
|
1574
1650
|
fixUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md"
|
|
1575
1651
|
};
|
|
1576
1652
|
}
|
|
@@ -1587,11 +1663,12 @@ var imageAltRule = defineRule({
|
|
|
1587
1663
|
effort: "medium",
|
|
1588
1664
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#cntimage-alt",
|
|
1589
1665
|
title: "\u226580% of <img> have alt text",
|
|
1666
|
+
title_ko: "<img>\uC758 80% \uC774\uC0C1 alt \uD14D\uC2A4\uD2B8 \uBCF4\uC720 \uC5EC\uBD80",
|
|
1590
1667
|
description: "Alt text gives AI engines a textual anchor for visual content and improves accessibility.",
|
|
1591
1668
|
run(ctx) {
|
|
1592
1669
|
const imgs = ctx.$("img");
|
|
1593
1670
|
const total = imgs.length;
|
|
1594
|
-
if (total === 0) return { status: "skip", score: 0, rationale: "No <img> on the page." };
|
|
1671
|
+
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." };
|
|
1595
1672
|
let withAlt = 0;
|
|
1596
1673
|
imgs.each((_i, el) => {
|
|
1597
1674
|
const alt = ctx.$(el).attr("alt");
|
|
@@ -1599,12 +1676,13 @@ var imageAltRule = defineRule({
|
|
|
1599
1676
|
});
|
|
1600
1677
|
const ratio = withAlt / total;
|
|
1601
1678
|
if (ratio >= 0.8) {
|
|
1602
|
-
return { status: "pass", score: 1, rationale: `${withAlt}/${total} images have alt (${Math.round(ratio * 100)}%).` };
|
|
1679
|
+
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)}%).` };
|
|
1603
1680
|
}
|
|
1604
1681
|
return {
|
|
1605
1682
|
status: "warn",
|
|
1606
1683
|
score: ratio,
|
|
1607
1684
|
rationale: `Only ${withAlt}/${total} images have alt text (${Math.round(ratio * 100)}%). Aim for \u226580%.`,
|
|
1685
|
+
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.`,
|
|
1608
1686
|
fixUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md"
|
|
1609
1687
|
};
|
|
1610
1688
|
}
|
|
@@ -1621,11 +1699,12 @@ var tldrOrFaqRule = defineRule({
|
|
|
1621
1699
|
effort: "medium",
|
|
1622
1700
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#cnttldr-or-faq",
|
|
1623
1701
|
title: "TL;DR summary or FAQ block",
|
|
1702
|
+
title_ko: "TL;DR \uC694\uC57D \uB610\uB294 FAQ \uBE14\uB85D \uC874\uC7AC \uC5EC\uBD80",
|
|
1624
1703
|
description: 'AI engines strongly prefer content with a quotable summary or FAQ \u2014 it makes the page "citation-ready".',
|
|
1625
1704
|
run(ctx) {
|
|
1626
1705
|
for (const node of flattenJsonLd(ctx.jsonLd)) {
|
|
1627
1706
|
if (getTypes(node).includes("FAQPage")) {
|
|
1628
|
-
return { status: "pass", score: 1, rationale: "FAQPage schema present." };
|
|
1707
|
+
return { status: "pass", score: 1, rationale: "FAQPage schema present.", rationale_ko: "FAQPage \uC2A4\uD0A4\uB9C8\uAC00 \uC788\uC2B5\uB2C8\uB2E4." };
|
|
1629
1708
|
}
|
|
1630
1709
|
}
|
|
1631
1710
|
const sel = [
|
|
@@ -1638,12 +1717,13 @@ var tldrOrFaqRule = defineRule({
|
|
|
1638
1717
|
"[data-tldr]"
|
|
1639
1718
|
].join(", ");
|
|
1640
1719
|
if (ctx.$(sel).length > 0) {
|
|
1641
|
-
return { status: "pass", score: 0.85, rationale: "TL;DR / summary / FAQ region detected by selector." };
|
|
1720
|
+
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." };
|
|
1642
1721
|
}
|
|
1643
1722
|
return {
|
|
1644
1723
|
status: "warn",
|
|
1645
1724
|
score: 0,
|
|
1646
1725
|
rationale: "No TL;DR / summary / FAQ found; add one to boost AI citation odds.",
|
|
1726
|
+
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.",
|
|
1647
1727
|
fixUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md"
|
|
1648
1728
|
};
|
|
1649
1729
|
}
|
|
@@ -1660,6 +1740,7 @@ var wordCountRule = defineRule({
|
|
|
1660
1740
|
effort: "high",
|
|
1661
1741
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#cntword-count",
|
|
1662
1742
|
title: "Page has enough body text",
|
|
1743
|
+
title_ko: "\uCDA9\uBD84\uD55C \uBCF8\uBB38 \uD14D\uC2A4\uD2B8 \uC5EC\uBD80",
|
|
1663
1744
|
description: "Thin pages (under ~100 words) are rarely cited by AI engines. Aim for \u2265300 words of meaningful body copy.",
|
|
1664
1745
|
run(ctx) {
|
|
1665
1746
|
const $ = ctx.$;
|
|
@@ -1667,12 +1748,13 @@ var wordCountRule = defineRule({
|
|
|
1667
1748
|
clone.find("script, style, noscript, nav, header, footer, aside").remove();
|
|
1668
1749
|
const text = clone.text().replace(/\s+/g, " ").trim();
|
|
1669
1750
|
const words = text ? text.split(" ").length : 0;
|
|
1670
|
-
if (words >= 300) return { status: "pass", score: 1, rationale: `${words} words of body text.` };
|
|
1671
|
-
if (words >= 100) return { status: "warn", score: 0.5, rationale: `Only ${words} words; aim for 300
|
|
1751
|
+
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.` };
|
|
1752
|
+
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.` };
|
|
1672
1753
|
return {
|
|
1673
1754
|
status: "fail",
|
|
1674
1755
|
score: 0,
|
|
1675
1756
|
rationale: `Only ${words} words; too thin to be cited.`,
|
|
1757
|
+
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.`,
|
|
1676
1758
|
fixUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md"
|
|
1677
1759
|
};
|
|
1678
1760
|
}
|
|
@@ -1723,11 +1805,12 @@ var qaStructureRule = defineRule({
|
|
|
1723
1805
|
effort: "medium",
|
|
1724
1806
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#cntqa-structure",
|
|
1725
1807
|
title: "Content uses Q&A structure for answer extraction",
|
|
1808
|
+
title_ko: "\uB2F5\uBCC0 \uCD94\uCD9C\uC744 \uC704\uD55C Q&A \uAD6C\uC870 \uC0AC\uC6A9 \uC5EC\uBD80",
|
|
1726
1809
|
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.",
|
|
1727
1810
|
run(ctx) {
|
|
1728
1811
|
for (const node of flattenJsonLd(ctx.jsonLd)) {
|
|
1729
1812
|
if (getTypes(node).includes("FAQPage")) {
|
|
1730
|
-
return { status: "pass", score: 1, rationale: "FAQPage JSON-LD provides explicit Q&A." };
|
|
1813
|
+
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." };
|
|
1731
1814
|
}
|
|
1732
1815
|
}
|
|
1733
1816
|
const questionHeadings = [];
|
|
@@ -1740,6 +1823,7 @@ var qaStructureRule = defineRule({
|
|
|
1740
1823
|
status: "pass",
|
|
1741
1824
|
score: 1,
|
|
1742
1825
|
rationale: `${questionHeadings.length} question-style headings detected.`,
|
|
1826
|
+
rationale_ko: `\uC9C8\uBB38\uD615 \uC81C\uBAA9\uC774 ${questionHeadings.length}\uAC1C \uAC10\uC9C0\uB429\uB2C8\uB2E4.`,
|
|
1743
1827
|
evidence: { headings: questionHeadings.slice(0, 5) }
|
|
1744
1828
|
};
|
|
1745
1829
|
}
|
|
@@ -1748,6 +1832,7 @@ var qaStructureRule = defineRule({
|
|
|
1748
1832
|
status: "warn",
|
|
1749
1833
|
score: 0.6,
|
|
1750
1834
|
rationale: "1 question-style heading. Add a second to strengthen answer extraction.",
|
|
1835
|
+
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.",
|
|
1751
1836
|
evidence: { headings: questionHeadings },
|
|
1752
1837
|
estimatedImpact: 1
|
|
1753
1838
|
};
|
|
@@ -1756,6 +1841,7 @@ var qaStructureRule = defineRule({
|
|
|
1756
1841
|
status: "warn",
|
|
1757
1842
|
score: 0,
|
|
1758
1843
|
rationale: "No question-style H2/H3 headings or FAQPage JSON-LD found.",
|
|
1844
|
+
rationale_ko: "\uC9C8\uBB38\uD615 H2/H3 \uC81C\uBAA9\uC774\uB098 FAQPage JSON-LD\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.",
|
|
1759
1845
|
fixHint: 'Reframe at least 2 H2 headings as questions ("How do I\u2026?", "What is\u2026?") or add FAQPage JSON-LD.',
|
|
1760
1846
|
estimatedImpact: 3
|
|
1761
1847
|
};
|
|
@@ -1773,13 +1859,14 @@ var externalCitationsRule = defineRule({
|
|
|
1773
1859
|
effort: "medium",
|
|
1774
1860
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#cntexternal-citations",
|
|
1775
1861
|
title: "Content cites external sources",
|
|
1862
|
+
title_ko: "\uC678\uBD80 \uCD9C\uCC98 \uC778\uC6A9 \uC5EC\uBD80",
|
|
1776
1863
|
description: "Outbound links to authoritative external sources are an E-E-A-T trust signal. AI engines treat well-cited pages as more credible.",
|
|
1777
1864
|
run(ctx) {
|
|
1778
1865
|
let host;
|
|
1779
1866
|
try {
|
|
1780
1867
|
host = new URL(ctx.finalUrl).hostname.toLowerCase();
|
|
1781
1868
|
} catch {
|
|
1782
|
-
return { status: "skip", score: 0, rationale: "Invalid finalUrl." };
|
|
1869
|
+
return { status: "skip", score: 0, rationale: "Invalid finalUrl.", rationale_ko: "\uC720\uD6A8\uD558\uC9C0 \uC54A\uC740 finalUrl\uC785\uB2C8\uB2E4." };
|
|
1783
1870
|
}
|
|
1784
1871
|
const seen = /* @__PURE__ */ new Set();
|
|
1785
1872
|
ctx.$("main a[href], article a[href], body a[href]").each((_i, el) => {
|
|
@@ -1804,6 +1891,7 @@ var externalCitationsRule = defineRule({
|
|
|
1804
1891
|
status: "pass",
|
|
1805
1892
|
score: 1,
|
|
1806
1893
|
rationale: `${count} distinct external host(s) cited (excluding nofollow).`,
|
|
1894
|
+
rationale_ko: `\uC678\uBD80 \uC0AC\uC774\uD2B8 ${count}\uAC1C\uB97C \uC778\uC6A9\uD569\uB2C8\uB2E4 (nofollow \uC81C\uC678).`,
|
|
1807
1895
|
evidence: { hosts: [...seen].slice(0, 8) }
|
|
1808
1896
|
};
|
|
1809
1897
|
}
|
|
@@ -1812,6 +1900,7 @@ var externalCitationsRule = defineRule({
|
|
|
1812
1900
|
status: "pass",
|
|
1813
1901
|
score: 0.7,
|
|
1814
1902
|
rationale: `${count} external host(s) cited. Aim for \u22653 for stronger E-E-A-T.`,
|
|
1903
|
+
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.`,
|
|
1815
1904
|
evidence: { hosts: [...seen] },
|
|
1816
1905
|
estimatedImpact: 1
|
|
1817
1906
|
};
|
|
@@ -1820,6 +1909,7 @@ var externalCitationsRule = defineRule({
|
|
|
1820
1909
|
status: "warn",
|
|
1821
1910
|
score: 0,
|
|
1822
1911
|
rationale: "No external follow citations found in main content.",
|
|
1912
|
+
rationale_ko: "\uBCF8\uBB38\uC5D0 \uC678\uBD80 \uCD9C\uCC98 \uB9C1\uD06C\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.",
|
|
1823
1913
|
fixHint: "Cite at least one authoritative external source (research paper, official docs, news outlet).",
|
|
1824
1914
|
estimatedImpact: 2
|
|
1825
1915
|
};
|