Explore the Benefits of Astro.js by Building a Quick App

If you’re keeping up with the latest web dev tools, you’ve probably built some sort of “web app” using meta-frameworks like Next.js, Nuxt, SvelteKit, Angular, and more. These all offer some serious strengths:

  • The freedom to think in components to isolate and reuse parts of your UI.
  • JS-driven interactivity to add page transitions, image carousels, multi-step forms, and the like.
  • Options to render at build-time or on-request using servers and serverless functions.

These are great benefits. Heck, even table stakes these days. But these frameworks often come at a cost: shipping more code than your user needs.

Tools like Next.js and Angular effectively ship JavaScript for every element on the page, just in case you need some interactivity. The simpler your site, the more this hydration code becomes a needless hit to your page load times and other core web vitals.

That’s why we’re exploring Astro.js in this issue of The Optimized Dev.

Not signed up for The Optimized Dev?

Staying on the leading edge of web development just got easier. Explore new tech with a fun, scaffolded coding challenge that lands in your inbox once a month.

843667ae D988 49ea 81f6 544d7da7ec85 Home Optimized Dev 5

Let’s talk about Astro.js

Astro is a web framework that checks all the bullets we listed above, but without the JavaScript bloat. Where a Next.js homepage may hit 100+KB of JavaScript, Astro can pull this down to less than 10 KB while letting you use the tools you love: React, Vue, Svelte, Markdown, and more.

Astro really shines on content-driven websites like marketing pages and blogs, where Time to First Byte and developer ergonomics are key. To put this to the test, we’re going to keep things small by … browsing the entire universe 🌌

Screenshot: The complete Star Gazer

We’re going to build a space imagery explorer powered by the NASA API, a couple Svelte components for those carousel transitions, and Astro SSR to generate unique URLs for every “learn more” page. Through this app, we’ll get to see the benefits of Astro.js in action. Astro.js:

  • Is about starting simple, and adding complexity where needed.
  • Is about zero-config — any config explained will be handled by our astro add CLI command (i.e. add Svelte support with astro add svelte).
  • Can deploy to serverless. To showcase this, we will tackle a use case where static sites fail us and server-side rendering shines.
  • Supports any component framework, with opt-ins to ship client-side JavaScript.
  • Makes talking to APIs very simple with built-in fetch support. We will use this to call a third-party to get usable data.

astro add

astro add svelte

fetch

Prismic’s Selected Frameworks

This Astro tutorial serves as a creative exploration or a ‘toy project’. For professional-grade company websites, Prismic strongly recommends Nuxt, Next.js, and SvelteKit. These frameworks offer robust features and community support, ideal for enterprise solutions.

Explore our Next.js tutorial, Nuxt tutorial or Sveltekit tutorial.

Getting started

Open this project on Stackblitz to follow along 🚀

This is a tweaked version of the Astro Basics template that you can use for future projects!

Now, you can poke around the repo to get comfortable with Astro’s conventions. If you’re coming from Next.js, you’ll notice a familiar folder structure:

  1. pages/ for your routes. This is the only “magic” folder under src, where every file translates to a live URL on your site.
  2. components/ for static and interactive components. These could be written in Astro syntax, React, Vue, Svelte, etc.
  3. layouts/ for your page layouts. You will import and use these in your pages like any other component.

pages/

src

components/

layouts/

pages

Shout out to our Optimizers!

Thanks for sharing your challenge with us @helge_eight! Who else has been following along each month? Let us know! 🚀

Make our first fetch call

fetch

To start, let’s head to our homepage (src/pages/index.astro) and add a fetch call to the page’s front-matter (aka the space between those --- fences). This is where all server-side logic will go. If you’re coming from Next.js, think of this like your getStaticProps and/or getServerSideProps:

src/pages/index.astro

fetch

---

getStaticProps

getServerSideProps

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

const images = await fetch(
  `https://api.nasa.gov/planetary/apod?api_key=DEMO_KEY&count=10`
).then(res => res.json());

console.log(images)
---

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

const images = await fetch(
`https://api.nasa.gov/planetary/apod?api_key=DEMO_KEY&count=10`
).then(res => res.json());

console.log(images)
---

Notice we could use await and fetch at the top of the file, no setup necessary. These are both supported out-of-the-box by Astro!

await

fetch

Now if you open your console, you should see a list of 10 random, intergalactic images from the NASA API 🪐

