Markdown と同じディレクトリの画像を表示できるようにした
目次
はじめに
Markdown の中に挿入した画像をこのブログに表示できるようにした。
やりたいこと
このブログでは次のようにディレクトリごとに Markdown ファイルと画像を管理したい。
posts
├── 20230101
│ ├── 20230101.md
│ ├── img1.png
│ └── img2.png
└── 20231231
├── 20231231.md
└── img3.png
記事の ID(現在は作成日にしている)のディレクトリに [ID].md
と画像ファイルを配置すれば管理が楽になると考えた。
課題
Next.js では、画像などのテキストファイルをルートの public
ディレクトリに配置する。例えば /public/images/img.png
に配置された画像を表示するには次のようになる。
<img src="/images/img.png" />
しかし Markdown ファイルを基準とした相対パスで次のように画像を指定したい。
![alt](img.png)
つまり、Markdown と同じディレクトリに画像ファイルを配置しただけでは画像を表示させることができない。なんらかの方法で画像を public
ディレクトリに移動させ、出力された HTML から正しく参照させる必要がある。
解決策
2つの役割
- 画像ファイルを
public
ディレクトリにコピーする - Markdown をパースする段階で、 img タグの src 属性を書き換える
を持つ関数コンポーネント MdImg
を作成した。また、
import styles from "./md-img.module.scss";
import path from "path";
import fs from "fs";
export default function MdImg({ dir, src, alt }: {
dir: string,
src: string,
alt: string
}) {
// C:\Users\sasak\workspace\html\next-app\src\assets\posts\test\nyancat.png
const srcPath = path.join(process.cwd(), dir, src);
// test
const parentDir = dir.split(path.sep).pop() || "unknown";
// C:\Users\sasak\workspace\html\next-app\public\images\test\nyancat.png
const dstPath = path.join(process.cwd(), "public", "images", parentDir, src);
fs.mkdirSync(path.join(process.cwd(), "public", "images", parentDir), { recursive: true });
fs.copyFileSync(srcPath, dstPath);
// /images/test/nyancat.png
const srcUrl = path.join("/", "images", parentDir, src).replaceAll("\\", "/");
return (
<div className={styles.container}>
<img
src={srcUrl}
alt={alt}
className={styles.mdImg}
/>
</div>
);
}
MdImg
関数の props は次のようになっている。
dir
: Markdown の存在するディレクトリsrc
: img タグの src 属性(Markdown ファイルからの相対パス)alt
: img タグの alt 属性
public
ディレクトリにコピーする
1. 画像ファイルを 少し強引な方法ではあるが、Markdown ファイルと同じディレクトリにある画像ファイルを public
ディレクトリにコピーしてしまえば参照することが可能になる。
コピーでは次の変数を利用する。
srcPath
: コピー元の画像ファイルのフルパスparentDir
: Markdown ファイルと画像ファイルが格納されているディレクトリ名dstPath
: コピー先のフルパス
/public/images/
にそのまま画像を配置してしまうと、ファイル名の被りを許容できなくなる。そのため Markdown ファイルがあるディレクトリ名を噛ませて /public/images/${parentDir}
として記事ごとに画像を分ける。
2. Markdown を parse する段階で、 img タグの src 属性を書き換える
今までの処理ではパースの段階では Markdown が存在するディレクトリを使った処理が行えなかった。そのため、引数に dir
を加え、関数内でパースを行い、返り値はパース後のデータとなるような関数 process
に変更した。dir
にはルートから Markdown ファイルのあるディレクトリまでのパスを文字列で渡す。(toc
は目次を表示するかどうかのオプション)
export function process(content: string, dir: string, toc: boolean)
また、rehype-react
のオプションを使い、img タグを先ほど作成した MdImg
コンポーネントに置き換える。
.use(rehypeReact, {
...prod,
components: {
...
img: ({ src, alt }: { src: string, alt: string }) => {
return (
<MdImg
dir={dir}
src={src}
alt={alt}
/>
)
},
},
} as any)
これで Markdown の画像を表示することができるようになった。
問題点
画像の表示が可能になったものの、いくつかの問題点が生まれた。
複数ファイルでのパース効率
以前までは unified を使ったパーサを宣言しておき、1 つのパーサで複数の Markdown を処理していた。しかし、親ディレクトリ名という Markdown ファイルごとに固有の情報を使用する必要があり、処理 1 回ごとに毎回パーサを再構築する必要がある。そのため複数ファイルでの処理の効率は悪化してしまった。
画像ファイルの複製によるプロジェクト容量の増大
posts
ディレクトリに存在している画像を public
ディレクトリにコピーして利用しているため、同じ画像が2枚存在することになる。よって容量効率は悪化する。
モジュールシステムを使って自動化できないのだろうか。
画像の動的インポート
Next.js では次のようにモジュールシステムを使って静的に画像をインポートできる。
import img from "./img.png";
function Component() {
return (
<img src={img} />
);
}
この方法でインポートされた画像は、ビルド時に自動的にエクスポートされる。
また、次のように動的にインポートすることも可能である。
function Component() {
return (
<img src={require("./img.png")} />
);
}
この方法を使い、require
の引数切り替えることで動的に画像をインポートすることができるのではと考えた。
function Component({ src }: { src: string }) {
return (
<img src={require(src)} />
);
}
しかし、require
の引数は静的に決定している必要があり、変数で切り替えるといったことはできなかった。結果として、コピーするという方法を取ることとなった。
まとめ
最善とは行かなかったものの、無事に Markdown の画像を表示することができた。まだ他にも実装していないものがあるので色々試してみたい。