Let’s explore Astro’s core features

By Zjerilon Ferero

9 min read

When it comes to creating websites, performance is crucial, but how do you achieve a performant website? This is why Astro was created; let's look at how it works and why it's so effective.

Let’s explore Astro’s core features
Authors

Astro has been steadily increasing in popularity, but why? And what exactly is Astro? And how does it pile up against other web frameworks? Let's find out together by exploring Astro's core features.

What is Astro?

Astro is a comprehensive web framework that makes it simple to build content-rich, performance-oriented websites. Astro achieves this by delivering as minimal as possible JavaScript to the client; in contrast to popular modern web frameworks such as ReactJS, Vue, and Svelte, which are Single Page Applications (SPAs). Astro uses the Multi-Page Application (MPA) approach, and pages are therefore server side rendered. This methodology is followed by traditional server-side frameworks such as Laravel (PHP), Ruby on Rails, and so on.

How does Astro work?

Astro delivers zero JavaScript to the client by default, resulting in 100% static HTML and CSS for high-performance websites. But what if we need interactivity? This is where the concept of "Astro islands" comes into play.

What are Astro Islands?

Astro islands are UI components that are initially rendered server-side, and as developers, we choose which UI component should be hydrated. This allows us to keep the majority of the site static while adding interactive elements. The advantage of this approach in terms of performance is that we only ship JavaScript for the components that require it.

The architecture behind Astro Islands is called the Islands Architecture, whereby interactive UI components are hydrated with the partial/progressive hydration technique

example of an Astro islands

A prime example of the so-called "Astro islands" can be found in the image above; the Header, Sidebar, and Image Carousel are all "Astro islands" (interactive UI components) and are thus generated and hydrated individually.

Due to the fact that each Astro Island is separately hydrated, you could develop them using any popular UI Framework, such as ReactJS:

---
import MyReactComponent from '../components/MyReactComponent.jsx';
import AstroHeader from '../components/AstroHeader';
---
<html>
  <body>
    <AstroHeader/>
    <MyReactComponent />
  </body>
</html>

Astro has support for most popular UI Frameworks: ReactPreactSvelteVueSolidJSAlpineJS and Lit

It is even possible to combine multiple frameworks.

By default, the UI Framework component will render on the server, and thus be converted to static HTML.

If you want the component to be hydrated you have to pass a "Client Directive", Astro has different options to decide when a component should be hydrated:

// Load and hydrate the component JavaScript immediately on page load.
<MyCustomUIComponent client:load />

// Load and hydrate the component JavaScript once the page is done with its initial load
// and the requestIdleCallback event has fired.
<ShowHideButton client:idle />

//Load and hydrate the component JavaScript once the component has entered the user’s viewport.
//This uses an IntersectionObserver internally to keep track of visibility.
<HeavyImageCarousel client:visible />

// client:media={string} loads and hydrates the component JavaScript once
// a certain CSS media query is met.
<SidebarToggle client:media="(max-width: 50em)" />

//client:only={string} skips HTML server-rendering, and renders only on the client.
//It acts similar to client:load in that it loads, renders and hydrates the component immediately on page load.
<SomeReactComponent client:only="react" />

The two modes of Astro

Astro can be run in two different modes. Astro runs in “SSG” mode by default, but it is possible to switch to “SSR” mode.

Server-side rendering (SSR) is a method of rendering a website on the server before serving it to the client. This mode is great for websites that frequently updates for example a social media platform

Static site generation (SSG) is a method whereby a website is pre-generated at build time. Because of this, it’s better suited for sites that don’t require frequent updates for example a blog, because updates requires the website to be generated again.

For the sake of simplicity I wont go to deep into pro’s and cons of the two but its good to know that in both modes, render ready html is served to the client, causing it to be indexable by search engines and be more performant as opposed to the approach of modern frontend libraries, which serves minimal html to the client and then hydrates it with Javascript.

Astro vs NextJS

Astro is frequently compared to NextJS, but why would you choose Astro over the NextJS?

Flexibility

Next.js renders your website with React. Astro is more adaptable: you can create UI using any popular component library or Astro's HTML-like component syntax, as previously demonstrated.

Performance

