Using GitHub Issues as a Simple Static CMS

Why use a complex CMS when you can use GitHub Issues as a lightweight blogging engine!

This site uses GitHub Issues as a lightweight blogging CMS. Why? Because it's one of the simplest methods of creating markdown files and keeping track of all my posts. The GitHub issues editor contains drag-and-drop image upload, markdown previews, and numerous text editing tools.

The setup to start using issues as the post source is very straightforward. Each Issue becomes a blog post, labels become the extra tags or categories, and any images added to the issue are copied into the site at build time. If you are using a public repo, you could simply let GitHub host the images.


  • Issue = a post
  • Drafts = labels.filter(l => l.name === "draft")
  • Labels = tags/categories
  • Images = drag-and-drop hosted by GitHub or copied into the site at build time
  • Netlify = auto-deployment

Fetching the First Issue (the Post)

import "dotenv/config";

import { endpoint } from "@octokit/endpoint";

async function fetchPosts() {
  const { url, ...options } = endpoint("GET /repos/:owner/:repo/issues", {
    owner: "reyemtm",
    repo: "webjrnl",
    headers: {
      authorization: `token ${process.env.GH_TOKEN}`,
    },
  });
  const res = await fetch(url, options);
  if (!res.ok) {
    throw new Error(`GitHub API request failed: ${res.status} ${res.statusText}`);
  }
  const data = await res.json();
  return data;
}

Converting the Issue into a Post

import { marked } from "marked";

async function createSimpleGitHubFeed() {
  try {
    const issues = await fetchPosts();
    const PROD = process.env.NODE_ENV === "production";
    const feed = issues
      .filter((e) => PROD ? !e.labels?.find((l) => l.name === "draft") : true)
      .map((data) => {
        const tags = data?.labels?.map((l) => l.name) ?? [];
        const postTypes = ["post", "draft"];
        const type = tags?.length && postTypes.includes(tags[0])
          ? tags[0]
          : tags[0]
            ? tags[0]
            : "post";
        const fullContent = marked.parse(data.body);

        return {
          type: type,
          tags: tags.filter(t => t !== type),
          title: data.title,
          content: fullContent,
          date: new Date(data.created_at),
          url: `/post/${data.title.trim().toLowerCase().replace(/\s/g, "-")}`,
        };
      });

    writeFileSync("src/feeds/github.json", JSON.stringify(feed, 0, 2));
    return feed;
  } catch (error) {
    console.error(error);
    return [];
  }
}

While I do not currently have comments on this blog, it would be trivial to enable. For simplicity you would want your repo to be public. Then it is just a matter of adding a link to the post issue in your comments section, and rendering the comments on your website.

Fetching Comments for That Post

async function fetchIssueComments(issueNumber) {
  const { url, ...options } = endpoint("GET /repos/:owner/:repo/issues/:issue_number/comments", {
    owner: "reyemtm",
    repo: "webjrnl",
    issue_number: issueNumber,
    headers: {
      authorization: `token ${process.env.GH_TOKEN_II}`,
    },
  });
  console.log(`Fetching comments for issue #${issueNumber} from:`, url);
  const res = await fetch(url, options);
  if (!res.ok) {
    throw new Error(`GitHub API request for comments failed: ${res.status} ${res.statusText}`);
  }
  const data = await res.json();
  return data;
}