Open9

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()を使うのが良さそうです。

SLMNLLSLMNLL

AstroでCSS Scroll-driven AnimationsのPolyfillを使う際の注意

結論
  1. CSS Scroll-driven Animationsに関わるスタイルは、<style is:inline>の中に書くべし
  2. FireFox用に、animation-duration: 1ms;を書くべし

CSSだけでページスクロール時のアニメーションが表現できるCSS Scroll-driven Animationsは、2024年6月現在、Chromium系のブラウザでのみ動作します。

ただ、SafariやFireFoxなどのブラウザに対応したい場合でも、以下のPolyfillスクリプトを読み込んであげるだけで動作するようになります。

https://github.com/flackr/scroll-timeline

使い方は本当に簡単で、<script src="js/scroll-timeline.js></script>の一文を<head>タグ内などに挿入するだけです。

Astroでscroll-timeline.jsを使ってみる

ただ、ローカルでは上記の通りにPolyfillを実装すると動くものの、デプロイしたら動かない…という問題が発生し、1週間ほどハマりました[1][2]

実装環境:

  • ホスティング:Cloufdflare Pages
  • フレームワーク:Astro

原因:

<script><style>のそれぞれの外部ファイルの読み込まれる順番。恐らく[3]

対策:

  1. アニメーションに関わる部分をインラインスタイルとして書く。一番大事。
  2. FireFoxはanimation-duration:auto;だとアニメーションが動かないため、animation-duration:1ms;と書く。これで「Safariでは動くけど、FireFoxでは動かない」のような奇怪な現象にも悩まされずに済みます。

ちゃんと動く例:

<head>
    <!-- blah blah-->
    <style is:inline> /* is:inlineと書くと、HTMLファイルにインライン埋め込みされる */
        html{
            scroll-timeline: --scroll; /* スクロールタイムラインをhtmlに指定 */
        }
        
        .scroll-driven-animation__body {
            animation-name: fadeIn;
            animation-timeline: --scroll; /* 上で指定したタイムラインを参照 ≒ scroll(root) */
            animation-timing-function: ease-in-out;
            animation-fill-mode: both;
            animation-duration: 1ms; /* autoだとFireFoxの機嫌が悪くなる */
        }
        
        @keyframes fadeIn {
            from{ opacity:0; }
            to { opacity:1; }
        }
    </style>
</head>

AstroとCloudflare Pagesの組み合わせは非常に強力で、高速で快適な閲覧体験をもたらしてくれる反面、速すぎて「スタイルを当てるのが追いつかない」みたいな挙動が稀に起こる印象なので、そういった場合はis:inlineを駆使するのが良いと思いました。

脚注
  1. 状況としては、デプロイしたウェブを表示すると、アニメーションのto時点の部分がいきなり表示されちゃう。そしてスクロールにも反応しない。でもブラウザバックなどで同じページを表示すると、今度は正常にスクロールに反応してアニメーションする…といったもの。 ↩︎

  2. 正直にいうと、CSS Scroll-driven Animationsに関する知識不足と、各ブラウザの挙動の違いから、ローカルで動かすまでも大変苦労しました。情けない…。 ↩︎

  3. deferasyncも効果なし。 ↩︎