jme Blog

Markdown と同じディレクトリの画像を表示できるようにした

画像の表示を実装したときのメモ
2023-12-11
Next.js
Markdown
Tech
目次

はじめに

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つの役割

  1. 画像ファイルを public ディレクトリにコピーする
  2. 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 属性

1. 画像ファイルを public ディレクトリにコピーする

少し強引な方法ではあるが、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 の画像を表示することができた。まだ他にも実装していないものがあるので色々試してみたい。