LoginSignup
18
10

More than 1 year has passed since last update.

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

Last updated at Posted at 2022-11-17

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>

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

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

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

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

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

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

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

このような Slug 化を行うパッケージなどには Slugifygithub-slugger などといった名称が付けられている。あるいは、markdown-it-named-headings などがある。

大抵の Markdown パーサでは、Slug 化によってアンカー ID が SEO にとってイイカンジ (?) に置き替えられている。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

■ 重複するアンカー 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 は存在しない

アンカー 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.")

Example Domain

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

# リンクタイトル - Markdown リンクを理解したい - Qiita

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

[この節に移動](#参考 "参考の節に移動します")[この節に移動][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 を取得するには、対応するブラウザで以下を行う。

  1. テキストフラグメントにしたいテキストを選択
  2. 右クリックで表示されるコンテキストメニュー内の 選択箇所へのリンクをコピー を選択(ブラウザによっては 強調表示するリンクのコピー など)
  3. クリップボードにテキストフラグメント付きの 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 化は書き手にとっては重要であるが、その仕様については明文化されているパーサはないように思われる。

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 化による文字の置き替えの項目を他記事に移設。
  1. 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.

18
10
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
18
10