TRISHAFT

Astro製ブログをGitHub Pagesのサブディレクトリに公開する

  #プログラム#Astro

Astroは、主にSSG (Static Site Generator) として動作し、爆速の静的サイトを生成するライブラリ。

基本的に使いやすいのだけれど、タイトルのような条件でブログを作ろうとしたら微妙に大変だったので、やり方をメモしておく。

ちなみに一応これでちゃんと動くけど、書いているやり方が正しいのかどうかはよく分からず、全然自信がない。あくまでも自分で編み出した方法なので、もしもっと良いやり方があれば教えて欲しい。

結論

これで対応を行ったブログのリポジトリ動作するページがあるので、これを参考にしてもらうと分かりやすいかも。

前段の話・条件

タイトル通り、Astroで構築したブログをGitHub Pagesのサブディレクトリに公開するものとする。

!

この記事を読んでいるような人には説明不要だろうけど、GitHub Pagesを使えば静的サイトを無料で手軽に公開できる。リポジトリを公開するように設定して、コンテンツをリポジトリにプッシュするだけでOK。

他のホスティングサービス(VercelとかNetlifyとか)を使うのもアリで、実はAstroはそっちの方が楽だったりするけど、リポジトリ管理も合わせてGitHubだけで完結しているシンプルさが良い。

さて、GitHub Pagesでは、リポジトリ名でサブディレクトリができる形になって、URLは https://<ユーザ名>.github.io/<リポジトリ名>/ になる。このサブディレクトリというのが曲者で、Astroで対応するためには色々と工夫が必要になる。

逆に言えば、GitHub Pagesを使ったとしても、URLがサブディレクトリを持たない以下の2パターンのいずれかのときはここで書いているような工夫は不要なので、そのままデプロイすれば良い。

したがって、GitHub Pagesの通常のドメインのまま、好きなリポジトリ名を付けて公開するときが対象になる。

テンプレートからブログを作成する

ここから実際にブログを作成・修正しつつ手順を説明していくけど、その前提として、マシンへそれなりに新しい Node.js がインストールされている必要がある。詳しいことはAstro公式ドキュメントのインストールの項に書いてあるので、そちらを読むのが確実。

Astroのテンプレートを使ってブログを作成するところから始めるために、適当なディレクトリで以下のコマンドを実行する。

npm create astro@latest

色々質問されるので、それに答えていく。今回は以下のようにした。

基本的には好きなものやデフォルト項目を選べばいいけど、記事の内容をなぞるときはテンプレートとしてブログを選ぶと分かりやすい。

これでひとまず動く状態となったので、作成されたプロジェクトのディレクトリに移動して以下のコマンドを実行する。

npm run dev

開発サーバが立ち上がるので、http://localhost:3000/(多分)へWebブラウザでアクセスすれば、ひとまずローカル環境で動いていることが確認できる。

GitHub Pagesへのデプロイ設定

この時点できちんと動かないことを確認するため、早速GitHub Pagesへのデプロイを行う。

本記事では、GitHubへのプッシュで自動的にサイトがビルドされるよう、GitHub Actionsによる自動デプロイを設定していく。

まずは適当にGitHubにパブリックリポジトリを作成し、それをプロジェクトのリモートリポジトリとして登録する。(この記事ではastro-testとしてみた)

それから自動デプロイのために色々と設定していく。やり方は基本的に、Astro公式ドキュメントに書いてある通り。2023年2月時点では未翻訳で英語ではあるけど、書いてある内容は難しくない。

やることをざっくり列挙すると以下の通り。<ユーザ名><リポジトリ名>は適宜自分のGitHub環境に合わせる。

  1. astro.config.mjsに、sitebaseを設定する。
    • sitehttps://<ユーザ名>.github.io/
    • base/<リポジトリ名>(頭にスラッシュを入れ、末尾には入れない)
  2. プロジェクト内に.github/workflows/deploy.ymlを作成し、公式ドキュメントに記載されているコードを貼り付ける。
  3. GitHubのリポジトリで、Setting タブの Pages セクションを開く。
  4. SourceGitHub Actions を選ぶ。
  5. ローカルのリポジトリをコミットしてプッシュする。

