【Next.js】Markdownの変更を検出してリロードする

この記事はなに?

最近、Vue2で作っていたポートフォリオを捨てて、Next.jsで作り直しました。
ついでにmarkdornで記事をかけるようにしてブログもホストし始めたのですが、「mdを保存したら反映されてほしい.....」って思うことないですか?僕はあります。

ということで、mdを監視して変更があったらそれを反映するようにします。

環境

パッケージなど

Next.js側

  • Next.js ... 13.5.6
  • npm-run-all ... 4.1.5

監視ツール側

  • chokidar ... 3.5.3
  • socket.io ... 4.7.2
  • ts-node ... 10.9.1
  • typescript ... 5.2.2

フォルダ構成

ざっくり、以下のようなフォルダ構成とします。だいぶ端折っていますが、root直下はNext.jsの構造を持っていて、tool以下に監視ツールを入れると思ってください。
なお、 [slug].tsxはブログのメインページと解釈してください

root
├─ next.config.js
├─ package.json
├─ .....[slug].tsx
├─ articles
│    ├─ aaaaaa.md
│    └─ bbbbb.md
└─┬ tools
  ├─ package.json
  └─ watch
     └─ article.ts
  

ファイル監視ツールを作成する

tools/watch/article.tsは、おおよそ以下の実装になります。

tools/watch/article.ts
import { Server }  from "socket.io";
import chokidar from 'chokidar';

// process.cwd()はtoolsを指す
const watcher = chokidar.watch(`${process.cwd()}/../articles/**/*`);
const io = new Server(11654, {
  cors: {
    origin: "http://127.0.0.1:14617",
    allowedHeaders: ["*"],
    credentials: true,
  }
});


watcher.on('change', () => {
  io.emit('reload');
});

やっていることは簡単で、chokidarを使用してarticlesフォルダを監視して、変更があった際にはWebSocketにイベントを流し込みます。
今回は、Next.jsの開発サーバを http://127.0.0.1:14617 に見に行っているので、CORSを通してあげます。めんどくさいですね。

変更を受け取って再描画する

次に、各ページでイベントを受け取れるようにします。
ひとまず実装を先に見てください。

[slug].tsx
"use client"
import { useRouter } from "next/navigation";


import { ArticleDetail } from "@/interface/article";
import React, { useEffect } from "react";
import { env } from "process";
import { AppRouterInstance } from "next/dist/shared/lib/app-router-context.shared-runtime";

declare global {
    interface Window {
      io: any;
    }
  }

export const BlogMainContent: React.FC<{ article: ArticleDetail }> = (params) => {
    const router = useRouter();

    useEffect(() => {
        let ignore = false;

        if (env.NODE_ENV !== "production") {
            // 本番環境ではない場合、chokidarでファイル監視をしておいてmdが変更されたらWebSocketが飛んでくる
            new Promise((resolve, reject) => {
                const script = document.createElement('script');
                script.integrity = "sha384-c79GN5VsunZvi+Q/WObgk2in0CbZsHnjEqvFxC5DxHn9lTfNce2WW6h2pH6u/kF+"
                script.crossOrigin = "anonymous"
                script.src = "https://cdn.socket.io/4.6.0/socket.io.min.js";
                document.body.append(script);

                script.onerror = () => {
                    reject()
                }

                script.onload = () => {
                    const socket = (window.io as any).connect("http://127.0.0.1:11654", {
                        reconnection: true,
                        reconnectionDelay: 1000,
                        reconnectionDelayMax: 5000,
                        reconnectionAttempts: 99999,
                        crossOriginIsolated: false,
                    });
                    resolve(socket)
                }
            }).then((socket: any) => {
                if(ignore) return
                socket.on('reload', () => {
                    router?.refresh()
                });
                
            }).catch(() => {
                console.log('Listen event failed...');
            })
            return () => {
                ignore = true;
            }
        }
    }, []);
    return (
      <>
       { /* コンテンツ */ }
      </>
    )
}

production環境でない場合、socket.ioを取り込んで監視するようにしています。それだけです。

監視ツールと開発サーバを起動する

ツール側の設定

ツール側のpackage.jsonで、以下のコマンドを登録しておきます。

tools/package.json
{
  "scripts": {
    "watch:article": "ts-node watch/article.ts"
  },
}

Next.js側の設定

Next.js側でも同じように設定しておきます。

package.json
{
 "scripts": {
   "watch:article": "pnpm --prefix ./tools run watch:article",
   "next:dev": "next dev",
   "dev": "run-p watch:article next:dev",
   "next:build": "next build",
   "next:start": "next start",
   "next:lint": "next lint"
 },
}

これで、mdの変更を検出して反映されるようになるはずです。
欲をいえば一覧画面も更新させたかったですが、サーバーコンポーネントのままでいたかったので、今回はやりませんでした。

ちなみにちなみに、本番ビルドをまだ試していませんm(_ _)m

最後に

ちなみに Next.jsというかReact.js歴3日です。
2年くらい触ってるVueより体験が良い気がしてきた。

© 2019 - 2023 ここあ