Every README, docs site and blog post that explains code eventually links into a repository. And every one of those links has to make a choice, often without anyone noticing:
https://github.com/acme/widget/blob/main/src/parser.ts#L42
https://github.com/acme/widget/blob/9f3c2e1/src/parser.ts#L42
The first follows the branch. The second is pinned to a commit (GitHub calls it a permalink. Press y on a file page to get one).
I've changed my mind on this a few times, so I'd like to lay out both sides and hear how your team handles it.
The case for branch links (main)
They show current code. A reader following a link from your docs wants to see how things work now, not how they worked two years ago.
They read naturally. blob/main/src/parser.ts tells you exactly what you're looking at.
They break loudly. And that might be the best argument for them. When a file moves or is renamed, the link 404s. A link checker catches it, someone fixes it, and the docs stay honest.
But: line anchors drift silently. #L42 pointed at parseHeader() when you wrote the post. Six refactors later, line 42 is a blank line in the middle of an unrelated function. No 404, no warning. The link "works", and it's wrong.
Also, branches get renamed. Plenty of repos moved from master to main. GitHub redirects many of those, but not every tool or mirror does, and you're relying on that redirect staying in place.
The case for permalinks (commit SHA)
They never break. As long as the commit exists in the repository, the link resolves to exactly the code you meant, with the right lines highlighted.
They're correct by definition. A blog post explaining a bug fix should show the code as it was. A permalink is also the right choice for code review comments and incident writeups, where history is the point.
But: they go stale without telling anyone. The reader sees code that might have been rewritten, renamed or deleted since. Nothing signals that it's old, apart from a small commit hash most people don't notice.
And "never break" has small print. Force-pushes and history rewrites can make commits unreachable, repos get deleted or made private, and mirrors don't always keep every object.
A third option: tags
https://github.com/acme/widget/blob/v2.4.0/src/parser.ts#L42
For docs that track releases, linking to a version tag is a reasonable middle ground. It's pinned like a permalink, but readable, and it signals its age clearly ("this is v2.4.0"). When you publish docs for v3, you update the links deliberately.
What I do now (and why I'm not sure)
My current rules:
- Blog posts, incident writeups, review comments: permalinks. The code at that moment is the point.
- Versioned docs: tag links, updated per release.
- "Getting started" READMEs: branch links to files or directories, never to line numbers. Lines drift, files rarely do.
- Link checking in CI for everything, so branch links that 404 after a move are caught the same day.
The line-number rule does most of the work. In my experience, most "the docs link to the wrong code" complaints come from line anchors on branch links, not from 404s.
For the checking part, a small script is enough to start:
# list GitHub branch links with line anchors in your docs, which are the risky ones
grep -rhoE 'https://github\.com/[^ )"]+/blob/(main|master)/[^ )"]+#L[0-9]+' docs/ | sort -u
Run a link checker (your own script, or a crawler like linkbruno.com for a published docs site) to catch the 404s. Line drift needs a human or a smarter script, because no status code reports it.
Catching line drift with a script
You can partly automate the drift problem. Store the text you expect at the anchored line alongside the link, and compare it with what's there now:
# check_anchors.py: compares expected code at GitHub line anchors with the raw file
import re, sys, urllib.request
LINK = re.compile(r"https://github\.com/([^/]+)/([^/]+)/blob/([^/]+)/([^#\s)]+)#L(\d+)")
def line_at(owner, repo, ref, path, n):
raw = f"https://raw.githubusercontent.com/{owner}/{repo}/{ref}/{path}"
with urllib.request.urlopen(raw, timeout=10) as r:
lines = r.read().decode("utf-8", "replace").splitlines()
return lines[n - 1].strip() if n <= len(lines) else None
# usage: python3 check_anchors.py "<github link>" "expected snippet"
m = LINK.match(sys.argv[1])
got = line_at(*m.groups()[:4], int(m.group(5)))
print("OK" if got and sys.argv[2] in got else f"DRIFTED: line is now {got!r}")
It's crude, since you have to record the expected snippet, but even running it on your ten most-visited docs links will find surprises.
A CI check for line-anchored branch links
If you choose branch links, the riskiest ones are those with a line anchor (#L42), because they can drift without ever returning a 404. A small CI step can at least make them visible in review:
#!/usr/bin/env bash
# fail the PR if it adds new main-branch links with line anchors
set -euo pipefail
added=$(git diff --unified=0 origin/main -- docs/ \
| grep '^+' \
| grep -oE 'github\.com/[^ )"]+/blob/(main|master)/[^ )"]+#L[0-9]+' || true)
if [ -n "$added" ]; then
echo "New line-anchored branch links (consider a permalink or tag):"
echo "$added"
exit 1
fi
It only looks at lines added in the pull request, so existing links don't block anyone. The author then makes a conscious choice: switch to a permalink, or override the check because the link should follow main. Most of the value comes from that moment of decision, not from the script itself.
Where I'm unsure
- Should "getting started" docs ever use permalinks? Stability is attractive, but showing beginners stale code seems worse than an occasional 404.
- Is GitHub's branch-rename redirect something to rely on? It's convenient, but it's still a dependency.
- Should we link to code at all? Some teams embed snippets in the docs and test them, so links aren't needed. That moves the rot problem into the docs build, which at least fails visibly.
Related reading
- Check links in Markdown docs with a 60-line Python script
- Anchor link not working? How to find and fix broken #section links
- Should a broken link fail your CI build? Block internal links, warn on external ones
- The anatomy of a URL, and which part of it broke your link
So which do you use: main, permalinks or tags? And has a drifting line anchor ever sent a reader to completely the wrong code? Tell me in the comments. I'm genuinely trying to settle this for our own docs.
Top comments (0)