Corredor

ウェブ、プログラミングの勉強メモ。

リポジトリごとの GitHub Pages でルート相対パスを使うには

GitHub Pages でルート相対パスを使う時の荒業

ルート相対パスとは?

HTML から画像やスタイルシートなどの外部ファイルを指定する時のパスの書き方は、以下の3つがある。

  1. 絶対パス : https://USER-NAME.github.io/SUB-REPO/style.csshttps://USER-NAME.github.io/scripts.js
    • http:// などのプロトコルから表記する。呼び出し元の HTML がどこに配置されようと、目的の外部ファイルを取得できる
  2. 相対パス : style.css (= ./style.css)・../scripts.js
    • 呼び出し元の HTML のパスから見て相対的な位置を ./ (同階層) ないしは ../ (一階層上) で指定する
  3. ルート相対パス (Root Relative Path) : /SUB-REPO/style.css/scripts.js
    • 呼び出し元の HTML から見たルート / を起点としてパスを指定する (ドメイン部分を省略した絶対パスといえる)
    • 呼び方は「ルートパス」「サイトルートパス (Site-Root Relative Path)」などとも

今回対象にするのは、3つ目のルート相対パス。

ローカルで直接 HTML ファイルを開いてしまうと、ルート相対パスの「ルート」が特定できないため上手く開けないが、何らかのサーバ上で動作させれば、http://localhost:8080/ などをルートとみなして解釈できるようになる、というモノだ。

GitHub Pages におけるルート相対パス

GitHub Pages にデプロイした HTML ファイルに関しても、ルート相対パスが使える。

User Site の場合

USER-NAME.github.io の名前で作ったリポジトリで GitHub Pages を公開する場合、ルートは https://USER-NAME.github.io/ とみなされる。

よって、/index.html から、同階層の styles.css を参照したい場合は、以下のように link 要素が書ける。

<link rel="stylesheet" href="/styles.css">

コレは違和感ないだろう。

Project Site の場合

任意のリポジトリで、よく docs/ ディレクトリを作るか gh-pages ブランチを作るかして公開する GitHub Pages。

URL としては https://USER-NAME.github.io/SUB-REPO/ 配下が当該リポジトリの GitHub Pages となるが、この時のルートは https://USER-NAME.github.io/ とみなされてしまう。

コレはどういうことかというと、任意のリポジトリ SUB-REPO の直下に index.htmlscripts.js を配置し、index.html から以下のようにルート相対パスで参照させようとした場合、

<script src="/scripts.js"></script>

当該リポジトリ直下から探して、

  • https://USER-NAME.github.io/SUB-REPO/scripts.js

を参照して欲しいところだが、実際は一階層上の

  • https://USER-NAME.github.io/scripts.js

を探しに行ってしまうのだ。

プロジェクト・サイトでルート相対パスを使いたい場合は、

<script src="/SUB-REPO/scripts.js"></script>

このようにルート相対パスに当該リポジトリ名を含めてやらないといけない、というワケだ。

…お察しかと思うが、この挙動は大変不便だ。

base 要素でも直せない

「ルート」とみなされるパスを変更する方法はないかと思い、base 要素を用意してみた。

SUB-REPO リポジトリ内で、以下のようなバリエーションで base 要素を記述して検証してみた。

<!-- 絶対パスで SUB-REPO リポジトリまで指定・末尾スラッシュなし -->
<base href="https://USER-NAME.github.io/SUB-REPO">
<!-- 絶対パスで SUB-REPO リポジトリまで指定・末尾スラッシュあり -->
<base href="https://USER-NAME.github.io/SUB-REPO/">

<!-- 相対パスで何かを指定してみる・リポジトリ名を含めるのは嫌だけど… -->
<base href=".">
<base href="./">
<base href="SUB-REPO">
<base href="SUB-REPO/">
<base href="./SUB-REPO">
<base href="./SUB-REPO/">

<!-- ルート相対パスで指定してみる・リポジトリ名を含めるのは嫌だけど… -->
<base href="/">
<base href="/SUB-REPO">
<base href="/SUB-REPO/">

また、検証のため、次のようなリンクを用意した。いずれも、https://USER-NAME.github.io/SUB-REPO/test.html に遷移できれば良いな…と思って書いているモノ。

<!-- 相対パス。挙動を確認するため設置 -->
<a href="test.html">
<a href="./test.html">

<!-- ルート相対パス、こう書いて動作させたい… -->
<a href="/test.html">

