In this Astro.js tutorial series, we will explore how to work with Astro.js, a popular front-end framework, and Strapi Headless CMS.
This post is the first of many in our Astro and Strapi tutorial. You can find the outline for upcoming post here.
Throughout this tutorial, you will learn the following:
In the final part of this tutorial, we will be building a website for the Harry Potter universe.
You can see a demo for the application below:
Below are the several features of the application. We will be putting the individual pieces together for this application in a step-by-step manner.
As seen above, Astro is known as "The web framework for content-driven websites".
Astro is an all-in-one framework designed for speed. The Astro website offers various resources and documentation.
Our journey with Astro begins with a simple task-creating a basic application. This straightforward step is designed to ease us into the framework.
To get started, we open up an editor. In the editor, we open up the terminal and run the command below:
npm create astro@latest
When the command above is run, we will be presented with the following:
> npx
> create-astro
astro Launch sequence initiated.
dir Where should we create your new project?
./astro101
tmpl How would you like to start your new project?
Empty
ts Do you plan to write TypeScript?
No
◼ No worries! TypeScript is supported in Astro by default,
but you are free to continue writing JavaScript instead.
deps Install dependencies?
Yes
git Initialize a new git repository?
Yes
✔ Project initialized!
■ Template copied
■ Dependencies installed
■ Git initialized
next Liftoff confirmed. Explore your project!
Enter your project directory using cd ./astro101
Run npm run dev to start the dev server. CTRL+C to stop.
Add frameworks like react or tailwind using astro add.
Stuck? Join us at https://astro.build/chat
╭─────╮ Houston:
│ ◠ ◡ ◠ Good luck out there, astronaut! 🚀
╰─────╯
Ensure to specify the following:
dir
: Project name should be astro101
.tmpl
: Astro allows us to create a blog template and include some sample files or an empty project. For our project, we will use the Empty
project.ts
: Astro supports TypeScript out of the box. However, we won't be using TypeScript for this project. We will choose No
.deps
and git
: Select Yes
to install dependencies and initialize a new git repository.astro101/
┣ .vscode/
┃ ┣ extensions.json
┃ ┗ launch.json
┣ public/
┃ ┗ favicon.svg
┣ src/
┃ ┣ pages/
┃ ┃ ┗ index.astro
┃ ┗ env.d.ts
┣ .gitignore
┣ README.md
┣ astro.config.mjs
┣ package-lock.json
┣ package.json
┗ tsconfig.json
The display above is a basic Astro project folder structure. Below is what they mean:
src
: This is the folder for creating our Astro application.pages
: This folder shows that Astro uses a file-based routing system, which is notable in other front-end frameworks.pages/index.astro
: This represents the index page of a route. Every route will have an individual file called index.astro
. It is important to note that we can also include other files such as .md
, .js
, .ts
, .html
, and so on inside the pages
folder.public
: For the time being, we have only an icon here. We can add other assets later.astro.config.mjs
: This is where we can specify some of Astro's behaviours. For example, we can change from Static Site Generation mode to Server-Side Rendering. You can also add extensions and plugins here as well. With the Astro robust CLI, we can modify this configuration file automatically.tsconfig.json
: This is the TypeScript configuration file, but we won't be writing any TypeScript..vscode
: Here, you can add some settings for Visual Studio Code.index.astro
FileNow, let's demystify the index.astro
file. Every Astro file is divided into two clear sections, making it easy for us to understand and use.
Because the default index.astro
file executes at build time, Astro default behaviour is Static Site Generation (SSG). This means the application you will deploy to any provider will be generated when you create a production build.
Let us demonstrate what we have learned so far. Update the code inside the index.astro
file in your src/pages
folder with the following code:
1---
2const name = "John"
3---
4
5<html lang="en">
6 <head>
7 <meta charset="utf-8" />
8 <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
9 <meta name="viewport" content="width=device-width" />
10 <meta name="generator" content="{Astro.generator}" />
11 <title>Astro</title>
12 </head>
13 <body>
14 <h1>Hello, {name}!</h1>
15 </body>
16</html>
In the code above, we created a varialbe called name
in the script section. And then, we evaluated and displayed this variable using the curly braces {}
in the template section.
Run the Astro dev command below to fire up our development server.
npm run dev
Our application should be visible in the URL: http://localhost:4321/
NOTE: Astro uses PORT number
4321
by default. If it is not available, Astro will use4322
,4323
and so on.
As seen above, we created a variable called name
and gave it the value John
which we displayed using the curly braces {}
.
Try playing around with different variables and values.
As stated before, the .astro
pages can contain entire HTML. Creating a generic layout applicable to every Astro page makes a lot of sense. This approach will allow us to insert only some HTML into the design rather than repeating the entire HTML sections. This is the idea behind Layouts. Astro files can also be used as components, as we will see below.
Within the src
folder, create a new folder named layouts
. Inside this new folder, create a new file by naming it Layout.astro
.
Inside the new file, Layout.astro
, paste the following code:
1<html lang="en">
2 <body>
3 <main>
4 <slot />
5 </main>
6 </body>
7</html>
In the code above, the <slot/>
tag plays a crucial role. It serves as the injection point for all the content in the astro file. We'll delve into this in a moment.
Like every other Astro file, components have the .astro
extension. Let us create one.
Inside the layouts
folder, create a new file called Navbar.astro
and paste inside it the code below:
1<nav>
2 <a href="/">Home</a> | <a href="/about">About</a>
3</nav>
In the code above, we created a navigation bar component. The next step will be to import this into the layout file.
Open the Layout.astro
file and paste in the code below:
1---
2import Navbar from "./Navbar.astro";
3---
4
5<html lang="en">
6 <body>
7 <Navbar />
8 <main>
9 <slot />
10 </main>
11 </body>
12</html>
As we can see in the code above, we imported the Navbar
component in the top section, the code section, or the JavaScript execution context of the Astro file and then used it as a component in the Astro file.
At this point, we are unsure how the content of index.astro
, inside the pages
folder will be added to the <slot/>
tag to be displayed.
Now, create another file inside the pages
folder and give it the name about.astro
. Inside this new file, add the following code:
1<p>this is the about page</p>
As said before, an Astro file can be any HTML or HTML content.
Inside the index.astro
file, import the layout.astro
component file by adding the following code.
1---
2import Layout from "../layouts/Layout.astro";
3const name = "John";
4---
5
6<Layout>
7 <h1>Hello, {name}!</h1>
8</Layout>
In the code above, we imported the Layout.astro
file. We removed all other HTML content and wrapped the <h1>
inside the Layout
component. Notice that we left out only the <h1>
element together with its content. This is because we only want this to be displayed through the <slot/>
we mentioned above.
Now, do this for the about
page. Modify the code inside the about.astro
file and add the following code:
1---
2import Layout from "../layouts/Layout.astro";
3---
4
5<Layout>
6 <p>this is the about page</p>
7</Layout>
When you go back to the browser, we can see the navigation bar displayed. And with it, we can navigate through to the home page and the about page.
Linking between pages in Astro is refreshingly simple. Unlike other frontend frameworks, we don't need any dedicated component. Astro uses the standard HTML and anchor element <a>
to effortlessly create links between various pages.
Sometimes, we might need an unknown number of parameters for our route. So far, we only have the index
and about
pages.
What if we have data from a Content Management System (CMS), such as a list of destinations, blog posts, or any other data? Creating an Astro file in the pages
folder for every potential route would be unwise because we would be creating every page for every new data. This is where Dynamic Routes in Astro comes in.
Dynamic routes in Astro offer a world of possibilities. They allow us to handle data dynamically, giving us the power to tell Astro, programmatically, all the potential options for a given route. And to do this, we have the flexibility to make use of one of Astro's in-built functions called getStaticPaths
.
Create a file called [destination].astro
inside the pages
folder. Notice the square brackets []
. This tells Astro that it is going to be a dynamic route.
Inside the [destination].astro
file, paste the following code:
1---
2import Layout from "../layouts/Layout.astro";
3const { destination } = Astro.params;
4
5export function getStaticPaths() {
6 // Data from a CMS or API
7 const destinations = [
8 "london",
9 "rome",
10 "sanfracisco",
11 "singapore",
12 "cairo",
13 "medellin",
14 ];
15 return destinations.map((destination) => ({
16 params: { destination },
17 }));
18}
19---
20
21<Layout>
22 <h1>Welcome to {destination}</h1>
23</Layout>
In the code above, we did the following:
getStaticPaths
function; we called this data destinations
. getStaticPaths
must return a format, we return an object with a params
property. The params
property represents each destination we get by looping through the CMS or API Call data, destinations
.Astro
object. For this, we used the Astro.params
property. destination
from the Astro.params
property. That way, we can get any destination and display it on the browser.Layout
component was imported and used to wrap the <h1>
HTML element.Below, we can see the result. When we change the route to /rome
, /singapore
or any of the destinations, it gets displayed in the browser.
Quit the current terminal and run the command below to generate the production build of our app.
npm run build
When the command above is run, Astro will generate a build folder that looks like this.
dist/
┣ about/
┃ ┗ index.html
┣ cairo/
┃ ┗ index.html
┣ london/
┃ ┗ index.html
┣ medellin/
┃ ┗ index.html
┣ rome/
┃ ┗ index.html
┣ sanfracisco/
┃ ┗ index.html
┣ singapore/
┃ ┗ index.html
┣ favicon.svg
┗ index.html
Because we have a dynamic route, Astro generated HTML files for all our destinations. This means that if we deploy this project, we will not need to worry about making API requests or anything else because it has been done in build time.
While Astro's Static Site Generation is impressive, it does have its limitations. For instance, handling highly dynamic data, like an e-commerce store's product listings and quantity updates, can be tricky. But with Astro's Server-Side Rendering features, we can overcome these challenges and enjoy the benefits of a more dynamic site.
Getting started with Server-Side Rendering (SSG) in Astro is a breeze. Simply configure the astro.config.mjs
file to tell Astro about your preferences. You can choose to manually update this file or use the robust Astro CLI, whichever suits your workflow best.
Navigate to astro.config.mjs
and modify the code with the following:
1import { defineConfig } from 'astro/config';
2
3// https://astro.build/config
4export default defineConfig({
5 output: "server"
6});
In the file above, we modified output
as server
in the defineConfig
. The server
mode means we want Server-Side Rendering for most of our sites.
There are two other modes or behaviors of an Astro application aside from server
, the hybrid
and static
modes. hybrid
is used when we want Server-Side Rendering for most of our site. And static
is used when we want to prerender HTML by default, and should be used if most of the content is static.
It is important to note that since we have set our output
as server
, we need to install and enable an adapter. We need a server runtime since we have set up a server mode.
One advantage of using Astro is its support for various adapters, including Vercel, Netlify, and Node.js. This variety ensures that you can choose the adapter that best fits your specific requirements. In this example, we'll be using the node
adapter.
Instead of manually configuring Astro's behavior, you can use the Astro CLI to automate this process. This not only saves time but also ensures that the configuration is accurate. As we mentioned earlier, simply navigate to your terminal, start the active process, and run the command below to enjoy this convenience.
npx astro add node
The command above will add the node
adapter to the configuration file.
NOTE: We can use the command above to add adapter for vercel, netlify, etc. by specifying the adapter name. For example,
npx astro add netlify
ornpx astro add vercel
Make sure to select yes
to the prompts. This is what we will see in our terminal after running the command above:
1✔ Resolving packages...
2
3 Astro will run the following command:
4 If you skip this step, you can always run it yourself later
5
6 ╭───────────────────────────────────╮
7 │ npm install @astrojs/node@^8.3.0 │
8 ╰───────────────────────────────────╯
9
10✔ Continue? … yes
11✔ Installing dependencies...
12
13 Astro will make the following changes to your config file:
14
15 ╭ astro.config.mjs ─────────────────────────────╮
16 │ import { defineConfig } from 'astro/config'; │
17 │ │
18 │ import node from "@astrojs/node"; │
19 │ │
20 │ // https://astro.build/config │
21 │ export default defineConfig({ │
22 │ output: "server", │
23 │ adapter: node({ │
24 │ mode: "standalone" │
25 │ }) │
26 │ }); │
27 ╰───────────────────────────────────────────────╯
28
29 For complete deployment options, visit
30 https://docs.astro.build/en/guides/deploy/
31
32✔ Continue? … yes
33
34 success Added the following integration to your project:
35 - @astrojs/node
After running the command above, we changed Astro's default behavior from static site generation to full-blown server-side rendering. You can see this in the newly modified astro.config.mjs
file:
1import { defineConfig } from 'astro/config';
2
3import node from "@astrojs/node";
4
5// https://astro.build/config
6export default defineConfig({
7 output: "server",
8 adapter: node({
9 mode: "standalone"
10 })
11});
Start up the application once again by running the dev server command:
npm run dev
Now that we have changed the behavior of our Astro application, we need to see how it indeed behaves in a Server-Side Rendering mode.
Create a new directory named utils
within the src
folder. In this directory, generate a new file with the name db.js
and insert the provided code below.
1export const products = [
2 {
3 id: 1,
4 name: "Sunglasses",
5 price: 12.99,
6 inStock: 12,
7 },
8 {
9 id: 2,
10 name: "Shoes",
11 price: 52.99,
12 inStock: 25,
13 },
14];
15
16export const listProducts = () => {
17 return products;
18};
19
20export const purchaseProduct = (id, quantity) => {
21 const [product] = products.filter((product) => product.id === parseInt(id));
22
23 if (product.inStock > 0 && product.inStock >= quantity) {
24 product.inStock -= quantity;
25 } else {
26 product.inStock = 0;
27 }
28
29 return products;
30};
The code above is a fake database of products, including shoes and sunglasses. In the code, we will expose two functions: listProducts
and purchaseProduct
. purchaseProduct
grabs the ID of the product we want to purchase and reduces the stock value with the purchased quantity, while listProducts
will return the products available.
We will have to expose the product information above as an API endpoint. Astro also supports API endpoints.
Create a folder called api
inside the pages
folder. Inside the api
folder, create a file called products.json.js
and add the following code:
1import { listProducts } from "../../utils/db";
2
3export async function GET() {
4 const products = await listProducts();
5 return new Response(JSON.stringify({ products }));
6}
In the code above:
export
statement with the function name mapped to an HTTP method called GET
. The GET
HTTP method here will handle requests to the /api/products.json
endpoint. Note that during the build process, the extension .js
or .ts
will be removed.listProducts
function we described earlier, which is a fake database. It is essential to note that if we opt into the Static Site Rendering mode, custom endpoints like the one above will be called at build time and will also produce a list of static files. This is not the case with Server-Side Rendering. These custom endpoints will turn into server endpoints and will be called on requests.
Now, when we try to access the /api/products.json
on our browser, we get the following:
Great! Now that we have made the API endpoint accessible, let us demonstrate how to make an API request in an Astro file.
Create a products.astro
file inside the pages
folder. We will make some API requests inside this file. Now, add the following code:
1---
2const response = await fetch(`${Astro.url.origin}/api/products.json`);
3const { products } = await response.json();
4---
5
6<ul>
7 { products.map((product) => (
8 <li>{product.name} (Current stock: {product.inStock})</li>
9 )) }
10</ul>
In the code above:
/api/products.json
endpoint.Astro.url.origin
of the Astro object, we get the site URL dynamically instead of hard-coding it.products
from the response list and displayed each product by looping through the products.When we open up the products
page, this is what we will see:
Awesome! We can see the products and their current quantity in the stock.
Since we are using Server-Side Rendering, how can we ensure that data is updated dynamically? Let's do this using JavaScript in the browser!
We will purchase to demonstrate the dynamic data change due to Server-Side Rendering.
First, we will create a dynamic endpoint! We must send the ID of every product we want to purchase. We will create a new endpoint to handle this new API request.
Remember, just like we created the [destination].astro
page, which is changeable, we will create a changeable API endpoint [id].json.js
. This is because the ID of a product can change. So, inside the api
folder, create a new file called [id].json.js
and paste in the following code:
1import { purchaseProduct } from "../../utils/db";
2
3export async function GET({ params }) {
4 const id = params.id;
5 const randomQuantity = Math.floor(Math.random() * 3) + 1;
6 const updatedProductList = await purchaseProduct(id, randomQuantity);
7 return new Response(
8 JSON.stringify({
9 updatedProductList,
10 }),
11 );
12}
In the code above:
db.js
file, we have the purchaseProduct
function, which reduces the value products.inStock
, which represents the number of products in stock. This means that when we purchase, the products in stock get reduced.GET
function. This is because the GET
function allows us to pass in params
as a parameter, which allows us to access the ID of any product.products.id
is the file's name, and the changeable parameter between the square brackets is [id]
. In other words, this represents the ID of the product we want to purchase.randomQuantity
.id
and random quantity randomQuantity
.Now that we have the new endpoint, we must implement this logic inside our client-side JavaScript. So, update the code inside the products.astro
page with the following:
1---
2const response = await fetch(`${Astro.url.origin}/api/products.json`);
3const { products } = await response.json();
4---
5
6<ul>
7 {
8 products.map((product) => (
9 <li>
10 {product.name} (Current stock: {product.inStock})
11 </li>
12 ))
13 }
14</ul>
15
16<button id="purchase">Purchase something</button>
17<script>
18 const button = document.querySelector("#purchase");
19 button.addEventListener("click", async () => {
20 const randomProduct = Math.floor(Math.random() * 2) + 1;
21 const response = await fetch(
22 `${location.origin}/api/${randomProduct}.json`
23 );
24 await response.json();
25 });
26</script>
In the code above, we created a button that, when clicked, makes a GET
request to the new endpoint /api/[id].json
. It does this by generating a random product we called randomProduct
. This randomProduct
can be a shoe or sunglasses, which has an ID of 1
or 2
. Either way, it represents the ID of the product.
We could not use the Astro.url.origin
property or the Astro global object because we are on the client side. Luckily, with the location.origin
, we get the site's URL. And with the URL, we can call the API endpoint that will execute the purchase of a product.
In the demo below, we can see a dynamic change of data in action. In the first window on the left-hand side, a user makes some purchases by clicking the "Purchase something" button about three or four times. When another user refreshes or opens the page in the second window on the right-hand side, they will discover that the data has changed—the current stock of each product has changed!
If this was done in Static Site Generation, this behavior would not be possible because the data that users will see is the same as it was when the page's production build was created.
So far, we have switched from Static Site Generation to Server-Side Rendering. However, index.astro
, about.astro
, and [destination].astro
files except products.astro
file do not need Server Side Rendering.
How do we tell Astro to do Static Site Rendering for these pages but products.astro
which only needs Server Side Rendering? This is possible by adding the code below to these pages:
1export const prerender = true;
Replace the code inside the index.astro
with the following:
1---
2export const prerender = true;
3import Layout from "../layouts/Layout.astro";
4const name = "Kate";
5---
6
7<Layout>
8 <h1>Hello, {name}!</h1>
9</Layout>
In the code above, we tell Astro to pre-render the index.astro
file. In other words, it should do Static Site Generation. Do this for about.astro
and [destination].astro
.
There is a lot more to unpack about Astro. It is advisable to look through the Astro documentation.
Some of these features include Content Collections which allows us to build our pages out of Markdown among other things. Other features are View Transitions, Prefetches, etc. Astro also has integrations with popular frameworks like Tailwind CSS. Recently, an SQL database Astro DB, was launched. This database works exclusively with Astro.
Astro also has a concept called Astro Islands. With this concept, you can take any component from any framework, such as React, and add it to Astro by specifying how the component should load.
There are also what are called Client Directives. With these directives, you can control how components are hydrated or displayed on the browser. For example, we can load a component when the browser is idle using client:idle
, when you want a component to be visible in different screen sizes using client:media
, etc.
In this Astro.js tutorial, we learned about Astro, a popular frontend framework designed for speed and content-based websites.
We also delved into Static Site Generation (SSG) in Astro, Server-Side Rendering (SSR) in Astro, pre-rendering, layouts and components, linking between pages, pre-rendering, making API requests, and so on.
Next, we will look at Strapi Headless CMS. Let's go!
part-1
.We are excited to have Ben Holmes from Astro chatting with us about why Astro is awesome and best way to build content-driven websites fast.
Topics
Join us to learn more about why Astro can be great choice for your next project.
Theodore is a Technical Writer and a full-stack software developer. He loves writing technical articles, building solutions, and sharing his expertise.
Tamas is a Google Developer Expert in Web Technologies and a seasoned Developer Evangelist. He is a passionate advocate for modern web technologies, helping people understand and unlock the latest & greatest features of web development.