Lifting Off
October 19, 2023

And I "had" to change web site generator yet again. This time was rather weird. I didn't update NodeJS nor any package. Out of the blue the generation simply stopped working. Since I wasn't entirely happy with a few things of the previous generator, I decided to move on without attempting that much to fix the build process. Well, OK, I didn't try at all to fix it.

So the first thing to tell here is the reason I wasn't entirely happy with Nuxt. Well, it generates sort of a "black box", a directory named _nuxt within the final build. Its contents were always a mystery to me. Whenever I updated anything in my site I noticed that its contents were also different, forcing me to upload way more files than I really wanted to every time I updated something. This little directory has always driven me rather unease with the entire framework. I guess I was always awaiting for a reason to switch to another generator. Well... it became broken!

And so, I have switched to Astro . One of the things that attracted me is the philosophy of outputting the least amount of data possible. Astro also allows the usage of additional "UI frameworks", like React, Vue and so on. Yet I decided not to use any of those.

Before I delve into some of the details of what I have now with Astro, while also comparing to what I previously had (both Metalsmith and Nuxt), I have to tell that this time I have done two different things in an attempt to prevent having to rebuild everything again:

  1. I have created a document describing every configuration that I had to change/add so the build would output what I need. When both Metalsmith and Nuxt broke I didn't remember every single thing that I had changed. Specifically for the Metalsmith (which I did attempt to fix), this was a huge problem. As for Nuxt I'm absolutely sure that I would have similar problem if I actually attempted to fix the setup.
  2. I created a Docker image. Well, this will probably be the most resilient thing. Yet I cannot be entirely sure. While I fully understand the concept of Docker, just making it work was a tremendous nightmare. I do not know if I did everything correctly or not, much less if this will indeed be as resilient as I want it to be. Nevertheless, the attempt is there.

Since I didn't want to deal with CSS yet again, I decided to use Tailwind CSS . And as it turns out, Astro does have an official package to integrate this CSS framework. This decision was pretty interesting. Instead of having to write a ton of (S)CSS, I only had to declare the color variables, a main default font and a ::before to count the codeblocks within the page.

Notice the fact that I opted to use CSS variables to define colors rather than Tailwind's recommended method, which is to directly use color names. I decided to use variables because it makes things a lot easier to deal with when there is theme switching involved. To help explain here, let's see the recommended way to define the background and text colors. In it we have to specify the default color and, through a class selector, specify the color for the dark theme. So the body element would be declared something like this:

<body class="bg-slate-50 dark:bg-slate-950 text-slate-950 dark:text-slate-50">

Then, to switch to the dark theme we need to add the dark class to the <html> element of the page. Removing said class goes back to the default theme.

We can declare the variables with default values and, inside a .dark { ... } class block within the CSS, we assign the dark theme colors to those variables. Adding the dark class to the <html> will essentially make the variables use the values assigned in the CSS block. The amazing thing here is that we can map those variables to Tailwind color IDs

module.exports = {
   theme: {
      extend: {
         colors: {
            "main": "var(--main)",
            "fore": "var(--fore)",
         },
      },
   },
}

And then we can update the <body> to something like this:

<body class="bg-main text-fore">

The same simplification becomes valid to pretty much every other element that requires colors in the entire project!

There is another thing I want to mention regarding the colors. I have opted to default the values to correspond to the dark theme. This is contrary to pretty much every single page I remember visiting that also allows theme switching. But why did I do this? Well, during development I was indeed defaulting color values to the light theme. Yet I did notice that when there was a slight slow down in the loading of a page, there was a brief moment that the default colors were shown before switching to the dark theme. Now consider selecting dark mode, browsing in the dark. If there is a hiccup in the connection, then it's possible that a bright light in the face will occur for a short moment. That's very uncomfortable. Now consider the opposite situation. The light mode is selected, there is a hiccup. A brief moment of dark colors will be much less uncomfortable, thus the reason I opted to default the colors to dark ones.

Earlier in this text I have mentioned that my CSS file has pretty much three things in it. The color variables, the default page font and the codeblock counter. At to this point I have placed 3 blocks of code snippets. You can see a "Listing: x" label bellow each. This is the result of that CSS counter.

As for the default font. Well, this is something that has puzzled me a lot! As I began experimenting with Astro and Tailwind I didn't specify any font for quite some time as I was focusing on learning both frameworks. Then I opened the layout (Astro) component and added the font specification like <html class="font-serif">. Well, the font did indeed change to a serif one, but was a lot bigger than if no font were specified. Just as an experiment I tried to add html { font-family: serif; } into the CSS file. And then, the font changed to what I wanted with a size very similar to no font specification. I don't know why this difference in font height happened, but it somewhat "forced" me to keep the specification within the CSS block rather than through adding the Tailwind class.

If you want to know why I chose to use a serif font, it's because those tend to be a lot easier to differentiate certain characters, specially i, I, l and L.

In the Astro documentation there is a significant portion of the text showcasing one of the official integrations, which is MDX . I found it very interesting because it meant I would be able to map the default Markdown elements into Astro components without effort. This mapping would be almost necessary so the output would use the Tailwind classes. And indeed, the initial experiments I did were amazing! Yet, there was a problem. A big one. MDX does not give an easy way (if at all) to manually render the Markdown contents. Extracting a portion of the text is out of the question. Why I want this? Well, if you open the blog section you will probably notice that I have listed all posts with just an excerpt of each. I also need those excerpts for the RSS/Atom feeds. And speaking of feeds, I need manual rendering to add the content into both RSS and Atom.

