Markdown のアンカーリンクを理解したい
Markdown では、ページ内の見出しに移動するアンカーリンク(ページ内リンク)を作成することが出来る。
このアンカーリンクは、基本的に見出し項目をそのまま書き下すことで作成できるが、そのまま書き下すだけでは利用できない場合が数多くある。これがどうして発生するのか、どのように解決すれば良いかを理解したい。
🔗 アンカーリンクの作成
Markdown リンクを利用することで、特定の見出しへのリンクを作成することが出来る。これをクリックすることで、ページ内の見出しに移動することが出来る。
[このリンクは “参考” 見出しにリンクされる](#参考)。
[このリンクは “余談 𒐈” 見出しにリンクされる][label]。
[label]: #余談-𒐈
このリンクは “参考” 見出しにリンクされる。
このリンクは “余談 𒐈” 見出しにリンクされる。
このリンクは 1 つの #
と見出し項目を利用することで作成できる。このリンクは アンカーリンク (anchor link) と呼ばれる。また、この #
から続くアドレスを アンカー ID と呼ぶこととする。
実は、この見出しにアンカー ID が与えられる仕様は CommonMark になく、独自拡張となっている。実際、Markdown 見出しは HTML に変換される際、以下のようになっている。
## ■ CommonMark 仕様の見出し
↓
<h2>■ CommonMark 仕様の見出し </h2>
## ■ アンカー ID のある見出し
↓
<h2 id="-アンカー-id-のある見出し">■ アンカー ID のある見出し </h2>
一方で、アンカーリンク内では見出し項目が日本語であっても、そのままアンカー ID として利用することが出来る。これはアンカー ID が自動的にエンコードされるためである。
[■ アンカー 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>
アンカーリンクの注意点
アンカーリンクには、以下のいくつかの注意点がある。
- ATX スタイルと Setext スタイルのスタイルを問わず同じ記法を用いる
- 複数行に渡る Setext スタイル見出しではリンクが機能しない場合がある
- 次の半角スペースはアンカー ID に影響しない
- ATX スタイル見出しにおける、連続する
#
と見出し項目との間 - 複数行に渡る Setext スタイル見出しにおけるインデント
- ATX スタイル見出しにおける、連続する
- ATX スタイル見出しにおいて、末尾に付けられる装飾用の
#
はアンカー ID に影響しない - 見出しレベルを問わず、
#
は 1 つ(この#
は URL フラグメント識別子に由来 [↓]) - “見出し項目” → “アンカー ID” への変換(Slugify:本記事では Slug 化 と呼称)
- 一部の文字に対して、無視や別の文字への置き替え
- 同じアンカー ID の場合、無印に続いて
-1
、-2
… が付随する
Slug 化はアンカーリンクを理解する上でかなりの肝となっている。次節で詳しく示す。
▽ Slug 化による文字の置き替え
アンカーリンクの注意点において「Slug 化による見出し項目からアンカー ID への変換」は書き手にとってかなり厄介なものになっている。これは、パーサによって結果が異なるためである。
Slug 化 の目的は、SEO(検索エンジン最適化)戦略の 1 つで、より検索に引っ掛かりやすくすることにある。Slug 化はこれを自動的に行うことが出来るようにしている。これによって、SEO フレンドリーでユーザーフレンドリーな URL を実現している。
このような Slug 化を行うパッケージなどには Slugify や github-slugger などといった名称が付けられている。あるいは、markdown-it-named-headings などがある。
大抵の Markdown パーサでは、Slug 化によってアンカー ID が SEO にとってイイカンジ (?) に置き替えられている。Qiita も例外なく Slug 化が加わっているため、書き手はこの置き替えを注意する必要がある。
Slug 化による文字の置き替えについて、いくつか代表的なものを確認しておきたい。(※ これがすべてではない)
- 半角スペースを
-
に置き替える- アンカー ID 先頭の
-
を削除 - アンカー ID 末尾の
-
を削除 - 連続する 2 つ以上の
-
を 1 つにする
- アンカー ID 先頭の
- いくつかの文字を無視する(規則性がなく、パーサによって異なる。)
- アルファベットを大文字から小文字に置き替え (case mapping)
- Unicode 文字を ASCII 文字のみで表現 (ASCII transliterations of Unicode text)
- 音訳 (transliterate)(例:
你好
→ni-hao
) - ダイアクリティカルマークを取り除く
- 絵文字や記号から ASCII 文字へ置き替え
- 音訳 (transliterate)(例:
- キャメルケースをケバブケースに置き替え
Unicode 正規化を行うような Slug 化は無いようだ。一方で、Slug 化がまったく加わえていない場合もある。
この文字の置き替えに関して、Qiita Markdown や GitHub Markdown における変換例を以下の記事に移設した。
Slug 化による見出し項目からアンカー ID への変換 - Qiita
■ 重複するアンカー ID
同じ見出し項目が複数ある場合や、Slug 化によって文字が無視された結果としてアンカー ID が重複する場合、アンカー ID に番号付けが行われる。
例えば、Qiita Markdown では、丸数字は Other Number に含まれるため無視される。これを利用して、次のような見出しを考える。
## 見出し ①
## 見出し ②
## 見出し ③
この場合、丸数字が無視されるため、すべて同じアンカー ID (#見出し-
) となる。同じアンカー ID にならないように自動的に無印に続いて -1
、-2
… が付与される。
[見出し ①](#見出し-)
[見出し ②](#見出し--1)
[見出し ③](#見出し--2)
これは、見出しが現れる順に番号が付与される。1 つ目のアンカー ID には番号が付与されないことに注意したい。
重複するアンカー ID を持つ見出しを作成すると、アンカーリンクを作成する際にナンバリングが必要になるため、少し面倒なこととなる。これを踏まえると、アンカー ID が重複するような見出しを作成しないことを心がけたい。
■ インライン要素と Slug 化
見出しにインライン要素のマークアップを施していた場合、これらはマークアップに解釈されたのちに Slug 化される。
そのため、これらのアンカー ID を考える際には、実際に表示される文字がアンカー ID になると考えると良い。
## *強調*
↓
[強調を含む見出しのアンカーリンク](#強調)
## `Code`
↓
[コードを含む見出しのアンカーリンク](#code)
## [リンク](https://qiita.com)
↓
[リンクを含む見出しのアンカーリンク](#リンク)
## `[コード化されたリンク](#強調)`
↓
[コード化されたリンクを含む見出しのアンカーリンク](#コード化されたリンク強調)
## <em>HTML</em>
↓
[HTML タグを利用した見出しのアンカーリンク](#html)
コード化されたリンクを見出しにした場合、リンク全体はリテラルな文字に解釈されるため、アンカーリンクにはリンクのアドレスも含まれる。
_
による強調には少し配慮が必要になる。Slug 化では強調のための _
は無視されるものの、強調以外の _
は無視されない。そのため、以下のようになる。(_
は Connector Punctuation に含まれている)
## _Low_Line_
↓
[`_` による強調と `_` を含むアンカーリンク](#low_line)
*
による強調の場合、そもそも *
が無視される対象のカテゴリ (Other Punctuation) に含まれているため、このようなことを考える必要がない。
CommonMark にはないが、:emoji:
や MathJax などの数式が見出し項目に含まれる場合のアンカー ID は次のようになる。
## :smile:
↓
[emoji を含む見出しのアンカーリンク](#smile)
## $e^{i\pi}+1 = 0$
↓
[数式を含む見出しのアンカーリンク](#-eipi1--0)
Unicode 絵文字が無視されるのに対して、:emoji:
は無視されない。
数式内では半角スペースは無視するようにレンダリングされるが、アンカー ID では -
に変換されるため注意が必要になる。(なるべく不要な半角スペースは削除しておくと良いだろう)
■ 文字列のないアンカー ID
以下のような #
のみのアンカー ID はページの上部に移動するアンカーリンクとなる。
[ページの上部に移動](#)
この挙動は HTML Living Standard 1 で規定されている標準的な動作となっている。ページ上部に移動するアンカーリンクは、#top
とすることで移動先を明示的にすることも出来る。
一方で、Slug 化で無視される文字のみの見出しの場合、アンカー ID のアドレスがない。これはアンカー ID として上手く機能せず、ページの上部に移動するアンカーリンクと同一視される。
## 👻
↓
<h2 id="">👻</h2>
[👻へのアンカーリンク](#)
↑
これはページの上部に移動するアンカーリンクとして解釈される。
そのため、Slug 化で無視される文字のみの見出しは避けた方が良い。
ちなみに、見出しの 1 つに # Top
がある場合、アンカー ID #top
はページの上部に移動せず、見出し # Top
への移動を優先するようだ。
■ 存在しないアンカー ID
存在しないアンカー ID を指定した場合、クリックしてもどこにも移動しない。
[このアンカー ID は存在しない](#-このアンカー-id-は存在しない)
アンカー ID を 1 文字でも間違えている場合、アンカーリンクは機能しないため注意が必要になる。
すでに示しているが、アンカー ID #top
はページの上部がデフォルトで指定されているため、存在しないアンカー ID には該当しない。
■ アンカーリンクのリンクタイトル
リンクにはリンクタイトルを付けることが出来る。("
で囲まれた部分)これは、リンクにマウスポインタをホバーさせた際に表示されるツールチップに表示される。
[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.")
より詳細には、以下の記事を参照してほしい。
これと同じことをアンカーリンクで行うことも出来る。「この節」などと書いた場合、どこの節に移動するか分からないため、リンクタイトルを含めることで少しだけ読み手フレンドリーな文章になるだろう。
[この節に移動](#参考 "参考の節に移動します")。
[この節に移動][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 属性)
▽ テキストフラグメント
テキストフラグメント (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 でもテキストフラグメントをページ内で利用することが出来る。
[ページ内のテキストフラグメント](#:~:text=Qiita%20Markdown%20でも,利用することが出来る。)
また、複数の文字列を指定する場合は、&text=
によって続ける。
ちなみに、簡単にテキストフラグメント付きの URL を取得するには、対応するブラウザで以下を行う。
- テキストフラグメントにしたいテキストを選択
- 右クリックで表示されるコンテキストメニュー内の
選択箇所へのリンクをコピー
を選択(ブラウザによっては強調表示するリンクのコピー
など) - クリップボードにテキストフラグメント付きの URL を取得できる
以前は拡張機能を導入する必要があったが、ブラウザ標準で実装されている。
■ アンカー ID に関するその他
▽ Custom IDs
アンカー ID を見出し項目と異なる ID に変更できる場合もある。これは PHP Markdown Extra ⧉ に由来している。
## 表示される見出し項目 {#custom-id}
[カスタム ID がアンカーリンクに利用される](#custom-id)
これは ATX スタイル見出しのみで利用でき、Setext スタイル見出しでは利用できないようだ。
ただし、すべてのパーサでこの機能があるわけではないため、互換性を考慮して利用することは避けた方が良いだろう。
▽ footnote のアンカー ID
footnote が利用できる場合、footnote にもアンカー ID が付与されている。footnote のラベル名が [^Footnote]
の場合における本文側のアンカー ID と脚注側のアンカー ID を、いくつかの場合について示しておきたい。
"fnref"
↓
[^Footnote]
[^Footnote]: Here is footnote.
↑
"fn"
-
markdown-it ⧉ の場合
#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 として有効に機能するかは不明。
- 本文中:
どうやら、どの footnote のアンカー ID にも Slug 化が効いていないようだ。これはおそらく、脚注に対して SEO 対策してもしょうがないからだろう。
ちなみに、footnote アンカー ID に fnref
/fn
のような接頭辞を付けることで、見出しアンカー ID との差別化を行っている。しかし、意図的に footnote のアンカー ID と同じアンカー ID となるように構成すると、2 つのアンカー ID が被ってしまう。そのため、一方にジャンプすることが出来ない。
## fnref-Footnote
本文中の footnote [^Footnote]
[^Footnote]: ここは footnote です。
しかしながら、fnref
/fn
のような接頭辞を見出し内で付けることはまずないため、あまり気にする必要はないだろう。(Markdown パーサを設計する人は気にするべきかもしれない)
参考
- # Slug - Writing style guide - The MDN Web Docs project | MDN
- What’s a slug. and why would I use one? | by Dave Sag | ITNEXT
- What Is a URL Slug? Why It Matters & How to Optimize - Viral Solutions
- Slug - Clean URL - Wikipedia
- これは人類最初の試みとして、驚異に満ちたリンクの物語である: テキストフラグメント
- ページ内アンカーへのキーボード操作でのアクセシビリティ (ブラウザ側の機能改善) | Accessible & Usable
- # Linking to an element on the same page -
<a>
: The Anchor element - HTML: HyperText Markup Language | MDN
余談 𒐈
アンカーリンクに関する見出し項目の Slug 化は書き手にとっては重要であるが、その仕様については明文化されているパーサはないように思われる。
Markdown のアンカーリンクについて知りたいだけなのに、Slug 化とか言う SEO 戦略を知る必要になる罠にかかってしまい、かなり沼な記事になってしまった。
本記事の見出しには、Slug 化で無視されるような絵文字や記号を含んでいるため、ぜひアドレスバーでどのようになっているのか確認してみてほしい。
テキストフラグメントでは、半角スペースを %20
にエンコードする必要があるが、<
~ >
で囲うことでエンコードを省略することも出来る。
[A text fragment enclosed in angle brackets.](<#:~:text=A text fragment enclosed in angle brackets.>)
A text fragment enclosed in angle brackets.
ちなみに、“fragment” は、断片や破片を意味しており、fragile と似た語源を持つ ⧉。flag に関することばではない。
追記
- 2022/11/19: footnote におけるアンカーリンクについて追記。
- 2022/11/20: アンカーリンクとアンカー ID に関して、記事内で区別していなかったため、この 2 つを区別修正しました。
- 2022/11/21: Slug 化に伴って注意の必要な文字、文字列のないアンカー ID、重複するアンカー ID について追記。
- 2022/11/24: 存在しないアンカー ID について追記。
- 2022/12/25: Slug 化による文字の置き替えの項目を他記事に移設。
-
The indicated part of the document - § 7.4.6.3 Scrolling to a fragment - HTML Living Standard
2. If fragment is the empty string, then return the special value top of the document.