Web development has come a long way from the early days of static, single-page personal websites. Now, we have a plethora of different languages, frameworks, and content management systems to choose from that have been created to cater to every conceivable niche.

That’s where Astro comes in, one of the latest cool kids on the JavaScript framework block.

Created by Fred K. Schott and a group of other contributors, Astro has quickly become a favorite in the development community. It is an all-in-one framework that works a lot like a static site generator.

In this article, we will explain why so many developers like Astro and are picking it over other solutions. We’ll also walk you through how to build a markdown-based blog using the framework.

What Is Astro?

The Astro logo in black, showing
Astro

Astro, or Astro.js, is a popular static site generator conceived for those who want to create content-rich websites that run quickly and smoothly. Its lightweight nature, intuitive structure, and gentle learning curve make it attractive to developers of all experience levels.

Despite its small footprint, Astro comes with powerful tools that drastically increase your site’s flexibility, saving you hours in content and theme management. In addition, it gives developers the option of working with their preferred frameworks in conjunction with Astro — an appealing prospect for seasoned coders who already have a host of favorites.

Here are just a few of the ways Astro stands out from the crowd:

  • Island architecture: Astro extracts your user interface (UI) into smaller, isolated components known as “Astro Islands” that can be used on any page. Unused JavaScript is replaced with lightweight HTML.
  • Zero JavaScript (by default): While you can use all the JavaScript you want to create your websites, Astro will attempt to deploy zero JavaScript to production by transcribing your code for you. This a perfect approach if your focus is on site speed.
  • SSG and SSR included: Astro started as a static site generator, but along the way, it became a framework that uses both static site generation (SSG) and server-side rendering (SSR). And you can pick which pages will use which approach.
  • Framework-agnostic: When using Astro, you can use any JavaScript framework you like — even multiple frameworks at once. (We’ll discuss this in greater detail later in this article.)

What’s more, Astro is edge-ready, meaning it can be deployed anywhere, anytime, with ease.

Ready to learn more? Then let’s dig deeper into how Astro works.

Astro’s Structure

Before we venture any further, it’s important to understand how Astro is set up so you can use it effectively. Let’s take a look at Astro’s core file structure:

├── dist/
├── src/
│   ├── components/
│   ├── layouts/
│   └── pages/
│       └── index.astro
├── public/
└── package.json

As you can see, the structure itself is quite simple. However, there are some key points you should remember:

  • Most of our project lives in the src folder. You can arrange your components, layouts, and pages into subfolders. You may add additional folders to make your project easier to navigate.
  • The public folder is for all the files that live outside of the build process, such as fonts, images, or a robots.txt file.
  • The dist folder will contain all the content you want to deploy on your production server.

Next, let’s dive deeper into Astro’s main constituents: components, layouts, and pages.

Components

Components are reusable chunks of code that can be included all over your website, similar to shortcodes in WordPress. By default, they have the file extension .astro, but you can also use non-Astro components built with Vue, React, Preact, or Svelte.

The following is an example of what a simple component looks like — in this case, a classed div tag containing an h2:

<!-- src/components/Kinsta.astro -->
<div class="kinsta_component">
    <h2>Hello, Kinsta!</h2>
</div>

And here’s how we can incorporate that component into our site:

---
import KinstaComponent from ../components/Kinsta.astro
---
<div>
    <KinstaComponent />
</div>

As demonstrated above, you first have to import the component. Only then can it be included on the page.

Now it’s time to add some properties to our component. Let’s start with a {title} property:

---
const { title = 'Hello' } = Astro.props
---

<div class="kinsta_component">
    <h2>{title}</h2>
</div>

And here’s how our property would be implemented:

---
import KinstaComponent from ../components/Kinsta.astro
---

<div>
    <!-- This shows "Good day" -->
    <KinstaComponent title="Good day"/>

    <!-- This shows "Hello" -->
    <KinstaComponent />
 </div>

Simple, right?

As you’ve probably already realized, the real power of Astro’s components is in their global and reusable nature. They enable you to make sweeping changes to your entire site by editing only a few lines of code, which can save you countless hours that would otherwise be spent on tedious, painstaking text replacements.

Layouts

Now, let’s talk about layouts. In addition to their familiar thematic function, layouts in Astro are also reusable components, but they’re employed as code wrappers.

