Sean Davis Tinkerer, teacher, sandwich-eater.

Astro: Build faster apps with less JavaScript

8 min read 2242

Astro: The Cool Parts

Astro is the newest static site generator on the scene. And, it’s cool! I’m excited about Astro because of how it views its role as a framework. It seems to have a balanced approach to the opinions and abilities it offers.

Rather than offering super strong opinions (think: Next.js’s page routing, Gatsby’s GraphQL layer), Astro takes many of the popular approaches to common patterns and provides a foundation on which developers can build. Instead of saying, “Do it yourself,” Astro says, “Here are some common options today. Pick one (or more).”

You can read more about why I’m so excited. Also, take a look at Astro’s announcement if you haven’t read it yet. It’s some great background on the tool.

What is Astro?

Astro is new to the scene, but there are already a handful of tutorials on it. Let’s not make this yet another one. Instead, let’s explore the cool parts of what makes Astro so exciting by going through an example.

We’re going to take the default starting point for Astro projects and turn it into a simple one-page website that lists some fun facts about rockets! Why? Because Astro and LogRocket go together like … avocado and toast?

WARNING! Not Production Ready
You’re at the forefront of the Astro journey — it’s not technically ready for production yet. That’s likely still a ways out, but it’s solid enough to explore. And if you like it enough, sure, live on the edge and take the thing to production.

Getting started with Astro

Begin by getting this thing set up. Make a new directory for your project and navigate to it:

mkdir astro-the-cool-parts
cd astro-the-cool-parts

Then you can start a new Astro project:

npm init astro

This will walk you through a few questions. Here are the answers I gave:

  • y to confirm
  • Starter Kit (Generic) as the starting point
  • React and Svelte for my components

Once you’re through that step, install the dependencies:

We made a custom demo for .
No really. Click here to check it out.

npm install

Then you can start the server:

npm start

Now, visit localhost:3000 in your web browser to show the default starting point.

Browser Page Welcoming User To Astro Project
Default Astro starting point.

Creating the static build

I recommend exploring the contents of this project. It provides a good base for showing what Astro can do. However, we’re going to rip most of that out for our example.

The pages for Astro projects are in the src/pages directory. The homepage is src/pages/index.astro. I removed most of the default content and replaced it with fun facts and a few inline styles to support. Here’s what my file looks like.

Browser Page Displaying Fun Facts About Rockets
Rocket fun facts, using styles from Astro’s default starter.

Now we’re going to build the project and inspect what happens. Run the build:

npm run build

The output files are in the dist directory. Notice there’s no JavaScript!

Adding components to Astro

Let’s add a component to represent a fun fact. Create a new file at src/components/FunFact.jsx with the following content:

import styles from "./FunFact.module.css";

export default function FunFact({ children, heading, sourceUrl }) {
  return (
    <div className={styles.wrapper}>
      <h2 className={styles.heading}>{heading}</h2>
      <p className={styles.fact}>{children}</p>
      <p>
        <a href={sourceUrl}>Source</a>
      </p>
    </div>
  );
}

Note: this is a React component. You’re welcome to use a different framework. Change the syntax, file extensions, and imports as needed.

Notice that we imported a file for styles (using CSS Modules) that doesn’t exist yet. Let’s create it now at src/components/FunFact.module.css and drop in these styles:

.wrapper {
  margin-bottom: 2rem;
}

.heading {
  margin-bottom: 0.5rem;
  font-size: 1.4rem;
}

.fact {
  font-size: 1rem;
  line-height: 1.5;
  margin-bottom: 0.5rem;
}

.source {
  font-size: 0.75rem;
}

Next, let’s rearrange index.astro to use the components.

At the top of the file, import our new component:

import FunFact from '../components/FunFact.jsx'
// ...

Then adjust the main body content to use the <FunFact /> component:

