🔧
Quartz4でZenn互換の画像パス(/images)を使えるようにする改造
はじめに
Quartz4は素晴らしい静的サイトジェネレーターですが、デフォルトではcontentフォルダ内の画像のみを処理します。一方、Zennでは/imagesフォルダに画像を配置するのが標準的です。
両方のプラットフォームでコンテンツを管理する場合、画像パスの整合性を保つことが重要になります。今回は、Quartz4でZenn互換の画像パス(/images/画像名.png)を使えるように改造した話です。
問題:Quartz4のデフォルトの画像管理
Quartz4のデフォルト設定では、以下のような制限があります:
- 画像は
contentフォルダ内に配置する必要がある -
のような相対パスでの参照が前提 - プロジェクトルートの
/imagesフォルダは処理されない
これにより、Zennで使用している形式の画像参照が機能せず、コンテンツの移植時に大量のパス修正が必要になってしまいます。
解決方法:assets.tsプラグインの改造
Quartz4のquartz/plugins/emitters/assets.tsファイルを改造して、プロジェクトルートの/imagesフォルダからも画像を読み込めるようにしました。
主な変更点
1. filesToCopy関数の拡張
const filesToCopy = async (argv: Argv, cfg: QuartzConfig) => {
// 既存:contentフォルダの非MDファイルを取得
const contentFiles = await glob("**", argv.directory, ["**/*.md", ...cfg.configuration.ignorePatterns])
// 新規:rootのimagesフォルダからもファイルを取得
const rootImagesPath = path.resolve(path.dirname(argv.directory), "images")
let imagesFiles: FilePath[] = []
if (fs.existsSync(rootImagesPath)) {
const imagesGlob = await glob("**", rootImagesPath as FilePath, cfg.configuration.ignorePatterns)
// パス区切り文字を統一してimages/プレフィックスを付与
imagesFiles = imagesGlob.map(fp => path.posix.join("images", fp) as FilePath)
}
return [...contentFiles, ...imagesFiles]
}
2. copyFile関数の改良
const copyFile = async (argv: Argv, fp: FilePath) => {
let src: FilePath
// Windows/Linux対応:パス区切り文字を正規化
const normalizedFp = fp.replace(/\\/g, '/')
// images/で始まるファイルはrootディレクトリから取得
if (normalizedFp.startsWith("images/")) {
const rootDir = path.dirname(argv.directory)
src = joinSegments(rootDir, fp) as FilePath
} else {
// 従来通りcontentディレクトリから取得
src = joinSegments(argv.directory, fp) as FilePath
}
const name = slugifyFilePath(fp)
const dest = joinSegments(argv.output, name) as FilePath
// ディレクトリ作成とファイルコピー
const dir = path.dirname(dest) as FilePath
await fs.promises.mkdir(dir, { recursive: true })
await fs.promises.copyFile(src, dest)
return dest
}
3. partialEmit関数でのホットリロード対応
async *partialEmit(ctx, _content, _resources, changeEvents) {
for (const changeEvent of changeEvents) {
const ext = path.extname(changeEvent.path)
if (ext === ".md") continue
// rootのimagesフォルダの変更を検出
const rootImagesPath = path.resolve(path.dirname(ctx.argv.directory), "images")
const isRootImageFile = changeEvent.path.startsWith(rootImagesPath)
if (changeEvent.type === "add" || changeEvent.type === "change") {
if (isRootImageFile) {
// 絶対パスを相対パスに変換
const relativePath = path.relative(path.dirname(ctx.argv.directory), changeEvent.path) as FilePath
yield copyFile(ctx.argv, relativePath)
} else {
yield copyFile(ctx.argv, changeEvent.path)
}
} else if (changeEvent.type === "delete") {
// 削除処理も同様に対応
let filePath: FilePath
if (isRootImageFile) {
filePath = path.relative(path.dirname(ctx.argv.directory), changeEvent.path) as FilePath
} else {
filePath = changeEvent.path
}
const name = slugifyFilePath(filePath)
const dest = joinSegments(ctx.argv.output, name) as FilePath
await fs.promises.unlink(dest)
}
}
}
技術的なポイント
Windows/Linux対応
パス区切り文字の違い(Windows: \, Linux: /)に対応するため、path.posix.join()と正規化処理を使用しました。
ホットリロード対応
開発時のnpx quartz syncでも、rootの/imagesフォルダの変更を即座に反映できるようにpartialEmit関数も対応しました。
既存機能の保持
従来のcontentフォルダ内の画像処理は維持しつつ、新機能を追加する形で実装しました。
結果
改造後は以下のようになりました:
- ✅
/images/sample.png形式でZenn互換の画像参照が可能 - ✅
content/images/sample.png形式の従来の方法も継続利用可能 - ✅ ホットリロード(
npx quartz sync)でも正常動作 - ✅ Windows/Linux両環境で動作確認済み
まとめ
この改造により、Quartz4でZennと同じ画像パス構造を使用できるようになりました。これで:
- コンテンツの移植が簡単: Zennの記事をそのままQuartz4で使用可能
-
管理の一元化: 画像を一箇所(
/images)で管理 - 開発効率向上: パス修正の手間が不要
同様の課題を抱えている方の参考になれば幸いです。
Discussion