これで自動的にデプロイされ、大体1分くらい経てばGitHub Pagesに公開されるので、https://<ユーザ名>.github.io/<リポジトリ名>で開けるはず。もしダメならリポジトリのActionsタブを見てビルドの実行結果を確認・対応する。

この時点での状況を確認

ローカル環境

ここまでやって、ローカルで開発サーバを開いていたら(npm run devが実行中のままだったら)、ページhttp://localhost:3000/の表示が「404: Not found」のエラーページになっているはず。

これは先程行ったbaseの設定が効いていて、ページ構造が全部baseで設定したディレクトリ内に存在することになっているため、ルートにはコンテンツが存在しないのが理由。

したがってページを表示するには、http://localhost:3000/baseで設定したフォルダ名を書き加える。それか404の画面に書いてあるフォルダへのリンクをクリックしてもよい。

GitHub Pages

デプロイした結果として、https://<ユーザ名>.github.io/<リポジトリ名>でページは開けているはず。

ただ、ブラウザの開発ツールでコンソールを見ると、ファビコン(favicon.svg)が404エラーとなって読み込めていないことが確認できる。ひとまず現時点ではこれで問題ない。

リンクの動作を見る

さてさて、ひとまずトップページは表示されたものの、ここからが問題。ローカルでもGitHub Pagesでも、自サイト内へのリンク(例えば上部の「About」)をクリックすると、404エラーになってアクセスできないことがわかる。

これはアドレスバーを見ると分かる通り、リンク先のURLが正しくないため。サブディレクトリ内にページが存在しているはずなのに、URLがサブディレクトリではないページになっている。

プロジェクト内のソースコードを見れば分かりやすい。ヘッダ部分のコードはsrc/components/Header.astroであり、この13行目を見ると、Aboutのリンク先が/aboutと指定されている。つまりルートからのリンクになっているので、<ドメイン>/about/を読み込むことになっていて、正しい<ドメイン>/<リポジトリ名>/about/ではない。サブディレクトリが反映されておらず、存在しないページを表示しようとしているので404エラーとなっている。

このように、Astroはリンクに対してbaseの設定を反映してくれない。これはaタグのリンクだけでなく、ファビコンへのリンクのようなルートからの絶対パスで記載するものすべてが同様。

以降、これをきちんと動作するように修正していく。

!

ちなみにこの時点でもローカル側ではファビコンを読み込めている。これは実際には404であるファビコンのリクエストに対して、開発サーバが301 Moved Permanentlyを返すことでリダイレクトしてくれているため。

このおせっかいな動作のおかげで、ローカルでは表示されているのにデプロイすると表示されないので、結構な罠。

 


基本的な対応方針

前述の通り、リンクや画像の読み込みが上手く動かないのは、URLが実態と異なっているから。astro.config.mjsbaseの設定が反映されるところと反映されないところがあり、自サイト内へのリンクは自分で対応する必要がある。

この対応自体は非常に単純で、ルート「/」から始まっているパスに対して、ディレクトリ名(=リポジトリ名)を頭に付けること。要するに、以下のように書けば良い。

ただ、コードにこうやって直接記載するのは色々とよろしくない。例えば将来的にリポジトリ名を変更する場合、ソースコードのあちこちを修正することになってしまう。ということで、実際には後述するようにちょっと工夫を加えた形で対応する。

もう1つ考えられる対応方針(この記事ではやらない方法)

もう1つ単純に思い付くであろうシンプルな対応方針がある。それはリンクを相対パスで指定すること。例えばトップページからblogページへのリンクは ./blog と書けば良い。

ただこれ、実際にやると色々と面倒なことになる。

ということで逆に面倒なので、この記事ではこの対応方法は採用しない。少し工夫した形で絶対パスを指定するやり方を行っていく。


対応の実施

ここから実際に対応していく。

まずはDRY原則に従い、リンクで使うパスを変換する関数を作ることにする。各ページのリンクは、この関数により生成されたパスを使用する。

関数でやりたいことは単純で、例えば/blogが渡されたら/<リポジトリ名>/blogを返すだけ。

パス変換関数の作成

src/scripts/util.tsを作成して、以下のようなコードを書く。中身はまぁ単純。

export function href(s: string): string {
  if (s.charAt(0) === '/') {
    s = s.substring(1);
  }
  return `${import.meta.env.BASE_URL}${s}`;
}