<main>
  <FunFact
    heading="Rockets can reach speeds of 22,000 miles per hour!"
    sourceUrl="https://www.ulalaunch.com/explore/rocket-science/fun-facts">
      A typical rocket produces more than a million pounds of thrust that
      allows it to carry more than 6,000 pounds at speeds topping 22,000
      miles per hour. This is equivalent to the power generated by 13 Hoover
      Dams, carrying the weight of eight horses, and traveling at speeds 15
      times faster than a speeding bullet!
  </FunFact>

  <FunFact
    heading="Prior to 1926, all rockets were powered by some form of gunpowder."
    sourceUrl="https://www.factsjustforkids.com/technology-facts/rocket-facts-for-kids/">
      The first rocket to use liquid fuel was created by Robert H. Goddard
      and first launched on March 16th, 1926.
  </FunFact>

  <FunFact
    heading="The first rocket in space was launched by Germany in 1942."
    sourceUrl="https://facts.net/science/technology/rocket-facts/">
      Germany launched the first rocket capable of reaching space in 1942.
      Dubbed the V-2 rocket, it was not actually intended for space travel.
      Instead, the V-2 was constructed as a ballistic missile during WWII.
      Nonetheless, it was revealed in a flight test to be the first man-made
      object to fly into space.
  </FunFact>

  <FunFact
    heading="The first rocket was invented in China around 1100 AD"
    sourceUrl="https://facts.net/science/technology/rocket-facts/">
      The rockets invented in the 10th century China used solid propellants
      and were mainly used as weapons and fireworks. It was not until the
      1920s that rocket science was studied further. By the 1930s and 1940s,
      professional rocket engineering started to take off.
  </FunFact>
</main>

Here you can see all the changes I made during this step.

When your browser looks good, run the build again (npm run build) and take a look at the dist directory.

Notice how minimal the changes were, and, more importantly, there’s still no JavaScript, even though we’ve included a React component!

Mixing frameworks

As a quick example, let’s say that another developer is more comfortable with Svelte, so they are going to build the header using Svelte.

Add a new component at src/components/Header.svelte with the following code from the home page:

<header>
  <div>
    <h1>🚀 Rocket Fun Facts 🚀</h1>
  </div>
</header>

<style>
  header {
    display: flex;
    flex-direction: column;
    gap: 1em;
    margin: 0 auto 3rem;
    max-width: min(100%, 68ch);
  }
</style>

Now, in index.astro, you can import the new header:

import Header from '../components/Header.svelte'

And use it in the homepage markup:

<head>
  <!-- ... -->

  <style>
    main {
      margin: 0 auto;
      max-width: 42rem;
    }
  </style>
</head>
<body>
  <Header />

  <!-- ... -->
</body>

Quick note: header styles are hanging out in public/style/home.css, which you could (and should) clean up if you were taking this to production. I’m leaving them where they are because this is a quick proof of concept.

If everything went well, there should be no changes to the result on the screen, as you’ve only done some reorganization.

Here are the changes I made, which again resulted in only minor build changes, leaving the dist directory nice and clean.

Data fetching with Astro

I love the way Astro approaches data fetching right at the top of component files. It even supports top-level await, which works to reduce the amount of boilerplate code you have to write.

I didn’t want to do anything too fancy here because we’re just playing around, so I dropped all the content into a JSON file at content/fun-facts.json. It looks like this:

[
  {
    "heading": "Rockets can reach speeds of 22,000 miles per hour!",
    "sourceUrl": "https://www.ulalaunch.com/explore/rocket-science/fun-facts",
    "body": "A typical rocket produces more than a million pounds of thrust that allows it to carry more than 6,000 pounds at speeds topping 22,000 miles per hour. This is equivalent to the power generated by 13 Hoover Dams, carrying the weight of eight horses, and traveling at speeds 15 times faster than a speeding bullet!"
  },

  {
    "heading": "Prior to 1926, all rockets were powered by some form of gunpowder.",
    "sourceUrl": "https://www.factsjustforkids.com/technology-facts/rocket-facts-for-kids/",
    "body": "The first rocket to use liquid fuel was created by Robert H. Goddard and first launched on March 16th, 1926."
  },

  {
    "heading": "The first rocket in space was launched by Germany in 1942.",
    "sourceUrl": "https://facts.net/science/technology/rocket-facts/",
    "body": "Germany launched the first rocket capable of reaching space in 1942. Dubbed the V-2 rocket, it was not actually intended for space travel. Instead, the V-2 was constructed as a ballistic missile during WWII. Nonetheless, it was revealed in a flight test to be the first man-made object to fly into space."
  },

  {
    "heading": "The first rocket was invented in China around 1100 AD",
    "sourceUrl": "https://facts.net/science/technology/rocket-facts/",
    "body": "The rockets invented in the 10th century China used solid propellants and were mainly used as weapons and fireworks. It was not until the 1920s that rocket science was studied further. By the 1930s and 1940s, professional rocket engineering started to take off."
  }
]

Then I committed and pushed the code, and generated a URL to that file using this service. This will make it feel like we’re fetching data from an API. You can follow the same process or just use the URL I generated.