After some research I decided to experiment Markdoc , which also does have an official Astro integration. This one did fit my needs perfectly! In Markdoc's documentation there is information on how to manually render the Markdown contents. One of the aspects that caught my attention is the fact that the rendering is broken into three major steps (after the raw text is gathered):

  1. Convert the raw text into an abstract syntax tree (AST). There is some information about AST in this Wikipedia article .
  2. Convert the AST into a renderable tree.
  3. Use the renderable tree to generate the final HTML document.

Having access to the abstract tree is a fantastic thing! It allows us to manipulate the contents before rendering them. And indeed, to extract excerpts I'm simply deleting several branches from the AST before feeding it into the final two steps of the rendering process. To be more precise here I have used another interesting feature of Markdoc. As with MDX we can use components directly within the document. Those must be defined within the Markdoc configuration. So I configured a component named "more". When traversing the AST I simply delete everything that comes after the "more" element. And of course, this custom element doesn't render anything, it just generates a node in the AST.

Markdoc also brought another fantastic thing to my use case. In several of my latest news posts I have added images to them. Not only that, almost all of the posts have links to other sections of this web page. When the news posts are converted into RSS/Atom feeds those links must be full paths. When I was working with Metalsmith and Nuxt I had to pretty much use string replace features within the final HTML output in order to add the full path to the feeds. Now within the Markdoc configuration I simply "intercept" the <a> and <img> elements through the renderable tree transformation. At that moment I can modify the href and src attributes to become full paths. No need to parse the rendered HTML!

When I was working with Metalsmith I had to write extensions to the Markdown parser so I could deal with several UI elements that I have created. In a way this is an extra source of possible bugs. Indeed, when I transitioned to Nuxt I did find several small problems rooted at those extensions I created. Now with Astro + Markdoc I don't have to worry about writing extensions. To be fair, I could probably have used Markdoc with Metalsmith and/or Nuxt. Unfortunately I only learnt about it recently when I was evaluating if Astro would solve my problem.

As mentioned it's possible to directly use Vue components within the Markdown text when working with Nuxt. It's also possible to use Astro components within Markdoc text, provided those are "registered". However there is a big difference in syntax. In Nuxt we can use the common HTML syntax to invoke a Vue component. In Markdoc the syntax is like {%some_component%}. I'm not particularly happy with this syntax, however I have to accept it.

There is another difference in the parsers. When I was working with Metalsmith and Nuxt I got several wrongly generated pages because I forgot to "close" the elements. Be that a tag or the common ``` fence marking to start a code block. Markdoc error out if something is not closed. This is a huge win for me! Indeed, when migrating the contents to Astro/Markdoc I did find several pages with unclosed tags and/or fence code blocks

Markdoc does not have the most common math syntax used in Markdown extensions, which is the $ symbol. And there is no easy way to add this kind of support. Instead we have to create a custom tag for it. So, in my text files instead of $ y = x^2 $ I have to write {%math%}y = x^2{%/math%} to obtain y=x2y = x^2 as result. This is a lot more verbose than I wanted. Unfortunately not much that I can do about it.

As part of my initial evaluation, I have tested Astro output. I was very happy that I didn't have to deal with any additional unexpected file or even extraneous elements/classes within the resulting HTML files like I had with Nuxt. While Markdoc by default wraps everything in an <article> element, it's incredibly easy to bypass that. So, yeah, I did regain almost full control over the output of my page.

At the beginning of this post I mentioned that Nuxt generates a directory named _nuxt that is a complete mystery to me. Astro also generates a directory, _astro. However its contents are very easy to understand what they are. Basically, this is where bundled scripts and stylesheets will be placed at. I did have to change a setting just so the bundled script files resulted in consistent names. By default those names have a hash value appended to them. This becomes a problem because each generation results in different file names. This would force uploading the entire output after every single page update.

Now something that is interesting here is the amount of additional packages I had to install:

The rather low number of packages that I'm using now, added to the fact that just two of them are tightly part of the building process allows me to believe that there is a lower chance that things will suddenly break again.

Based on what I wrote here might seem that I'm considering Astro as the perfect framework. Well, it's not. I did have to go around some corners (read: use some workarounds) to perform some tasks. As an example, Astro does provide an official integration to generate RSS feeds. However the output is not exactly complete. When I used RSS validator I did get several warnings. Unfortunately I didn't save those warnings nor any of the test RSS files so I can't exemplify here the errors. But more than that, its configuration is extremely limited, so fixing the problems pointed by the validators were rather problematic. Luckily it was very easy to simply use the Feed for Node.js package. As a benefit of using this, I now have Atom feeds too.

Then there is the sitemap generation. Astro's default sitemap system is extremely limited! It simply includes every single valid route into the map. The filtering method is based on the output path. This is a problem because drafts aren't easily identified simply by their routes. So I ended creating a custom sitemap generation, using the same Astro API I used to generate the RSS/Atom feeds. More specifically the Endpoints API .

Another aspect of Astro that is not particularly to my style is the JSX syntax. I do find it a bit weird and sometimes hard to format it into readable code. But then again, this is my way of thinking and a particular thing, it's not exactly a "problem" of the framework itself. Yes, as I get older I also get grumpier.

Now that we have lifted off, hopefully I won't have to change frameworks again and all I can say now is happy forging... in space!