🔗

Astro でリンクや参照などを相対パスでビルドする

2023/03/07に公開

はじめに

まず、Astro では、相対パスでの生成は公式的には対応しておりません。

https://github.com/withastro/astro/issues/4229

https://github.com/withastro/roadmap/pull/381

上記の issue と提案は上がったことはあるものの、現在は close されており、やり取りの中身を見ても、公式がサポートすることはあまり期待できなそうです。

個人的にも、こういった汎用的なフレームワークはやはり絶対パスのほうが何かと無難です。それは開発する側でも使う側でも。ですので、特殊な事情がないかぎりは絶対パスか絶対 URL を推奨します。

ただ、静的書き出しができるフレームワークにおいて、相対パスの需要も理解できます。

まず結論

https://www.npmjs.com/package/astro-relative-links

プロジェクト内のリンクや参照を全部相対パスにして問題ないということであれば、astro-relative-links という Astro integration (Astro のプラグインみたいなもの) を作りましたので、こちらを使えば実現できます。使い方は後述。

要件

今回、複数ページのコンテンツをサブディレクトリに配置するというご依頼がございました。ここまでなら、Astro の設定の base を使えば特に問題はないです。

しかし別の要件として、こちらのコンテンツは大学の授業などで利用できるよう配布予定になっており、その際、自由な場所に配置できるようにする必要があります。

こちらのコンテンツをダウンロードする大学の教職員さんは、基礎的な HTML の知識はあるものの、都度 base を変更してビルドするというプロセスを取ることは難しい。となるとやはり相対パスにする必要があります。

1 ページの LP であれば、相対パス直書きでもさほど問題ないと思いますが、複数ページになると、共通コンポーネントやスタイル・スクリプトなどで問題が生じるので、やはり自動的に相対パスを解決する仕組みが必要になってきます。

アプローチ 1:相対パスコンポーネント

src/components/A.astro
---
import type { HTMLAttributes } from 'astro/types';
import path from 'path';

type Props = HTMLAttributes<'a'>;

const { href: _href, ...attrs } = Astro.props;

const href = (() => {
  if (typeof _href === 'string' && _href.startsWith('/')) {
    return path.relative(Astro.url.pathname, _href);
  }
  return _href;
})();
---

<a href="{href}" {...attrs}>
  <slot />
</a>

上記のようなパスを相対パスに変換するコンポーネントを作成して、相対パスにしたいところでこれを使う。

Astro では、--- に囲まれたコンポーネントスクリプトはビルド時にも実行され、その結果は静的 HTML として生成されます。Node.js の path.relative()Astro.url とリンク先を渡せば、それぞれのページから見たリンク先の相対パスを取得することができます。

src/components/Header.astro
---
import A from './A.astro';
---

<A href="/">Home</A>
<A href="/apple">Apple</A>
<A href="/banana">Banana</A>

上記のように書けば以下のように生成されます。

dist/index.html
<a href="">Home</a>
<a href="apple">Apple</a>
<a href="banana">Banana</a>
dist/apple/index.html
<a href="..">Home</a>
<a href="">Apple</a>
<a href="../banana">Banana</a>
dist/banana/index.html
<a href="..">Home</a>
<a href="../apple">Apple</a>
<a href="">Banana</a>

スタイルやスクリプトなどについても a タグ同様、相対パスに変換する LinkScript コンポーネントなどを作成すれば一応は実現できます。

問題点

  1. 相対パスにしたいタグのコンポーネントをそれぞれ用意する必要があります。alinkscriptimg などなど。
  2. Tailwind CSS や TypeScript など、コンパイルが必要なスタイルやスクリプトは Astro の機能を利用できず、自分で用意する必要があります。

アプローチ 2:Astro Integration でどうにかする

Astro には Integration という API が用意されており、さまざまなタイミングで独自の機能や動作を Astro に対して追加することができます。React や Tailwind CSS などを使えるようにするには、Integration 経由で行うことが基本かと思います。

しかし残念ながら、僕が調べたかぎりでは HTML やアセットを生成される前にいい感じに変換する方法は見つかりませんでした。結局、生成された HTML 内のパスを一括置換する方法が一番シンプルで確実でした。

そこで作ったのが上記 astro-relative-links という integration です。

インストール

自動インストール

Astro 公式の add コマンドに対応しておりますので、ご自身のパッケージマネージャーに合わせて下記のいずれかのコマンドを実行してください。あとはプロンプトに従って進めば自動的に有効化されます。

# npm
npx astro add astro-relative-links
# or Yarn
yarn astro add astro-relative-links
# or pnpm
pnpm astro add astro-relative-links

手動インストール

まずはパッケージをインストールします。

# npm
npm install astro-relative-links
# or Yarn
yarn add astro-relative-links
# or pnpm
pnpm add astro-relative-links

次に、astro.config.* ファイル内で integrations プロパティに astro-relative-links を適用します。

  import { defineConfig } from 'astro/config';
+ import relativeLinks from 'astro-relative-links';

  export default defineConfig({
    // ...
+   integrations: [relativeLinks()],
  });

使い方

通常の Astro プロジェクトのようにビルドしてください。プロジェクト内のパスはすべて相対パスで生成されます。https などから始まる絶対 URL や ./ などの相対パスは変換されません。

また、Astro の base 設定にも対応しており、たとえば以下のような設定とコンポーネントの場合、

astro.config.mjs
import { defineConfig } from 'astro/config'
import relativeLinks from 'astro-relative-links'

export default defineConfig({
  base: 'fruits',
  integrations: [relativeLinks()],
})
src/components/Header.astro
<a href="/">Home</a>
<a href="/fruits/">Fruits</a>
<a href="/fruits/apple/">
  <img src="/fruits/images/apple.png" alt="" />
  Apple
</a>
<a href="/fruits/banana/">
  <img src="/fruits/images/banana.png" alt="" />
  Banana
</a>

以下のように base 配下のリンクのみが相対パスに変換されます。Home はプロジェクト外であるため ../ には変換されないことについてご注意ください。

dist/index.html
<a href="/">Home</a>
<a href="./">Fruits</a>
<a href="./apple/">
  <img src="./images/apple.png" alt="" />
  Apple
</a>
<a href="./banana/">
  <img src="./images/banana.png" alt="" />
  Banana
</a>

その他の例については README をご参照ください。

おわりに

最初に述べたように相対パスは何かと厄介なので、astro-relative-links はすべてのケースに対応できるとは思いませんが、静的書き出しして納品するような場合はおそらく問題ないと思います。

今回は必要にかられて integration を作りましたが、自分で積極的に相対パスを採用することはないと思います。Twitter でつぶやいたところ、意外と反応をいただきましたので integration を公開して記事にしました。

https://twitter.com/ixkaito/status/1631188229113217025?s=20

Discussion