Skip to main content
kld.dev

My brand new Astro site

January 25, 2023

I finally decided it was time to redesign my personal site, which will now host all of my written content.

Motivation

My first website was hosted on Angelfire in 1996. At first, it was mostly a place to plaster my collection of animated gifs. Over the years, it evolved into a Friday the 13th film series fan site, and later into a B-movie review site. There is a growing desire to get back to the IndieWeb of these personal sites, and I’m all for it.

Also, there’s no better place to play around with new technology than your personal site, so welcome to the jungle.

Technical stuff

These were my requirements for the build toolchain:

  • Static pages generated from markdown
  • Fast and lightweight (prefetching, plenty of build-time optimizations)
  • Low effort to add content
  • Good documentation, ecosystem, and community

SvelteKit vs. Astro

I initially adopted SvelteKit after seeing this excellent guide to building a blog with SvelteKit by Josh Collinsworth. I have always liked Svelte’s approach of doing as much rendering as possible during the build instead of having a large runtime library. SvelteKit did not disappoint. It easily met all my requirements.

However… I decided to give Astro a try just to see if it could possibly be any better than SvelteKit. And for my site, it actually did prove to be better for a few reasons:

  • If I ever want to demo components written for other frameworks like Vue or React, Astro makes this very trivial.
  • The markdown handling is a little bit better. For example, to get an array of headings from a markdown file in SvelteKit, I had to write a custom remark plugin, whereas Astro gives you this out of the box.
  • The Astro documentation is phenomenal.
  • Astro’s proprietary component syntax is close to JSX, so it feels more familiar.

Integrations

I’m using the following Astro integrations and packages:

At first, my pages were not prefetching correctly even after installing the prefetch integration and adding rel="prefetch" to my links. I didn’t realize my mistake until I noticed that the Age response header was always zero for my page requests. While following a tutorial to set up my site, I had mistakenly enabled server-side rendering in place of static site generation. For static site generation, you do not want output: 'server' in your Astro config, nor do you need to specify an adapter.

Layouts

Astro lets you specify a layout (an Astro component) in your markdown frontmatter, so right now, all of my markdown files start like this:

---
layout: ../layouts/ArticleLayout.astro

I wish there was a way to have all of my markdown files just default to this layout (Update: This is possible!). Regardless, by doing this, my article layout receives two props from the markdown: frontmatter and headings. I use these to display the title and publication date, as well as to build my table of contents. The actual markup generated from the markdown file is injected into a <slot /> in the layout.

Here is a heavily simplified version of my ArticleLayout.astro:

---
import TableOfContents from '../components/TableOfContents.astro';
import BaseLayout from './BaseLayout.astro';

const { frontmatter, headings } = Astro.props;
const formattedDate = new Date(frontmatter.pubDate).toLocaleDateString('default', {
  year: 'numeric',
  month: 'long',
  day: 'numeric',
});
---

<BaseLayout title={frontmatter.title} canonicalUrl={frontmatter.canonicalUrl}>
  <h1>{frontmatter.title}</h1>
  <div>
    <span>{formattedDate}</span>
    {frontmatter.tags.map(tag => <span>{tag}</span>)}
  </div>

  <TableOfContents headings={headings} />

  <article>
    <slot />
  </article>
</BaseLayout>

The content goes inside the <slot />, and the full article with its title, table of contents, etc. are nested inside <BaseLayout>. I plan on covering the table of contents in a future article, but let’s take a look at the BaseLayout.astro as well. Again, this is a heavily abridged version with only the relevant bits:

---
const { title, canonicalUrl } = Astro.props;
import KldFooter from '../components/KldFooter.astro';
import Logo from '../components/Logo.astro';
import ThemeToggle from '../components/ThemeToggle.astro';
import '../styles/global.scss';
---

<html lang="en" data-theme="dark">
  <head>
    <!-- ... -->
    {canonicalUrl && <link rel="canonical" href={canonicalUrl} />}
    <title>{title} {title ? '|' : ''} Kevin Lee Drum</title>
  </head>
  <body>
    <header>
      <Logo />
      <ThemeToggle />
    </header>
    <main>
      <slot />
    </main>
    <KldFooter />
  </body>
</html>

As you can see, the base layout provides the <html>, <head>, <body>, etc. for every page on my site (not just articles). Everything we nested inside <BaseLayout> in the article layout will take the place of the <slot /> above. I also passed title and canonicalUrl as props, and they are used for <title> and <link ref="canonical"> elements respectively.

Styling

I will not fight anyone about it, but I like to use Tailwind for most of my styling when I can. It is a faster workflow for me, and it keeps me from having to create too many arbitrary one-off classes for the sake of “semantics”, especially when all I need is yet another flex box with centered children.

Since a lot of my content’s markup is generated from markdown during build, I am also writing quite a bit of sass within an Astro <style> block. At first, a lot of it looked something like this.

article :global(ul) {
  list-style-type: disc;
}

Like other frameworks, Astro scopes styling by default. The <article> element is in the layout, but all of the paragraphs, subheadings, lists, etc. are one scope deeper inside the <slot />, so we need to wrap those selectors with :global().

Alternatively, you can specify that your whole <style> block is global, which is what I ended up doing.

<style lang="scss" is:global>
  article {
    ul {
      list-style-type: disc;
      /* ... */

What’s Next

There are a number of features I am still considering, such as webmentions, a Uses page, and a Now page.

I have already done a write-up on how I am using webfonts. A few other things I might cover in future articles:

  • the TableOfContents component (Done! Part one and part two)
  • the front page, including the C64 that displays a tiny version of the front page (to scale!)
  • the ThemeToggle component
  • overall design considerations
  • little things like the favicon, keyboard navigation, and logo?