DEV Community

ivanleomk
ivanleomk

Posted on • Updated on

Part 2 : Using Github Issues as a CMS

Quick Introduction

This is a series of blog posts which detail how I'm setting up my website with the t3 stack. If you haven't read the previous articles, I suggest looking at

Current State

Previously, we successfully managed to get our website to render out the list of issues we had configured on Github using the t3-stack. If you haven't read it yet, you can read it here

However, now we want to do something a bit different, we want to be able to render these articles and dynamically generate pages that correspond to each of these articles.

For reference here's how the main page looks like now

image

It's not super inspiring but we've got it working such that we only list articles that are labelled as published. This is a big step for us.

Today in this article, we have two main goals

  • Using Static Generation to create individual pages for each article
  • Rendering out the content we've written in markdown as HTML in the page

Creating individual pages

Installing New Libraries

In this walkthrough, we will be using the following libraries

  • unified
  • gray-matter
  • remark-parse
  • remark-rehype
  • rehype-stringify

So let's go ahead and install the libraries with npm as seen below.

npm i - s unified gray-matter remark-parse remark-rehype rehype-stringify
Enter fullscreen mode Exit fullscreen mode

Walkthrough

In NextJS, we are able to create dynamic routes, which are essentially routes that are generated. Let's start by creating a file src/pages/blog/[issueId].tsx. By using the [...] syntax, we are able to indicate to NextJS that we want to create a dynamic route.

In this case, what this means is that the following routes

  • /blog/2
  • /blog/thisisarandomstring

will all match and be rendered by the [issueId] file.

How do we then know what are valid routes and invalid routes if any random combination that matches the specification is able to be rendered?

The answer to that is a function called getStaticPaths. This tells NextJS what are the paths that we wish to support and in turn it will do the matching on the backend for us. If we attempt to access an invalid path, it will simply return a 404 Page. Let's first begin by writing out a quick function which allows us to get a list of all the individual Issue Ids.

export const getPostIds: () => Promise<number[]> = async () => {
  const { repository } = await graphqlWithAuth(
    `query getPostIds {
      repository(owner: <Your Github Username> , name: <Your Repository name>) {
        issues(last: 200) {
            nodes {
                number
            }
        }
      }
  }
`,
    {}
  );

  // Quick Type Definition here
  return repository.issues.nodes.map(
    (issue: { number: string }) => parseInt(issue.number)
  );
};
Enter fullscreen mode Exit fullscreen mode

Let's breakdown what is happening in this specific function

 repository(owner: <Your Github Username> , name: <Your Repository name>) {
        issues(last: 200) {
            nodes {
                number
            }
        }
      }
  }
Enter fullscreen mode Exit fullscreen mode

Recall that Github Issues can be identified by a unique number. This is a property which exists on the Issue Object in the Github API .

We extract out the list of all existing github issue numbers and transform it into an array. Since I currently have 2 published articles, this gives

[1,2]
Enter fullscreen mode Exit fullscreen mode

We specify that we want the issues from which is owned by and that allows us to get the correct issues we are looking for.

Now that we have a list of post Ids, we can now generate our list of paths using getStaticPaths as seen below

export async function getStaticPaths() {
  const posts = await getPostIds();
  const paths = posts.map((issueId) => `/blog/${issueId}`);

  return {
    paths,
    fallback: false,
  };
}
Enter fullscreen mode Exit fullscreen mode

Now that we have indicated a list of paths, we need to now tell NextJS how to render each specific page, that's where getStaticProps comes in handy.

Note : I previously made the mistake of writing getStaticProps using my NextJS APIs. This caused the production build to fail multiple times. Please don't make the mistake I did and just write a wrapper function around the code that handles the server communication. It will save you years of pain.

But before we use getStaticProps, we need to first write a small function that will allow us to obtain the metadata and raw markdown for our code given an Issue Id. This can be done by making another API call to the GraphQL API as seen below

export const getPostByIssueId: (
  issueId: number
) => Promise<githubPost> = async (issueId) => {
  const { repository } = await graphqlWithAuth(
    `query getPost($number: Int!){
      repository(owner: <Your Github Username>, name: <Your Github Repository>) {
        issue(number: $number) {
            title
            number
            createdAt
            body
        }
      }
  }
`,
    {
      number: Number(issueId),
    }
  );

  return repository.issue;
};
Enter fullscreen mode Exit fullscreen mode

with the corresponding type definition of githubPost seen below

