This Website I

This Website II: Build

In Part I, we set up the site with a couple of bonus features on top of plain HTML, most notably fancy footnotes
Like this.
that expand in place when clicked.

Careful readers will remember that this was possible with plain HTML and CSS, but required a fair bit of it. Here's the HTML for footnote 1 above:
    <label for="fn-1-toggle">
        <sup>1</sup>
    </label>
    <input type="checkbox" hidden="" id="fn-1-toggle" />
    <div class="fn-content">
        Like this.
    </div>
Not the most pleasant writing experience, to say the least.
As another example, the angle brackets < and > are special characters in HTML---so to display the code above, the HTML would look like this.
    &lt;label for="fn-1-toggle"&gt;
        &lt;sup&gt;1&lt;/sup&gt;
    &lt;/label&gt; &lt;input type="checkbox" hidden="" id="fn-1-toggle" /&gt;
    &lt;div class="fn-content"&gt;
        Like this.
    &lt;/div&gt;


A much more pleasant way to write would be <Footnote>Like this.</Footnote>. And maybe do away with some of the HTML faffing.
And maybe write code like `<div>`, instead of <code>&lt;div&gt;</code>.
Luckily, this is all possible, with the power of MDX! MDX is markdown + JSX, which means we can write in markdown rather than HTML, and also write custom components like <Footnote> and drop them right in.

A Shorter and Worse Intro to Client-Side Frameworks

Of course, this opens up a whole can of worms. We're writing a website, so it needs to be HTML eventually, and our custom components need to be included, and if we want to write import then it becomes an "ESM module", which means it needs a "bundler", and---suddenly the project is ballooning out of proportion, all just because we didn't want to write <div> quite so many times.

The impression that I've gotten following this rabbit hole is that there are many complicated and annoying things that people want to do with websites, and as a result there are many complicated
and, some would say, annoying
"frameworks" to make things less annoying. But if you're just trying to write a static site, you don't need any of them!
In fact, it took me a while to get my bearings here. There was a version of this site that was a single page app with a bunch of React states and contexts managing everything, bundled with Vite. Eventually I found it was going to be a pain to make routing work the way I wanted, which was when I finally started to think that there must be a way to just make HTML files. Even then, it took some searching through existing tools
There's a Vite plugin vite-plugin-ssr that can do this! How do I set it up? Oh wait, it's now its own framework. Let's see, can I use it together with Vite, or do I have to migrate? Hmmm
before it occurred to me that I could do it myself.
If you want to start out with something else and end up with HTML, you need
  1. a compiler.
That's it!

Static Site Generator From Scratch
Well, from esbuild.
And a bit of React.

Of course, you do have to find the right compiler for your setup. Since I want to write MDX with custom JSX components, a good tool for the job is esbuild with the @mdx-js/esbuild plugin. That whole part of the process looks like this:
npm install esbuild @mdx-js/esbuild react react-dom
    import esbuild from 'esbuild'
    import mdx from '@mdx-js/esbuild'

    await esbuild.build({
        entryPoints: ['pages/*.mdx'],
        outdir: 'out',
        bundle: true,
        format: 'esm',
        platform: 'node',
        plugins: [mdx()],
    })
This tells esbuild to go through all the MDX files in pages/, compile them (along with their imports, etc.), and put the result in out/.

Since we've eschewed all those fancy frameworks, we still have some more work to do---what this step spits out isn't a finished website. For each MDX page, we now have a Javascript file that produces that page's content. What remains is to convert it to HTML, wrap it in the trappings of an HTML webpage, and assemble them into a full site.

But the point is that this is easy, and doing it by hand gives us a lot of flexibility. To convert the compiled Javascript for pages/${slug}.mdx to HTML, we have a handy function provided by React:
    import { renderToString } from 'react-dom/server'

    const { default: Content } = await import(`./out/${slug}.js`)
    html = renderToString(Content())
This produces HTML, but it still doesn't have the full page structure of <html>, <head>, <body>, etc. For our purposes, this boilerplate will be the same every time, so we can just write it as a string and insert the rendered html string in the appropriate place. Now that we have a fully-formed page, we can write it to our output directory (say, dist/).

And that's the whole process---just compile & render & layout each page, copy static assets (pictures, scripts, stylesheets) over to dist/, throw that all those steps together in build.js, and we've got our own static site generator!
To pretend we're fancy, let's also add
    "type": "module",
    "scripts": { "build": "node build.js" }
to package.json. The first line warns esbuild that we're going to use big words like import, and the second tells npm what to do when we say npm run build.
Full setup on GitHub somewhere around this commit.


Well, that's not the whole process. Since we're writing it ourselves, we can do whatever else we want! Old HTML files from the previous version of this site? No need to rewrite them in MDX, just copy them through to dist/. Posts in a series with their own navigation? Just a quick JSX component. Whatever new features come to mind writing the next one? You bet!

Appendix: GitHub Pages

For some reason I find the documentation surrounding GitHub pages rather mystifying. In the end I've discovered that you can indeed tell it to "just build my shit and serve it", but it took a while to find the correct incantations.
For lack of anything more enlightening to say, I'll simply repeat them here. In .github/workflows/build-and-deploy.yml I have inscribed the following:
    name: Build and Deploy

    on:
        push:
            branches:
                - main
    permissions:
        pages: write
        id-token: write
        contents: read

    jobs:
        build:
            runs-on: ubuntu-latest
            steps:
                - name: Checkout
                uses: actions/checkout@v4

                - name: Build static files
                id: build
                run: |
                    npm ci
                    npm run build

                - name: Upload static files as artifact
                id: deployment
                uses: actions/upload-pages-artifact@v3
                with:
                    path: dist/

        deploy:
            environment:
                name: github-pages
                url: ${{ steps.deployment.outputs.page_url }}
            runs-on: ubuntu-latest
            needs: build
            steps:
                - name: Deploy to GitHub Pages
                id: deployment
                uses: actions/deploy-pages@v4
For this particular action, the only variable is where the built files end up, which is the line path: dist/.