Javascript

Building Web Applications using Astro - What makes it special?

Jae Anne Bach Hardie
6 min read
Building Web Applications using Astro - What makes it special?
This article was written over 18 months ago and may contain information that is out of date. Some content may still be relevant, but please refer to official documentation for the latest information.

You might already have heard that there is a new player in the static site generator space that is generating a lot of excitement and asking some hard questions of the modern Javascript ecosystem. I'm talking about Astro, a framework constructed around two simple but revolutionary concepts:

  1. Accepting components from any UI framework
  2. Partial hydration

I have recently been working on a site built from the ground up using Astro, and even in the early state its in, I've been able to see the amazing possibilities it opens up in web development. So, let me give you a tour of what those two points mean, both conceptually and for the future of Javascript development.

Bring your own framework

The modern Javascript ecosystem is divided into separate and sometimes very distant camps, based on what UI library you're using. Next for React, Nuxt for Vue, SvelteKit for Svelte, etc. It doesn't have to be this way. All of these UI libraries ultimately use the same Javascript to output and transform the same HTML and CSS. Why should you have to replace your whole toolkit just to use a different brand of paint?

Astro uses a set of plugin renderers to support components written in different formats. Currently, they have ones for React, Preact, Vue, and Svelte, as well as supporting their own minimalist templating format in .astro files. However, there's no reason why this couldn't be expanded by the community in the future.

Of course, to hydrate components on the client you need to bring the framework's runtime with you, so it's never going to be particularly efficient to mix and match components from different frameworks on the same page. But, if you absolutely have to, it's now an option. What's much more important is the ability to leverage the potential of Astro, no matter which set of tools you're most familiar with. In a future where more parts of the ecosystem plug into different libraries like this, we might even see more iteration in the UI library space without new entrants having to worry about producing an end-to-end build, serve and hydrate story.

Just a sip (of client hydration)

The framework compatibility is great, but at the end of the day, the only thing it meant for us was that using Astro with React was possibile. It's all well and good that you can use it, but why would you want to? Partial hydration is the answer.

You see, most websites you will ever build have some interactivity beyond just clicking hyperlinks. It isn't the 90s anymore, rich app-like behavior is increasingly part of user expectations. This is why frameworks like Vue and React have taken off the way they have. However, most websites you will ever build also have large sections which don't have any interactivity beyond clicking hyperlinks. Maybe you have a blog that is totally static except for a search bar and a comment section. Or a marketing site that includes a carousel and a navigation popover. Or even a very complex interactive app, which is mostly fully interactive, but needs some highly-performant static marketing and e-commerce pages to sell it to customers.

This is the "islands of interactivity" model. Most web applications are a mix of static content and interactive widgets. However, in all of our frameworks, even if they pre-generate some HTML at build time or on the server using Server-Side Rendering, if you want to use components, you have to download those components on the client. Even if they're never going to do anything! A set of paragraphs with CSS classes that won't ever change? The client has to download that twice: once in the HTML that will actually be displayed, and then again as a sluggishly-parsed Javascript bundle that will execute during hydration, but have exactly no effect on the result.

Astro says "no" to this inefficient use of resources, and gives us a rich set of tools to send to the client only the Javascript that is minimally necessary to enable interactivity. This is called "partial hydration" because only the islands of interactivity, those widgets that you mark as needing to change, get "hydrated" by loading the client-side version of their components.

Astro components

The way this is all done is with a new syntax for HTML templating that allows you to include components from your framework of choice with annotations that decide whether they should hydrate, as well as Javascript, to execute during build-time. The following is an astro component extracted from a project I am currently working on, and only lightly edited, showing most major features:

---
import '$system/src/globals/global-styles'
import { fontHeadTags } from '$system/src/globals/font-import.mjs'
import { sprinkles } from '$system/src/sprinkles/sprinkles.css'
import { reactTheme } from '$system/src/themes/themes.css'
import { Sidebar } from '$system/src/components/sidebar.tsx'
import { MobileNav } from '../components/mobile-nav.tsx'
import { fetchCategories } from '../models/category.ts'

const { title } = Astro.props