export type githubPost = {
  title: string;
  number: string;
  createdAt: string;
  body: string;
};
Enter fullscreen mode Exit fullscreen mode

We can then utilise this in our main component as seen below

export async function getStaticProps({ params }: BlogPostParams) {
  const { issueId } = params;
  const post = await getPostByIssueId(parseInt(issueId));
  const { title, body } = post;
  const { content: parsedBody } = matter(body);

  const content = await unified()
    .use(remarkParse)
    .use(remarkRehype, { allowDangerousHtml: true })
    .use(rehypeStringify, { allowDangerousHtml: true })
    .process(parsedBody);

  return {
    props: {
      content: String(content),
    },
  };
}

export async function getStaticPaths() {
  const posts = await getPostIds();
  const paths = posts.map((issueId) => `/blog/${issueId}`);

  return {
    paths,
    fallback: false,
  };
}
Enter fullscreen mode Exit fullscreen mode

Where the following chunk basically takes a raw markdown text and rehydrates it in order to generate HTML

  const { title, body } = post;
  const { content: parsedBody } = matter(body);

  const content = await unified()
    .use(remarkParse)
    .use(remarkRehype, { allowDangerousHtml: true })
    .use(rehypeStringify, { allowDangerousHtml: true })
    .process(parsedBody);

Enter fullscreen mode Exit fullscreen mode

We can now put together a simple initial component which gives us the following output

import { getPostByIssueId, getPostIds } from "../../utils/github";
import matter from "gray-matter";

import { unified } from "unified";
import remarkParse from "remark-parse";
import remarkRehype from "remark-rehype";
import rehypeStringify from "rehype-stringify";
import Link from "next/link";

type BlogPostProps = {
  title: string;
  content: string;
};

type BlogPostParams = {
  params: { issueId: string };
};

export default function BlogPost({ title, content }: BlogPostProps) {
  return (
    <>
      <h1>{title}</h1>
      <div
        dangerouslySetInnerHTML={{
          __html: content,
        }}
      />
    </>
  );
}

export async function getStaticProps({ params }: BlogPostParams) {
  const { issueId } = params;
  const post = await getPostByIssueId(parseInt(issueId));
  const { title, body } = post;
  const { content: parsedBody } = matter(body);

  const content = await unified()
    .use(remarkParse)
    .use(remarkRehype, { allowDangerousHtml: true })
    .use(rehypeStringify, { allowDangerousHtml: true })
    .process(parsedBody);

  return {
    props: {
      content: String(content),
    },
  };
}

export async function getStaticPaths() {
  const posts = await getPostIds();
  const paths = posts.map((issueId) => `/blog/${issueId}`);

  return {
    paths,
    fallback: false,
  };
}

Enter fullscreen mode Exit fullscreen mode

Which generates the following output
Screenshot 2022-07-28 at 4 12 10 PM

So now that we've got the content, let's work on styling the specific page now.

Styling the page

For this specific portion, we'll be using Tailwind to style our article section. If you didn't install TailwindCSS, please follow the instructions here and get it set up first.

Tailwind provides a useful utility class called prose which applies automatic styles for us into the rendered HTML so that we don't need to generate custom rules.

But before we can plug it in, we need to install a new plugin called @tailwindcss/typography. You can read about it here. Execute the command below to install the plugin

npm install -D @tailwindcss/typography
Enter fullscreen mode Exit fullscreen mode

and then modify your tailwind.config.js file accordingly as seen below

/** @type {import('tailwindcss').Config} */
module.exports = {
  content: ["./src/**/*.{js,ts,jsx,tsx}"],
  theme: {
    extend: {},
  },
  plugins: [require("@tailwindcss/typography")],
};
Enter fullscreen mode Exit fullscreen mode

Notice here that we've added in @tailwindcss/typography in the plugins section. We can then rewrite our existing component as

export default function BlogPost({ title, content }: BlogPostProps) {
  return (
    <>
      <Link
        href={{
          pathname: "/",
        }}
      >
        <p>  Go Back Home</p>
      </Link>
   // We Added This!
      <div className="prose mx-auto">
        <h1>{title}</h1>
        <div
          dangerouslySetInnerHTML={{
            __html: content,
          }}
        />
      </div>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

which gives the following output as

Screenshot 2022-07-28 at 4 16 30 PM

Voila! Now we have basic styling for any article that we write in our github issues as long as we tag it with the published label. In the next article, we'll look at adding a table of contents to our article which is automatically generated based on our content.

Top comments (0)