Static site generators improve performance and security of web pages. They can be cached and it’s easy to deploy them to a CDN (Content Delivery Network).

Static sites are an ideal candidate for web pages that don’t require highly dynamic content, for example, blogs or portfolio pages.

Astro is a new static site builder for JavaScript developers that is lightning fast.

In the future, I can see it as a replacement for tools like Hugo (written in Go) or Gatsby (JavaScript + React).

Here are some benefits of using Astro:

  • Bring Your Own Framework (BYOF): Build your site using React, Svelte, Vue, Preact, web components, or just plain ol’ HTML + JavaScript.
  • 100% Static HTML, No JS: Astro renders your entire page to static HTML, removing all JavaScript from your final build by default.
  • On-Demand Components: Need some JS? Astro can automatically hydrate interactive components when they become visible on the page. If the user never sees it, they never load it.
  • Fully-Featured: Astro supports TypeScript, Scoped CSS, CSS Modules, Sass, Tailwind, Markdown, MDX, and any of your favorite npm packages.
  • SEO Enabled: Automatic sitemaps, RSS feeds, pagination and collections take the pain out of SEO and syndication

Astro is still in beta, but let’s take a look at the current state of affairs.

Let’s Build A Static Site With Astro

1. Installation

You’ll need Node.js - v12.20.0, v14.13.1, v16.0.0, or higher.

In the command-line terminal:

mkdir <project-name>
cd <project-name>
npm init astro

Choose the blank starter project.

Follow the instructions to install Astro.

