The engineering behind octotype: GitHub Issues as CMS, Next.js, and 8 times OpenAI saved me
This is the story of how I made this site, octotype.
First of all, being a lousy programmer as I am, I wanted to minimize damage and use a stack that maximizes the developer experience. For me, a good developer experience creates spaces for you to focus on fixing the business problem at hand. It's not about magic tools that do hundred of things. That's why the following stack fits the bill for me:
- GitHub Issues as CMS ๐. GitHub's Issue management system gives me all I need to create and manage content behind the scenes. I am very familiar with it, and it's easy to use, widely adopted, and has an out-of-the-box API with a good free tier. It's just perfect!
- Next.js with the Blog template โ๐ป. My new blog site is leaning heavily on this template, and I only added a few new types, components, and UI functionality to fetch data from GitHub through its API, such as issues (as posts), comments, and reactions. Of course, it's a statically generated site, but I have used Incremental Site Regeneration, so new issues created in GitHub Issues show up as blog posts after a very short while. It's just perfect and lightning-fast. And ISR even works with dynamic paths!
- Typescript ๐ป. I feel very comfortable programming with types. I think it forces you into a kind of healthy DDD thinking and way of programming.
- Vercel as frontend platform ๐. Nothing to add, you know I am in love with Vercel
- OpenAI ๐ค. Down below, I share 8 times ChatGPT saved my ass. I am still in absolute awe.
Wait, OpenAI? For what? Well, building the whole new site took me about two (2) days. Seriously. It may not sound like a lot to you, but it's a remarkable achievement for someone like me who does not code daily. I have used it extensively to customize the app created from the Next.js template, and ChatGPT helped me to write Tailwind, Typescript, and other Next.js code when I hit a wall. This was one of the main reasons I wanted to build my own site: I felt capable of doing something like this in a reasonable amount of time with this tool. Based on my comments about the developer experience above, I can say that ChatGPT provided me with an Augmented Developer Experience.
The frontend work
The octotype site is 100% pre-rendered, which gives us an optimal user experience with a super fast site. This means that the landing page, every user page, and every blog post are pre-rendered on the server using Next.js ISR with an interval of 120
seconds. The data in these pages are generated by fetching the content directly from the GitHub API.
Using Next.js, I managed to maintain simple yet dynamic URL routes like /:username/stories/:number
. Next.js provides a beautiful out-of-the-box feature to help implement this use case where you need to handle combinatorial paths, and it's called optional catch-all routes. Using this feature, I could have added a [[...slug]].tsx
file to rule them all, and that would have helped me catch routes such as:
- /
- /peibolsang
- /peibolsang/stories
- /peibolsang/stories/1
However, since optional catch-all routes are implemented in one TSX file, this would have forced me to implement different layouts on one page with a lot of conditional logic to render different large UI components for each layout. What did I do instead? I didn't know it until I googled it, but you can actually apply catch-all routes to folder names, too, as long as you include an index.tsx
file under that folder. In other words, [username].tsx
is the same as [username]/index.tsx
๐คท๐ปโโ๏ธ. This allowed me to create the following folder structure where I can pre-render every page using getStaticProps
and getStaticPaths
:
- pages/index.tsx
- pages/[username]/index.tsx
- pages/[username]/stories/[slug].tsx
This also gives me some future proofing, just in case I want to switch from pre-rendering using getStaticProps
to SSR using getServerSideProps
for some of the pages. Why would I want to do that? Keep reading ๐
Using GitHub's API
This MVP of octotype uses the GitHub API free tier, which has a rate limit of 5000
calls per hour. Why did I choose this? Let me put some use cases based on how the octotype site is designed (i.e., how the app calls the GitHub API in the getStaticProps
and getStaticPaths
functions):
- Scenario 1:
10
octotype users with5
posts each with a site regeneration every120 seconds
will fire about3900
API calls. This is still reasonable and within the API rate limits, and we can pre-render all blog posts in this scenario! - Scenario 2: An increment to
15
octotype users with5
posts each with a site regeneration every120 seconds
will trigger about5940
API calls, which is above the rate limit of5000
.
As you can see, the site's scalability is limited due to the API rate restriction, so how can we fix it? I wish I had this problem (๐), but I see different options:
-
We could increment the ISR revalidation from
120 seconds
to180 seconds
or beyond. This will trigger API calls less frequently, of course. That's cool and all that, but users would see a delay of 3 mins between the issue being created on GitHub and the blog post being published on the Web. -
For immediate rendering without delay, we could server-side render blog posts which, while detrimental to performance, is a safe option because it will reduce the number of API calls in an order of magnitude.
-
I am unsure whether creating a GitHub App or an OAuth App is a valid option since I'd prefer to stick to the GitHub API free tier. Why? I sought simplicity and wanted to avoid the user logging in with GitHub credentials to get a better API rate limit. For me forcing the user to log in with GitHub credentials just to read content was a deal breaker. The audience of every developer's blog shouldn't be restricted to other developers only!
As you can imagine, I hit the limit multiple times while building the site using example Issues. However, Next.js gives us a super handy way of adding local environment variables out of the box. To get a better API rate during development time, I just had to put my personal GitHub token in the .env.local
file and add a feature flag to use it if it was available (which is not the case on the server)
Finally, rendering the markdown of a GitHub issue as HTML was super easy too. The API itself returns a raw markdown, so you only need a small library like remark
to parse it and transform it into HTML. Something like this:
import { remark } from 'remark'
import html from 'remark-html'
export default async function markdownToHtml(markdown: string) {
const result = await remark().use(html).process(markdown)
return result.toString()
}
8 Examples of Augmented Developer Experience with OpenAI
As I mentioned earlier, I wouldn't have built this site without ChatGPT. Here are some very good examples as screenshots of the type of inputs I gave to the tool together with the provided answer. Did it work? Well, if you are reading this post through octotype's Web, yes, it is. Beautifully. BIG TIME
(Note: dont' even judge me ๐)
Example 1: Problem with async/await: I just copy-pasted a code snippet. Then the AI detected the problem and came up with a solution.
Example 2: I wanted to implement a simple tabs component: There you go, say no more
Example 3: I did ask the same for a carousel component: It didn't work at first because of a bug. The AI fixed it:
Example 4: An easy one about pure UI aspects using TailwindCSS.
Example 5: A basic Typescript question: Because of stupid me ๐
Example 6: Using Next.js getStaticProps
: These are my programming skills!
Example 7: The typewriter effect on the landing page: This is how I did it
Example 8: More on the typewriter effect.