改めて vite による ssg に取り組んでみた。 遂にシンプルな vite ssg を捻り出すことに成功した。

理念として、

であり、生成物は static な HTML でいいのです。 なんとなく vite が核心であり、他はなるべく少なくしたいという気持ちがあった。

vite の server / client の区別がついてなかった

vite の plugin にデバッガをアタッチしたくて 方法を探していたのだけど、 ようやく発見。 これにより、やっと vite のサーバー側の一歩を踏み出すことができた。 要するに vite コマンドではなくて、自前の js から launch すればできる。

// server.ts
import { createServer } from "vite";
const viteServer = await createServer();
await viteServer.listen();
viteServer.printUrls();

さらに全体を ts-nodeesm で始動させたい。

node --nolazy --import \"data:text/javascript,import { register } from 'node:module'; import { pathToFileURL } from 'node:url'; register('ts-node/esm', pathToFileURL('./'));\" index.ts"

これで vscode のデバッガーをアタッチできるので、手に負える見通しができた。

でも express も入れたくない

vite がサーバー能力あるからそれでいい。

https://ja.vitejs.dev/guide/ssr

に書いてある内容を vite の plugin の中に入れることでできる。 この手法は、 minista などが実践している。

https://github.com/qrac/minista/blob/main/packages/minista/src/cli/develop.ts

export default function pluginDevelop(): Plugin {
  return {
    name: "mydev-vite-plugin",

    configureServer: (vite: ViteDevServer) => {
      return () => {
        // https://ja.vitejs.dev/guide/ssr
        vite.middlewares.use(
          async (req: http.IncomingMessage, res: http.ServerResponse, next) => {
            // SSR
          },
        );
      };
    },
  };
}

シンプル SSR で React が動くようにする

@vitejs/plugin-react なしでシンプルなものを作った。

hyderate も最初は要らない。 あとで必要になったら足そう。

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Vite + React</title>
    <!--app-head-->
  </head>
  <body>
    <div id="root"><!--app-html--></div>
    <script type="module" src="/src/entry-client.jsx"></script>
    <!-- 👆 この行を削除した -->
  </body>
</html>

React の Router 要らんかった

hyderate しないならば Router は無用だった。 あると複雑さが跳ね上がる。async とか。 無ければシンプルで、非同期も自然に記述できる。 remark の await を仕込むのに頭をひねる必要もない。

// url でswitch して html 文字列を返すだけ
async function(url: string): {html: string}
{
  switch(url)
  {
    case "/" => "<html>root</html>";
    case "/hoge.html" => "<html>hoge/html>";
  }
}

import.meta.glob

基本的に import.meta.glob で目的は達成できるのだけど、 ライブラリーに任せると markdown が ReactComponent 化するのが 早すぎて逆に難しくなるという状況だった。

この段階では frontmatter にアクセスしたい。 自作して import.meta.glob は frontmatter のコレションを返すようにした。 すべてのページで、すべての frontmatter にアクセスできるので ナビゲーションの実装も簡単。

prerender もimport.meta.glob で OK

vite.ssrLoadModule 内で import.meta.glob して順に HTML 化して ファイルに出力すればできた。 Reactssr-manifest.json は作れなかったのだけど、使わなかったのでよし。

:::note manifest は import.meta.glob でファイル名の一覧を出力ればいよさそう。 :::

// prerender.ts
import path from "node:path";
import url from "node:url";
const __dirname = path.dirname(url.fileURLToPath(import.meta.url));
const dist = path.resolve(__dirname, "dist");

import { createServer } from "vite";
const vite = await createServer();
const { generate } = await vite.ssrLoadModule("/src/entry-ssg.ts");
generate(dist);
vite.close(); // vite は listen せずに終了する

remark & rehype を手作業で組込み

たしかに mdast => dom の方が mdast => hast => dom より簡単かもしれない。

あと mdx ももういいかなと。 mdx は、 markdown だと ok な記述がシンタックスエラーになってしまうのがわりとつらい。 html タグ風の記述や、 import / export などの特定の英単語がプログラム要素として 誤認されてしまう。

全体的に easy から simple に倒したのだが、 simple の要求する練度の高さがわりと険しいのであった。