相対URIを計算するのが意外に難しい

専用のライブラリーを使いましょう

静的サイトジェネレーターを使うような時にたまに苦労する話。仕様的な厳密さを欠いた用語で話します。

結論としては、「静的HTMLファイルを生成する時に、相対パスでリンクを作るのは結構難しいので、ライブラリーを使いましょう」ということ。

絶対URIと相対URI

例えば https://blogs.kitaitimakoto.net/~/Apehuci/article1 という記事を見ているとしよう。この全体をURIと言い、 /~/Apehuci/article1 という部分をパスと言う。

この記事から、別の https://blogs.kitaitimakoto.net/~/Announcement/article2 という記事へリンクを張る時、

<a href="https://blogs.kitaitimakoto.net/~/Announcement/article2">article2</a>

という風に書くことができる。こういう書き方のURIを「絶対URI」と言う。定義で言うと「スキーム( https の部分)が書かれているURI」が絶対URIだ。

また、こういう書き方もできる。

<a href="/~/Announcement/article2">article2</a>
<a href="../Announcement/article2">article2</a>

こういう、スキームがないURIを「相対URI」と言う。

  • https://blogs.kitaitimakoto.net/~/Announcement/article2 は絶対URI(スキームがある)
  • /~/Announcement/article2相対 URI(スキームがない。スラッシュで始まっていても相対)
  • ../Announcement/article2 は相対URI

絶対パスと相対パス

ところで、コンピューター上のファイルの場所を表す際に「パス」という物を使う。 /var/www/html とか ~/bin とか ../another-dir/file などだ。ここにも「絶対」と「相対」がある。

/」から始まるパスが「絶対パス」で、

  • /var/www/html
  • /etc/password

といった書き方。この時、「/」一字であらわされる場所を「ルート(root)」と言う。

一方で「/」で始まらないのが「相対パス」で、

  • ./cmd
  • ../dir/file
  • ../../dir/subdir

などと書く。

ファイルではなくURIでも「パス」という言い方があり、スキームとホスト(ドメイン)部分の後の部分を指す。絶対URIの場合は必ず「/」から始まる。

/~/Apehuci/article1 みたいなのがURIでのパス。また ../../Announcement/article2 みたいなのもある。ここで、「/」で始まるURIを「ルート相対URI」と呼ぼう。ファイルシステムで「/」をルートと呼ぶのに対比させた言い方だ。

URIとファイルパスの違う所

さてここでお立ち会い。

  • 現在のページ: /~/Apehuci/article1
  • リンクURI: ./article3
  • リンク先ルート相対URI: /~/Apehuci/article3

これはいいね? リンク先URIの「./」を外しても同じだ。

  • 現在のページ: /~/Apehuci/article1
  • リンクURI: article4
  • リンク先ルート相対URI: /~/Apehuci/article4

ではこれはどうだろう。

  • 現在のページ: /~/Apehuci/article1
  • リンクURI: ../Announcement/article2
  • リンク先ルート相対URI: /~/Announcement/article2

たぶん、大丈夫だと思う。

今度は?

  • 現在のページ: /~/Apehuci/article1/
  • リンクURI: ../Announcement/article2
  • リンク先ルート相対URI: /~/Apehuci/Announcement/article2

ApehuciAnnouncement の両方がURIに含まれている。「現在のページ」の最後の「/」の有無で、リンク先に違いが生じている。 /~/Announcement/article2 にリンクさせたい場合は、リンクURIを ../../Announcement/article2 としなくてはいけない。

これがURIでルート相対でないパスを使う際のややこしさだ。

  • ファイルシステムを使う際には、「/」で終わるパスという物は存在しない(少なくとも今みたいなリンクを考える場合は)
  • しかしURIでは「/~/Apehuci/article1/」のように「/」で終わるパスも存在する
  • それは、ルート相対ではない相対パスを扱う際に「/~/Apehuci/article1」とは違う働きをする
  • 世の中には「/~/Apehuci/article1/」と「/~/Apehuci/article1」とで同じ内容を配信するサーバー(ウェブアプリケーション)が多数存在する

静的HTMLファイル内で機械的にリンク先URIを相対パスにしようとした時に、この組み合わせがややこしさを生む。

静的HTMLファイルでのリンクURIの計算

./site というディレクトリーにファイルを入れて、ウェブサイトを作っているとしよう。./site/page1.html というファイルが /page1.html というウェブページとして表示される。で、 ./site/index.html のようにindex.html はそのファイル名を省略できる。この場合は / というURIで index.html の内容が表示されるし、サブディレクトリーも同様で ./site/category1/index.html/category1/ だけでアクセスできる。よくある想定だと思う。

/category1/item1/というページから/category1/item2/というページにリンクを張りたい時は、./site/category1/item1/index.htmlというファイルに../item2/と書く。

/category1/item1/ + ../item2/ -> /category1/item2/

さて、世の中のサーバーには/category1/item1//category1/item1(最後の「/」が無い)とで同じ内容を表示する物も多い。あなたが/category1/item1を見ている時に../item2/というリンクを踏んだらどうなるだろう?

/category1/item1 + ../item2/ -> /item2/

これは意図しないページへのリンクとなってしまっている。

プログラムでファイルを一括処理して相対パスのリンクURIを作ろうとしている時に、ファイルのパス用のライブラリーを使うとこういうことが起こりやすい。ファイル(ディレクトリー)としては ./site/category1/item1./site/category1/item1/も同じだからだ。

Pathname("./site/category1/item2").relative_path_from("./site/category1/item1/")
# => #<Pathname:../item2>
Pathname("./site/category1/item2").relative_path_from("./site/category1/item1")
# => #<Pathname:../item2>

このことを気にせず何となくファイルパス処理用のライブラリー(ここではRubyのPathname)を使っていると、意図しないリンクが出来上がることになる。

  • 必ず item1方式かitem1/方式かを統一した上で、
  • URIとしての相対パス計算を行う

という必要がある。

item1= Addressable::URI.parse("http://example.net/category1/item1")
item1.path = item1.path + "/" unless item1.path.end_with? "/"
item2 = Addressable::URI.parse("http://example.net/category1/item2/")
item2.route_from(item1)
# => #<Addressable::URI:0x280 URI:../item2/>

これは最後に「/」を使うことにした場合だけど、使わないことにした場合もAddressableは計算できる。

item1= Addressable::URI.parse("http://example.net/category1/item1/")
item1.path = item1.path[0..-2] if item1.path.end_with? "/"
item2 = Addressable::URI.parse("http://example.net/category1/item2")
item2.route_from(item1)
# => #<Addressable::URI:0x2e4 URI:item2>

このややこしい計算を自分で実装するのはよした方がいいやね……。