[
  {
    date: '2013-11-05',
    explanation: "ven though Kepler-78b is only...",
    hdurl: '<https://apod.nasa.gov/apod/image/1311/kepler78b_cfa_2400.jpg>',
    media_type: 'image',
    service_version: 'v1',
    title: 'Kepler-78b: Earth-Sized Planet Discovered',
    url: '<https://apod.nasa.gov/apod/image/1311/kepler78b_cfa_960.jpg>'
  },
  ...
]

[
{
date: '2013-11-05',
explanation: "ven though Kepler-78b is only...",
hdurl: '<https://apod.nasa.gov/apod/image/1311/kepler78b_cfa_2400.jpg>',
media_type: 'image',
service_version: 'v1',
title: 'Kepler-78b: Earth-Sized Planet Discovered',
url: '<https://apod.nasa.gov/apod/image/1311/kepler78b_cfa_960.jpg>'
},
...
]

We’re using their DEMO_KEY as our API key for now, but feel free to request your own API key if you run into rate limiting. Don’t worry, it’s free and quick to request!

DEMO_KEY

Add type safety

Just a note: This section won’t offer type hints in the online StackBlitz editor just yet! If you clone this project locally, you should enjoy Intellisense in VS Code.

Astro also comes with built-in TypeScript support. We use a relaxed tsconfig.json by default so you can use TS when you want to, and ignore it when you don’t. Here, we’ll assign a type to our array of NASA imagery:

tsconfig.json

<!--src/pages/index.astro-->
---
import Layout from '../layouts/Layout.astro';

type Image = {
  title: string;
  explanation: string;
  media_type: 'image' | 'video';
  /** Date published in YYYY-MM-DD format */
  date: string;
  /** Video URL or standard resolution image URL */
  url: string;
  /** High-res image URL (for media_type: 'image' only) */
  hdurl?: string;
  /** API version. Defaults to "v1" */
  service_version: string;
}

const images: Image[] = await fetch(
  `https://api.nasa.gov/planetary/apod?api_key=DEMO_KEY&count=10`
).then(res => res.json());

console.log(images)
---

<!--src/pages/index.astro-->
---
import Layout from '../layouts/Layout.astro';

type Image = {
title: string;
explanation: string;
media_type: 'image' | 'video';
/** Date published in YYYY-MM-DD format */
date: string;
/** Video URL or standard resolution image URL */
url: string;
/** High-res image URL (for media_type: 'image' only) */
hdurl?: string;
/** API version. Defaults to "v1" */
service_version: string;
}

const images: Image[] = await fetch(
`https://api.nasa.gov/planetary/apod?api_key=DEMO_KEY&count=10`
).then(res => res.json());

console.log(images)
---

Challenge step [solved]: Display some images

Now that we have our data, let’s render these to an interactive carousel. We could use Astro’s Preact or Vue integrations here, but Svelte’s transition API should make animating our carousel a bit simpler. See our React vs. Svelte article if you’re curious about where Svelte shines.

To add Svelte support, stop the dev server from your terminal with ^C and run:

^C

# Linux and MacOS
astro add svelte
# Windows
npx astro add svelte

# Linux and MacOS
astro add svelte
# Windows
npx astro add svelte

You can restart the dev server by running npm run dev.

npm run dev

This will update your config file and install all necessary dependencies for you. You can learn more by running astro add for a help page.

astro add

Let’s start by displaying all of our images statically. To speed us along, we’ve included a basic component for displaying this media under src/components/Image.svelte. This component accepts our media info as props (title, url, alt, and media_type), and displays within a captioned figure element.

src/components/Image.svelte

title

url

alt

media_type

figure

We can use this component on our index.astro page like any other component. Just import at the top of our pages/index.astro:

index.astro

pages/index.astro

<!--src/pages/index.astro-->
---
import Layout from '../layouts/Layout.astro';
import Image from '../components/Image.svelte';
...

<!--src/pages/index.astro-->
---
import Layout from '../layouts/Layout.astro';
import Image from '../components/Image.svelte';
...

And use the component in your HTML. Since Astro files support JSX syntax, we’ll render each image in our list using a .map over the images array:

.map

images

<!--src/pages/index.astro-->
---
import Layout from '../layouts/Layout.astro';
import Image from '../components/Image.svelte';

type Image = ...

const images: Image[] = await fetch(
  `https://api.nasa.gov/planetary/apod?api_key=DEMO_KEY&count=10`
).then(res => res.json());

---

<Layout title="Welcome to Astro.">
  <main>
    <h1>Welcome to <span class="text-gradient">Star Gazers</span></h1>
    {images.map(image => (
      <Image
        title={image.title}
        url={image.url}
        alt={image.explanation}
        media_type={image.media_type}
      />
      ))}
  </main>
