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:
- @astrojs/prefetch for prefetching pages
- astro-compress for compressing HTML, CSS, JS, and SVG
- @astrojs/rss for generating an RSS feed
- @astrojs/tailwind for Tailwind integration
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: