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
- Part 1. : Redesigning My Website with the t3-stack
- Part 2 : Displaying Individual Articles
- Part 3 : Adding a table of contents to our blog articles
- Part 4 : Tidying Up the final product
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
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
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)
);
};
Let's breakdown what is happening in this specific function
repository(owner: <Your Github Username> , name: <Your Repository name>) {
issues(last: 200) {
nodes {
number
}
}
}
}
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]
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,
};
}
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;
};
with the corresponding type definition of githubPost
seen below
export type githubPost = {
title: string;
number: string;
createdAt: string;
body: string;
};
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,
};
}
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);
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,
};
}
Which generates the following output
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
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")],
};
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>
</>
);
}
which gives the following output as
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)