Let’s begin by only fetching the content and logging the result to the console. Adjust the code at the top of your index.astro file:

const dataUrl = 'https://raw.githack.com/seancdavis/astro-the-cool-parts/77d3b5dd2ce2253c33d50fc91a21875f90a8ced5/content/fun-facts.json'
const response = await fetch(dataUrl);
const facts = await response.json();

console.log(facts);

Now, restart the server. Notice that the content is logged to the server and not in the browser. That’s because this code is run during the Astro build process, not when the page is loaded.

Handling iterable data in Astro
Astro components don’t have logic baked into the markup portion. So, rather than looping through the data returned from our fake API, we’ll create another component to handle the looping.

Add the component to src/components/FunFactList.jsx, like so:

import FunFact from "./FunFact";

export default function FunFactList({ facts }) {
  return (
    <>
      {facts.map((fact, idx) => (
        <FunFact key={idx} heading={fact.heading} sourceUrl={fact.sourceUrl}>
          {fact.body}
        </FunFact>
      ))}
    </>
  );
}

Notice all this does is take an array of data, loop through it, and generate individual <FunFact /> components.

Back in the index.astro file, change import to use the FunFactList component, not the FunFact component.

import FunFactList from '../components/FunFactList.jsx'

Then remove the console.log and replace your existing fun facts with the single FunFactList component:

<FunFactList facts={facts} />

Here are the component changes, and when building, there’s still no JavaScript! And we’re bringing in dynamic data!

Progressive enhancement with Astro

Of all the features Astro offers, progressive enhancement (or partial hydration) is perhaps the coolest. We can selectively tell Astro when to hydrate components to make them interactive.

Let’s say we want to collapse the fun facts by default and show them when clicking on the heading. We’ll also toggle an emoji to indicate when a particular fact is opened or closed.

Adjust the code in the fun fact component:

import { useState } from "react";
import styles from "./FunFact.module.css";

export default function FunFact({ children, heading, sourceUrl }) {
  const [expanded, setExpanded] = useState(false);

  let wrapperClasses = styles.wrapper;
  if (expanded) wrapperClasses += ` ${styles.wrapperExpanded}`;

  return (
    <div className={wrapperClasses}>
      <h2 className={styles.heading}>
        <button onClick={() => setExpanded(!expanded)}>
          <span>{expanded ? "🟣" : "⚪️"}</span>
          <span>{heading}</span>
        </button>
      </h2>
      <p className={styles.fact}>{children}</p>
      <p>
        <a href={sourceUrl}>Source</a>
      </p>
    </div>
  );
}

Let’s add a bit more styling:

.wrapper {
  margin-bottom: 2rem;
}

.wrapperExpanded .fact {
  display: block;
}

.heading {
  margin-bottom: 0.5rem;
  font-size: 1.4rem;
}

.heading button {
  background-color: inherit;
  border: inherit;
  color: inherit;
  display: inherit;
  font-size: inherit;
  line-height: inherit;
  margin: inherit;
  padding: inherit;
  text-align: inherit;
}

.heading button:hover {
  cursor: pointer;
}

.heading button span:first-child {
  display: inline-block;
  margin-right: 0.5rem;
}

.fact {
  display: none;
  font-size: 1rem;
  line-height: 1.5;
  margin-bottom: 0.5rem;
}

.source {
  font-size: 0.75rem;
}

Here’s the commit with these changes.

Now load the homepage in the browser. The styles are there, but nothing works. What the heck?

That’s because Astro’s interactivity is declarative. You have to opt into it using one of its client directives. Adjust the index.astro to add the client:visible directive when rendering the FunFactList component.

<FunFactList facts={facts} client:visible />

Reload the page and everything should now be working. Here is the commit.

There are a few different directives that can be used, and it’s all about timing. In this case, we used client:visible, which will make the component interactive only after it enters the viewport.

Now take a look at the dist directory. There’s a lot more going on, including some JavaScript files.

Conclusion

That was a quick exploration of the parts of Astro that I’m most excited about. Of course, there’s plenty more to explore, and Astro will continue to evolve. But there is a place for Astro in this already crowded ecosystem of static site generators, and I’m excited to put it into practice on my projects.

: Full visibility into your web apps

LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.

In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page apps.

.
Sean Davis Tinkerer, teacher, sandwich-eater.

Testing accessibility with Storybook

One big challenge when building a component library is prioritizing accessibility. Accessibility is usually seen as one of those “nice-to-have” features, and unfortunately, we’re...
Laura Carballo
4 min read

Leave a Reply