geo-checker 0.3.0 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.cjs +297 -71
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +297 -71
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +289 -67
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +6 -1
- package/dist/index.d.ts +6 -1
- package/dist/index.js +289 -67
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.cjs
CHANGED
|
@@ -286,10 +286,12 @@ async function buildContext(url, opts = {}) {
|
|
|
286
286
|
"Site appears to be JS-rendered (sparse body + SPA root element). Re-run with --render for accurate results."
|
|
287
287
|
);
|
|
288
288
|
}
|
|
289
|
-
const [robotsRaw, llmsRaw, llmsFullRaw] = await Promise.all([
|
|
289
|
+
const [robotsRaw, llmsRaw, llmsFullRaw, skillMdRaw, agentPermissionsRaw] = await Promise.all([
|
|
290
290
|
fetchText(`${origin}/robots.txt`, opts),
|
|
291
291
|
fetchText(`${origin}/llms.txt`, opts),
|
|
292
|
-
fetchText(`${origin}/llms-full.txt`, opts)
|
|
292
|
+
fetchText(`${origin}/llms-full.txt`, opts),
|
|
293
|
+
fetchText(`${origin}/skill.md`, opts),
|
|
294
|
+
fetchText(`${origin}/agent-permissions.json`, opts)
|
|
293
295
|
]);
|
|
294
296
|
let sitemapUrl = null;
|
|
295
297
|
const robots = robotsRaw ? parseRobots(robotsRaw) : null;
|
|
@@ -297,6 +299,13 @@ async function buildContext(url, opts = {}) {
|
|
|
297
299
|
if (!sitemapUrl) sitemapUrl = `${origin}/sitemap.xml`;
|
|
298
300
|
const sitemapRaw = await fetchText(sitemapUrl, opts);
|
|
299
301
|
const sitemap = sitemapRaw ? parseSitemap(sitemapRaw) : null;
|
|
302
|
+
let agentPermissions = null;
|
|
303
|
+
if (agentPermissionsRaw && agentPermissionsRaw.trim().length > 0) {
|
|
304
|
+
try {
|
|
305
|
+
agentPermissions = JSON.parse(agentPermissionsRaw);
|
|
306
|
+
} catch {
|
|
307
|
+
}
|
|
308
|
+
}
|
|
300
309
|
return {
|
|
301
310
|
url,
|
|
302
311
|
finalUrl,
|
|
@@ -311,7 +320,9 @@ async function buildContext(url, opts = {}) {
|
|
|
311
320
|
jsonLd: extractJsonLd($),
|
|
312
321
|
renderMode,
|
|
313
322
|
fetchedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
314
|
-
warnings
|
|
323
|
+
warnings,
|
|
324
|
+
skillMd: skillMdRaw && skillMdRaw.trim().length > 0 ? skillMdRaw : null,
|
|
325
|
+
agentPermissions
|
|
315
326
|
};
|
|
316
327
|
}
|
|
317
328
|
|
|
@@ -320,10 +331,11 @@ function defineRule(rule) {
|
|
|
320
331
|
return rule;
|
|
321
332
|
}
|
|
322
333
|
var CATEGORY_WEIGHTS = {
|
|
323
|
-
crawler:
|
|
324
|
-
"structured-data":
|
|
325
|
-
citation:
|
|
326
|
-
content:
|
|
334
|
+
crawler: 20,
|
|
335
|
+
"structured-data": 25,
|
|
336
|
+
citation: 20,
|
|
337
|
+
content: 15,
|
|
338
|
+
aeo: 20
|
|
327
339
|
};
|
|
328
340
|
|
|
329
341
|
// src/engine.ts
|
|
@@ -337,7 +349,8 @@ async function runRules(ctx, rules, opts = {}) {
|
|
|
337
349
|
crawler: { score: 0, weight: weights.crawler, results: [] },
|
|
338
350
|
"structured-data": { score: 0, weight: weights["structured-data"], results: [] },
|
|
339
351
|
citation: { score: 0, weight: weights.citation, results: [] },
|
|
340
|
-
content: { score: 0, weight: weights.content, results: [] }
|
|
352
|
+
content: { score: 0, weight: weights.content, results: [] },
|
|
353
|
+
aeo: { score: 0, weight: weights.aeo, results: [] }
|
|
341
354
|
};
|
|
342
355
|
for (const rule of rules) {
|
|
343
356
|
if (onlySet && !onlySet.has(rule.id) && (!rule.stableId || !onlySet.has(rule.stableId))) continue;
|
|
@@ -362,6 +375,7 @@ async function runRules(ctx, rules, opts = {}) {
|
|
|
362
375
|
durationMs
|
|
363
376
|
};
|
|
364
377
|
if (rule.stableId !== void 0) entry.stableId = rule.stableId;
|
|
378
|
+
if (rule.title_ko !== void 0) entry.title_ko = rule.title_ko;
|
|
365
379
|
if (rule.group !== void 0) entry.group = rule.group;
|
|
366
380
|
if (rule.impact !== void 0) entry.impact = rule.impact;
|
|
367
381
|
if (rule.effort !== void 0) entry.effort = rule.effort;
|
|
@@ -424,13 +438,15 @@ var httpsRule = defineRule({
|
|
|
424
438
|
effort: "medium",
|
|
425
439
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#crawlerhttps",
|
|
426
440
|
title: "Site is served over HTTPS",
|
|
441
|
+
title_ko: "\uC0AC\uC774\uD2B8\uAC00 HTTPS\uB85C \uC81C\uACF5\uB428",
|
|
427
442
|
description: "AI crawlers treat HTTPS pages as more trustworthy and some skip plain HTTP entirely.",
|
|
428
443
|
run(ctx) {
|
|
429
444
|
const isHttps = ctx.finalUrl.startsWith("https://");
|
|
430
|
-
return isHttps ? { status: "pass", score: 1, rationale: "Final URL uses HTTPS." } : {
|
|
445
|
+
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
446
|
status: "fail",
|
|
432
447
|
score: 0,
|
|
433
448
|
rationale: "Final URL does not use HTTPS. Redirect HTTP \u2192 HTTPS site-wide.",
|
|
449
|
+
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
450
|
fixUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md"
|
|
435
451
|
};
|
|
436
452
|
}
|
|
@@ -447,15 +463,17 @@ var robotsReachableRule = defineRule({
|
|
|
447
463
|
effort: "low",
|
|
448
464
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#crawlerrobots-reachable",
|
|
449
465
|
title: "robots.txt is reachable",
|
|
466
|
+
title_ko: "robots.txt \uC811\uADFC \uAC00\uB2A5 \uC5EC\uBD80",
|
|
450
467
|
description: "A reachable robots.txt lets crawlers confirm their permissions; missing file is treated as allow-all but blocks explicit signalling.",
|
|
451
468
|
run(ctx) {
|
|
452
469
|
if (ctx.robots) {
|
|
453
|
-
return { status: "pass", score: 1, rationale: "robots.txt returned successfully." };
|
|
470
|
+
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
471
|
}
|
|
455
472
|
return {
|
|
456
473
|
status: "warn",
|
|
457
474
|
score: 0.3,
|
|
458
475
|
rationale: "robots.txt is missing. Add one even if empty to explicitly signal crawl policy.",
|
|
476
|
+
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
477
|
fixUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md"
|
|
460
478
|
};
|
|
461
479
|
}
|
|
@@ -503,13 +521,15 @@ var robotsAiAllowRule = defineRule({
|
|
|
503
521
|
effort: "low",
|
|
504
522
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#crawlerrobots-ai-allow",
|
|
505
523
|
title: "AI crawlers are allowed",
|
|
524
|
+
title_ko: "AI \uD06C\uB864\uB7EC \uC811\uADFC \uD5C8\uC6A9 \uC5EC\uBD80",
|
|
506
525
|
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
526
|
run(ctx) {
|
|
508
527
|
if (!ctx.robots) {
|
|
509
528
|
return {
|
|
510
529
|
status: "warn",
|
|
511
530
|
score: 0.5,
|
|
512
|
-
rationale: "robots.txt missing; AI crawlers default to allow, but explicit allow is recommended."
|
|
531
|
+
rationale: "robots.txt missing; AI crawlers default to allow, but explicit allow is recommended.",
|
|
532
|
+
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
533
|
};
|
|
514
534
|
}
|
|
515
535
|
const path = new URL(ctx.finalUrl).pathname || "/";
|
|
@@ -527,6 +547,7 @@ var robotsAiAllowRule = defineRule({
|
|
|
527
547
|
status: "fail",
|
|
528
548
|
score: Math.max(0, 1 - blocked.length / AI_BOTS.length),
|
|
529
549
|
rationale: `Blocked: ${blocked.join(", ")}. Remove the Disallow or add an explicit Allow for these user-agents.`,
|
|
550
|
+
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
551
|
evidence: { blocked, mentioned, totalBots: AI_BOTS.length },
|
|
531
552
|
fixUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md"
|
|
532
553
|
};
|
|
@@ -535,13 +556,15 @@ var robotsAiAllowRule = defineRule({
|
|
|
535
556
|
return {
|
|
536
557
|
status: "warn",
|
|
537
558
|
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
|
|
559
|
+
rationale: `All ${AI_BOTS.length} AI crawlers reach the page via default rules, but none are explicitly listed. Consider explicit Allow entries.`,
|
|
560
|
+
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
561
|
};
|
|
540
562
|
}
|
|
541
563
|
return {
|
|
542
564
|
status: "pass",
|
|
543
565
|
score: 1,
|
|
544
566
|
rationale: `All ${AI_BOTS.length} AI crawlers can reach the page; ${mentioned.length} explicitly listed.`,
|
|
567
|
+
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
568
|
evidence: { mentioned, totalBots: AI_BOTS.length }
|
|
546
569
|
};
|
|
547
570
|
}
|
|
@@ -558,15 +581,17 @@ var llmsTxtPresentRule = defineRule({
|
|
|
558
581
|
effort: "medium",
|
|
559
582
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#crawlerllms-txt-present",
|
|
560
583
|
title: "llms.txt is present",
|
|
584
|
+
title_ko: "llms.txt \uD30C\uC77C \uC874\uC7AC \uC5EC\uBD80",
|
|
561
585
|
description: "An /llms.txt file at the site root gives AI assistants a curated map of the most citation-worthy pages.",
|
|
562
586
|
run(ctx) {
|
|
563
587
|
if (ctx.llmsTxt) {
|
|
564
|
-
return { status: "pass", score: 1, rationale: "llms.txt found at site root." };
|
|
588
|
+
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
589
|
}
|
|
566
590
|
return {
|
|
567
591
|
status: "warn",
|
|
568
592
|
score: 0,
|
|
569
593
|
rationale: "No /llms.txt found. Add one to curate the pages AI assistants should read.",
|
|
594
|
+
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
595
|
fixUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md"
|
|
571
596
|
};
|
|
572
597
|
}
|
|
@@ -583,10 +608,11 @@ var llmsTxtWellformedRule = defineRule({
|
|
|
583
608
|
effort: "low",
|
|
584
609
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#crawlerllms-txt-wellformed",
|
|
585
610
|
title: "llms.txt follows the spec",
|
|
611
|
+
title_ko: "llms.txt \uC2A4\uD399 \uC900\uC218 \uC5EC\uBD80",
|
|
586
612
|
description: "Must start with an H1 project title, then a brief summary, then at least one H2 section containing link items.",
|
|
587
613
|
run(ctx) {
|
|
588
614
|
if (!ctx.llmsTxt) {
|
|
589
|
-
return { status: "skip", score: 0, rationale: "No llms.txt to validate." };
|
|
615
|
+
return { status: "skip", score: 0, rationale: "No llms.txt to validate.", rationale_ko: "\uAC80\uC99D\uD560 llms.txt\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4." };
|
|
590
616
|
}
|
|
591
617
|
const check = isLlmsTxtWellFormed(ctx.llmsTxt);
|
|
592
618
|
if (check.ok) {
|
|
@@ -594,13 +620,15 @@ var llmsTxtWellformedRule = defineRule({
|
|
|
594
620
|
return {
|
|
595
621
|
status: "pass",
|
|
596
622
|
score: 1,
|
|
597
|
-
rationale: `Well-formed with ${ctx.llmsTxt.sections.length} section(s) and ${totalLinks} link(s)
|
|
623
|
+
rationale: `Well-formed with ${ctx.llmsTxt.sections.length} section(s) and ${totalLinks} link(s).`,
|
|
624
|
+
rationale_ko: `\uC2A4\uD399\uC5D0 \uB9DE\uAC8C \uC791\uC131\uB428 (\uC139\uC158 ${ctx.llmsTxt.sections.length}\uAC1C, \uB9C1\uD06C ${totalLinks}\uAC1C).`
|
|
598
625
|
};
|
|
599
626
|
}
|
|
600
627
|
return {
|
|
601
628
|
status: "warn",
|
|
602
629
|
score: 0.3,
|
|
603
630
|
rationale: `llms.txt does not fully match the spec: ${check.reason}.`,
|
|
631
|
+
rationale_ko: `llms.txt\uAC00 \uC2A4\uD399\uC744 \uC644\uC804\uD788 \uB530\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4: ${check.reason}.`,
|
|
604
632
|
fixUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md"
|
|
605
633
|
};
|
|
606
634
|
}
|
|
@@ -617,13 +645,15 @@ var llmsFullTxtRule = defineRule({
|
|
|
617
645
|
effort: "medium",
|
|
618
646
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#crawlerllms-full-txt",
|
|
619
647
|
title: "llms-full.txt provides full-content mirror",
|
|
648
|
+
title_ko: "llms-full.txt \uC804\uCCB4 \uCF58\uD150\uCE20 \uBBF8\uB7EC \uC81C\uACF5 \uC5EC\uBD80",
|
|
620
649
|
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
650
|
run(ctx) {
|
|
622
651
|
if (ctx.llmsFullTxt && ctx.llmsFullTxt.length > 200) {
|
|
623
652
|
return {
|
|
624
653
|
status: "pass",
|
|
625
654
|
score: 1,
|
|
626
|
-
rationale: `/llms-full.txt found (${ctx.llmsFullTxt.length.toLocaleString()} chars)
|
|
655
|
+
rationale: `/llms-full.txt found (${ctx.llmsFullTxt.length.toLocaleString()} chars).`,
|
|
656
|
+
rationale_ko: `/llms-full.txt\uAC00 \uC874\uC7AC\uD569\uB2C8\uB2E4 (${ctx.llmsFullTxt.length.toLocaleString()}\uC790).`
|
|
627
657
|
};
|
|
628
658
|
}
|
|
629
659
|
if (ctx.llmsFullTxt) {
|
|
@@ -631,6 +661,7 @@ var llmsFullTxtRule = defineRule({
|
|
|
631
661
|
status: "warn",
|
|
632
662
|
score: 0.5,
|
|
633
663
|
rationale: `/llms-full.txt found but very short (${ctx.llmsFullTxt.length} chars). Consider expanding with page bodies.`,
|
|
664
|
+
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
665
|
fixHint: "Mirror full article bodies into /llms-full.txt so AI assistants can quote without re-crawling."
|
|
635
666
|
};
|
|
636
667
|
}
|
|
@@ -638,6 +669,7 @@ var llmsFullTxtRule = defineRule({
|
|
|
638
669
|
status: "warn",
|
|
639
670
|
score: 0,
|
|
640
671
|
rationale: "No /llms-full.txt found. Adding one lets AI assistants ingest the full corpus in a single request.",
|
|
672
|
+
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
673
|
fixHint: "Publish /llms-full.txt alongside /llms.txt with the full body text of your top pages.",
|
|
642
674
|
estimatedImpact: 1
|
|
643
675
|
};
|
|
@@ -655,19 +687,22 @@ var sitemapPresentRule = defineRule({
|
|
|
655
687
|
effort: "low",
|
|
656
688
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#crawlersitemap-present",
|
|
657
689
|
title: "sitemap.xml is present",
|
|
690
|
+
title_ko: "sitemap.xml \uC874\uC7AC \uC5EC\uBD80",
|
|
658
691
|
description: "A sitemap helps AI crawlers discover and prioritise pages; many crawlers short-circuit discovery without one.",
|
|
659
692
|
run(ctx) {
|
|
660
693
|
if (ctx.sitemap && ctx.sitemap.urls.length > 0) {
|
|
661
694
|
return {
|
|
662
695
|
status: "pass",
|
|
663
696
|
score: 1,
|
|
664
|
-
rationale: `Sitemap found with ${ctx.sitemap.urls.length} URL(s)
|
|
697
|
+
rationale: `Sitemap found with ${ctx.sitemap.urls.length} URL(s).`,
|
|
698
|
+
rationale_ko: `\uC0AC\uC774\uD2B8\uB9F5\uC5D0 URL\uC774 ${ctx.sitemap.urls.length}\uAC1C \uC788\uC2B5\uB2C8\uB2E4.`
|
|
665
699
|
};
|
|
666
700
|
}
|
|
667
701
|
return {
|
|
668
702
|
status: "warn",
|
|
669
703
|
score: 0.2,
|
|
670
704
|
rationale: "No sitemap.xml found (checked /sitemap.xml and Sitemap: directive in robots.txt).",
|
|
705
|
+
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
706
|
fixUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md"
|
|
672
707
|
};
|
|
673
708
|
}
|
|
@@ -695,15 +730,17 @@ var jsonLdPresentRule = defineRule({
|
|
|
695
730
|
effort: "medium",
|
|
696
731
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#sdjsonld-present",
|
|
697
732
|
title: "JSON-LD structured data is present",
|
|
733
|
+
title_ko: "JSON-LD \uAD6C\uC870\uD654 \uB370\uC774\uD130 \uC874\uC7AC \uC5EC\uBD80",
|
|
698
734
|
description: 'At least one <script type="application/ld+json"> block is the primary way AI engines map your page to an entity.',
|
|
699
735
|
run(ctx) {
|
|
700
736
|
if (ctx.jsonLd.length > 0) {
|
|
701
|
-
return { status: "pass", score: 1, rationale: `Found ${ctx.jsonLd.length} JSON-LD block(s).` };
|
|
737
|
+
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
738
|
}
|
|
703
739
|
return {
|
|
704
740
|
status: "fail",
|
|
705
741
|
score: 0,
|
|
706
742
|
rationale: "No JSON-LD blocks found. Add schema.org structured data.",
|
|
743
|
+
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
744
|
fixUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md"
|
|
708
745
|
};
|
|
709
746
|
}
|
|
@@ -779,20 +816,22 @@ var jsonLdValidJsonRule = defineRule({
|
|
|
779
816
|
effort: "low",
|
|
780
817
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#sdjsonld-valid-json",
|
|
781
818
|
title: "JSON-LD blocks parse as valid JSON",
|
|
819
|
+
title_ko: "JSON-LD \uBE14\uB85D\uC758 JSON \uC720\uD6A8\uC131",
|
|
782
820
|
description: "Malformed JSON in an ld+json block is silently ignored by most consumers \u2014 a costly silent failure.",
|
|
783
821
|
run(ctx) {
|
|
784
822
|
if (ctx.jsonLd.length === 0) {
|
|
785
|
-
return { status: "skip", score: 0, rationale: "No JSON-LD to validate." };
|
|
823
|
+
return { status: "skip", score: 0, rationale: "No JSON-LD to validate.", rationale_ko: "\uAC80\uC99D\uD560 JSON-LD\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4." };
|
|
786
824
|
}
|
|
787
825
|
if (hasParseError(ctx.jsonLd)) {
|
|
788
826
|
return {
|
|
789
827
|
status: "fail",
|
|
790
828
|
score: 0,
|
|
791
829
|
rationale: "One or more JSON-LD blocks failed to parse.",
|
|
830
|
+
rationale_ko: "JSON-LD \uBE14\uB85D \uD558\uB098 \uC774\uC0C1\uC744 \uD30C\uC2F1\uD558\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4.",
|
|
792
831
|
fixUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md"
|
|
793
832
|
};
|
|
794
833
|
}
|
|
795
|
-
return { status: "pass", score: 1, rationale: "All JSON-LD blocks parse cleanly." };
|
|
834
|
+
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
835
|
}
|
|
797
836
|
});
|
|
798
837
|
|
|
@@ -807,10 +846,11 @@ var schemaTypeRecognizedRule = defineRule({
|
|
|
807
846
|
effort: "low",
|
|
808
847
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#sdschema-type-recognized",
|
|
809
848
|
title: "Schema.org @type is a recognised kind",
|
|
849
|
+
title_ko: "Schema.org @type \uC778\uC2DD \uAC00\uB2A5 \uC5EC\uBD80",
|
|
810
850
|
description: "AI engines match pages against well-known types (Article, Product, FAQPage...). Obscure types weaken the signal.",
|
|
811
851
|
run(ctx) {
|
|
812
852
|
if (ctx.jsonLd.length === 0) {
|
|
813
|
-
return { status: "skip", score: 0, rationale: "No JSON-LD to analyse." };
|
|
853
|
+
return { status: "skip", score: 0, rationale: "No JSON-LD to analyse.", rationale_ko: "\uBD84\uC11D\uD560 JSON-LD\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4." };
|
|
814
854
|
}
|
|
815
855
|
const nodes = flattenJsonLd(ctx.jsonLd);
|
|
816
856
|
const recognized = /* @__PURE__ */ new Set();
|
|
@@ -826,6 +866,7 @@ var schemaTypeRecognizedRule = defineRule({
|
|
|
826
866
|
status: "pass",
|
|
827
867
|
score: 1,
|
|
828
868
|
rationale: `Recognised: ${[...recognized].join(", ")}.`,
|
|
869
|
+
rationale_ko: `\uC778\uC2DD\uB41C \uD0C0\uC785: ${[...recognized].join(", ")}.`,
|
|
829
870
|
evidence: { recognized: [...recognized], all: [...seenTypes] }
|
|
830
871
|
};
|
|
831
872
|
}
|
|
@@ -833,6 +874,7 @@ var schemaTypeRecognizedRule = defineRule({
|
|
|
833
874
|
status: "warn",
|
|
834
875
|
score: 0.3,
|
|
835
876
|
rationale: `No recognised schema.org types. Saw: ${[...seenTypes].join(", ") || "(none)"}.`,
|
|
877
|
+
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
878
|
fixUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md"
|
|
837
879
|
};
|
|
838
880
|
}
|
|
@@ -849,10 +891,11 @@ var requiredFieldsRule = defineRule({
|
|
|
849
891
|
effort: "medium",
|
|
850
892
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#sdrequired-fields",
|
|
851
893
|
title: "Required fields for recognised types are set",
|
|
894
|
+
title_ko: "\uC778\uC2DD\uB41C \uD0C0\uC785\uC758 \uD544\uC218 \uD544\uB4DC \uCDA9\uC871 \uC5EC\uBD80",
|
|
852
895
|
description: "Article needs headline/author/datePublished, FAQPage needs mainEntity, Product needs offers, etc.",
|
|
853
896
|
run(ctx) {
|
|
854
897
|
if (ctx.jsonLd.length === 0) {
|
|
855
|
-
return { status: "skip", score: 0, rationale: "No JSON-LD to analyse." };
|
|
898
|
+
return { status: "skip", score: 0, rationale: "No JSON-LD to analyse.", rationale_ko: "\uBD84\uC11D\uD560 JSON-LD\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4." };
|
|
856
899
|
}
|
|
857
900
|
const nodes = flattenJsonLd(ctx.jsonLd);
|
|
858
901
|
const missing = [];
|
|
@@ -871,14 +914,16 @@ var requiredFieldsRule = defineRule({
|
|
|
871
914
|
return {
|
|
872
915
|
status: "skip",
|
|
873
916
|
score: 0,
|
|
874
|
-
rationale: "No types with known required fields were found."
|
|
917
|
+
rationale: "No types with known required fields were found.",
|
|
918
|
+
rationale_ko: "\uD544\uC218 \uD544\uB4DC\uAC00 \uC815\uC758\uB41C \uD0C0\uC785\uC774 \uC5C6\uC2B5\uB2C8\uB2E4."
|
|
875
919
|
};
|
|
876
920
|
}
|
|
877
921
|
if (missing.length === 0) {
|
|
878
922
|
return {
|
|
879
923
|
status: "pass",
|
|
880
924
|
score: 1,
|
|
881
|
-
rationale: `Required fields set on ${checked.length} node(s)
|
|
925
|
+
rationale: `Required fields set on ${checked.length} node(s).`,
|
|
926
|
+
rationale_ko: `${checked.length}\uAC1C \uB178\uB4DC\uC758 \uD544\uC218 \uD544\uB4DC\uAC00 \uBAA8\uB450 \uCDA9\uC871\uB429\uB2C8\uB2E4.`
|
|
882
927
|
};
|
|
883
928
|
}
|
|
884
929
|
const msg = missing.map((m) => `${m.type}.${m.field}`).join(", ");
|
|
@@ -886,6 +931,7 @@ var requiredFieldsRule = defineRule({
|
|
|
886
931
|
status: "fail",
|
|
887
932
|
score: Math.max(0, 1 - missing.length / (checked.length * 2)),
|
|
888
933
|
rationale: `Missing required fields: ${msg}.`,
|
|
934
|
+
rationale_ko: `\uB204\uB77D\uB41C \uD544\uC218 \uD544\uB4DC: ${msg}.`,
|
|
889
935
|
evidence: missing,
|
|
890
936
|
fixUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md"
|
|
891
937
|
};
|
|
@@ -903,10 +949,11 @@ var microdataFallbackRule = defineRule({
|
|
|
903
949
|
effort: "medium",
|
|
904
950
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#sdmicrodata-fallback",
|
|
905
951
|
title: "Microdata or RDFa fallback when JSON-LD is missing",
|
|
952
|
+
title_ko: "JSON-LD \uC5C6\uC744 \uB54C Microdata/RDFa \uB300\uCCB4 \uC5EC\uBD80",
|
|
906
953
|
description: "If JSON-LD is absent, inline microdata (itemscope/itemtype) or RDFa still gives some structured signal.",
|
|
907
954
|
run(ctx) {
|
|
908
955
|
if (ctx.jsonLd.length > 0) {
|
|
909
|
-
return { status: "skip", score: 0, rationale: "JSON-LD is present; fallback not needed." };
|
|
956
|
+
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
957
|
}
|
|
911
958
|
const microdata = ctx.$("[itemscope][itemtype]").length;
|
|
912
959
|
const rdfa = ctx.$("[typeof][vocab], [typeof][property]").length;
|
|
@@ -914,13 +961,15 @@ var microdataFallbackRule = defineRule({
|
|
|
914
961
|
return {
|
|
915
962
|
status: "pass",
|
|
916
963
|
score: 1,
|
|
917
|
-
rationale: `Found ${microdata} microdata and ${rdfa} RDFa nodes
|
|
964
|
+
rationale: `Found ${microdata} microdata and ${rdfa} RDFa nodes.`,
|
|
965
|
+
rationale_ko: `Microdata ${microdata}\uAC1C, RDFa ${rdfa}\uAC1C \uBC1C\uACAC\uB429\uB2C8\uB2E4.`
|
|
918
966
|
};
|
|
919
967
|
}
|
|
920
968
|
return {
|
|
921
969
|
status: "fail",
|
|
922
970
|
score: 0,
|
|
923
971
|
rationale: "No structured data at all (no JSON-LD, no microdata, no RDFa).",
|
|
972
|
+
rationale_ko: "\uAD6C\uC870\uD654 \uB370\uC774\uD130\uAC00 \uC804\uD600 \uC5C6\uC2B5\uB2C8\uB2E4 (JSON-LD, Microdata, RDFa \uBAA8\uB450 \uC5C6\uC74C).",
|
|
924
973
|
fixUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md"
|
|
925
974
|
};
|
|
926
975
|
}
|
|
@@ -938,10 +987,11 @@ var noDuplicateTypesRule = defineRule({
|
|
|
938
987
|
effort: "low",
|
|
939
988
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#sdno-duplicate-types",
|
|
940
989
|
title: "No conflicting duplicate @types",
|
|
990
|
+
title_ko: "@type \uC911\uBCF5 \uCDA9\uB3CC \uC5C6\uC74C",
|
|
941
991
|
description: "Multiple competing entities of the same primary type (e.g. two Articles) confuse the engine about which one represents the page.",
|
|
942
992
|
run(ctx) {
|
|
943
993
|
if (ctx.jsonLd.length === 0) {
|
|
944
|
-
return { status: "skip", score: 0, rationale: "No JSON-LD to analyse." };
|
|
994
|
+
return { status: "skip", score: 0, rationale: "No JSON-LD to analyse.", rationale_ko: "\uBD84\uC11D\uD560 JSON-LD\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4." };
|
|
945
995
|
}
|
|
946
996
|
const counts = /* @__PURE__ */ new Map();
|
|
947
997
|
for (const node of flattenJsonLd(ctx.jsonLd)) {
|
|
@@ -951,12 +1001,13 @@ var noDuplicateTypesRule = defineRule({
|
|
|
951
1001
|
}
|
|
952
1002
|
const dupes = [...counts.entries()].filter(([, n]) => n > 1);
|
|
953
1003
|
if (dupes.length === 0) {
|
|
954
|
-
return { status: "pass", score: 1, rationale: "No duplicate primary types." };
|
|
1004
|
+
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
1005
|
}
|
|
956
1006
|
return {
|
|
957
1007
|
status: "warn",
|
|
958
1008
|
score: 0.4,
|
|
959
1009
|
rationale: `Duplicate primary types: ${dupes.map(([t, n]) => `${t}\xD7${n}`).join(", ")}.`,
|
|
1010
|
+
rationale_ko: `\uC911\uBCF5\uB41C \uAE30\uBCF8 \uD0C0\uC785: ${dupes.map(([t, n]) => `${t}\xD7${n}`).join(", ")}.`,
|
|
960
1011
|
fixUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md"
|
|
961
1012
|
};
|
|
962
1013
|
}
|
|
@@ -1004,10 +1055,11 @@ var sameAsEntityRule = defineRule({
|
|
|
1004
1055
|
effort: "medium",
|
|
1005
1056
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#sdsameas-entity",
|
|
1006
1057
|
title: "Entity nodes link the knowledge graph via sameAs",
|
|
1058
|
+
title_ko: "sameAs\uB85C \uC9C0\uC2DD \uADF8\uB798\uD504 \uC5F0\uACB0 \uC5EC\uBD80",
|
|
1007
1059
|
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
1060
|
run(ctx) {
|
|
1009
1061
|
if (ctx.jsonLd.length === 0) {
|
|
1010
|
-
return { status: "skip", score: 0, rationale: "No JSON-LD to analyse." };
|
|
1062
|
+
return { status: "skip", score: 0, rationale: "No JSON-LD to analyse.", rationale_ko: "\uBD84\uC11D\uD560 JSON-LD\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4." };
|
|
1011
1063
|
}
|
|
1012
1064
|
const nodes = flattenJsonLd(ctx.jsonLd);
|
|
1013
1065
|
const entities = nodes.filter((n) => getTypes(n).some((t) => ENTITY_TYPES.includes(t)));
|
|
@@ -1015,7 +1067,8 @@ var sameAsEntityRule = defineRule({
|
|
|
1015
1067
|
return {
|
|
1016
1068
|
status: "skip",
|
|
1017
1069
|
score: 0,
|
|
1018
|
-
rationale: "No Organization/Person/LocalBusiness/Brand entity to link."
|
|
1070
|
+
rationale: "No Organization/Person/LocalBusiness/Brand entity to link.",
|
|
1071
|
+
rationale_ko: "\uC5F0\uACB0\uD560 Organization/Person/LocalBusiness/Brand \uC5D4\uD2F0\uD2F0\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4."
|
|
1019
1072
|
};
|
|
1020
1073
|
}
|
|
1021
1074
|
let bestScore = 0;
|
|
@@ -1039,6 +1092,7 @@ var sameAsEntityRule = defineRule({
|
|
|
1039
1092
|
status: "pass",
|
|
1040
1093
|
score: 1,
|
|
1041
1094
|
rationale: `Entity links ${bestEvidence.trusted} trusted knowledge-graph hosts via sameAs.`,
|
|
1095
|
+
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
1096
|
evidence: bestEvidence
|
|
1043
1097
|
};
|
|
1044
1098
|
}
|
|
@@ -1047,6 +1101,7 @@ var sameAsEntityRule = defineRule({
|
|
|
1047
1101
|
status: "pass",
|
|
1048
1102
|
score: bestScore,
|
|
1049
1103
|
rationale: `Entity has 1 trusted sameAs link. Add Wikipedia/Wikidata for stronger E-E-A-T.`,
|
|
1104
|
+
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
1105
|
evidence: bestEvidence,
|
|
1051
1106
|
estimatedImpact: 1
|
|
1052
1107
|
};
|
|
@@ -1056,6 +1111,7 @@ var sameAsEntityRule = defineRule({
|
|
|
1056
1111
|
status: "warn",
|
|
1057
1112
|
score: bestScore,
|
|
1058
1113
|
rationale: "Entity declares sameAs but no trusted knowledge-graph hosts (Wikipedia/Wikidata/LinkedIn).",
|
|
1114
|
+
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
1115
|
evidence: bestEvidence,
|
|
1060
1116
|
fixHint: "Add Wikipedia/Wikidata/LinkedIn URLs to your Organization sameAs[].",
|
|
1061
1117
|
estimatedImpact: 2
|
|
@@ -1065,6 +1121,7 @@ var sameAsEntityRule = defineRule({
|
|
|
1065
1121
|
status: "warn",
|
|
1066
1122
|
score: 0,
|
|
1067
1123
|
rationale: `${entities.length} entity node(s) found but none declare sameAs links.`,
|
|
1124
|
+
rationale_ko: `\uC5D4\uD2F0\uD2F0 \uB178\uB4DC\uAC00 ${entities.length}\uAC1C \uC788\uC9C0\uB9CC sameAs \uB9C1\uD06C\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.`,
|
|
1068
1125
|
fixHint: 'Add sameAs:["https://en.wikipedia.org/wiki/...", "https://www.linkedin.com/company/..."] to your Organization JSON-LD.',
|
|
1069
1126
|
estimatedImpact: 3
|
|
1070
1127
|
};
|
|
@@ -1096,15 +1153,16 @@ var breadcrumbValidRule = defineRule({
|
|
|
1096
1153
|
effort: "medium",
|
|
1097
1154
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#sdbreadcrumb-valid",
|
|
1098
1155
|
title: "BreadcrumbList items declare position, name, and item",
|
|
1156
|
+
title_ko: "BreadcrumbList \uD56D\uBAA9\uC758 \uD544\uC218 \uD544\uB4DC \uCDA9\uC871 \uC5EC\uBD80",
|
|
1099
1157
|
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
1158
|
run(ctx) {
|
|
1101
1159
|
if (ctx.jsonLd.length === 0) {
|
|
1102
|
-
return { status: "skip", score: 0, rationale: "No JSON-LD to analyse." };
|
|
1160
|
+
return { status: "skip", score: 0, rationale: "No JSON-LD to analyse.", rationale_ko: "\uBD84\uC11D\uD560 JSON-LD\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4." };
|
|
1103
1161
|
}
|
|
1104
1162
|
const nodes = flattenJsonLd(ctx.jsonLd);
|
|
1105
1163
|
const breadcrumbs = nodes.filter((n) => getTypes(n).includes("BreadcrumbList"));
|
|
1106
1164
|
if (breadcrumbs.length === 0) {
|
|
1107
|
-
return { status: "skip", score: 0, rationale: "No BreadcrumbList present." };
|
|
1165
|
+
return { status: "skip", score: 0, rationale: "No BreadcrumbList present.", rationale_ko: "BreadcrumbList\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4." };
|
|
1108
1166
|
}
|
|
1109
1167
|
const allIssues = [];
|
|
1110
1168
|
let totalItems = 0;
|
|
@@ -1117,7 +1175,8 @@ var breadcrumbValidRule = defineRule({
|
|
|
1117
1175
|
return {
|
|
1118
1176
|
status: "pass",
|
|
1119
1177
|
score: 1,
|
|
1120
|
-
rationale: `BreadcrumbList(s) valid (${totalItems} items)
|
|
1178
|
+
rationale: `BreadcrumbList(s) valid (${totalItems} items).`,
|
|
1179
|
+
rationale_ko: `BreadcrumbList\uAC00 \uC720\uD6A8\uD569\uB2C8\uB2E4 (\uD56D\uBAA9 ${totalItems}\uAC1C).`
|
|
1121
1180
|
};
|
|
1122
1181
|
}
|
|
1123
1182
|
const fatalCount = allIssues.length;
|
|
@@ -1127,6 +1186,7 @@ var breadcrumbValidRule = defineRule({
|
|
|
1127
1186
|
status: score < 0.5 ? "fail" : "warn",
|
|
1128
1187
|
score,
|
|
1129
1188
|
rationale: `${fatalCount} breadcrumb item(s) missing required fields.`,
|
|
1189
|
+
rationale_ko: `breadcrumb \uD56D\uBAA9 ${fatalCount}\uAC1C\uC5D0 \uD544\uC218 \uD544\uB4DC\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.`,
|
|
1130
1190
|
evidence: allIssues.slice(0, 5),
|
|
1131
1191
|
fixHint: 'Each itemListElement needs { "@type": "ListItem", position: N, name, item }.',
|
|
1132
1192
|
estimatedImpact: Math.round(2 * (1 - score))
|
|
@@ -1157,6 +1217,7 @@ var titleRule = defineRule({
|
|
|
1157
1217
|
effort: "low",
|
|
1158
1218
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#cittitle",
|
|
1159
1219
|
title: "<title> is set with a reasonable length",
|
|
1220
|
+
title_ko: "<title> \uD0DC\uADF8 \uC801\uC815 \uAE38\uC774 \uC124\uC815 \uC5EC\uBD80",
|
|
1160
1221
|
description: "The document title is the single most-cited piece of text and should be 10\u201370 characters.",
|
|
1161
1222
|
run(ctx) {
|
|
1162
1223
|
const title = ctx.$("head > title").first().text().trim();
|
|
@@ -1165,6 +1226,7 @@ var titleRule = defineRule({
|
|
|
1165
1226
|
status: "fail",
|
|
1166
1227
|
score: 0,
|
|
1167
1228
|
rationale: "Page has no <title>.",
|
|
1229
|
+
rationale_ko: "\uD398\uC774\uC9C0\uC5D0 <title>\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.",
|
|
1168
1230
|
fixUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md"
|
|
1169
1231
|
};
|
|
1170
1232
|
}
|
|
@@ -1172,17 +1234,19 @@ var titleRule = defineRule({
|
|
|
1172
1234
|
return {
|
|
1173
1235
|
status: "warn",
|
|
1174
1236
|
score: 0.4,
|
|
1175
|
-
rationale: `Title is only ${title.length} chars; consider a more descriptive one
|
|
1237
|
+
rationale: `Title is only ${title.length} chars; consider a more descriptive one.`,
|
|
1238
|
+
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
1239
|
};
|
|
1177
1240
|
}
|
|
1178
1241
|
if (title.length > 70) {
|
|
1179
1242
|
return {
|
|
1180
1243
|
status: "warn",
|
|
1181
1244
|
score: 0.6,
|
|
1182
|
-
rationale: `Title is ${title.length} chars; search UIs commonly truncate after ~70
|
|
1245
|
+
rationale: `Title is ${title.length} chars; search UIs commonly truncate after ~70.`,
|
|
1246
|
+
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
1247
|
};
|
|
1184
1248
|
}
|
|
1185
|
-
return { status: "pass", score: 1, rationale: `Title length ${title.length} is within range.` };
|
|
1249
|
+
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
1250
|
}
|
|
1187
1251
|
});
|
|
1188
1252
|
|
|
@@ -1197,6 +1261,7 @@ var metaDescriptionRule = defineRule({
|
|
|
1197
1261
|
effort: "low",
|
|
1198
1262
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#citmeta-description",
|
|
1199
1263
|
title: "meta description is set (50\u2013160 chars)",
|
|
1264
|
+
title_ko: "meta description \uC124\uC815 \uC5EC\uBD80 (50\u2013160\uC790)",
|
|
1200
1265
|
description: "AI snippets often quote the meta description verbatim; aim for 50\u2013160 chars.",
|
|
1201
1266
|
run(ctx) {
|
|
1202
1267
|
const desc = ctx.$('head meta[name="description"]').attr("content")?.trim() ?? "";
|
|
@@ -1205,16 +1270,17 @@ var metaDescriptionRule = defineRule({
|
|
|
1205
1270
|
status: "warn",
|
|
1206
1271
|
score: 0,
|
|
1207
1272
|
rationale: "No meta description set.",
|
|
1273
|
+
rationale_ko: "meta description\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.",
|
|
1208
1274
|
fixUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md"
|
|
1209
1275
|
};
|
|
1210
1276
|
}
|
|
1211
1277
|
if (desc.length < 50) {
|
|
1212
|
-
return { status: "warn", score: 0.5, rationale: `Only ${desc.length} chars; aim for 50
|
|
1278
|
+
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
1279
|
}
|
|
1214
1280
|
if (desc.length > 160) {
|
|
1215
|
-
return { status: "warn", score: 0.7, rationale: `${desc.length} chars; may be truncated after 160.` };
|
|
1281
|
+
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
1282
|
}
|
|
1217
|
-
return { status: "pass", score: 1, rationale: `Description length ${desc.length} is within range.` };
|
|
1283
|
+
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
1284
|
}
|
|
1219
1285
|
});
|
|
1220
1286
|
|
|
@@ -1229,6 +1295,7 @@ var canonicalRule = defineRule({
|
|
|
1229
1295
|
effort: "low",
|
|
1230
1296
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#citcanonical",
|
|
1231
1297
|
title: "Canonical URL is declared",
|
|
1298
|
+
title_ko: "Canonical URL \uC120\uC5B8 \uC5EC\uBD80",
|
|
1232
1299
|
description: 'rel="canonical" tells crawlers which URL is the source of truth, preventing duplicate-citation confusion.',
|
|
1233
1300
|
run(ctx) {
|
|
1234
1301
|
const href = ctx.$('head link[rel="canonical"]').attr("href")?.trim();
|
|
@@ -1237,14 +1304,15 @@ var canonicalRule = defineRule({
|
|
|
1237
1304
|
status: "warn",
|
|
1238
1305
|
score: 0,
|
|
1239
1306
|
rationale: 'No <link rel="canonical"> found.',
|
|
1307
|
+
rationale_ko: '<link rel="canonical">\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.',
|
|
1240
1308
|
fixUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md"
|
|
1241
1309
|
};
|
|
1242
1310
|
}
|
|
1243
1311
|
try {
|
|
1244
1312
|
const abs = new URL(href, ctx.finalUrl).toString();
|
|
1245
|
-
return { status: "pass", score: 1, rationale: `Canonical URL: ${abs}.` };
|
|
1313
|
+
return { status: "pass", score: 1, rationale: `Canonical URL: ${abs}.`, rationale_ko: `Canonical URL: ${abs}.` };
|
|
1246
1314
|
} catch {
|
|
1247
|
-
return { status: "fail", score: 0, rationale: `Canonical href is not a valid URL: ${href}` };
|
|
1315
|
+
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
1316
|
}
|
|
1249
1317
|
}
|
|
1250
1318
|
});
|
|
@@ -1261,6 +1329,7 @@ var ogTagsRule = defineRule({
|
|
|
1261
1329
|
effort: "low",
|
|
1262
1330
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#citog-tags",
|
|
1263
1331
|
title: "Open Graph tags are set",
|
|
1332
|
+
title_ko: "Open Graph \uD0DC\uADF8 \uC124\uC815 \uC5EC\uBD80",
|
|
1264
1333
|
description: "og:title/type/url/image power rich previews on AI chat, social, and messaging.",
|
|
1265
1334
|
run(ctx) {
|
|
1266
1335
|
const missing = [];
|
|
@@ -1269,13 +1338,14 @@ var ogTagsRule = defineRule({
|
|
|
1269
1338
|
if (!val) missing.push(prop);
|
|
1270
1339
|
}
|
|
1271
1340
|
if (missing.length === 0) {
|
|
1272
|
-
return { status: "pass", score: 1, rationale: "All required OG tags present." };
|
|
1341
|
+
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
1342
|
}
|
|
1274
1343
|
const ratio = 1 - missing.length / REQUIRED.length;
|
|
1275
1344
|
return {
|
|
1276
1345
|
status: missing.length === REQUIRED.length ? "fail" : "warn",
|
|
1277
1346
|
score: ratio,
|
|
1278
1347
|
rationale: `Missing: ${missing.join(", ")}.`,
|
|
1348
|
+
rationale_ko: `\uB204\uB77D\uB41C \uD0DC\uADF8: ${missing.join(", ")}.`,
|
|
1279
1349
|
fixUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md"
|
|
1280
1350
|
};
|
|
1281
1351
|
}
|
|
@@ -1292,20 +1362,22 @@ var twitterCardRule = defineRule({
|
|
|
1292
1362
|
effort: "low",
|
|
1293
1363
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#cittwitter-card",
|
|
1294
1364
|
title: "Twitter Card metadata is set",
|
|
1365
|
+
title_ko: "Twitter Card \uBA54\uD0C0\uB370\uC774\uD130 \uC124\uC815 \uC5EC\uBD80",
|
|
1295
1366
|
description: "twitter:card + twitter:title give better previews on X/Twitter and some AI surfaces that reuse the tags.",
|
|
1296
1367
|
run(ctx) {
|
|
1297
1368
|
const card = ctx.$('head meta[name="twitter:card"]').attr("content")?.trim();
|
|
1298
1369
|
const title = ctx.$('head meta[name="twitter:title"]').attr("content")?.trim();
|
|
1299
1370
|
if (card && title) {
|
|
1300
|
-
return { status: "pass", score: 1, rationale: `Card type: ${card}.` };
|
|
1371
|
+
return { status: "pass", score: 1, rationale: `Card type: ${card}.`, rationale_ko: `\uCE74\uB4DC \uC720\uD615: ${card}.` };
|
|
1301
1372
|
}
|
|
1302
1373
|
if (card || title) {
|
|
1303
|
-
return { status: "warn", score: 0.5, rationale: "Partial twitter:* metadata; add the missing tag." };
|
|
1374
|
+
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
1375
|
}
|
|
1305
1376
|
return {
|
|
1306
1377
|
status: "warn",
|
|
1307
1378
|
score: 0,
|
|
1308
1379
|
rationale: "No twitter:card metadata.",
|
|
1380
|
+
rationale_ko: "twitter:card \uBA54\uD0C0\uB370\uC774\uD130\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.",
|
|
1309
1381
|
fixUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md"
|
|
1310
1382
|
};
|
|
1311
1383
|
}
|
|
@@ -1322,6 +1394,7 @@ var langAttrRule = defineRule({
|
|
|
1322
1394
|
effort: "low",
|
|
1323
1395
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#citlang-attr",
|
|
1324
1396
|
title: "<html lang> is set",
|
|
1397
|
+
title_ko: "<html lang> \uC18D\uC131 \uC124\uC815 \uC5EC\uBD80",
|
|
1325
1398
|
description: "A lang attribute helps AI engines route the page to the right-language search surface (and helps screen readers).",
|
|
1326
1399
|
run(ctx) {
|
|
1327
1400
|
const lang = ctx.$("html").attr("lang")?.trim();
|
|
@@ -1330,10 +1403,11 @@ var langAttrRule = defineRule({
|
|
|
1330
1403
|
status: "warn",
|
|
1331
1404
|
score: 0,
|
|
1332
1405
|
rationale: "No lang attribute on <html>.",
|
|
1406
|
+
rationale_ko: "<html>\uC5D0 lang \uC18D\uC131\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.",
|
|
1333
1407
|
fixUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md"
|
|
1334
1408
|
};
|
|
1335
1409
|
}
|
|
1336
|
-
return { status: "pass", score: 1, rationale: `lang="${lang}".` };
|
|
1410
|
+
return { status: "pass", score: 1, rationale: `lang="${lang}".`, rationale_ko: `lang="${lang}".` };
|
|
1337
1411
|
}
|
|
1338
1412
|
});
|
|
1339
1413
|
|
|
@@ -1348,25 +1422,27 @@ var authorVisibleRule = defineRule({
|
|
|
1348
1422
|
effort: "medium",
|
|
1349
1423
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#citauthor-visible",
|
|
1350
1424
|
title: "Author is declared",
|
|
1425
|
+
title_ko: "\uC791\uC131\uC790 \uC815\uBCF4 \uC120\uC5B8 \uC5EC\uBD80",
|
|
1351
1426
|
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
1427
|
run(ctx) {
|
|
1353
1428
|
for (const node of flattenJsonLd(ctx.jsonLd)) {
|
|
1354
1429
|
if (hasField(node, "author")) {
|
|
1355
|
-
return { status: "pass", score: 1, rationale: "Author found in JSON-LD." };
|
|
1430
|
+
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
1431
|
}
|
|
1357
1432
|
}
|
|
1358
1433
|
const metaAuthor = ctx.$('head meta[name="author"]').attr("content")?.trim();
|
|
1359
|
-
if (metaAuthor) return { status: "pass", score: 1, rationale: `meta[name=author] = "${metaAuthor}".` };
|
|
1434
|
+
if (metaAuthor) return { status: "pass", score: 1, rationale: `meta[name=author] = "${metaAuthor}".`, rationale_ko: `meta[name=author] = "${metaAuthor}".` };
|
|
1360
1435
|
if (ctx.$('[rel="author"]').length > 0) {
|
|
1361
|
-
return { status: "pass", score: 1, rationale: 'rel="author" link found.' };
|
|
1436
|
+
return { status: "pass", score: 1, rationale: 'rel="author" link found.', rationale_ko: 'rel="author" \uB9C1\uD06C\uB97C \uCC3E\uC558\uC2B5\uB2C8\uB2E4.' };
|
|
1362
1437
|
}
|
|
1363
1438
|
if (ctx.$('.author, [class*="author"], [itemprop="author"]').length > 0) {
|
|
1364
|
-
return { status: "pass", score: 0.8, rationale: "Author-ish DOM selector found (weaker signal)." };
|
|
1439
|
+
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
1440
|
}
|
|
1366
1441
|
return {
|
|
1367
1442
|
status: "warn",
|
|
1368
1443
|
score: 0,
|
|
1369
1444
|
rationale: "No author signal found (JSON-LD, meta, rel, or .author).",
|
|
1445
|
+
rationale_ko: "\uC791\uC131\uC790 \uC815\uBCF4\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4 (JSON-LD, meta, rel, .author \uBAA8\uB450 \uC5C6\uC74C).",
|
|
1370
1446
|
fixUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md"
|
|
1371
1447
|
};
|
|
1372
1448
|
}
|
|
@@ -1383,25 +1459,27 @@ var datesRule = defineRule({
|
|
|
1383
1459
|
effort: "low",
|
|
1384
1460
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#citdates",
|
|
1385
1461
|
title: "Publish / modified date is present",
|
|
1462
|
+
title_ko: "\uBC1C\uD589\uC77C / \uC218\uC815\uC77C \uC874\uC7AC \uC5EC\uBD80",
|
|
1386
1463
|
description: "AI engines rank recent pages higher; expose datePublished via JSON-LD, <time datetime>, or article:published_time meta.",
|
|
1387
1464
|
run(ctx) {
|
|
1388
1465
|
for (const node of flattenJsonLd(ctx.jsonLd)) {
|
|
1389
1466
|
if (hasField(node, "datePublished")) {
|
|
1390
|
-
return { status: "pass", score: 1, rationale: "datePublished found in JSON-LD." };
|
|
1467
|
+
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
1468
|
}
|
|
1392
1469
|
}
|
|
1393
1470
|
const articleTime = ctx.$('head meta[property="article:published_time"]').attr("content")?.trim();
|
|
1394
1471
|
if (articleTime) {
|
|
1395
|
-
return { status: "pass", score: 1, rationale: `article:published_time = ${articleTime}.` };
|
|
1472
|
+
return { status: "pass", score: 1, rationale: `article:published_time = ${articleTime}.`, rationale_ko: `article:published_time = ${articleTime}.` };
|
|
1396
1473
|
}
|
|
1397
1474
|
const timeEl = ctx.$("time[datetime]").first().attr("datetime")?.trim();
|
|
1398
1475
|
if (timeEl) {
|
|
1399
|
-
return { status: "pass", score: 0.8, rationale: `<time datetime="${timeEl}"> found.` };
|
|
1476
|
+
return { status: "pass", score: 0.8, rationale: `<time datetime="${timeEl}"> found.`, rationale_ko: `<time datetime="${timeEl}">\uB97C \uCC3E\uC558\uC2B5\uB2C8\uB2E4.` };
|
|
1400
1477
|
}
|
|
1401
1478
|
return {
|
|
1402
1479
|
status: "warn",
|
|
1403
1480
|
score: 0,
|
|
1404
1481
|
rationale: "No publish date found (JSON-LD, meta article:published_time, or <time datetime>).",
|
|
1482
|
+
rationale_ko: "\uBC1C\uD589\uC77C\uC774 \uC5C6\uC2B5\uB2C8\uB2E4 (JSON-LD, meta article:published_time, <time datetime> \uBAA8\uB450 \uC5C6\uC74C).",
|
|
1405
1483
|
fixUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md"
|
|
1406
1484
|
};
|
|
1407
1485
|
}
|
|
@@ -1429,6 +1507,7 @@ var contentFreshnessRule = defineRule({
|
|
|
1429
1507
|
effort: "low",
|
|
1430
1508
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#citcontent-freshness",
|
|
1431
1509
|
title: "Article content is fresh (dateModified within 1 year)",
|
|
1510
|
+
title_ko: "\uCF58\uD150\uCE20 \uCD5C\uC2E0\uC131 (dateModified 1\uB144 \uC774\uB0B4)",
|
|
1432
1511
|
description: "AI engines down-rank stale content. Surface a recent dateModified (\u2264365 days) on Article-like pages so retrieval rankings stay strong.",
|
|
1433
1512
|
run(ctx) {
|
|
1434
1513
|
const nodes = flattenJsonLd(ctx.jsonLd);
|
|
@@ -1437,7 +1516,8 @@ var contentFreshnessRule = defineRule({
|
|
|
1437
1516
|
return {
|
|
1438
1517
|
status: "skip",
|
|
1439
1518
|
score: 0,
|
|
1440
|
-
rationale: "No Article/BlogPosting/NewsArticle JSON-LD; freshness signal not applicable."
|
|
1519
|
+
rationale: "No Article/BlogPosting/NewsArticle JSON-LD; freshness signal not applicable.",
|
|
1520
|
+
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
1521
|
};
|
|
1442
1522
|
}
|
|
1443
1523
|
let bestMs = null;
|
|
@@ -1459,6 +1539,7 @@ var contentFreshnessRule = defineRule({
|
|
|
1459
1539
|
status: "warn",
|
|
1460
1540
|
score: 0,
|
|
1461
1541
|
rationale: "Article has no parseable dateModified or datePublished.",
|
|
1542
|
+
rationale_ko: "Article JSON-LD\uC5D0 \uD30C\uC2F1 \uAC00\uB2A5\uD55C dateModified \uB610\uB294 datePublished\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.",
|
|
1462
1543
|
fixHint: "Add ISO-8601 dateModified and datePublished to your Article JSON-LD.",
|
|
1463
1544
|
estimatedImpact: 3
|
|
1464
1545
|
};
|
|
@@ -1469,6 +1550,7 @@ var contentFreshnessRule = defineRule({
|
|
|
1469
1550
|
status: "pass",
|
|
1470
1551
|
score: 1,
|
|
1471
1552
|
rationale: `${usedField} within the last year (~${ageDays} day${ageDays === 1 ? "" : "s"} ago).`,
|
|
1553
|
+
rationale_ko: `${usedField}\uC774 1\uB144 \uC774\uB0B4\uC785\uB2C8\uB2E4 (\uC57D ${ageDays}\uC77C \uC804).`,
|
|
1472
1554
|
evidence: { ageDays, field: usedField }
|
|
1473
1555
|
};
|
|
1474
1556
|
}
|
|
@@ -1477,6 +1559,7 @@ var contentFreshnessRule = defineRule({
|
|
|
1477
1559
|
status: "warn",
|
|
1478
1560
|
score: 0.6,
|
|
1479
1561
|
rationale: `${usedField} is ${ageDays} days old. Refresh within a year for best AI ranking.`,
|
|
1562
|
+
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
1563
|
evidence: { ageDays, field: usedField },
|
|
1481
1564
|
estimatedImpact: 2
|
|
1482
1565
|
};
|
|
@@ -1485,6 +1568,7 @@ var contentFreshnessRule = defineRule({
|
|
|
1485
1568
|
status: "warn",
|
|
1486
1569
|
score: 0.2,
|
|
1487
1570
|
rationale: `${usedField} is ${ageDays} days old (>2 years). AI engines treat this as stale.`,
|
|
1571
|
+
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
1572
|
evidence: { ageDays, field: usedField },
|
|
1489
1573
|
fixHint: "Update content and bump dateModified to today's date.",
|
|
1490
1574
|
estimatedImpact: 3
|
|
@@ -1516,22 +1600,25 @@ var singleH1Rule = defineRule({
|
|
|
1516
1600
|
effort: "low",
|
|
1517
1601
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#cntsingle-h1",
|
|
1518
1602
|
title: "Exactly one <h1>",
|
|
1603
|
+
title_ko: "<h1> \uD0DC\uADF8 1\uAC1C \uC5EC\uBD80",
|
|
1519
1604
|
description: "A single H1 tells AI engines the primary topic of the page without ambiguity.",
|
|
1520
1605
|
run(ctx) {
|
|
1521
1606
|
const n = ctx.$("h1").length;
|
|
1522
|
-
if (n === 1) return { status: "pass", score: 1, rationale: "Exactly one <h1>." };
|
|
1607
|
+
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
1608
|
if (n === 0) {
|
|
1524
1609
|
return {
|
|
1525
1610
|
status: "fail",
|
|
1526
1611
|
score: 0,
|
|
1527
1612
|
rationale: "No <h1> on the page.",
|
|
1613
|
+
rationale_ko: "\uD398\uC774\uC9C0\uC5D0 <h1>\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.",
|
|
1528
1614
|
fixUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules/cnt.single-h1.md"
|
|
1529
1615
|
};
|
|
1530
1616
|
}
|
|
1531
1617
|
return {
|
|
1532
1618
|
status: "warn",
|
|
1533
1619
|
score: Math.max(0.3, 1 / n),
|
|
1534
|
-
rationale: `Found ${n} <h1> tags; prefer one primary heading
|
|
1620
|
+
rationale: `Found ${n} <h1> tags; prefer one primary heading.`,
|
|
1621
|
+
rationale_ko: `<h1>\uC774 ${n}\uAC1C \uC788\uC2B5\uB2C8\uB2E4. \uB300\uD45C \uC81C\uBAA9 1\uAC1C\uB9CC \uC0AC\uC6A9\uD558\uC138\uC694.`
|
|
1535
1622
|
};
|
|
1536
1623
|
}
|
|
1537
1624
|
});
|
|
@@ -1547,6 +1634,7 @@ var headingHierarchyRule = defineRule({
|
|
|
1547
1634
|
effort: "medium",
|
|
1548
1635
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#cntheading-hierarchy",
|
|
1549
1636
|
title: "Heading levels do not skip",
|
|
1637
|
+
title_ko: "\uC81C\uBAA9 \uB2E8\uACC4 \uC21C\uC11C \uC900\uC218 \uC5EC\uBD80",
|
|
1550
1638
|
description: "Going from H2 directly to H4 breaks the outline AI engines use to segment content.",
|
|
1551
1639
|
run(ctx) {
|
|
1552
1640
|
const levels = [];
|
|
@@ -1556,7 +1644,7 @@ var headingHierarchyRule = defineRule({
|
|
|
1556
1644
|
if (m?.[1]) levels.push(parseInt(m[1], 10));
|
|
1557
1645
|
});
|
|
1558
1646
|
if (levels.length === 0) {
|
|
1559
|
-
return { status: "skip", score: 0, rationale: "No headings found." };
|
|
1647
|
+
return { status: "skip", score: 0, rationale: "No headings found.", rationale_ko: "\uC81C\uBAA9 \uD0DC\uADF8\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4." };
|
|
1560
1648
|
}
|
|
1561
1649
|
const skips = [];
|
|
1562
1650
|
for (let i = 1; i < levels.length; i++) {
|
|
@@ -1565,12 +1653,13 @@ var headingHierarchyRule = defineRule({
|
|
|
1565
1653
|
if (curr > prev + 1) skips.push({ from: prev, to: curr });
|
|
1566
1654
|
}
|
|
1567
1655
|
if (skips.length === 0) {
|
|
1568
|
-
return { status: "pass", score: 1, rationale: "No heading-level skips." };
|
|
1656
|
+
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
1657
|
}
|
|
1570
1658
|
return {
|
|
1571
1659
|
status: "warn",
|
|
1572
1660
|
score: Math.max(0.3, 1 - skips.length / levels.length),
|
|
1573
1661
|
rationale: `${skips.length} heading skip(s) detected (e.g. h${skips[0].from}\u2192h${skips[0].to}).`,
|
|
1662
|
+
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
1663
|
fixUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md"
|
|
1575
1664
|
};
|
|
1576
1665
|
}
|
|
@@ -1587,11 +1676,12 @@ var imageAltRule = defineRule({
|
|
|
1587
1676
|
effort: "medium",
|
|
1588
1677
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#cntimage-alt",
|
|
1589
1678
|
title: "\u226580% of <img> have alt text",
|
|
1679
|
+
title_ko: "<img>\uC758 80% \uC774\uC0C1 alt \uD14D\uC2A4\uD2B8 \uBCF4\uC720 \uC5EC\uBD80",
|
|
1590
1680
|
description: "Alt text gives AI engines a textual anchor for visual content and improves accessibility.",
|
|
1591
1681
|
run(ctx) {
|
|
1592
1682
|
const imgs = ctx.$("img");
|
|
1593
1683
|
const total = imgs.length;
|
|
1594
|
-
if (total === 0) return { status: "skip", score: 0, rationale: "No <img> on the page." };
|
|
1684
|
+
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
1685
|
let withAlt = 0;
|
|
1596
1686
|
imgs.each((_i, el) => {
|
|
1597
1687
|
const alt = ctx.$(el).attr("alt");
|
|
@@ -1599,12 +1689,13 @@ var imageAltRule = defineRule({
|
|
|
1599
1689
|
});
|
|
1600
1690
|
const ratio = withAlt / total;
|
|
1601
1691
|
if (ratio >= 0.8) {
|
|
1602
|
-
return { status: "pass", score: 1, rationale: `${withAlt}/${total} images have alt (${Math.round(ratio * 100)}%).` };
|
|
1692
|
+
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
1693
|
}
|
|
1604
1694
|
return {
|
|
1605
1695
|
status: "warn",
|
|
1606
1696
|
score: ratio,
|
|
1607
1697
|
rationale: `Only ${withAlt}/${total} images have alt text (${Math.round(ratio * 100)}%). Aim for \u226580%.`,
|
|
1698
|
+
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
1699
|
fixUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md"
|
|
1609
1700
|
};
|
|
1610
1701
|
}
|
|
@@ -1621,11 +1712,12 @@ var tldrOrFaqRule = defineRule({
|
|
|
1621
1712
|
effort: "medium",
|
|
1622
1713
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#cnttldr-or-faq",
|
|
1623
1714
|
title: "TL;DR summary or FAQ block",
|
|
1715
|
+
title_ko: "TL;DR \uC694\uC57D \uB610\uB294 FAQ \uBE14\uB85D \uC874\uC7AC \uC5EC\uBD80",
|
|
1624
1716
|
description: 'AI engines strongly prefer content with a quotable summary or FAQ \u2014 it makes the page "citation-ready".',
|
|
1625
1717
|
run(ctx) {
|
|
1626
1718
|
for (const node of flattenJsonLd(ctx.jsonLd)) {
|
|
1627
1719
|
if (getTypes(node).includes("FAQPage")) {
|
|
1628
|
-
return { status: "pass", score: 1, rationale: "FAQPage schema present." };
|
|
1720
|
+
return { status: "pass", score: 1, rationale: "FAQPage schema present.", rationale_ko: "FAQPage \uC2A4\uD0A4\uB9C8\uAC00 \uC788\uC2B5\uB2C8\uB2E4." };
|
|
1629
1721
|
}
|
|
1630
1722
|
}
|
|
1631
1723
|
const sel = [
|
|
@@ -1638,12 +1730,13 @@ var tldrOrFaqRule = defineRule({
|
|
|
1638
1730
|
"[data-tldr]"
|
|
1639
1731
|
].join(", ");
|
|
1640
1732
|
if (ctx.$(sel).length > 0) {
|
|
1641
|
-
return { status: "pass", score: 0.85, rationale: "TL;DR / summary / FAQ region detected by selector." };
|
|
1733
|
+
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
1734
|
}
|
|
1643
1735
|
return {
|
|
1644
1736
|
status: "warn",
|
|
1645
1737
|
score: 0,
|
|
1646
1738
|
rationale: "No TL;DR / summary / FAQ found; add one to boost AI citation odds.",
|
|
1739
|
+
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
1740
|
fixUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md"
|
|
1648
1741
|
};
|
|
1649
1742
|
}
|
|
@@ -1660,6 +1753,7 @@ var wordCountRule = defineRule({
|
|
|
1660
1753
|
effort: "high",
|
|
1661
1754
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#cntword-count",
|
|
1662
1755
|
title: "Page has enough body text",
|
|
1756
|
+
title_ko: "\uCDA9\uBD84\uD55C \uBCF8\uBB38 \uD14D\uC2A4\uD2B8 \uC5EC\uBD80",
|
|
1663
1757
|
description: "Thin pages (under ~100 words) are rarely cited by AI engines. Aim for \u2265300 words of meaningful body copy.",
|
|
1664
1758
|
run(ctx) {
|
|
1665
1759
|
const $ = ctx.$;
|
|
@@ -1667,12 +1761,13 @@ var wordCountRule = defineRule({
|
|
|
1667
1761
|
clone.find("script, style, noscript, nav, header, footer, aside").remove();
|
|
1668
1762
|
const text = clone.text().replace(/\s+/g, " ").trim();
|
|
1669
1763
|
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
|
|
1764
|
+
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.` };
|
|
1765
|
+
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
1766
|
return {
|
|
1673
1767
|
status: "fail",
|
|
1674
1768
|
score: 0,
|
|
1675
1769
|
rationale: `Only ${words} words; too thin to be cited.`,
|
|
1770
|
+
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
1771
|
fixUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md"
|
|
1677
1772
|
};
|
|
1678
1773
|
}
|
|
@@ -1723,11 +1818,12 @@ var qaStructureRule = defineRule({
|
|
|
1723
1818
|
effort: "medium",
|
|
1724
1819
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#cntqa-structure",
|
|
1725
1820
|
title: "Content uses Q&A structure for answer extraction",
|
|
1821
|
+
title_ko: "\uB2F5\uBCC0 \uCD94\uCD9C\uC744 \uC704\uD55C Q&A \uAD6C\uC870 \uC0AC\uC6A9 \uC5EC\uBD80",
|
|
1726
1822
|
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
1823
|
run(ctx) {
|
|
1728
1824
|
for (const node of flattenJsonLd(ctx.jsonLd)) {
|
|
1729
1825
|
if (getTypes(node).includes("FAQPage")) {
|
|
1730
|
-
return { status: "pass", score: 1, rationale: "FAQPage JSON-LD provides explicit Q&A." };
|
|
1826
|
+
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
1827
|
}
|
|
1732
1828
|
}
|
|
1733
1829
|
const questionHeadings = [];
|
|
@@ -1740,6 +1836,7 @@ var qaStructureRule = defineRule({
|
|
|
1740
1836
|
status: "pass",
|
|
1741
1837
|
score: 1,
|
|
1742
1838
|
rationale: `${questionHeadings.length} question-style headings detected.`,
|
|
1839
|
+
rationale_ko: `\uC9C8\uBB38\uD615 \uC81C\uBAA9\uC774 ${questionHeadings.length}\uAC1C \uAC10\uC9C0\uB429\uB2C8\uB2E4.`,
|
|
1743
1840
|
evidence: { headings: questionHeadings.slice(0, 5) }
|
|
1744
1841
|
};
|
|
1745
1842
|
}
|
|
@@ -1748,6 +1845,7 @@ var qaStructureRule = defineRule({
|
|
|
1748
1845
|
status: "warn",
|
|
1749
1846
|
score: 0.6,
|
|
1750
1847
|
rationale: "1 question-style heading. Add a second to strengthen answer extraction.",
|
|
1848
|
+
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
1849
|
evidence: { headings: questionHeadings },
|
|
1752
1850
|
estimatedImpact: 1
|
|
1753
1851
|
};
|
|
@@ -1756,6 +1854,7 @@ var qaStructureRule = defineRule({
|
|
|
1756
1854
|
status: "warn",
|
|
1757
1855
|
score: 0,
|
|
1758
1856
|
rationale: "No question-style H2/H3 headings or FAQPage JSON-LD found.",
|
|
1857
|
+
rationale_ko: "\uC9C8\uBB38\uD615 H2/H3 \uC81C\uBAA9\uC774\uB098 FAQPage JSON-LD\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.",
|
|
1759
1858
|
fixHint: 'Reframe at least 2 H2 headings as questions ("How do I\u2026?", "What is\u2026?") or add FAQPage JSON-LD.',
|
|
1760
1859
|
estimatedImpact: 3
|
|
1761
1860
|
};
|
|
@@ -1773,13 +1872,14 @@ var externalCitationsRule = defineRule({
|
|
|
1773
1872
|
effort: "medium",
|
|
1774
1873
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#cntexternal-citations",
|
|
1775
1874
|
title: "Content cites external sources",
|
|
1875
|
+
title_ko: "\uC678\uBD80 \uCD9C\uCC98 \uC778\uC6A9 \uC5EC\uBD80",
|
|
1776
1876
|
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
1877
|
run(ctx) {
|
|
1778
1878
|
let host;
|
|
1779
1879
|
try {
|
|
1780
1880
|
host = new URL(ctx.finalUrl).hostname.toLowerCase();
|
|
1781
1881
|
} catch {
|
|
1782
|
-
return { status: "skip", score: 0, rationale: "Invalid finalUrl." };
|
|
1882
|
+
return { status: "skip", score: 0, rationale: "Invalid finalUrl.", rationale_ko: "\uC720\uD6A8\uD558\uC9C0 \uC54A\uC740 finalUrl\uC785\uB2C8\uB2E4." };
|
|
1783
1883
|
}
|
|
1784
1884
|
const seen = /* @__PURE__ */ new Set();
|
|
1785
1885
|
ctx.$("main a[href], article a[href], body a[href]").each((_i, el) => {
|
|
@@ -1804,6 +1904,7 @@ var externalCitationsRule = defineRule({
|
|
|
1804
1904
|
status: "pass",
|
|
1805
1905
|
score: 1,
|
|
1806
1906
|
rationale: `${count} distinct external host(s) cited (excluding nofollow).`,
|
|
1907
|
+
rationale_ko: `\uC678\uBD80 \uC0AC\uC774\uD2B8 ${count}\uAC1C\uB97C \uC778\uC6A9\uD569\uB2C8\uB2E4 (nofollow \uC81C\uC678).`,
|
|
1807
1908
|
evidence: { hosts: [...seen].slice(0, 8) }
|
|
1808
1909
|
};
|
|
1809
1910
|
}
|
|
@@ -1812,6 +1913,7 @@ var externalCitationsRule = defineRule({
|
|
|
1812
1913
|
status: "pass",
|
|
1813
1914
|
score: 0.7,
|
|
1814
1915
|
rationale: `${count} external host(s) cited. Aim for \u22653 for stronger E-E-A-T.`,
|
|
1916
|
+
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
1917
|
evidence: { hosts: [...seen] },
|
|
1816
1918
|
estimatedImpact: 1
|
|
1817
1919
|
};
|
|
@@ -1820,6 +1922,7 @@ var externalCitationsRule = defineRule({
|
|
|
1820
1922
|
status: "warn",
|
|
1821
1923
|
score: 0,
|
|
1822
1924
|
rationale: "No external follow citations found in main content.",
|
|
1925
|
+
rationale_ko: "\uBCF8\uBB38\uC5D0 \uC678\uBD80 \uCD9C\uCC98 \uB9C1\uD06C\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.",
|
|
1823
1926
|
fixHint: "Cite at least one authoritative external source (research paper, official docs, news outlet).",
|
|
1824
1927
|
estimatedImpact: 2
|
|
1825
1928
|
};
|
|
@@ -1837,12 +1940,130 @@ var contentRules = [
|
|
|
1837
1940
|
externalCitationsRule
|
|
1838
1941
|
];
|
|
1839
1942
|
|
|
1943
|
+
// src/rules/aeo/skill-md.ts
|
|
1944
|
+
var aeoSkillMdRule = defineRule({
|
|
1945
|
+
id: "aeo.skill-md",
|
|
1946
|
+
stableId: "aeo.skill-md",
|
|
1947
|
+
category: "aeo",
|
|
1948
|
+
group: "opportunity",
|
|
1949
|
+
weight: 3,
|
|
1950
|
+
impact: "high",
|
|
1951
|
+
effort: "low",
|
|
1952
|
+
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#aeoskill-md",
|
|
1953
|
+
title: "skill.md is present",
|
|
1954
|
+
title_ko: "skill.md \uD30C\uC77C \uC874\uC7AC \uC5EC\uBD80",
|
|
1955
|
+
description: "A /skill.md file describes site capabilities so AI agents know what this site can do for them.",
|
|
1956
|
+
run(ctx) {
|
|
1957
|
+
if (ctx.skillMd !== null) {
|
|
1958
|
+
return {
|
|
1959
|
+
status: "pass",
|
|
1960
|
+
score: 1,
|
|
1961
|
+
rationale: "skill.md found at site root.",
|
|
1962
|
+
rationale_ko: "skill.md\uAC00 \uC0AC\uC774\uD2B8 \uB8E8\uD2B8\uC5D0 \uC874\uC7AC\uD569\uB2C8\uB2E4."
|
|
1963
|
+
};
|
|
1964
|
+
}
|
|
1965
|
+
return {
|
|
1966
|
+
status: "warn",
|
|
1967
|
+
score: 0,
|
|
1968
|
+
rationale: "No /skill.md found. Add one to describe your site capabilities to AI agents.",
|
|
1969
|
+
rationale_ko: "/skill.md\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4. AI \uC5D0\uC774\uC804\uD2B8\uAC00 \uC0AC\uC774\uD2B8 \uAE30\uB2A5\uC744 \uD30C\uC545\uD560 \uC218 \uC788\uB3C4\uB85D \uCD94\uAC00\uD558\uC138\uC694.",
|
|
1970
|
+
fixHint: "Create /skill.md listing what services, products, and capabilities this site offers."
|
|
1971
|
+
};
|
|
1972
|
+
}
|
|
1973
|
+
});
|
|
1974
|
+
|
|
1975
|
+
// src/rules/aeo/agent-permissions.ts
|
|
1976
|
+
var aeoAgentPermissionsRule = defineRule({
|
|
1977
|
+
id: "aeo.agent-permissions",
|
|
1978
|
+
stableId: "aeo.agent-permissions",
|
|
1979
|
+
category: "aeo",
|
|
1980
|
+
group: "opportunity",
|
|
1981
|
+
weight: 3,
|
|
1982
|
+
impact: "medium",
|
|
1983
|
+
effort: "low",
|
|
1984
|
+
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#aeoagent-permissions",
|
|
1985
|
+
title: "agent-permissions.json is present",
|
|
1986
|
+
title_ko: "agent-permissions.json \uD30C\uC77C \uC874\uC7AC \uC5EC\uBD80",
|
|
1987
|
+
description: "Declares explicit read/summarize/cite/train permissions for AI agents.",
|
|
1988
|
+
run(ctx) {
|
|
1989
|
+
if (ctx.agentPermissions !== null) {
|
|
1990
|
+
return {
|
|
1991
|
+
status: "pass",
|
|
1992
|
+
score: 1,
|
|
1993
|
+
rationale: "agent-permissions.json found at site root.",
|
|
1994
|
+
rationale_ko: "agent-permissions.json\uC774 \uC0AC\uC774\uD2B8 \uB8E8\uD2B8\uC5D0 \uC874\uC7AC\uD569\uB2C8\uB2E4.",
|
|
1995
|
+
evidence: ctx.agentPermissions
|
|
1996
|
+
};
|
|
1997
|
+
}
|
|
1998
|
+
return {
|
|
1999
|
+
status: "warn",
|
|
2000
|
+
score: 0,
|
|
2001
|
+
rationale: "No /agent-permissions.json found. Add one to declare AI agent access policy.",
|
|
2002
|
+
rationale_ko: "/agent-permissions.json\uC774 \uC5C6\uC2B5\uB2C8\uB2E4. AI \uC5D0\uC774\uC804\uD2B8 \uC811\uADFC \uC815\uCC45\uC744 \uBA85\uC2DC\uD558\uB824\uBA74 \uCD94\uAC00\uD558\uC138\uC694.",
|
|
2003
|
+
fixHint: "Create /agent-permissions.json with read, summarize, cite, and train permission flags."
|
|
2004
|
+
};
|
|
2005
|
+
}
|
|
2006
|
+
});
|
|
2007
|
+
|
|
2008
|
+
// src/rules/aeo/token-length.ts
|
|
2009
|
+
var THRESHOLD_OPTIMAL = 15e3;
|
|
2010
|
+
var THRESHOLD_MAX = 25e3;
|
|
2011
|
+
var aeoTokenLengthRule = defineRule({
|
|
2012
|
+
id: "aeo.token-length",
|
|
2013
|
+
stableId: "aeo.token-length",
|
|
2014
|
+
category: "aeo",
|
|
2015
|
+
group: "diagnostic",
|
|
2016
|
+
weight: 4,
|
|
2017
|
+
impact: "medium",
|
|
2018
|
+
effort: "medium",
|
|
2019
|
+
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#aeotoken-length",
|
|
2020
|
+
title: "Content token length within AI agent limits",
|
|
2021
|
+
title_ko: "\uCF58\uD150\uCE20 \uD1A0\uD070 \uC218 AI \uC5D0\uC774\uC804\uD2B8 \uAD8C\uC7A5 \uBC94\uC704",
|
|
2022
|
+
description: "Pages under 15K tokens are optimal for AI agents (per Addy Osmani's AEO guidance).",
|
|
2023
|
+
run(ctx) {
|
|
2024
|
+
const text = ctx.$("body").text();
|
|
2025
|
+
const tokenEstimate = Math.round(text.length / 3);
|
|
2026
|
+
const evidence = { tokenEstimate, thresholds: { optimal: THRESHOLD_OPTIMAL, max: THRESHOLD_MAX } };
|
|
2027
|
+
if (tokenEstimate <= THRESHOLD_OPTIMAL) {
|
|
2028
|
+
return {
|
|
2029
|
+
status: "pass",
|
|
2030
|
+
score: 1,
|
|
2031
|
+
rationale: `Estimated ~${tokenEstimate.toLocaleString()} tokens \u2014 within optimal range.`,
|
|
2032
|
+
rationale_ko: `\uC608\uC0C1 \uD1A0\uD070 \uC218 ~${tokenEstimate.toLocaleString()} \u2014 \uAD8C\uC7A5 \uBC94\uC704(15K) \uC774\uB0B4\uC785\uB2C8\uB2E4.`,
|
|
2033
|
+
evidence
|
|
2034
|
+
};
|
|
2035
|
+
}
|
|
2036
|
+
if (tokenEstimate <= THRESHOLD_MAX) {
|
|
2037
|
+
return {
|
|
2038
|
+
status: "warn",
|
|
2039
|
+
score: 0.5,
|
|
2040
|
+
rationale: `Estimated ~${tokenEstimate.toLocaleString()} tokens \u2014 exceeds 15K recommendation.`,
|
|
2041
|
+
rationale_ko: `\uC608\uC0C1 \uD1A0\uD070 \uC218 ~${tokenEstimate.toLocaleString()} \u2014 15K \uAD8C\uC7A5\uCE58\uB97C \uCD08\uACFC\uD569\uB2C8\uB2E4.`,
|
|
2042
|
+
fixHint: "Consider splitting content into shorter, focused pages.",
|
|
2043
|
+
evidence
|
|
2044
|
+
};
|
|
2045
|
+
}
|
|
2046
|
+
return {
|
|
2047
|
+
status: "fail",
|
|
2048
|
+
score: 0,
|
|
2049
|
+
rationale: `Estimated ~${tokenEstimate.toLocaleString()} tokens \u2014 exceeds 25K agent processing limit.`,
|
|
2050
|
+
rationale_ko: `\uC608\uC0C1 \uD1A0\uD070 \uC218 ~${tokenEstimate.toLocaleString()} \u2014 AI \uC5D0\uC774\uC804\uD2B8 \uCC98\uB9AC \uD55C\uACC4(25K)\uB97C \uCD08\uACFC\uD569\uB2C8\uB2E4.`,
|
|
2051
|
+
fixHint: "Split this page into multiple focused pages under 15K tokens.",
|
|
2052
|
+
evidence
|
|
2053
|
+
};
|
|
2054
|
+
}
|
|
2055
|
+
});
|
|
2056
|
+
|
|
2057
|
+
// src/rules/aeo/index.ts
|
|
2058
|
+
var aeoRules = [aeoSkillMdRule, aeoAgentPermissionsRule, aeoTokenLengthRule];
|
|
2059
|
+
|
|
1840
2060
|
// src/rules/index.ts
|
|
1841
2061
|
var defaultRules = [
|
|
1842
2062
|
...crawlerRules,
|
|
1843
2063
|
...structuredDataRules,
|
|
1844
2064
|
...citationRules,
|
|
1845
|
-
...contentRules
|
|
2065
|
+
...contentRules,
|
|
2066
|
+
...aeoRules
|
|
1846
2067
|
];
|
|
1847
2068
|
|
|
1848
2069
|
// src/config.ts
|
|
@@ -2038,7 +2259,8 @@ var CATEGORY_LABELS = {
|
|
|
2038
2259
|
crawler: "AI Crawler Access",
|
|
2039
2260
|
"structured-data": "Structured Data",
|
|
2040
2261
|
citation: "Citation Signals",
|
|
2041
|
-
content: "Content Structure"
|
|
2262
|
+
content: "Content Structure",
|
|
2263
|
+
aeo: "AEO Stack"
|
|
2042
2264
|
};
|
|
2043
2265
|
function scoreBadge(score) {
|
|
2044
2266
|
const color = score >= 85 ? "brightgreen" : score >= 60 ? "yellow" : "red";
|
|
@@ -2333,7 +2555,8 @@ var CATEGORY_LABELS2 = {
|
|
|
2333
2555
|
crawler: "AI Crawler Access",
|
|
2334
2556
|
"structured-data": "Structured Data",
|
|
2335
2557
|
citation: "Citation Signals",
|
|
2336
|
-
content: "Content Structure"
|
|
2558
|
+
content: "Content Structure",
|
|
2559
|
+
aeo: "AEO Stack"
|
|
2337
2560
|
};
|
|
2338
2561
|
function colorScore(score) {
|
|
2339
2562
|
if (score >= 85) return import_kleur.default.green().bold(`${score}`);
|
|
@@ -2433,7 +2656,8 @@ var CATEGORY_LABELS3 = {
|
|
|
2433
2656
|
crawler: "AI Crawler Access",
|
|
2434
2657
|
"structured-data": "Structured Data",
|
|
2435
2658
|
citation: "Citation Signals",
|
|
2436
|
-
content: "Content Structure"
|
|
2659
|
+
content: "Content Structure",
|
|
2660
|
+
aeo: "AEO Stack"
|
|
2437
2661
|
};
|
|
2438
2662
|
var IMPACT_ORDER = {
|
|
2439
2663
|
critical: 4,
|
|
@@ -2516,13 +2740,15 @@ function partitionResults(report) {
|
|
|
2516
2740
|
crawler: [],
|
|
2517
2741
|
"structured-data": [],
|
|
2518
2742
|
citation: [],
|
|
2519
|
-
content: []
|
|
2743
|
+
content: [],
|
|
2744
|
+
aeo: []
|
|
2520
2745
|
};
|
|
2521
2746
|
const passed = {
|
|
2522
2747
|
crawler: [],
|
|
2523
2748
|
"structured-data": [],
|
|
2524
2749
|
citation: [],
|
|
2525
|
-
content: []
|
|
2750
|
+
content: [],
|
|
2751
|
+
aeo: []
|
|
2526
2752
|
};
|
|
2527
2753
|
for (const cat of Object.keys(report.categories)) {
|
|
2528
2754
|
for (const r of report.categories[cat].results) {
|