Run npm run start and the development server will start on port 3000 (navigate to http://localhost:3000 in your web browser).

2. Load Blog Posts from JSON Endpoint

We will now create dynamic pages from an API. The website JSONPlaceholder can give us 100 fake blog posts.

We need to use Astro collections to generate multiple pages from a single template.

If you use Astro, you need to follow their project structure. Pages belong in the src/pages folder.

Create a new file as src/pages/$posts.astro (the $-sign is important as it tells Astro that this file will create a collection):

// src/pages/$posts.astro
---
// Define the `collection` prop.
const { collection } = Astro.props;

// Define a `createCollection` function.
// In this example, we'll create a new page for every post.
export async function createCollection() {
	const allPostsResponse = await fetch('https://jsonplaceholder.typicode.com/posts');
  const allPosts= await allPostsResponse.json();
  return {
    // `routes` defines the total collection of routes as data objects.
    routes: allPosts.map((p) => {
      const params = {title: p.title, id: p.id};
      return params;
    }),
    // `permalink` defines the final URL for each route object defined in `routes`.
    permalink: ({ params }) => `/posts/${params.id}`,
    // `data` is now responsible for return the data for each page.
    // Luckily we had already loaded all of the data at the top of the function,
    // so we just filter the data here to group pages by first letter.
    // If you needed to fetch more data for each page, you can do that here as well.
    // Note: data() is expected to return an array!
    async data({ params }) {
      return [allPosts[params.id]];
    },
    // Note: The default pageSize is fine because technically only one data object
    // is ever returned per route. We set it to Infinity in this example for completeness.
    pageSize: Infinity,
  };
}
---
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>{collection.params?.title}</title>
    <link rel="icon" type="image/svg+xml" href="/favicon.svg">
    <link rel="stylesheet" href="/style/global.css">
    <link rel="stylesheet" href="/style/home.css">

    <style>
    </style>
</head>
<body>
    <main>
		<p>Post placeholder</p>
    </main>
</body>
</html>

Everything inside the --- dashes is server-side JavaScript. Here we use the JavaScript fetch API to load all blog posts from JSONPlaceholder.

If you open the browser on http://localhost:3000/posts/1 you’ll see Post placeholder, but not the content of the post.

Let’s remedy that and create a component for each blog post.

Create a new file: src/components/Post.astro:

// src/components/Post.astro
---

export interface Props {
  title: string;
  body: string
}

const {title, body } = Astro.props
---
<header>
    <div>
        <img width="60" height="80" src="/assets/logo.svg" alt="Astro logo">
        <h1>{title?.charAt(0).toUpperCase() + title?.slice(1)}</h1>
    </div>
</header>
<article>
    <section>
    </section>
	{body}
     <section>
    </section>
</article>

<style>
    header {
        display: flex;
        flex-direction: column;
        gap: 1em;
		max-width: 68ch;
    }
    article {
        padding-top: 2em;
        line-height: 1.5;
		max-width: 68ch;
    }
    section {
        margin-top: 2em;
        display: flex;
        flex-direction: column;
        gap: 1em;
        max-width: 68ch;
		word-wap: break-word;
    }
</style>

Here we define two props: title and body. If you take a look at https://jsonplaceholder.typicode.com/posts, you’ll see that each blog post has a title and the content as the body key.

The Post.astro file displays the title in the h1 HTML tag and the blog post content in the article HTML tag. There is some basic (scoped) CSS styling in the style tag. (Please don’t judge my CSS skills.)

Change the $posts.astro file to import our post template and use it to display the blog post:

// src/pages/$posts.astro

---
import Post from '../components/Post.astro'
// Define the `collection` prop.
const { collection } = Astro.props;

//
---
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>{collection.params?.title}</title>
    <link rel="icon" type="image/svg+xml" href="/favicon.svg">
    <link rel="stylesheet" href="/style/global.css">
    <link rel="stylesheet" href="/style/home.css">

    <style>
    </style>
</head>
<body>
    <main>
		<Post title={collection.params?.title} body={collection.data[0]?.body}/>
    </main>
</body>
</html>

(Complete file on GitLab).

Now go to http://localhost:3000/post/3 and you should see one of our fake blog posts.

You can also inspect the network tab in your browser. You’ll find no JavaScript (except the Astro hot reload development server which won’t exist in production).

Let’s Add React!

But what if we want to add a front-end framework like React or Vue? We can!

First, we need to add the React renderer to Astro. Open astro.config.mjs and add React to the renderers array:

// astro.config.mjs

export default {
  // projectRoot: '.',     // Where to resolve all URLs relative to. Useful if you have a monorepo project.
  // pages: './src/pages', // Path to Astro components, pages, and data
  // dist: './dist',       // When running `astro build`, path to final static output
  // public: './public',   // A folder of static files Astro will copy to the root. Useful for favicons, images, and other files that don’t need processing.
  buildOptions: {
    // site: 'http://example.com',           // Your public domain, e.g.: https://my-site.dev/. Used to generate sitemaps and canonical URLs.
    sitemap: true, // Generate sitemap (set to "false" to disable)
  },
  devOptions: {
    // port: 3000,         // The port to run the dev server on.
    // tailwindConfig: '', // Path to tailwind.config.js if used, e.g. './tailwind.config.js'
  },
  renderers: ['@astrojs/renderer-react'],
}

Stop your development server.
In your terminal, type npx astro --reload to make sure that Astro loads the new settings.

Let’s try to display toast messages with react-toastify.

We need to install the package with npm:

npm install --save react-toastify

Create a new Toast component in src/components/Toast.jsx. I copied the basic example from the documentation and renamed the component to Toast :

// src/components/Toast.jsx

import React from 'react'

import { ToastContainer, toast } from 'react-toastify'
import 'react-toastify/dist/ReactToastify.css'

export const Toast = () => {
  const notify = () => toast('Astro is awesome!')

  return (
    <div>
      <button style={{ marginTop: '2em', display: 'block' }} onClick={notify}>
        Click Me
      </button>
      <ToastContainer />
    </div>
  )
}

The component shows a basic button with an onClickHandler. If you’ve written React before, this should look familiar.

We want to add the toast button to our blog posts.

Open src/components/Post.astro, import the Toast component and add it to the markup:

// src/components/Post.astro

---
import { Toast } from './Toast.jsx'

//
---
<header>
    <div>
        <img width="60" height="80" src="/assets/logo.svg" alt="Astro logo">
        <h1>{title?.charAt(0).toUpperCase() + title?.slice(1)}</h1>
    </div>
</header>
<article>
    <section>
    </section>
	{body}
	<Toast/>
     <section>
    </section>
</article>

// styles

Open one of your blog posts in the brower and click on the ugly looking button.

Nothing happens!

That’s by design. Astro will render the HTML part but strips out all JavaScript.

In our case, we want to ship JavaScript, so we need to enable it. Astro offers partial hydration, so that we can load JavaScript sparingly for each component.

We’ll keep it simple and enable JavaScript as soon as the page loads. Change the <Toast /> markup to <Toast client:load />:

// src/pages/$posts.astro

//
---
//
<article>
    <section>
    </section>
	{body}
	<Toast client:load />
     <section>
    </section>
</article>

// styles
---

(Complete file on GitLab.)

If you click on the button now, it should show a sweet-looking toast message. Hooray!

Recap

In this article, we learned how to create a basic Astro website that loads dynamic data from an external API and uses a React component to enhance the page with JavaScript.

Astro is still in beta and a few things don’t work yet.

For example, in React you can use the children prop to enable composition. This helps building re-usable components. Astro does not allow you to do that and the team is unsure if they want to support it.

You can build the Astro page for deployment with the command npm run build. Currently Astro does not build our collection (blog post) pages. The collections API is still in flux; expect breaking changes.

Astro looks promising, but it’s still early days.