</Layout>

<!--src/pages/index.astro-->
---
import Layout from '../layouts/Layout.astro';
import Image from '../components/Image.svelte';

type Image = ...

const images: Image[] = await fetch(
`https://api.nasa.gov/planetary/apod?api_key=DEMO_KEY&count=10`
).then(res => res.json());

---

<Layout title="Welcome to Astro.">
<main>
<h1>Welcome to <span class="text-gradient">Star Gazers</span></h1>
{images.map(image => (
<Image
title={image.title}
url={image.url}
alt={image.explanation}
media_type={image.media_type}
/>
))}
</main>
</Layout>

When done, you should see 10 interstellar images rendered to the page 🌌

Screenshot: The star gazers app with the heading,

Add an animated carousel

We have our content listed out, but it’s not very … fun. Let’s add some interactivity!

To start, let’s open our base Carousel implementation under src/components/Carousel.svelte. Most of this is plain CSS and some HTML templating to loop over our images. Still, you might’ve noticed this style in particular:

src/components/Carousel.svelte

.image-container {
  grid-area: 1 / -1;
}

.image-container {
grid-area: 1 / -1;
}

This places every image in the same row / column on our images element. In other words, we “sandwiched” all of our images on top of each other so we can cross-fade between them. More on that soon!

images

Now, let’s replace our image rendering from earlier with this Carousel component:

Carousel

<!--src/pages/index.astro-->
---
  import Layout from '../layouts/Layout.astro';

  import Carousel from '../components/Carousel.svelte';
...
---

<Layout title="Welcome to Astro.">
  <main>
    <h1>Welcome to <span class="text-gradient">Star Gazers</span></h1>
    <Carousel images={images} />
  </main>
</Layout>

<!--src/pages/index.astro-->
---
import Layout from '../layouts/Layout.astro';

import Carousel from '../components/Carousel.svelte';
...
---

<Layout title="Welcome to Astro.">
<main>
<h1>Welcome to <span class="text-gradient">Star Gazers</span></h1>
<Carousel images={images} />
</main>
</Layout>

You should see a stack of NASA images like this:

Screenshot: the Star Gazer

Wire up our previous / next buttons

Let’s make those arrow buttons actually do something. We’ll start with a currentIdx variable to keep track of which array index is displaying. We’ll also add previous() and next() events to increment and decrement currentIdx, wrapping to the beginning whenever we hit the end of our array:

currentIdx

previous()

next()

currentIdx

<!--src/components/Carousel.svelte-->
<script>
  import Image from './Image.svelte';

  export let images = [];

  let currentIdx = 0;
  function next() {
    currentIdx = (currentIdx + 1) % images.length;
  }
  function previous() {
    if (currentIdx === 0) {
      currentIdx = images.length - 1;
    } else {
      currentIdx -= 1;
    }
  }
</script>

<!--src/components/Carousel.svelte-->
<script>
import Image from './Image.svelte';

export let images = [];

let currentIdx = 0;
function next() {
currentIdx = (currentIdx + 1) % images.length;
}
function previous() {
if (currentIdx === 0) {
currentIdx = images.length - 1;
} else {
currentIdx -= 1;
}
}
</script>

Now, we can wire these functions to on:click events on our <button> elements. We can also use images[currentIdx] to display the correct image title in our h2:

on:click

<button>

images[currentIdx]

h2

<!--src/components/Carousel.svelte-->
<script>
...
</script>

<div class="buttons">
  <button on:click={previous} aria-label="Previous">
  <!--left arrow icon-->
  <svg>...</svg>
  </button>
  <h2>{images[currentIdx].title}</h2>
  <button on:click={next} aria-label="Next">
  <!--right arrow icon-->
  <svg>...</svg>
  </button>
</div>

<!--src/components/Carousel.svelte-->
<script>
...
</script>

<div class="buttons">
<button on:click={previous} aria-label="Previous">
<!--left arrow icon-->
<svg>...</svg>
</button>
<h2>{images[currentIdx].title}</h2>
<button on:click={next} aria-label="Next">
<!--right arrow icon-->
<svg>...</svg>
</button>
</div>

