Open8

Astro備忘録

SLMNLLSLMNLL

エンコードされたURL(日本語URL)のルーティング

ヘッドレス化したWordPressで記事を管理して、Astroでフロントエンドを構築する際、日本語が混入したURLだとAstroのルーティングがうまくいきませんでした。

URLエンコードされない文字列だけを想定して開発するなら、例えば以下のようなコードでURLのルーティング可能です。

[slug].astro 🧐
---
// [slug].astro の [slug]に、WPで設定した記事のスラッグが対応します
const { slug } = Astro.params;

export async function getStaticPaths() {
  const res = await fetch( WPURL + "/wp-json/wp/v2/posts");
  const posts = await res.json();

  return posts.map((post:any) => ({
    params: { slug: post.slug },
    props: { post: post },
  }));
}
...

ですが、URLに日本語が含まれてたりする場合、404エラーを吐き出してしまい正しくルーティングできません。WordPressのREST APIから返ってくるJSONのslugの値はURLエンコードされてるのに…。

デコードしてみよう

コード側でJSONのslugをデコードしたら正しくルーティングできました。

[slug].astro ✅
---
// [slug].astro の [slug]に、WPで設定した記事のスラッグが対応します
const { slug } = Astro.params;

export async function getStaticPaths() {
  const res = await fetch( WPURL + "/wp-json/wp/v2/posts");
  const posts = await res.json();

  return posts.map((post:any) => ({
    params: { slug: decodeURI(post.slug) }, // <- ここにdecodeURI()を追加
    props: { post: post },
  }));
}
...
SLMNLLSLMNLL

astro:assetsでも、getImage()stringでファイルパスを渡したい

Astro 3.0でastro:assetsが正式に実装され、@astrojs/imagedeprecatedとなりました。

astro:assetsでも@astrojs/imageのような感覚で画像の最適化をすることが可能です。ただ、今までと違う点もあり、例えばgetImage()にurlやファイルパスをstringで雑に渡すような使い方は、(今のところ)できません[1]

とはいえ、ファイルパスのstringから画像を最適化したい場合もあると思います。

Astro.globの出番

まずはAstro.globimport.meta.globを使って、条件に合う画像を全部インポートします。そこからファイルパス(string)を使って、目的の画像を抽出するような操作をすればOKでした。

コードとしてはこんな感じです。

astro3.0以降でgetImage()を使う✅
async function getAvifFromFilePath(filePath: string) {
  // public内のimagesディレクトリのサブディレクトリに画像がある場合
  const images = import.meta.glob('/public/images/**/*');

  // imagesは条件 ('/public/images/**/*') に合致した全ファイルの
  // ファイルパスがkeyとなっているオブジェクトなので、
  // 目的の画像のファイルパスをkeyとして指定すれば、
  // images内にある目的の画像のオブジェクトを抽出できる
  const image = await images[filePath]();

  // getImage()には、先ほど取得した画像のオブジェクト内にある値、defaultを渡す
  return await getImage({ 
    src: image.default, 
    format: 'avif', 
    quality: 50, 
    height: 1024, 
    width: 1024
    });
}

ローカルでビルドする分にはいいんですが、リモートでビルドする場合は、いちいち画像を全部インポートするのは少しモヤモヤします[2][3]。もっとスマートな方法がありそう…。

脚注
  1. getImage()src引数にstringでファイルパスを渡すと、Expected src to be an image.というエラーが出ます。ファイルパスが間違ってない場合は、画像自体は表示される(ただ、表示されるのは最適化されていない元々の画像)ので、最適化が上手くいってないことに気付きづらい。
    ちなみにコンソールを見た限り、srcstringを渡した場合は、widthheightのどちらも要求されます。ただし、渡してもやっぱり最適化はしてくれない。 ↩︎

  2. import.meta.globには、例えば'public/images/${foo}/*'みたいなかたちで動的な条件を指定することはできず、Invalid glob import syntax: Could only use literalsと返されます。
    globの書き方はこちらを参照:globのパターン構文↩︎

  3. switchで切り分けるやり方もあるようです。 ↩︎

SLMNLLSLMNLL

dynamic importを使う方法

もう少し良い方法がないか調べてたら、dynamic importの存在を思い出しました。

ファイルパスを指定してインポートすることができるのですが、やはりdynamic importに渡すファイルパスも静的リテラルである必要があります[1]

例えば、以下のコードでも動きますが、コンソールにはThe above dynamic import cannot be analyzed by Vite.というWarningが表示され、「コンパイル時にViteで値が検証できなくて困るよ!」と文句を言われます。

dynamic importを使ってgetImage()を動かす✅
async function getAvifFromFilePath(filePath: string) {
  const image = await import(filePath); // コンソールでwarning表示される行

  // getImage()には、上でインポートした画像のオブジェクト内にある値、defaultを渡す
  return await getImage({ 
    src: image.default, 
    format: 'avif', 
    quality: 50, 
    height: 1024, 
    width: 1024
    });
}
脚注
  1. Viteの公式ドキュメントにはconst module = await import('./dir/${file}.js')のような使い方ができると書いてありますが、実際にそのように書くとwarningが出ます。また、ファイルパスは相対パスにしなければいけないようです。絶対パスでも動きますが…。ままならない。 ↩︎

SLMNLLSLMNLL

Viteに黙ってもらうには、/* @vite-ignore */キーワードを使うこともできます。コンパイル時に渡すファイルパスが「確実にあっている」ことが自明である場合などに使うと良さそうです。

上述のコードの2行めを以下のように書き換えます。

const image = await import(/* @vite-ignore */ filePath);
SLMNLLSLMNLL

ビルドをしてみたところ、await importを使ってgetImage()を呼び出すと 'ERR_MODULE_NOT_FOUND'エラーが発生してしまいました。公式のドキュメント通りに、Aseto.glob()を使うのが良さそうです。