<!-- ルート相対パス、リポジトリ名を含むのが嫌… -->
<a href="/SUB-REPO/test.html">

結果から行くと、絶対パスで SUB-REPO リポジトリまで記述した場合しか、上手く行かなかった。どれも思った効果は得られなかった。

まず、base 要素にどのようなパスを書いても、ルート相対パスを書いた a 要素に対しては効果がなかった。

base 要素が影響を及ぼすのは相対パス記述のみで、末尾のスラッシュの有無は関係ない。

概念的には、相対パスの記述に対し、手前に base 要素の値を付与している、というモノのようだ。以下の結果表を見れば少しは想像が付くかもしれない。

base 要素の値 + a 要素などの相対パス値 最終的なパス
https://USER-NAME.github.io / ./test.html https://USER-NAME.github.io/test.html
../ (= https://USER-NAME.github.io/) (/) test.html https://USER-NAME.github.io/test.html
https://USER-NAME.github.io/SUB-REPO/ (/) ./test.html https://USER-NAME.github.io/SUB-REPO/test.html
https://USER-NAME.github.io/SUB-REPO/ (/) ../test.html https://USER-NAME.github.io/test.html

base 要素の値に ../ のような相対パスが指定された場合は、その HTML ファイルのパスを起点に、ベースとするフルパスが導かれる。

Node.js を触った人なら、path.join()path.resolve() のように単に結合しているだけ、と思えば分かりやすいか。

で、ルート相対パスが書かれた場合は、base 要素の値は無視して、サイトルート、この場合は https://USER-NAME.github.io/ をルートと見なす、という動きのようだ。

JavaScript でなんとかしてやろう…

それでも、どうしてもプロジェクトサイトでルート相対パスを使いたい、https://USER-NAME.github.io/SUB-REPO/ のようなドメイン以下の任意のパス以下をルートと見なしたい、ということで、JavaScript で制御する方法を編み出した。

今回はたまたま、SUB-REPO 以下の全ての HTML ファイルが /scripts.js を参照する仕様にしてあった。

<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8">
    <link rel="stylesheet" href="/styles.css">
    <script src="/scripts.js"></script>  <!-- ← コレ -->
  </head>
  <body>
    <a href="/test.html"><img src="/image.png"></a>
  </body>
</html>

ルート相対パス /scripts.js は、

  • https://USER-NAME.github.io/scripts.js

と解釈される。プロジェクトサイトではなく、ユーザサイトの領域にある scripts.js を探しに行ってしまうワケだ。

そこで、だ。

ユーザサイトに scripts.js を作成しておき、SUB-REPO の HTML からこの JavaScript ファイルを読み込ませ、そこでルート相対パスを置換することにする。コードの全量は以下。

/** ルート相対パス置換処理 */
(function() {
  /** 定数 : 本ファイルの名前 */
  var thisFileName = 'scripts.js';  // ← ファイル名が異なる場合は変更する
  /** 定数 : 当該 GitHub Pages のルートパス URL を用意する・末尾にスラッシュを付けない */
  var rootPath = 'https://USER-NAME.github.io/SUB-REPO';  // ← ココをリポジトリごとに変更する
  
  // 対象の GitHub Pages から呼び出されていなければ、何も処理せず終了する
  if(location.href.indexOf(rootPath) < 0) {
    return;
  }
  
  /**
   * 指定の要素の属性値をチェックし、ルート相対パス (スラッシュ `/` から始まる値) だった場合、
   * 定数 rootPath を先頭に付与した絶対パスに変換する
   * 
   * @param {string} elementName 要素名
   * @param {string} attributeName 属性名
   */
  var replaceAttribute = function(elementName, attributeName) {
    // console.log(elementName, attributeName, '置換処理開始');
    Array.prototype.forEach.call(document.querySelectorAll(elementName), function(element, index) {
      var attribute = element.getAttribute(attributeName);
      // 属性値がない場合、スラッシュ2つで始まるプロトコル省略の絶対パスの場合、ルート相対パスでない場合は処理しない
      if(!attribute || attribute.substr(0, 2) === '//' || attribute.substr(0, 1) !== '/') {
        return;
      }
      
      if(elementName === 'script' && attribute === thisFileName) {
        // 本ファイル自体は読み込まれているため element は操作しないでおく
        // 代わりに、当該リポジトリ配下にあるはずの同名ファイルを読み込ませるため別要素を作って追加する
        var theScript = document.createElement('script');
        theScript.src = rootPath + attribute;
        element.parentNode.appendChild(theScript);
      }
      else {
        // a 要素・img 要素は属性値変更のみで正しく読み込まれる
        element.setAttribute(attributeName, rootPath + attribute);
        
        // link 要素・script 要素は Node の再挿入を行わないと読込が開始されない
        if(elementName === 'link' || elementName === 'script') {
          var clone = element.cloneNode(true);
          element.parentNode.replaceChild(clone, element);
        }
      }
    });
  };
  
  // 画面のチラつきが発生するため、link 要素のみ本ファイルが読み込まれたタイミングで即処理する
  // (本スクリプトの読み込みは head 要素に書いておくとチラつきが発生しにくくなる)
  replaceAttribute('link', 'href');
  
  /** 初期処理定義 */
  var init = function() {
    replaceAttribute('link', 'href');  // 上の即処理で漏れた CSS ファイルのみ改めて処理する
    replaceAttribute('script', 'src');
    replaceAttribute('a', 'href');
    replaceAttribute('img', 'src');
  };
  
  // 読み込みタイミングに関わらず確実に実行されるよう制御する
  if(!document.readyState || document.readyState === 'interactive') {
    window.addEventListener('load', init);
  }
  else if(document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', init);
  }
  else {
    init();
  }
})();

少々長いので順を追って解説する。

  • IE11 でもトランスパイルせず動作するよう書いてある (そのため const は未使用)
  • 最初に2つ定数を定義してある。ルート相対パスの置換処理で必要となる、「本スクリプトファイルの名前」と「ルートと見なしたいパス」の定義だ
  • このスクリプトが「ルートと見なしたいパス」配下の HTML から読み込まれていない場合は、何も処理せず終了する
  • replaceAttribute 関数を定義。ページ中の全ての linkscriptaimg 要素に対応して、ルート相対パスの記述があれば、「ルートと見なしたいパス」を結合して絶対パスに置換する、という処理を定義している
  • a 要素と img 要素については、setAttribute() で値を置換してやれば、すぐに画面上にも反映される。
    しかし、link 要素と script 要素についてはそれだけでは外部ファイルを読み込んでくれず、replaceChild() で DOM を再挿入する必要があった
  • 本来は https://USER-NAME.github.io/SUB-REPO/scripts.js を読み込みたかったのに、ルート相対パスのせいで、この https://USER-NAME.github.io/scripts.js ファイルが読み込まれて、この処理が実行されている。
    ということは、<script src="/scripts.js"> の部分だけは、コレとは別に <script src="https://USER-NAME.github.io/SUB-REPO/scripts.js"> を読み込んで、本来の処理を行わせてあげる必要がある。
    そこで、「本スクリプトファイル名」の script 要素が登場した時だけ、別に script 要素を生成して appendChild() している
  • CSS ファイル (link 要素) は head 要素内で読み込まれるため、本スクリプトが DOMContentLoaded 以降に動作すると、「ページの内容は表示されているのにスタイルが適用されていない」状態が一瞬発生してしまう。その後、本スクリプトによって link 要素のパスが修正され、CSS ファイルが適用されると、結果的に「画面のチラつき」が発生する。
    本スクリプトファイルを head 要素内で読み込んでいれば、その場で link 要素のパスだけ修正してしまい、チラつきを可能な限りなくそうしている
  • その他の要素に関しては、どこでどう呼ばれても実行できるよう、DOMContentLoadedwindow.onloadinit() 関数を予約して処理している。
    このやり方は、読み込みタイミングが本来のタイミングより遅くなる <script src="https://USER-NAME.github.io/SUB-REPO/scripts.js"> にも組み込んでおくと安全だろう

こんな感じ。ハッキリ言ってメチャクチャ強引。

自分が必要なかったので、iframeembedobject 要素などには対応させていないが、replaceAttribute() 関数を拡張すれば対応できると思う。

以上

今回のスクリプトは、自分のメインサイト Neo's World のミラーサイトを GitHub Pages で公開するにあたって編み出した。実際に動作している様子は以下のリンクを覗いてもらえば分かるかと思う。

ということでまとめ。

  • 正規の?方法で、ルート相対パスの起点を変更する方法はなかった (base 要素は効かなかった)
  • スクリプトを読み込むような HTML になっていれば、ユーザサイトの方に同名のスクリプトファイルを置いて、ソイツから「ルート相対パス」を「絶対パス」に置換する処理を行ってやれば、なんとか直せなくもない…

という話でした。

スラスラわかるHTML&CSSのきほん 第2版

スラスラわかるHTML&CSSのきほん 第2版