Using GitHub as a CMS
In this post we'll create a blog using Next.js, MDX, and GitHub.
Background
While I can list reasons I think GitHub works well as a CMS, PostHog already said it better than me in this blog post. So instead, let's dive into the code and see how it works.
GitHub API
By default, GitHub exposes a beautiful API to fetch content from a repository that does not require any sort of authorization. That said, if you find yourself fetching content frequently, you may want to authorize your requests to avoid being ratelimited.
To fetch your repository content, follow this API structure:
https://api.github.com/repos/{GITHUB_USER}/{REPO_NAME}?ref=main
To create a succinct and effective CMS using GitHub, we'll want to sructure our repo correctly on GitHub. You can take liberty in how you define this. Here is how I did it:
dir
├── blog
│ ├── ipsum.md
│ └── lorem.md
└── post
├── one.md
└── two.md
This repo has two subdirectories blog
and post
, each with their own markdown files.
Next.js
Create a dynamic route in your Next.js app for the blog. It should resemble app/blog/[slug]/page.tsx
. In this page.tsx
file is where we will fetch the content for a blog based on the GitHub API.
The code for a page will look something like this:
🚧 Some things to be aware of:
- GitHub returns the content base64 encoded.
- I'm using React markdown w/ Tailwind typography, but you could technically store content as HTML if you prefer.
import ReactMarkdown from "react-markdown"
export const revalidate = 3600
export async function generateStaticParams() {
const response = await fetch(
"https://api.github.com/repos/{GITHUB_USER}/{REPO_NAME}/contents/blog?ref=main"
)
const files = await response.json()
return files.map((file) => ({
slug: file.name.split(".")[0],
}))
}
export default async function Page({
params,
}: {
params: Promise<{ slug: string }>
}) {
const { slug } = await params
const blog = await fetch(
`https://api.github.com/repos/{GITHUB_USER}/{REPO_NAME}/contents/blog/${slug}.md?ref=main`
)
if (!blog.ok) {
return notFound()
}
const blogContent = await blog.json()
// Decode the base64 content
const decodedContent = Buffer.from(blogContent.content, "base64").toString(
"utf-8"
)
return (
<main className="p-20">
<article className="prose">
<ReactMarkdown>{decodedContent}</ReactMarkdown>
</article>
</main>
)
}
Notice how we're generating static params using the URL for the directory, and then we're fetching each individual blog post using the slug being passed through as page props.
We can now do the same thing for app/posts/[slug]/page.tsx
for our other directory of content in our CMS.
Wrapping Up
That's it! You can tweak the ISR settings and styles, but that's how you can use GitHub as a CMS.
I haven't yet played around with loading images and using custom components, but I may add to this in the future.