Astro websites will typically load much faster than Next.js websites. This is due to the fact that Astro automatically removes unnecessary JavaScript from the page, hydrating only the individual components that require it (Partial Hydration).

Because NextJS does not yet support partial hydration, your website will be less performant because it will include a lot of unnecessary JavaScript.

Both NextJS and Astro are excellent website-building tools, however NextJS truly shines when it comes to applications, whereas Astro really shines when it comes to webpages with static content.

Enough talking, show some code

Astro has many amazing features, but in order to keep this blog post as interesting as possible, I'll go over the essential features required to build a website.

Astro components

The syntax of the Astro components is intended to be as straightforward as possible, and it is similar to writing plain HTML and Javascript.

---
// Import statements
import PageTitle from './PageTitle.astro';
import User from "../types.ts"

// If you are using Typescript you can define props and Astro will
// Automatically give type errors/warnings
interface Props {
    page: string;
    user?: User;
}

// Recieve props from parent component
const { page, user } = Astro.props;

const isUserLoggedIn = !!user

// Fetch data from an api.
// Since Astro Components are rendered at build time on a server
// its also possible to do a direct call to the database
// or to read from a file from the filesystem
const res = await fetch(`localhost:3000/pages/${page}`)
const contents = await res.json()

---

<main>
	<PageTitle>{Welcome to the {page} page</PageTitle>
	{isUserLoggedIn && <button>log out</button>}
	{contents.map(c => (<>{c}</>))}
<main/>

As seen on the example above Astro components exist of 2 parts:

The component script (---):

In this part you write your Javascript/Typescript that you need to render your component such as:

  • importing other Astro components / UI Framework components
  • fetching content from an API or database
  • creating variables that you will reference in your template
---
// Import statements
import PageTitle from './PageTitle.astro';
import User from "../types.ts"

// If you are using Typescript you can define props and Astro will
// Automatically give type errors/warnings
interface Props {
    page: string;
	user?: User;
}

// Recieve props from parent component
const { page, user } = Astro.props;

const isUserLoggedIn = !!user

// Fetch data from an api.
// Since Astro Components are server side rendered
// its also possible to do a direct call to the database
// or to read from a file from the filesystem
const res = await fetch(`localhost:3000/pages/${page}`)
const contents = await res.json()

---

The component template:

This is where you write your html, in this part you can reference variables, imports etc from the the component script. If you're familiar with ReactJS, this should feel familiar to JSX.

<main>
	<>
	<PageTitle>Welcome to the {page} page</PageTitle>
	{isUserLoggedIn && <button>log out</button>}
	</>
	{contents.map(c => (<>{c}</>))}
<main/>

The most important aspect of Astro components is that they render to HTML by default during the build process. Even if you run JavaScript code within your components, it will all be executed ahead of time and removed from the final HTML that you send to your users. As a result, the site loads faster and has no JavaScript footprint by default. If you need client side JavaScript you can make use of a <script/> tag in the component’s template

Adding interactivity

In AstroJS html attributes get converted to strings, therefore passing an object or a function to a HTML element wont work:

---
function handleClick () {
    console.log("button clicked!");
}
---
<!--This doesn't work!-->
<button onClick={handleClick}>Nothing will happen when you click me!</button>

Instead, use a client-side script to add the event handler, like you would in vanilla JavaScript:

---
---

<button id="button">Click Me</button>
<script>
  function handleClick() {
    console.log('button clicked!')
  }
  document.getElementById('button').addEventListener('click', handleClick)
</script>

Slots

The <slot /> element is a placeholder for external HTML content, allowing you to inject (or “slot”) child elements from other files into your component template.

---
import BlogPostTitle from './BlogPostTitle.astro';

const { title } = Astro.props
---
<div id="blog-post-wrapper">
  <BlogPostTitle title={title} />
  <slot name="introduction"/>  <!--  children with the `slot="introduction"` attribute will go here -->
  <hr/>
	<slot />  <!--  children without a `slot`, or with `slot="default"` attribute will go here -->
  <hr/>
  <slot name="final"/>  <!--  children with the `slot="final` attribute will go here -->
</div>

As seen in the example above, some slots have a name attribute; these are called "Named Slots" You can use Named Slots to specify which slot the content needs to be injected into.

---
import BlogPost from './BlogPost.astro';
---
<BlogPost title="Astro is awesome">
	<!-- This will get injected into the introduction slot -->
  <p slot="introduction">introduction text</p>

	<!-- this will be injected into the default slot, -->
	<!-- because we have not specified in which slot this should be injected -->
	<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed eu purus ac erat facilisis laoreet.</p>

	<!--  this will get injected into the final slots -->
	<p slot="final">Thank you for reading!</p>
</BlogPost>˜

As we can see, slots are quite helpful for reducing repetition in your code, and are typically use for defining a template.

Styling in Astro

Astro was created to make styling and CSS writing as simple as possible. You can write your own CSS directly inside an Astro component, import your preferred CSS library like Tailwind, or use advanced styling languages like Sass.

Using the <style/> tag, you can begin writing styles in your Astro component.

---
import BlogPostTitle from './BlogPostTitle.astro';
const { title } = Astro.props
---
<style>
	#blog-post-wrapper {
		padding: 10px
	}
	hr {
		color: #333;
	}