const categories = await fetchCategories()
---
<!DOCTYPE html>
<html class={reactTheme} lang="en">
  <head>
    {fontHeadTags}
    <title>react.framework.dev | {title}</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta charset="UTF-8"/>
  </head>
  <body>
    <MobileNav client:media="(max-width:1024px)" categories={categories} />
    <Sidebar>
      <div class={sprinkles({ layout: "stack", gap: 24 })}>
        {categories.map(category => (
          <a href={`/categories/${category.slug}`}>
            {category.name}
          </a>
        ))}
      </div>
    </Sidebar>
    <main class={sprinkles({ marginX: 64, marginY: 48 })}>
      <slot />
    </main>
  </body>
</html>

The section between the --- is "frontmatter" (a concept borrowed from Markdown-based site generators), but as opposed to Markdown frontmatter, it is not restricted to just declaring data. Any JavaScript code can be imported or run in frontmatter, and it will be executed whenever the template is rendered during build time. This makes it very convenient and powerful for fetching and processing data.

The rest of the file is a JSX template, which hopefully looks fairly familiar. This isn't full React — no reactive state or event handlers — just a way to produce static HTML from a syntax that requires less specialized syntax knowledge than something like Handlebars because you can just use .map for loops and && for conditional rendering. You can include any HTML tag, any component in the framework of your choice, and one or more <slot /> tags, which will be replaced with the inner HTML — or "children" in React parlance —that the component is rendered with. The code above is for a main layout component, so our <slot /> will contain the page content.

The final special of feature of Astro components is client: directives. You can see an example on the MobileNav component above. Client directives can be placed on any component authored in a framework that has a client runtime (so, currently, Vue, React or Svelte) and when the conditions met by the directive are met, Astro will load the framework runtime and that component's code, and hydrate it to make it interactive. There are a number of directives for different conditions:

  • client:load hydrates on page load. This mirrors how SPAs are usually loaded.
  • client:idle hydrates as soon as the main thread is free. This should theoretically get you interactivity as soon as possible at the cost of potentially delaying loading, but I haven't experimented with it.
  • client:media hydrates when the browser matches a media query. This is great for content that will only be shown to certain devices, like our mobile nav!
  • client:visible hydrates when the component becomes visible. This is great for content that might be below the fold. You can load the page as fast as possible, and only download Javascript if the user scrolls.
  • client:only doesn't output anything at build time and instead of hydrating does from-scratch client rendering on load. It's almost always better to use client:load and provide a placeholder, but there can be cases where a component simply cannot be made to run outside a browser.

The result of the above is that on desktop we have a fully-functional site with no Javascript at all — Sidebar is a React component, but it just renders a static menu with <a> tags for navigation — and on mobile, we download React and only what is needed to make the MobileNav component work. This allows us to still use Javascript and Javascript UI frameworks, but our users only pay the price in performance for the features that they actually see, without an extra tax just to allow us to have a unified developer experience!

I hope you now understand why Astro is exciting, and why we jumped to try it out even though it's still in its early days. If you want to learn more about it, check out their documentation and their extensive repository of examples.

About the author

Jae Anne Bach Hardie

Jae Anne Bach Hardie

Software Architect

Keep reading

View all posts →

Astro: Do You Even Need JavaScript? with James Quick

James Quick, content creator and co host of the Compressed FM podcast talks about the evolution of Astro, a powerful static site generator that we should all get familiar with. He talks in depth about the framework and where the framework may head in the future. Astro has been making waves in the web development community. It has evolved over time, and is now competing with major meta frameworks. But with its unique features and excellent developer experience, Astro offers developers a fresh perspective on building websites and applications. Astro v3 brings a host of exciting features to the table. James shares Astro supports the View Transition API, which allows for smoother page transitions and a seamless user experience. He and Dustin talk about Astro's image optimization component, which optimizes images for performance without compromising quality. One of the standout features of Astro is its island architecture. James explains how this architecture enables developers to seamlessly integrate other frameworks, providing unparalleled flexibility and versatility in web development. This unique approach empowers developers to leverage the strengths of multiple frameworks and create truly dynamic and powerful websites. James shares his thoughts on Astro’s new Qwik integration, highlighting its potential impact on web development workflows. Qwik integration opens up new possibilities and advantages for developers, streamlining their development process and enabling them to build faster and more efficiently. As web development continues to evolve, Astro stands as a powerful tool for developers seeking flexibility, performance, and enhanced user experiences. Listen to the full podcast episode here: https://modernweb.podbean.com/e/jamesquick/...

2 mins
AstroJavaScript

Upgrading from Astro 2 to Astro 4

Astro has released version 4 just a few months after launching version 3. Here’s are the most important new features to know about if you haven’t upgraded from v2 yet....

