EleventyVite.js(11ty公式Viteプラグイン)から読み解くベスト設定ガイド
元々がどのようにして作られているかを知らなければ、正しい設定などできはしないとGithubのソースコードを読み解いてどう設定するのが良いのだろうかを考えるという話です。
公式Viteプラグインの役割
ここでの重要なことは11tyが先にビルドしてViteがその結果を後処理するということです。Viteは11tyを知らず、11tyはViteを知りません。プラグインがその橋渡しをしています。
- Viteにそれらの処理も任せるように書くか、
- Viteは開発段階の表示のみ担当して、ビルドは11tyに任せる
という割り切り方が必要になると思います。これらは11tyのv3から、
const isServe = process.env.ELEVENTY_RUN_MODE === "serve";
というような書き方で判定できるので、開発段階だけViteを使用することができます。これは最後の方で書いておきたいと思います。
もしViteで画像他も扱うという場合については、そのディレクトリ構造などの環境によっても異なる部分がありますが、サイト全体で使用する共用画像他(sitemapやrobots.txt等)はpublic/にぶち込めばコピーされます。
dev時( getServer() )
- 11tyがビルドをし、outputディレクトリにHTMLを生成
- Viteがoutputディレクトリをrootとしてmiddlewareで起動
- HMRとasset解決はViteが担う
build時( runBuild() )
- outputディレクトリを
.11ty-viteにリネーム(丸ごと移動) 11ty-viteをrootとしてViteのbuildを実行- 入力は
.htmlファイルのみに絞られる
- 入力は
- viteのoutDirをoutputに設定して書き出し
.11ty-viteを削除- エラー時は
11ty-viteをoutputに設定して書き出し
- エラー時は
Viteがわかるものわからないもの
わかるもの
- HTML内の
<script src="...">、<link rel="stylesheet">は自動でバンドル対象 - CSS内の
url("/assets/font/...")なども自動でコピー対象になる。
Viteに明示的に教える必要があるもの
<img src="...">、<a href="...pdf">をはじめ、robots.txt、sitemap.xml等も教えないとコピーされない
こういう感じです。Viteに教える最も確実な方法は、public/に入れることですが、出力もpublic/の中に入るのでパスを工夫する必要があります。
viteのプラグインでvite-plugin-static-copyというのがあるので別途導入して、設定ファイルであるeleventy.config.jsの先頭でインポートします。src/からのパスで管理する場合、
viteOptions: {
plugins: [
viteStaticCopy({
targets: [
{
src: "blog/img/**/*", // .11ty-vite内のパス
dest: "blog/img"
},
{ src: "assets/fonts/**/*", dest: "assets/fonts" },
]
})
],
// ...
}
と言う方法もありますし、ファイルとして認識させる場合は、
assetsInclude: ["**/*.pdf", "**/*.xml", "**/*.xsl"]
などとして書くこともできます。
これ(assetsInclude)は、アセットとして認識しろという宣言だけなのでHTMLから参照されていないとやはりコピーはされません。vite-plugin-static-copyは参照の有無に関係なく指定したファイルを強制的にコピーするものです。robots.txtのようにルート下においてあるだけのものはこちらでやる必要があると思います。あるいはpublic/に入れるかなどでしょうか。
上記サンプルコードでは、blog/img/**/*、つまりblog記事が入っているディレクトリにimg/ディレクトリもあり、記事で使用する画像はこのimg/に入れて管理するなどの方がわかりやすいと思います。blog/imgにまとめると言う感じの使い方をする記述です。
_skipCopy_