</style>

<div id="blog-post-wrapper">
  <BlogPostTitle title={title} />
  <slot name="introduction"/>
	<slot />
  <slot name="final"/>
</div>

Styles defined in an Astro component are automatically scoped to this component; in other words, the styles provided for the <hr/> tag in the example above will only be applied to this component.

But what if you require global styling? Here's where the "is:global" directive comes in handy. When a style tag is tagged as global it will apply the given styles to the entire website.

<style is:global>
  /* Unscoped, delivered as-is to the browser.
     Applies to all <h1> tags on your site. */
  h1 { color: red; }
</style>

Its also possible to use a mixture of global and scoped styling:

<style>
  /* Scoped to this component, only. */
  h1 { color: red; }
  /* Mixed: Applies to child `h1` elements only. */
  article :global(h1) {
    color: blue;
  }
</style>

If you want to assign multiple classes to an HTML element, you can use the "class:list" directive.

class:list takes an array of several different possible value kinds:

  • string: Added to the element class
  • Object: All truthy keys are added to the element class
  • Array: flattened
  • Set: flattened
---
const { title, isCentered } = Astro.props;
---
<!-- If `isCentered` is truthy, class will be "title center". -->
<h1 class:list={['title', { center: isCentered }]}>{title}</h1>

<style>
  .title {
		font-style: italic;
	}
  .center {
		text-align: center;
	}
</style>

In this instance, the h1 is given the title class, and the center class is only applied if the isCentered property is true.

Routing in Astro

Astro uses file-based routing to generate your routes URLs based on the files in your src/pages/ directory. When a file (.astro or .md) is added to the src/pages directory of your project, it is automatically available as a route based on its filename.

Static Routes

As mentioned before every .astro or .md file in the src/pages directory becomes a page on your website:

src/pages/index.astro        -> mysite.com/
src/pages/about.astro        -> mysite.com/about
src/pages/about/me           -> mysite.com/about/me

The content returned by each of these routes will always be the same because they are static. But what if we need the content to alter depending on a route parameter? dynamic routes solves this problem for us.

Dynamic Routes

An Astro page file can specify dynamic route parameters in its filename to generate matching pages. For example, you might create a file users/[userId].astro this will create a route for every specified userId.

Astro pages are constructed at build time by default; with this in mind, you must specify a list of userIds for Astro to generate the corresponding pages.

SSG Mode (default)

Because all pages are built at build time, dynamic routes must exports a getStaticPaths() function, which returns an array of objects with the param property. So if we create a new dynamic page users/[userId].astro that exports the following function:

---
export function getStaticPaths() {
  return [
    {params: {userId: '1'}},
    {params: {userId: '2'}},
    {params: {userId: '3'}},
  ];
}

const { userId } = Astro.params;
---
<div>Hey there, {userId}!</div>

Astro will generate 3 pages: sitename.com/users/1,sitename.com/users/2 and sitename.com/users/3

Astro will also expose the userId in the route parameter and this can then be used in the component template

If we want to take a more dynamic approach we can also call an API or do a direct database call, and pass data through as props

  ---
