Adding GitHub as a Content Source in a Next.js Blog
@bge
If you're managing blog content in a Next.js site using local MDX files and a headless CMS like GraphCMS, you can take things a step further by sourcing posts directly from a private GitHub repo.
In this post, I’ll show you how to fetch .mdx
files from a private GitHub repository using the GitHub API and merge them with your existing content sources.
🧠 Why Add GitHub?
- Store posts in GitHub for remote editing or collaboration
- Use GitHub as a "lightweight CMS"
- Centralize content even if your site is deployed elsewhere (like Vercel)
🔐 Step 1: Create a GitHub Token
To access private repo content, you’ll need a Personal Access Token:
- Go to GitHub > Settings > Developer Settings > Tokens
- Click “Generate new token (classic)”
- Check the
repo
scope - Copy the token and store it in your
.env.local
:
1GITHUB_TOKEN=ghp_YourTokenHere
2GITHUB_REPO=yourusername/your-repo
3GITHUB_BRANCH=main
4
🔗 Step 2: Fetch GitHub Content in Next.js
Install dependencies:
1npm install axios gray-matter 2
Then, create a helper like lib/githubPosts.js
:
1import axios from 'axios'
2import matter from 'gray-matter'
3
4const headers = {
5 Authorization: `token ${process.env.GITHUB_TOKEN}`,
6 Accept: 'application/vnd.github.v3.raw',
7}
8
9export async function getGitHubPostsList() {
10 const res = await axios.get(
11 `https://api.github.com/repos/${process.env.GITHUB_REPO}/contents/posts?ref=${process.env.GITHUB_BRANCH}`,
12 { headers }
13 )
14
15 return res.data.filter(f => f.name.endsWith('.mdx'))
16}
17
18export async function getGitHubPostContent(filename) {
19 const rawUrl = `https://raw.githubusercontent.com/${process.env.GITHUB_REPO}/${process.env.GITHUB_BRANCH}/posts/${filename}`
20 const res = await axios.get(rawUrl, { headers })
21 return res.data
22}
23
🧩 Step 3: Merge GitHub Posts with Local and GraphCMS
In your post loader (e.g. getSortedPostsData()
), combine all sources:
1const githubFiles = await getGitHubPostsList() 2const githubPosts = await Promise.all( 3 githubFiles.map(async (file) => { 4 const content = await getGitHubPostContent(file.name) 5 const { data } = matter(content) 6 7 return { 8 id: file.name.replace('.mdx', ''), 9 source: 'github', 10 ...data, 11 } 12 }) 13) 14
Then merge:
1return [...localPosts, ...graphcmsPosts, ...githubPosts].sort(...) 2
📄 Step 4: Render the Post
For each post, determine the source and fetch accordingly:
1switch (source) { 2 case 'local': 3 return getLocalPostData(id) 4 case 'graphcms': 5 return getGraphCmsPostData(id) 6 case 'github': 7 return getGitHubPostData(id) 8} 9
✅ That’s It!
Now your blog can source content from:
- Local MDX files (e.g.
/posts
) - GraphCMS (via GraphQL)
- A private GitHub repo
It’s flexible, scalable, and keeps your content portable.
Got questions or want a starter template? Hit me up!
Back