How I Built My Blog

Inside look into the tech used to build this blog

Recently, my journey through web development has taken me down a path of simplicity. Every decision is always made with a single question in mind, will this make my life easier? So let’s break down the stack that makes my life easier.

The Framework

These things are all the rage now-a-days. There are so many to choose from. Just thinking off of the top of my head, there is React, Vue, Svelte, Solid, Angular, Lit, Alpine, Quik and the list goes on. Not to mention, each one of these having their own meta frameworks. Before I choose which one of these to use I need to understand my requirements. At a bare minimum I need:

  • The ability to write blog posts using markdown, preferrably mdx
  • Easy syntax highlighting
  • Simple deploys, preferrably to infrastructure that I own

After experimenting with a couple of options, it was clear that the simplest option was Astro.

Why Astro?

Astro’s tag line on their landing page is “The web framework for content-driven websites”. Is it ever. You can tell it has been expertly crafted to solve content on the web. #Not Sponsored.

Given that my personal blog is purely content, Astro was the obvious choice. By why? Well, let’s start with their markdown support.

Content Collections

Astro has created a feature called Content Collections. Content collections provide an easy way to write and manage markdown content for my blog. Markdown files are organized in the src/content directory. Content is loaded using a catch-all route in the src/pages directory using getStaticPaths.

src/pages/writing/[...slug].astro
export async function getStaticPaths() {
const posts = await getCollection("writing");
return posts.map((post) => ({
params: { slug: post.slug },
props: post,
}));
}

What is cool about this is getStaticPaths will expose the content collection as props on the route. Which allowed me to render the markdown into a layout component of my choosing.

src/pages/writing/[...slug].astro
...
const post = Astro.props;
const { Content, headings } = await post.render();
---
<WritingLayout {...post.data} headings={headings}>
<Content />
</WritingLayout>

and just like that, I can write as many blog posts as I want in the src/content/writing directory and they will automatically be rendered on the corresponding route.

But it doesn’t stop there. Content collections can be used in any astro component. In order for me to render the list of blog posts here I can use the content collection API to get a list of all of the blog posts and then render them in a list.

---
const blogPosts = ((await getCollection("writing")) ?? []).map((post) => {
return {
collection: post.collection,
slug: post.slug,
...post.data,
};
});
---
<ul class="writing-list">
{
blogPosts.map((post) => (
<li>
<a href={`/${post.collection}/${post.slug}`}>
<span>{post.title}</span>
<time datetime={post.date.getFullYear().toString()}>
{post.date.getFullYear()}
</time>
</a>
</li>
))
}
</ul>

Syntax Highlighting

Along with content collections, Astro’s expressive code library makes syntax highlighing a breeze. With Astro, syntax highlighting comes out of the box, but I wanted to make a few customizations. Luckily, adding ec.config.mjs with a few config changes is all I needed to get highlighting that works in both light and dark mode with Winnie UI.

The Styling

This is definitely biased, but all of the CSS is written with the help of Winnie UI. Winnie is an opinionated set of CSS primitives that make writing CSS a breeze. It provides me with all of the colors, spacing, typography and much more.

Winnie CSS lets you import all of the styles, or just the ones you need. For my blog I didn’t need much, just a couple colors, typography and spacing and a few other things.

global.css
@import "@winnie-ui/css/layer";
@import "@winnie-ui/css/reset";
@import "@winnie-ui/css/base/colors/accent";
@import "@winnie-ui/css/base/colors/orange";
@import "@winnie-ui/css/base/colors/grey";
@import "@winnie-ui/css/base/colors/black";
@import "@winnie-ui/css/base/colors/white";
@import "@winnie-ui/css/base/animation";
@import "@winnie-ui/css/base/border";
@import "@winnie-ui/css/base/scaling";
@import "@winnie-ui/css/base/shadow";
@import "@winnie-ui/css/base/space";

Each of these is imported to their respective layers to control the cascade. To finish it off I configured the theme I wanted to use in base layout.

base-layout.astro
<html
lang="en"
class="winnie-ui"
data-accent-color="orange"
data-scale="95%"
data-radius="lg"
data-theme="light"
data-code-theme="github-light"
>
<head>
<slot name="head" />
</head>
<body>
<slot />
</body>
</html>

With all that being said, I can now access all of the CSS variables that Winnie has to offer and soon the Spatial Components that I am working on. More details on those to come in the future. Would love for you to give Winnie a try on your next project.

The Deployment

There are plenty of choices as to where you can deploy your projects. Obviously something like Vercel would be the easiest. But ony of my requirements is that I own my infrastructure. Given that requirement, any service that wraps AWS is not really going to work for me. That is where SST comes in.

SST allows me to deploy directly to my own AWS account. On top of that, in a single config I can set up my Cloudflare DNS for my domain. So not only am I using my own AWS account but, I am also using my own Cloudflare account. I can’t say enough good things about my experience with SST so far. I also decided to set up CD with github actions which will deploy anytime I push to the main branch. An experience similar to Vercel, but on my own infrastructure.

At some point I may look into using a simple VPS. But that is for another day. In the meantime, have a look at my sst.config.ts below.

sst.config.ts
export default $config({
app(input) {
return {
name: "adamaho",
removal: input?.stage === "production" ? "retain" : "remove",
home: "aws",
providers: { cloudflare: true },
};
},
async run() {
const isProduction = $app.stage === "production";
const domain = isProduction
? {
name: "adamaho.com",
dns: sst.cloudflare.dns(),
}
: undefined;
new sst.aws.Astro("AdamAhoDotCom", {
domain,
});
},
});

Summary

To sum it all up, here is the final stack:

  • Astro
  • Expressive Code
  • Winnie UI
  • SST V3