CAUTION:
This post is archived. The setup still works as an idea, but keeping the content in a separate repository added enough friction that I sometimes avoided writing altogether. I eventually moved the MDX back beside the site.
First, the reason I ended up with this setup at all.
I wanted a better way to manage what I wrote. I had already tried Medium (please don't) and LinkedIn, but neither felt like a place where I wanted my work to live. That leads us to a quick tangent.
The problem I kept having with blogging platforms was one of incentives. They had to serve thousands of writers, every topic imaginable, and the company behind them. The writing itself stopped feeling like the center of the product.
Medium put some user-written posts behind a paywall, and LinkedIn was still a social network, a weird one, and a Microsoft one.
At the same time, I was giving my website a more-than-necessary facelift, so I sat down and listed my options.
Framework
Content management
With those choices made, the next problem was connecting them.
I had already decided on the tools I wanted to use, but I still needed to figure out how to make them work together, and that's where Contentlayer came in.
Contentlayer turned files into typed data and integrated neatly with Next.js, but its built-in sources did not match the Git-backed setup I wanted.

I wanted Git and MDX together, so the built-in list did not quite get me there.

Contentlayer supported custom content sources, including a file-based one. I could clone the repository, then let Contentlayer read the result as local files.
NOTE:
The next section only covers the Git sync. Contentlayer's own docs explain the rest of the setup.
IMPORTANT:
The code below is a simplified version of what the site used at the time. The complete implementation is linked below.
The code is here and can be used as a custom syncFiles function on the makeSource call, starting from the basics:
import { execFile } from "node:child_process";
import { existsSync } from "node:fs";
import { promisify } from "node:util";
const run = promisify(execFile);
const sync = async (dir: string) => run("git", ["pull"], { cwd: dir });
const clone = async (dir: string) => run("git", ["clone", SOURCE, dir]);
promisifyconverts Node's callback-basedexecFilefunction into one that can be awaited. Passing arguments separately also avoids building a shell command from paths and URLs.
And now for the actual syncFiles function:
const syncContentFromGit = async (dir: string) => {
if (existsSync(dir)) await sync(dir);
else await clone(dir);
return () => console.log("\nSyncing cancelled!");
};That is most of the custom behavior. The last step is passing the function to Contentlayer.
export default makeSource({
syncFiles: syncContentFromGit,
// ...
});At the time, I liked how much freedom the setup gave me. The posts lived independently, while Contentlayer made them available as data for anything from a word-frequency page to an ill-advised model trained to cover my time off.
Every edit needed a commit and a push, and the content repository could change without triggering a deployment of the site.
The setup worked, but it made small edits feel like releases. That friction is why the MDX eventually moved back beside the site.
latest commit : 3 additions 2 deletions