2484 文字
12 分
Fuwari を Astro 7 に上げると CSS のビルドが落ちる — 原因は画像用の import.meta.glob とコメントアウトした HTML

前回の記事の時点で、このブログの Astro はテーマを取り込んだときの 5.1.6 のままでした。今回、これを 7 系に上げています。

昨日の作業でAstroのバージョンが7とのことでしたので、新規ブランチで新しくできますか?

orangeVaper

この記事が扱う症状#

Fuwari をベースにしたブログで Astro を 7 系に上げ、pnpm build を実行すると、次のどちらかで止まります。

[ERROR] [vite] ✗ Build failed in 1.08s
[postcss] /Users/<username>/git/astro-blog/src/styles/markdown.css:23:9: The `link` class does not exist. If `link` is a custom class, make sure it is defined within a `@layer` directive.
[ERROR] [vite] ✗ Build failed in 1.26s
[lightningcss minify] Unexpected token Delim('&')
  Location:
    undefined:2305:19

1 つ目の原因は src/components/misc/ImageWrapper.astro の import.meta.glob('../../**') です。2 つ目の原因は src/components/Footer.astro にコメントアウトで残っている HTML です。どちらも CSS ファイル自体には問題がありません。

前提と、上げたもの#

Astro 5.1.6 から 7.4.4 への更新です。Tailwind は 3 系のまま残し、Astro と、それに合わせて上げる必要のある @astrojs/* だけを更新しました。

パッケージ更新前更新後
astro5.1.67.4.4
@astrojs/mdx4.3.148.1.2
@astrojs/svelte7.0.39.1.2
@astrojs/markdown-remark(なし)7.3.5
@astrojs/tailwind5.1.4削除

@astrojs/tailwind は peerDependencies が astro ^5.0.0 までで、Astro 7 では使えません。Fuwari には postcss.config.mjs がもともとあり、Tailwind 3 はそこから PostCSS プラグインとして読み込まれるので、インテグレーションは外しました。インテグレーションが自動で入れていた @tailwind base; @tailwind components; @tailwind utilities; は、src/styles/tailwind.css を作って Layout.astro から import しています。

Content Collections の書き換えは、公式の移行ガイドどおりです。

  • src/content/config.ts を src/content.config.ts に移し、glob() loader を付ける
  • entry.slug を entry.id に、entry.render() を render(entry) に置き換える
  • 定義していなかった src/content/spec も、コレクションとして明示的に定義する
WARNING

ディレクトリ形式の記事(<slug>/index.md)は、id が <slug> になります。 Fuwari はカバー画像の場所を getDir(entry.id) で求めているので、そのままでは画像が見つかりません。entry.filePath から求めるように変えました。

ここまででビルドを実行すると、1 つ目のエラーが出ました。

エラーは markdown.css の 23 行目です。

a:not(.no-styling) {
    @apply relative bg-none link font-medium text-[var(--primary)]
    underline decoration-[var(--link-underline)] decoration-1 decoration-dashed underline-offset-4;

link は Tailwind のクラスではなく、main.css の @layer components の中で定義されているクラスです。Tailwind 3 は、別のファイルで定義されたクラスを @apply できません。つまり、markdown.css が main.css と切り離されて、単独で処理されています。

そこで、markdown.css がどこで読み込まれているかを探しました。

grep -rn "markdown.css\|main.css" src

何も出ませんでした。main.css も markdown.css も、どこからも import されていません。それでも 5.1.6 のビルド結果には両方の中身が入っていました。

読み込んでいたのは ImageWrapper.astro です。

// TODO temporary workaround for images dynamic import
// https://github.com/withastro/astro/issues/3373
const files = import.meta.glob<ImageMetadata>('../../**', {
  import: 'default',
})

カバー画像を探すための glob ですが、パターンが '../../**' なので、画像に限らず src 配下のすべてのファイルを拾います。src/styles/ の CSS もこれに含まれます。サイト全体の CSS は、明示的な import ではなく、この glob に巻き込まれて読み込まれていました。

5.1.6 のビルド結果では、これらの CSS は Tailwind の base・components・utilities と一緒に 1 本のファイルにまとまっていました。7.4.4 では、glob で拾われた CSS が 1 ファイルずつ PostCSS にかかり、markdown.css の @apply link が解決できなくなっています。

直し方#

glob を画像だけに絞りました。

const files = import.meta.glob<ImageMetadata>(
  '../../**/*.{png,jpg,jpeg,gif,webp,avif,svg}',
  { import: 'default' },
)