Tom VanAntwerp3 mins
Astro

Leveraging Astro's Content Collections

Astro’s content focused approach to building websites got a major improvement with their v2 release. If you’re not familiar with Astro, it is web framework geared towards helping developers create content rich websites that are highly performant. They enable developers to use their favorite UI framework to build components leveraging an islands architecture, and provide the end user with just the minimal download needed to interact with the site and progressively enhance the site as needed. Astro is a fantastic tool for building technical documentation sites and blogs because it provides markdown and MDX support out of the box, which enables a rich writing experience when you need more than just your base markdown. The React Docs leverage MDX help the documentation writers provide the amazing experience we’ve all been enjoying with the new docs. In Astro v2, they launched Content Collections, which has significantly improved their already impressive developer experience (DX). In this post, we’re going to look into how Astro (and other frameworks) managed content before Content Collections, what Content Collections are, and some of the superpowers Content Collections give us in our websites. How Content is Managed in Projects? A little bit of history… Content management for websites has always been an interesting challenge. The question is typically: where should I store my content and manage it? We have content management systems (CMS), like WordPress, that people have historically and currently use to quickly build out websites. We also have Headless CMS like Contentful and Sanity that enable writers to enter their content, and then bring on developers to build out the site utilizing modern web frameworks to display content. All these solutions have enabled us to manage our content in a meaningful way, especially when the content writers aren’t developers or technical content writers. However, these tools and techniques can be limiting for writers who want to use rich content objects. For example, in the React Docs, they use Sandpack to create interactive code samples. How can we achieve these same results in our projects? The Power of MDX This is where MDX comes in. We can create re usable markdown components that allow writers to progressively enhance their blog posts with interactive elements without requiring them to write custom code into their article. In the example below, we can see the HeaderLink component that allows the writer to add a custom click handler on the link that executes a script. While this is a simple example, we could expand this to create charts, graphs, and other interactive elements that we normally couldn’t with plain markdown. Most CMS systems haven’t been upgraded to handle MDX yet, so to provide this type of experience, we need to provide a good writing experience in our codebases. The MDX Experience Before Content Collections, we had two main approaches for structuring content in our projects. The first was to write each new document as a markdown or MDX page in our pages directory, and allow the file system router to handling the routing and define pages for us. This makes it easy to map the blog post to a page quickly. However, this leads to a challenge of clutter where, as our content grows, our directory will grow. This can make it harder to find files or articles unless a clear naming convention is utilized which can be hard to enforce and maintain. It also mixes our implementation details and content documents which can cause some organizational mess. The second approach is to store our content in a separate directory, and then create a page to collect the data out of this directory and organize it. This is the approach the React Docs take. This model has the clear advantage that the content and implementation details are separated. However, in these models, the page responsible for bringing the content together becomes a glue file trying to do file system operations, and joining data, in a logical way. This can be very brittle as any refactor could cause breakage in this model. Astro enables doing this using their Astro.glob API, but it has some limitations we’ll go over a little later. So… What Are Content Collections? Content Collections enable you to better manage content files in your project. They provide a standard for organizing content, validating aspects of the content, and providing some type saftey features to your content. Content Collections took the best parts of the separate directory approach, similar to the React Docs, and did their best to eliminate all the cons of this approach. You can leverage Content Collections by simply moving your content into the src/content directory of your project under a folder of the type of content it represents. Is it a blog post? Stick it in blog. Working with a newsletter? Toss it in newsletter. These folders are the “collections” . You can stick either .md or .mdx files in these folders, and those are your “content entries” . Once your content is in this structure, you can now use Astro’s new content APIs to query your data out in a structured way, and start using its superpowers. Supercharging your Content! Query Your Content like a Database Astro’s content API provides two functions: getCollection() and getEntryBySlug() for querying your data. getCollection() has 2 arguments: the collection name and a filter function. This enables you to fetch all the content in a collection and filter to only specific files/entries based on parameters in the files frontmatter of your choosing. getEntryBySlug() takes in the collection name and file slug and returns the specific requested file. What’s particularly meaningful about these functions is that they return content with full TypeScript typings so you can validate your entries. You don’t need to write file system connecting logic and manage it yourself anymore. Configuring Content Entry Types Collection entries can be configured to meet specific requirements. In src/content/config.ts, you can define collections and their schemas using Zod and then registering those with the framework as demonstrated below. This is extremely powerful because now Astro can handle validating our markdown to ensure all the required fields are defined, AND it returns those entities in their target format through the content API. When you used the Astro.glob API, you would get all frontmatter data as strings or numbers requiring you to parse your data for other standard primitives. With this change, you can now put dates into your frontmatter and get them out as date objects via the content API. You can now remove all your previous validation and remapping code and convert it all to Zod types in your collection config. But instead of having to run linters and tests to find the issues, the Astro runtime will let you know about your collection errors as you’re creating them through your IDE, or server runtime. Content Collection Gotchas Content collections can only be top level folders in the src/content directory. This means you can’t nest collections. However, you can organize content within a collection using subdirectories, and use the filtering feature of the content API to create sub selections. The main use case for this would be i18n translations for a collection. You can place the content collections in a directory for that language, and use the filter function to select those at runtime for display. The other main "gotcha" is routing. Before, we were leveraging the file based router to handle rendering our pages. But now, there are no explicit routes defined for these pages. In order to get your pages to render properly, you’ll need to leverage Astro’s dynamic route features to generate pages from your entries. If you’re in static mode (the default), you need to define a getStaticPaths() function on your specified catch all route. If you’re in SSR mode, you’ll need to parse the route at runtime, and query for the expected data. Some Notes on Migrating from File Based Routing If you had a project using Astro before v2, you probably want to upgrade to using content collections. Astro has a good guide on how to accomplish this in their docs. There’s two main gotchas to highlight for you. The first is that layouts no longer need to be explicitly defined in the markdown files. Because you’re shifting content to use a specified layout, this property is unnecessary. However, if you leave it, it will cause the layout to be utilized on the page causing weird double layouting , so be sure to remove these properties from your frontmatter. The second is that the content API shifts the frontmatter properties into a new data property on the return entries. Before, you might have had a line of code like post.frontmatter.pubDate. This now needs to be post.data.pubDate. Also, if this was a stringified date before, you now need to stringify the date to make it behave properly, e.g. post.data.pubDate.toDateString(). Finally, you can remove any custom types you made before, because now you can get those directly from your collection config. In summary… Astro Content Collections are a great way to manage your content and websites, especially if they’re content focused and rich. I’ve put together some code demonstrating all the patterns and techniques described in this post that you can check out here. At This Dot, we love utilizing the right tool for the right job. Astro is increasingly becoming one of our favorites for content site projects. We use it for our open source projects framework.dev and starter.dev and are always considering it for additional projects....

