{"id":13910959,"url":"https://github.com/mattias-p/linky","last_synced_at":"2025-07-18T10:32:06.023Z","repository":{"id":26483165,"uuid":"108328194","full_name":"mattias-p/linky","owner":"mattias-p","description":"Extract and check links from Markdown files.","archived":false,"fork":false,"pushed_at":"2023-07-11T22:05:43.000Z","size":351,"stargazers_count":10,"open_issues_count":7,"forks_count":4,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-07-06T08:03:49.255Z","etag":null,"topics":["broken-links","cli","cli-app","commonmark","link-checker","markdown"],"latest_commit_sha":null,"homepage":"","language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/mattias-p.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"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}},"created_at":"2017-10-25T21:30:02.000Z","updated_at":"2024-01-09T21:15:19.000Z","dependencies_parsed_at":"2024-11-25T22:47:37.945Z","dependency_job_id":null,"html_url":"https://github.com/mattias-p/linky","commit_stats":{"total_commits":360,"total_committers":6,"mean_commits":60.0,"dds":"0.20277777777777772","last_synced_commit":"e7164e41c222dd0099ebc3e3417fb489679498bd"},"previous_names":[],"tags_count":10,"template":false,"template_full_name":null,"purl":"pkg:github/mattias-p/linky","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mattias-p%2Flinky","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mattias-p%2Flinky/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mattias-p%2Flinky/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mattias-p%2Flinky/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/mattias-p","download_url":"https://codeload.github.com/mattias-p/linky/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mattias-p%2Flinky/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":265742344,"owners_count":23820829,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","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":["broken-links","cli","cli-app","commonmark","link-checker","markdown"],"created_at":"2024-08-07T00:01:51.836Z","updated_at":"2025-07-18T10:32:05.751Z","avatar_url":"https://github.com/mattias-p.png","language":"Rust","funding_links":[],"categories":["Rust"],"sub_categories":[],"readme":"Linky\n=====\n[![Crates.io]][Crates.io badge]\n[![Changelog]][Changelog badge]\n\nExtract links from Markdown files and check links for brokenness.\n\n\nMotivation\n----------\n\nMaintaining Markdown documentation you often end up with lots of links to tend to.\nLinky extracts all links and checks them, pointing out the broken ones so you can fix them.\nSpecifically, linky was created to ease maintenance of Markdown documentation on Github.\n\n\nInstallation\n------------\n\n[Install stable Rust and Cargo]:\n\n```sh\n$ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh\n```\n\nCompile and install linky using Cargo:\n\n```sh\n$ cargo install linky\n```\n\n\nUsage\n-----\n\n### Extracting and checking links\n\nThe simplest thing you can do with linky is to extract links from a Markdown file:\n\n```sh\n$ linky example_site/path/to/example.md\nexample_site/path/to/example.md:3:  https://github.com/mattias-p/linky/blob/master/example_site/path/to/other.md\nexample_site/path/to/example.md:4:  https://github.com/mattias-p/linky/blob/master/example_site/path/to/other.md#existing\nexample_site/path/to/example.md:5:  other.md\nexample_site/path/to/example.md:6:  non-existing.md\nexample_site/path/to/example.md:7:  other.md#existing\nexample_site/path/to/example.md:8:  other.md#non-existing\nexample_site/path/to/example.md:9:  #heading\nexample_site/path/to/example.md:10:  #non-existing\nexample_site/path/to/example.md:11:  #heading-with-code\nexample_site/path/to/example.md:12:  #HEADING\n```\n\nThe output lists all the extracted links along with their respective\ninput files and line numbers.\n\nEnable the --check option to resolve those links:\n\n```sh\n$ linky --check example_site/path/to/example.md\nexample_site/path/to/example.md:3: OK https://github.com/mattias-p/linky/blob/master/example_site/path/to/other.md\nexample_site/path/to/example.md:4: NO_FRAG https://github.com/mattias-p/linky/blob/master/example_site/path/to/other.md#existing\nexample_site/path/to/example.md:5: OK other.md\nexample_site/path/to/example.md:6: NO_DOC non-existing.md\nexample_site/path/to/example.md:7: OK other.md#existing\nexample_site/path/to/example.md:8: NO_FRAG other.md#non-existing\nexample_site/path/to/example.md:9: OK #heading\nexample_site/path/to/example.md:10: NO_FRAG #non-existing\nexample_site/path/to/example.md:11: OK #heading-with-code\nexample_site/path/to/example.md:12: CASE_FRAG #HEADING\n```\n\nA status token is now added to each line indicating the outcome of\nthe resolution.\nAn `OK` token indicates that the resolution succeeded without remarks.\nFor details on how links are resolved see the [link resolution section].\n\n\n### Recursive directory traversal\n\nLinky doesn't do directory traversal on its own.\nInstead it integrates well with find and xargs:\n\n```sh\n$ find example_site -type f -print0 | xargs -0 linky\nexample_site/path/to/absolute.md:2:  /path/to/other.md\nexample_site/path/to/absolute.md:3:  /path/to/non-existing.md\nexample_site/path/to/absolute.md:4:  /path/to/other.md#existing\nexample_site/path/to/absolute.md:5:  /path/to/other.md#non-existing\nexample_site/path/to/example.md:3:  https://github.com/mattias-p/linky/blob/master/example_site/path/to/other.md\nexample_site/path/to/example.md:4:  https://github.com/mattias-p/linky/blob/master/example_site/path/to/other.md#existing\nexample_site/path/to/example.md:5:  other.md\nexample_site/path/to/example.md:6:  non-existing.md\nexample_site/path/to/example.md:7:  other.md#existing\nexample_site/path/to/example.md:8:  other.md#non-existing\nexample_site/path/to/example.md:9:  #heading\nexample_site/path/to/example.md:10:  #non-existing\nexample_site/path/to/example.md:11:  #heading-with-code\nexample_site/path/to/example.md:12:  #HEADING\nexample_site/path/to/follow.md:2:  http://github.com/mattias-p/linky/blob/master/example_site/path/to/other.md\nexample_site/path/to/follow.md:3:  http://github.com/mattias-p/linky/blob/master/example_site/path/to/other.md#non-existing\nexample_site/path/to/fragment.md:2:  https://github.com/mattias-p/linky/blob/master/example_site/path/to/other.md#existing\nexample_site/path/to/fragment.md:3:  https://github.com/mattias-p/linky/blob/master/example_site/path/to/other.md#non-existing\nexample_site/path/to/other.md:2:  example.md\nexample_site/path/to/transform.md:2:  https://github.com/mattias-p/linky/blob/master/example_site/path/to/non-existing.md\nexample_site/path/to/transform.md:3:  https://github.com/mattias-p/linky/blob/master/example_site/path/to/only-on-example-branch.md\n```\n\n\u003e **Note:** In case your paths contain spaces you may need the find -print0 and xargs -0 options.\n\n\n### Absolute local links\n\nBy default linky doesn't resolve absolute local links.\nThis way you're able to know which links do redirect.\nAlso, linky doesn't know the location of the document root.\n\n```sh\n$ linky --check example_site/path/to/absolute.md\nexample_site/path/to/absolute.md:2: ABSOLUTE /path/to/other.md\nexample_site/path/to/absolute.md:3: ABSOLUTE /path/to/non-existing.md\nexample_site/path/to/absolute.md:4: ABSOLUTE /path/to/other.md#existing\nexample_site/path/to/absolute.md:5: ABSOLUTE /path/to/other.md#non-existing\n```\n\nIf you specify the document root using the --root option linky proceeds\nwith the resolution relative to that directory:\n\n```sh\n$ linky --check --root=example_site example_site/path/to/absolute.md\nexample_site/path/to/absolute.md:2: OK /path/to/other.md\nexample_site/path/to/absolute.md:3: NO_DOC /path/to/non-existing.md\nexample_site/path/to/absolute.md:4: OK /path/to/other.md#existing\nexample_site/path/to/absolute.md:5: NO_FRAG /path/to/other.md#non-existing\n```\n\n\n### HTTP redirects\n\nBy default linky doesn't follow HTTP redirects.\nThis way you're able to know which links do redirect.\n\n```sh\n$ linky --check example_site/path/to/follow.md\nexample_site/path/to/follow.md:2: HTTP_301 http://github.com/mattias-p/linky/blob/master/example_site/path/to/other.md\nexample_site/path/to/follow.md:3: HTTP_301 http://github.com/mattias-p/linky/blob/master/example_site/path/to/other.md#non-existing\n```\n\nEnable the --follow option to make linky proceed with the resolution\nacross HTTP redirects:\n\n```sh\n$ linky --check --follow example_site/path/to/follow.md\nexample_site/path/to/follow.md:2: OK http://github.com/mattias-p/linky/blob/master/example_site/path/to/other.md\nexample_site/path/to/follow.md:3: NO_FRAG http://github.com/mattias-p/linky/blob/master/example_site/path/to/other.md#non-existing\n```\n\n\n### URI fragment identifiers\n\nSometimes when Markdown headings are converted into HTML id attributes a prefix is added to the id attribute.\nE.g. Github adds a \"user-content-\" prefix.\n\nFirst, let's just try out the example file without specifying a prefix:\n\n```sh\n$ linky --check example_site/path/to/fragment.md\nexample_site/path/to/fragment.md:2: NO_FRAG https://github.com/mattias-p/linky/blob/master/example_site/path/to/other.md#existing\nexample_site/path/to/fragment.md:3: NO_FRAG https://github.com/mattias-p/linky/blob/master/example_site/path/to/other.md#non-existing\n```\n\nNow, let's try adding that prefix:\n\n```sh\n$ linky --check --prefix='user-content-' example_site/path/to/fragment.md\nexample_site/path/to/fragment.md:2: PREFIXED https://github.com/mattias-p/linky/blob/master/example_site/path/to/other.md#existing\nexample_site/path/to/fragment.md:3: NO_FRAG https://github.com/mattias-p/linky/blob/master/example_site/path/to/other.md#non-existing\n```\n\n\n### Transforming links before resolution\n\nIf you, for example, want to check links against a development version of a sister site you can pipe your links through sed to transform the base URL.\n\nFirst, let's just extract all links from the example file:\n\n```sh\n$ linky example_site/path/to/transform.md\nexample_site/path/to/transform.md:2:  https://github.com/mattias-p/linky/blob/master/example_site/path/to/non-existing.md\nexample_site/path/to/transform.md:3:  https://github.com/mattias-p/linky/blob/master/example_site/path/to/only-on-example-branch.md\n```\n\nUse sed to edit the links so they point to the sister site:\n\n```sh\n$ linky example_site/path/to/transform.md | sed 's,/master/,/example/,'\nexample_site/path/to/transform.md:2:  https://github.com/mattias-p/linky/blob/example/example_site/path/to/non-existing.md\nexample_site/path/to/transform.md:3:  https://github.com/mattias-p/linky/blob/example/example_site/path/to/only-on-example-branch.md\n```\n\n\u003e **Note:** You may need to be careful with your sed expressions so you don't inadvertently transform the path prefixes.\n\nFinally, pipe the edited linky output into another linky process that actually checks the links:\n\n```sh\n$ linky example_site/path/to/transform.md | sed 's,/master/,/example/,' | linky --check\nexample_site/path/to/transform.md:2: HTTP_404 https://github.com/mattias-p/linky/blob/example/example_site/path/to/non-existing.md\nexample_site/path/to/transform.md:3: OK https://github.com/mattias-p/linky/blob/example/example_site/path/to/only-on-example-branch.md\n```\n\n\n### Resolution speed\n\nLinky uses a thread pool for link resolution.\nThe size of the thread pool affects throughput, but the output itself\nremains unaffected.\n\nYou set the size of the thread pool using the `RAYON_NUM_THREADS`\nenvironment variable:\n\n```sh\n$ env RAYON_NUM_THREADS=16 linky --check example_site/path/to/example.md\n```\n\nThe default size of the thead pool is equal to the number of logical\nCPU cores available to the process.\nYou may be able to achieve much higher throughputs with a larger than\ndefault thread pool.\nUse the `time` command to benchmark a suitable size.\n\n\n### Resolution details\n\nIn case you ever wonder why a certain link resolved to whatever status token it got,\nset the `RUST_LOG` environment variable to `warn`.\n\n```sh\n$ env RUST_LOG=warn linky --check example_site/path/to/example.md\nexample_site/path/to/example.md:3: OK https://github.com/mattias-p/linky/blob/master/example_site/path/to/other.md\n WARN  linky \u003e Fragment not found\n WARN  linky \u003e   context: link = https://github.com/mattias-p/linky/blob/master/example_site/path/to/other.md\n WARN  linky \u003e   context: fragment = #existing\nexample_site/path/to/example.md:4: NO_FRAG https://github.com/mattias-p/linky/blob/master/example_site/path/to/other.md#existing\nexample_site/path/to/example.md:5: OK other.md\n WARN  linky \u003e Document not found\n WARN  linky \u003e   context: link = /tmp/linky/example_site/path/to/non-existing.md\n WARN  linky \u003e   caused by: No such file or directory (os error 2)\nexample_site/path/to/example.md:6: NO_DOC non-existing.md\nexample_site/path/to/example.md:7: OK other.md#existing\n WARN  linky \u003e Fragment not found\n WARN  linky \u003e   context: link = /tmp/linky/example_site/path/to/other.md\n WARN  linky \u003e   context: fragment = #non-existing\nexample_site/path/to/example.md:8: NO_FRAG other.md#non-existing\nexample_site/path/to/example.md:9: OK #heading\n WARN  linky \u003e Fragment not found\n WARN  linky \u003e   context: link = /tmp/linky/example_site/path/to/example.md\n WARN  linky \u003e   context: fragment = #non-existing\nexample_site/path/to/example.md:10: NO_FRAG #non-existing\nexample_site/path/to/example.md:11: OK #heading-with-code\n WARN  linky \u003e Fragment not found case-sensitively\n WARN  linky \u003e   context: link = /tmp/linky/example_site/path/to/example.md\n WARN  linky \u003e   context: fragment = #HEADING\n WARN  linky \u003e   context: anchor = #heading\nexample_site/path/to/example.md:12: CASE_FRAG #HEADING\n```\n\n\nLink resolution\n---------------\n\nLocal links are resolved to readable ordinary files and directories in the local filesystem.\nHTTP(S) links are resolved using GET requests to 200-responses, optionally following redirects.\nTarget documents are read and decoded into character strings.\n\nFor HTTP(S) links fragments are resolved to HTML anchors.\nFor local links fragments are resolved to Markdown headings.\nFragment resolution is attempted first without any prefix and then,\nif that fails, with each of the prefixes, if any were provided.\n\n\nIssues\n------\n\nIn case you experience an issue using `linky`, it would be most helpful\nif you provide a test Markdown document and the verbose output of the\nexecution.\n\nFor example:\n\n```sh\n$ env RUST_LOG=debug RUST_BACKTRACE=1 linky --check  test.md 2\u0026\u003e linky_err.log\n```\n\n\u003e **Note:** `RUST_LOG` controls the logging verbosity.\n\u003e `RUST_BACKTRACE` controls the printing of the stack trace on panic.\n\nSimply drag-and-drop the resulting `linky_err.log` file into the issue\neditor of Github.\nUnfortunately, as of February 2018, Github does not allow to drag-and-drop\nof Markdown files (\\*.md). You can either:\n\n- rename your `test.md` file to `test.md.txt`\n- use a third party paste service (e.g. [Hastebin](https://hastebin.com/))\n- if the file is not large, inline it into the issue inside code blocks:\n  \n  ´´´Markdown\n  \n  your test markdown here ...\n  \n  ´´´\n\n\nLicense\n-------\n\nCopyright 2017-2019 Mattias Päivärinta\n\nLicensed under the [Apache License, Version 2.0] (the \"License\");\nyou may not use any of the files in this distribution except in compliance with\nthe License. You may obtain a copy of the License at\n\n\u003chttp://www.apache.org/licenses/LICENSE-2.0\u003e\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n\n\n[Apache License, Version 2.0]: LICENSE\n[Changelog badge]: https://github.com/mattias-p/linky/blob/master/CHANGELOG.md\n[Changelog]: https://raw.githubusercontent.com/mattias-p/linky/master/img/changelog.svg?sanitize=true\n[Crates.io badge]: https://crates.io/crates/linky\n[Crates.io]: https://img.shields.io/crates/v/linky.svg\n[Install stable Rust and Cargo]: https://www.rust-lang.org/en-US/install.html\n[Link resolution section]: #link-resolution\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmattias-p%2Flinky","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmattias-p%2Flinky","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmattias-p%2Flinky/lists"}