geo-checker 0.3.0 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.cjs +297 -71
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +297 -71
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +289 -67
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +6 -1
- package/dist/index.d.ts +6 -1
- package/dist/index.js +289 -67
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -255,10 +255,12 @@ async function buildContext(url, opts = {}) {
|
|
|
255
255
|
"Site appears to be JS-rendered (sparse body + SPA root element). Re-run with --render for accurate results."
|
|
256
256
|
);
|
|
257
257
|
}
|
|
258
|
-
const [robotsRaw, llmsRaw, llmsFullRaw] = await Promise.all([
|
|
258
|
+
const [robotsRaw, llmsRaw, llmsFullRaw, skillMdRaw, agentPermissionsRaw] = await Promise.all([
|
|
259
259
|
fetchText(`${origin}/robots.txt`, opts),
|
|
260
260
|
fetchText(`${origin}/llms.txt`, opts),
|
|
261
|
-
fetchText(`${origin}/llms-full.txt`, opts)
|
|
261
|
+
fetchText(`${origin}/llms-full.txt`, opts),
|
|
262
|
+
fetchText(`${origin}/skill.md`, opts),
|
|
263
|
+
fetchText(`${origin}/agent-permissions.json`, opts)
|
|
262
264
|
]);
|
|
263
265
|
let sitemapUrl = null;
|
|
264
266
|
const robots = robotsRaw ? parseRobots(robotsRaw) : null;
|
|
@@ -266,6 +268,13 @@ async function buildContext(url, opts = {}) {
|
|
|
266
268
|
if (!sitemapUrl) sitemapUrl = `${origin}/sitemap.xml`;
|
|
267
269
|
const sitemapRaw = await fetchText(sitemapUrl, opts);
|
|
268
270
|
const sitemap = sitemapRaw ? parseSitemap(sitemapRaw) : null;
|
|
271
|
+
let agentPermissions = null;
|
|
272
|
+
if (agentPermissionsRaw && agentPermissionsRaw.trim().length > 0) {
|
|
273
|
+
try {
|
|
274
|
+
agentPermissions = JSON.parse(agentPermissionsRaw);
|
|
275
|
+
} catch {
|
|
276
|
+
}
|
|
277
|
+
}
|
|
269
278
|
return {
|
|
270
279
|
url,
|
|
271
280
|
finalUrl,
|
|
@@ -280,7 +289,9 @@ async function buildContext(url, opts = {}) {
|
|
|
280
289
|
jsonLd: extractJsonLd($),
|
|
281
290
|
renderMode,
|
|
282
291
|
fetchedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
283
|
-
warnings
|
|
292
|
+
warnings,
|
|
293
|
+
skillMd: skillMdRaw && skillMdRaw.trim().length > 0 ? skillMdRaw : null,
|
|
294
|
+
agentPermissions
|
|
284
295
|
};
|
|
285
296
|
}
|
|
286
297
|
|
|
@@ -289,10 +300,11 @@ function defineRule(rule) {
|
|
|
289
300
|
return rule;
|
|
290
301
|
}
|
|
291
302
|
var CATEGORY_WEIGHTS = {
|
|
292
|
-
crawler:
|
|
293
|
-
"structured-data":
|
|
294
|
-
citation:
|
|
295
|
-
content:
|
|
303
|
+
crawler: 20,
|
|
304
|
+
"structured-data": 25,
|
|
305
|
+
citation: 20,
|
|
306
|
+
content: 15,
|
|
307
|
+
aeo: 20
|
|
296
308
|
};
|
|
297
309
|
|
|
298
310
|
// src/engine.ts
|
|
@@ -306,7 +318,8 @@ async function runRules(ctx, rules, opts = {}) {
|
|
|
306
318
|
crawler: { score: 0, weight: weights.crawler, results: [] },
|
|
307
319
|
"structured-data": { score: 0, weight: weights["structured-data"], results: [] },
|
|
308
320
|
citation: { score: 0, weight: weights.citation, results: [] },
|
|
309
|
-
content: { score: 0, weight: weights.content, results: [] }
|
|
321
|
+
content: { score: 0, weight: weights.content, results: [] },
|
|
322
|
+
aeo: { score: 0, weight: weights.aeo, results: [] }
|
|
310
323
|
};
|
|
311
324
|
for (const rule of rules) {
|
|
312
325
|
if (onlySet && !onlySet.has(rule.id) && (!rule.stableId || !onlySet.has(rule.stableId))) continue;
|
|
@@ -331,6 +344,7 @@ async function runRules(ctx, rules, opts = {}) {
|
|
|
331
344
|
durationMs
|
|
332
345
|
};
|
|
333
346
|
if (rule.stableId !== void 0) entry.stableId = rule.stableId;
|
|
347
|
+
if (rule.title_ko !== void 0) entry.title_ko = rule.title_ko;
|
|
334
348
|
if (rule.group !== void 0) entry.group = rule.group;
|
|
335
349
|
if (rule.impact !== void 0) entry.impact = rule.impact;
|
|
336
350
|
if (rule.effort !== void 0) entry.effort = rule.effort;
|
|
@@ -393,13 +407,15 @@ var httpsRule = defineRule({
|
|
|
393
407
|
effort: "medium",
|
|
394
408
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#crawlerhttps",
|
|
395
409
|
title: "Site is served over HTTPS",
|
|
410
|
+
title_ko: "\uC0AC\uC774\uD2B8\uAC00 HTTPS\uB85C \uC81C\uACF5\uB428",
|
|
396
411
|
description: "AI crawlers treat HTTPS pages as more trustworthy and some skip plain HTTP entirely.",
|
|
397
412
|
run(ctx) {
|
|
398
413
|
const isHttps = ctx.finalUrl.startsWith("https://");
|
|
399
|
-
return isHttps ? { status: "pass", score: 1, rationale: "Final URL uses HTTPS." } : {
|
|
414
|
+
return isHttps ? { status: "pass", score: 1, rationale: "Final URL uses HTTPS.", rationale_ko: "\uCD5C\uC885 URL\uC774 HTTPS\uB97C \uC0AC\uC6A9\uD569\uB2C8\uB2E4." } : {
|
|
400
415
|
status: "fail",
|
|
401
416
|
score: 0,
|
|
402
417
|
rationale: "Final URL does not use HTTPS. Redirect HTTP \u2192 HTTPS site-wide.",
|
|
418
|
+
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.",
|
|
403
419
|
fixUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md"
|
|
404
420
|
};
|
|
405
421
|
}
|
|
@@ -416,15 +432,17 @@ var robotsReachableRule = defineRule({
|
|
|
416
432
|
effort: "low",
|
|
417
433
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#crawlerrobots-reachable",
|
|
418
434
|
title: "robots.txt is reachable",
|
|
435
|
+
title_ko: "robots.txt \uC811\uADFC \uAC00\uB2A5 \uC5EC\uBD80",
|
|
419
436
|
description: "A reachable robots.txt lets crawlers confirm their permissions; missing file is treated as allow-all but blocks explicit signalling.",
|
|
420
437
|
run(ctx) {
|
|
421
438
|
if (ctx.robots) {
|
|
422
|
-
return { status: "pass", score: 1, rationale: "robots.txt returned successfully." };
|
|
439
|
+
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." };
|
|
423
440
|
}
|
|
424
441
|
return {
|
|
425
442
|
status: "warn",
|
|
426
443
|
score: 0.3,
|
|
427
444
|
rationale: "robots.txt is missing. Add one even if empty to explicitly signal crawl policy.",
|
|
445
|
+
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.",
|
|
428
446
|
fixUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md"
|
|
429
447
|
};
|
|
430
448
|
}
|
|
@@ -472,13 +490,15 @@ var robotsAiAllowRule = defineRule({
|
|
|
472
490
|
effort: "low",
|
|
473
491
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#crawlerrobots-ai-allow",
|
|
474
492
|
title: "AI crawlers are allowed",
|
|
493
|
+
title_ko: "AI \uD06C\uB864\uB7EC \uC811\uADFC \uD5C8\uC6A9 \uC5EC\uBD80",
|
|
475
494
|
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.",
|
|
476
495
|
run(ctx) {
|
|
477
496
|
if (!ctx.robots) {
|
|
478
497
|
return {
|
|
479
498
|
status: "warn",
|
|
480
499
|
score: 0.5,
|
|
481
|
-
rationale: "robots.txt missing; AI crawlers default to allow, but explicit allow is recommended."
|
|
500
|
+
rationale: "robots.txt missing; AI crawlers default to allow, but explicit allow is recommended.",
|
|
501
|
+
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."
|
|
482
502
|
};
|
|
483
503
|
}
|
|
484
504
|
const path = new URL(ctx.finalUrl).pathname || "/";
|
|
@@ -496,6 +516,7 @@ var robotsAiAllowRule = defineRule({
|
|
|
496
516
|
status: "fail",
|
|
497
517
|
score: Math.max(0, 1 - blocked.length / AI_BOTS.length),
|
|
498
518
|
rationale: `Blocked: ${blocked.join(", ")}. Remove the Disallow or add an explicit Allow for these user-agents.`,
|
|
519
|
+
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.`,
|
|
499
520
|
evidence: { blocked, mentioned, totalBots: AI_BOTS.length },
|
|
500
521
|
fixUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md"
|
|
501
522
|
};
|
|
@@ -504,13 +525,15 @@ var robotsAiAllowRule = defineRule({
|
|
|
504
525
|
return {
|
|
505
526
|
status: "warn",
|
|
506
527
|
score: 0.6,
|
|
507
|
-
rationale: `All ${AI_BOTS.length} AI crawlers reach the page via default rules, but none are explicitly listed. Consider explicit Allow entries
|
|
528
|
+
rationale: `All ${AI_BOTS.length} AI crawlers reach the page via default rules, but none are explicitly listed. Consider explicit Allow entries.`,
|
|
529
|
+
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.`
|
|
508
530
|
};
|
|
509
531
|
}
|
|
510
532
|
return {
|
|
511
533
|
status: "pass",
|
|
512
534
|
score: 1,
|
|
513
535
|
rationale: `All ${AI_BOTS.length} AI crawlers can reach the page; ${mentioned.length} explicitly listed.`,
|
|
536
|
+
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.`,
|
|
514
537
|
evidence: { mentioned, totalBots: AI_BOTS.length }
|
|
515
538
|
};
|
|
516
539
|
}
|
|
@@ -527,15 +550,17 @@ var llmsTxtPresentRule = defineRule({
|
|
|
527
550
|
effort: "medium",
|
|
528
551
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#crawlerllms-txt-present",
|
|
529
552
|
title: "llms.txt is present",
|
|
553
|
+
title_ko: "llms.txt \uD30C\uC77C \uC874\uC7AC \uC5EC\uBD80",
|
|
530
554
|
description: "An /llms.txt file at the site root gives AI assistants a curated map of the most citation-worthy pages.",
|
|
531
555
|
run(ctx) {
|
|
532
556
|
if (ctx.llmsTxt) {
|
|
533
|
-
return { status: "pass", score: 1, rationale: "llms.txt found at site root." };
|
|
557
|
+
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." };
|
|
534
558
|
}
|
|
535
559
|
return {
|
|
536
560
|
status: "warn",
|
|
537
561
|
score: 0,
|
|
538
562
|
rationale: "No /llms.txt found. Add one to curate the pages AI assistants should read.",
|
|
563
|
+
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.",
|
|
539
564
|
fixUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md"
|
|
540
565
|
};
|
|
541
566
|
}
|
|
@@ -552,10 +577,11 @@ var llmsTxtWellformedRule = defineRule({
|
|
|
552
577
|
effort: "low",
|
|
553
578
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#crawlerllms-txt-wellformed",
|
|
554
579
|
title: "llms.txt follows the spec",
|
|
580
|
+
title_ko: "llms.txt \uC2A4\uD399 \uC900\uC218 \uC5EC\uBD80",
|
|
555
581
|
description: "Must start with an H1 project title, then a brief summary, then at least one H2 section containing link items.",
|
|
556
582
|
run(ctx) {
|
|
557
583
|
if (!ctx.llmsTxt) {
|
|
558
|
-
return { status: "skip", score: 0, rationale: "No llms.txt to validate." };
|
|
584
|
+
return { status: "skip", score: 0, rationale: "No llms.txt to validate.", rationale_ko: "\uAC80\uC99D\uD560 llms.txt\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4." };
|
|
559
585
|
}
|
|
560
586
|
const check = isLlmsTxtWellFormed(ctx.llmsTxt);
|
|
561
587
|
if (check.ok) {
|
|
@@ -563,13 +589,15 @@ var llmsTxtWellformedRule = defineRule({
|
|
|
563
589
|
return {
|
|
564
590
|
status: "pass",
|
|
565
591
|
score: 1,
|
|
566
|
-
rationale: `Well-formed with ${ctx.llmsTxt.sections.length} section(s) and ${totalLinks} link(s)
|
|
592
|
+
rationale: `Well-formed with ${ctx.llmsTxt.sections.length} section(s) and ${totalLinks} link(s).`,
|
|
593
|
+
rationale_ko: `\uC2A4\uD399\uC5D0 \uB9DE\uAC8C \uC791\uC131\uB428 (\uC139\uC158 ${ctx.llmsTxt.sections.length}\uAC1C, \uB9C1\uD06C ${totalLinks}\uAC1C).`
|
|
567
594
|
};
|
|
568
595
|
}
|
|
569
596
|
return {
|
|
570
597
|
status: "warn",
|
|
571
598
|
score: 0.3,
|
|
572
599
|
rationale: `llms.txt does not fully match the spec: ${check.reason}.`,
|
|
600
|
+
rationale_ko: `llms.txt\uAC00 \uC2A4\uD399\uC744 \uC644\uC804\uD788 \uB530\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4: ${check.reason}.`,
|
|
573
601
|
fixUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md"
|
|
574
602
|
};
|
|
575
603
|
}
|
|
@@ -586,13 +614,15 @@ var llmsFullTxtRule = defineRule({
|
|
|
586
614
|
effort: "medium",
|
|
587
615
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#crawlerllms-full-txt",
|
|
588
616
|
title: "llms-full.txt provides full-content mirror",
|
|
617
|
+
title_ko: "llms-full.txt \uC804\uCCB4 \uCF58\uD150\uCE20 \uBBF8\uB7EC \uC81C\uACF5 \uC5EC\uBD80",
|
|
589
618
|
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.",
|
|
590
619
|
run(ctx) {
|
|
591
620
|
if (ctx.llmsFullTxt && ctx.llmsFullTxt.length > 200) {
|
|
592
621
|
return {
|
|
593
622
|
status: "pass",
|
|
594
623
|
score: 1,
|
|
595
|
-
rationale: `/llms-full.txt found (${ctx.llmsFullTxt.length.toLocaleString()} chars)
|
|
624
|
+
rationale: `/llms-full.txt found (${ctx.llmsFullTxt.length.toLocaleString()} chars).`,
|
|
625
|
+
rationale_ko: `/llms-full.txt\uAC00 \uC874\uC7AC\uD569\uB2C8\uB2E4 (${ctx.llmsFullTxt.length.toLocaleString()}\uC790).`
|
|
596
626
|
};
|
|
597
627
|
}
|
|
598
628
|
if (ctx.llmsFullTxt) {
|
|
@@ -600,6 +630,7 @@ var llmsFullTxtRule = defineRule({
|
|
|
600
630
|
status: "warn",
|
|
601
631
|
score: 0.5,
|
|
602
632
|
rationale: `/llms-full.txt found but very short (${ctx.llmsFullTxt.length} chars). Consider expanding with page bodies.`,
|
|
633
|
+
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.`,
|
|
603
634
|
fixHint: "Mirror full article bodies into /llms-full.txt so AI assistants can quote without re-crawling."
|
|
604
635
|
};
|
|
605
636
|
}
|
|
@@ -607,6 +638,7 @@ var llmsFullTxtRule = defineRule({
|
|
|
607
638
|
status: "warn",
|
|
608
639
|
score: 0,
|
|
609
640
|
rationale: "No /llms-full.txt found. Adding one lets AI assistants ingest the full corpus in a single request.",
|
|
641
|
+
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.",
|
|
610
642
|
fixHint: "Publish /llms-full.txt alongside /llms.txt with the full body text of your top pages.",
|
|
611
643
|
estimatedImpact: 1
|
|
612
644
|
};
|
|
@@ -624,19 +656,22 @@ var sitemapPresentRule = defineRule({
|
|
|
624
656
|
effort: "low",
|
|
625
657
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#crawlersitemap-present",
|
|
626
658
|
title: "sitemap.xml is present",
|
|
659
|
+
title_ko: "sitemap.xml \uC874\uC7AC \uC5EC\uBD80",
|
|
627
660
|
description: "A sitemap helps AI crawlers discover and prioritise pages; many crawlers short-circuit discovery without one.",
|
|
628
661
|
run(ctx) {
|
|
629
662
|
if (ctx.sitemap && ctx.sitemap.urls.length > 0) {
|
|
630
663
|
return {
|
|
631
664
|
status: "pass",
|
|
632
665
|
score: 1,
|
|
633
|
-
rationale: `Sitemap found with ${ctx.sitemap.urls.length} URL(s)
|
|
666
|
+
rationale: `Sitemap found with ${ctx.sitemap.urls.length} URL(s).`,
|
|
667
|
+
rationale_ko: `\uC0AC\uC774\uD2B8\uB9F5\uC5D0 URL\uC774 ${ctx.sitemap.urls.length}\uAC1C \uC788\uC2B5\uB2C8\uB2E4.`
|
|
634
668
|
};
|
|
635
669
|
}
|
|
636
670
|
return {
|
|
637
671
|
status: "warn",
|
|
638
672
|
score: 0.2,
|
|
639
673
|
rationale: "No sitemap.xml found (checked /sitemap.xml and Sitemap: directive in robots.txt).",
|
|
674
|
+
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).",
|
|
640
675
|
fixUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md"
|
|
641
676
|
};
|
|
642
677
|
}
|
|
@@ -664,15 +699,17 @@ var jsonLdPresentRule = defineRule({
|
|
|
664
699
|
effort: "medium",
|
|
665
700
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#sdjsonld-present",
|
|
666
701
|
title: "JSON-LD structured data is present",
|
|
702
|
+
title_ko: "JSON-LD \uAD6C\uC870\uD654 \uB370\uC774\uD130 \uC874\uC7AC \uC5EC\uBD80",
|
|
667
703
|
description: 'At least one <script type="application/ld+json"> block is the primary way AI engines map your page to an entity.',
|
|
668
704
|
run(ctx) {
|
|
669
705
|
if (ctx.jsonLd.length > 0) {
|
|
670
|
-
return { status: "pass", score: 1, rationale: `Found ${ctx.jsonLd.length} JSON-LD block(s).` };
|
|
706
|
+
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.` };
|
|
671
707
|
}
|
|
672
708
|
return {
|
|
673
709
|
status: "fail",
|
|
674
710
|
score: 0,
|
|
675
711
|
rationale: "No JSON-LD blocks found. Add schema.org structured data.",
|
|
712
|
+
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.",
|
|
676
713
|
fixUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md"
|
|
677
714
|
};
|
|
678
715
|
}
|
|
@@ -748,20 +785,22 @@ var jsonLdValidJsonRule = defineRule({
|
|
|
748
785
|
effort: "low",
|
|
749
786
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#sdjsonld-valid-json",
|
|
750
787
|
title: "JSON-LD blocks parse as valid JSON",
|
|
788
|
+
title_ko: "JSON-LD \uBE14\uB85D\uC758 JSON \uC720\uD6A8\uC131",
|
|
751
789
|
description: "Malformed JSON in an ld+json block is silently ignored by most consumers \u2014 a costly silent failure.",
|
|
752
790
|
run(ctx) {
|
|
753
791
|
if (ctx.jsonLd.length === 0) {
|
|
754
|
-
return { status: "skip", score: 0, rationale: "No JSON-LD to validate." };
|
|
792
|
+
return { status: "skip", score: 0, rationale: "No JSON-LD to validate.", rationale_ko: "\uAC80\uC99D\uD560 JSON-LD\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4." };
|
|
755
793
|
}
|
|
756
794
|
if (hasParseError(ctx.jsonLd)) {
|
|
757
795
|
return {
|
|
758
796
|
status: "fail",
|
|
759
797
|
score: 0,
|
|
760
798
|
rationale: "One or more JSON-LD blocks failed to parse.",
|
|
799
|
+
rationale_ko: "JSON-LD \uBE14\uB85D \uD558\uB098 \uC774\uC0C1\uC744 \uD30C\uC2F1\uD558\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4.",
|
|
761
800
|
fixUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md"
|
|
762
801
|
};
|
|
763
802
|
}
|
|
764
|
-
return { status: "pass", score: 1, rationale: "All JSON-LD blocks parse cleanly." };
|
|
803
|
+
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." };
|
|
765
804
|
}
|
|
766
805
|
});
|
|
767
806
|
|
|
@@ -776,10 +815,11 @@ var schemaTypeRecognizedRule = defineRule({
|
|
|
776
815
|
effort: "low",
|
|
777
816
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#sdschema-type-recognized",
|
|
778
817
|
title: "Schema.org @type is a recognised kind",
|
|
818
|
+
title_ko: "Schema.org @type \uC778\uC2DD \uAC00\uB2A5 \uC5EC\uBD80",
|
|
779
819
|
description: "AI engines match pages against well-known types (Article, Product, FAQPage...). Obscure types weaken the signal.",
|
|
780
820
|
run(ctx) {
|
|
781
821
|
if (ctx.jsonLd.length === 0) {
|
|
782
|
-
return { status: "skip", score: 0, rationale: "No JSON-LD to analyse." };
|
|
822
|
+
return { status: "skip", score: 0, rationale: "No JSON-LD to analyse.", rationale_ko: "\uBD84\uC11D\uD560 JSON-LD\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4." };
|
|
783
823
|
}
|
|
784
824
|
const nodes = flattenJsonLd(ctx.jsonLd);
|
|
785
825
|
const recognized = /* @__PURE__ */ new Set();
|
|
@@ -795,6 +835,7 @@ var schemaTypeRecognizedRule = defineRule({
|
|
|
795
835
|
status: "pass",
|
|
796
836
|
score: 1,
|
|
797
837
|
rationale: `Recognised: ${[...recognized].join(", ")}.`,
|
|
838
|
+
rationale_ko: `\uC778\uC2DD\uB41C \uD0C0\uC785: ${[...recognized].join(", ")}.`,
|
|
798
839
|
evidence: { recognized: [...recognized], all: [...seenTypes] }
|
|
799
840
|
};
|
|
800
841
|
}
|
|
@@ -802,6 +843,7 @@ var schemaTypeRecognizedRule = defineRule({
|
|
|
802
843
|
status: "warn",
|
|
803
844
|
score: 0.3,
|
|
804
845
|
rationale: `No recognised schema.org types. Saw: ${[...seenTypes].join(", ") || "(none)"}.`,
|
|
846
|
+
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)"}.`,
|
|
805
847
|
fixUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md"
|
|
806
848
|
};
|
|
807
849
|
}
|
|
@@ -818,10 +860,11 @@ var requiredFieldsRule = defineRule({
|
|
|
818
860
|
effort: "medium",
|
|
819
861
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#sdrequired-fields",
|
|
820
862
|
title: "Required fields for recognised types are set",
|
|
863
|
+
title_ko: "\uC778\uC2DD\uB41C \uD0C0\uC785\uC758 \uD544\uC218 \uD544\uB4DC \uCDA9\uC871 \uC5EC\uBD80",
|
|
821
864
|
description: "Article needs headline/author/datePublished, FAQPage needs mainEntity, Product needs offers, etc.",
|
|
822
865
|
run(ctx) {
|
|
823
866
|
if (ctx.jsonLd.length === 0) {
|
|
824
|
-
return { status: "skip", score: 0, rationale: "No JSON-LD to analyse." };
|
|
867
|
+
return { status: "skip", score: 0, rationale: "No JSON-LD to analyse.", rationale_ko: "\uBD84\uC11D\uD560 JSON-LD\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4." };
|
|
825
868
|
}
|
|
826
869
|
const nodes = flattenJsonLd(ctx.jsonLd);
|
|
827
870
|
const missing = [];
|
|
@@ -840,14 +883,16 @@ var requiredFieldsRule = defineRule({
|
|
|
840
883
|
return {
|
|
841
884
|
status: "skip",
|
|
842
885
|
score: 0,
|
|
843
|
-
rationale: "No types with known required fields were found."
|
|
886
|
+
rationale: "No types with known required fields were found.",
|
|
887
|
+
rationale_ko: "\uD544\uC218 \uD544\uB4DC\uAC00 \uC815\uC758\uB41C \uD0C0\uC785\uC774 \uC5C6\uC2B5\uB2C8\uB2E4."
|
|
844
888
|
};
|
|
845
889
|
}
|
|
846
890
|
if (missing.length === 0) {
|
|
847
891
|
return {
|
|
848
892
|
status: "pass",
|
|
849
893
|
score: 1,
|
|
850
|
-
rationale: `Required fields set on ${checked.length} node(s)
|
|
894
|
+
rationale: `Required fields set on ${checked.length} node(s).`,
|
|
895
|
+
rationale_ko: `${checked.length}\uAC1C \uB178\uB4DC\uC758 \uD544\uC218 \uD544\uB4DC\uAC00 \uBAA8\uB450 \uCDA9\uC871\uB429\uB2C8\uB2E4.`
|
|
851
896
|
};
|
|
852
897
|
}
|
|
853
898
|
const msg = missing.map((m) => `${m.type}.${m.field}`).join(", ");
|
|
@@ -855,6 +900,7 @@ var requiredFieldsRule = defineRule({
|
|
|
855
900
|
status: "fail",
|
|
856
901
|
score: Math.max(0, 1 - missing.length / (checked.length * 2)),
|
|
857
902
|
rationale: `Missing required fields: ${msg}.`,
|
|
903
|
+
rationale_ko: `\uB204\uB77D\uB41C \uD544\uC218 \uD544\uB4DC: ${msg}.`,
|
|
858
904
|
evidence: missing,
|
|
859
905
|
fixUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md"
|
|
860
906
|
};
|
|
@@ -872,10 +918,11 @@ var microdataFallbackRule = defineRule({
|
|
|
872
918
|
effort: "medium",
|
|
873
919
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#sdmicrodata-fallback",
|
|
874
920
|
title: "Microdata or RDFa fallback when JSON-LD is missing",
|
|
921
|
+
title_ko: "JSON-LD \uC5C6\uC744 \uB54C Microdata/RDFa \uB300\uCCB4 \uC5EC\uBD80",
|
|
875
922
|
description: "If JSON-LD is absent, inline microdata (itemscope/itemtype) or RDFa still gives some structured signal.",
|
|
876
923
|
run(ctx) {
|
|
877
924
|
if (ctx.jsonLd.length > 0) {
|
|
878
|
-
return { status: "skip", score: 0, rationale: "JSON-LD is present; fallback not needed." };
|
|
925
|
+
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." };
|
|
879
926
|
}
|
|
880
927
|
const microdata = ctx.$("[itemscope][itemtype]").length;
|
|
881
928
|
const rdfa = ctx.$("[typeof][vocab], [typeof][property]").length;
|
|
@@ -883,13 +930,15 @@ var microdataFallbackRule = defineRule({
|
|
|
883
930
|
return {
|
|
884
931
|
status: "pass",
|
|
885
932
|
score: 1,
|
|
886
|
-
rationale: `Found ${microdata} microdata and ${rdfa} RDFa nodes
|
|
933
|
+
rationale: `Found ${microdata} microdata and ${rdfa} RDFa nodes.`,
|
|
934
|
+
rationale_ko: `Microdata ${microdata}\uAC1C, RDFa ${rdfa}\uAC1C \uBC1C\uACAC\uB429\uB2C8\uB2E4.`
|
|
887
935
|
};
|
|
888
936
|
}
|
|
889
937
|
return {
|
|
890
938
|
status: "fail",
|
|
891
939
|
score: 0,
|
|
892
940
|
rationale: "No structured data at all (no JSON-LD, no microdata, no RDFa).",
|
|
941
|
+
rationale_ko: "\uAD6C\uC870\uD654 \uB370\uC774\uD130\uAC00 \uC804\uD600 \uC5C6\uC2B5\uB2C8\uB2E4 (JSON-LD, Microdata, RDFa \uBAA8\uB450 \uC5C6\uC74C).",
|
|
893
942
|
fixUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md"
|
|
894
943
|
};
|
|
895
944
|
}
|
|
@@ -907,10 +956,11 @@ var noDuplicateTypesRule = defineRule({
|
|
|
907
956
|
effort: "low",
|
|
908
957
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#sdno-duplicate-types",
|
|
909
958
|
title: "No conflicting duplicate @types",
|
|
959
|
+
title_ko: "@type \uC911\uBCF5 \uCDA9\uB3CC \uC5C6\uC74C",
|
|
910
960
|
description: "Multiple competing entities of the same primary type (e.g. two Articles) confuse the engine about which one represents the page.",
|
|
911
961
|
run(ctx) {
|
|
912
962
|
if (ctx.jsonLd.length === 0) {
|
|
913
|
-
return { status: "skip", score: 0, rationale: "No JSON-LD to analyse." };
|
|
963
|
+
return { status: "skip", score: 0, rationale: "No JSON-LD to analyse.", rationale_ko: "\uBD84\uC11D\uD560 JSON-LD\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4." };
|
|
914
964
|
}
|
|
915
965
|
const counts = /* @__PURE__ */ new Map();
|
|
916
966
|
for (const node of flattenJsonLd(ctx.jsonLd)) {
|
|
@@ -920,12 +970,13 @@ var noDuplicateTypesRule = defineRule({
|
|
|
920
970
|
}
|
|
921
971
|
const dupes = [...counts.entries()].filter(([, n]) => n > 1);
|
|
922
972
|
if (dupes.length === 0) {
|
|
923
|
-
return { status: "pass", score: 1, rationale: "No duplicate primary types." };
|
|
973
|
+
return { status: "pass", score: 1, rationale: "No duplicate primary types.", rationale_ko: "\uC911\uBCF5\uB41C \uAE30\uBCF8 \uD0C0\uC785\uC774 \uC5C6\uC2B5\uB2C8\uB2E4." };
|
|
924
974
|
}
|
|
925
975
|
return {
|
|
926
976
|
status: "warn",
|
|
927
977
|
score: 0.4,
|
|
928
978
|
rationale: `Duplicate primary types: ${dupes.map(([t, n]) => `${t}\xD7${n}`).join(", ")}.`,
|
|
979
|
+
rationale_ko: `\uC911\uBCF5\uB41C \uAE30\uBCF8 \uD0C0\uC785: ${dupes.map(([t, n]) => `${t}\xD7${n}`).join(", ")}.`,
|
|
929
980
|
fixUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md"
|
|
930
981
|
};
|
|
931
982
|
}
|
|
@@ -973,10 +1024,11 @@ var sameAsEntityRule = defineRule({
|
|
|
973
1024
|
effort: "medium",
|
|
974
1025
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#sdsameas-entity",
|
|
975
1026
|
title: "Entity nodes link the knowledge graph via sameAs",
|
|
1027
|
+
title_ko: "sameAs\uB85C \uC9C0\uC2DD \uADF8\uB798\uD504 \uC5F0\uACB0 \uC5EC\uBD80",
|
|
976
1028
|
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).",
|
|
977
1029
|
run(ctx) {
|
|
978
1030
|
if (ctx.jsonLd.length === 0) {
|
|
979
|
-
return { status: "skip", score: 0, rationale: "No JSON-LD to analyse." };
|
|
1031
|
+
return { status: "skip", score: 0, rationale: "No JSON-LD to analyse.", rationale_ko: "\uBD84\uC11D\uD560 JSON-LD\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4." };
|
|
980
1032
|
}
|
|
981
1033
|
const nodes = flattenJsonLd(ctx.jsonLd);
|
|
982
1034
|
const entities = nodes.filter((n) => getTypes(n).some((t) => ENTITY_TYPES.includes(t)));
|
|
@@ -984,7 +1036,8 @@ var sameAsEntityRule = defineRule({
|
|
|
984
1036
|
return {
|
|
985
1037
|
status: "skip",
|
|
986
1038
|
score: 0,
|
|
987
|
-
rationale: "No Organization/Person/LocalBusiness/Brand entity to link."
|
|
1039
|
+
rationale: "No Organization/Person/LocalBusiness/Brand entity to link.",
|
|
1040
|
+
rationale_ko: "\uC5F0\uACB0\uD560 Organization/Person/LocalBusiness/Brand \uC5D4\uD2F0\uD2F0\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4."
|
|
988
1041
|
};
|
|
989
1042
|
}
|
|
990
1043
|
let bestScore = 0;
|
|
@@ -1008,6 +1061,7 @@ var sameAsEntityRule = defineRule({
|
|
|
1008
1061
|
status: "pass",
|
|
1009
1062
|
score: 1,
|
|
1010
1063
|
rationale: `Entity links ${bestEvidence.trusted} trusted knowledge-graph hosts via sameAs.`,
|
|
1064
|
+
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.`,
|
|
1011
1065
|
evidence: bestEvidence
|
|
1012
1066
|
};
|
|
1013
1067
|
}
|
|
@@ -1016,6 +1070,7 @@ var sameAsEntityRule = defineRule({
|
|
|
1016
1070
|
status: "pass",
|
|
1017
1071
|
score: bestScore,
|
|
1018
1072
|
rationale: `Entity has 1 trusted sameAs link. Add Wikipedia/Wikidata for stronger E-E-A-T.`,
|
|
1073
|
+
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.",
|
|
1019
1074
|
evidence: bestEvidence,
|
|
1020
1075
|
estimatedImpact: 1
|
|
1021
1076
|
};
|
|
@@ -1025,6 +1080,7 @@ var sameAsEntityRule = defineRule({
|
|
|
1025
1080
|
status: "warn",
|
|
1026
1081
|
score: bestScore,
|
|
1027
1082
|
rationale: "Entity declares sameAs but no trusted knowledge-graph hosts (Wikipedia/Wikidata/LinkedIn).",
|
|
1083
|
+
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.",
|
|
1028
1084
|
evidence: bestEvidence,
|
|
1029
1085
|
fixHint: "Add Wikipedia/Wikidata/LinkedIn URLs to your Organization sameAs[].",
|
|
1030
1086
|
estimatedImpact: 2
|
|
@@ -1034,6 +1090,7 @@ var sameAsEntityRule = defineRule({
|
|
|
1034
1090
|
status: "warn",
|
|
1035
1091
|
score: 0,
|
|
1036
1092
|
rationale: `${entities.length} entity node(s) found but none declare sameAs links.`,
|
|
1093
|
+
rationale_ko: `\uC5D4\uD2F0\uD2F0 \uB178\uB4DC\uAC00 ${entities.length}\uAC1C \uC788\uC9C0\uB9CC sameAs \uB9C1\uD06C\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.`,
|
|
1037
1094
|
fixHint: 'Add sameAs:["https://en.wikipedia.org/wiki/...", "https://www.linkedin.com/company/..."] to your Organization JSON-LD.',
|
|
1038
1095
|
estimatedImpact: 3
|
|
1039
1096
|
};
|
|
@@ -1065,15 +1122,16 @@ var breadcrumbValidRule = defineRule({
|
|
|
1065
1122
|
effort: "medium",
|
|
1066
1123
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#sdbreadcrumb-valid",
|
|
1067
1124
|
title: "BreadcrumbList items declare position, name, and item",
|
|
1125
|
+
title_ko: "BreadcrumbList \uD56D\uBAA9\uC758 \uD544\uC218 \uD544\uB4DC \uCDA9\uC871 \uC5EC\uBD80",
|
|
1068
1126
|
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.",
|
|
1069
1127
|
run(ctx) {
|
|
1070
1128
|
if (ctx.jsonLd.length === 0) {
|
|
1071
|
-
return { status: "skip", score: 0, rationale: "No JSON-LD to analyse." };
|
|
1129
|
+
return { status: "skip", score: 0, rationale: "No JSON-LD to analyse.", rationale_ko: "\uBD84\uC11D\uD560 JSON-LD\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4." };
|
|
1072
1130
|
}
|
|
1073
1131
|
const nodes = flattenJsonLd(ctx.jsonLd);
|
|
1074
1132
|
const breadcrumbs = nodes.filter((n) => getTypes(n).includes("BreadcrumbList"));
|
|
1075
1133
|
if (breadcrumbs.length === 0) {
|
|
1076
|
-
return { status: "skip", score: 0, rationale: "No BreadcrumbList present." };
|
|
1134
|
+
return { status: "skip", score: 0, rationale: "No BreadcrumbList present.", rationale_ko: "BreadcrumbList\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4." };
|
|
1077
1135
|
}
|
|
1078
1136
|
const allIssues = [];
|
|
1079
1137
|
let totalItems = 0;
|
|
@@ -1086,7 +1144,8 @@ var breadcrumbValidRule = defineRule({
|
|
|
1086
1144
|
return {
|
|
1087
1145
|
status: "pass",
|
|
1088
1146
|
score: 1,
|
|
1089
|
-
rationale: `BreadcrumbList(s) valid (${totalItems} items)
|
|
1147
|
+
rationale: `BreadcrumbList(s) valid (${totalItems} items).`,
|
|
1148
|
+
rationale_ko: `BreadcrumbList\uAC00 \uC720\uD6A8\uD569\uB2C8\uB2E4 (\uD56D\uBAA9 ${totalItems}\uAC1C).`
|
|
1090
1149
|
};
|
|
1091
1150
|
}
|
|
1092
1151
|
const fatalCount = allIssues.length;
|
|
@@ -1096,6 +1155,7 @@ var breadcrumbValidRule = defineRule({
|
|
|
1096
1155
|
status: score < 0.5 ? "fail" : "warn",
|
|
1097
1156
|
score,
|
|
1098
1157
|
rationale: `${fatalCount} breadcrumb item(s) missing required fields.`,
|
|
1158
|
+
rationale_ko: `breadcrumb \uD56D\uBAA9 ${fatalCount}\uAC1C\uC5D0 \uD544\uC218 \uD544\uB4DC\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.`,
|
|
1099
1159
|
evidence: allIssues.slice(0, 5),
|
|
1100
1160
|
fixHint: 'Each itemListElement needs { "@type": "ListItem", position: N, name, item }.',
|
|
1101
1161
|
estimatedImpact: Math.round(2 * (1 - score))
|
|
@@ -1126,6 +1186,7 @@ var titleRule = defineRule({
|
|
|
1126
1186
|
effort: "low",
|
|
1127
1187
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#cittitle",
|
|
1128
1188
|
title: "<title> is set with a reasonable length",
|
|
1189
|
+
title_ko: "<title> \uD0DC\uADF8 \uC801\uC815 \uAE38\uC774 \uC124\uC815 \uC5EC\uBD80",
|
|
1129
1190
|
description: "The document title is the single most-cited piece of text and should be 10\u201370 characters.",
|
|
1130
1191
|
run(ctx) {
|
|
1131
1192
|
const title = ctx.$("head > title").first().text().trim();
|
|
@@ -1134,6 +1195,7 @@ var titleRule = defineRule({
|
|
|
1134
1195
|
status: "fail",
|
|
1135
1196
|
score: 0,
|
|
1136
1197
|
rationale: "Page has no <title>.",
|
|
1198
|
+
rationale_ko: "\uD398\uC774\uC9C0\uC5D0 <title>\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.",
|
|
1137
1199
|
fixUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md"
|
|
1138
1200
|
};
|
|
1139
1201
|
}
|
|
@@ -1141,17 +1203,19 @@ var titleRule = defineRule({
|
|
|
1141
1203
|
return {
|
|
1142
1204
|
status: "warn",
|
|
1143
1205
|
score: 0.4,
|
|
1144
|
-
rationale: `Title is only ${title.length} chars; consider a more descriptive one
|
|
1206
|
+
rationale: `Title is only ${title.length} chars; consider a more descriptive one.`,
|
|
1207
|
+
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.`
|
|
1145
1208
|
};
|
|
1146
1209
|
}
|
|
1147
1210
|
if (title.length > 70) {
|
|
1148
1211
|
return {
|
|
1149
1212
|
status: "warn",
|
|
1150
1213
|
score: 0.6,
|
|
1151
|
-
rationale: `Title is ${title.length} chars; search UIs commonly truncate after ~70
|
|
1214
|
+
rationale: `Title is ${title.length} chars; search UIs commonly truncate after ~70.`,
|
|
1215
|
+
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.`
|
|
1152
1216
|
};
|
|
1153
1217
|
}
|
|
1154
|
-
return { status: "pass", score: 1, rationale: `Title length ${title.length} is within range.` };
|
|
1218
|
+
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.` };
|
|
1155
1219
|
}
|
|
1156
1220
|
});
|
|
1157
1221
|
|
|
@@ -1166,6 +1230,7 @@ var metaDescriptionRule = defineRule({
|
|
|
1166
1230
|
effort: "low",
|
|
1167
1231
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#citmeta-description",
|
|
1168
1232
|
title: "meta description is set (50\u2013160 chars)",
|
|
1233
|
+
title_ko: "meta description \uC124\uC815 \uC5EC\uBD80 (50\u2013160\uC790)",
|
|
1169
1234
|
description: "AI snippets often quote the meta description verbatim; aim for 50\u2013160 chars.",
|
|
1170
1235
|
run(ctx) {
|
|
1171
1236
|
const desc = ctx.$('head meta[name="description"]').attr("content")?.trim() ?? "";
|
|
@@ -1174,16 +1239,17 @@ var metaDescriptionRule = defineRule({
|
|
|
1174
1239
|
status: "warn",
|
|
1175
1240
|
score: 0,
|
|
1176
1241
|
rationale: "No meta description set.",
|
|
1242
|
+
rationale_ko: "meta description\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.",
|
|
1177
1243
|
fixUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md"
|
|
1178
1244
|
};
|
|
1179
1245
|
}
|
|
1180
1246
|
if (desc.length < 50) {
|
|
1181
|
-
return { status: "warn", score: 0.5, rationale: `Only ${desc.length} chars; aim for 50
|
|
1247
|
+
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.` };
|
|
1182
1248
|
}
|
|
1183
1249
|
if (desc.length > 160) {
|
|
1184
|
-
return { status: "warn", score: 0.7, rationale: `${desc.length} chars; may be truncated after 160.` };
|
|
1250
|
+
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.` };
|
|
1185
1251
|
}
|
|
1186
|
-
return { status: "pass", score: 1, rationale: `Description length ${desc.length} is within range.` };
|
|
1252
|
+
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.` };
|
|
1187
1253
|
}
|
|
1188
1254
|
});
|
|
1189
1255
|
|
|
@@ -1198,6 +1264,7 @@ var canonicalRule = defineRule({
|
|
|
1198
1264
|
effort: "low",
|
|
1199
1265
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#citcanonical",
|
|
1200
1266
|
title: "Canonical URL is declared",
|
|
1267
|
+
title_ko: "Canonical URL \uC120\uC5B8 \uC5EC\uBD80",
|
|
1201
1268
|
description: 'rel="canonical" tells crawlers which URL is the source of truth, preventing duplicate-citation confusion.',
|
|
1202
1269
|
run(ctx) {
|
|
1203
1270
|
const href = ctx.$('head link[rel="canonical"]').attr("href")?.trim();
|
|
@@ -1206,14 +1273,15 @@ var canonicalRule = defineRule({
|
|
|
1206
1273
|
status: "warn",
|
|
1207
1274
|
score: 0,
|
|
1208
1275
|
rationale: 'No <link rel="canonical"> found.',
|
|
1276
|
+
rationale_ko: '<link rel="canonical">\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.',
|
|
1209
1277
|
fixUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md"
|
|
1210
1278
|
};
|
|
1211
1279
|
}
|
|
1212
1280
|
try {
|
|
1213
1281
|
const abs = new URL(href, ctx.finalUrl).toString();
|
|
1214
|
-
return { status: "pass", score: 1, rationale: `Canonical URL: ${abs}.` };
|
|
1282
|
+
return { status: "pass", score: 1, rationale: `Canonical URL: ${abs}.`, rationale_ko: `Canonical URL: ${abs}.` };
|
|
1215
1283
|
} catch {
|
|
1216
|
-
return { status: "fail", score: 0, rationale: `Canonical href is not a valid URL: ${href}` };
|
|
1284
|
+
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}` };
|
|
1217
1285
|
}
|
|
1218
1286
|
}
|
|
1219
1287
|
});
|
|
@@ -1230,6 +1298,7 @@ var ogTagsRule = defineRule({
|
|
|
1230
1298
|
effort: "low",
|
|
1231
1299
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#citog-tags",
|
|
1232
1300
|
title: "Open Graph tags are set",
|
|
1301
|
+
title_ko: "Open Graph \uD0DC\uADF8 \uC124\uC815 \uC5EC\uBD80",
|
|
1233
1302
|
description: "og:title/type/url/image power rich previews on AI chat, social, and messaging.",
|
|
1234
1303
|
run(ctx) {
|
|
1235
1304
|
const missing = [];
|
|
@@ -1238,13 +1307,14 @@ var ogTagsRule = defineRule({
|
|
|
1238
1307
|
if (!val) missing.push(prop);
|
|
1239
1308
|
}
|
|
1240
1309
|
if (missing.length === 0) {
|
|
1241
|
-
return { status: "pass", score: 1, rationale: "All required OG tags present." };
|
|
1310
|
+
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." };
|
|
1242
1311
|
}
|
|
1243
1312
|
const ratio = 1 - missing.length / REQUIRED.length;
|
|
1244
1313
|
return {
|
|
1245
1314
|
status: missing.length === REQUIRED.length ? "fail" : "warn",
|
|
1246
1315
|
score: ratio,
|
|
1247
1316
|
rationale: `Missing: ${missing.join(", ")}.`,
|
|
1317
|
+
rationale_ko: `\uB204\uB77D\uB41C \uD0DC\uADF8: ${missing.join(", ")}.`,
|
|
1248
1318
|
fixUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md"
|
|
1249
1319
|
};
|
|
1250
1320
|
}
|
|
@@ -1261,20 +1331,22 @@ var twitterCardRule = defineRule({
|
|
|
1261
1331
|
effort: "low",
|
|
1262
1332
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#cittwitter-card",
|
|
1263
1333
|
title: "Twitter Card metadata is set",
|
|
1334
|
+
title_ko: "Twitter Card \uBA54\uD0C0\uB370\uC774\uD130 \uC124\uC815 \uC5EC\uBD80",
|
|
1264
1335
|
description: "twitter:card + twitter:title give better previews on X/Twitter and some AI surfaces that reuse the tags.",
|
|
1265
1336
|
run(ctx) {
|
|
1266
1337
|
const card = ctx.$('head meta[name="twitter:card"]').attr("content")?.trim();
|
|
1267
1338
|
const title = ctx.$('head meta[name="twitter:title"]').attr("content")?.trim();
|
|
1268
1339
|
if (card && title) {
|
|
1269
|
-
return { status: "pass", score: 1, rationale: `Card type: ${card}.` };
|
|
1340
|
+
return { status: "pass", score: 1, rationale: `Card type: ${card}.`, rationale_ko: `\uCE74\uB4DC \uC720\uD615: ${card}.` };
|
|
1270
1341
|
}
|
|
1271
1342
|
if (card || title) {
|
|
1272
|
-
return { status: "warn", score: 0.5, rationale: "Partial twitter:* metadata; add the missing tag." };
|
|
1343
|
+
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." };
|
|
1273
1344
|
}
|
|
1274
1345
|
return {
|
|
1275
1346
|
status: "warn",
|
|
1276
1347
|
score: 0,
|
|
1277
1348
|
rationale: "No twitter:card metadata.",
|
|
1349
|
+
rationale_ko: "twitter:card \uBA54\uD0C0\uB370\uC774\uD130\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.",
|
|
1278
1350
|
fixUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md"
|
|
1279
1351
|
};
|
|
1280
1352
|
}
|
|
@@ -1291,6 +1363,7 @@ var langAttrRule = defineRule({
|
|
|
1291
1363
|
effort: "low",
|
|
1292
1364
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#citlang-attr",
|
|
1293
1365
|
title: "<html lang> is set",
|
|
1366
|
+
title_ko: "<html lang> \uC18D\uC131 \uC124\uC815 \uC5EC\uBD80",
|
|
1294
1367
|
description: "A lang attribute helps AI engines route the page to the right-language search surface (and helps screen readers).",
|
|
1295
1368
|
run(ctx) {
|
|
1296
1369
|
const lang = ctx.$("html").attr("lang")?.trim();
|
|
@@ -1299,10 +1372,11 @@ var langAttrRule = defineRule({
|
|
|
1299
1372
|
status: "warn",
|
|
1300
1373
|
score: 0,
|
|
1301
1374
|
rationale: "No lang attribute on <html>.",
|
|
1375
|
+
rationale_ko: "<html>\uC5D0 lang \uC18D\uC131\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.",
|
|
1302
1376
|
fixUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md"
|
|
1303
1377
|
};
|
|
1304
1378
|
}
|
|
1305
|
-
return { status: "pass", score: 1, rationale: `lang="${lang}".` };
|
|
1379
|
+
return { status: "pass", score: 1, rationale: `lang="${lang}".`, rationale_ko: `lang="${lang}".` };
|
|
1306
1380
|
}
|
|
1307
1381
|
});
|
|
1308
1382
|
|
|
@@ -1317,25 +1391,27 @@ var authorVisibleRule = defineRule({
|
|
|
1317
1391
|
effort: "medium",
|
|
1318
1392
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#citauthor-visible",
|
|
1319
1393
|
title: "Author is declared",
|
|
1394
|
+
title_ko: "\uC791\uC131\uC790 \uC815\uBCF4 \uC120\uC5B8 \uC5EC\uBD80",
|
|
1320
1395
|
description: "AI engines prefer citing content with an identifiable author; expose one via JSON-LD, meta[name=author], rel=author, or a .author class.",
|
|
1321
1396
|
run(ctx) {
|
|
1322
1397
|
for (const node of flattenJsonLd(ctx.jsonLd)) {
|
|
1323
1398
|
if (hasField(node, "author")) {
|
|
1324
|
-
return { status: "pass", score: 1, rationale: "Author found in JSON-LD." };
|
|
1399
|
+
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." };
|
|
1325
1400
|
}
|
|
1326
1401
|
}
|
|
1327
1402
|
const metaAuthor = ctx.$('head meta[name="author"]').attr("content")?.trim();
|
|
1328
|
-
if (metaAuthor) return { status: "pass", score: 1, rationale: `meta[name=author] = "${metaAuthor}".` };
|
|
1403
|
+
if (metaAuthor) return { status: "pass", score: 1, rationale: `meta[name=author] = "${metaAuthor}".`, rationale_ko: `meta[name=author] = "${metaAuthor}".` };
|
|
1329
1404
|
if (ctx.$('[rel="author"]').length > 0) {
|
|
1330
|
-
return { status: "pass", score: 1, rationale: 'rel="author" link found.' };
|
|
1405
|
+
return { status: "pass", score: 1, rationale: 'rel="author" link found.', rationale_ko: 'rel="author" \uB9C1\uD06C\uB97C \uCC3E\uC558\uC2B5\uB2C8\uB2E4.' };
|
|
1331
1406
|
}
|
|
1332
1407
|
if (ctx.$('.author, [class*="author"], [itemprop="author"]').length > 0) {
|
|
1333
|
-
return { status: "pass", score: 0.8, rationale: "Author-ish DOM selector found (weaker signal)." };
|
|
1408
|
+
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)." };
|
|
1334
1409
|
}
|
|
1335
1410
|
return {
|
|
1336
1411
|
status: "warn",
|
|
1337
1412
|
score: 0,
|
|
1338
1413
|
rationale: "No author signal found (JSON-LD, meta, rel, or .author).",
|
|
1414
|
+
rationale_ko: "\uC791\uC131\uC790 \uC815\uBCF4\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4 (JSON-LD, meta, rel, .author \uBAA8\uB450 \uC5C6\uC74C).",
|
|
1339
1415
|
fixUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md"
|
|
1340
1416
|
};
|
|
1341
1417
|
}
|
|
@@ -1352,25 +1428,27 @@ var datesRule = defineRule({
|
|
|
1352
1428
|
effort: "low",
|
|
1353
1429
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#citdates",
|
|
1354
1430
|
title: "Publish / modified date is present",
|
|
1431
|
+
title_ko: "\uBC1C\uD589\uC77C / \uC218\uC815\uC77C \uC874\uC7AC \uC5EC\uBD80",
|
|
1355
1432
|
description: "AI engines rank recent pages higher; expose datePublished via JSON-LD, <time datetime>, or article:published_time meta.",
|
|
1356
1433
|
run(ctx) {
|
|
1357
1434
|
for (const node of flattenJsonLd(ctx.jsonLd)) {
|
|
1358
1435
|
if (hasField(node, "datePublished")) {
|
|
1359
|
-
return { status: "pass", score: 1, rationale: "datePublished found in JSON-LD." };
|
|
1436
|
+
return { status: "pass", score: 1, rationale: "datePublished found in JSON-LD.", rationale_ko: "JSON-LD\uC5D0\uC11C datePublished\uB97C \uCC3E\uC558\uC2B5\uB2C8\uB2E4." };
|
|
1360
1437
|
}
|
|
1361
1438
|
}
|
|
1362
1439
|
const articleTime = ctx.$('head meta[property="article:published_time"]').attr("content")?.trim();
|
|
1363
1440
|
if (articleTime) {
|
|
1364
|
-
return { status: "pass", score: 1, rationale: `article:published_time = ${articleTime}.` };
|
|
1441
|
+
return { status: "pass", score: 1, rationale: `article:published_time = ${articleTime}.`, rationale_ko: `article:published_time = ${articleTime}.` };
|
|
1365
1442
|
}
|
|
1366
1443
|
const timeEl = ctx.$("time[datetime]").first().attr("datetime")?.trim();
|
|
1367
1444
|
if (timeEl) {
|
|
1368
|
-
return { status: "pass", score: 0.8, rationale: `<time datetime="${timeEl}"> found.` };
|
|
1445
|
+
return { status: "pass", score: 0.8, rationale: `<time datetime="${timeEl}"> found.`, rationale_ko: `<time datetime="${timeEl}">\uB97C \uCC3E\uC558\uC2B5\uB2C8\uB2E4.` };
|
|
1369
1446
|
}
|
|
1370
1447
|
return {
|
|
1371
1448
|
status: "warn",
|
|
1372
1449
|
score: 0,
|
|
1373
1450
|
rationale: "No publish date found (JSON-LD, meta article:published_time, or <time datetime>).",
|
|
1451
|
+
rationale_ko: "\uBC1C\uD589\uC77C\uC774 \uC5C6\uC2B5\uB2C8\uB2E4 (JSON-LD, meta article:published_time, <time datetime> \uBAA8\uB450 \uC5C6\uC74C).",
|
|
1374
1452
|
fixUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md"
|
|
1375
1453
|
};
|
|
1376
1454
|
}
|
|
@@ -1398,6 +1476,7 @@ var contentFreshnessRule = defineRule({
|
|
|
1398
1476
|
effort: "low",
|
|
1399
1477
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#citcontent-freshness",
|
|
1400
1478
|
title: "Article content is fresh (dateModified within 1 year)",
|
|
1479
|
+
title_ko: "\uCF58\uD150\uCE20 \uCD5C\uC2E0\uC131 (dateModified 1\uB144 \uC774\uB0B4)",
|
|
1401
1480
|
description: "AI engines down-rank stale content. Surface a recent dateModified (\u2264365 days) on Article-like pages so retrieval rankings stay strong.",
|
|
1402
1481
|
run(ctx) {
|
|
1403
1482
|
const nodes = flattenJsonLd(ctx.jsonLd);
|
|
@@ -1406,7 +1485,8 @@ var contentFreshnessRule = defineRule({
|
|
|
1406
1485
|
return {
|
|
1407
1486
|
status: "skip",
|
|
1408
1487
|
score: 0,
|
|
1409
|
-
rationale: "No Article/BlogPosting/NewsArticle JSON-LD; freshness signal not applicable."
|
|
1488
|
+
rationale: "No Article/BlogPosting/NewsArticle JSON-LD; freshness signal not applicable.",
|
|
1489
|
+
rationale_ko: "Article/BlogPosting/NewsArticle JSON-LD\uAC00 \uC5C6\uC5B4 \uCD5C\uC2E0\uC131 \uC2E0\uD638\uB97C \uD655\uC778\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4."
|
|
1410
1490
|
};
|
|
1411
1491
|
}
|
|
1412
1492
|
let bestMs = null;
|
|
@@ -1428,6 +1508,7 @@ var contentFreshnessRule = defineRule({
|
|
|
1428
1508
|
status: "warn",
|
|
1429
1509
|
score: 0,
|
|
1430
1510
|
rationale: "Article has no parseable dateModified or datePublished.",
|
|
1511
|
+
rationale_ko: "Article JSON-LD\uC5D0 \uD30C\uC2F1 \uAC00\uB2A5\uD55C dateModified \uB610\uB294 datePublished\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.",
|
|
1431
1512
|
fixHint: "Add ISO-8601 dateModified and datePublished to your Article JSON-LD.",
|
|
1432
1513
|
estimatedImpact: 3
|
|
1433
1514
|
};
|
|
@@ -1438,6 +1519,7 @@ var contentFreshnessRule = defineRule({
|
|
|
1438
1519
|
status: "pass",
|
|
1439
1520
|
score: 1,
|
|
1440
1521
|
rationale: `${usedField} within the last year (~${ageDays} day${ageDays === 1 ? "" : "s"} ago).`,
|
|
1522
|
+
rationale_ko: `${usedField}\uC774 1\uB144 \uC774\uB0B4\uC785\uB2C8\uB2E4 (\uC57D ${ageDays}\uC77C \uC804).`,
|
|
1441
1523
|
evidence: { ageDays, field: usedField }
|
|
1442
1524
|
};
|
|
1443
1525
|
}
|
|
@@ -1446,6 +1528,7 @@ var contentFreshnessRule = defineRule({
|
|
|
1446
1528
|
status: "warn",
|
|
1447
1529
|
score: 0.6,
|
|
1448
1530
|
rationale: `${usedField} is ${ageDays} days old. Refresh within a year for best AI ranking.`,
|
|
1531
|
+
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.`,
|
|
1449
1532
|
evidence: { ageDays, field: usedField },
|
|
1450
1533
|
estimatedImpact: 2
|
|
1451
1534
|
};
|
|
@@ -1454,6 +1537,7 @@ var contentFreshnessRule = defineRule({
|
|
|
1454
1537
|
status: "warn",
|
|
1455
1538
|
score: 0.2,
|
|
1456
1539
|
rationale: `${usedField} is ${ageDays} days old (>2 years). AI engines treat this as stale.`,
|
|
1540
|
+
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.`,
|
|
1457
1541
|
evidence: { ageDays, field: usedField },
|
|
1458
1542
|
fixHint: "Update content and bump dateModified to today's date.",
|
|
1459
1543
|
estimatedImpact: 3
|
|
@@ -1485,22 +1569,25 @@ var singleH1Rule = defineRule({
|
|
|
1485
1569
|
effort: "low",
|
|
1486
1570
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#cntsingle-h1",
|
|
1487
1571
|
title: "Exactly one <h1>",
|
|
1572
|
+
title_ko: "<h1> \uD0DC\uADF8 1\uAC1C \uC5EC\uBD80",
|
|
1488
1573
|
description: "A single H1 tells AI engines the primary topic of the page without ambiguity.",
|
|
1489
1574
|
run(ctx) {
|
|
1490
1575
|
const n = ctx.$("h1").length;
|
|
1491
|
-
if (n === 1) return { status: "pass", score: 1, rationale: "Exactly one <h1>." };
|
|
1576
|
+
if (n === 1) return { status: "pass", score: 1, rationale: "Exactly one <h1>.", rationale_ko: "<h1>\uC774 \uC815\uD655\uD788 1\uAC1C\uC785\uB2C8\uB2E4." };
|
|
1492
1577
|
if (n === 0) {
|
|
1493
1578
|
return {
|
|
1494
1579
|
status: "fail",
|
|
1495
1580
|
score: 0,
|
|
1496
1581
|
rationale: "No <h1> on the page.",
|
|
1582
|
+
rationale_ko: "\uD398\uC774\uC9C0\uC5D0 <h1>\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.",
|
|
1497
1583
|
fixUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules/cnt.single-h1.md"
|
|
1498
1584
|
};
|
|
1499
1585
|
}
|
|
1500
1586
|
return {
|
|
1501
1587
|
status: "warn",
|
|
1502
1588
|
score: Math.max(0.3, 1 / n),
|
|
1503
|
-
rationale: `Found ${n} <h1> tags; prefer one primary heading
|
|
1589
|
+
rationale: `Found ${n} <h1> tags; prefer one primary heading.`,
|
|
1590
|
+
rationale_ko: `<h1>\uC774 ${n}\uAC1C \uC788\uC2B5\uB2C8\uB2E4. \uB300\uD45C \uC81C\uBAA9 1\uAC1C\uB9CC \uC0AC\uC6A9\uD558\uC138\uC694.`
|
|
1504
1591
|
};
|
|
1505
1592
|
}
|
|
1506
1593
|
});
|
|
@@ -1516,6 +1603,7 @@ var headingHierarchyRule = defineRule({
|
|
|
1516
1603
|
effort: "medium",
|
|
1517
1604
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#cntheading-hierarchy",
|
|
1518
1605
|
title: "Heading levels do not skip",
|
|
1606
|
+
title_ko: "\uC81C\uBAA9 \uB2E8\uACC4 \uC21C\uC11C \uC900\uC218 \uC5EC\uBD80",
|
|
1519
1607
|
description: "Going from H2 directly to H4 breaks the outline AI engines use to segment content.",
|
|
1520
1608
|
run(ctx) {
|
|
1521
1609
|
const levels = [];
|
|
@@ -1525,7 +1613,7 @@ var headingHierarchyRule = defineRule({
|
|
|
1525
1613
|
if (m?.[1]) levels.push(parseInt(m[1], 10));
|
|
1526
1614
|
});
|
|
1527
1615
|
if (levels.length === 0) {
|
|
1528
|
-
return { status: "skip", score: 0, rationale: "No headings found." };
|
|
1616
|
+
return { status: "skip", score: 0, rationale: "No headings found.", rationale_ko: "\uC81C\uBAA9 \uD0DC\uADF8\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4." };
|
|
1529
1617
|
}
|
|
1530
1618
|
const skips = [];
|
|
1531
1619
|
for (let i = 1; i < levels.length; i++) {
|
|
@@ -1534,12 +1622,13 @@ var headingHierarchyRule = defineRule({
|
|
|
1534
1622
|
if (curr > prev + 1) skips.push({ from: prev, to: curr });
|
|
1535
1623
|
}
|
|
1536
1624
|
if (skips.length === 0) {
|
|
1537
|
-
return { status: "pass", score: 1, rationale: "No heading-level skips." };
|
|
1625
|
+
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." };
|
|
1538
1626
|
}
|
|
1539
1627
|
return {
|
|
1540
1628
|
status: "warn",
|
|
1541
1629
|
score: Math.max(0.3, 1 - skips.length / levels.length),
|
|
1542
1630
|
rationale: `${skips.length} heading skip(s) detected (e.g. h${skips[0].from}\u2192h${skips[0].to}).`,
|
|
1631
|
+
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}).`,
|
|
1543
1632
|
fixUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md"
|
|
1544
1633
|
};
|
|
1545
1634
|
}
|
|
@@ -1556,11 +1645,12 @@ var imageAltRule = defineRule({
|
|
|
1556
1645
|
effort: "medium",
|
|
1557
1646
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#cntimage-alt",
|
|
1558
1647
|
title: "\u226580% of <img> have alt text",
|
|
1648
|
+
title_ko: "<img>\uC758 80% \uC774\uC0C1 alt \uD14D\uC2A4\uD2B8 \uBCF4\uC720 \uC5EC\uBD80",
|
|
1559
1649
|
description: "Alt text gives AI engines a textual anchor for visual content and improves accessibility.",
|
|
1560
1650
|
run(ctx) {
|
|
1561
1651
|
const imgs = ctx.$("img");
|
|
1562
1652
|
const total = imgs.length;
|
|
1563
|
-
if (total === 0) return { status: "skip", score: 0, rationale: "No <img> on the page." };
|
|
1653
|
+
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." };
|
|
1564
1654
|
let withAlt = 0;
|
|
1565
1655
|
imgs.each((_i, el) => {
|
|
1566
1656
|
const alt = ctx.$(el).attr("alt");
|
|
@@ -1568,12 +1658,13 @@ var imageAltRule = defineRule({
|
|
|
1568
1658
|
});
|
|
1569
1659
|
const ratio = withAlt / total;
|
|
1570
1660
|
if (ratio >= 0.8) {
|
|
1571
|
-
return { status: "pass", score: 1, rationale: `${withAlt}/${total} images have alt (${Math.round(ratio * 100)}%).` };
|
|
1661
|
+
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)}%).` };
|
|
1572
1662
|
}
|
|
1573
1663
|
return {
|
|
1574
1664
|
status: "warn",
|
|
1575
1665
|
score: ratio,
|
|
1576
1666
|
rationale: `Only ${withAlt}/${total} images have alt text (${Math.round(ratio * 100)}%). Aim for \u226580%.`,
|
|
1667
|
+
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.`,
|
|
1577
1668
|
fixUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md"
|
|
1578
1669
|
};
|
|
1579
1670
|
}
|
|
@@ -1590,11 +1681,12 @@ var tldrOrFaqRule = defineRule({
|
|
|
1590
1681
|
effort: "medium",
|
|
1591
1682
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#cnttldr-or-faq",
|
|
1592
1683
|
title: "TL;DR summary or FAQ block",
|
|
1684
|
+
title_ko: "TL;DR \uC694\uC57D \uB610\uB294 FAQ \uBE14\uB85D \uC874\uC7AC \uC5EC\uBD80",
|
|
1593
1685
|
description: 'AI engines strongly prefer content with a quotable summary or FAQ \u2014 it makes the page "citation-ready".',
|
|
1594
1686
|
run(ctx) {
|
|
1595
1687
|
for (const node of flattenJsonLd(ctx.jsonLd)) {
|
|
1596
1688
|
if (getTypes(node).includes("FAQPage")) {
|
|
1597
|
-
return { status: "pass", score: 1, rationale: "FAQPage schema present." };
|
|
1689
|
+
return { status: "pass", score: 1, rationale: "FAQPage schema present.", rationale_ko: "FAQPage \uC2A4\uD0A4\uB9C8\uAC00 \uC788\uC2B5\uB2C8\uB2E4." };
|
|
1598
1690
|
}
|
|
1599
1691
|
}
|
|
1600
1692
|
const sel = [
|
|
@@ -1607,12 +1699,13 @@ var tldrOrFaqRule = defineRule({
|
|
|
1607
1699
|
"[data-tldr]"
|
|
1608
1700
|
].join(", ");
|
|
1609
1701
|
if (ctx.$(sel).length > 0) {
|
|
1610
|
-
return { status: "pass", score: 0.85, rationale: "TL;DR / summary / FAQ region detected by selector." };
|
|
1702
|
+
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." };
|
|
1611
1703
|
}
|
|
1612
1704
|
return {
|
|
1613
1705
|
status: "warn",
|
|
1614
1706
|
score: 0,
|
|
1615
1707
|
rationale: "No TL;DR / summary / FAQ found; add one to boost AI citation odds.",
|
|
1708
|
+
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.",
|
|
1616
1709
|
fixUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md"
|
|
1617
1710
|
};
|
|
1618
1711
|
}
|
|
@@ -1629,6 +1722,7 @@ var wordCountRule = defineRule({
|
|
|
1629
1722
|
effort: "high",
|
|
1630
1723
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#cntword-count",
|
|
1631
1724
|
title: "Page has enough body text",
|
|
1725
|
+
title_ko: "\uCDA9\uBD84\uD55C \uBCF8\uBB38 \uD14D\uC2A4\uD2B8 \uC5EC\uBD80",
|
|
1632
1726
|
description: "Thin pages (under ~100 words) are rarely cited by AI engines. Aim for \u2265300 words of meaningful body copy.",
|
|
1633
1727
|
run(ctx) {
|
|
1634
1728
|
const $ = ctx.$;
|
|
@@ -1636,12 +1730,13 @@ var wordCountRule = defineRule({
|
|
|
1636
1730
|
clone.find("script, style, noscript, nav, header, footer, aside").remove();
|
|
1637
1731
|
const text = clone.text().replace(/\s+/g, " ").trim();
|
|
1638
1732
|
const words = text ? text.split(" ").length : 0;
|
|
1639
|
-
if (words >= 300) return { status: "pass", score: 1, rationale: `${words} words of body text.` };
|
|
1640
|
-
if (words >= 100) return { status: "warn", score: 0.5, rationale: `Only ${words} words; aim for 300
|
|
1733
|
+
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.` };
|
|
1734
|
+
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.` };
|
|
1641
1735
|
return {
|
|
1642
1736
|
status: "fail",
|
|
1643
1737
|
score: 0,
|
|
1644
1738
|
rationale: `Only ${words} words; too thin to be cited.`,
|
|
1739
|
+
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.`,
|
|
1645
1740
|
fixUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md"
|
|
1646
1741
|
};
|
|
1647
1742
|
}
|
|
@@ -1692,11 +1787,12 @@ var qaStructureRule = defineRule({
|
|
|
1692
1787
|
effort: "medium",
|
|
1693
1788
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#cntqa-structure",
|
|
1694
1789
|
title: "Content uses Q&A structure for answer extraction",
|
|
1790
|
+
title_ko: "\uB2F5\uBCC0 \uCD94\uCD9C\uC744 \uC704\uD55C Q&A \uAD6C\uC870 \uC0AC\uC6A9 \uC5EC\uBD80",
|
|
1695
1791
|
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.",
|
|
1696
1792
|
run(ctx) {
|
|
1697
1793
|
for (const node of flattenJsonLd(ctx.jsonLd)) {
|
|
1698
1794
|
if (getTypes(node).includes("FAQPage")) {
|
|
1699
|
-
return { status: "pass", score: 1, rationale: "FAQPage JSON-LD provides explicit Q&A." };
|
|
1795
|
+
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." };
|
|
1700
1796
|
}
|
|
1701
1797
|
}
|
|
1702
1798
|
const questionHeadings = [];
|
|
@@ -1709,6 +1805,7 @@ var qaStructureRule = defineRule({
|
|
|
1709
1805
|
status: "pass",
|
|
1710
1806
|
score: 1,
|
|
1711
1807
|
rationale: `${questionHeadings.length} question-style headings detected.`,
|
|
1808
|
+
rationale_ko: `\uC9C8\uBB38\uD615 \uC81C\uBAA9\uC774 ${questionHeadings.length}\uAC1C \uAC10\uC9C0\uB429\uB2C8\uB2E4.`,
|
|
1712
1809
|
evidence: { headings: questionHeadings.slice(0, 5) }
|
|
1713
1810
|
};
|
|
1714
1811
|
}
|
|
@@ -1717,6 +1814,7 @@ var qaStructureRule = defineRule({
|
|
|
1717
1814
|
status: "warn",
|
|
1718
1815
|
score: 0.6,
|
|
1719
1816
|
rationale: "1 question-style heading. Add a second to strengthen answer extraction.",
|
|
1817
|
+
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.",
|
|
1720
1818
|
evidence: { headings: questionHeadings },
|
|
1721
1819
|
estimatedImpact: 1
|
|
1722
1820
|
};
|
|
@@ -1725,6 +1823,7 @@ var qaStructureRule = defineRule({
|
|
|
1725
1823
|
status: "warn",
|
|
1726
1824
|
score: 0,
|
|
1727
1825
|
rationale: "No question-style H2/H3 headings or FAQPage JSON-LD found.",
|
|
1826
|
+
rationale_ko: "\uC9C8\uBB38\uD615 H2/H3 \uC81C\uBAA9\uC774\uB098 FAQPage JSON-LD\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.",
|
|
1728
1827
|
fixHint: 'Reframe at least 2 H2 headings as questions ("How do I\u2026?", "What is\u2026?") or add FAQPage JSON-LD.',
|
|
1729
1828
|
estimatedImpact: 3
|
|
1730
1829
|
};
|
|
@@ -1742,13 +1841,14 @@ var externalCitationsRule = defineRule({
|
|
|
1742
1841
|
effort: "medium",
|
|
1743
1842
|
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#cntexternal-citations",
|
|
1744
1843
|
title: "Content cites external sources",
|
|
1844
|
+
title_ko: "\uC678\uBD80 \uCD9C\uCC98 \uC778\uC6A9 \uC5EC\uBD80",
|
|
1745
1845
|
description: "Outbound links to authoritative external sources are an E-E-A-T trust signal. AI engines treat well-cited pages as more credible.",
|
|
1746
1846
|
run(ctx) {
|
|
1747
1847
|
let host;
|
|
1748
1848
|
try {
|
|
1749
1849
|
host = new URL(ctx.finalUrl).hostname.toLowerCase();
|
|
1750
1850
|
} catch {
|
|
1751
|
-
return { status: "skip", score: 0, rationale: "Invalid finalUrl." };
|
|
1851
|
+
return { status: "skip", score: 0, rationale: "Invalid finalUrl.", rationale_ko: "\uC720\uD6A8\uD558\uC9C0 \uC54A\uC740 finalUrl\uC785\uB2C8\uB2E4." };
|
|
1752
1852
|
}
|
|
1753
1853
|
const seen = /* @__PURE__ */ new Set();
|
|
1754
1854
|
ctx.$("main a[href], article a[href], body a[href]").each((_i, el) => {
|
|
@@ -1773,6 +1873,7 @@ var externalCitationsRule = defineRule({
|
|
|
1773
1873
|
status: "pass",
|
|
1774
1874
|
score: 1,
|
|
1775
1875
|
rationale: `${count} distinct external host(s) cited (excluding nofollow).`,
|
|
1876
|
+
rationale_ko: `\uC678\uBD80 \uC0AC\uC774\uD2B8 ${count}\uAC1C\uB97C \uC778\uC6A9\uD569\uB2C8\uB2E4 (nofollow \uC81C\uC678).`,
|
|
1776
1877
|
evidence: { hosts: [...seen].slice(0, 8) }
|
|
1777
1878
|
};
|
|
1778
1879
|
}
|
|
@@ -1781,6 +1882,7 @@ var externalCitationsRule = defineRule({
|
|
|
1781
1882
|
status: "pass",
|
|
1782
1883
|
score: 0.7,
|
|
1783
1884
|
rationale: `${count} external host(s) cited. Aim for \u22653 for stronger E-E-A-T.`,
|
|
1885
|
+
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.`,
|
|
1784
1886
|
evidence: { hosts: [...seen] },
|
|
1785
1887
|
estimatedImpact: 1
|
|
1786
1888
|
};
|
|
@@ -1789,6 +1891,7 @@ var externalCitationsRule = defineRule({
|
|
|
1789
1891
|
status: "warn",
|
|
1790
1892
|
score: 0,
|
|
1791
1893
|
rationale: "No external follow citations found in main content.",
|
|
1894
|
+
rationale_ko: "\uBCF8\uBB38\uC5D0 \uC678\uBD80 \uCD9C\uCC98 \uB9C1\uD06C\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.",
|
|
1792
1895
|
fixHint: "Cite at least one authoritative external source (research paper, official docs, news outlet).",
|
|
1793
1896
|
estimatedImpact: 2
|
|
1794
1897
|
};
|
|
@@ -1806,12 +1909,130 @@ var contentRules = [
|
|
|
1806
1909
|
externalCitationsRule
|
|
1807
1910
|
];
|
|
1808
1911
|
|
|
1912
|
+
// src/rules/aeo/skill-md.ts
|
|
1913
|
+
var aeoSkillMdRule = defineRule({
|
|
1914
|
+
id: "aeo.skill-md",
|
|
1915
|
+
stableId: "aeo.skill-md",
|
|
1916
|
+
category: "aeo",
|
|
1917
|
+
group: "opportunity",
|
|
1918
|
+
weight: 3,
|
|
1919
|
+
impact: "high",
|
|
1920
|
+
effort: "low",
|
|
1921
|
+
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#aeoskill-md",
|
|
1922
|
+
title: "skill.md is present",
|
|
1923
|
+
title_ko: "skill.md \uD30C\uC77C \uC874\uC7AC \uC5EC\uBD80",
|
|
1924
|
+
description: "A /skill.md file describes site capabilities so AI agents know what this site can do for them.",
|
|
1925
|
+
run(ctx) {
|
|
1926
|
+
if (ctx.skillMd !== null) {
|
|
1927
|
+
return {
|
|
1928
|
+
status: "pass",
|
|
1929
|
+
score: 1,
|
|
1930
|
+
rationale: "skill.md found at site root.",
|
|
1931
|
+
rationale_ko: "skill.md\uAC00 \uC0AC\uC774\uD2B8 \uB8E8\uD2B8\uC5D0 \uC874\uC7AC\uD569\uB2C8\uB2E4."
|
|
1932
|
+
};
|
|
1933
|
+
}
|
|
1934
|
+
return {
|
|
1935
|
+
status: "warn",
|
|
1936
|
+
score: 0,
|
|
1937
|
+
rationale: "No /skill.md found. Add one to describe your site capabilities to AI agents.",
|
|
1938
|
+
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.",
|
|
1939
|
+
fixHint: "Create /skill.md listing what services, products, and capabilities this site offers."
|
|
1940
|
+
};
|
|
1941
|
+
}
|
|
1942
|
+
});
|
|
1943
|
+
|
|
1944
|
+
// src/rules/aeo/agent-permissions.ts
|
|
1945
|
+
var aeoAgentPermissionsRule = defineRule({
|
|
1946
|
+
id: "aeo.agent-permissions",
|
|
1947
|
+
stableId: "aeo.agent-permissions",
|
|
1948
|
+
category: "aeo",
|
|
1949
|
+
group: "opportunity",
|
|
1950
|
+
weight: 3,
|
|
1951
|
+
impact: "medium",
|
|
1952
|
+
effort: "low",
|
|
1953
|
+
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#aeoagent-permissions",
|
|
1954
|
+
title: "agent-permissions.json is present",
|
|
1955
|
+
title_ko: "agent-permissions.json \uD30C\uC77C \uC874\uC7AC \uC5EC\uBD80",
|
|
1956
|
+
description: "Declares explicit read/summarize/cite/train permissions for AI agents.",
|
|
1957
|
+
run(ctx) {
|
|
1958
|
+
if (ctx.agentPermissions !== null) {
|
|
1959
|
+
return {
|
|
1960
|
+
status: "pass",
|
|
1961
|
+
score: 1,
|
|
1962
|
+
rationale: "agent-permissions.json found at site root.",
|
|
1963
|
+
rationale_ko: "agent-permissions.json\uC774 \uC0AC\uC774\uD2B8 \uB8E8\uD2B8\uC5D0 \uC874\uC7AC\uD569\uB2C8\uB2E4.",
|
|
1964
|
+
evidence: ctx.agentPermissions
|
|
1965
|
+
};
|
|
1966
|
+
}
|
|
1967
|
+
return {
|
|
1968
|
+
status: "warn",
|
|
1969
|
+
score: 0,
|
|
1970
|
+
rationale: "No /agent-permissions.json found. Add one to declare AI agent access policy.",
|
|
1971
|
+
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.",
|
|
1972
|
+
fixHint: "Create /agent-permissions.json with read, summarize, cite, and train permission flags."
|
|
1973
|
+
};
|
|
1974
|
+
}
|
|
1975
|
+
});
|
|
1976
|
+
|
|
1977
|
+
// src/rules/aeo/token-length.ts
|
|
1978
|
+
var THRESHOLD_OPTIMAL = 15e3;
|
|
1979
|
+
var THRESHOLD_MAX = 25e3;
|
|
1980
|
+
var aeoTokenLengthRule = defineRule({
|
|
1981
|
+
id: "aeo.token-length",
|
|
1982
|
+
stableId: "aeo.token-length",
|
|
1983
|
+
category: "aeo",
|
|
1984
|
+
group: "diagnostic",
|
|
1985
|
+
weight: 4,
|
|
1986
|
+
impact: "medium",
|
|
1987
|
+
effort: "medium",
|
|
1988
|
+
docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#aeotoken-length",
|
|
1989
|
+
title: "Content token length within AI agent limits",
|
|
1990
|
+
title_ko: "\uCF58\uD150\uCE20 \uD1A0\uD070 \uC218 AI \uC5D0\uC774\uC804\uD2B8 \uAD8C\uC7A5 \uBC94\uC704",
|
|
1991
|
+
description: "Pages under 15K tokens are optimal for AI agents (per Addy Osmani's AEO guidance).",
|
|
1992
|
+
run(ctx) {
|
|
1993
|
+
const text = ctx.$("body").text();
|
|
1994
|
+
const tokenEstimate = Math.round(text.length / 3);
|
|
1995
|
+
const evidence = { tokenEstimate, thresholds: { optimal: THRESHOLD_OPTIMAL, max: THRESHOLD_MAX } };
|
|
1996
|
+
if (tokenEstimate <= THRESHOLD_OPTIMAL) {
|
|
1997
|
+
return {
|
|
1998
|
+
status: "pass",
|
|
1999
|
+
score: 1,
|
|
2000
|
+
rationale: `Estimated ~${tokenEstimate.toLocaleString()} tokens \u2014 within optimal range.`,
|
|
2001
|
+
rationale_ko: `\uC608\uC0C1 \uD1A0\uD070 \uC218 ~${tokenEstimate.toLocaleString()} \u2014 \uAD8C\uC7A5 \uBC94\uC704(15K) \uC774\uB0B4\uC785\uB2C8\uB2E4.`,
|
|
2002
|
+
evidence
|
|
2003
|
+
};
|
|
2004
|
+
}
|
|
2005
|
+
if (tokenEstimate <= THRESHOLD_MAX) {
|
|
2006
|
+
return {
|
|
2007
|
+
status: "warn",
|
|
2008
|
+
score: 0.5,
|
|
2009
|
+
rationale: `Estimated ~${tokenEstimate.toLocaleString()} tokens \u2014 exceeds 15K recommendation.`,
|
|
2010
|
+
rationale_ko: `\uC608\uC0C1 \uD1A0\uD070 \uC218 ~${tokenEstimate.toLocaleString()} \u2014 15K \uAD8C\uC7A5\uCE58\uB97C \uCD08\uACFC\uD569\uB2C8\uB2E4.`,
|
|
2011
|
+
fixHint: "Consider splitting content into shorter, focused pages.",
|
|
2012
|
+
evidence
|
|
2013
|
+
};
|
|
2014
|
+
}
|
|
2015
|
+
return {
|
|
2016
|
+
status: "fail",
|
|
2017
|
+
score: 0,
|
|
2018
|
+
rationale: `Estimated ~${tokenEstimate.toLocaleString()} tokens \u2014 exceeds 25K agent processing limit.`,
|
|
2019
|
+
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.`,
|
|
2020
|
+
fixHint: "Split this page into multiple focused pages under 15K tokens.",
|
|
2021
|
+
evidence
|
|
2022
|
+
};
|
|
2023
|
+
}
|
|
2024
|
+
});
|
|
2025
|
+
|
|
2026
|
+
// src/rules/aeo/index.ts
|
|
2027
|
+
var aeoRules = [aeoSkillMdRule, aeoAgentPermissionsRule, aeoTokenLengthRule];
|
|
2028
|
+
|
|
1809
2029
|
// src/rules/index.ts
|
|
1810
2030
|
var defaultRules = [
|
|
1811
2031
|
...crawlerRules,
|
|
1812
2032
|
...structuredDataRules,
|
|
1813
2033
|
...citationRules,
|
|
1814
|
-
...contentRules
|
|
2034
|
+
...contentRules,
|
|
2035
|
+
...aeoRules
|
|
1815
2036
|
];
|
|
1816
2037
|
|
|
1817
2038
|
// src/config.ts
|
|
@@ -2007,7 +2228,8 @@ var CATEGORY_LABELS = {
|
|
|
2007
2228
|
crawler: "AI Crawler Access",
|
|
2008
2229
|
"structured-data": "Structured Data",
|
|
2009
2230
|
citation: "Citation Signals",
|
|
2010
|
-
content: "Content Structure"
|
|
2231
|
+
content: "Content Structure",
|
|
2232
|
+
aeo: "AEO Stack"
|
|
2011
2233
|
};
|
|
2012
2234
|
function scoreBadge(score) {
|
|
2013
2235
|
const color = score >= 85 ? "brightgreen" : score >= 60 ? "yellow" : "red";
|