Craig Glennie

Rewriting my blog with Astro JS

I recently decided to rewrite this blog. I had been using NextJS with an MDX plugin (the plugin always felt kinda janky), but I never really liked that it wasn’t serving static HTML. I learned about Astro, which is a new static site builder supporting partial hydration (aka “islands of interactivity”). I wanted partial hydration because I have some interactive elements on the site, and I preferred to keep MDX because it’s nice to be able to write those posts in Markdown and just drop my React components in (and also I wanted React because I like it, and Typescript because I can’t go back)

It took a little bit of fiddling aroun to get things setup right - as expected, it’s a new framework. Ultimately I decided not to use one of their templates/themes (these are pretty hit-and-miss at the moment) and start from scratch. Fortunately, Astro already supported everything I wanted to use out of the box, so it was mostly just learning the “Astro way”… and then deviating from it.

Frontmatter and types

The main deviation is that I do as little as possible in .astro files, and jump out to my usual .tsx as quickly as possible. I found the IDE support for Typescript was just better with a traditional Typescript file than what I got with the .astro hybrid files. So every .astro file has a corresponding .tsx file where most of the actual code is.

For example I load my posts in the Component Script block of my index.astro file

---
import { Index } from "./Index.tsx";
let allPosts = (await Astro.glob("./posts/**/*.md"))
---

And pass them straight into my <Index /> component in the Component Template section

<Index posts={allPosts} />

Frontmatter support is built-in. And I like my type hints, so I created a schema in zod, and step one is piping the frontmatter through it. After that I have a fully-typed frontmatter object to use. My schema looks like this:

export const postSchema = z.object({
  url: z.string(),
  frontmatter: z
    .object({
      layout: z.string(),
      title: z.string(),
      publishedAt: dateSchema,
      publish: z.boolean(),
      setup: z.string().optional(),
      metaDescription: PROD ? z.string() : z.string().optional(),
    })
    .strict(),
});

Which is extracted from a frontmatter that looks like this

---
layout: ../../../layouts/BaseLayout.astro
title: "Pretzels"
publishedAt: "2020-05-20"
publish: true
metaDescription: "Tips and notes on making pretzels"
---

MDX support

The MDX format allows you to embed React components into a Markdown file. This is great for putting little bits of interactivity into a document. In my case, I’ve used it to build a TVP ratio calculator. Astro seems to treat all Markdown files as MDX, which is to say that you can use MDX features in a file with a .md extension.

An MDX file could look like this

# MDX Example

Look, here's a widget
<Widget />

Isn't that neat!

In order for <Widget/> to work the component needs to be imported. This is where setup comes in. It’s a built-in frontmatter feature (though not well documented, IIRC) and you can use it to import components. Here’s an example from the frontmatter of my TVP calculator

setup: |
  import { Calculator } from "./calculator.tsx"
  import './calculator.css';

That’s about all there is to it. It’s really simple and really fast. Having done no intentional optimisation I’m smashing a near-perfect Lighthouse score