Creating your own React static site generator

Gatsby turned out to be too much. But React makes it easy to create your own static site generator.

2023-01-12

Metadata
Creating your own React static site generator
Gatsby turned out to be too much. But React makes it easy to create your own static site generator.
./static-site.webp
2023-01-12
this-site
reactjavascript

A while back I rewrote my website using Gatsby which resulted in having to put the website content in the same GIT repository as the Gatsby code. Thus I had to be on a laptop to both write and publish the website. More recently, I find myself doing a lot of my writing on my iPad in the Obsidian app. Inspired by a post by James Long I wanted a way to both write and publish my website from the Obsidian app, regardless of if I was on my laptop or my iPad.

Obsidian has a growing API for plugin developers, and I've written a few myself, so I figured this should be doable. Only to quickly stall on the idea when I realized, not only was I 2 major versions behind on Gatsby, bundling Gatsby into a single file, as required by Obsidian plugins, was not going to be possible.

But this turned out to be a good thing. It helped me realize that my website probably didn't need a tool as sophisticated as Gatsby. I really just need a way of turning Markdown to HTML, and embedding it into a bunch of static HTML pages. Luckily this is pretty straight forward with React.

At a high level there are 4 steps involved.

  1. Gather all your content.
  2. Use react to render components to strings.
  3. Embed the resulting html into an HTML template.
  4. Write the html to a file.

Gather all your content

Obsidian makes step 1 pretty easy, including rendering markdown to HTML. But even if you just have a bunch of files on disk, or they are in a headless CMS, you just need to gather it all first, to then pass to React.

React renderToString

I have a React component that represents each page type on my website. Because I've gathered all the data for my website in step one, I can loop over all the files representing "posts", and render them into a React component and get a string back using react-dom/server.

import React from 'react'
import { renderToString } from 'react-dom/server'
import { BlogPostComponent } from './BlogPostComponent'

const allPosts = await gatherAllPosts()

for (const post of allPosts) {
	const postHtml = renderToString(<BlogPostComponent post={post} />)
}

Embed the post HTML into an HTML template

It's likely possible to use React to render the entirety of the HTML document (besides the doctype?), but I'm only rendering the body. So I then inject that HTML into a template string with the rest of the HTML document. Expanding on the above:

...

const pageTemplate = (html) => /* html*/`
	 <!DOCTYPE>
	 <html>
		 <head>
			<!-- all the things -->
		 </head>
		 <body>
		   ${html}
		 </body
	 </html>
`

for (const post of allPosts) {
	const postHtml = renderToString(<BlogPostComponent post={post} />)
	const htmlDoc = pageTemplate(postHtml)
}

Write to a file

Then finally write the html to a file. I'm using the path of the file that I captured while gathering data in step 1 to write to new directory, but with a matching structure. So if the file was blog/some-blog-post/index.md, I will write the file to an output directory with the path blog/some-blog-post/index.html.

import { promises as fs } from 'fs'
import path from 'path'

...

for (const post of allPosts) {
	const postHtml = renderToString(<BlogPostComponent post={post} />)
	const htmlDoc = pageTemplate(postHtml)
	await fs.writeFile(path.join(OUTPUT_PATH, post.dir, post.fileName, 'html'))
}

Of course, there was a lot more to getting my publishing workflow to function how I wanted it to (static assets, css bundling, image compression, etc...) but this illustrates the fundamentals to what it took to write a React static site generator. Which enables me to both write and publish my blog write from Obsidian.