export async function getStaticPaths() {
  const users = await userService.getAllUsers();

  return users.map((user) => {
    return {
      params: { userId: users.id },
      props: { user },
    };
  });
}

const { id } = Astro.params;
const { user } = Astro.props;
---
<h1>{user.name}</h1>

SSR Mode

In this mode, dynamic routes are also defined by making a new file in the src/pages directory, for example: /src/pages/games/[gameId]. But since they are not built ahead of time, these pages don’t have to export the getStaticPaths function, and therefore any id passed as a route parameter will work.

---
const { resource, id } = Astro.params;
const user = await userService.getUserById(id)
---
<h1>{user?.name || "user not found"}</h1>

Endpoints

Endpoints can also be created with AstroJS to serve data; Static File and SSR endpoints are defined similarly, but SSR endpoints support additional features.

Static File Endpoints (SSG Mode)

To create a custom endpoint, add a .js or .ts file to the /pages directory. The extension will be removed during the build process, so the name of the file should include the extension of the data you want to create. For example, src/pages/data.json.ts will build a /data.json endpoint.

Endpoints export a get function that will receive a context object and it should return a object with a body property:

export const get: APIRoute = async ({ params, request }) => {
  const id = params.id
  const user = userService.getUserById(id)
  return {
    body: JSON.stringify({
      user,
    }),
  }
}

export function getStaticPaths() {
  return [{ params: { id: '0' } }, { params: { id: '1' } }, { params: { id: '2' } }]
}

This get function will be called at build time and use the content of this body property to generate the file. Since these files are getting created at build time we have to also return a getStaticPaths function incase we want to make use of params

Server Endpoints (SSR Mode)

Returning a static file is also conceivable in SSR mode, although in this case the endpoint will be built upon request, opening up new functionalities like listening to requests and running code.

Server endpoints can make use of params without the getStaticPath function and they can return a response object with allowing you to set status codes and headers:

export async function get({ params }) {
  const id = params.id
  const user = await getUserById(id)

  if (!user) {
    return new Response(null, {
      status: 404,
      statusText: 'Not found',
    })
  }

  return new Response(JSON.stringify(user), {
    status: 200,
    headers: {
      'Content-Type': 'application/json',
    },
  })
}

Endpoints can also return other HTTP methods functions for example create, update, del (delete).

Conclusion

If you believe Astro is deserving of its recent surge in popularity and want to give it a try, I would gladly refer you to their website and documentation at https://astro.build/.


