Refreshing the Next.js App Router When Your Markdown Content Changes
Are you writing MDX content for a Next.js blog and want to see live reloads when the content changes? Here's how to do it.
The Solution
Install some dependencies:
npm i -D ws concurrently tsx
Create a watcher.ts
file in the root of your Next.js project.
import fs from 'fs'
import { WebSocketServer } from 'ws'
const CONTENT_FOLDER = 'content'
fs.watch(
CONTENT_FOLDER,
{ persistent: true, recursive: true },
async (eventType, fileName) => {
clients.forEach((ws) => {
// do any pre-processing you want to do here...
ws.send('refresh')
})
}
)
const wss = new WebSocketServer({ port: 3201 })
const clients = new Set<any>()
wss.on('connection', function connection(ws) {
clients.add(ws)
ws.on('error', console.error)
ws.on('close', () => clients.delete(ws))
})
Create a script in your package.json
that starts the watcher on dev
.
{
...,
"scripts": {
"dev": "concurrently \"next dev\" \"tsx ./watcher.ts\" --kill-others",
}
}
Create a component named AutoRefresh
.
'use client'
import { useRouter } from 'next/navigation'
import { useEffect } from 'react'
let AutoRefresh = ({ children }: { children: any }) => children
if (process.env.NODE_ENV === 'development') {
AutoRefresh = ({ children }) => {
const router = useRouter()
useEffect(() => {
const ws = new WebSocket('ws://localhost:3201')
ws.onmessage = (event) => {
if (event.data === 'refresh') {
router.refresh()
}
}
return () => {
ws.close()
}
}, [router])
return children
}
}
export default AutoRefresh
Wrap your root layout.tsx
in the AutoRefresh
component.
async function RootLayout({ children }: { children: any }) {
return (
<AutoRefresh>
<html>...</html>
</AutoRefresh>
)
}
export default RootLayout
The problem
Vercel provides excellent documentation of using Markdown / MDX together with Next.js (including with its new App Router). While it's possible to have .mdx
files map 1-1 to pages using the file-based routing system, this is impractical for most projects and especially for projects with many dozens or hundreds of content files. For these projects, the answer has been to use remote MDX via the Hashicorp library next-mdx-remote, where content files are created server-side upon first request.
This is a great solution, but it has one drawback: the Next.js App Router does not refresh when content in the folder changes. This is a problem for content creators who want to see their changes reflected in the browser without having to restart the server or manually refresh the page.
The solution used to be using a library named next-remote-watch
. However, this library only seems to work with the pages directory and does not work with the new Next.js app router.
Luckily, Dan Abramov found a great solution. The solution shared above is a slight adaptation that I used on the tldraw docs site.
How it works
When you run npm run dev
, you'll start a regular Next.js dev server and a websocket server. That server will use fs.watch
to observe changes in a folder where your content is stored.
Meanwhile, when the root layout mounts, a hook in the AutoRefresh
component will connect to the websocket server. Once it's connected, it will set a listener that calls router.refresh()
when it receives a refresh
message from the websocket server.
When the server detects a change anywhere in the content directory, the websocket server will send a refresh
message to its connected clients. This will trigger the listener in in the AutoRefresh
component and cause the Next.js App Router to refresh and display the freshly edited content.
Tweaks and modifications
You can do whatever you want inside of watcher.ts
before dispatching the refresh
event.
In our case, we respond to a change in the content directory by parsing every file in the content directory and stuffing it into a sqlite database with navigation links and so on—and only when that's done (maybe 12ms later) do we send off the refresh
event. However, because this is an asyncronous process, and because dev-mode react runs every hook, our watch callback wrapped in a debounce function to avoid trying to do the work twice in a row.
You can also dispatch the refresh
event whenever you want, such as on an interval or in response to some other listener if you're pulling data from a different source. The general idea is still using websockets to trigger a refresh of the Next.js App Router.
And of course you don't have to use these exact libraries or methods. For example, you can skip tsx
as a dependency and use plain JavaScript for your watcher script. If you've got a websocket server running and a listener in your root layout, you're good to go.
Did you like this article? Follow me on TwitterX or click here and share the link with a friend.