August 30, 2023

Try Astro

My experience with Astro, a new static site generator.

The last few years have seen a flurry of new JavaScript-based static site generators. First Hexo, then Gatsby, later Next.js, and now Astro with its Island-based architecture. However, I’ve had a bad impression of JS-based SSG despite the fact that the first SSG I’ve ever used was Hexo.

Back in 2016, I had an old laptop with a hard drive that was at verge of death. Every time it ran an npm command, it and I had to endure the noisy fan. Still, it was a success at the moment compared to Jekyll (which I never got to run it locally).

Then I met Hugo, and its performance convinced me to make the leap from dynamic sites (I had used Ghost, WordPress and Typecho). I still remember back in the days when I first met Hexo and downloaded a theme from GitHub (I can’t remember its name). It used Jade as its template engine, and somehow the Hexo’s live reload plugin did not play well with it. Every time a change was made, the website reloaded but nothing changed, and the only solution was to manually restart the server. There were indeed many plugins to play with, but the question was: do they work well together?

Years later, I was finally able to retire that old laptop that had been with me since 2012 and start using an SSD. But I was already on Hugo’s ship and had learnt its template syntax, and there was no reason to switch back to Hexo since I could write my own theme.

Then React became the thing everyone was talking about and using. And Gatsby came along with GraphQL. I tried it and I did not like it. The build speed and the startup time of the development server was too much for me after I had a taste of how fast it could be.

Another thing that bothered me a lot was the fact that Gatsby ships with a lot of client-side JavaScript, and I mean a lot. Although the page transition was almost instantaneous, the first-time load performance was not that good. And being a simple blog site, I want as few things as possible, keep it simple.

I haven’t written a blog in months and shut down this site previously. This summer while I was on vacation I thought it was time to learn something new, so I installed Astro locally on my laptop. My first impression was that the cold-start time of the development server was much faster, probably thanks to Vite (in contrast to Webpack). The syntax of the Astro template wasn’t out of place, I would say it’s really similar to Svelte but with JSX, it won’t take a long time to get used to it if you’ve written JSX before or used some of the frontend frameworks.

Another strong reason for me to five it a try was the assets feature, which was experimental on Astro 2.x but is due be released with Astro 3.0 this week. I always find that the page bundle feature of Hugo to be the ideal way to organise them, rather than putting them all in the public folder.

I started making changes to the starter blog template and learning its mechanics.

Change image tag’s output

The first thing is to change how images are rendered in Markdown. The default output is a simple <img> tag with its dimensions (thanks to the assets feature, which reads that information automatically). But I would like it to be wrapped in a <figure> tag and include <figcaption> if the image has a title.

In Hugo, there’s render-image.html which allows me to change an image’s HTML output easily.

In Astro it ain’t that simple. To begin with, if you use MDX, the official solution should be MDX’s custom components. However, it does not work with Astro’s assets feature (Issue #7223).

I did not use MDX because I did not need to include components in my post. (By the way, it’s said that MDX slows the build time since it needs to parse JSX syntax too, which makes sense to me). The default Markdown render that Astro uses is Remark.

Remark is a very popular ecosystem of plugins that work with markdown as structured data, specifically ASTs (abstract syntax trees). ASTs make it easy for programs to deal with markdown

In order to change the image tag’s output, we can create a Remark plugin. It was tricky at first as it had no Typescript support and in order to figure out the data structure I had to do serval console.log just to make sure that I was accessing the right field.

But once I figured it out the mechanics, I have to say it’s really flexible and powerful and allows me to do so much more. For example, the image gallery feature that comes with Stack theme can be done using the following Remark plugin. The biggest advantage is that it’s done at the build time, so there’s no content layout shift.

function gallery(options = {}) {
    return (tree, file) => visit(
        (node) => node.type === 'paragraph' && node?.children.every((child) => {
            /// Paragraph with only local images and empty text
            return (child.type === 'image' && !child.url.startsWith('http')) || (child.type === 'text' && child.value.trim() === '');
        node => {
            node.type = 'element';
   = {
                hName: 'div',
                hProperties: {
                    class: 'gallery'

            node.children = node.children.filter(node => node.type === 'image').map(img => {
                const alt = img.title || img.alt || '';
                const src = img.url;
                const children = [img];

                if (alt) {
                    const figcaption = {
                        type: 'element',
                        data: { hName: "figcaption" },
                        children: [
                                type: 'text',
                                value: alt.trim()
                const figureElement = {
                    type: "element",
                    data: {
                        hName: "figure",

                return figureElement;

            return node;

This is my first time writing a Remark plugin, probably the code can be simplified further. But I believe this is much better than manipulating the DOM on the client side.

View transitions

It’s currently supported only on Chromium-based browsers and delivers a smooth transition between pages. Astro 2.9 shipped with built-in experimental support for this feature. The size of JavaScript bundle did not increase significantly, a thumbs up to the team.

I’m really happy with the result. It reminds me of morph transition in PowerPoint.

The real experience isn’t that laggy as it seems.

Developer Experience

Writing a theme for Astro is a pleasure, thanks to Typescript and the official VSCode plugin. Hugo’s template syntax is powerful, but not easy to learn. And the lack of IDE support really makes it hard for beginners. I don’t even dare to code format my Hugo template, because it might just break it.

On the other hand, I can get IDE suggestions as I write JSX, and it reminds me of the parameters that a component accepts, rather than jumping back and forth between open files.

Web Component

While playing around with view transitions, I ran into the problem of destroying event handlers before switching pages. The solution I’d found (may not the best) is to use WebComponent (which is also a new thing to me).

I wanted to add a local time clock to my homepage using setInterval, and before each page transitions the correct way would be to call clearInterval so that the timer is destroyed. A similar problem happened with PhotoSwipe, I had to destroy and re-initiate the lightbox when the article page was loaded.

Web Component is a simple solution to this problem, as Astro explains on their documentation page:

Although a <script> only runs once, the browser will run our custom element’s constructor() method each time it finds <astro-heart> on the page. This means you can safely write code for one component at a time, even if you intend to use this component multiple times on a page.

It also has a disconnectedCallback method, for when the element is removed from the page, perfect for my use.

    <span data-time></span>

    class LocalTime extends HTMLElement {
        #time: HTMLElement;
        #intervalID: NodeJS.Timer;
        // Called when element is added to page
        constructor() {

            this.#time = this.querySelector("[data-time]")!;

            const updateTime = () => {
                const date = new Date();
                const hours = date.getHours().toString().padStart(2, "0");
                const minutes = date.getMinutes().toString().padStart(2, "0");
                const seconds = date.getSeconds().toString().padStart(2, "0");

                this.#time.innerHTML = `${hours}:${minutes}:${seconds}`;

            this.#intervalID = setInterval(updateTime, 1000);

        // This is called when the element is removed from the page.
        disconnectedCallback() {

    customElements.define("local-time", LocalTime);

Why didn’t I switch to Astro at the end

Short answer: Because I’m too lazy to do so.

I started learning Astro because I wanted to give an overhaul to my personal page and reactivate my blog. But at the end, I’ve wasted too much time trying to polish Astro to the way I wanted it to be when I could just sit down and start writing something as I’m doing right now. I’ve already got a functional Hugo theme, porting it to Astro or making one new might just be a waste of time.

If I have to create another theme for my blog in the future, I’ll probably switch to Astro then. But for now, I don’t really have a big motivation for it. I’ll keep an eye on Astro’s development and see how it goes.

After all, if it ain’t broke, don’t fix it.

I haven’t written an English blog for a long time so this kind of an experiment and a challenge for me. I’m trying to improve my English skills and this could be the way.