Take a look at this example:

---
// src/layouts/Base.astro
const { pageTitle = 'Hello world' } = Astro.props
---

<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width">
    <title>{pageTitle}</title>
</head>
<body>
    <main>
        <slot />
    </main>
</body>
</html>

Note the <slot /> tag here. The <slot /> element in Astro acts as a placeholder for actual HTML tags and content.

Let’s see it in action.

The code below shows our <slot /> tag getting replaced with our desired code, all of which is wrapped by our Base.astro layout:

---
import Base from '../layouts/Base.astro';
---

<Base title="Hello world">
    <div>
        <p>Some example text.</p>
    </div>
</Base>

As you can see, our <slot /> tag was replaced by the HTML it represents, which is:

<div>
    <p>Some example text.</p>
</div>

As you can see, layouts, like components, allow you to reuse chunks of code across your site, simplifying the challenge of updating your global content and design.

Pages

Pages are a special type of component that is responsible for routing, data loading, and templating.

Astro uses file-based routing to generate pages, rather than dynamic routing. Not only does the file-based method consume less bandwidth, but it also saves you from having to import your components manually.

Here’s an example of defined routes:

src/pages/index.astro => yourdomain.com
src/pages/test.astro => domain.com/test
src/pages/test/subpage => domain.com/test/subpage

With these routes, our resulting homepage would be rendered as follows:

<!-- src/pages/index.astro -->
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width">
    <title>Hello World</title>
</head>
<body>
    <h1>Hello, Kinsta</h1>
</body>
</html>

But we already know how to use layouts, so let’s convert this into something that’s globally accessible:

---
import Base from '../layouts/Base.astro';
---

<Base>
    <h1>Hello, Kinsta</h1>
</Base>

There – that’s much cleaner.

We’ll discuss routing in Astro in more detail later in this article, but for now, let’s move on to the fun stuff: site construction and customization.

Customizing and Extending Astro

It’s time to learn how to customize your Astro site! We’re going to use Markdown collections, routing, image handling, and an integration with React to build out and personalize our static site.

Markdown Collections

With version 2.0, Astro introduced a much better way to maintain Markdown content than before. Thanks to collections, we can be sure that all our frontmatter data is included and has the correct type of association.

Lately, in version 2.5, they added a possibility to also manage JSON and YAML files as collections.

Ready to get your hands dirty?

First, put all your Markdown articles in the src/content/collection_name folder. We’re going to create a blog collection for this project, so in our demonstration, the folder will be src/content/blog.

Now it’s time to define all the required frontmatter fields in our src/content/config.ts file. Our blog will need the following:

  • title (string)
  • tags (array)
  • publishDate (time)
  • image (string, optional)

This is what it all looks like put together:

import { z, defineCollection } from 'astro:content';

const blogCollection = defineCollection({ 
    schema: z.object({
        title: z.string(),
        tags: z.array(z.string()),
        image: z.string().optional(),
        publishDate: z.date(),
    }),
});

export const collections = {
    'blog': blogCollection,
};

And this is what our article-about-astro.md Markdown file contains:

---
title: Article about Astro
tags: [tag1, tag3]
publishDate: 2023-03-01
---
## Tamen risit

Lorem *markdownum flumina*, laceraret quodcumque Pachyne, **alter** enim
cadavera choro.

True, there’s nothing special about our Markdown file. But there’s some hidden magic here that will manifest if we make a typo.

Let’s say, for instance, that instead of typing publishDate, we accidentally typed publishData. In the case of a misspelling like this, Astro will throw an error:

blog → article-about-astro.md frontmatter does not match collection schema.
  "publishDate" is required.

Amazing, right? This nifty feature can help us find errors relating to frontmatter in a matter of seconds.

The last thing we need to add is a page showing our data. Let’s create a file at src/page/blog/[slug].astro with the following code:

---
import Base from '../../layouts/Base.astro';
import { getCollection } from 'astro:content';
export async function getStaticPaths() {
    const blogEntries = await getCollection('blog');
    return blogEntries.map(entry => ({
        params: { slug: entry.slug }, props: { entry },
  }));
}
const { entry } = Astro.props;
const { Content } = await entry.render();
---
<Base>
    <h1>{entry.data.title} </h1>
    <Content />
</Base>

