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 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
  };