Markdown のアンカーリンクを理解したい

# Markdown のアンカーリンクを理解したい

Markdown では、ページ内の見出しに移動するアンカーリンク（ページ内リンク）を作成することが出来る。

このアンカーリンクは、基本的に見出し項目をそのまま書き下すことで作成できるが、そのまま書き下すだけでは利用できない場合が数多くある。これがどうして発生するのか、どのように解決すれば良いかを理解したい。

## 🔗 アンカーリンクの作成

Markdown リンクを利用することで、特定の見出しへのリンクを作成することが出来る。これをクリックすることで、ページ内の見出しに移動することが出来る。

```markdown
[このリンクは “参考” 見出しにリンクされる](#参考)。
[このリンクは “余談 𒐈” 見出しにリンクされる][label]。

[label]: #余談-𒐈
```

[このリンクは “参考” 見出しにリンクされる](#参考)。
[このリンクは “余談 𒐈” 見出しにリンクされる][label]。

[label]: #余談-𒐈

このリンクは 1 つの `#` と見出し項目を利用することで作成できる。このリンクは **アンカーリンク (anchor link)** と呼ばれる。また、この `#` から続くアドレスを **アンカー ID** と呼ぶこととする。

実は、この見出しにアンカー ID が与えられる仕様は CommonMark になく、独自拡張となっている。実際、Markdown 見出しは HTML に変換される際、以下のようになっている。

```markdown
## ■ CommonMark 仕様の見出し
  ↓
<h2>■ CommonMark 仕様の見出し </h2>

## ■ アンカー ID のある見出し
  ↓
<h2 id="-アンカー-id-のある見出し">■ アンカー ID のある見出し </h2>
```

一方で、アンカーリンク内では見出し項目が日本語であっても、そのままアンカー ID として利用することが出来る。これはアンカー ID が自動的にエンコードされるためである。

```markdown
[■ アンカー ID のある見出し](#-アンカー-id-のある見出し)
  ↓
<a href="#-%E3%82%A2%E3%83%B3%E3%82%AB%E3%83%BC-id-%E3%81%AE%E3%81%82%E3%82%8B%E8%A6%8B%E5%87%BA%E3%81%97">
  ■ アンカー ID のある見出し
</a>
```

## :exclamation: アンカーリンクの注意点

アンカーリンクには、以下のいくつかの注意点がある。

- ATX スタイルと Setext スタイルのスタイルを問わず同じ記法を用いる
    - 複数行に渡る Setext スタイル見出しではリンクが機能しない場合がある
- 次の半角スペースはアンカー ID に影響しない
    - ATX スタイル見出しにおける、連続する `#` と見出し項目との間
    - 複数行に渡る Setext スタイル見出しにおけるインデント
- ATX スタイル見出しにおいて、末尾に付けられる装飾用の `#` はアンカー ID に影響しない
- 見出しレベルを問わず、`#` は 1 つ（この `#` は URL フラグメント識別子に由来 [[↓]][url-fragment]）
- “見出し項目” → “アンカー ID” への変換（*Slugify*：本記事では **Slug 化** と呼称）
    - 一部の文字に対して、無視や別の文字への置き替え
    - 同じアンカー ID の場合、無印に続いて `-1`、`-2` … が付随する

[url-fragment]: #-url-フラグメント識別子 "“URL フラグメント識別子” の節に移動"

<!-- - 見出し項目がない場合、アンカー ID が作成される場合と作成されない場合がある -->

Slug 化はアンカーリンクを理解する上でかなりの肝となっている。次節で詳しく示す。

<!-- 本記事では、“見出し項目” → “アンカー ID” への変換を **Slug 化** と呼ぶこととする。 -->

### ▽ Slug 化による文字の置き替え ##

アンカーリンクの注意点において「Slug 化による見出し項目からアンカー ID への変換」は書き手にとってかなり厄介なものになっている。これは、パーサによって結果が異なるためである。

:::note info
*Slug 化* の目的は、SEO（検索エンジン最適化）戦略の 1 つで、より検索に引っ掛かりやすくすることにある。Slug 化はこれを自動的に行うことが出来るようにしている。これによって、SEO フレンドリーでユーザーフレンドリーな URL を実現している。

このような Slug 化を行うパッケージなどには [Slugify][sindresorhus] や [github-slugger][flet] などといった名称が付けられている。あるいは、[markdown-it-named-headings][mdit-named-headings] などがある。
:::

[flet]: https://github.com/Flet/github-slugger "Generate a slug just like GitHub does for markdown headings."
[mdit-named-headings]: https://github.com/rstacruz/markdown-it-named-headings "Add IDs to heading elements in markdown-it"
[sindresorhus]: https://github.com/sindresorhus/slugify "Slugify a string"

大抵の Markdown パーサでは、Slug 化によってアンカー ID が SEO にとってイイカンジ <sup>(?)</sup> に置き替えられている。Qiita も例外なく Slug 化が加わっているため、書き手はこの置き替えを注意する必要がある。

Slug 化による文字の置き替えについて、いくつか代表的なものを確認しておきたい。（※ これがすべてではない）

- 半角スペースを `-` に置き替える
    - アンカー ID 先頭の `-` を削除
    - アンカー ID 末尾の `-` を削除
    - 連続する 2 つ以上の `-` を 1 つにする
- いくつかの文字を無視する（規則性がなく、パーサによって異なる。）
- アルファベットを大文字から小文字に置き替え (case mapping)
- Unicode 文字を ASCII 文字のみで表現 (ASCII transliterations of Unicode text)
    - 音訳 (transliterate)（例：`你好` → `ni-hao`）
    - ダイアクリティカルマークを取り除く
    - 絵文字や記号から ASCII 文字へ置き替え
- キャメルケースをケバブケースに置き替え

Unicode 正規化を行うような Slug 化は無いようだ。一方で、Slug 化がまったく加わえていない場合もある。

この文字の置き替えに関して、Qiita Markdown や GitHub Markdown における変換例を以下の記事に移設した。

[Slug 化による見出し項目からアンカー ID への変換 - Qiita](https://qiita.com/Yarakashi_Kikohshi/items/acf1d48b77575978612d)

## ■ 重複するアンカー ID

同じ見出し項目が複数ある場合や、Slug 化によって文字が無視された結果としてアンカー ID が重複する場合、アンカー ID に番号付けが行われる。

例えば、Qiita Markdown では、丸数字は Other Number に含まれるため無視される。これを利用して、次のような見出しを考える。

```markdown
## 見出し ①

## 見出し ②

## 見出し ③
```

この場合、丸数字が無視されるため、すべて同じアンカー ID (`#見出し-`) となる。同じアンカー ID にならないように自動的に無印に続いて `-1`、`-2` … が付与される。

```markdown
[見出し ①](#見出し-)
[見出し ②](#見出し--1)
[見出し ③](#見出し--2)
```

これは、見出しが現れる順に番号が付与される。1 つ目のアンカー ID には番号が付与されないことに注意したい。

重複するアンカー ID を持つ見出しを作成すると、アンカーリンクを作成する際にナンバリングが必要になるため、少し面倒なこととなる。これを踏まえると、アンカー ID が重複するような見出しを作成しないことを心がけたい。

## ■ インライン要素と Slug 化

見出しにインライン要素のマークアップを施していた場合、これらはマークアップに解釈されたのちに Slug 化される。

そのため、これらのアンカー ID を考える際には、実際に表示される文字がアンカー ID になると考えると良い。

```markdown
## *強調*
  ↓
[強調を含む見出しのアンカーリンク](#強調)

## `Code`
  ↓
[コードを含む見出しのアンカーリンク](#code)

## [リンク](https://qiita.com)
  ↓
[リンクを含む見出しのアンカーリンク](#リンク)

## `[コード化されたリンク](#強調)`
  ↓
[コード化されたリンクを含む見出しのアンカーリンク](#コード化されたリンク強調)

## <em>HTML</em>
  ↓
[HTML タグを利用した見出しのアンカーリンク](#html)
```

コード化されたリンクを見出しにした場合、リンク全体はリテラルな文字に解釈されるため、アンカーリンクにはリンクのアドレスも含まれる。

`_` による強調には少し配慮が必要になる。Slug 化では強調のための `_` は無視されるものの、強調以外の `_` は無視されない。そのため、以下のようになる。（`_` は Connector Punctuation に含まれている）

```markdown
## _Low_Line_
  ↓
[`_` による強調と `_` を含むアンカーリンク](#low_line)
```

`*` による強調の場合、そもそも `*` が無視される対象のカテゴリ (Other Punctuation) に含まれているため、このようなことを考える必要がない。

CommonMark にはないが、`:emoji:` や MathJax などの数式が見出し項目に含まれる場合のアンカー ID は次のようになる。

```markdown
## :smile:
  ↓
[emoji を含む見出しのアンカーリンク](#smile)

## $e^{i\pi}+1 = 0$
  ↓
[数式を含む見出しのアンカーリンク](#-eipi1--0)
```

Unicode 絵文字が無視されるのに対して、`:emoji:` は無視されない。

数式内では半角スペースは無視するようにレンダリングされるが、アンカー ID では `-` に変換されるため注意が必要になる。（なるべく不要な半角スペースは削除しておくと良いだろう）

## ■ 文字列のないアンカー ID

以下のような `#` のみのアンカー ID はページの上部に移動するアンカーリンクとなる。

```markdown
[ページの上部に移動](#)
```

[ページの上部に移動](#)

この挙動は HTML Living Standard [^movePageTop] で規定されている標準的な動作となっている。ページ上部に移動するアンカーリンクは、[`#top`](#top) とすることで移動先を明示的にすることも出来る。

[^movePageTop]:
    [The indicated part of the document \- § 7.4.6.3 Scrolling to a fragment \- HTML Living Standard](https://html.spec.whatwg.org/multipage/browsing-the-web.html#the-indicated-part-of-the-document)
    > 2\. If fragment is the empty string, then return the special value **top of the document**.

一方で、Slug 化で無視される文字のみの見出しの場合、アンカー ID のアドレスがない。これはアンカー ID として上手く機能せず、ページの上部に移動するアンカーリンクと同一視される。

```markdown
## 👻
  ↓
<h2 id="">👻</h2>

[👻へのアンカーリンク](#)
  ↑
これはページの上部に移動するアンカーリンクとして解釈される。
```

そのため、Slug 化で無視される文字のみの見出しは避けた方が良い。

ちなみに、見出しの 1 つに `# Top` がある場合、アンカー ID `#top` はページの上部に移動せず、見出し `# Top` への移動を優先するようだ。

## ■ 存在しないアンカー ID

存在しないアンカー ID を指定した場合、クリックしてもどこにも移動しない。

```markdown
[このアンカー ID は存在しない](#-このアンカー-id-は存在しない)
```

[このアンカー ID は存在しない](#-このアンカー-id-は存在しない)

アンカー ID を 1 文字でも間違えている場合、アンカーリンクは機能しないため注意が必要になる。

すでに示しているが、アンカー ID `#top` はページの上部がデフォルトで指定されているため、存在しないアンカー ID には該当しない。

## ■ アンカーリンクのリンクタイトル

リンクにはリンクタイトルを付けることが出来る。（`"` で囲まれた部分）これは、リンクにマウスポインタをホバーさせた際に表示されるツールチップに表示される。

```markdown
[Example Domain](https://www.example.com "This domain is for use in illustrative examples in documents. You may use this domain in literature without prior coordination or asking for permission.")
```

[Example Domain](https://www.example.com "This domain is for use in illustrative examples in documents. You may use this domain in literature without prior coordination or asking for permission.")

より詳細には、以下の記事を参照してほしい。

> [\# リンクタイトル \- Markdown リンクを理解したい - Qiita](https://qiita.com/Yarakashi_Kikohshi/items/47135dded31879dfaf0e#-%E3%83%AA%E3%83%B3%E3%82%AF%E3%82%BF%E3%82%A4%E3%83%88%E3%83%AB)

これと同じことをアンカーリンクで行うことも出来る。「この節」などと書いた場合、どこの節に移動するか分からないため、リンクタイトルを含めることで少しだけ読み手フレンドリーな文章になるだろう。

```markdown
[この節に移動](#参考 "参考の節に移動します")。
[この節に移動][digression]。

[digression]: #余談-𒐈 "余談の節に移動します"
```

[この節に移動](#参考 "参考の節に移動します")。
[この節に移動][digression]。

[digression]: #余談-𒐈 "余談の節に移動します"

## ■ URL フラグメント識別子

アンカー ID は URL の末尾に `#` に続く形で表記され、この部分を **URL フラグメント識別子 (URL fragment identifier)** と呼ぶ。

```
https://qiita.com/Qiita/items/c686397e4a0f4f11683d#headings---%E8%A6%8B%E5%87%BA%E3%81%97
                                                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
                                                    ↑
                                                    URL フラグメント識別子
```

見出しごとのアンカー ID は、フラグメント識別子として URL に記載されることとなる。上の例では、末尾の方がエンコードされ読めないものとなっているが、ブラウザのアドレスバーに入力すると読める状態で確認することが出来る。

このフラグメント識別子のみのリンクは、ページ内の特定の位置に移動することを意味している。（今回の場合、見出しに付随する id 属性）

### ▽ テキストフラグメント

:::note warn
テキストフラグメント (text fragments) はバージョン 80 以降の Chromium ベースのブラウザに限定されています。
:::

URL の末尾に `#:~:text=` と続けてページ内のテキストを書いた URL にアクセスすると、そのテキストを強調することが出来る。

テキストフラグメントの完全な構文は以下のようになっている。

```
#:~:text=[prefix-,]textStart[,textEnd][,-suffix]
```

|    構成     |      | 説明                                         |
| :---------: | ---- | :------------------------------------------- |
| `prefix-,`  | 任意 | `textStart` の前の文章                       |
| `textStart` | 必須 | ハイライトされる文章（あるいは、その始まり） |
| `,textEnd`  | 任意 | ハイライトされる文章の終わり                 |
| `,-suffix`  | 任意 | `textStart`（または `textEnd`）の後ろの文章  |

ハイライトしたい文章がページ内で複数回登場する場合、`prefix-,` や `,-suffix` を用いることで特定することが出来るようになる。また、`textEnd` はハイライトされる文章が長い場合、`textStart` \~ `textEnd` までハイライトするように書くことも出来る。テキストフラグメントには Slug 化の必要はない。これは、リンク先の強調を目的としているからだろう。

ただし、半角スペース (`%20`)、`,` (`%2C`)、`-` (`%2D`) は事前にエンコードしておく必要がある。これらがエンコードされていないと期待したリンクを上手く得られない。

Qiita Markdown でもテキストフラグメントをページ内で利用することが出来る。

```markdown
[ページ内のテキストフラグメント](#:~:text=Qiita%20Markdown%20でも,利用することが出来る。)
```

[ページ内のテキストフラグメント](#:~:text=Qiita%20Markdown%20でも,利用することが出来る。)

また、複数の文字列を指定する場合は、`&text=` によって続ける。

ちなみに、簡単にテキストフラグメント付きの URL を取得するには、対応するブラウザで以下を行う。

1. テキストフラグメントにしたいテキストを選択
1. 右クリックで表示されるコンテキストメニュー内の ` 選択箇所へのリンクをコピー ` を選択（ブラウザによっては ` 強調表示するリンクのコピー ` など）
1. クリップボードにテキストフラグメント付きの URL を取得できる

以前は拡張機能を導入する必要があったが、ブラウザ標準で実装されている。

## ■ アンカー ID に関するその他

### ▽ Custom IDs

アンカー ID を見出し項目と異なる ID に変更できる場合もある。これは PHP Markdown Extra [<sup>⧉</sup>][phpMdEx-speattr] に由来している。

[phpMdEx-speattr]: https://michelf.ca/projects/php-markdown/extra/#spe-attr
    "Special Attributes With Markdown Extra, you can set the id and class attribute on certain elements using an attribute block."

```markdown
## 表示される見出し項目 {#custom-id}

[カスタム ID がアンカーリンクに利用される](#custom-id)
```

これは ATX スタイル見出しのみで利用でき、Setext スタイル見出しでは利用できないようだ。

ただし、すべてのパーサでこの機能があるわけではないため、互換性を考慮して利用することは避けた方が良いだろう。

### ▽ footnote のアンカー ID

footnote が利用できる場合、footnote にもアンカー ID が付与されている。footnote のラベル名が `[^Footnote]` の場合における本文側のアンカー ID と脚注側のアンカー ID を、いくつかの場合について示しておきたい。

```markdown
  "fnref"
  ↓
[^Footnote]

[^Footnote]: Here is footnote.
  ↑
  "fn"
```

- markdown-it [<sup>⧉</sup>][markdown-it-footnote] の場合

    `#fnref`/`#fn` に続いて、通し番号になっている。

    - 本文中：`#fnref1`
    - 脚注側：`#fn1`

- Qiita の場合

    `#fnref-`/`#fn-` に続いて、ラベル名となっている。

    - 本文中：`#fnref-Footnote`
    - 脚注側：`#fn-Footnote`

- GitHub の場合

    `#user-content-fnref-`/`#user-content-fn-` に続いて、ラベル名とハッシュとなっている。ハッシュは編集中に知ることはできないようだ。また、ラベル名は大文字や日本語を含むと上手く機能しないようだ。

    - 本文中：`#user-content-fnref-Footnote-<hash>`
    - 脚注側：`#user-content-fn-Footnote-<hash>`

    実際、どの文字がアンカー ID として有効に機能するかは不明。

[markdown-it-footnote]: https://github.com/markdown-it/markdown-it-footnote

どうやら、どの footnote のアンカー ID にも Slug 化が効いていないようだ。これはおそらく、脚注に対して SEO 対策してもしょうがないからだろう。

ちなみに、footnote アンカー ID に `fnref`/`fn` のような接頭辞を付けることで、見出しアンカー ID との差別化を行っている。しかし、意図的に footnote のアンカー ID と同じアンカー ID となるように構成すると、2 つのアンカー ID が被ってしまう。そのため、一方にジャンプすることが出来ない。

```markdown
## fnref-Footnote

本文中の footnote [^Footnote]

[^Footnote]: ここは footnote です。
```

しかしながら、`fnref`/`fn` のような接頭辞を見出し内で付けることはまずないため、あまり気にする必要はないだろう。（Markdown パーサを設計する人は気にするべきかもしれない）

# 参考

- [\# Slug \- Writing style guide \- The MDN Web Docs project \| MDN](https://developer.mozilla.org/en-US/docs/MDN/Writing_guidelines/Writing_style_guide#slugs)
- [What’s a slug. and why would I use one? \| by Dave Sag \| ITNEXT](https://itnext.io/whats-a-slug-f7e74b6c23e0)
- [What Is a URL Slug? Why It Matters & How to Optimize \- Viral Solutions](https://viralsolutions.net/what-is-a-url-slug/)
- [Slug \- Clean URL \- Wikipedia](https://en.wikipedia.org/wiki/Clean_URL#Slug)
- [これは人類最初の試みとして、驚異に満ちたリンクの物語である: テキストフラグメント](https://web.dev/i18n/ja/text-fragments/)
- [ページ内アンカーへのキーボード操作でのアクセシビリティ (ブラウザ側の機能改善) \| Accessible & Usable](https://accessible-usable.net/2020/12/entry_201207.html)
- [\# Linking to an element on the same page \- `<a>`: The Anchor element \- HTML: HyperText Markup Language \| MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a#linking_to_an_element_on_the_same_page)

# 余談 𒐈

アンカーリンクに関する見出し項目の Slug 化は書き手にとっては重要であるが、その仕様については明文化されているパーサはないように思われる。

Markdown のアンカーリンクについて知りたいだけなのに、Slug 化とか言う SEO 戦略を知る必要になる罠にかかってしまい、かなり沼な記事になってしまった。

本記事の見出しには、Slug 化で無視されるような絵文字や記号を含んでいるため、ぜひアドレスバーでどのようになっているのか確認してみてほしい。

<!-- 本記事では ATX スタイル見出しのみを考えているものの、複数行に渡る Setext スタイル見出しのアンカーリンクを作成する場合、改行を `-` と見なすか無視するかはパーサによって解釈が異なるようだ。Qiita では無視される。 -->

テキストフラグメントでは、半角スペースを `%20` にエンコードする必要があるが、`<` \~ `>` で囲うことでエンコードを省略することも出来る。

```markdown
[A text fragment enclosed in angle brackets.](<#:~:text=A text fragment enclosed in angle brackets.>)
```

[A text fragment enclosed in angle brackets.](<#:~:text=A text fragment enclosed in angle brackets.>)

ちなみに、“fragment” は、断片や破片を意味しており、fragile と似た語源を持つ [<sup>⧉</sup>][fragment-gogen]。flag に関することばではない。

[fragment-gogen]: https://gogen-ejd.info/fragment/#:~:text=%E3%83%A9%E3%83%86%E3%83%B3%E8%AA%9E%20fragmentum%EF%BC%88%E7%A0%B4%E7%89%87%EF%BC%89%E2%87%92%20%E3%83%A9%E3%83%86%E3%83%B3%E8%AA%9E,%E3%81%A8%E5%90%8C%E3%81%98%E8%AA%9E%E6%BA%90%E3%82%92%E3%82%82%E3%81%A4%E3%80%82

# 追記

- 2022/11/19: footnote におけるアンカーリンクについて追記。
- 2022/11/20: アンカーリンクとアンカー ID に関して、記事内で区別していなかったため、この 2 つを区別修正しました。
- 2022/11/21: Slug 化に伴って注意の必要な文字、文字列のないアンカー ID、重複するアンカー ID について追記。
- 2022/11/24: 存在しないアンカー ID について追記。
- 2022/12/25: Slug 化による文字の置き替えの項目を他記事に移設。