CS (in JS)

✨ A static site generator built with fastify js

Building a blog has been my favorite way of playing with new tech since forever. I'm not constraint by anything, and I can make stuff the way I like.

It's an excellent playground for experimentation, and even though there's a great choice in terms of static site generators, I chose to build my own, and I'm going to walk you through the concepts involved in the process.

A perfect 100 score from lightouse for this blog

The rule of least power

I like to think about tools in terms of cost: anything I add on top of what's available natively will add some overhead; if I can avoid it, I will. The least powerful tool that can get the job done is most probably the right one.

Inception

The blog generator reads markdown files, parses this data to HTML, and saves it to the appropriate location:

A typical post looks like:

---
title: A title
date: 05/04/2021
---

# This is a title

This a paragraph

```js
const whatIsThis = 'some code';
```

Pages are built using the handlebars template system, which allows to create HTML dynamically.

const variables = { posts: [{ title: 'Post title', formattedDate: Date.now() }] };
const html = handlebars.render('postList.hbs', variables);
  <ul class="postList">
    {{# each posts}}
    <li>[{{formattedDate}}] <a href="{{url}}">{{title}}</a></li>
    {{/each}}
  </ul>

A list of all posts is saved as index.html in the public root folder, and then all posts are saved one by one with the file structure: post-slug/index.html A GitHub action will run the build command every time I push to main and publish a new version of my blog to GitHub pages that is served using my custom domain.

index.html
├── my-first-post/
│   └── index.html
├── my-second-post/
│   └── index.html
└── my-latest-blog-post/
    └── index.html

Adding a web server for development

This process works pretty well, but it scales poorly. Every time I'm writing a blog post, I would have to build the entire blog to check the changes. This could happen a hundred times as I'm writing a new article, highly inefficient!

That's why I added a fastify web server in development to help me build things on the fly. I don't have to build the entire blog every time: changes are just a page refresh away now!

Creating dynamic HTML pages with a web server is relatively easy: that's what web servers are for! I can use the same logic I had before to transform markdown to HTML and then pass it to my routes, point-of-view can render html from handlebars templates, so I can reuse those too!

fastify.register(require('point-of-view'), {});

fastify.get('/', (request, reply) => {
  reply.view('list.hbs', { posts: postsCache, ...viewGlobalOptions });
});

Even reusing some logic, though, I wasn't satisfied with repeating the same process for development and production in two different ways; there must be a more efficient way.

It turns out there is. For a project like this, I can leverage fastify.inject, a utility function that helps you make fake HTTP calls to the server. It's intended to be used to test applications, but it works pretty well for our use case: I call every blog post to let fastify do all the heavy lifting, and then I save the HTML response to its appropriate file and location.

The fastify web server is used to navigate the blog in development and to generate static HTML at build time!

const urls = await getAllUrls();
for (const url of urls) {
  // here's where the magic happens.
  // inject will get a response without any network calls
  const response = await app.inject({ method: 'GET', url: `/${url}` });

  await createPost(`${url}/index.html`, response.body);
}

The fastify web server is used to navigate the blog in development and to generate static HTML at build time!

Nice to haves

Now that the process is working end to end, I can add some candies: fast refresh is first!

Again, we use native tools and leverage how they work. I want to automatically refresh the page every time I make some changes on the backend, and I know already that every change will restart the server already: WebSockets is perfect for this!

When the server restarts, it will close the connection, and the client will try to reconnect; when it does, it will simply refresh the page to show the new changes.

function connect(shouldReload = false) {
  const ws = new WebSocket('ws://localhost:3000/ws');

  // on open is called as soon as a connection happens with the server
  ws.onopen = () => shouldReload && location.reload();

  // on close is called when the server disconnects (because it's restarting)
  ws.onclose = () => setTimeout(() => connect(true), 1000);
}

What we learned

Building a static site generator consists of transforming data from an arbitrary format to HTML and save this HTML to disk ready to be served to a web browser.

For a better developer experience, we use a web server in development: watch mode reloads the server when files change on disk, removing the burden of manually perform an action to see changes reflected in the browser.
We can leverage this fact by listening to this event through a WebSockets connection and trigger a page reload with javascript.

The development process can be automated through services like GitHub actions, executing commands every time the code base is updated.

[You can read the source code for this blog on GitHub!]