What is smoores.dev?
Nov. 30, 2024
Recently, as I decided to reinvest in the contents of my personal blog, I also decided to rebuild it. Previously, it had been built as a very simple Flask app, with posts written in Jinja-templated HTML, and relied on the PyPI package Frozen-Flask to export the contents to static assets for deployment. This setup always required a bit more effort than I liked, but I really enjoyed the freedom that came with writing my posts as essentially plain HTML. So I gave my blogging system another shot, this time relying on React Server Components and Next.js to build out a system that was hopefully more fun to work on and easier to maintain.
Requirements
Like any project, this one had some requirements, and it’s worth laying them out explicitly:
The blog contents need to be static. I’m comfortable with adding some nice developer experience improvements for myself, but not at the expense of the reader. At the end of the day, I want to be serving static HTML, CSS, and JavaScript assets.
The blog contents should be written with HTML, or at least HTML-like markup. Look, I’m a web developer. I like web technologies like HTML. I’m very comfortable with them, and I enjoy using them to express myself. And it’s tough to match the expressiveness of HTML for semantic text content!
It should work exactly the same without JavaScript. Unless some future post adds client-side interaction for some reason (not out of the question, but not super likely), there’s no reason at all that readers should need JavaScript to get the entire experience of reading the blog.
Boilerplate should be at least mostly automated. This includes things like the site header and footer, which are the same on every page, but also includes adding posts to the home page and the Atom syndication feed. Speaking of which...
There must be a syndication feed (ideally Atom 1.0). I don’t know how many folks still use RSS readers to follow blogs, and I certainly don’t know if a single person has ever subscribed to my RSS/Atom feed, but I just love the philosophy of these syndication systems too much to miss an opportunity to participate in them.
Luckily, this is a pretty short list of requirements. Even more luckily, Next.js 15 had just been released, with stable support for React Server Components. Server Components allow developers to use React to author components that render to HTML entirely on the server, without any client-side logic whatsoever. This is distinct from earlier versions of Next.js (< 14), which supported Server-Side Rendering (SSR), which rendered a first pass of the component tree on the server, and then “hydrated” the entire React tree on the client with client-side JavaScript. This hydration step occured even if there are no interactive components!
The Basics
Next.js’ “App Router” implementation actually directly enables the first four of our requirements. We can use a layout component to implement the header and footer, and then implement each post as its own React component.
Here’s our layout:
Each page on the website will be wrapped in the markup above.
Speaking of which, we’d like to generate a page, with it’s own URL, for each post. I decided to implement this with a single page component that imports an array of post metadata and components and renders a static page for each one:
To power this page generation, we need to define this posts array that gets imported here:
Each post exports a Server Component with the contents of the post, and a metadata
object with the metadata for that post. Here’s what that looks like for this post:
This is pretty great. I can write each post in its own file, focusing just on the contents, and using JSX to mark them up. For the most part, I have standard reusable components, styled with Tailwind CSS, that most of my posts use for markup. I’ve also recently begun using Code Hike’s Bright package for a Server Component-based approach to code highlighting, which is what’s powering all of the code blocks in this post, for example.
Atom Feeds
This just leaves one requirement: a syndication feed. The standard protocol for web syndication is RSS (Really Simple Syndication), which is an XML-based protocol that allows clients to subscribe to a feed provider (like this blog) and check for updates at some interval. I ended up choosing an alternative protocol — Atom — for this, because it’s generally easier to work with and addresses some of the issues with RSS, but the same principles apply.
This means that I need to export a route that returns content other than HTML (specifically, it needs to return application/atom+xml
). I’m doing this with a Next.js route handler, which is just a function that takes a Request
and returns a Response
.
Atom feeds can (and, in my opinion, should) contain the entire contents of each entry, when possible. In order to accomplish this with this Next.js app, we’ll need to use react-dom/server
to programmatically render our post components to markup strings, and then HTML escape them to add them to our entries. Specifically, we’d like to import renderToReadableStream
, which will allow us to asynchronously render each component and wait for any Suspense boundaries to resolve (this is important, because Bright’s <Code />
components use Suspense!):
Unfortunately, Next.js doesn’t like this. It works in development, but when we try to run a static production build, it throws an error:
I guess I understand what they’re going for here (in nearly any other context, this would be indicative that something was wrong), but it’s a little frustrating that this error isn’t configurable in any way. There is a way around it, though — we can import the module dynamically, within the handler function:
This is goofier than I’d like, but this is a small personal project, with code that ought to be touched only very infrequently, and only by me. So I decided that was good enough.
Now it’s time to actually set up these atom entries. We can import the same posts
array that we use for generating the pages, and for each post, call renderToReadableStream
to retrieve the serialized markup. That method actually returns an extension of ReadableStream specifically for use cases like this, where we need to statically render out the entire contents of the component for search engine crawlers or feed readers, rather than streaming the results incrementally, like we would for an interactive browser client. We can use it like this:
Now we can just use the escapeHTML
function from the escape-html
npm package (since Atom content represented as HTML must be escaped), and generate our XML feed representation. There are a few (somewhat stale) projects on npm for generating Atom feeds, but generating XML is much more straightforward than parsing it, and in the end I decided to just read the Atom 1.0 spec myself and use some simple string interpolation:
Deployment
That’s it for the code. It feels about as simple as it can get without hand-writing static files, which would be too error-prone for my liking. Now we just need to deploy it!
I run the server that hosts https://smoores.dev/ myself, and often when I’m deploying applications to it, I set up a CI workflow in GitLab that builds a docker container or some other artifacts, and configure my server to run that container, periodically checking for updates. That can make a lot of sense for full-stack applications like Storyteller, which have their own databases, dynamic backends, and interactive clients. But it absolutely felt like overkill for this website, especially given that I made sure it could be rendered out to static files!
Instead, I configured Next.js to export the entire site to static files during the build:
And then I removed the build/
directory from the project’s gitignore file. That means that a publish is now as simple as running deno task build
on my development machine and committing the results. To update the deployment, I landed on the tried-and-true “ssh in and pull the latest”, which instantly updates the results, since I’m just serving the static files from my Caddy reverse proxy.
Conclusion
So far, I’m having a blast working on this blog. The freedom that comes from writing in JSX is a joy, and I genuinely find React development fun and energizing.
If you want to check out the rest of the code that powers this blog in a bit more depth, it’s open source and hosted on GitLab.