Upcoming events

  • The Test Automation Meetup

    PLEASE RSVP SO THAT WE KNOW HOW MUCH FOOD WE WILL NEED Test automation is a cornerstone of effective software development. It's about creating robust, predictable test suites that enhance quality and reliability. By diving into automation, you're architecting systems that ensure consistency and catch issues early. This expertise not only improves the development process but also broadens your skillset, making you a more versatile team member. Whether you're a developer looking to enhance your testing skills or a QA professional aiming to dive deeper into automation, RSVP for an evening of learning, delicious food, and the fusion of coding and quality assurance! 🚀🚀 18:00 – 🚪 Doors open to the public 18:15 – 🍕 Let’s eat 19:00 – 📢 First round of Talks 19:45 – 🍹 Small break 20:00 – 📢 Second round of Talks 20:45 – 🍻 Drinks 21:00 – 🙋‍♀️ See you next time? First Round of Talks: The Power of Cross-browser Component Testing - Clarke Verdel, SR. Front-end Developer at iO How can you use Component Testing to ensure consistency cross-browser? Second Round of Talks: Omg who wrote this **** code!? - Erwin Heitzman, SR. Test Automation Engineer at Rabobank How can tests help you and your team? Beyond the Unit Test - Christian Würthner, SR. Android Developer at iO How can you do advanced automated testing for, for instance, biometrics? RSVP now to secure your spot, and let's explore the fascinating world of test automation together!

    | Coven of Wisdom - Amsterdam

    Go to page for The Test Automation Meetup
  • Coven of Wisdom - Herentals - Winter `24 edition

    Worstelen jij en je team met automated testing en performance? Kom naar onze meetup waar ervaren sprekers hun inzichten en ervaringen delen over het bouwen van robuuste en efficiënte applicaties. Schrijf je in voor een avond vol kennis, heerlijk eten en een mix van creativiteit en technologie! 🚀 18:00 – 🚪 Deuren open 18:15 – 🍕 Food & drinks 19:00 – 📢 Talk 1 20:00 – 🍹 Kleine pauze 20:15 – 📢 Talk 2 21:00 – 🙋‍♀️ Drinks 22:00 – 🍻 Tot de volgende keer? Tijdens deze meetup gaan we dieper in op automated testing en performance. Onze sprekers delen heel wat praktische inzichten en ervaringen. Ze vertellen je hoe je effectieve geautomatiseerde tests kunt schrijven en onderhouden, en hoe je de prestaties van je applicatie kunt optimaliseren. Houd onze updates in de gaten voor meer informatie over de sprekers en hun specifieke onderwerpen. Over iO Wij zijn iO: een groeiend team van experts die end-to-end-diensten aanbieden voor communicatie en digitale transformatie. We denken groot en werken lokaal. Aan strategie, creatie, content, marketing en technologie. In nauwe samenwerking met onze klanten om hun merken te versterken, hun digitale systemen te verbeteren en hun toekomstbestendige groei veilig te stellen. We helpen klanten niet alleen hun zakelijke doelen te bereiken. Samen verkennen en benutten we de eindeloze mogelijkheden die markten in constante verandering bieden. De springplank voor die visie is talent. Onze campus is onze broedplaats voor innovatie, die een omgeving creëert die talent de ruimte en stimulans geeft die het nodig heeft om te ontkiemen, te ontwikkelen en te floreren. Want werken aan de infinite opportunities van morgen, dat doen we vandaag.

    | Coven of Wisdom Herentals

    Go to page for Coven of Wisdom - Herentals - Winter `24 edition
  • Mastering Event-Driven Design

    PLEASE RSVP SO THAT WE KNOW HOW MUCH FOOD WE WILL NEED Are you and your team struggling with event-driven microservices? Join us for a meetup with Mehmet Akif Tütüncü, a senior software engineer, who has given multiple great talks so far and Allard Buijze founder of CTO and founder of AxonIQ, who built the fundaments of the Axon Framework. RSVP for an evening of learning, delicious food, and the fusion of creativity and tech! 🚀 18:00 – 🚪 Doors open to the public 18:15 – 🍕 Let’s eat 19:00 – 📢 Getting Your Axe On Event Sourcing with Axon Framework 20:00 – 🍹 Small break 20:15 – 📢 Event-Driven Microservices - Beyond the Fairy Tale 21:00 – 🙋‍♀️ drinks 22:00 – 🍻 See you next time? Details: Getting Your Axe On - Event Sourcing with Axon Framework In this presentation, we will explore the basics of event-driven architecture using Axon Framework. We'll start by explaining key concepts such as Event Sourcing and Command Query Responsibility Segregation (CQRS), and how they can improve the scalability and maintainability of modern applications. You will learn what Axon Framework is, how it simplifies implementing these patterns, and see hands-on examples of setting up a project with Axon Framework and Spring Boot. Whether you are new to these concepts or looking to understand them more, this session will provide practical insights and tools to help you build resilient and efficient applications. Event-Driven Microservices - Beyond the Fairy Tale Our applications need to be faster, better, bigger, smarter, and more enjoyable to meet our demanding end-users needs. In recent years, the way we build, run, and operate our software has changed significantly. We use scalable platforms to deploy and manage our applications. Instead of big monolithic deployment applications, we now deploy small, functionally consistent components as microservices. Problem. Solved. Right? Unfortunately, for most of us, microservices, and especially their event-driven variants, do not deliver on the beautiful, fairy-tale-like promises that surround them.In this session, Allard will share a different take on microservices. We will see that not much has changed in how we build software, which is why so many “microservices projects” fail nowadays. What lessons can we learn from concepts like DDD, CQRS, and Event Sourcing to help manage the complexity of our systems? He will also show how message-driven communication allows us to focus on finding the boundaries of functionally cohesive components, which we can evolve into microservices should the need arise.

    | Coven of Wisdom - Utrecht

    Go to page for Mastering Event-Driven Design

Share