How this site is built

I rebuild my personal site often enough that I wanted to write down how it actually works — partly so I remember the moving parts, and partly because I find other people’s colophons genuinely useful. This is that writeup: the theme, the build, the deploy, and a few details I’m fond of.

The short version: it’s a static site generated by Jekyll, built in CI, and served as prebuilt files. Nothing runs on a server at request time, which keeps it fast and effectively free to host.

The theme

The site is built on al-folio, an open-source Jekyll theme aimed at academics. I picked it because it gets the boring things right out of the box: a clean publications list backed by BibTeX, dark mode, responsive layout, and good typography. The blog you’re reading uses the theme’s native features rather than anything bespoke — the table of contents beside this post, the syntax highlighting below, and the math further down are all things the theme already supports once you turn them on.

I keep my changes small and on top of the theme. The philosophy is to lean on upstream and only override what I actually need, so pulling in theme updates stays cheap.

How it builds

The source lives on GitHub. Every push triggers a GitHub Actions workflow that installs Ruby and Node, runs the Jekyll build, and processes images. The output is a directory of plain static files — HTML, CSS, JS, and images — with no runtime dependencies.

A trimmed-down version of the build step looks like this:

- name: Build site
  run: |
    bundle exec jekyll build
  env:
    JEKYLL_ENV: production

Building in CI rather than on my laptop means the deployed site is always reproducible from a clean environment, and I never have to think about whether my local Ruby happens to match.

How it deploys

The built files are deployed as a prebuilt static site to Vercel. Because CI has already produced the final output, Vercel doesn’t need to run the Jekyll build itself — it just serves what it’s handed. I tell it not to re-run a build with a small project config:

{
  "buildCommand": null,
  "outputDirectory": "_site"
}

That _site directory is Jekyll’s default output, and buildCommand: null is the bit that says “don’t try to build, just serve.” Treating the deploy as a dumb file upload keeps it predictable.

The full deploy flow, step by step 1. I push a commit (or merge a PR) to the source repository on GitHub. 2. GitHub Actions checks out the code, installs Ruby + Node, and restores caches. 3. Jekyll builds the site into _site/, generating responsive images along the way. 4. The workflow hands the prebuilt _site/ to Vercel. 5. Vercel skips its own build (per the config above) and publishes the files to its CDN. 6. The apex domain serves the new version; the redirects below make sure everyone lands there. The nice property is that step 3 is the only place the site is ever actually built. Everything after it just moves bytes around.

Domains and redirects

The canonical home is the apex domain, ryankim.me. Both www.ryankim.me and the old *.github.io page redirect to it, so there’s a single canonical URL no matter how someone arrives. Consolidating on one origin keeps links, analytics, and search indexing clean — there’s exactly one address that “counts.”


Writing posts

I write posts in Markdown, but I don’t always do it by hand-editing files. The site ships a hidden Sveltia CMS at /admin — a small, Git-backed editor that commits straight to the repository. It’s there mostly for convenience: I can fix a typo or draft something from a browser, and the commit it produces kicks off the exact same build-and-deploy pipeline as if I’d pushed from my terminal. There’s no separate database and no second source of truth; the Git history is the content.

Images

Images are the easiest way to make a static site slow, so the build does the work ahead of time. Every raster image is converted to WebP and emitted at three widths — 480, 800, and 1400 pixels — which the theme wires up as a srcset so the browser downloads whichever size fits the viewport.

Concretely, the generated width set is

\[W = \lbrace 480,\ 800,\ 1400 \rbrace \text{ pixels}\]

and for a layout slot of CSS width \(w\), the browser picks the smallest \(W_i \in W\) with \(W_i \ge w \cdot \text{dpr}\), where \(\text{dpr}\) is the device pixel ratio. On a phone that usually means the 480- or 800-pixel variant instead of the full-resolution original — a real bandwidth win for a thumbnail that’s only ever shown small.1

An illustrative photo demonstrating the responsive figure include.
A figure rendered through the al-folio figure include — the same WebP/srcset pipeline applies to every image on the site.

A few smaller details

There’s a first-load-only intro loader: the little animation you see the very first time you visit in a session, and not on subsequent navigations within it. It sets a tone without getting in the way once you’re actually reading.

For discoverability, each page emits Open Graph tags for link previews, JSON-LD structured data so search engines understand what a page is, and there’s a generated sitemap.xml. None of this is glamorous, but it’s the difference between a link that unfurls nicely in a message and one that shows a bare URL.

Why static

People sometimes ask why I don’t reach for a heavier framework. The honest answer is that this is a blog and a portfolio, not an app. A static build has effectively constant lookup cost per request — call it \(O(1)\) at the edge, since every page is already a file on a CDN — and there’s no server to patch, no database to back up, and nothing to wake up cold. The whole system is small enough that I can hold it in my head, which is exactly what I want from infrastructure I maintain in my spare time.

If you’re building something similar, my one piece of advice is to keep the moving parts boring. The interesting work should be in what you write, not in the plumbing that publishes it.


  1. WebP typically gives meaningfully smaller files than JPEG or PNG at comparable visual quality, which is why it’s worth converting at build time rather than shipping the originals. The exact savings depend on the image, so I treat it as “usually smaller” rather than quoting a fixed number.