What happened
I ran audit.mjs and gate.mjs against a bilingual Next.js blog: 10 posts, 5 slugs times 2 locales, named <slug>.<locale>.md (agent-fleet.es.md, agent-fleet.en.md). Content dir is content/blog, extension .md.
The audit reported 12 errors and 10 advisories. All 22 are false positives, and they trace back to one line.
errors: broken-internal-link x8, title-length x4
advisories: orphan-page x10
The 8 broken links are links between my own posts. They resolve fine in production:
200 https://nahuelsoria.com/es/blog/agent-fleet
200 https://nahuelsoria.com/en/blog/agent-fleet
200 https://nahuelsoria.com/es/blog/honest-ai-visibility-score
200 https://nahuelsoria.com/en/blog/honest-ai-visibility-score
Root cause
slugForFile in skill/scripts/linkgraph.mjs:41:
export function slugForFile(filePath, extRe = /\.mdx?$/i) {
return path.basename(filePath).replace(extRe, "");
}
agent-fleet.es.md becomes the slug agent-fleet.es, so a link to /es/blog/agent-fleet matches nothing in the slug set. My site derives the route slug with ^(.+)\.([a-z]{2})\.md$ and serves /[locale]/blog/[slug], which is a common i18n convention in Next, Astro and Hugo.
Everything else cascades from that:
broken-internal-link (8) fires on every cross-post link.
orphan-page (10) fires on every post, because no link resolves anywhere, so nothing has inbound links.
- In the gate,
internal-links-min fires too, since resolving links are counted after resolution.
The gate blocks a post that has been live for weeks, on two rules that are the same failure:
$ node gate.mjs content/blog/nightly-bug-hunter.es.md --term "bug hunter nocturno"
broken-internal-link: "/es/blog/agent-fleet" resolves to no existing post
internal-links-min: 0 resolving internal link(s), need at least 2
exit=1
There is no configuration that avoids this. The failure is in slug derivation, not in the mapping.
Suggested fix
An optional slugPattern in config.json, a regex with one capture group applied to the basename, falling back to current behavior when absent:
"slugPattern": "^(.+)\\.[a-z]{2}$"
That keeps slugForFile a pure function and leaves the default untouched for single-locale repos. A narrower alternative is a locales array in config, stripping a trailing .<locale> only for the locales the repo declares, which is harder to misconfigure.
Worth deciding alongside it: whether two locale variants of the same slug are one post or two for cannibalization-fuzzy. Right now agent-fleet.es and agent-fleet.en are separate posts with different titles, so they do not collide, but a repo with two locales that share a language would.
Happy to send the PR if you tell me which of the two shapes you prefer.
Config
{
"schemaVersion": 1,
"siteUrl": "https://nahuelsoria.com",
"language": "es",
"locationCode": 2032,
"framework": "next",
"contentDir": "content/blog",
"extension": ".md",
"frontmatter": {
"title": "title",
"description": "description",
"date": "date",
"updatedDate": null,
"image": null,
"tags": "tags",
"draft": null,
"tldr": null,
"faqs": null
},
"bodyH1": false,
"internalLinksMin": 2,
"thinContentWords": 500,
"titleMax": 60,
"descriptionMax": 160,
"tldrMin": 100,
"tldrMax": 400,
"faqsMin": 3,
"faqAnswerMax": 500
}
Script output
linkgraph.mjs inventory, one post, trimmed:
{
"path": "content/blog/nightly-bug-hunter.es.md",
"slug": "nightly-bug-hunter.es",
"outboundInternal": [],
"brokenInternal": ["/es/blog/agent-fleet"],
"wordCount": 1265
}
audit.mjs, two of the failures:
{
"rule": "broken-internal-link",
"file": "content/blog/automated-newsroom.es.md",
"message": "internal link \"/es/blog/agent-fleet\" resolves to no existing post",
"severity": "error"
}
{
"rule": "orphan-page",
"file": "content/blog/agent-fleet.es.md",
"message": "no other post links to this one",
"severity": "advisory"
}
detect.mjs got the repo right, for what it is worth: next, content/blog, .md, and the frontmatter keys.
Framework and file extension
Next.js (App Router, file-based markdown loader), .md
Node version and harness
node 22.23.1, run headless from the CLI
Minimal repro
Two files in contentDir:
post-a.es.md
---
title: Post A
description: whatever
date: 2026-08-01
---
Body.
post-b.es.md
---
title: Post B
description: whatever
date: 2026-08-02
---
A link to [post A](/es/blog/post-a).
audit.mjs reports broken-internal-link on post-b and orphan-page on both.
What happened
I ran
audit.mjsandgate.mjsagainst a bilingual Next.js blog: 10 posts, 5 slugs times 2 locales, named<slug>.<locale>.md(agent-fleet.es.md,agent-fleet.en.md). Content dir iscontent/blog, extension.md.The audit reported 12 errors and 10 advisories. All 22 are false positives, and they trace back to one line.
The 8 broken links are links between my own posts. They resolve fine in production:
Root cause
slugForFileinskill/scripts/linkgraph.mjs:41:agent-fleet.es.mdbecomes the slugagent-fleet.es, so a link to/es/blog/agent-fleetmatches nothing in the slug set. My site derives the route slug with^(.+)\.([a-z]{2})\.md$and serves/[locale]/blog/[slug], which is a common i18n convention in Next, Astro and Hugo.Everything else cascades from that:
broken-internal-link(8) fires on every cross-post link.orphan-page(10) fires on every post, because no link resolves anywhere, so nothing has inbound links.internal-links-minfires too, since resolving links are counted after resolution.The gate blocks a post that has been live for weeks, on two rules that are the same failure:
There is no configuration that avoids this. The failure is in slug derivation, not in the mapping.
Suggested fix
An optional
slugPatterninconfig.json, a regex with one capture group applied to the basename, falling back to current behavior when absent:That keeps
slugForFilea pure function and leaves the default untouched for single-locale repos. A narrower alternative is alocalesarray in config, stripping a trailing.<locale>only for the locales the repo declares, which is harder to misconfigure.Worth deciding alongside it: whether two locale variants of the same slug are one post or two for
cannibalization-fuzzy. Right nowagent-fleet.esandagent-fleet.enare separate posts with different titles, so they do not collide, but a repo with two locales that share a language would.Happy to send the PR if you tell me which of the two shapes you prefer.
Config
{ "schemaVersion": 1, "siteUrl": "https://nahuelsoria.com", "language": "es", "locationCode": 2032, "framework": "next", "contentDir": "content/blog", "extension": ".md", "frontmatter": { "title": "title", "description": "description", "date": "date", "updatedDate": null, "image": null, "tags": "tags", "draft": null, "tldr": null, "faqs": null }, "bodyH1": false, "internalLinksMin": 2, "thinContentWords": 500, "titleMax": 60, "descriptionMax": 160, "tldrMin": 100, "tldrMax": 400, "faqsMin": 3, "faqAnswerMax": 500 }Script output
linkgraph.mjs inventory, one post, trimmed:{ "path": "content/blog/nightly-bug-hunter.es.md", "slug": "nightly-bug-hunter.es", "outboundInternal": [], "brokenInternal": ["/es/blog/agent-fleet"], "wordCount": 1265 }audit.mjs, two of the failures:{ "rule": "broken-internal-link", "file": "content/blog/automated-newsroom.es.md", "message": "internal link \"/es/blog/agent-fleet\" resolves to no existing post", "severity": "error" } { "rule": "orphan-page", "file": "content/blog/agent-fleet.es.md", "message": "no other post links to this one", "severity": "advisory" }detect.mjsgot the repo right, for what it is worth:next,content/blog,.md, and the frontmatter keys.Framework and file extension
Next.js (App Router, file-based markdown loader),
.mdNode version and harness
node 22.23.1, run headless from the CLI
Minimal repro
Two files in
contentDir:post-a.es.mdpost-b.es.mdaudit.mjsreportsbroken-internal-linkon post-b andorphan-pageon both.