と書けます。これは(記事の)permalinkの設定がどう書いてあろうと、使用する画像は/blog/img/にあるので絶対パスで書くということです。src/blog/img/に画像を入れていくという事だけです。もちろん画像のファイル名を間違えてはいけません。
Viteはあくまで JS/CSS のバンドラーであり、それ以外は明示的に伝えない限り存在を知らないという前提で設計する必要があります。
どういうものかのまとめ
公式のViteプラグインは、Viteをフル活用するという設計ではなくあくまでも開発時に、より便利に表示させるものとしてViteを活用するという意味合いが強く、11tyがHTMLを生成し、Viteがjsやcss等を含めビルドで完了となっています。
これがもし、11tyがHTMLを生成し、Viteがjsやcss等を含めビルド、finalyとして静的アセットのコピーができるようにフックが用意されていれば技術的に11ty + Viteはもっと融合して連携できるようになるでしょう。
- 11tyはHTMLを作るもの
- ViteはJS/CSSを最適化するもの
として設計されていますので、public/を経由させる、あるいはvite-plugin-static-copyを使って補完するというのがプラグイン側の現在の答えとなっています。
11ty側の設定で守るべきこと
outputディレクトリは./プレフィックス付きで書く事が必要となります。
# skipCopy
dir: { output: "dist" } // これは壊れる
dir: { output: "./dist" } // 安全
dir: { output: "_site" } // デフォルトのまま使用が最も安全
これらの理由として、runBuild()内のコードでパスの比較が行われる所からもわかる部分です。
# skipCopy
if (!entry.outputPath.startsWith(this.directories.output)) {
throw new Error(`Unexpected output path ...`);
}
"dist"と"./dist/index.html"はstartWithで一致しないので、コマンドライン環境でクラッシュします(ローカルは偶然通ることもあるかも知れませんが)。
.11ty-vite を .gitignore と watchIgnores の除外に入れる
eleventyConfig.watchIgnores.add(".11ty-vite/**");
ブラグインのgetIgnoreDirectory()がこのパスを返していて、11tyのwatchIgnoreとし設定されることを前提にしています。ビルドエラー時にこのフォルダが残ることがあります。
- 過去にVite buildを試していた残留
- 何らかの異常終了で残ってしまった
- 将来的にVite buildを試す可能性がある
等で .11ty-vite/ が存在していると、11tyがそのフォルダを監視・処理しようとしてエラーになります。
Viteを表示のみに使用する場合は不要ともいえますが、置いておいて問題ではないのでお守り的に書いておくのが良いと思います。.gitignore に .11ty-vite を追加しておくのも同様の理由でセットでやっておくと安心です。
addPassthroughCopy と setServerPassthroughCopyBehavior
# skipCopy
// Vite の publicDir と対応させるため
eleventyConfig.setServerPassthroughCopyBehavior("copy"); // "passthrough"ではなく"copy"
eleventyConfig.addPassthroughCopy("public");
dev時にpassthrough(デフォルト)だとファイルがoutputにコピーされずViteのrootから見えなくなってしまいます。"copy"にするとoutputにコピーされてViteが正しく見つけられます。
これは上記のwatchIgnoresの件と同様でdev時に必要になるものです。ビルドは11tyがするという方法であれば不要です。
Vite側の設定で守ること
デフォルトでそうなっていますが、appType: "mpa"は変更しないようにすることが最重要設定です。
"spa"にすると単一エントリーを前提にした処理になり、複数ページの11tyサイトではルーティングが壊れます。input管理が複雑になるため、理由がない場合は"mpa"のままが一番無難です。
viteOptions.rootを自分で設定してはいけない
# skipCopy
// getServer() / runBuild() 内で自動設定される
viteOptions.root = this.directories.output; // dev時
viteOptions.root = tempFolderPath; // build時
viteOptionsにrootを自分で書いても、プラグイン側で上書きされるようになっていますので、書いても無意味であり混乱の元になります。
publicDir の設定
# skipCopy
viteOptions: {
publicDir: "public", // プロジェクトルートからの相対パス
}
ViteのpublicDirはプラグイン設定時点でのroot(プロジェクトルート)からの相対パスで解釈されます。ただしビルド時はrootが11ty-viteに変わるために注意が必要です。publicと言う名前をoutputディレクトリに使うと衝突が起こるため、outputは"_site"や"dist"を使って下さい。
resolve.alias の /node_modules エイリアス
# skipCopy
resolve: {
alias: {
"/node_modules": path.resolve(".", "node_modules"),
},
},
これにより、HTMLテンプレート内から/node_modules/some-lib/file.jsと言う絶対パスで直接インポートできます。Viteはデフォルトでは node_modules への直接パス参照を許可しないため、このエイリアスが必要です。
Tailwind cssは別プロセスが安定する
公式Viteプラグインのソースコードには Tailwindを意識したコードがどこにもありません。
build フローでのタイミング問題
- 11tyがビルド
- この時点でTailwindが未処理だとHTMLにClassはあるがCSSがないということになります
- outputを
.11ty-viteにリネームする - ViteがHTMLをバンドル処理
Tailwind を ViteのPostCSSプラグインとして組み込んだ場合
- TailwindのJITはHTMLを走査して必要なクラスを確定する
- Viteの
rootはoutput(HTMLが生成された後のフォルダ)なので、元のテンプレートファイル(src/)を参照できない contentのパスをsrc/**/*.njkなどと明示すると動作するが、build/devでrootが変わるためcontentの相対パスが壊れやすい
これらのことから、package.jsonでは、別プロセスとしてTailwindの出力CSSを11tyのaddPassthroughCopy対処に含め、Viteがそれをバンドルする流れにするのが最も良い方法であると考えられます。@importベースになるので、ViteのCSS処理と相性が良くなりますが、それでもcontentのパス問題は残ります。
別プロセス化するために
まず、並行してプロセスを起動させるためにconcurrentlyが必要になります。また別プロセスでTailwind cssを動作させるために@tailwindcss/cliが必要((package.jsonのコマンドで使用するため))になります。
npm install -D concurrently @tailwindcss/cli
そして、package.jsonは、11tyを--serve --incrementalで起動し、src/assets/css/main.cssをsrc/assets/css/output.cssに変更させます。スタイルシートの名称は自身の使用するものに合わせて下さい。
{
"scripts": {
"build:css": "tailwindcss -i ./src/assets/css/main.css -o ./src/assets/css/output.css",
"build": "npm run build:css && eleventy",
"dev": "concurrently \"tailwindcss -i ./src/assets/css/main.css -o ./src/assets/css/output.css --watch\" \"eleventy --serve --incremental\" "
}
}
| コマンド | 注意点・解説 |
|---|---|
build:css |
-i: inputのcssを -o: outputのcssに変更する。 名称が同じだと止まるので作業するcssをstyles.css、出力をstyle.cssのようにする。 Tailwindcssもこの時に組み込まれる |
build |
build:cssを動作させてからeleventyを起動して最終出力にする |
dev |
ローカルで作業するためのコマンド。 Tailwindcssでまず処理させてから変更を監視。その後11tyを起動して作業できるようにする |
バージョンの違いによって書くものが変わる
- Tailwindcss v3を使用している場合、別途
tailwind.config.jsが必要になります。 - v4を使用している場合
tailwind.config.jsは不要でスタイルシートにソースも記載することになります。
v4では、スタイルシート先頭にこのように書きます。
/* main.css */
@import "tailwindcss";
/* contentの代わり。@sourceでCSS内に直接書く */
@source "../src/**/*.njk";
@source "../src/**/*.html";
@source "../src/**/*.md";
v3を使用している場合は、まずスタイルシートには、
/* main.css */
/* @importではなく@tailwindディレクティブで読み込む */
@tailwind base;
@tailwind components;
@tailwind utilities;
そして別途、tailwind.config.jsが必要。プロジェクトフォルダ直下(eleventy.config.jsと同じ場所)に置いて次のように書きます
// v3 では tailwind.config.js 必須
export default {
content: [
"./src/**/*.njk",
"./src/**/*.html",
"./src/**/*.md",
],
// ...
};
という設定になります。
他のプラグインとの兼ね合い
eleventy-plugin-viteは必ず最後に addPlugin する
プラグインは afterBuild イベントと serverMiddleware を登録します。他プラグインが transform や addPassthroughCopy を行うより後に Vite が動く必要があるため、呼び出し順が重要になります。
# skipCopy
eleventyConfig.addPlugin(EleventyPluginNavigation);
eleventyConfig.addPlugin(EleventyPluginRss);
eleventyConfig.addPlugin(EleventyPluginSyntaxhighlight);
eleventyConfig.addPlugin(EleventyVitePlugin, { ... }); // 最後
こういう感じに最後に呼び出す。
eleventy-img との組み合わせ
eleventy-img が生成する画像はoutputディレクトリに書き出されます。Viteは .html 以外のアセットをassetsIncludeで明示しない限り処理対象外とするため、画像はViteのバンドル対象にならず、そのままoutputにコピーされます。
RSS プラグインの XML / sitemap の txt
# skipCopy
assetsInclude: ["**/*.xml", "**/*.txt"],
これを設定しないと Vite が XML/TXT ファイルをモジュールとして解釈しようとしてエラーになります。
推奨ディレクトリ構造
project/
├── src/ # 11ty の input
│ ├── _includes/
│ ├── _data/
│ ├── assets/
│ │ ├── css/main.css # CSSエントリー(Viteが処理)
│ │ └── js/main.js # JSエントリー(Viteが処理)
│ └── posts/
├── public/ # Vite の publicDir(そのままコピー)
│ └── favicon.ico
├── _site/ # 11ty output(Viteのroot)← "./dist"でなくこちら推奨
├── .11ty-vite/ # 一時フォルダ(.gitignoreに追加)
├── eleventy.config.js # ESM形式推奨
└── package.json
eleventy.config.js 例
// eleventy.config.js
import path from "path";
import EleventyVitePlugin from "@11ty/eleventy-plugin-vite";
export default function (eleventyConfig) {
// passthrough を実コピーにする(Vite の root から見えるようにするため)
eleventyConfig.setServerPassthroughCopyBehavior("copy");
eleventyConfig.addPassthroughCopy("public");
// Vite プラグインは最後に追加
eleventyConfig.addPlugin(EleventyVitePlugin, {
tempFolderName: ".11ty-vite",
serverOptions: {
domDiff: false, // Vite HMR と競合するため無効化推奨
},
viteOptions: {
publicDir: "public",
clearScreen: false,
appType: "mpa", // ← MPA必須。変えない
assetsInclude: ["**/*.xml", "**/*.txt"],
server: {
middlewareMode: true,
},
build: {
emptyOutDir: true,
manifest: true,
rollupOptions: {
output: {
assetFileNames: "assets/css/[name].[hash].css",
chunkFileNames: "assets/js/[name].[hash].js",
entryFileNames: "assets/js/[name].[hash].js",
},
},
},
resolve: {
alias: {
"/node_modules": path.resolve(".", "node_modules"),
},
},
},
});
return {
dir: {
input: "src",
output: "_site", // ← "./dist" でなく "_site" 推奨
includes: "_includes",
layouts: "_layouts",
data: "_data",
},
templateFormats: ["md", "njk", "html"],
htmlTemplateEngine: "njk",
};
}
デフォルトで設定されているもので省略できる項目をもう少しわかりやすいように。
import EleventyVitePlugin from "@11ty/eleventy-plugin-vite";
export default function (eleventyConfig) {
// 必須:実コピーにしないとViteのrootから見えない
eleventyConfig.setServerPassthroughCopyBehavior("copy");
eleventyConfig.addPassthroughCopy("public");
eleventyConfig.addPlugin(EleventyVitePlugin, {
// tempFolderName: ".11ty-vite" ← デフォルト、省略可
// appType: "mpa" ← デフォルト、省略可
// clearScreen: false ← デフォルト、省略可
// server.middlewareMode: true ← デフォルト、省略可
// build.emptyOutDir: true ← デフォルト、省略可
// resolve.alias["/node_modules"]← デフォルト、省略可
viteOptions: {
// 必須:rootがbuild時に変わるため明示が必要
publicDir: "public",
// RSSやsitemapを使うなら必要(デフォルト外)
assetsInclude: ["**/*.xml", "**/*.txt"],
build: {
// アセットにハッシュを付けるなら必要(デフォルトはfalse)
manifest: true,
rollupOptions: {
output: {
assetFileNames: "assets/css/[name].[hash].css",
chunkFileNames: "assets/js/[name].[hash].js",
entryFileNames: "assets/js/[name].[hash].js",
},
},
},
},
});
return {
dir: {
input: "src", // デフォルトは"."なので必須
// output: "_site" ← デフォルト、省略可
// includes: "_includes"← デフォルト、省略可
layouts: "_layouts", // デフォルトは"_includes"と共用なので分けるなら必須
// data: "_data" ← デフォルト、省略可
},
// デフォルトは"liquid"なのでnjkを使うなら必須
htmlTemplateEngine: "njk",
};
}
こんな感じにまとめられます。
更にViteをdev時のみに使用する設定
// import path from "path"; //resolve.alias["/node_modules"]が不要になればこれも
import EleventyVitePlugin from "@11ty/eleventy-plugin-vite";
export default function (eleventyConfig) {
// 必須:実コピーにしないとViteのrootから見えない
eleventyConfig.addPassthroughCopy("public");
const isServe = process.env.ELEVENTY_RUN_MODE === "serve";
if (isServe) {
eleventyConfig.watchIgnores.add(".11ty-vite/**"); //おまじない的な意味で
// Viteがミドルウェアとして動く
// "copy"にしないとViteのrootから静的ファイルが見えないので
// Viteが動作する際には 必須
eleventyConfig.setServerPassthroughCopyBehavior("copy");
eleventyConfig.addPlugin(EleventyVitePlugin, {
// tempFolderName: ".11ty-vite" ← デフォルト、省略可
// appType: "mpa" ← デフォルト、省略可
// clearScreen: false ← デフォルト、省略可
// server.middlewareMode: true ← デフォルト、省略可
// build.emptyOutDir: true ← デフォルト、省略可
// resolve.alias["/node_modules"]← デフォルト、省略可
serverOptions: {
domDiff: false, // Vite HMR と競合するため無効化推奨
},
viteOptions: {
// 必須:rootがbuild時に変わるため明示が必要
publicDir: "public",
// RSSやsitemapを使うなら必要(デフォルト外)
assetsInclude: ["**/*.xml", "**/*.xsl", "**/*.txt"],
build: {
// アセットにハッシュを付けるなら必要(デフォルトはfalse)
//manifest: true,
//rollupOptions: {
// output: {
// assetFileNames: "assets/css/[name].[hash].css",
// chunkFileNames: "assets/js/[name].[hash].js",
// entryFileNames: "assets/js/[name].[hash].js",
// },
//},
},
},
});
}
return {
dir: {
input: "src", // デフォルトは"."なので必須
// output: "_site" ← デフォルト、省略可
// includes: "_includes"← デフォルト、省略可
layouts: "_layouts", // デフォルトは"_includes"と共用なので分けるなら必須
// data: "_data" ← デフォルト、省略可
},
// デフォルトは"liquid"なのでnjkを使うなら必須
htmlTemplateEngine: "njk",
};
}
コメントを取り除くとこうなる
import EleventyVitePlugin from "@11ty/eleventy-plugin-vite";
export default function (eleventyConfig) {
eleventyConfig.addPassthroughCopy("public");
const isServe = process.env.ELEVENTY_RUN_MODE === "serve";
if (isServe) {
eleventyConfig.watchIgnores.add(".11ty-vite/**"); //おまじない的な意味で
eleventyConfig.setServerPassthroughCopyBehavior("copy");
eleventyConfig.addPlugin(EleventyVitePlugin, {
serverOptions: {
domDiff: false,
},
viteOptions: {
publicDir: "public",
assetsInclude: ["**/*.xml", "**/*.xsl", "**/*.txt"],
build: {},
},
});
}
return {
dir: {
input: "src",
layouts: "_layouts",
},
htmlTemplateEngine: "njk",
};
}
まとめ
11tyの公式プラグインViteの本質としては、11tyとViteを繋ぐためのアダプターであり、どちらかがもう一方を内包しているわけではないので、互いに知らない者同士を設定することになるため起点を正しく設定することが大切になります。
Tailwindの別プロセス化も、RSSのassetsIncludeも全てこのViteはoutputしか見えていないということから理解できるのではないかと思います。