Finally, we can conditionally show a given image in the list using an {#if} block:

{#if}

<!--src/components/Carousel.svelte-->
...
<div class="images">
  {#each images as image, idx}
    {#if idx === currentIdx}
      <div class="image-container">
        <Image title={image.title} url={image.url} alt={image.explanation} media_type={image.media_type}>
        <a slot="figcaption" href={`/${image.date}`}>Learn more</a>
        </Image>
      </div>
    {/if}
  {/each}
</div>

<!--src/components/Carousel.svelte-->
...
<div class="images">
{#each images as image, idx}
{#if idx === currentIdx}
<div class="image-container">
<Image title={image.title} url={image.url} alt={image.explanation} media_type={image.media_type}>
<a slot="figcaption" href={`/${image.date}`}>Learn more</a>
</Image>
</div>
{/if}
{/each}
</div>

Looks like we’re ready to go! We can click that next button and … wait … nothing happened 😳 What gives?

Challenge step [solved]: Hydrate our carousel

Well, there’s one more step to make your components interactive. By default, Astro will only server-render your component’s HTML and CSS, ignoring any client-side JavaScript. This lets you use your favorite framework while shipping zero JS to the browser 👀

When you do need that interactivity, you can add a client: directive wherever that component is used. We'll apply client:idle to our <Carousel /> like so:

client: directive

wherever that component is used. We'll apply

to our

like so:

<!--src/pages/index.astro-->
---
...
---

<Layout title="Welcome to Astro.">
  <main>
    <h1>Welcome to <span class="text-gradient">Star Gazers</span></h1>
    <Carousel client:idle {images} />
  </main>
</Layout>

<!--src/pages/index.astro-->
---
...
---

<Layout title="Welcome to Astro.">
<main>
<h1>Welcome to <span class="text-gradient">Star Gazers</span></h1>
<Carousel client:idle {images} />
</main>
</Layout>

This is one of many client: directives you can use in Astro. client:idle will load our component's JavaScript when the main thread is free, letting other scripts like analytics and eagerly-loaded components take precedence. You can also wait to load JavaScript until the component scrolls into view, only when a CSS media query is satisfied, and more 🔥

client:

will load our component's JavaScript when the main thread is free, letting other scripts like analytics and eagerly-loaded components take precedence. You can also wait to load JavaScript

With this directive applied, you can happily click through that carousel!

Add transitions

What’s a carousel without some animations? Since we’re using Svelte, we can reach for some nice built-in transitions here. Let’s try importing the fly transition from our carousel:

fly

<!--src/components/Carousel.svelte-->
<script>
 import { fly } from 'svelte/transition';
...
</script>

<!--src/components/Carousel.svelte-->
<script>
import { fly } from 'svelte/transition';
...
</script>

And apply it to our image-container with the transition: directive:

image-container

transition:

<!--src/components/Carousel.svelte-->
...
<div class="images">
  {#each images as image, idx}
    {#if idx === currentIdx}
      <div class="image-container" transition:fly={{ y: 20 }}>
        <Image title={image.title} url={image.url} alt={image.explanation} media_type={image.media_type}>
        <a slot="figcaption" href={`/${image.date}`}>Learn more</a>
        </Image>
      </div>
    {/if}
  {/each}
</div>

<!--src/components/Carousel.svelte-->
...
<div class="images">
{#each images as image, idx}
{#if idx === currentIdx}
<div class="image-container" transition:fly={{ y: 20 }}>
<Image title={image.title} url={image.url} alt={image.explanation} media_type={image.media_type}>
<a slot="figcaption" href={`/${image.date}`}>Learn more</a>
</Image>
</div>
{/if}
{/each}
</div>

Our carousel should feel astronomically better to click now 👩‍🚀

Screen recording: The complete Star Gazer

Bonus: Add some “learn more” routes

If you got this far and you’re hungry for more, try out Astro’s dynamic routing! You may have noticed that our “learn more” link doesn’t take us anywhere right now:

Screenshot: Astro

Since these images are randomly generated, our route could be thousands of different entries (one for each day of this API’s existence actually). This sounds like a job for serverless functions 🚀

Before jumping in, let’s add output: 'server' to the project’s astro.config.mjs. This should enable the dynamic routing we’re after:

output: 'server'

astro.config.mjs

// astro.config.mjs
import { defineConfig } from 'astro/config';

import svelte from "@astrojs/svelte";

// <https://astro.build/config>
export default defineConfig({
  output: 'server',
  integrations: [svelte()]
});

// astro.config.mjs
import { defineConfig } from 'astro/config';

import svelte from "@astrojs/svelte";

// <https://astro.build/config>
export default defineConfig({
output: 'server',
integrations: [svelte()]
});

Restart the dev server with ^C + npm run dev for these changes to take effect.

^C

npm run dev

We should note that dynamic routing is also possible in “static site” mode using getStaticPaths. We don't know all of our possible routes up-front though, so this won't serve (heh) our use case!

getStaticPaths

. We don't know all of our possible routes up-front though, so this won't serve (heh) our use case!

After restarting the dev server, create a new route to read our image date to a variable using [bracket syntax]. Say, an [imageDate].astro file like this one:

[bracket syntax]

[imageDate].astro

---
// src/pages/[imageDate].astro
import Layout from '../layouts/Layout.astro';

const { imageDate } = Astro.params;
---

<Layout title={imageDate}>
  <main>
    <h1>{imageDate}</h1>
  </main>
</Layout>

<style>
  h1 {
    margin: 2rem 0;
  }

  main {
    margin: auto;
    padding: 1em;
    max-width: 60ch;
  }
</style>

---
// src/pages/[imageDate].astro
import Layout from '../layouts/Layout.astro';

const { imageDate } = Astro.params;
---

<Layout title={imageDate}>
<main>
<h1>{imageDate}</h1>
</main>
</Layout>

<style>
h1 {
margin: 2rem 0;
}

main {
margin: auto;
padding: 1em;
max-width: 60ch;
}
</style>

This route will display for every base-level route other than our homepage. Right now, this should render our visited route to that h1 heading:

h1

Screenshot: The h1 heading should render for now. This h1 says

Let’s try requesting more information using the NASA API. We can reuse the same “apod” endpoint from our home page, this time passing a date parameter:

date

---
// src/pages/[imageDate].astro
import Layout from '../layouts/Layout.astro';

const { imageDate } = Astro.params;
 const image = await fetch(
   `https://api.nasa.gov/planetary/apod?api_key=DEMO_KEY&date=${imageDate}`
 ).then(res => res.json());
---

---
// src/pages/[imageDate].astro
import Layout from '../layouts/Layout.astro';

const { imageDate } = Astro.params;
const image = await fetch(
`https://api.nasa.gov/planetary/apod?api_key=DEMO_KEY&date=${imageDate}`
).then(res => res.json());
---

Since the result is the same shape as our homepage images, we can reuse our Image component to display this to the user:

Image

---
// src/pages/[imageDate].astro
import Layout from '../layouts/Layout.astro';
import Image from '../components/Image.svelte';

const { imageDate } = Astro.params;
const image = await fetch(
  `https://api.nasa.gov/planetary/apod?api_key=DEMO_KEY&date=${imageDate}`
).then(res => res.json());
---

<Layout title={image.title}>
  <main>
   <h1>{image.title}</h1>
   <Image title={image.title} url={image.url} alt={image.explanation} media_type={image.media_type}>
    <p slot="figcaption">
      {image.explanation}
    </p>
    </Image>
  </main>
</Layout>

---
// src/pages/[imageDate].astro
import Layout from '../layouts/Layout.astro';
import Image from '../components/Image.svelte';

const { imageDate } = Astro.params;
const image = await fetch(
`https://api.nasa.gov/planetary/apod?api_key=DEMO_KEY&date=${imageDate}`
).then(res => res.json());
---

<Layout title={image.title}>
<main>
<h1>{image.title}</h1>
<Image title={image.title} url={image.url} alt={image.explanation} media_type={image.media_type}>
<p slot="figcaption">
{image.explanation}
</p>
</Image>
</main>
</Layout>

Try heading to our homepage and clicking “learn more.” You should be taken to a unique URL with a full explanation block, shareable to anyone on the galaxy-wide-web 🌌

Screenshot: Here we see the Learn More route

Oh, and this entire page is rendered server-side with zero JavaScript in the browser. How’s that for warp speed!

Deployment

If you want to deploy your Star Gazers site, we suggest deploying to SSR by following our deployment guides. We can get you on Netlify, Vercel, Deno Deploy, and more in minutes ❤️

You should also check out:

0bbb09d9 8343 4af5 Acc5 70e2ee53d79a Compare Svelte Vs React 2022

Svelte vs. React: Choosing the Best Match for You

Fb623a6e 8942 41a4 9528 26c7f3feffdc Nextjs Seo Tutorial 1

Next.js SEO Guide: Build a Next.js Site with SEO Super Powers

Wrapping up

We hope this tutorial makes you as excited about Astro as we are! There’s still countless features to explore, like:

And more. If you’re excited to explore the Astro-verse, hop into our docs and join our sparkling Discord community. We hit version 1.0 as of 2022, so our voyage has only just begun 🚀

Leave a Comment

Your email address will not be published. Required fields are marked *