Earlier this year, a colleague of mine introduced me to Astro, which I quickly recognized would be an excellent update for my own website. It was a way to give a facelift to a site that had gone stale, use some different technology, and more importantly, implement some of the things I’ve enjoyed doing in my day-to-day as a developer.
Don’t forget, a few years ago I wrote an article about why I built this site in Jekyll, and yet here we are. But the reality of the web is that no one site should be expected to live on its platform forever - in fact, this same domain has been built up and torn down by yours truely over the last 20 years in various integrations, including:
- Custom-built PHP CMS with no framework;
- Hand-rolled static HTML with no framework;
- Jekyll with Bootstrap;
- …now, Astro with Tailwind / DaisyUI
So what learnings and findings did I encounter in building this site (again)? What pros / cons do I have for Astro vs Jekyll (or any other platform)?
Strengths
Easy as F to set up
One of my pain points with Jekyll was keeping the under-the-hood Ruby stuff working. For whatever reason, across machines, maintaining Ruby versions and bundlers has been a sore spot for me - Astro is self-contained and runs like any other front-end project since it is Node based. Run npm i && npm dev
and you’re up and going in no time.
Astro has TypeScript support OOB.
I love the discipline that TypeScript forces me into. I also get frustrated by it, but for the right reasons! Not only that, but there is a way of utilizing Zod to enforce a schema - and thus, Types - on your content collections. This means that while developing, you are ensuring that front matter will be following a certain format, which is a good thing when you’re keying things in by hand.
If you don’t follow the rules, your compiler will yell at you, you won’t be able to see it locally or deploy and mistakes are avoided.
That said, you don’t have to define a schema for your collections - in this project, for example, I have Zod schemas for:
- Blog
- Experience (on the About page)
- Projects
The collection called “Sandbox” doesn’t have one yet - but I can modify that later.
Markdown Is Fine
Moving my data from Jekyll to Astro was a cinch. All I had to do is copy my existing markdown files over for my projects and blogs, then create a schema for them.
Moreover, I can use MDX which is like Componentized-Markdown, which can utilize a hybrid of JSX and Markdown formats! This post you are reading now is in MDX, and so I can utilize an Astro native “Image” component right within it:
Code for this is:
import { Image } from 'astro:assets';
import tux from './images//2024/09/02/tux.jpeg';
...
<Image
alt="Tux"
src={tux}
/>
I could define a custom component, and utilize that as well, such as this goofy little Link Card component:
<Card
href="https://www.colinbayer.com/"
title="Colin Bayer"
body="Visit My Website"
/>
I also don’t have to choose - each post can be either .md or .mdx depending on the needs or requirements of that individual post. Flexible!
Personal Growth
Over the years, I’ve become less strict around trying to keep a proper “separation of concerns” knowing that the technology exists now to dynamically load scripts and styles modularly. While this wasn’t something Jekyll could handle, it is definitely something Astro can, following in the footsteps of many platforms before it, like NextJS or Gatsby. Overall, Astro definitely takes a “component-based” web approach, where there are lots of smaller files that do one thing, rather than centralized files which is how the web “used” to work.
This meant I could write scripts or styles that are either universal or specific to components and not have to worry so much about bloat or conflicts.
This does result in somewhat ugly code without some disclipline. Astro has a :global directive for style tags, for example, that will globally apply styles that could be buried in some component somewhere. I use it myself to set up some themeing:
<style is:global>
:root {
--ccb-heading-slice-background-color: theme("colors.amber.100");
--ccb-heading-accent-color: theme("colors.pink.500");
--ccb-heading-secondary-accent-color: theme("colors.indigo.300");
--ccb-heading-shadow-color: theme("colors.neutral");
--ccb-header-background: theme("colors.zinc.800");
--ccb-header-blend-mode: invert;
}
</style>
This could get ugly if you are scattering global styles everywhere. Between this feature, other component-level styling, and Tailwind - I don’t actually have a monolithic CSS or standard SCSS file on this project!
Islands
This sounds like a chaotic nightmare at first, but it’s actually quite neat. Astro supports a concept called Islands, which isolate client-side code to their relative files - meaning you can have a Vue.js component over here, a react component over there, an Alpine.JS component over here - all independently running to work as you’d expect. I’m not sure if I’d want to spread myself that thin, but there is something to be said to allow the freedom to give yourself the best tool for the job. Astro’s own docs say:
Although most developers will stick to just one UI framework, Astro supports multiple frameworks in the same project. This allows you to:
- Choose the framework that is best for each component.
- Learn a new framework without needing to start a new project.
- Collaborate with others even when working in different frameworks.
- Incrementally convert an existing site to another framework with no downtime.
Challenges
Thankfully, I did not run into many challenges - just finding the time to work on this project! However, a few things tripped me up that are worth sharing.
Client vs Server
While Astro has a lot of JS under the hood, the thing to remember is that it is “server first”. Therefore, you don’t have access to the window object unless you explicitly identify something as client-side. This is not unlike NextJS 13+, where client-side components need the use client
directive at the top of the files. In Astro, if I need some client-side code, I can add a similar directive - such as this JS written in the Search Overlay component:
<script is:inline>
const toggleMenu = () => {
const mobileMenu = document.getElementById('mobileMenu');
mobileMenu.classList.toggle('hidden');
}
const
searchButton = document.getElementById('searchTrigger'),
searchDialog = document.getElementById('searchDialog'),
searchOverlay = document.getElementById('searchOverlay'),
searchInner = document.getElementById('searchInner')
;
searchButton?.addEventListener('click', () => {
if(searchDialog) {
const isHidden = searchDialog.classList.contains('hidden');
if(isHidden) {
searchDialog?.classList.remove('hidden');
searchDialog?.classList.add('block');
// focus
searchDialog.querySelector('input').focus();
} else {
searchDialog?.classList.add('hidden');
searchDialog?.classList.remove('block');
searchButton.focus();
}
}
});
// hide if user is clicking outside the modal
searchOverlay?.addEventListener('click', (event) => {
const isInSearchInner = event.target.contains(searchInner);
if(isInSearchInner) {
searchDialog?.classList.add('hidden');
}
});
document.addEventListener('keyup', function(e) {
if (e.key === "Escape") {
searchDialog?.classList.add('hidden');
}
});
</script>
Data Importing Can Be Tricky
I had some issues importing arrays of images to generate a carousel; I couldn’t just hand it an array of image names, construct a path and render the images. I spent a long time trying to simply access the images by key since i had the client slug - but the paths were all incorrect. When Astro compiles your project, the directories it creates that get served up are different than the paths that are used on the development / local server.
Instead, I had to create a component for my gallery, pass it the project’s image array, which was defined as front-matter on each project - then parse that to display the images.
Front Matter:
....
gallery:
- src: "./images/clearchoice/home.png"
alt: "Home Screen"
- src: "./images/clearchoice/location-detail.png"
alt: "Location Detail Screen"
---
on the Front End, I’d need to ensure the src got treated like an image, so I import the “src” as Type ImageMetadata - this is also reflected in my Zod schema:
gallery: z.array(z.object({
src: image(),
alt: z.string()
})).optional()
Rendering the gallery was just a matter of looping over the array in the gallery and passing the props to the Image component:
{gallery.map((img, index) => (
<div
class="tile"
onclick={`openInModal(${index})`}
data-image-tile={index}
>
<Image
src={img.src}
alt={img.alt}
format="webp"
class="mb-8 rounded-lg object-cover max-h-60"
/>
</div>
))}
…but it took me a long while to get there.
Conclusion
I am liking this switch so far! I also like the facelift I gave it - it feels nice to move away from Bootstrap for the layout, and Jekyll in general.
My goal is to continue writing content, maybe keeping it more frequent and less lengthy, as well as add some more Sandbox type projects. The flexibility that this framework gives me to mix in React, or other frameworks as needed should help in that regard.