import.meta.env.BASE_URLAstroの環境変数で、baseで設定している値を取得できる。これにより、リポジトリ名が変わった場合でも設定ファイルでbaseを変更するだけで済み、ソースコードの変更が不要になる。

!

ただこの関数、チェックが明らかに不十分なので、このまま使うのはおすすめしない。例えば引数として相対パスや https://... のようなURLが渡されたときに不正な値を返してしまうので、きちんとチェックすべき。

import.meta.env.BASE_URLの罠

さて、この関数内で利用しているimport.meta.env.BASE_URLの値にはちょっとした罠があり、ドキュメントに記載されている使い方が嘘だったりする。

ドキュメントには使い方の例として以下のように書かれている。

<img src=`${import.meta.env.BASE_URL}/image.png`>

しかし実際に使ってみると分かるけど、この変数はなぜか末尾にスラッシュが付いた形で返ってくる。すなわちドキュメント通りに書くと、スラッシュが2つ連続した不正なパスになってしまうという罠がある。

ということで上記のhref()関数では、引数で渡された1文字目のスラッシュをif内で削除するとともに、文字列の連結時に間にスラッシュを入れないようにしている。

!

ちなみにbaseに関する当該ドキュメントの日本語版はバージョンが古いのか、使い方の記載自体がない。

関数の使い方

この関数を使ってリンクを行うときは、当該ページでhref()をインポートして、リンクが書かれている部分でhref()を使うように修正する。

具体的には以下のようになる。

// src/components/Header.astro

import { href } from '../scripts/util'; // (この行だけコードフェンス内)

...(略)...
// <HeaderLink href="/">Home</HeaderLink> を修正
<HeaderLink href={href("/")}>Home</HeaderLink>
// <HeaderLink href="/blog">Blog</HeaderLink> を修正
<HeaderLink href={href("/blog")}>Blog</HeaderLink>
// <HeaderLink href="/about">About</HeaderLink> を修正
<HeaderLink href={href("/about")}>About</HeaderLink>
...(略)...

他にも以下のような部分を同様に修正すれば良い。

これでコミットしてプッシュすれば、リンクがきちんと働くことを確認できる。

記事のMarkdownファイルから読み込む画像にMDXで対応する

これで完璧……と思いきや、これだけではブログ記事での画像読み込みに対応していない

この時点でデプロイしたGitHub Pagesの方を見ると、例えば「Markdown Style Guide」の記事に埋め込まれている画像が表示されていない。開発者ツールのコンソールを見ると、画像のURLがサブディレクトリなしになっているので404エラーになっていることが分かる。

!

ちなみにローカル環境では301のリダイレクトにより表示されてしまうので注意。

前項の修正で対応していないので当然と言えば当然ではある。ただ同じように修正しようとしても、.mdファイルの中ではJavaScriptを書けないので、少し違った対応を行う必要がある。

ここで登場するのがMDX形式の.mdxファイル。MDXはマークダウンの中でJSXが書けるようになるファイルフォーマット。JSXが書けるのならJavaScriptも書けるということで、これで対応する。

ちなみにAstroのブログテンプレートでは最初からMDXに対応しているので、非常に簡単。

例えば「First post」の記事の中に画像を表示するなら、src/content/blog/first-post.mdのファイル名をfirst-post.mdxと拡張子を変えてMDXファイルにする。そして以下のようなコードを記事のどこかに追記する。

import { href } from '../../scripts/util';

<img src={href('/placeholder-hero.jpg')} />

すると画像のパスが解決され、「First post」の記事の中に画像が表示される。これで一件落着。


ここまで書いてきた通り、Astroでサブディレクトリにデプロイするときは自前で色々と対応する必要がある。こんなのbaseの値を元にしてAstro側で自動的に上手いことやってくれれば楽なのに、と思わなくもないけど、色々なケースを考えるとそう一筋縄ではいかないであろうことは容易に想像できるので、ユーザ側に任せる形になっているんだろうと思っている。

ただまぁ、ここまで色々と書いてきたけど、自分がまだAstroをよく分かっていないこともあり、 実はもっとスマートな方法が存在する可能性がかなりあると思っているので、もしそんな方法を知っている人がいれば教えて欲しい。