CSS は、先ほどの src/styles/tailwind.css に @import でまとめます。postcss-import が postcss.config.mjs に入っているので、1 本のファイルとして処理され、@apply link も解決されます。

@import "tailwindcss/base";
@import "tailwindcss/components";
@import "./main.css";
@import "./markdown.css";
@import "./photoswipe.css";
@import "./scrollbar.css";
@import "./transition.css";
@import "tailwindcss/utilities";

並び順は、5.1.6 のビルド結果の CSS を開き、各ファイルの代表的なセレクタが何バイト目に出てくるかを調べて合わせました。main.css の先頭にあった @tailwind components; は、ここで読み込む components と重複するので削除しています。Stylus のファイル(variables.styl と markdown-extend.styl)は postcss-import で読めないので、Layout.astro から直接 import しました。

これで 1 つ目のエラーは消え、2 つ目のエラーが出ました。

Unexpected token Delim('&')#

[lightningcss minify] とあるとおり、CSS を圧縮する段階のエラーです。Astro 7 が使う Vite 8 では、CSS の圧縮に lightningcss が使われます。場所は undefined:2305:19 で、元のファイル名が分かりません。

astro.config.mjs の vite.build.cssMinify を一時的に false にしてビルドすると、圧縮前の CSS が dist/_astro/ に出力されます。その中の & を探しました。

