{"id":51417772,"url":"https://github.com/didrod205/linklint","last_synced_at":"2026-07-04T21:30:39.025Z","repository":{"id":368145730,"uuid":"1255704294","full_name":"didrod205/linklint","owner":"didrod205","description":"Catch broken links \u0026 dead anchors in Markdown/HTML docs — relative links, GitHub-slug #anchors, cross-file anchors, images, references. Deterministic, offline, runs in CI. A fast internal-integrity link checker.","archived":false,"fork":false,"pushed_at":"2026-06-29T07:24:57.000Z","size":58,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-29T09:17:30.708Z","etag":null,"topics":["anchor","broken-link-checker","cli","dead-link","docs","documentation","link-checker","link-lint","linkcheck","markdown","markdown-link","static-analysis","typescript"],"latest_commit_sha":null,"homepage":"https://www.npmjs.com/package/@didrod2539/linklint","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/didrod205.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null},"funding":{"github":"didrod205"}},"created_at":"2026-06-01T05:14:09.000Z","updated_at":"2026-06-29T07:25:00.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/didrod205/linklint","commit_stats":null,"previous_names":["didrod205/linklint"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/didrod205/linklint","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/didrod205%2Flinklint","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/didrod205%2Flinklint/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/didrod205%2Flinklint/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/didrod205%2Flinklint/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/didrod205","download_url":"https://codeload.github.com/didrod205/linklint/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/didrod205%2Flinklint/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35136712,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-07-04T02:00:05.987Z","response_time":113,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["anchor","broken-link-checker","cli","dead-link","docs","documentation","link-checker","link-lint","linkcheck","markdown","markdown-link","static-analysis","typescript"],"created_at":"2026-07-04T21:30:38.425Z","updated_at":"2026-07-04T21:30:38.952Z","avatar_url":"https://github.com/didrod205.png","language":"TypeScript","funding_links":["https://github.com/sponsors/didrod205"],"categories":[],"sub_categories":[],"readme":"\u003cdiv align=\"center\"\u003e\n\n# 🔗 linklint\n\n### Catch broken links \u0026 dead anchors in your docs — locally, no network.\n\n[![npm version](https://img.shields.io/npm/v/@didrod2539/linklint.svg?color=success)](https://www.npmjs.com/package/@didrod2539/linklint)\n[![CI](https://github.com/didrod205/linklint/actions/workflows/ci.yml/badge.svg)](https://github.com/didrod205/linklint/actions/workflows/ci.yml)\n[![node](https://img.shields.io/node/v/@didrod2539/linklint.svg)](https://www.npmjs.com/package/@didrod2539/linklint)\n[![license](https://img.shields.io/npm/l/@didrod2539/linklint.svg)](./LICENSE)\n\nA deterministic CLI that checks the **internal integrity** of your Markdown/HTML\ndocs: broken relative links, dead `#anchors` (using GitHub's slug rules),\ncross-file anchors (`other.md#section`), missing images, and undefined\nreferences — **without making a single network request**. Score, A–F grade, and\nJSON/Markdown reports.\n\n\u003c/div\u003e\n\n---\n\n## One-line summary\n\n`linklint` builds a link/anchor graph of your docs and reports every internal\nlink that points at a file, heading, or image that doesn't exist — fast,\ndeterministic, and offline.\n\n## Why this project exists\n\nWhen you rename a file, restructure `docs/`, or edit a heading, links break\n**silently**:\n\n- A relative link points at a file you moved — readers hit a 404.\n- A table-of-contents anchor (`#installation`) survives, but you renamed the\n  heading — the link now scrolls nowhere.\n- A cross-file link `../api.md#auth` references a section that was deleted.\n- An image `![](./diagram.png)` lost its file in a refactor.\n\nThese never fail your build, so **users find them before you do** — and broken\ndocs erode trust, hurt SEO, and slow onboarding. Existing checkers like\n`markdown-link-check` and `lychee` focus on **external URLs**, which is slow and\nrate-limited. `linklint` does the part that's fully knowable and deterministic:\n**internal** link \u0026 anchor integrity. It's the perfect pre-commit / CI gate.\n\n## Key features\n\n- 📄 **Relative file links** — resolves and verifies every `[x](./path)` link\n  (with directory-`index`/`README` fallback and extension-less resolution).\n- ⚓ **Anchor checks** — validates `#section` against headings using **GitHub's\n  slug algorithm** (including duplicate `-1`/`-2` suffixes) and explicit `id`s.\n- 🔀 **Cross-file anchors** — checks `other.md#heading` against the *target*\n  document's headings, project-wide.\n- 🖼️ **Images** — flags missing local image sources.\n- 🧩 **Reference links** — resolves `[text][id]` to its definition and verifies\n  it; flags undefined references.\n- 🧹 **Hygiene** — empty links, malformed `mailto:`, absolute filesystem paths,\n  ambiguous duplicate-heading anchors, and optional orphan-document detection.\n- 📊 **Score + A–F grade**, JSON/Markdown export, **CI gate** exit codes.\n- 🔒 **No network.** Internal integrity only — deterministic and instant.\n\n## Install\n\n```bash\n# run without installing\nnpx @didrod2539/linklint scan\n\n# or install\nnpm install -g @didrod2539/linklint    # global CLI (provides `linklint`)\nnpm install -D @didrod2539/linklint    # project dev-dependency (for CI)\n```\n\nNode ≥ 18. ESM + CJS + TypeScript types.\n\n## Quick start\n\n```bash\n# scan the current directory (Markdown + HTML)\nlinklint scan\n```\n\n```\ndocs/README.md  31/100 (F)  13 links\n  ✗ Link target not found: \"missing-page.md\":13\n  ✗ Anchor \"#does-not-exist\" has no matching heading or id in this document:9\n  ✗ Anchor \"#no-such-heading\" not found in docs/guide.md:12\n  ✗ Image not found: \"images/missing.png\":16\n\ndocs/guide.md  74/100 (C)  3 links\n  ✗ Reference \"[nope]\" is never defined:17\n  ⚠ Multiple headings slug to \"#setup\" — anchors get -1/-2 suffixes:7\n\nOverall  68/100 (D)  3 doc(s), 17 link(s), 6 error(s), 1 warning(s), 0 info\n```\n\n## CLI usage\n\n```bash\nlinklint scan [...targets]    # check docs (files or directories; default: .)\nlinklint report \u003cinput.json\u003e  # re-render a saved JSON report as Markdown\nlinklint init                 # scaffold linklint.config.json\nlinklint --help\nlinklint --version\n```\n\n`scan` options:\n\n| Option | Description |\n| --- | --- |\n| `--config \u003cfile\u003e` | Path to a config file (otherwise auto-detected) |\n| `--external` | Also report external (http) links as info |\n| `--orphans` | Flag documents nothing links to |\n| `--json \u003cfile\u003e` | Write a JSON report |\n| `--md \u003cfile\u003e` | Write a Markdown report |\n| `--min-score \u003cn\u003e` | Exit non-zero if the overall score \u003c n |\n| `--quiet` | Hide info-level issues in the console |\n\nWithout `--min-score`, `scan` exits non-zero if there are **any errors** — so\nit's a CI gate out of the box. Pointed at a directory it recurses, skipping\n`node_modules`, `.git`, build folders, etc.\n\n## Example result\n\nFull reports for the bundled sample docs are in\n[`examples/sample-report.md`](./examples/sample-report.md) and\n[`examples/sample-report.json`](./examples/sample-report.json).\n\n\u003e 📸 _Screenshot / demo GIF placeholder:_ `./docs/screenshot.png` — record the\n\u003e terminal running `npx @didrod2539/linklint scan examples/docs`.\n\n## Configuration\n\nCreate `linklint.config.json` (or run `linklint init`):\n\n```json\n{\n  \"extensions\": [\".md\", \".markdown\", \".html\", \".htm\"],\n  \"ignoreDirs\": [\"node_modules\", \".git\", \"dist\", \"build\"],\n  \"reportExternal\": false,\n  \"checkOrphans\": true,\n  \"ignoreTargets\": [\"CHANGELOG\"],\n  \"minScore\": 90,\n  \"ruleSeverity\": { \"ambiguous-anchor\": \"warning\" }\n}\n```\n\n| Field | Meaning |\n| --- | --- |\n| `extensions` | File extensions to scan |\n| `ignoreDirs` | Directory names to skip while walking |\n| `reportExternal` | Emit external `http(s)` links as info (not network-checked) |\n| `checkOrphans` | Flag documents that nothing links to |\n| `ignoreTargets` | Substrings; matching link targets are skipped |\n| `minScore` | CI gate threshold (overridable with `--min-score`) |\n| `ruleSeverity` | Override severity per rule id |\n\nRule ids: `broken-file-link`, `broken-anchor`, `broken-cross-anchor`,\n`broken-image`, `undefined-reference`, `ambiguous-anchor`, `empty-link`,\n`mailto-format`, `absolute-path-link`, `external-link`, `orphan-document`.\n\n## Real-world use cases\n\n1. **Gate docs in CI.** Add `linklint scan` to your workflow. A PR that renames\n   a file or a heading and leaves a stale link fails the build before the broken\n   doc ships.\n2. **Pre-commit safety net.** Run `linklint scan docs/ README.md` in a\n   pre-commit hook so internal links are verified at authoring time — no browser,\n   no network, instant.\n3. **Audit a large docs site or wiki.** `linklint scan . --orphans --md\n   link-audit.md` produces a graded report of every broken link, dead anchor,\n   and orphaned page across hundreds of files.\n\n## Programmatic API\n\n```ts\nimport { buildProjectFromInputs, analyze, toMarkdown } from \"@didrod2539/linklint\";\n\nconst project = buildProjectFromInputs(root, inputs); // inputs: { path, content }[]\nconst report = analyze(project, config);\nconsole.log(report.summary.errors, report.documents);\nawait fs.writeFile(\"links.md\", toMarkdown(report));\n```\n\n## Roadmap\n\n- Optional external link checking (opt-in, cached, rate-limited).\n- AsciiDoc and reStructuredText support.\n- `--fix` to update links when files are renamed (with a moves map).\n- Auto-suggest the closest heading slug for a typo'd anchor (basic version ships\n  in the \"Did you mean…?\" detail).\n- Monorepo / multi-root awareness and a base-path option for site builders.\n- A GitHub Action with PR annotations on changed docs.\n\n## FAQ\n\n**Does it check external URLs (http/https)?**\nNo — by design. External checking is slow, flaky, and rate-limited. linklint\nfocuses on **internal** integrity, which is deterministic and instant. Use\n`--external` to at least *list* external links, and pair with a dedicated\nexternal checker if you need it.\n\n**How does anchor checking work?**\nIt computes each heading's slug with **GitHub's algorithm** (lowercase, strip\npunctuation, spaces→hyphens, duplicate `-1`/`-2` suffixes) and matches your\n`#anchor` against those slugs plus any explicit HTML `id`/`\u003ca name\u003e`.\n\n**Does it handle `other.md#section`?**\nYes. linklint builds a project-wide index, so cross-file anchors are validated\nagainst the *target* document's actual headings.\n\n**Markdown and HTML?**\nBoth. Markdown is parsed with a dependency-free, code-fence-aware parser; HTML\nwith a fast static parser. Links inside code spans/fences are correctly ignored.\n\n**Will it have false positives on templated links?**\nUse `ignoreTargets` (substring match) for generated or templated paths, or\n`ruleSeverity`/`ignoreDirs` to tune. linklint errs toward correctness on real\nrelative links.\n\n## Contributing\n\nContributions welcome! Each check is a small, self-contained rule in\n`src/rules/`. See [CONTRIBUTING.md](./CONTRIBUTING.md) and the\n[Code of Conduct](./CODE_OF_CONDUCT.md).\n\n```bash\ngit clone https://github.com/didrod205/linklint.git\ncd linklint\nnpm install\nnpm test\nnpm run build\nnode dist/cli.js scan examples/docs\n```\n\n## License\n\n[MIT](./LICENSE) © linklint contributors\n\n## 💖 Sponsor\n\nlinklint is free, MIT-licensed, and built in spare time. If it caught a broken\nlink before your readers did, please consider supporting it:\n\n- ⭐ **Star this repo** — free, and it helps others find it.\n- 🍋 **[Sponsor via Lemon Squeezy](https://elab-studio.lemonsqueezy.com/checkout/buy/5d059b89-51d0-456b-b33a-ed56994f7010)** — one-time or recurring.\n\n**Where your support goes:** opt-in external checking, AsciiDoc/reST support, a\n`--fix` mode for renames, a PR-annotating GitHub Action, and fast issue\nresponses.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdidrod205%2Flinklint","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdidrod205%2Flinklint","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdidrod205%2Flinklint/lists"}