Thanks to getStaticPaths, Astro will create all the static pages for each post in the blog collection.

The only thing we’re missing now is a listing of all our articles:

---
import Base from '../../layouts/Base.astro';

import { getCollection } from 'astro:content';
const blogEntries = await getCollection('blog');
---
<Base>
<ul>
    {blogEntries.map(item => <li> <strong><a href={'/blog/' + item.slug}>{item.data.title}</a></strong></li>)}
</ul>
</Base>

As you can see, using collections makes this task remarkably simple.

Now, let’s create a data type collection. First, we must open the src/content/config.ts file again and add a new data collection:

import { z, defineCollection, referenece } from 'astro:content';

const blogCollection = defineCollection({ 
	type: 'content',
    schema: z.object({
        title: z.string(),
        tags: z.array(z.string()),
        image: z.string().optional(),
        publishDate: z.date(),
	    author: reference('authors')
    }),
});

const authorsCollection = defineCollection({ 
	type: 'data',
    schema: z.object({
        fullName: z.string(),
        country: z.string()
    }),
});


export const collections = {
    'blog': blogCollection,
'authors': authorsCollection,
};

Apart from creating a new collection, we also added the author reference in the blogCollection.

Time to create a new author. We must create a file called maciek-palmowski.json in the content/authors.json:

{
    "fullName": "Maciek Palmowski",
    "country": "Poland"
}

The last thing left is to grab this data in our Post. To do so, we’ll need to use getEntry:

---
import Base from '../../layouts/Base.astro';
import { getCollection, getEntry } from 'astro:content';
export async function getStaticPaths() {
  const blogEntries = await getCollection('blog');
  return blogEntries.map(entry => ({
    params: { slug: entry.slug }, props: { entry },
  }));
}
const { entry } = Astro.props;
const author = await getEntry(entry.data.author);
const { Content } = await entry.render();
---
<Base>
<h1>{entry.data.title}</h1>
<h2>Author: {author.data.fullName}</h2>
<Content />
</Base>

Routing

Astro has two different routing modes. We already learned about the first – static (file-based) routing – when we covered pages earlier.

Now we’re going to shift our focus to dynamic routing.

Using dynamic route parameters, you can instruct an Astro page file to automate the creation of multiple pages with the same structure. This is useful when you have a lot of one particular type of page (think author bios, user profiles, documentation articles, and so on).

For this next example, we’ll work on generating bio pages for our authors.

In Astro’s default static output mode, these pages are generated at build time, meaning you must predetermine the list of authors that get a corresponding file. In dynamic mode, on the other hand, pages are generated upon request for any route that matches.

If you want to pass a variable as your filename, add brackets around it:

pages/blog/[slug].astro -> blog/test, blog/about-me 

Let’s dive deeper into this using the code from our src/page/blog/[slug] file:

---
import Base from '../../layouts/Base.astro';
import { getCollection } from 'astro:content';
export async function getStaticPaths() {
    const blogEntries = await getCollection('blog');
    return blogEntries.map(entry => ({
        params: { slug: entry.slug }, props: { entry },
  }));
}
const { entry } = Astro.props;
const { Content } = await entry.render();
---
<Base>
    <h1>{entry.data.title}</h1>
    <Content />
</Base>

The getStaticPaths route is responsible for generating all the static pages. It returns two objects:

  • params: Used to fill the brackets in our URLs
  • props: All the values we’re passing to the page

And with that, your page generation is taken care of.

Image Handling

We can’t talk about performant websites without bringing up modern image formats, correct resizing methods, and lazy loading.

Luckily, Astro’s got us covered here, too. Thanks to the @astrojs/image package, we can introduce all the above in a matter of minutes.

After installing the package, we gain access to two components: Image and Picture.

The Image component is used to create an optimized <img /> tag. Here’s an example:

---
import { Image } from '@astrojs/image/components';
import heroImage from '../assets/hero.png';
---

<Image src={heroImage} format="avif" alt="descriptive text" />
<Image src={heroImage} width={300} alt="descriptive text" />
<Image src={heroImage} width={300} height={600} alt="descriptive text" />

Similarly, the Picture component creates an optimized <picture/> component:

---
import { Picture } from '@astrojs/image/components';
import hero from '../assets/hero.png';
---
<Picture src={hero} widths={[200, 400, 800]} sizes="(max-width: 800px) 100vw, 800px" alt="descriptive text" />