.max-w-\[var\(\&\#45\;\&\#45\;page-width\)\]{
    max-width: var(&#45;&#45;page-width);
}
.border-\[var\(\&\#45\;\&\#45\;primary\)\]{
    border-color: var(&#45;&#45;primary);
}

&#45; は - を HTML の文字参照で書いたものです。Tailwind が max-w-[var(&#45;&#45;page-width)] というクラス名をどこかから拾い、そのまま CSS を生成していました。

拾われていたのは Footer.astro の 8 行目です。

<!--<div class="border-t border-[var(&#45;&#45;primary)] mx-16 border-dashed py-8 max-w-[var(&#45;&#45;page-width)] flex flex-col items-center justify-center px-6">-->

HTML のコメントの中ですが、Tailwind はファイルをテキストとして走査するので、コメントの中のクラス名も拾います。5.1.6 のビルド結果の CSS にも、同じ 3 つのルールが圧縮された状態で入っていました。Vite 6 の既定の圧縮(esbuild)はこれをエラーにせず、そのまま出力していました。lightningcss はこれを不正な CSS として止めます。

直し方#

使われていないコメントアウトの行なので、2 行とも削除しました。

NOTE

特定には、cssMinify: false でビルドした CSS を & で grep するのが早いです。エラーの Location は、まとめられた後の CSS の行番号なので、元のファイルを指しません。

この記事でも同じエラーが出た#

この記事の下書きを置いた状態でビルドすると、同じ Unexpected token Delim('&') が出ました。本文のコードとして載せた max-w-[var(&#45;&#45;page-width)] などを、Tailwind が .mdx から拾っていました。draft: true の記事はビルド結果に含まれませんが、Tailwind は src 配下のファイルを draft かどうかに関係なく走査します。

記事中のコードブロックとインラインコードは表示用の文字列で、スタイルには使われません。tailwind.config.cjs の content.transform で、.md と .mdx からコード部分を取り除いてから Tailwind に渡すようにしました。

const stripMarkdownCode = (content) =>
  content.replace(/^(```|~~~)[\s\S]*?^\1/gm, "").replace(/``[^`]*``|`[^`\n]*`/g, "")

module.exports = {
  content: {
    files: ["./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue,mjs}"],
    transform: {
      md: stripMarkdownCode,
      mdx: stripMarkdownCode,
    },
  },
  // ...
}

変更の前後で、生成された CSS のセレクタを比べました。どちらも 1026 個で、差はありませんでした。

前後の記事ナビ#

ビルドが通ったあと、5.1.6 と 7.4.4 の出力を比べたところ、記事ページに 1 か所だけ差がありました。記事の下にある「前の記事・次の記事」のボタンです。5.1.6 では常に空で表示されておらず、7.4.4 では表示されていました。

Fuwari は getSortedPosts() の中で記事を日付順に並べ、entry.data.nextSlug と entry.data.prevSlug に前後の記事を書き込みます。一方、記事ページの getStaticPaths() は getCollection() で記事を取り直していて、getSortedPosts() を呼んでいません。

7.4.4 では getCollection() が同じオブジェクトを返すので、トップページなどが先に getSortedPosts() を呼んでいれば、その書き込みが記事ページにも届きます。ビルドではこの順番になるので、全記事で表示されていました。dev サーバーで記事ページを直接開くと、まだ書き込まれていないので空のままでした。トップページを開いてから記事を開くと表示されます。

直し方#

記事ページの getStaticPaths() で、getCollection() の代わりに getSortedPosts() の戻り値を使うようにしました。draft の扱いは同じです。

export async function getStaticPaths() {
  const blogEntries = (await getSortedPosts()) as unknown as CollectionEntry<'posts'>[]
  return blogEntries.map(entry => ({
    params: { slug: entry.id },
    props: { entry },
  }))
}

これで、dev で記事を直接開いた場合もビルドでも、前後の記事が日付順で決まります。ビルド結果では 22 記事のうち 20 記事に前後両方、両端の 2 記事に片側だけのボタンが出ています。

Netlify の Node のバージョン#

Astro 7 は Node 22.23.4 以上が必要です(engines は >=22.23.4)。このブログは netlify.toml にも .nvmrc にも Node のバージョンを書いておらず、Netlify はサイトを作ったときの既定のバージョンでビルドします。push する前に netlify.toml で指定しました。

[build.environment]
  NODE_VERSION = "22"

確認したこと#

更新前にビルドした dist を残しておき、更新後と比べました。

項目5.1.67.4.4
HTML・XML・TXT の出力ファイル120120(URL も一致)
RSS の <item>2323
Pagefind の単語数36413641
_astro/ の CSS の合計約 196KB約 160KB

ブラウザでも新旧を並べて開き、見出し・コードブロック・リンク・カードの computed style を JavaScript で取り出して比べました。値はすべて一致しました。

残った警告は markdown.remarkPlugins と markdown.rehypePlugins が非推奨というものだけで、動作には影響しません。

この日に分かったこと#

エラー・変化実際の原因
The `link` class does not existImageWrapper.astro の import.meta.glob('../../**') が CSS を巻き込んで読み込んでいた
Unexpected token Delim('&')Footer.astro のコメントアウトした HTML から、Tailwind がクラス名を拾っていた
記事の下書きを置くと Unexpected token Delim('&')本文のコードに載せたクラス名を、Tailwind が .mdx から拾っていた
前後の記事ナビの有無記事ページが getSortedPosts() を呼んでおらず、他のページが先に呼んだかどうかで変わっていた

2 つのエラーと前後の記事ナビは、いずれも Fuwari のコードに 5.1.6 の時点からあったものが、Astro 7 と Vite 8 で表に出た形です。

このブログは現在 Astro 7.4.4 で動いています。

この記事は AI(Claude) を活用して書いています
アイキャッチ画像は ComfyUI + Flux.1 で生成しています
Fuwari を Astro 7 に上げると CSS のビルドが落ちる — 原因は画像用の import.meta.glob とコメントアウトした HTML
https://orangevaper.dev/posts/fuwari-astro-7-upgrade-css-build-error/
作者
orangeVaper
公開日
2026-10-29
ライセンス
CC BY-NC-SA 4.0