Internal Links in Markdown: Every Syntax That Works
Markdown internal links across GitHub, Obsidian, Notion, MkDocs, and Docusaurus: inline syntax, heading anchors, wikilinks, and the gotchas.
Nedim Mehić
August 9, 2026 · 7 min read

The universal Markdown internal link is [link text](/path/to/page): square brackets for the text, parentheses for the destination. Linking to a section uses a fragment built from the heading: [see setup](#getting-started). The complications are all platform-specific: GitHub, Obsidian, Notion, MkDocs, and Docusaurus each generate heading anchors by slightly different rules, and Obsidian adds a whole second syntax (wikilinks). This is the complete reference.
The core syntax: inline links
[our guide to pillar pages](/blog/pillar-pages)
Renders as a normal HTML anchor: <a href="/blog/pillar-pages">our guide to pillar pages</a>. Everything that makes a good HTML link makes a good Markdown link, because that's what it compiles to: root-relative paths for same-site links, and descriptive text in the brackets rather than "here" or "this page." (The rendered-HTML side (absolute vs. relative resolution, id attributes, accessibility) is covered in our companion on internal links in HTML; this post stays in the source files.)
Three inline-syntax gotchas:
- Spaces in paths break the link.
[text](/my page)won't parse. URL-encode (/my%20page) or wrap the destination in angle brackets:[text](</my page>). - Parentheses in URLs need escaping:
[text](/page\(v2\))or angle brackets again. - Relative file links vs. site links. In repos and doc generators you often link to the file (
./setup.md); on rendered sites you link to the URL (/docs/setup). Which one is correct depends on the platform; covered per-platform below.
Linking to headings: how anchor slugs are generated
Markdown renderers auto-generate an id for every heading, and you link to it with #:
## Getting Started: First Steps
[jump to setup](#getting-started-first-steps)
[from another page](/docs/install#getting-started-first-steps)
The universal baseline rules (GitHub's flavor, which most tools copy): lowercase everything, drop punctuation, replace spaces with hyphens. ## Getting Started: First Steps → #getting-started-first-steps.
Where platforms diverge is the edge cases, and the edge cases are where your links break:
| Case | Punctuation | Spaces | Duplicate headings | |
|---|---|---|---|---|
| GitHub | lowercased | stripped (hyphens kept) | - | suffix -1, -2 |
| MkDocs | lowercased | stripped | - | suffix _1 (note underscore) |
| Docusaurus | lowercased | stripped | - | suffix -1; custom ids supported |
| Obsidian | preserved in syntax | preserved | kept as spaces in [[Note#Heading]] | first match wins |
| Notion | n/a (block ids, not slugs) | n/a | n/a | n/a |
Practical consequences of that table:
- Emoji and symbols in headings are anchor poison.
## 🚀 Deployslugs differently across platforms (GitHub:#-deploy). Keep headings you intend to link to as plain text. - Duplicate heading suffixes differ (
-1vs_1), so documents with repeated headings ("Example", "Example") don't port cleanly between GitHub and MkDocs. - Renaming a heading silently breaks every inbound fragment link: the reader just lands at the top of the page. Treat heading renames like URL changes.
Reference-style links
Markdown's second syntax separates the text from the URL, defining destinations once at the bottom of the document:
Both [the quickstart][qs] and [the API reference][api] cover auth.
[qs]: /docs/quickstart
[api]: /docs/api
Reference style shines in two situations: documents that link to the same destination many times (update the URL in one place), and prose-heavy files where inline URLs hurt readability of the source. The shortcut form [quickstart][] uses the text itself as the label. It's supported essentially everywhere, including GitHub; it's core Markdown, not an extension. The main gotcha: labels are case-insensitive but must match; a typo in the label renders the brackets as literal text rather than erroring, so broken reference links hide in plain sight.
Platform specifics
GitHub
- Link to files, not URLs, within a repo:
[setup guide](./docs/setup.md). GitHub resolves relative file paths from the current file's location, and the links work in both the repo browser and most downstream renderers. - Section links follow the slug rules above:
[architecture](./DESIGN.md#system-architecture). - READMEs get automatic tables of contents in the UI, but in-document
[](#section)links are still the portable approach.
Obsidian: the wikilink world
Obsidian's native syntax is the double-bracket wikilink, linking notes by name rather than path:
[[Topic Clusters]] → link to the note by name
[[Topic Clusters|our cluster guide]] → custom display text (pipe)
[[Topic Clusters#Linking Rules]] → link to a heading
[[Topic Clusters#^quoteblock]] → link to a specific block (^ block id)
Notes: headings inside wikilinks keep their original case and spaces (no kebab-casing); the pipe character sets display text (opposite argument order from standard Markdown: target first, text second); and Obsidian updates wikilinks automatically when you rename a note, which standard Markdown links in the same vault only get if you enable it. Obsidian also supports standard [text](path) syntax; necessary if the vault is published through a standard Markdown pipeline, since wikilinks are non-portable outside tools that support them (Obsidian, some static-site plugins). If your notes might ever leave Obsidian, standard syntax is the safer habit.
Notion
Notion looks like Markdown while you type, but links are a different mechanism: pages and blocks have permanent ids, and internal links are created with @-mentions (@Page Name) or by pasting a page URL (choosing "mention" or "link" from the prompt). Because links target ids, they survive page renames and moves (genuinely nice), but there are no heading slugs; you link to a block by copying its link (block menu → "Copy link to block"). When you export Notion to Markdown, internal links become URLs with long id hashes, which is a real migration tax to plan for.
MkDocs
Link to the source file path, relative to the current file: [install](../getting-started/install.md). MkDocs rewrites file links to final URLs at build time and (crucially) validates them, warning on broken links during build. Always link the .md file, not the pretty URL: [install](/getting-started/install/) builds without warning but bypasses validation and breaks if the site moves to a subpath. Duplicate-heading anchors get _1 suffixes, and the autorefs plugin ecosystem can resolve headings by title across the whole site.
Docusaurus
Also file-path based: [tutorial](./tutorial.md). Relative file paths are rewritten to routes and validated at build (broken links fail the production build, configurably). Docusaurus additionally supports explicit heading ids, which is the fix for fragile auto-slugs:
## Long Heading That Might Get Reworded {#stable-id}
[link that survives rewording](./page.md#stable-id)
If you're on a platform that supports custom heading ids (Docusaurus, MkDocs via attr_list), use them for any heading other documents link to.
The one habit that prevents most breakage
Link to the most stable identifier the platform offers: file paths where the builder validates them (MkDocs, Docusaurus), explicit heading ids where supported, block/page ids in Notion and Obsidian. Auto-generated heading slugs are the least stable identifier in the whole system; they change whenever a writer rewords a heading, and nothing warns you.
Common gotchas, collected
- Reworded headings = silently broken fragments. No 404, no build error on most platforms; the reader lands at the page top. Grep for the old slug after editing headings.
- Case sensitivity mismatches.
[x](#Getting-Started)fails on GitHub (slugs are lowercase); wikilink headings in Obsidian are case-tolerant. Don't rely on either behavior when writing portable docs. - Wikilinks leaking into portable Markdown.
[[Like This]]renders as literal brackets on GitHub and most static site generators. - Linking rendered URLs instead of source files in MkDocs/Docusaurus: skips build-time validation, the only free link checking you get.
- Bare URLs without brackets. Some renderers auto-link
https://…text, some (strict CommonMark) don't. Always use explicit[text](url)or<https://url>. - Trailing punctuation glued to bare links: the classic
(see https://example.com/page).where the.or)becomes part of the URL.
Markdown links at publishing scale
Everything above is syntax: necessary, but the smaller half of the problem. On a docs site or Markdown-based blog with a few hundred pages, the real questions become which pages should reference each other and whether yesterday's pages link to today's: the strategy layer covered in our complete guide to internal linking. And unlike WordPress (where an ecosystem of plugins handles link upkeep, as our WordPress internal linking guide covers), Markdown sites usually have no linking tooling at all beyond build-time 404 checks.
Two habits close the gap. First, run a crawler-based internal link checker against the rendered site, not the source files; it catches what builders can't: orphan pages nothing links to, fragment links to renamed headings, and redirect chains, across every page at once. Second, when adding links to old posts, keep anchors as phrases that already exist in the prose; retrofitted links read naturally when the linked words were already there, which is exactly the constraint good tooling enforces automatically.
The bottom line
Master three things and Markdown internal links stop breaking: the two core syntaxes (inline and reference-style), your platform's heading-slug rules (and its stable-id alternative: custom ids, file-path links, or block ids), and the discipline of checking the rendered site rather than trusting the source. The syntax takes ten minutes; the platforms' edge cases are what this page is for; the site-wide strategy is a different discipline worth learning next.
Related reading
Internal Links in HTML: Syntax, Anchors, Jump Links
The complete HTML internal link reference: <a href> syntax, relative vs absolute paths, jump links with id fragments, and accessible link text.
Internal Linking: The Complete SEO Guide
What internal links are, why they move rankings, and exactly how to audit, build, and maintain internal linking that compounds. The complete guide.
How to Add Internal Links in WordPress (3 Ways)
Add internal links in WordPress three ways: manually in Gutenberg or Classic, with Yoast or Rank Math suggestions, or with automation plugins.
Put this on autopilot
Linkagent finds and ships internal links for you. Scan your site free, no account needed.
Free scan, no account needed. Takes about 20 seconds.