Dustin Goodman8 mins
AstroJavaScript

Using Astro on framework.dev

Have you heard of Astro? It's an exciting, if still experimental, static site generation framework that allows you to author components using your choice of framework and completely control if and when javascript is shipped to the client. If you would like to learn more, I wrote an introductory blog post about it. Considering how new and experimental Astro is, you might be wondering what it's like to actually try to build a website with it. Well, you're in luck because we chose to build react.framework.dev in Astro, and I'm here to tell you how it went. Some background When I was first presented the pitch for the framework.dev project, it had the following characteristics: 1. It was going to be primarily a reference site, allowing users to browse static data. 2. It should be low cost, fast and accessible. 3. Because it was a small internal project, we were pretty free to go out on a limb on our technology choices, especially if it let us learn something new. At the time, I had recently heard exciting things about this new static site generator called "Astro" from a few developers on Twitter, and from cursory research, it seemed perfect for what we had planned. Most of the site was going to be category pages for browsing, with search existing as a bonus feature for advanced users. Astro would allow us to create the whole site in React but hydrate it only on the search pages, giving us the best of the static and dynamic worlds. This plan didn't quite survive contact with the enemy (see the "ugly" section below) but it was good enough to get the green light. We also picked vanilla extract as a styling solution because we wanted to be able to use the same styles in both React and Astro and needed said styles to be easily themeable for the different site variants. With its wide array of plugins for different bundlers, it seemed like an extremely safe solution. Being able to leverage Typescript type checking and intellisense to make sure theme variables were always referenced correctly was certainly helpful. But as you'll see, the journey was much less smooth than expected. The Good Astro did exactly what it had promised. Defining what static pages would be generated based on data, and what sections of those pages would be hydrated on the client, was extremely straightforward. The experience was very similar to using NextJS, with the same system of file system paths defining the routing structure of the site, augmented with a getStaticPaths function to define the generation of dynamic routes. However, Astro is easier to use in many ways due to its more focused feature set: Very streamlined and focused documentation. No potential confusion between static generation and server rendering. All code in Astro frontmatter is run at build time only, so it's much easier to know where it's safe to write or import code that shouldn't be leaked to the client bundle. No need to use special components for things like links, which greatly simplifies writing and testing components. Our Storybook setup didn't have to know about Astro at all, and in fact is running exactly the same code but bundling it with its own webpack based builder. Choosing what code should be executed client side is so easy that describing it is somewhat anticlimactic: However, this is a feature that is simply not present in any other framework, and it gives a site very predictable performance characteristics: each page loads only as much Javascript as has been marked as needing to be loaded. This means that each page can be profiled and optimized in isolation, and increases in complexity in other pages will not affect it. For example, we have kept the site's homepage entirely static, so even when it shares code with dynamic areas like search, the volume of that code doesn't impact load times. The Bad Although we were pleasantly surprised by how many things worked flawlessly despite Astro being relatively new, we were still plagued by a number of small issues: A lot of things didn't work in a monorepo. We had to revert to installing all npm libraries in the root package rather than using the full power of workspaces, as we had trouble with hoisting and path resolution. Furthermore, we had to create local shims for any component we wanted to be a hydration root, as Astro didn't handle monorepo imports being roots. We were hit multiple times with a hard to trace issue that prevented hydration from working correctly with a variety of errors mostly relating to node modules being left in the client bundle. The only way to fix this was to clear the Snowpack cache and build the site for production before trying to start the dev server. You can imagine how long it took to figure out this slightly bizarre workaround. Astro integration with Typescript and Prettier was pretty shaky, so the experience of editing Astro components was a bit of a throwback to the days before mature Javascript tooling and editor integrations. I'm very thankful that we had always intended to write almost all of our components in React rather than Astro's native format. We also hit a larger hurdle that contributed to the above problems' remaining issues for the lifetime of the project: Astro moved from Snowpack to Vite in version 0.21, but despite vanilla extract having a Vite plugin, we were unable to get CSS working with the new compiler. It's uncertain whether this is an issue with Astro's Vite compiler, or whether it's down to vanilla extract's Vite plugin not having been updated for compatibility with Vite's new (and still experimental) SSR mode that Astro uses under the hood. Whatever the reason, what we had thought was a very flexible styling solution left us locked in version 0.20 with all its issues. The lesson to be learnt here is probably that when dealing with new and untested frameworks it's wise to stick to the approaches recommended by their authors, because there's no guarantees of backwards compatibility for third party extensions. The Ugly As alluded to in the introduction, our plans for framework.dev evolved in ways that made the benefits of Astro less clear. As the proposed design for the site evolved, the search experience was foregrounded and eventually became the core of every page other than the homepage. Instead of a static catalogue with an option to search, we found ourselves building a search experience where browsing was just a selection of pre populated search terms. This means that, in almost all pages, almost all of the page is hydrated, because it is an update results as you type search box. In these conditions, where most pages are fully interactive and share almost all of their code, it's arguable that a client side rendering SPA like you'd get with NextJS or Gatsby would be of equal or even superior performance. Only slightly more code would have to be downloaded for the first view and subsequent navigation would actually require less markup to be fetched from the server. The flip side of choosing the right tool for the job is that the job can often change during development as product ideas are refined, and feedback is incorporated. However, even though replacing Astro with NextJS would be fairly simple since the bulk of the code is in plain React components, we decided against it. Even Astro's worst case is still quite acceptable, and maybe in the future, we will add features to the site which will be able to truly take advantage of the islands of interactivity hydration model. Closing thoughts Astro's documentation does not lie. It is a very flexible and easy to use framework with unique options to improve performance that is also still firmly in an experimental stage of its development. You should not use it in large production projects yet. The ground is likely to shift under you as it did with us in the Snowpack to Vite move. The team behind Astro is now targeting a 1.0 release which will hopefully mean greater guarantees of backwards compatibility and a lack of major bugs. It will be interesting to see what features end up making it in, and whether developer tools like auto formatting, linting and type checking are going to also be finished and supported going forward. Without those quality of life improvements, Astro has still been an exciting tool to test out, which has made me think differently about the possibilities of static generation. With them, it might become the only SSG framework you will need....

Jae Anne Bach Hardie6 mins
AstroJavaScriptReact