Link text is one of those things I never thought about until I started paying attention to how people actually move through a page. Most readers don't read every word. They scan, and links stand out because they look different from everything around them. If the highlighted words are "click here" or "read more", the scan tells them nothing.
It matters even more for people using a screen reader. Many screen readers can list all the links on a page so you can jump straight to the one you want. A list that says "here, here, read more, this, here" is useless. A list that says "keyboard shortcut cheat sheet, style guide, accessibility checklist" works without any surrounding text at all.
So I made a rule for myself: every link should make sense if you read only the linked words. Then I wrote a tiny script, because I knew I would forget the rule by Thursday.
What a good link looks like
The fix is almost always to move the link onto the words that describe the destination. A few before-and-after pairs from my own drafts:
<!-- Before -->
For the full list of shortcuts, [click here](https://example.com/shortcuts).
<!-- After -->
Here is the [full list of keyboard shortcuts](https://example.com/shortcuts).
<!-- Before -->
The style guide is at [https://example.com/style](https://example.com/style).
<!-- After -->
Everything about tone and spelling is in our [house style guide](https://example.com/style).
The second one is sneaky. A bare URL as link text feels precise, but a screen reader may read it out character by character, and on a phone a long URL can wrap in strange places. A URL only belongs on screen when the reader genuinely needs to see or copy the address, like in printed material.
A couple of extra habits I picked up along the way:
- If two links on the same page go to different places, give them different text.
- Put the important words at the start of the link. "Pricing for teams" scans better than "Find out more about our pricing for teams".
- Don't add "link to" in the text. It's already a link.
The script
My drafts live as Markdown files in one folder, so the check only needs to read .md files and look for inline links. Here it is:
#!/usr/bin/env python3
"""Flag vague link text in Markdown files."""
import re
import sys
from pathlib import Path
VAGUE = {
"click here", "here", "this", "this link", "link",
"read more", "more", "learn more", "this page", "this article",
}
# [text](url) but not images 
LINK = re.compile(r"(?<!!)\[([^\]]+)\]\(([^)\s]+)[^)]*\)")
def check(path):
problems = 0
lines = path.read_text(encoding="utf-8").splitlines()
for number, line in enumerate(lines, start=1):
for match in LINK.finditer(line):
text = match.group(1).strip()
cleaned = text.lower().strip(".,:;!?")
if cleaned in VAGUE:
reason = "vague link text"
elif cleaned.startswith(("http://", "https://", "www.")):
reason = "bare URL as link text"
else:
continue
print(f"{path}:{number}: {reason}: [{text}]")
problems += 1
return problems
def main():
folder = Path(sys.argv[1]) if len(sys.argv) > 1 else Path(".")
total = sum(check(p) for p in sorted(folder.rglob("*.md")))
print(f"{total} link(s) to look at")
return 1 if total else 0
if __name__ == "__main__":
sys.exit(main())
The regular expression matches [text](url) and skips images, which start with !. The [^)]* at the end allows for an optional title like [text](url "title"). Punctuation is stripped before comparing, so [read more.] is still caught.
Save it as check_links.py and point it at a folder:
python3 check_links.py drafts
On a test file the output looks like this:
drafts/newsletter.md:3: vague link text: [click here]
drafts/newsletter.md:4: bare URL as link text: [https://example.com/style]
drafts/newsletter.md:6: vague link text: [read more.]
3 link(s) to look at
The file:line format is handy because most editors, including VS Code's terminal, turn it into something you can click to jump straight to the line.
Running it before I publish
The script exits with code 1 when it finds something, so it plays nicely with anything that cares about exit codes. I run it as part of my usual pre-publish routine, but you could just as easily drop it into a Git pre-commit hook or a CI step next to other checks.
What it won't catch
It's deliberately simple. It ignores reference-style links ([text][ref]) and HTML <a> tags, and the list of vague phrases is short. That's fine for me, because the point isn't a perfect audit. The point is a nudge at the moment I'm most likely to get lazy, which is right at the end of a draft.
Feel free to grow the VAGUE set with your own bad habits. Mine now includes "this", which I apparently type far more often than I'd like to admit.
Top comments (0)