Getting Started with the Basics of Astro
Astro is a modern static site generator that allows developers to build fast, optimized websites—a critical requirement in today’s rapidly evolving ecosystem, where user demands are high. Astro’s story goes back to 2021, when Fred K. Schott released the first version of the framework. At the time of writing this tutorial, Astro is already at version 4 and has seen many amazing features added to it.
Astro has gained popularity over its brief existence, due to:
- its innovative approach to creating websites, which emphasizes performance
- its smooth developer experience
- its ability to blend various other frameworks seamlessly
Due in part to a new monthly sponsorship from Vercel, and because it’s the first framework to implement the View Transitions API, Astro is a force to be reckoned with.
This tutorial is the first in a series that will introduce Astro and walk through some its basics—such as rendering, content collections, data fetching, and image optimization.
Key Concepts of Astro
How can one framework stand out in terms of performance? Let’s start by discussing one of the key concepts of Astro, called Astro Islands (or Component Islands). To better understand this web architecture style, let’s take a step back and discuss purely component-based development. If you’ve worked with React, Vue or Angular before, you likely have created components. Components encapsulate functionality: a React application is just a set of components put together in a logical way. My favorite analogy is that components are like Lego blocks: we use them to build our project.
There’s a striking problem with how components work under the hood in frameworks such as React and Angular. Usually, when a project is created, some components don’t necessarily require JavaScript, as the component isn’t interactive. For example, think about a navigation bar. Built out as a component, it may contain little apart from text, HTML and some styling. However, due to the nature of a framework such as React, we must load all the JavaScript. Otherwise, the component will fail to function.
When speaking of an Astro island, we refer to an interactive piece of UI element—“interactive” in the sense that it requires some JavaScript code to be run in order for it to be fully functional. Still, at the same time, Astro can also work with static (that is, non-interactive) components. Returning to our previous example, we could have a non-interactive navbar requiring zero bytes of JavaScript, while at the same time another interactive component loads some JavaScript.
This architectural pattern technique may also be called partial or selective hydration, which pertains to optimizing how web apps load and render content. Both of these techniques involve the deliberate loading and rendering of only essential parts, deferring less critical components until they’re needed by the app. The main purpose of this is to strike a balance between providing a speedy initial page load and allowing for a responsive user experience.
To sum up, Astro uses interactive and non-interactive components to create a site. This approach means that much less JavaScript is loaded, and only when it’s needed.
Astro’s default behavior is to ship zero client-side JavaScript. That doesn’t mean we can’t use JavaScript in an Astro project. Far from it. But its island architecture means that client-side JavaScript will only be loaded where necessary.
Getting Started with Astro
What better way to explore Astro than to create a project? And it couldn’t be easier: all we need to do is execute npm create astro@latest
, which will allow the creation of a brand new project.
npm
The command above refers to npm, the Node Package Manager. Make sure you have an up-to-date version of Node.js and npm on your computer before working with Astro. Check out this comprehensive tutorial on installing and working with npm if you need help with this.
The above command will ask multiple questions, such as the project’s location and how to start a project. This includes a choice of sample templates such as “blog”. For the purposes of this tutorial, just choose an empty project.
Starter Templates
Astro offers a wide range of starter templates, which you may like to check out, as well as an extensive range of themes.
The next step in the process is to make sure that the dependencies are installed. Astro also supports TypeScript by default, and the installer will prompt us to select whether we plan to write TypeScript or just JavaScript.
Let’s next look at some of the key folders and files. The good news is that there’s only a small number of these within an empty project installation.
Project Structure
The src
folder is where the entire application will be built. By default, the src
folder contains a pages
folder and an env.d.ts
file. As stated earlier, Astro has TypeScript support out of the box, and the env.d.ts
file has a triple slash directive that points to the appropriate Astro typings.
Astro uses file system-based routing. This essentially means that every folder and file placed in the pages
folder will act as a route. Therefore, adding about.astro
to src/pages
will create the /about
page. Adding a new countries
folder, along with a singapore.astro
file, will create the route /countries/singapore
—and so on.
What’s really amazing about Astro is that linking between these pages doesn’t require a third-party dependency or a dedicated component. Instead, a standard <a href="/about">
element can be used.
File Extensions
We don’t need to use the .astro
extension. Astro can also use .md
, .mdx
, and .html
extensions, as well as .js
and .ts
(for API endpoints, but more on this later). For this tutorial, we’ll stick to creating .astro
files and components.
The last file that needs mentioning is astro.config.mjs
, where we can specify some of Astro’s behavior or add plugins. We’ll return to this file later in the series and explore some of the options available.
It’s also important to discuss some naming conventions Astro suggests. Some of these are just suggestions. However, Astro reserves some folder names. Similarly to building a project with React, an Astro project comprises multiple components (either .astro
or others). Therefore, it makes sense to store these in a centralized location. Astro’s documentation suggests storing these in src/components
.
In the following tutorial, we’ll talk about layouts in Astro, which can be used to define the UI structure of the entire application. Astro again suggests using src/layouts
for files related to UI structures.
Styling is an important part of modern web applications, and src/styles
could be used to store CSS or even Sass files.
The public
folder can store assets such as images and fonts.
Last but not least, apart from src/pages
, the other folder name that’s reserved by Astro is src/content
. This location is to be used by content collections, which we’ll explore in a future tutorial.
Now that we’ve discussed the folder structure, let’s also discuss what files go into some folders. We’ll start by looking at how an Astro file is constructed. (An Astro file has a .astro
extension, referring to an “Astro component”.)
Anatomy of an Astro File
An .astro
component has two parts: a script and a template. The component script usually goes on top, and it’s separated by what’s called a code fence —three dashed lines at the top and the bottom of the “script” section: ---
. (If you’ve used frontmatter
in Markdown, the syntax is exactly the same.)
The component script is basically JavaScript code executed using a Node.js runtime during build time. Typically, we can import other components in here (either other Astro components or React components as well!), do data imports, execute Fetch API requests, create variables that would be used in the template, and even safely use API keys or write to a database. The key thing to understand here is that the JavaScript (or TypeScript) code written in this part of an Astro component will never make it to the client.
Any variable created in the script section can be referenced in the template section by using curly braces. (We’ll see an example of this later in this tutorial.)
The second part of an Astro component is the template. This is where the HTML tags should be added, and based on this, Astro will generate its final output during the build process. Apart from standard HTML elements, in this part of the template we can use JavaScript expressions, Astro components (or components from any other supported framework), and special Astro directives. The template can also naturally accept a <script>
element if client-side JavaScript is required.
This is how a sample .astro
file might look, showing both the script and the HTML template parts:
---const name = 'Jack'---<h1>Hello there, {name}</h1>
Placing JavaScript
By default, any JavaScript added to the <script>
element will be bundled and added to the <head>
of the statically generated page using the type="module"
attribute. Add <script is:inline>
if you want your JavaScript to be inline instead. See the next section for more details.
Client-side JavaScript vs Build-time JavaScript
Let’s check quickly to ensure we understand the concept of JavaScript added in the script part of an Astro component versus adding JavaScript in a <script>
element in the template.
Here’s one option:
---const copyrightYear = new Date().getFullYear();---<small>Copyright, {copyrightYear}</small>
Here’s another option:
<small>Copyright</small><script>document.querySelector('small').insertAdjacentText('beforeend',` ${new Date().getFullYear()}`);</script>
Which option would we use to display the copyright year in Astro? Both of them work (seemingly) correctly, and both of them will display the current year.
However, there’s a crucial difference. Imagine we end up using the first solution and build our project on December 29, 2024 and deploy it. Then we don’t do another build for two years. Opening the page will still display 2024 once that year has expired. Why? Because the JavaScript code placed between the code fence (---
) executes during build time. The second solution, however, will always display the correct year, as that code executes at the request time. That is, every time someone views the page, that piece of JavaScript code will run.
Styling an Astro Component
Now that we have a good idea of what an Astro component looks like, let’s also discuss styling these components. Astro, out of the box, supports CSS, Sass and Less. Similar to how adding a <script>
element is possible in a component, adding a <style>
element is also possible. And the good news? CSS rules are scoped by default to the component where they’re defined. This means that the styles defined at the individual page or component level don’t pollute the rest of the application and the CSS rules will only apply to the necessary pages/components. That said, global styles can also be created by adding the is:global
attribute to the <style>
element.
Take this example:
<style is:global>p { font-weight: bold;}</style>
In the code sample above, all paragraphs will be bold in the entire project because of the is:global
directive.
These two approaches can also be combined by using the <style>
element and the :global()
CSS selector.
Furthermore, CSS variables can be used from the script part of an Astro component by utilizing the define:vars
attribute on the <style>
element:
---const important = `#f00`;---<style define:vars={{ important }}>p { color: var(--important);}</style><p>Important message</p>
In the code above, we’re defining a variable in JavaScript (important
) and then reusing it as a CSS variable within the Astro template by way of the define:vars
directive on the <style>
element.
Lastly, if we have external stylesheets and we’ve placed them in our public
folder, we can easily import them from within the component using the standard ESM import
syntax:
---import '../path/to/style.css'---<p>some html...</p>
Let’s also talk about Astro in the context of a larger ecosystem. Astro integrates with tools such as Tailwind (a utility-first CSS framework) and Partytown (a library designed to assist moving resource intensive scripts—such as third-party scripts—to a web worker, therefore relieving the main thread to process app specific JavaScript instead).
Custom Integrations
It’s worth noting that we can build a custom integration as well via Astro’s Integrations API.
The easiest way to use Tailwind (and other officially supported integrations) is by using the npx astro add [integration]
command: npx astro add tailwind
. This command automatically installs a compatible version of Tailwind, generates a custom tailwind.config.cjs
file, and updates the astro.config.mjs
to look like this, making it really easy to add Tailwind to a project:
import { defineConfig } from 'astro/config';
import tailwind from "@astrojs/tailwind";
// https://astro.build/configexport default defineConfig({ integrations: [tailwind()]});
Demo
Throughout this tutorial series, we’ll be referencing a sample app, which you can also take a look at by visiting stackblitz.com. For the purposes of consolidating what you’ve learned in this first tutorial, it would be good to review the file and folder structure as well as the Tailwind setup in this demo.
Conclusion
In this tutorial, we got an introduction to Astro, a modern frontend framework. Astro’s rising popularity is attributed to its innovative approach to web development, prioritizing performance, seamless developer experience, and the ability to integrate with other frameworks and tools.
Some of the key takeaways from this tutorial are:
- Islands. Astro’s island architecture allows us to create islands of interactivity—loading JavaScript where and when needed in our app.
- Project structure. Our overview of the project structure of an Astro project showed the key files and folders found in an Astro project.
- File anatomy. We explored the anatomy of a
.astro
file, discussing what the script and template sections of a typical.astro
file look like. - Styling. We briefly touched on how to use styles in Astro.
And that’s it for the first tutorial in this Astro series. Now that we’ve covered the basic concepts, we’ll next take a look at Astro’s options for rendering and layouts.