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.js
CHANGED
|
@@ -263,10 +263,12 @@ async function buildContext(url, opts = {}) {
|
|
|
263
263
|
"Site appears to be JS-rendered (sparse body + SPA root element). Re-run with --render for accurate results."
|
|
264
264
|
);
|
|
265
265
|
}
|
|
266
|
-
const [robotsRaw, llmsRaw, llmsFullRaw] = await Promise.all([
|
|
266
|
+
const [robotsRaw, llmsRaw, llmsFullRaw, skillMdRaw, agentPermissionsRaw] = await Promise.all([
|
|
267
267
|
fetchText(`${origin}/robots.txt`, opts),
|
|
268
268
|
fetchText(`${origin}/llms.txt`, opts),
|
|
269
|
-
fetchText(`${origin}/llms-full.txt`, opts)
|
|
269
|
+
fetchText(`${origin}/llms-full.txt`, opts),
|
|
270
|
+
fetchText(`${origin}/skill.md`, opts),
|
|
271
|
+
fetchText(`${origin}/agent-permissions.json`, opts)
|
|
270
272
|
]);
|
|
271
273
|
let sitemapUrl = null;
|
|
272
274
|
const robots = robotsRaw ? parseRobots(robotsRaw) : null;
|
|
@@ -274,6 +276,13 @@ async function buildContext(url, opts = {}) {
|
|
|
274
276
|
if (!sitemapUrl) sitemapUrl = `${origin}/sitemap.xml`;
|
|
275
277
|
const sitemapRaw = await fetchText(sitemapUrl, opts);
|
|
276
278
|
const sitemap = sitemapRaw ? parseSitemap(sitemapRaw) : null;
|
|
279
|
+
let agentPermissions = null;
|
|
280
|
+
if (agentPermissionsRaw && agentPermissionsRaw.trim().length > 0) {
|
|
281
|
+
try {
|
|
282
|
+
agentPermissions = JSON.parse(agentPermissionsRaw);
|
|
283
|
+
} catch {
|
|
284
|
+
}
|
|
285
|
+
}
|
|
277
286
|
return {
|
|
278
287
|
url,
|
|
279
288
|
finalUrl,
|
|
@@ -288,7 +297,9 @@ async function buildContext(url, opts = {}) {
|
|
|
288
297
|
jsonLd: extractJsonLd($),
|
|
289
298
|
renderMode,
|
|
290
299
|
fetchedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
291
|
-
warnings
|
|
300
|
+
warnings,
|
|
301
|
+
skillMd: skillMdRaw && skillMdRaw.trim().length > 0 ? skillMdRaw : null,
|
|
302
|
+
agentPermissions
|
|
292
303
|
};
|
|
293
304
|
}
|
|
294
305
|
|
|
@@ -297,10 +308,11 @@ function defineRule(rule) {
|
|
|
297
308
|
return rule;
|
|
298
309
|
}
|
|
299
310
|
var CATEGORY_WEIGHTS = {
|
|
300
|
-
crawler:
|
|
301
|
-
"structured-data":
|
|
302
|
-
citation:
|
|
303
|
-
content:
|
|
311
|
+
crawler: 20,
|
|
312
|
+
"structured-data": 25,
|
|
313
|
+
citation: 20,
|
|
314
|
+
content: 15,
|
|
315
|
+
aeo: 20
|
|
304
316
|
};
|
|
305
317
|
|
|
306
318
|
// src/engine.ts
|
|
@@ -314,7 +326,8 @@ async function runRules(ctx, rules, opts = {}) {
|
|
|
314
326
|
crawler: { score: 0, weight: weights.crawler, results: [] },
|
|
315
327
|
"structured-data": { score: 0, weight: weights["structured-data"], results: [] },
|
|
316
328
|
citation: { score: 0, weight: weights.citation, results: [] },
|
|
317
|
-
content: { score: 0, weight: weights.content, results: [] }
|
|
329
|
+
content: { score: 0, weight: weights.content, results: [] },
|
|
330
|
+
aeo: { score: 0, weight: weights.aeo, results: [] }
|
|
318
331
|
};
|
|
319
332
|
for (const rule of rules) {
|
|
320
333
|
if (onlySet && !onlySet.has(rule.id) && (!rule.stableId || !onlySet.has(rule.stableId))) continue;
|
|
@@ -339,6 +352,7 @@ async function runRules(ctx, rules, opts = {}) {
|
|
|
339
352
|
durationMs
|
|
340
353
|
};
|
|
341
354
|
if (rule.stableId !== void 0) entry.stableId = rule.stableId;
|
|
355
|
+
if (rule.title_ko !== void 0) entry.title_ko = rule.title_ko;
|
|
342
356
|
if (rule.group !== void 0) entry.group = rule.group;
|
|
343
357
|
if (rule.impact !== void 0) entry.impact = rule.impact;
|
|
344
358
|
if (rule.effort !== void 0) entry.effort = rule.effort;
|
|
@@ -401,13 +415,15 @@ var httpsRule = defineRule({
|
|
|
401
415
|
effort: "medium",
|
|
402
416
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#crawlerhttps",
|
|
403
417
|
title: "Site is served over HTTPS",
|
|
418
|
+
title_ko: "\uC0AC\uC774\uD2B8\uAC00 HTTPS\uB85C \uC81C\uACF5\uB428",
|
|
404
419
|
description: "AI crawlers treat HTTPS pages as more trustworthy and some skip plain HTTP entirely.",
|
|
405
420
|
run(ctx) {
|
|
406
421
|
const isHttps = ctx.finalUrl.startsWith("https://");
|
|
407
|
-
return isHttps ? { status: "pass", score: 1, rationale: "Final URL uses HTTPS." } : {
|
|
422
|
+
return isHttps ? { status: "pass", score: 1, rationale: "Final URL uses HTTPS.", rationale_ko: "\uCD5C\uC885 URL\uC774 HTTPS\uB97C \uC0AC\uC6A9\uD569\uB2C8\uB2E4." } : {
|
|
408
423
|
status: "fail",
|
|
409
424
|
score: 0,
|
|
410
425
|
rationale: "Final URL does not use HTTPS. Redirect HTTP \u2192 HTTPS site-wide.",
|
|
426
|
+
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.",
|
|
411
427
|
fixUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md"
|
|
412
428
|
};
|
|
413
429
|
}
|
|
@@ -424,15 +440,17 @@ var robotsReachableRule = defineRule({
|
|
|
424
440
|
effort: "low",
|
|
425
441
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#crawlerrobots-reachable",
|
|
426
442
|
title: "robots.txt is reachable",
|
|
443
|
+
title_ko: "robots.txt \uC811\uADFC \uAC00\uB2A5 \uC5EC\uBD80",
|
|
427
444
|
description: "A reachable robots.txt lets crawlers confirm their permissions; missing file is treated as allow-all but blocks explicit signalling.",
|
|
428
445
|
run(ctx) {
|
|
429
446
|
if (ctx.robots) {
|
|
430
|
-
return { status: "pass", score: 1, rationale: "robots.txt returned successfully." };
|
|
447
|
+
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." };
|
|
431
448
|
}
|
|
432
449
|
return {
|
|
433
450
|
status: "warn",
|
|
434
451
|
score: 0.3,
|
|
435
452
|
rationale: "robots.txt is missing. Add one even if empty to explicitly signal crawl policy.",
|
|
453
|
+
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.",
|
|
436
454
|
fixUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md"
|
|
437
455
|
};
|
|
438
456
|
}
|
|
@@ -480,13 +498,15 @@ var robotsAiAllowRule = defineRule({
|
|
|
480
498
|
effort: "low",
|
|
481
499
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#crawlerrobots-ai-allow",
|
|
482
500
|
title: "AI crawlers are allowed",
|
|
501
|
+
title_ko: "AI \uD06C\uB864\uB7EC \uC811\uADFC \uD5C8\uC6A9 \uC5EC\uBD80",
|
|
483
502
|
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.",
|
|
484
503
|
run(ctx) {
|
|
485
504
|
if (!ctx.robots) {
|
|
486
505
|
return {
|
|
487
506
|
status: "warn",
|
|
488
507
|
score: 0.5,
|
|
489
|
-
rationale: "robots.txt missing; AI crawlers default to allow, but explicit allow is recommended."
|
|
508
|
+
rationale: "robots.txt missing; AI crawlers default to allow, but explicit allow is recommended.",
|
|
509
|
+
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."
|
|
490
510
|
};
|
|
491
511
|
}
|
|
492
512
|
const path = new URL(ctx.finalUrl).pathname || "/";
|
|
@@ -504,6 +524,7 @@ var robotsAiAllowRule = defineRule({
|
|
|
504
524
|
status: "fail",
|
|
505
525
|
score: Math.max(0, 1 - blocked.length / AI_BOTS.length),
|
|
506
526
|
rationale: `Blocked: ${blocked.join(", ")}. Remove the Disallow or add an explicit Allow for these user-agents.`,
|
|
527
|
+
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.`,
|
|
507
528
|
evidence: { blocked, mentioned, totalBots: AI_BOTS.length },
|
|
508
529
|
fixUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md"
|
|
509
530
|
};
|
|
@@ -512,13 +533,15 @@ var robotsAiAllowRule = defineRule({
|
|
|
512
533
|
return {
|
|
513
534
|
status: "warn",
|
|
514
535
|
score: 0.6,
|
|
515
|
-
rationale: `All ${AI_BOTS.length} AI crawlers reach the page via default rules, but none are explicitly listed. Consider explicit Allow entries
|
|
536
|
+
rationale: `All ${AI_BOTS.length} AI crawlers reach the page via default rules, but none are explicitly listed. Consider explicit Allow entries.`,
|
|
537
|
+
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.`
|
|
516
538
|
};
|
|
517
539
|
}
|
|
518
540
|
return {
|
|
519
541
|
status: "pass",
|
|
520
542
|
score: 1,
|
|
521
543
|
rationale: `All ${AI_BOTS.length} AI crawlers can reach the page; ${mentioned.length} explicitly listed.`,
|
|
544
|
+
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.`,
|
|
522
545
|
evidence: { mentioned, totalBots: AI_BOTS.length }
|
|
523
546
|
};
|
|
524
547
|
}
|
|
@@ -535,15 +558,17 @@ var llmsTxtPresentRule = defineRule({
|
|
|
535
558
|
effort: "medium",
|
|
536
559
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#crawlerllms-txt-present",
|
|
537
560
|
title: "llms.txt is present",
|
|
561
|
+
title_ko: "llms.txt \uD30C\uC77C \uC874\uC7AC \uC5EC\uBD80",
|
|
538
562
|
description: "An /llms.txt file at the site root gives AI assistants a curated map of the most citation-worthy pages.",
|
|
539
563
|
run(ctx) {
|
|
540
564
|
if (ctx.llmsTxt) {
|
|
541
|
-
return { status: "pass", score: 1, rationale: "llms.txt found at site root." };
|
|
565
|
+
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." };
|
|
542
566
|
}
|
|
543
567
|
return {
|
|
544
568
|
status: "warn",
|
|
545
569
|
score: 0,
|
|
546
570
|
rationale: "No /llms.txt found. Add one to curate the pages AI assistants should read.",
|
|
571
|
+
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.",
|
|
547
572
|
fixUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md"
|
|
548
573
|
};
|
|
549
574
|
}
|
|
@@ -560,10 +585,11 @@ var llmsTxtWellformedRule = defineRule({
|
|
|
560
585
|
effort: "low",
|
|
561
586
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#crawlerllms-txt-wellformed",
|
|
562
587
|
title: "llms.txt follows the spec",
|
|
588
|
+
title_ko: "llms.txt \uC2A4\uD399 \uC900\uC218 \uC5EC\uBD80",
|
|
563
589
|
description: "Must start with an H1 project title, then a brief summary, then at least one H2 section containing link items.",
|
|
564
590
|
run(ctx) {
|
|
565
591
|
if (!ctx.llmsTxt) {
|
|
566
|
-
return { status: "skip", score: 0, rationale: "No llms.txt to validate." };
|
|
592
|
+
return { status: "skip", score: 0, rationale: "No llms.txt to validate.", rationale_ko: "\uAC80\uC99D\uD560 llms.txt\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4." };
|
|
567
593
|
}
|
|
568
594
|
const check = isLlmsTxtWellFormed(ctx.llmsTxt);
|
|
569
595
|
if (check.ok) {
|
|
@@ -571,13 +597,15 @@ var llmsTxtWellformedRule = defineRule({
|
|
|
571
597
|
return {
|
|
572
598
|
status: "pass",
|
|
573
599
|
score: 1,
|
|
574
|
-
rationale: `Well-formed with ${ctx.llmsTxt.sections.length} section(s) and ${totalLinks} link(s)
|
|
600
|
+
rationale: `Well-formed with ${ctx.llmsTxt.sections.length} section(s) and ${totalLinks} link(s).`,
|
|
601
|
+
rationale_ko: `\uC2A4\uD399\uC5D0 \uB9DE\uAC8C \uC791\uC131\uB428 (\uC139\uC158 ${ctx.llmsTxt.sections.length}\uAC1C, \uB9C1\uD06C ${totalLinks}\uAC1C).`
|
|
575
602
|
};
|
|
576
603
|
}
|
|
577
604
|
return {
|
|
578
605
|
status: "warn",
|
|
579
606
|
score: 0.3,
|
|
580
607
|
rationale: `llms.txt does not fully match the spec: ${check.reason}.`,
|
|
608
|
+
rationale_ko: `llms.txt\uAC00 \uC2A4\uD399\uC744 \uC644\uC804\uD788 \uB530\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4: ${check.reason}.`,
|
|
581
609
|
fixUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md"
|
|
582
610
|
};
|
|
583
611
|
}
|
|
@@ -594,13 +622,15 @@ var llmsFullTxtRule = defineRule({
|
|
|
594
622
|
effort: "medium",
|
|
595
623
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#crawlerllms-full-txt",
|
|
596
624
|
title: "llms-full.txt provides full-content mirror",
|
|
625
|
+
title_ko: "llms-full.txt \uC804\uCCB4 \uCF58\uD150\uCE20 \uBBF8\uB7EC \uC81C\uACF5 \uC5EC\uBD80",
|
|
597
626
|
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.",
|
|
598
627
|
run(ctx) {
|
|
599
628
|
if (ctx.llmsFullTxt && ctx.llmsFullTxt.length > 200) {
|
|
600
629
|
return {
|
|
601
630
|
status: "pass",
|
|
602
631
|
score: 1,
|
|
603
|
-
rationale: `/llms-full.txt found (${ctx.llmsFullTxt.length.toLocaleString()} chars)
|
|
632
|
+
rationale: `/llms-full.txt found (${ctx.llmsFullTxt.length.toLocaleString()} chars).`,
|
|
633
|
+
rationale_ko: `/llms-full.txt\uAC00 \uC874\uC7AC\uD569\uB2C8\uB2E4 (${ctx.llmsFullTxt.length.toLocaleString()}\uC790).`
|
|
604
634
|
};
|
|
605
635
|
}
|
|
606
636
|
if (ctx.llmsFullTxt) {
|
|
@@ -608,6 +638,7 @@ var llmsFullTxtRule = defineRule({
|
|
|
608
638
|
status: "warn",
|
|
609
639
|
score: 0.5,
|
|
610
640
|
rationale: `/llms-full.txt found but very short (${ctx.llmsFullTxt.length} chars). Consider expanding with page bodies.`,
|
|
641
|
+
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.`,
|
|
611
642
|
fixHint: "Mirror full article bodies into /llms-full.txt so AI assistants can quote without re-crawling."
|
|
612
643
|
};
|
|
613
644
|
}
|
|
@@ -615,6 +646,7 @@ var llmsFullTxtRule = defineRule({
|
|
|
615
646
|
status: "warn",
|
|
616
647
|
score: 0,
|
|
617
648
|
rationale: "No /llms-full.txt found. Adding one lets AI assistants ingest the full corpus in a single request.",
|
|
649
|
+
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.",
|
|
618
650
|
fixHint: "Publish /llms-full.txt alongside /llms.txt with the full body text of your top pages.",
|
|
619
651
|
estimatedImpact: 1
|
|
620
652
|
};
|
|
@@ -632,19 +664,22 @@ var sitemapPresentRule = defineRule({
|
|
|
632
664
|
effort: "low",
|
|
633
665
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#crawlersitemap-present",
|
|
634
666
|
title: "sitemap.xml is present",
|
|
667
|
+
title_ko: "sitemap.xml \uC874\uC7AC \uC5EC\uBD80",
|
|
635
668
|
description: "A sitemap helps AI crawlers discover and prioritise pages; many crawlers short-circuit discovery without one.",
|
|
636
669
|
run(ctx) {
|
|
637
670
|
if (ctx.sitemap && ctx.sitemap.urls.length > 0) {
|
|
638
671
|
return {
|
|
639
672
|
status: "pass",
|
|
640
673
|
score: 1,
|
|
641
|
-
rationale: `Sitemap found with ${ctx.sitemap.urls.length} URL(s)
|
|
674
|
+
rationale: `Sitemap found with ${ctx.sitemap.urls.length} URL(s).`,
|
|
675
|
+
rationale_ko: `\uC0AC\uC774\uD2B8\uB9F5\uC5D0 URL\uC774 ${ctx.sitemap.urls.length}\uAC1C \uC788\uC2B5\uB2C8\uB2E4.`
|
|
642
676
|
};
|
|
643
677
|
}
|
|
644
678
|
return {
|
|
645
679
|
status: "warn",
|
|
646
680
|
score: 0.2,
|
|
647
681
|
rationale: "No sitemap.xml found (checked /sitemap.xml and Sitemap: directive in robots.txt).",
|
|
682
|
+
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).",
|
|
648
683
|
fixUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md"
|
|
649
684
|
};
|
|
650
685
|
}
|
|
@@ -672,15 +707,17 @@ var jsonLdPresentRule = defineRule({
|
|
|
672
707
|
effort: "medium",
|
|
673
708
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#sdjsonld-present",
|
|
674
709
|
title: "JSON-LD structured data is present",
|
|
710
|
+
title_ko: "JSON-LD \uAD6C\uC870\uD654 \uB370\uC774\uD130 \uC874\uC7AC \uC5EC\uBD80",
|
|
675
711
|
description: 'At least one <script type="application/ld+json"> block is the primary way AI engines map your page to an entity.',
|
|
676
712
|
run(ctx) {
|
|
677
713
|
if (ctx.jsonLd.length > 0) {
|
|
678
|
-
return { status: "pass", score: 1, rationale: `Found ${ctx.jsonLd.length} JSON-LD block(s).` };
|
|
714
|
+
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.` };
|
|
679
715
|
}
|
|
680
716
|
return {
|
|
681
717
|
status: "fail",
|
|
682
718
|
score: 0,
|
|
683
719
|
rationale: "No JSON-LD blocks found. Add schema.org structured data.",
|
|
720
|
+
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.",
|
|
684
721
|
fixUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md"
|
|
685
722
|
};
|
|
686
723
|
}
|
|
@@ -756,20 +793,22 @@ var jsonLdValidJsonRule = defineRule({
|
|
|
756
793
|
effort: "low",
|
|
757
794
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#sdjsonld-valid-json",
|
|
758
795
|
title: "JSON-LD blocks parse as valid JSON",
|
|
796
|
+
title_ko: "JSON-LD \uBE14\uB85D\uC758 JSON \uC720\uD6A8\uC131",
|
|
759
797
|
description: "Malformed JSON in an ld+json block is silently ignored by most consumers \u2014 a costly silent failure.",
|
|
760
798
|
run(ctx) {
|
|
761
799
|
if (ctx.jsonLd.length === 0) {
|
|
762
|
-
return { status: "skip", score: 0, rationale: "No JSON-LD to validate." };
|
|
800
|
+
return { status: "skip", score: 0, rationale: "No JSON-LD to validate.", rationale_ko: "\uAC80\uC99D\uD560 JSON-LD\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4." };
|
|
763
801
|
}
|
|
764
802
|
if (hasParseError(ctx.jsonLd)) {
|
|
765
803
|
return {
|
|
766
804
|
status: "fail",
|
|
767
805
|
score: 0,
|
|
768
806
|
rationale: "One or more JSON-LD blocks failed to parse.",
|
|
807
|
+
rationale_ko: "JSON-LD \uBE14\uB85D \uD558\uB098 \uC774\uC0C1\uC744 \uD30C\uC2F1\uD558\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4.",
|
|
769
808
|
fixUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md"
|
|
770
809
|
};
|
|
771
810
|
}
|
|
772
|
-
return { status: "pass", score: 1, rationale: "All JSON-LD blocks parse cleanly." };
|
|
811
|
+
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." };
|
|
773
812
|
}
|
|
774
813
|
});
|
|
775
814
|
|
|
@@ -784,10 +823,11 @@ var schemaTypeRecognizedRule = defineRule({
|
|
|
784
823
|
effort: "low",
|
|
785
824
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#sdschema-type-recognized",
|
|
786
825
|
title: "Schema.org @type is a recognised kind",
|
|
826
|
+
title_ko: "Schema.org @type \uC778\uC2DD \uAC00\uB2A5 \uC5EC\uBD80",
|
|
787
827
|
description: "AI engines match pages against well-known types (Article, Product, FAQPage...). Obscure types weaken the signal.",
|
|
788
828
|
run(ctx) {
|
|
789
829
|
if (ctx.jsonLd.length === 0) {
|
|
790
|
-
return { status: "skip", score: 0, rationale: "No JSON-LD to analyse." };
|
|
830
|
+
return { status: "skip", score: 0, rationale: "No JSON-LD to analyse.", rationale_ko: "\uBD84\uC11D\uD560 JSON-LD\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4." };
|
|
791
831
|
}
|
|
792
832
|
const nodes = flattenJsonLd(ctx.jsonLd);
|
|
793
833
|
const recognized = /* @__PURE__ */ new Set();
|
|
@@ -803,6 +843,7 @@ var schemaTypeRecognizedRule = defineRule({
|
|
|
803
843
|
status: "pass",
|
|
804
844
|
score: 1,
|
|
805
845
|
rationale: `Recognised: ${[...recognized].join(", ")}.`,
|
|
846
|
+
rationale_ko: `\uC778\uC2DD\uB41C \uD0C0\uC785: ${[...recognized].join(", ")}.`,
|
|
806
847
|
evidence: { recognized: [...recognized], all: [...seenTypes] }
|
|
807
848
|
};
|
|
808
849
|
}
|
|
@@ -810,6 +851,7 @@ var schemaTypeRecognizedRule = defineRule({
|
|
|
810
851
|
status: "warn",
|
|
811
852
|
score: 0.3,
|
|
812
853
|
rationale: `No recognised schema.org types. Saw: ${[...seenTypes].join(", ") || "(none)"}.`,
|
|
854
|
+
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)"}.`,
|
|
813
855
|
fixUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md"
|
|
814
856
|
};
|
|
815
857
|
}
|
|
@@ -826,10 +868,11 @@ var requiredFieldsRule = defineRule({
|
|
|
826
868
|
effort: "medium",
|
|
827
869
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#sdrequired-fields",
|
|
828
870
|
title: "Required fields for recognised types are set",
|
|
871
|
+
title_ko: "\uC778\uC2DD\uB41C \uD0C0\uC785\uC758 \uD544\uC218 \uD544\uB4DC \uCDA9\uC871 \uC5EC\uBD80",
|
|
829
872
|
description: "Article needs headline/author/datePublished, FAQPage needs mainEntity, Product needs offers, etc.",
|
|
830
873
|
run(ctx) {
|
|
831
874
|
if (ctx.jsonLd.length === 0) {
|
|
832
|
-
return { status: "skip", score: 0, rationale: "No JSON-LD to analyse." };
|
|
875
|
+
return { status: "skip", score: 0, rationale: "No JSON-LD to analyse.", rationale_ko: "\uBD84\uC11D\uD560 JSON-LD\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4." };
|
|
833
876
|
}
|
|
834
877
|
const nodes = flattenJsonLd(ctx.jsonLd);
|
|
835
878
|
const missing = [];
|
|
@@ -848,14 +891,16 @@ var requiredFieldsRule = defineRule({
|
|
|
848
891
|
return {
|
|
849
892
|
status: "skip",
|
|
850
893
|
score: 0,
|
|
851
|
-
rationale: "No types with known required fields were found."
|
|
894
|
+
rationale: "No types with known required fields were found.",
|
|
895
|
+
rationale_ko: "\uD544\uC218 \uD544\uB4DC\uAC00 \uC815\uC758\uB41C \uD0C0\uC785\uC774 \uC5C6\uC2B5\uB2C8\uB2E4."
|
|
852
896
|
};
|
|
853
897
|
}
|
|
854
898
|
if (missing.length === 0) {
|
|
855
899
|
return {
|
|
856
900
|
status: "pass",
|
|
857
901
|
score: 1,
|
|
858
|
-
rationale: `Required fields set on ${checked.length} node(s)
|
|
902
|
+
rationale: `Required fields set on ${checked.length} node(s).`,
|
|
903
|
+
rationale_ko: `${checked.length}\uAC1C \uB178\uB4DC\uC758 \uD544\uC218 \uD544\uB4DC\uAC00 \uBAA8\uB450 \uCDA9\uC871\uB429\uB2C8\uB2E4.`
|
|
859
904
|
};
|
|
860
905
|
}
|
|
861
906
|
const msg = missing.map((m) => `${m.type}.${m.field}`).join(", ");
|
|
@@ -863,6 +908,7 @@ var requiredFieldsRule = defineRule({
|
|
|
863
908
|
status: "fail",
|
|
864
909
|
score: Math.max(0, 1 - missing.length / (checked.length * 2)),
|
|
865
910
|
rationale: `Missing required fields: ${msg}.`,
|
|
911
|
+
rationale_ko: `\uB204\uB77D\uB41C \uD544\uC218 \uD544\uB4DC: ${msg}.`,
|
|
866
912
|
evidence: missing,
|
|
867
913
|
fixUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md"
|
|
868
914
|
};
|
|
@@ -880,10 +926,11 @@ var microdataFallbackRule = defineRule({
|
|
|
880
926
|
effort: "medium",
|
|
881
927
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#sdmicrodata-fallback",
|
|
882
928
|
title: "Microdata or RDFa fallback when JSON-LD is missing",
|
|
929
|
+
title_ko: "JSON-LD \uC5C6\uC744 \uB54C Microdata/RDFa \uB300\uCCB4 \uC5EC\uBD80",
|
|
883
930
|
description: "If JSON-LD is absent, inline microdata (itemscope/itemtype) or RDFa still gives some structured signal.",
|
|
884
931
|
run(ctx) {
|
|
885
932
|
if (ctx.jsonLd.length > 0) {
|
|
886
|
-
return { status: "skip", score: 0, rationale: "JSON-LD is present; fallback not needed." };
|
|
933
|
+
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." };
|
|
887
934
|
}
|
|
888
935
|
const microdata = ctx.$("[itemscope][itemtype]").length;
|
|
889
936
|
const rdfa = ctx.$("[typeof][vocab], [typeof][property]").length;
|
|
@@ -891,13 +938,15 @@ var microdataFallbackRule = defineRule({
|
|
|
891
938
|
return {
|
|
892
939
|
status: "pass",
|
|
893
940
|
score: 1,
|
|
894
|
-
rationale: `Found ${microdata} microdata and ${rdfa} RDFa nodes
|
|
941
|
+
rationale: `Found ${microdata} microdata and ${rdfa} RDFa nodes.`,
|
|
942
|
+
rationale_ko: `Microdata ${microdata}\uAC1C, RDFa ${rdfa}\uAC1C \uBC1C\uACAC\uB429\uB2C8\uB2E4.`
|
|
895
943
|
};
|
|
896
944
|
}
|
|
897
945
|
return {
|
|
898
946
|
status: "fail",
|
|
899
947
|
score: 0,
|
|
900
948
|
rationale: "No structured data at all (no JSON-LD, no microdata, no RDFa).",
|
|
949
|
+
rationale_ko: "\uAD6C\uC870\uD654 \uB370\uC774\uD130\uAC00 \uC804\uD600 \uC5C6\uC2B5\uB2C8\uB2E4 (JSON-LD, Microdata, RDFa \uBAA8\uB450 \uC5C6\uC74C).",
|
|
901
950
|
fixUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md"
|
|
902
951
|
};
|
|
903
952
|
}
|
|
@@ -915,10 +964,11 @@ var noDuplicateTypesRule = defineRule({
|
|
|
915
964
|
effort: "low",
|
|
916
965
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#sdno-duplicate-types",
|
|
917
966
|
title: "No conflicting duplicate @types",
|
|
967
|
+
title_ko: "@type \uC911\uBCF5 \uCDA9\uB3CC \uC5C6\uC74C",
|
|
918
968
|
description: "Multiple competing entities of the same primary type (e.g. two Articles) confuse the engine about which one represents the page.",
|
|
919
969
|
run(ctx) {
|
|
920
970
|
if (ctx.jsonLd.length === 0) {
|
|
921
|
-
return { status: "skip", score: 0, rationale: "No JSON-LD to analyse." };
|
|
971
|
+
return { status: "skip", score: 0, rationale: "No JSON-LD to analyse.", rationale_ko: "\uBD84\uC11D\uD560 JSON-LD\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4." };
|
|
922
972
|
}
|
|
923
973
|
const counts = /* @__PURE__ */ new Map();
|
|
924
974
|
for (const node of flattenJsonLd(ctx.jsonLd)) {
|
|
@@ -928,12 +978,13 @@ var noDuplicateTypesRule = defineRule({
|
|
|
928
978
|
}
|
|
929
979
|
const dupes = [...counts.entries()].filter(([, n]) => n > 1);
|
|
930
980
|
if (dupes.length === 0) {
|
|
931
|
-
return { status: "pass", score: 1, rationale: "No duplicate primary types." };
|
|
981
|
+
return { status: "pass", score: 1, rationale: "No duplicate primary types.", rationale_ko: "\uC911\uBCF5\uB41C \uAE30\uBCF8 \uD0C0\uC785\uC774 \uC5C6\uC2B5\uB2C8\uB2E4." };
|
|
932
982
|
}
|
|
933
983
|
return {
|
|
934
984
|
status: "warn",
|
|
935
985
|
score: 0.4,
|
|
936
986
|
rationale: `Duplicate primary types: ${dupes.map(([t, n]) => `${t}\xD7${n}`).join(", ")}.`,
|
|
987
|
+
rationale_ko: `\uC911\uBCF5\uB41C \uAE30\uBCF8 \uD0C0\uC785: ${dupes.map(([t, n]) => `${t}\xD7${n}`).join(", ")}.`,
|
|
937
988
|
fixUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md"
|
|
938
989
|
};
|
|
939
990
|
}
|
|
@@ -981,10 +1032,11 @@ var sameAsEntityRule = defineRule({
|
|
|
981
1032
|
effort: "medium",
|
|
982
1033
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#sdsameas-entity",
|
|
983
1034
|
title: "Entity nodes link the knowledge graph via sameAs",
|
|
1035
|
+
title_ko: "sameAs\uB85C \uC9C0\uC2DD \uADF8\uB798\uD504 \uC5F0\uACB0 \uC5EC\uBD80",
|
|
984
1036
|
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).",
|
|
985
1037
|
run(ctx) {
|
|
986
1038
|
if (ctx.jsonLd.length === 0) {
|
|
987
|
-
return { status: "skip", score: 0, rationale: "No JSON-LD to analyse." };
|
|
1039
|
+
return { status: "skip", score: 0, rationale: "No JSON-LD to analyse.", rationale_ko: "\uBD84\uC11D\uD560 JSON-LD\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4." };
|
|
988
1040
|
}
|
|
989
1041
|
const nodes = flattenJsonLd(ctx.jsonLd);
|
|
990
1042
|
const entities = nodes.filter((n) => getTypes(n).some((t) => ENTITY_TYPES.includes(t)));
|
|
@@ -992,7 +1044,8 @@ var sameAsEntityRule = defineRule({
|
|
|
992
1044
|
return {
|
|
993
1045
|
status: "skip",
|
|
994
1046
|
score: 0,
|
|
995
|
-
rationale: "No Organization/Person/LocalBusiness/Brand entity to link."
|
|
1047
|
+
rationale: "No Organization/Person/LocalBusiness/Brand entity to link.",
|
|
1048
|
+
rationale_ko: "\uC5F0\uACB0\uD560 Organization/Person/LocalBusiness/Brand \uC5D4\uD2F0\uD2F0\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4."
|
|
996
1049
|
};
|
|
997
1050
|
}
|
|
998
1051
|
let bestScore = 0;
|
|
@@ -1016,6 +1069,7 @@ var sameAsEntityRule = defineRule({
|
|
|
1016
1069
|
status: "pass",
|
|
1017
1070
|
score: 1,
|
|
1018
1071
|
rationale: `Entity links ${bestEvidence.trusted} trusted knowledge-graph hosts via sameAs.`,
|
|
1072
|
+
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.`,
|
|
1019
1073
|
evidence: bestEvidence
|
|
1020
1074
|
};
|
|
1021
1075
|
}
|
|
@@ -1024,6 +1078,7 @@ var sameAsEntityRule = defineRule({
|
|
|
1024
1078
|
status: "pass",
|
|
1025
1079
|
score: bestScore,
|
|
1026
1080
|
rationale: `Entity has 1 trusted sameAs link. Add Wikipedia/Wikidata for stronger E-E-A-T.`,
|
|
1081
|
+
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.",
|
|
1027
1082
|
evidence: bestEvidence,
|
|
1028
1083
|
estimatedImpact: 1
|
|
1029
1084
|
};
|
|
@@ -1033,6 +1088,7 @@ var sameAsEntityRule = defineRule({
|
|
|
1033
1088
|
status: "warn",
|
|
1034
1089
|
score: bestScore,
|
|
1035
1090
|
rationale: "Entity declares sameAs but no trusted knowledge-graph hosts (Wikipedia/Wikidata/LinkedIn).",
|
|
1091
|
+
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.",
|
|
1036
1092
|
evidence: bestEvidence,
|
|
1037
1093
|
fixHint: "Add Wikipedia/Wikidata/LinkedIn URLs to your Organization sameAs[].",
|
|
1038
1094
|
estimatedImpact: 2
|
|
@@ -1042,6 +1098,7 @@ var sameAsEntityRule = defineRule({
|
|
|
1042
1098
|
status: "warn",
|
|
1043
1099
|
score: 0,
|
|
1044
1100
|
rationale: `${entities.length} entity node(s) found but none declare sameAs links.`,
|
|
1101
|
+
rationale_ko: `\uC5D4\uD2F0\uD2F0 \uB178\uB4DC\uAC00 ${entities.length}\uAC1C \uC788\uC9C0\uB9CC sameAs \uB9C1\uD06C\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.`,
|
|
1045
1102
|
fixHint: 'Add sameAs:["https://en.wikipedia.org/wiki/...", "https://www.linkedin.com/company/..."] to your Organization JSON-LD.',
|
|
1046
1103
|
estimatedImpact: 3
|
|
1047
1104
|
};
|
|
@@ -1073,15 +1130,16 @@ var breadcrumbValidRule = defineRule({
|
|
|
1073
1130
|
effort: "medium",
|
|
1074
1131
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#sdbreadcrumb-valid",
|
|
1075
1132
|
title: "BreadcrumbList items declare position, name, and item",
|
|
1133
|
+
title_ko: "BreadcrumbList \uD56D\uBAA9\uC758 \uD544\uC218 \uD544\uB4DC \uCDA9\uC871 \uC5EC\uBD80",
|
|
1076
1134
|
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.",
|
|
1077
1135
|
run(ctx) {
|
|
1078
1136
|
if (ctx.jsonLd.length === 0) {
|
|
1079
|
-
return { status: "skip", score: 0, rationale: "No JSON-LD to analyse." };
|
|
1137
|
+
return { status: "skip", score: 0, rationale: "No JSON-LD to analyse.", rationale_ko: "\uBD84\uC11D\uD560 JSON-LD\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4." };
|
|
1080
1138
|
}
|
|
1081
1139
|
const nodes = flattenJsonLd(ctx.jsonLd);
|
|
1082
1140
|
const breadcrumbs = nodes.filter((n) => getTypes(n).includes("BreadcrumbList"));
|
|
1083
1141
|
if (breadcrumbs.length === 0) {
|
|
1084
|
-
return { status: "skip", score: 0, rationale: "No BreadcrumbList present." };
|
|
1142
|
+
return { status: "skip", score: 0, rationale: "No BreadcrumbList present.", rationale_ko: "BreadcrumbList\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4." };
|
|
1085
1143
|
}
|
|
1086
1144
|
const allIssues = [];
|
|
1087
1145
|
let totalItems = 0;
|
|
@@ -1094,7 +1152,8 @@ var breadcrumbValidRule = defineRule({
|
|
|
1094
1152
|
return {
|
|
1095
1153
|
status: "pass",
|
|
1096
1154
|
score: 1,
|
|
1097
|
-
rationale: `BreadcrumbList(s) valid (${totalItems} items)
|
|
1155
|
+
rationale: `BreadcrumbList(s) valid (${totalItems} items).`,
|
|
1156
|
+
rationale_ko: `BreadcrumbList\uAC00 \uC720\uD6A8\uD569\uB2C8\uB2E4 (\uD56D\uBAA9 ${totalItems}\uAC1C).`
|
|
1098
1157
|
};
|
|
1099
1158
|
}
|
|
1100
1159
|
const fatalCount = allIssues.length;
|
|
@@ -1104,6 +1163,7 @@ var breadcrumbValidRule = defineRule({
|
|
|
1104
1163
|
status: score < 0.5 ? "fail" : "warn",
|
|
1105
1164
|
score,
|
|
1106
1165
|
rationale: `${fatalCount} breadcrumb item(s) missing required fields.`,
|
|
1166
|
+
rationale_ko: `breadcrumb \uD56D\uBAA9 ${fatalCount}\uAC1C\uC5D0 \uD544\uC218 \uD544\uB4DC\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.`,
|
|
1107
1167
|
evidence: allIssues.slice(0, 5),
|
|
1108
1168
|
fixHint: 'Each itemListElement needs { "@type": "ListItem", position: N, name, item }.',
|
|
1109
1169
|
estimatedImpact: Math.round(2 * (1 - score))
|
|
@@ -1134,6 +1194,7 @@ var titleRule = defineRule({
|
|
|
1134
1194
|
effort: "low",
|
|
1135
1195
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#cittitle",
|
|
1136
1196
|
title: "<title> is set with a reasonable length",
|
|
1197
|
+
title_ko: "<title> \uD0DC\uADF8 \uC801\uC815 \uAE38\uC774 \uC124\uC815 \uC5EC\uBD80",
|
|
1137
1198
|
description: "The document title is the single most-cited piece of text and should be 10\u201370 characters.",
|
|
1138
1199
|
run(ctx) {
|
|
1139
1200
|
const title = ctx.$("head > title").first().text().trim();
|
|
@@ -1142,6 +1203,7 @@ var titleRule = defineRule({
|
|
|
1142
1203
|
status: "fail",
|
|
1143
1204
|
score: 0,
|
|
1144
1205
|
rationale: "Page has no <title>.",
|
|
1206
|
+
rationale_ko: "\uD398\uC774\uC9C0\uC5D0 <title>\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.",
|
|
1145
1207
|
fixUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md"
|
|
1146
1208
|
};
|
|
1147
1209
|
}
|
|
@@ -1149,17 +1211,19 @@ var titleRule = defineRule({
|
|
|
1149
1211
|
return {
|
|
1150
1212
|
status: "warn",
|
|
1151
1213
|
score: 0.4,
|
|
1152
|
-
rationale: `Title is only ${title.length} chars; consider a more descriptive one
|
|
1214
|
+
rationale: `Title is only ${title.length} chars; consider a more descriptive one.`,
|
|
1215
|
+
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.`
|
|
1153
1216
|
};
|
|
1154
1217
|
}
|
|
1155
1218
|
if (title.length > 70) {
|
|
1156
1219
|
return {
|
|
1157
1220
|
status: "warn",
|
|
1158
1221
|
score: 0.6,
|
|
1159
|
-
rationale: `Title is ${title.length} chars; search UIs commonly truncate after ~70
|
|
1222
|
+
rationale: `Title is ${title.length} chars; search UIs commonly truncate after ~70.`,
|
|
1223
|
+
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.`
|
|
1160
1224
|
};
|
|
1161
1225
|
}
|
|
1162
|
-
return { status: "pass", score: 1, rationale: `Title length ${title.length} is within range.` };
|
|
1226
|
+
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.` };
|
|
1163
1227
|
}
|
|
1164
1228
|
});
|
|
1165
1229
|
|
|
@@ -1174,6 +1238,7 @@ var metaDescriptionRule = defineRule({
|
|
|
1174
1238
|
effort: "low",
|
|
1175
1239
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#citmeta-description",
|
|
1176
1240
|
title: "meta description is set (50\u2013160 chars)",
|
|
1241
|
+
title_ko: "meta description \uC124\uC815 \uC5EC\uBD80 (50\u2013160\uC790)",
|
|
1177
1242
|
description: "AI snippets often quote the meta description verbatim; aim for 50\u2013160 chars.",
|
|
1178
1243
|
run(ctx) {
|
|
1179
1244
|
const desc = ctx.$('head meta[name="description"]').attr("content")?.trim() ?? "";
|
|
@@ -1182,16 +1247,17 @@ var metaDescriptionRule = defineRule({
|
|
|
1182
1247
|
status: "warn",
|
|
1183
1248
|
score: 0,
|
|
1184
1249
|
rationale: "No meta description set.",
|
|
1250
|
+
rationale_ko: "meta description\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.",
|
|
1185
1251
|
fixUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md"
|
|
1186
1252
|
};
|
|
1187
1253
|
}
|
|
1188
1254
|
if (desc.length < 50) {
|
|
1189
|
-
return { status: "warn", score: 0.5, rationale: `Only ${desc.length} chars; aim for 50
|
|
1255
|
+
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.` };
|
|
1190
1256
|
}
|
|
1191
1257
|
if (desc.length > 160) {
|
|
1192
|
-
return { status: "warn", score: 0.7, rationale: `${desc.length} chars; may be truncated after 160.` };
|
|
1258
|
+
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.` };
|
|
1193
1259
|
}
|
|
1194
|
-
return { status: "pass", score: 1, rationale: `Description length ${desc.length} is within range.` };
|
|
1260
|
+
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.` };
|
|
1195
1261
|
}
|
|
1196
1262
|
});
|
|
1197
1263
|
|
|
@@ -1206,6 +1272,7 @@ var canonicalRule = defineRule({
|
|
|
1206
1272
|
effort: "low",
|
|
1207
1273
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#citcanonical",
|
|
1208
1274
|
title: "Canonical URL is declared",
|
|
1275
|
+
title_ko: "Canonical URL \uC120\uC5B8 \uC5EC\uBD80",
|
|
1209
1276
|
description: 'rel="canonical" tells crawlers which URL is the source of truth, preventing duplicate-citation confusion.',
|
|
1210
1277
|
run(ctx) {
|
|
1211
1278
|
const href = ctx.$('head link[rel="canonical"]').attr("href")?.trim();
|
|
@@ -1214,14 +1281,15 @@ var canonicalRule = defineRule({
|
|
|
1214
1281
|
status: "warn",
|
|
1215
1282
|
score: 0,
|
|
1216
1283
|
rationale: 'No <link rel="canonical"> found.',
|
|
1284
|
+
rationale_ko: '<link rel="canonical">\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.',
|
|
1217
1285
|
fixUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md"
|
|
1218
1286
|
};
|
|
1219
1287
|
}
|
|
1220
1288
|
try {
|
|
1221
1289
|
const abs = new URL(href, ctx.finalUrl).toString();
|
|
1222
|
-
return { status: "pass", score: 1, rationale: `Canonical URL: ${abs}.` };
|
|
1290
|
+
return { status: "pass", score: 1, rationale: `Canonical URL: ${abs}.`, rationale_ko: `Canonical URL: ${abs}.` };
|
|
1223
1291
|
} catch {
|
|
1224
|
-
return { status: "fail", score: 0, rationale: `Canonical href is not a valid URL: ${href}` };
|
|
1292
|
+
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}` };
|
|
1225
1293
|
}
|
|
1226
1294
|
}
|
|
1227
1295
|
});
|
|
@@ -1238,6 +1306,7 @@ var ogTagsRule = defineRule({
|
|
|
1238
1306
|
effort: "low",
|
|
1239
1307
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#citog-tags",
|
|
1240
1308
|
title: "Open Graph tags are set",
|
|
1309
|
+
title_ko: "Open Graph \uD0DC\uADF8 \uC124\uC815 \uC5EC\uBD80",
|
|
1241
1310
|
description: "og:title/type/url/image power rich previews on AI chat, social, and messaging.",
|
|
1242
1311
|
run(ctx) {
|
|
1243
1312
|
const missing = [];
|
|
@@ -1246,13 +1315,14 @@ var ogTagsRule = defineRule({
|
|
|
1246
1315
|
if (!val) missing.push(prop);
|
|
1247
1316
|
}
|
|
1248
1317
|
if (missing.length === 0) {
|
|
1249
|
-
return { status: "pass", score: 1, rationale: "All required OG tags present." };
|
|
1318
|
+
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." };
|
|
1250
1319
|
}
|
|
1251
1320
|
const ratio = 1 - missing.length / REQUIRED.length;
|
|
1252
1321
|
return {
|
|
1253
1322
|
status: missing.length === REQUIRED.length ? "fail" : "warn",
|
|
1254
1323
|
score: ratio,
|
|
1255
1324
|
rationale: `Missing: ${missing.join(", ")}.`,
|
|
1325
|
+
rationale_ko: `\uB204\uB77D\uB41C \uD0DC\uADF8: ${missing.join(", ")}.`,
|
|
1256
1326
|
fixUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md"
|
|
1257
1327
|
};
|
|
1258
1328
|
}
|
|
@@ -1269,20 +1339,22 @@ var twitterCardRule = defineRule({
|
|
|
1269
1339
|
effort: "low",
|
|
1270
1340
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#cittwitter-card",
|
|
1271
1341
|
title: "Twitter Card metadata is set",
|
|
1342
|
+
title_ko: "Twitter Card \uBA54\uD0C0\uB370\uC774\uD130 \uC124\uC815 \uC5EC\uBD80",
|
|
1272
1343
|
description: "twitter:card + twitter:title give better previews on X/Twitter and some AI surfaces that reuse the tags.",
|
|
1273
1344
|
run(ctx) {
|
|
1274
1345
|
const card = ctx.$('head meta[name="twitter:card"]').attr("content")?.trim();
|
|
1275
1346
|
const title = ctx.$('head meta[name="twitter:title"]').attr("content")?.trim();
|
|
1276
1347
|
if (card && title) {
|
|
1277
|
-
return { status: "pass", score: 1, rationale: `Card type: ${card}.` };
|
|
1348
|
+
return { status: "pass", score: 1, rationale: `Card type: ${card}.`, rationale_ko: `\uCE74\uB4DC \uC720\uD615: ${card}.` };
|
|
1278
1349
|
}
|
|
1279
1350
|
if (card || title) {
|
|
1280
|
-
return { status: "warn", score: 0.5, rationale: "Partial twitter:* metadata; add the missing tag." };
|
|
1351
|
+
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." };
|
|
1281
1352
|
}
|
|
1282
1353
|
return {
|
|
1283
1354
|
status: "warn",
|
|
1284
1355
|
score: 0,
|
|
1285
1356
|
rationale: "No twitter:card metadata.",
|
|
1357
|
+
rationale_ko: "twitter:card \uBA54\uD0C0\uB370\uC774\uD130\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.",
|
|
1286
1358
|
fixUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md"
|
|
1287
1359
|
};
|
|
1288
1360
|
}
|
|
@@ -1299,6 +1371,7 @@ var langAttrRule = defineRule({
|
|
|
1299
1371
|
effort: "low",
|
|
1300
1372
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#citlang-attr",
|
|
1301
1373
|
title: "<html lang> is set",
|
|
1374
|
+
title_ko: "<html lang> \uC18D\uC131 \uC124\uC815 \uC5EC\uBD80",
|
|
1302
1375
|
description: "A lang attribute helps AI engines route the page to the right-language search surface (and helps screen readers).",
|
|
1303
1376
|
run(ctx) {
|
|
1304
1377
|
const lang = ctx.$("html").attr("lang")?.trim();
|
|
@@ -1307,10 +1380,11 @@ var langAttrRule = defineRule({
|
|
|
1307
1380
|
status: "warn",
|
|
1308
1381
|
score: 0,
|
|
1309
1382
|
rationale: "No lang attribute on <html>.",
|
|
1383
|
+
rationale_ko: "<html>\uC5D0 lang \uC18D\uC131\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.",
|
|
1310
1384
|
fixUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md"
|
|
1311
1385
|
};
|
|
1312
1386
|
}
|
|
1313
|
-
return { status: "pass", score: 1, rationale: `lang="${lang}".` };
|
|
1387
|
+
return { status: "pass", score: 1, rationale: `lang="${lang}".`, rationale_ko: `lang="${lang}".` };
|
|
1314
1388
|
}
|
|
1315
1389
|
});
|
|
1316
1390
|
|
|
@@ -1325,25 +1399,27 @@ var authorVisibleRule = defineRule({
|
|
|
1325
1399
|
effort: "medium",
|
|
1326
1400
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#citauthor-visible",
|
|
1327
1401
|
title: "Author is declared",
|
|
1402
|
+
title_ko: "\uC791\uC131\uC790 \uC815\uBCF4 \uC120\uC5B8 \uC5EC\uBD80",
|
|
1328
1403
|
description: "AI engines prefer citing content with an identifiable author; expose one via JSON-LD, meta[name=author], rel=author, or a .author class.",
|
|
1329
1404
|
run(ctx) {
|
|
1330
1405
|
for (const node of flattenJsonLd(ctx.jsonLd)) {
|
|
1331
1406
|
if (hasField(node, "author")) {
|
|
1332
|
-
return { status: "pass", score: 1, rationale: "Author found in JSON-LD." };
|
|
1407
|
+
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." };
|
|
1333
1408
|
}
|
|
1334
1409
|
}
|
|
1335
1410
|
const metaAuthor = ctx.$('head meta[name="author"]').attr("content")?.trim();
|
|
1336
|
-
if (metaAuthor) return { status: "pass", score: 1, rationale: `meta[name=author] = "${metaAuthor}".` };
|
|
1411
|
+
if (metaAuthor) return { status: "pass", score: 1, rationale: `meta[name=author] = "${metaAuthor}".`, rationale_ko: `meta[name=author] = "${metaAuthor}".` };
|
|
1337
1412
|
if (ctx.$('[rel="author"]').length > 0) {
|
|
1338
|
-
return { status: "pass", score: 1, rationale: 'rel="author" link found.' };
|
|
1413
|
+
return { status: "pass", score: 1, rationale: 'rel="author" link found.', rationale_ko: 'rel="author" \uB9C1\uD06C\uB97C \uCC3E\uC558\uC2B5\uB2C8\uB2E4.' };
|
|
1339
1414
|
}
|
|
1340
1415
|
if (ctx.$('.author, [class*="author"], [itemprop="author"]').length > 0) {
|
|
1341
|
-
return { status: "pass", score: 0.8, rationale: "Author-ish DOM selector found (weaker signal)." };
|
|
1416
|
+
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)." };
|
|
1342
1417
|
}
|
|
1343
1418
|
return {
|
|
1344
1419
|
status: "warn",
|
|
1345
1420
|
score: 0,
|
|
1346
1421
|
rationale: "No author signal found (JSON-LD, meta, rel, or .author).",
|
|
1422
|
+
rationale_ko: "\uC791\uC131\uC790 \uC815\uBCF4\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4 (JSON-LD, meta, rel, .author \uBAA8\uB450 \uC5C6\uC74C).",
|
|
1347
1423
|
fixUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md"
|
|
1348
1424
|
};
|
|
1349
1425
|
}
|
|
@@ -1360,25 +1436,27 @@ var datesRule = defineRule({
|
|
|
1360
1436
|
effort: "low",
|
|
1361
1437
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#citdates",
|
|
1362
1438
|
title: "Publish / modified date is present",
|
|
1439
|
+
title_ko: "\uBC1C\uD589\uC77C / \uC218\uC815\uC77C \uC874\uC7AC \uC5EC\uBD80",
|
|
1363
1440
|
description: "AI engines rank recent pages higher; expose datePublished via JSON-LD, <time datetime>, or article:published_time meta.",
|
|
1364
1441
|
run(ctx) {
|
|
1365
1442
|
for (const node of flattenJsonLd(ctx.jsonLd)) {
|
|
1366
1443
|
if (hasField(node, "datePublished")) {
|
|
1367
|
-
return { status: "pass", score: 1, rationale: "datePublished found in JSON-LD." };
|
|
1444
|
+
return { status: "pass", score: 1, rationale: "datePublished found in JSON-LD.", rationale_ko: "JSON-LD\uC5D0\uC11C datePublished\uB97C \uCC3E\uC558\uC2B5\uB2C8\uB2E4." };
|
|
1368
1445
|
}
|
|
1369
1446
|
}
|
|
1370
1447
|
const articleTime = ctx.$('head meta[property="article:published_time"]').attr("content")?.trim();
|
|
1371
1448
|
if (articleTime) {
|
|
1372
|
-
return { status: "pass", score: 1, rationale: `article:published_time = ${articleTime}.` };
|
|
1449
|
+
return { status: "pass", score: 1, rationale: `article:published_time = ${articleTime}.`, rationale_ko: `article:published_time = ${articleTime}.` };
|
|
1373
1450
|
}
|
|
1374
1451
|
const timeEl = ctx.$("time[datetime]").first().attr("datetime")?.trim();
|
|
1375
1452
|
if (timeEl) {
|
|
1376
|
-
return { status: "pass", score: 0.8, rationale: `<time datetime="${timeEl}"> found.` };
|
|
1453
|
+
return { status: "pass", score: 0.8, rationale: `<time datetime="${timeEl}"> found.`, rationale_ko: `<time datetime="${timeEl}">\uB97C \uCC3E\uC558\uC2B5\uB2C8\uB2E4.` };
|
|
1377
1454
|
}
|
|
1378
1455
|
return {
|
|
1379
1456
|
status: "warn",
|
|
1380
1457
|
score: 0,
|
|
1381
1458
|
rationale: "No publish date found (JSON-LD, meta article:published_time, or <time datetime>).",
|
|
1459
|
+
rationale_ko: "\uBC1C\uD589\uC77C\uC774 \uC5C6\uC2B5\uB2C8\uB2E4 (JSON-LD, meta article:published_time, <time datetime> \uBAA8\uB450 \uC5C6\uC74C).",
|
|
1382
1460
|
fixUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md"
|
|
1383
1461
|
};
|
|
1384
1462
|
}
|
|
@@ -1406,6 +1484,7 @@ var contentFreshnessRule = defineRule({
|
|
|
1406
1484
|
effort: "low",
|
|
1407
1485
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#citcontent-freshness",
|
|
1408
1486
|
title: "Article content is fresh (dateModified within 1 year)",
|
|
1487
|
+
title_ko: "\uCF58\uD150\uCE20 \uCD5C\uC2E0\uC131 (dateModified 1\uB144 \uC774\uB0B4)",
|
|
1409
1488
|
description: "AI engines down-rank stale content. Surface a recent dateModified (\u2264365 days) on Article-like pages so retrieval rankings stay strong.",
|
|
1410
1489
|
run(ctx) {
|
|
1411
1490
|
const nodes = flattenJsonLd(ctx.jsonLd);
|
|
@@ -1414,7 +1493,8 @@ var contentFreshnessRule = defineRule({
|
|
|
1414
1493
|
return {
|
|
1415
1494
|
status: "skip",
|
|
1416
1495
|
score: 0,
|
|
1417
|
-
rationale: "No Article/BlogPosting/NewsArticle JSON-LD; freshness signal not applicable."
|
|
1496
|
+
rationale: "No Article/BlogPosting/NewsArticle JSON-LD; freshness signal not applicable.",
|
|
1497
|
+
rationale_ko: "Article/BlogPosting/NewsArticle JSON-LD\uAC00 \uC5C6\uC5B4 \uCD5C\uC2E0\uC131 \uC2E0\uD638\uB97C \uD655\uC778\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4."
|
|
1418
1498
|
};
|
|
1419
1499
|
}
|
|
1420
1500
|
let bestMs = null;
|
|
@@ -1436,6 +1516,7 @@ var contentFreshnessRule = defineRule({
|
|
|
1436
1516
|
status: "warn",
|
|
1437
1517
|
score: 0,
|
|
1438
1518
|
rationale: "Article has no parseable dateModified or datePublished.",
|
|
1519
|
+
rationale_ko: "Article JSON-LD\uC5D0 \uD30C\uC2F1 \uAC00\uB2A5\uD55C dateModified \uB610\uB294 datePublished\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.",
|
|
1439
1520
|
fixHint: "Add ISO-8601 dateModified and datePublished to your Article JSON-LD.",
|
|
1440
1521
|
estimatedImpact: 3
|
|
1441
1522
|
};
|
|
@@ -1446,6 +1527,7 @@ var contentFreshnessRule = defineRule({
|
|
|
1446
1527
|
status: "pass",
|
|
1447
1528
|
score: 1,
|
|
1448
1529
|
rationale: `${usedField} within the last year (~${ageDays} day${ageDays === 1 ? "" : "s"} ago).`,
|
|
1530
|
+
rationale_ko: `${usedField}\uC774 1\uB144 \uC774\uB0B4\uC785\uB2C8\uB2E4 (\uC57D ${ageDays}\uC77C \uC804).`,
|
|
1449
1531
|
evidence: { ageDays, field: usedField }
|
|
1450
1532
|
};
|
|
1451
1533
|
}
|
|
@@ -1454,6 +1536,7 @@ var contentFreshnessRule = defineRule({
|
|
|
1454
1536
|
status: "warn",
|
|
1455
1537
|
score: 0.6,
|
|
1456
1538
|
rationale: `${usedField} is ${ageDays} days old. Refresh within a year for best AI ranking.`,
|
|
1539
|
+
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.`,
|
|
1457
1540
|
evidence: { ageDays, field: usedField },
|
|
1458
1541
|
estimatedImpact: 2
|
|
1459
1542
|
};
|
|
@@ -1462,6 +1545,7 @@ var contentFreshnessRule = defineRule({
|
|
|
1462
1545
|
status: "warn",
|
|
1463
1546
|
score: 0.2,
|
|
1464
1547
|
rationale: `${usedField} is ${ageDays} days old (>2 years). AI engines treat this as stale.`,
|
|
1548
|
+
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.`,
|
|
1465
1549
|
evidence: { ageDays, field: usedField },
|
|
1466
1550
|
fixHint: "Update content and bump dateModified to today's date.",
|
|
1467
1551
|
estimatedImpact: 3
|
|
@@ -1493,22 +1577,25 @@ var singleH1Rule = defineRule({
|
|
|
1493
1577
|
effort: "low",
|
|
1494
1578
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#cntsingle-h1",
|
|
1495
1579
|
title: "Exactly one <h1>",
|
|
1580
|
+
title_ko: "<h1> \uD0DC\uADF8 1\uAC1C \uC5EC\uBD80",
|
|
1496
1581
|
description: "A single H1 tells AI engines the primary topic of the page without ambiguity.",
|
|
1497
1582
|
run(ctx) {
|
|
1498
1583
|
const n = ctx.$("h1").length;
|
|
1499
|
-
if (n === 1) return { status: "pass", score: 1, rationale: "Exactly one <h1>." };
|
|
1584
|
+
if (n === 1) return { status: "pass", score: 1, rationale: "Exactly one <h1>.", rationale_ko: "<h1>\uC774 \uC815\uD655\uD788 1\uAC1C\uC785\uB2C8\uB2E4." };
|
|
1500
1585
|
if (n === 0) {
|
|
1501
1586
|
return {
|
|
1502
1587
|
status: "fail",
|
|
1503
1588
|
score: 0,
|
|
1504
1589
|
rationale: "No <h1> on the page.",
|
|
1590
|
+
rationale_ko: "\uD398\uC774\uC9C0\uC5D0 <h1>\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.",
|
|
1505
1591
|
fixUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules/cnt.single-h1.md"
|
|
1506
1592
|
};
|
|
1507
1593
|
}
|
|
1508
1594
|
return {
|
|
1509
1595
|
status: "warn",
|
|
1510
1596
|
score: Math.max(0.3, 1 / n),
|
|
1511
|
-
rationale: `Found ${n} <h1> tags; prefer one primary heading
|
|
1597
|
+
rationale: `Found ${n} <h1> tags; prefer one primary heading.`,
|
|
1598
|
+
rationale_ko: `<h1>\uC774 ${n}\uAC1C \uC788\uC2B5\uB2C8\uB2E4. \uB300\uD45C \uC81C\uBAA9 1\uAC1C\uB9CC \uC0AC\uC6A9\uD558\uC138\uC694.`
|
|
1512
1599
|
};
|
|
1513
1600
|
}
|
|
1514
1601
|
});
|
|
@@ -1524,6 +1611,7 @@ var headingHierarchyRule = defineRule({
|
|
|
1524
1611
|
effort: "medium",
|
|
1525
1612
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#cntheading-hierarchy",
|
|
1526
1613
|
title: "Heading levels do not skip",
|
|
1614
|
+
title_ko: "\uC81C\uBAA9 \uB2E8\uACC4 \uC21C\uC11C \uC900\uC218 \uC5EC\uBD80",
|
|
1527
1615
|
description: "Going from H2 directly to H4 breaks the outline AI engines use to segment content.",
|
|
1528
1616
|
run(ctx) {
|
|
1529
1617
|
const levels = [];
|
|
@@ -1533,7 +1621,7 @@ var headingHierarchyRule = defineRule({
|
|
|
1533
1621
|
if (m?.[1]) levels.push(parseInt(m[1], 10));
|
|
1534
1622
|
});
|
|
1535
1623
|
if (levels.length === 0) {
|
|
1536
|
-
return { status: "skip", score: 0, rationale: "No headings found." };
|
|
1624
|
+
return { status: "skip", score: 0, rationale: "No headings found.", rationale_ko: "\uC81C\uBAA9 \uD0DC\uADF8\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4." };
|
|
1537
1625
|
}
|
|
1538
1626
|
const skips = [];
|
|
1539
1627
|
for (let i = 1; i < levels.length; i++) {
|
|
@@ -1542,12 +1630,13 @@ var headingHierarchyRule = defineRule({
|
|
|
1542
1630
|
if (curr > prev + 1) skips.push({ from: prev, to: curr });
|
|
1543
1631
|
}
|
|
1544
1632
|
if (skips.length === 0) {
|
|
1545
|
-
return { status: "pass", score: 1, rationale: "No heading-level skips." };
|
|
1633
|
+
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." };
|
|
1546
1634
|
}
|
|
1547
1635
|
return {
|
|
1548
1636
|
status: "warn",
|
|
1549
1637
|
score: Math.max(0.3, 1 - skips.length / levels.length),
|
|
1550
1638
|
rationale: `${skips.length} heading skip(s) detected (e.g. h${skips[0].from}\u2192h${skips[0].to}).`,
|
|
1639
|
+
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}).`,
|
|
1551
1640
|
fixUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md"
|
|
1552
1641
|
};
|
|
1553
1642
|
}
|
|
@@ -1564,11 +1653,12 @@ var imageAltRule = defineRule({
|
|
|
1564
1653
|
effort: "medium",
|
|
1565
1654
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#cntimage-alt",
|
|
1566
1655
|
title: "\u226580% of <img> have alt text",
|
|
1656
|
+
title_ko: "<img>\uC758 80% \uC774\uC0C1 alt \uD14D\uC2A4\uD2B8 \uBCF4\uC720 \uC5EC\uBD80",
|
|
1567
1657
|
description: "Alt text gives AI engines a textual anchor for visual content and improves accessibility.",
|
|
1568
1658
|
run(ctx) {
|
|
1569
1659
|
const imgs = ctx.$("img");
|
|
1570
1660
|
const total = imgs.length;
|
|
1571
|
-
if (total === 0) return { status: "skip", score: 0, rationale: "No <img> on the page." };
|
|
1661
|
+
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." };
|
|
1572
1662
|
let withAlt = 0;
|
|
1573
1663
|
imgs.each((_i, el) => {
|
|
1574
1664
|
const alt = ctx.$(el).attr("alt");
|
|
@@ -1576,12 +1666,13 @@ var imageAltRule = defineRule({
|
|
|
1576
1666
|
});
|
|
1577
1667
|
const ratio = withAlt / total;
|
|
1578
1668
|
if (ratio >= 0.8) {
|
|
1579
|
-
return { status: "pass", score: 1, rationale: `${withAlt}/${total} images have alt (${Math.round(ratio * 100)}%).` };
|
|
1669
|
+
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)}%).` };
|
|
1580
1670
|
}
|
|
1581
1671
|
return {
|
|
1582
1672
|
status: "warn",
|
|
1583
1673
|
score: ratio,
|
|
1584
1674
|
rationale: `Only ${withAlt}/${total} images have alt text (${Math.round(ratio * 100)}%). Aim for \u226580%.`,
|
|
1675
|
+
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.`,
|
|
1585
1676
|
fixUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md"
|
|
1586
1677
|
};
|
|
1587
1678
|
}
|
|
@@ -1598,11 +1689,12 @@ var tldrOrFaqRule = defineRule({
|
|
|
1598
1689
|
effort: "medium",
|
|
1599
1690
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#cnttldr-or-faq",
|
|
1600
1691
|
title: "TL;DR summary or FAQ block",
|
|
1692
|
+
title_ko: "TL;DR \uC694\uC57D \uB610\uB294 FAQ \uBE14\uB85D \uC874\uC7AC \uC5EC\uBD80",
|
|
1601
1693
|
description: 'AI engines strongly prefer content with a quotable summary or FAQ \u2014 it makes the page "citation-ready".',
|
|
1602
1694
|
run(ctx) {
|
|
1603
1695
|
for (const node of flattenJsonLd(ctx.jsonLd)) {
|
|
1604
1696
|
if (getTypes(node).includes("FAQPage")) {
|
|
1605
|
-
return { status: "pass", score: 1, rationale: "FAQPage schema present." };
|
|
1697
|
+
return { status: "pass", score: 1, rationale: "FAQPage schema present.", rationale_ko: "FAQPage \uC2A4\uD0A4\uB9C8\uAC00 \uC788\uC2B5\uB2C8\uB2E4." };
|
|
1606
1698
|
}
|
|
1607
1699
|
}
|
|
1608
1700
|
const sel = [
|
|
@@ -1615,12 +1707,13 @@ var tldrOrFaqRule = defineRule({
|
|
|
1615
1707
|
"[data-tldr]"
|
|
1616
1708
|
].join(", ");
|
|
1617
1709
|
if (ctx.$(sel).length > 0) {
|
|
1618
|
-
return { status: "pass", score: 0.85, rationale: "TL;DR / summary / FAQ region detected by selector." };
|
|
1710
|
+
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." };
|
|
1619
1711
|
}
|
|
1620
1712
|
return {
|
|
1621
1713
|
status: "warn",
|
|
1622
1714
|
score: 0,
|
|
1623
1715
|
rationale: "No TL;DR / summary / FAQ found; add one to boost AI citation odds.",
|
|
1716
|
+
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.",
|
|
1624
1717
|
fixUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md"
|
|
1625
1718
|
};
|
|
1626
1719
|
}
|
|
@@ -1637,6 +1730,7 @@ var wordCountRule = defineRule({
|
|
|
1637
1730
|
effort: "high",
|
|
1638
1731
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#cntword-count",
|
|
1639
1732
|
title: "Page has enough body text",
|
|
1733
|
+
title_ko: "\uCDA9\uBD84\uD55C \uBCF8\uBB38 \uD14D\uC2A4\uD2B8 \uC5EC\uBD80",
|
|
1640
1734
|
description: "Thin pages (under ~100 words) are rarely cited by AI engines. Aim for \u2265300 words of meaningful body copy.",
|
|
1641
1735
|
run(ctx) {
|
|
1642
1736
|
const $ = ctx.$;
|
|
@@ -1644,12 +1738,13 @@ var wordCountRule = defineRule({
|
|
|
1644
1738
|
clone.find("script, style, noscript, nav, header, footer, aside").remove();
|
|
1645
1739
|
const text = clone.text().replace(/\s+/g, " ").trim();
|
|
1646
1740
|
const words = text ? text.split(" ").length : 0;
|
|
1647
|
-
if (words >= 300) return { status: "pass", score: 1, rationale: `${words} words of body text.` };
|
|
1648
|
-
if (words >= 100) return { status: "warn", score: 0.5, rationale: `Only ${words} words; aim for 300
|
|
1741
|
+
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.` };
|
|
1742
|
+
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.` };
|
|
1649
1743
|
return {
|
|
1650
1744
|
status: "fail",
|
|
1651
1745
|
score: 0,
|
|
1652
1746
|
rationale: `Only ${words} words; too thin to be cited.`,
|
|
1747
|
+
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.`,
|
|
1653
1748
|
fixUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md"
|
|
1654
1749
|
};
|
|
1655
1750
|
}
|
|
@@ -1700,11 +1795,12 @@ var qaStructureRule = defineRule({
|
|
|
1700
1795
|
effort: "medium",
|
|
1701
1796
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#cntqa-structure",
|
|
1702
1797
|
title: "Content uses Q&A structure for answer extraction",
|
|
1798
|
+
title_ko: "\uB2F5\uBCC0 \uCD94\uCD9C\uC744 \uC704\uD55C Q&A \uAD6C\uC870 \uC0AC\uC6A9 \uC5EC\uBD80",
|
|
1703
1799
|
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.",
|
|
1704
1800
|
run(ctx) {
|
|
1705
1801
|
for (const node of flattenJsonLd(ctx.jsonLd)) {
|
|
1706
1802
|
if (getTypes(node).includes("FAQPage")) {
|
|
1707
|
-
return { status: "pass", score: 1, rationale: "FAQPage JSON-LD provides explicit Q&A." };
|
|
1803
|
+
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." };
|
|
1708
1804
|
}
|
|
1709
1805
|
}
|
|
1710
1806
|
const questionHeadings = [];
|
|
@@ -1717,6 +1813,7 @@ var qaStructureRule = defineRule({
|
|
|
1717
1813
|
status: "pass",
|
|
1718
1814
|
score: 1,
|
|
1719
1815
|
rationale: `${questionHeadings.length} question-style headings detected.`,
|
|
1816
|
+
rationale_ko: `\uC9C8\uBB38\uD615 \uC81C\uBAA9\uC774 ${questionHeadings.length}\uAC1C \uAC10\uC9C0\uB429\uB2C8\uB2E4.`,
|
|
1720
1817
|
evidence: { headings: questionHeadings.slice(0, 5) }
|
|
1721
1818
|
};
|
|
1722
1819
|
}
|
|
@@ -1725,6 +1822,7 @@ var qaStructureRule = defineRule({
|
|
|
1725
1822
|
status: "warn",
|
|
1726
1823
|
score: 0.6,
|
|
1727
1824
|
rationale: "1 question-style heading. Add a second to strengthen answer extraction.",
|
|
1825
|
+
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.",
|
|
1728
1826
|
evidence: { headings: questionHeadings },
|
|
1729
1827
|
estimatedImpact: 1
|
|
1730
1828
|
};
|
|
@@ -1733,6 +1831,7 @@ var qaStructureRule = defineRule({
|
|
|
1733
1831
|
status: "warn",
|
|
1734
1832
|
score: 0,
|
|
1735
1833
|
rationale: "No question-style H2/H3 headings or FAQPage JSON-LD found.",
|
|
1834
|
+
rationale_ko: "\uC9C8\uBB38\uD615 H2/H3 \uC81C\uBAA9\uC774\uB098 FAQPage JSON-LD\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.",
|
|
1736
1835
|
fixHint: 'Reframe at least 2 H2 headings as questions ("How do I\u2026?", "What is\u2026?") or add FAQPage JSON-LD.',
|
|
1737
1836
|
estimatedImpact: 3
|
|
1738
1837
|
};
|
|
@@ -1750,13 +1849,14 @@ var externalCitationsRule = defineRule({
|
|
|
1750
1849
|
effort: "medium",
|
|
1751
1850
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#cntexternal-citations",
|
|
1752
1851
|
title: "Content cites external sources",
|
|
1852
|
+
title_ko: "\uC678\uBD80 \uCD9C\uCC98 \uC778\uC6A9 \uC5EC\uBD80",
|
|
1753
1853
|
description: "Outbound links to authoritative external sources are an E-E-A-T trust signal. AI engines treat well-cited pages as more credible.",
|
|
1754
1854
|
run(ctx) {
|
|
1755
1855
|
let host;
|
|
1756
1856
|
try {
|
|
1757
1857
|
host = new URL(ctx.finalUrl).hostname.toLowerCase();
|
|
1758
1858
|
} catch {
|
|
1759
|
-
return { status: "skip", score: 0, rationale: "Invalid finalUrl." };
|
|
1859
|
+
return { status: "skip", score: 0, rationale: "Invalid finalUrl.", rationale_ko: "\uC720\uD6A8\uD558\uC9C0 \uC54A\uC740 finalUrl\uC785\uB2C8\uB2E4." };
|
|
1760
1860
|
}
|
|
1761
1861
|
const seen = /* @__PURE__ */ new Set();
|
|
1762
1862
|
ctx.$("main a[href], article a[href], body a[href]").each((_i, el) => {
|
|
@@ -1781,6 +1881,7 @@ var externalCitationsRule = defineRule({
|
|
|
1781
1881
|
status: "pass",
|
|
1782
1882
|
score: 1,
|
|
1783
1883
|
rationale: `${count} distinct external host(s) cited (excluding nofollow).`,
|
|
1884
|
+
rationale_ko: `\uC678\uBD80 \uC0AC\uC774\uD2B8 ${count}\uAC1C\uB97C \uC778\uC6A9\uD569\uB2C8\uB2E4 (nofollow \uC81C\uC678).`,
|
|
1784
1885
|
evidence: { hosts: [...seen].slice(0, 8) }
|
|
1785
1886
|
};
|
|
1786
1887
|
}
|
|
@@ -1789,6 +1890,7 @@ var externalCitationsRule = defineRule({
|
|
|
1789
1890
|
status: "pass",
|
|
1790
1891
|
score: 0.7,
|
|
1791
1892
|
rationale: `${count} external host(s) cited. Aim for \u22653 for stronger E-E-A-T.`,
|
|
1893
|
+
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.`,
|
|
1792
1894
|
evidence: { hosts: [...seen] },
|
|
1793
1895
|
estimatedImpact: 1
|
|
1794
1896
|
};
|
|
@@ -1797,6 +1899,7 @@ var externalCitationsRule = defineRule({
|
|
|
1797
1899
|
status: "warn",
|
|
1798
1900
|
score: 0,
|
|
1799
1901
|
rationale: "No external follow citations found in main content.",
|
|
1902
|
+
rationale_ko: "\uBCF8\uBB38\uC5D0 \uC678\uBD80 \uCD9C\uCC98 \uB9C1\uD06C\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.",
|
|
1800
1903
|
fixHint: "Cite at least one authoritative external source (research paper, official docs, news outlet).",
|
|
1801
1904
|
estimatedImpact: 2
|
|
1802
1905
|
};
|
|
@@ -1814,12 +1917,130 @@ var contentRules = [
|
|
|
1814
1917
|
externalCitationsRule
|
|
1815
1918
|
];
|
|
1816
1919
|
|
|
1920
|
+
// src/rules/aeo/skill-md.ts
|
|
1921
|
+
var aeoSkillMdRule = defineRule({
|
|
1922
|
+
id: "aeo.skill-md",
|
|
1923
|
+
stableId: "aeo.skill-md",
|
|
1924
|
+
category: "aeo",
|
|
1925
|
+
group: "opportunity",
|
|
1926
|
+
weight: 3,
|
|
1927
|
+
impact: "high",
|
|
1928
|
+
effort: "low",
|
|
1929
|
+
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#aeoskill-md",
|
|
1930
|
+
title: "skill.md is present",
|
|
1931
|
+
title_ko: "skill.md \uD30C\uC77C \uC874\uC7AC \uC5EC\uBD80",
|
|
1932
|
+
description: "A /skill.md file describes site capabilities so AI agents know what this site can do for them.",
|
|
1933
|
+
run(ctx) {
|
|
1934
|
+
if (ctx.skillMd !== null) {
|
|
1935
|
+
return {
|
|
1936
|
+
status: "pass",
|
|
1937
|
+
score: 1,
|
|
1938
|
+
rationale: "skill.md found at site root.",
|
|
1939
|
+
rationale_ko: "skill.md\uAC00 \uC0AC\uC774\uD2B8 \uB8E8\uD2B8\uC5D0 \uC874\uC7AC\uD569\uB2C8\uB2E4."
|
|
1940
|
+
};
|
|
1941
|
+
}
|
|
1942
|
+
return {
|
|
1943
|
+
status: "warn",
|
|
1944
|
+
score: 0,
|
|
1945
|
+
rationale: "No /skill.md found. Add one to describe your site capabilities to AI agents.",
|
|
1946
|
+
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.",
|
|
1947
|
+
fixHint: "Create /skill.md listing what services, products, and capabilities this site offers."
|
|
1948
|
+
};
|
|
1949
|
+
}
|
|
1950
|
+
});
|
|
1951
|
+
|
|
1952
|
+
// src/rules/aeo/agent-permissions.ts
|
|
1953
|
+
var aeoAgentPermissionsRule = defineRule({
|
|
1954
|
+
id: "aeo.agent-permissions",
|
|
1955
|
+
stableId: "aeo.agent-permissions",
|
|
1956
|
+
category: "aeo",
|
|
1957
|
+
group: "opportunity",
|
|
1958
|
+
weight: 3,
|
|
1959
|
+
impact: "medium",
|
|
1960
|
+
effort: "low",
|
|
1961
|
+
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#aeoagent-permissions",
|
|
1962
|
+
title: "agent-permissions.json is present",
|
|
1963
|
+
title_ko: "agent-permissions.json \uD30C\uC77C \uC874\uC7AC \uC5EC\uBD80",
|
|
1964
|
+
description: "Declares explicit read/summarize/cite/train permissions for AI agents.",
|
|
1965
|
+
run(ctx) {
|
|
1966
|
+
if (ctx.agentPermissions !== null) {
|
|
1967
|
+
return {
|
|
1968
|
+
status: "pass",
|
|
1969
|
+
score: 1,
|
|
1970
|
+
rationale: "agent-permissions.json found at site root.",
|
|
1971
|
+
rationale_ko: "agent-permissions.json\uC774 \uC0AC\uC774\uD2B8 \uB8E8\uD2B8\uC5D0 \uC874\uC7AC\uD569\uB2C8\uB2E4.",
|
|
1972
|
+
evidence: ctx.agentPermissions
|
|
1973
|
+
};
|
|
1974
|
+
}
|
|
1975
|
+
return {
|
|
1976
|
+
status: "warn",
|
|
1977
|
+
score: 0,
|
|
1978
|
+
rationale: "No /agent-permissions.json found. Add one to declare AI agent access policy.",
|
|
1979
|
+
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.",
|
|
1980
|
+
fixHint: "Create /agent-permissions.json with read, summarize, cite, and train permission flags."
|
|
1981
|
+
};
|
|
1982
|
+
}
|
|
1983
|
+
});
|
|
1984
|
+
|
|
1985
|
+
// src/rules/aeo/token-length.ts
|
|
1986
|
+
var THRESHOLD_OPTIMAL = 15e3;
|
|
1987
|
+
var THRESHOLD_MAX = 25e3;
|
|
1988
|
+
var aeoTokenLengthRule = defineRule({
|
|
1989
|
+
id: "aeo.token-length",
|
|
1990
|
+
stableId: "aeo.token-length",
|
|
1991
|
+
category: "aeo",
|
|
1992
|
+
group: "diagnostic",
|
|
1993
|
+
weight: 4,
|
|
1994
|
+
impact: "medium",
|
|
1995
|
+
effort: "medium",
|
|
1996
|
+
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#aeotoken-length",
|
|
1997
|
+
title: "Content token length within AI agent limits",
|
|
1998
|
+
title_ko: "\uCF58\uD150\uCE20 \uD1A0\uD070 \uC218 AI \uC5D0\uC774\uC804\uD2B8 \uAD8C\uC7A5 \uBC94\uC704",
|
|
1999
|
+
description: "Pages under 15K tokens are optimal for AI agents (per Addy Osmani's AEO guidance).",
|
|
2000
|
+
run(ctx) {
|
|
2001
|
+
const text = ctx.$("body").text();
|
|
2002
|
+
const tokenEstimate = Math.round(text.length / 3);
|
|
2003
|
+
const evidence = { tokenEstimate, thresholds: { optimal: THRESHOLD_OPTIMAL, max: THRESHOLD_MAX } };
|
|
2004
|
+
if (tokenEstimate <= THRESHOLD_OPTIMAL) {
|
|
2005
|
+
return {
|
|
2006
|
+
status: "pass",
|
|
2007
|
+
score: 1,
|
|
2008
|
+
rationale: `Estimated ~${tokenEstimate.toLocaleString()} tokens \u2014 within optimal range.`,
|
|
2009
|
+
rationale_ko: `\uC608\uC0C1 \uD1A0\uD070 \uC218 ~${tokenEstimate.toLocaleString()} \u2014 \uAD8C\uC7A5 \uBC94\uC704(15K) \uC774\uB0B4\uC785\uB2C8\uB2E4.`,
|
|
2010
|
+
evidence
|
|
2011
|
+
};
|
|
2012
|
+
}
|
|
2013
|
+
if (tokenEstimate <= THRESHOLD_MAX) {
|
|
2014
|
+
return {
|
|
2015
|
+
status: "warn",
|
|
2016
|
+
score: 0.5,
|
|
2017
|
+
rationale: `Estimated ~${tokenEstimate.toLocaleString()} tokens \u2014 exceeds 15K recommendation.`,
|
|
2018
|
+
rationale_ko: `\uC608\uC0C1 \uD1A0\uD070 \uC218 ~${tokenEstimate.toLocaleString()} \u2014 15K \uAD8C\uC7A5\uCE58\uB97C \uCD08\uACFC\uD569\uB2C8\uB2E4.`,
|
|
2019
|
+
fixHint: "Consider splitting content into shorter, focused pages.",
|
|
2020
|
+
evidence
|
|
2021
|
+
};
|
|
2022
|
+
}
|
|
2023
|
+
return {
|
|
2024
|
+
status: "fail",
|
|
2025
|
+
score: 0,
|
|
2026
|
+
rationale: `Estimated ~${tokenEstimate.toLocaleString()} tokens \u2014 exceeds 25K agent processing limit.`,
|
|
2027
|
+
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.`,
|
|
2028
|
+
fixHint: "Split this page into multiple focused pages under 15K tokens.",
|
|
2029
|
+
evidence
|
|
2030
|
+
};
|
|
2031
|
+
}
|
|
2032
|
+
});
|
|
2033
|
+
|
|
2034
|
+
// src/rules/aeo/index.ts
|
|
2035
|
+
var aeoRules = [aeoSkillMdRule, aeoAgentPermissionsRule, aeoTokenLengthRule];
|
|
2036
|
+
|
|
1817
2037
|
// src/rules/index.ts
|
|
1818
2038
|
var defaultRules = [
|
|
1819
2039
|
...crawlerRules,
|
|
1820
2040
|
...structuredDataRules,
|
|
1821
2041
|
...citationRules,
|
|
1822
|
-
...contentRules
|
|
2042
|
+
...contentRules,
|
|
2043
|
+
...aeoRules
|
|
1823
2044
|
];
|
|
1824
2045
|
|
|
1825
2046
|
// src/config.ts
|
|
@@ -2015,7 +2236,8 @@ var CATEGORY_LABELS = {
|
|
|
2015
2236
|
crawler: "AI Crawler Access",
|
|
2016
2237
|
"structured-data": "Structured Data",
|
|
2017
2238
|
citation: "Citation Signals",
|
|
2018
|
-
content: "Content Structure"
|
|
2239
|
+
content: "Content Structure",
|
|
2240
|
+
aeo: "AEO Stack"
|
|
2019
2241
|
};
|
|
2020
2242
|
function scoreBadge(score) {
|
|
2021
2243
|
const color = score >= 85 ? "brightgreen" : score >= 60 ? "yellow" : "red";
|
|
@@ -2310,7 +2532,8 @@ var CATEGORY_LABELS2 = {
|
|
|
2310
2532
|
crawler: "AI Crawler Access",
|
|
2311
2533
|
"structured-data": "Structured Data",
|
|
2312
2534
|
citation: "Citation Signals",
|
|
2313
|
-
content: "Content Structure"
|
|
2535
|
+
content: "Content Structure",
|
|
2536
|
+
aeo: "AEO Stack"
|
|
2314
2537
|
};
|
|
2315
2538
|
function colorScore(score) {
|
|
2316
2539
|
if (score >= 85) return kleur.green().bold(`${score}`);
|
|
@@ -2410,7 +2633,8 @@ var CATEGORY_LABELS3 = {
|
|
|
2410
2633
|
crawler: "AI Crawler Access",
|
|
2411
2634
|
"structured-data": "Structured Data",
|
|
2412
2635
|
citation: "Citation Signals",
|
|
2413
|
-
content: "Content Structure"
|
|
2636
|
+
content: "Content Structure",
|
|
2637
|
+
aeo: "AEO Stack"
|
|
2414
2638
|
};
|
|
2415
2639
|
var IMPACT_ORDER = {
|
|
2416
2640
|
critical: 4,
|
|
@@ -2493,13 +2717,15 @@ function partitionResults(report) {
|
|
|
2493
2717
|
crawler: [],
|
|
2494
2718
|
"structured-data": [],
|
|
2495
2719
|
citation: [],
|
|
2496
|
-
content: []
|
|
2720
|
+
content: [],
|
|
2721
|
+
aeo: []
|
|
2497
2722
|
};
|
|
2498
2723
|
const passed = {
|
|
2499
2724
|
crawler: [],
|
|
2500
2725
|
"structured-data": [],
|
|
2501
2726
|
citation: [],
|
|
2502
|
-
content: []
|
|
2727
|
+
content: [],
|
|
2728
|
+
aeo: []
|
|
2503
2729
|
};
|
|
2504
2730
|
for (const cat of Object.keys(report.categories)) {
|
|
2505
2731
|
for (const r of report.categories[cat].results) {
|