HomePostsGithub Cms

Using GitHub as a CMS

Published Apr 18, 2025
Updated Apr 18, 2025
2 minutes read
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.