SSG vs SSR

By default, Astro runs as a static site generator. This means that all the content is converted to static HTML pages.

While this is a perfect approach from many perspectives (especially speed-related), we might sometimes prefer a more dynamic approach. If you want a separate profile page for each user, for example, or if you have thousands of articles on your site, re-rendering everything each time would be far too time-consuming.

Luckily, Astro also can work as a fully server-side-rendered framework instead or in a hybrid mode between the two.

To enable side-wide SSR, we need to add the following code to astro.config.mjs:

import { defineConfig } from 'astro/config';

export default defineConfig({
    output: 'server'
});

This is the standard approach.

The hybrid approach means that by default, everything is dynamically generated, apart from the pages with export const prerender = true added.

With Astro 2.5, there is also the possibility to set static rendering as default and select dynamic routes manually.

Thanks to those, we can, for example, create a fully statically generated website with dynamic login and profile pages. Neat, right?

You can read more about this in the official documentation.

Integrating Other JavaScript Frameworks

Another amazing feature of Astro allows you to bring along your favorite framework and use it in concert with Astro. You can mix Astro with React, Preact, Svelte, Vue, Solid, or Alpine (for all integrations, see Astro’s “Add Integrations” documentation).

Let’s say we want to use React. First, we need to install the integration by running the following in npm:

npx astro add react

Now that React has been integrated, we can create a React component. In our case, it will be the counter component at src/components/ReactCounter.tsx:

import { useState } from 'react';

/** A counter written with React */
export function Counter({ children }) {
    const [count, setCount] = useState(0);
    const add = () => setCount((i) => i + 1);
    const subtract = () => setCount((i) => i - 1);

    return (
        <>
            <div className="counter">
                <button onClick={subtract}>-</button>
                <pre>{count}</pre>
                <button onClick={add}>+</button>
                </div>
            <div className="counter-message">{children}</div>
        </>
    );
}

Last but not least, we need to place the counter on our page with the following code:

---
import * as react from '../components/ReactCounter';
---
<main>
    <react.Counter client:visible />
</main>

And voilà: Your React component has been integrated seamlessly into your site.

How To Deploy Astro Using Kinsta

Now it’s time to get our Astro site onto the web. Luckily, Kinsta is the perfect host for quick and painless Static Site Hosting.

Start by creating a GitHub repository for your site’s files. If you’re not ready to use your own files, you can clone this Astro starter site template our team created.

Once your repo is ready, follow these steps to deploy your static site to Kinsta:

    1. Login or create an account to view your MyKinsta dashboard.
    2. Authorize Kinsta with your Git provider.
    3. Click Static Sites on the left sidebar, then click Add site.
    4. Select the repository and the branch you wish to deploy from.
    5. Assign a unique name to your site.
    6. Add the build settings in the following format:
      • Build command: npm run build
      • Node version: 18.16.0
      • Publish directory: dist
    7. Finally, click Create site.

And that’s it! You now have a live, fully functioning static site created with the Astro framework.

A dark page with the Kinsta logo in white in the center above the words
Our live Astro homepage

You can find your live URL and other deployment details in the Deployments tab.

As an alternative to Static Site Hosting, you can opt for deploying your static site with Kinsta’s Application Hosting, which provides greater hosting flexibility, a wider range of benefits, and access to more robust features. For example, scalability, customized deployment using a Dockerfile, and comprehensive analytics encompassing real-time and historical data.

Summary

Astro’s clear structure, simple syntax, and global components make building and running an application really easy. , Its lightweight nature and dual use of static and dynamic routing dramatically increase site responsiveness, while its capacity for cooperating with and alongside other JavaScript frameworks makes it all the more appealing to experienced coders.

If your goal is to create a content-rich site that loads quickly, grants modular functionality, and provides both static and dynamic generation, then Astro could be the right choice for you.

You can host your static website with Kinsta’s Static Site Hosting for free.

What are your thoughts on the Astro static site generator? Have you used it on a project of your own? Let us know in the comments section below.

Maciek Palmowski

Maciek is a web developer working at Kinsta as a Development Advocate Analyst. After hours, he spends most of his time coding, trying to find interesting news for his newsletters, or drinking coffee.