50 min read
Edit pageChapter 1: Build your first Astro Application
Long is the road to learning by precepts, but short and successful by examples
— Seneca the Younger.
Get started with the basics of Astro by building a practical application: a personal site.
What you’ll learn
- Build a personal website with Astro.
- Set up a local development environment for Astro.
- Familiarity with Astro components, layouts and pages.
- A working knowledge of styles and scripts in Astro.
- Theming Astro sites via CSS variables.
- Leveraging markdown pages for ease.
- Deployment of a static Astro application.
Project Overview
I remember my first commercial web development project. In retrospect, it was a disaster. One built by a passionate self-taught engineer, but a disaster still.
Let’s make your first Astro project one we’ll remember for good.
Getting started
Astro is a web framework designed for speed. Before we get to the good stuff, let’s ensure we’re both on the same page.
Install Node.js
Firstly, make sure you have nodejs installed.
If unsure, run node --version
in your terminal. You will get back a node version if you have nodejs installed.
Don’t have nodejs installed? Then, visit the official download page and install the necessary package for your operating system. It’s as easy as installing any other computer program. Click, click, click!
Setting up your code editor
I’ll avoid any heated debate(s) on what code editor you should be writing software with. The truth is I do not care. Quite frankly.
However, I use Visual Studio Code (VSCode).
You can develop Astro applications with any code editor, but VSCode is also the officially recommended editor for Astro.
If you’re building with VSCode1, install the official Astro extension. This helps with syntax and semantic highlighting, diagnostic messages, IntelliSense, and more.
Let’s now get started setting up our first Astro project. To do this, we must install Astro, and the fastest way to do this is to use the Astro automatic CLI.
To start the install wizard, run the following command:
If on pnpm
or yarn
, the command looks as follows:
This will start the wizard, which will guide us through helpful prompts. It’s important to mention that we can run this from anywhere on our machine and later choose where exactly we want the project created.
When asked, “Where should we create your new project?” go ahead and pass a file path. In my case, this is documents/dev/books/understanding-astro/astro-beginner-project
.
Alternatively, we could have run the npm create astro@latest
command in our desired directory and just entered a shorter file path, e.g., ./astro-beginner-project
.
When asked, “How would you like to start your new project?” go ahead and choose “Empty”.
We want a fresh start to explore Astro from the ground up.
Now, we will be asked whether to install dependencies or not. Select yes and hit enter to continue the installation.
Once the dependencies are installed, answer the “Do you plan to write TypeScript?” prompt with a yes and choose the “strictest” option.
We want strong type safety.
Afterwards, answer the “Initialise a new git repository?” question with whatever works for you. I’ll go with a yes here and hit enter.
And voila! Believe it or not, our new project is created and ready to go!
Change into the directory where you set up the project. In my case, this looks like the following:
And then run the application via the following:
This will start the live application on an available local port 🚀
Project structure
Open the newly created project in your code editor, and you’ll notice that the create astro
CLI wizard has included some files and folders.
Astro has an opinionated folder structure. We can see some of this in our new project. By design, every Astro project will include the following in the root directory:
File / Directory | |
---|---|
astro.config.mjs | The Astro configuration file. This is where we provide configuration options for our Astro project. |
tsconfig.json | A TypeScript configuration file. This specifies the root files and TypeScript compiler options. |
package.json | A JSON file that holds the project metadata. This is typically found at the root of most Node.js projects. |
public/* | This directory holds files and assets that will be copied into the Astro build directory untouched, e.g., fonts, images and files such as robots.txt |
src/* | The source code of our project resides here. |
Let’s now look at the files in our newly generated project.
tsconfig.json
The content of our tsconfig.json
file is the following:
The extends
property points to the base configuration file path to inherit from, i.e., inherit the typescript configuration from the file in astro/tsconfigs/strictest
.
Using your editor, we may navigate to the referenced path, e.g., in vscode
by clicking on the link while holding CMD
. This will navigate us to node_modules/astro/tsconfigs/strictest.json
, where we’ll find a well-annotated file:
This is very well annotated, so we won’t spend time on this. However, the compilerOptions
for TypeScript are set in this file. The point to make here is Astro keeps a list of typescript configurations (base
, strict
and strictest
) that our project leverage when we initialise via the CLI wizard.
In this example, we’ll leave the tsconfig.json
file as is. TypeScript (and consequently the tsconfig.json
file is optional in Astro projects. However, I strongly recommend you leverage TypeScript. We’ll do so all through the book.
package.json
The package.json
file is easy to reason about. It holds metadata about our project and includes scripts for managing our Astro project, e.g., npm start
, npm run build
, and npm preview
.
package-lock.json
The package-lock.json
file is an autogenerated file that holds information on the dependencies/packages for our project. We won’t be touching this file manually. Instead, it is automatically generated (and updated) by npm.
A project’s lock file may differ depending on the package manager, e.g., yarn or pnpm.
astro.config.mjs
Most frameworks define a way for us to specify our project-specific configurations. For example, Astro achieves this via the astro.config
file.
At the moment, it defines an empty configuration. So we’ll leave it as is. However, this is the right place to specify different build and server options, for example.
src/env.d.ts
.d.ts
files are called type declaration files2. Yes, that’s for TypeScript alone, and they exist for one purpose: to describe the shape of some existing module. The information in this file is used for type checking by TypeScript.
The content of the file points to astro/client
. This is essentially a reference to another declaration file at astro/client.d.ts
src/pages/index.astro
As mentioned earlier, the src
folder is where the source code for our project resides. But what’s the pages
directory, and why’s there an index.astro
file?
First, consider the contents of the index.astro
file:
You’d notice that it looks remarkably similar to standard HTML, with some exceptions.
Also, notice what’s written within the <body>
tag. An <h1>
element with the text Astro
.
If we visit the running application in the browser, we have the <h1>
rendered.
Now change the text to read <h1>Hello world</h1>
and notice how the page is updated in the browser!
This leads us nicely to discuss pages in Astro — what I consider the entry point to our application.
Introduction to Astro pages
Astro leverages a file-based routing system and achieves this by using the files in the src/pages
directory.
For example, the src/pages/index.astro
file corresponds to the index
page served in the browser.
Let’s go ahead and create an src/pages/about.astro
page with similar content to index.astro
as shown below:
- Copy and paste the exact content of
index.astro
inabout.astro
. - Change the
<h1>
to have the textAbout us
.
Now, if we navigate to /about
in the browser, we should have the new page rendered.
What makes a valid Astro page?
We’ve defined Astro pages as files in the src/pages/
directory. Unfortunately, this is only partly correct.
For example, if we duplicate the favicon.svg
file in public/favicon.svg
into the pages
directory, does this represent a favicon
page?
Even though index.astro
and about.astro
correspond to our website’s index and about pages, /favicon
will return a 404: Not found
error.
This is because only specific files make a valid astro page. For example, if we consider the index
and about
files in the pages
directory, you perhaps notice something: they both have the .astro
file ending!
In layperson’s terms, these are Astro files, but a more technical terminology for these is Astro components.
So, quick quiz: what is an Astro component?
That’s easy: a file with the .astro
ending.
10 points to you! Well done.
Anatomy of an Astro component
We’ve established that index.astro
and about.astro
represent Astro components and are valid Astro pages.
Now, let’s dig into the content of these files.
Consider the contents of the index.astro
page:
Notice the distinction between the two parts of this file’s content.
The section at the bottom contains the page’s markup:
This part is called the component template section.
While the top section contains a rather strange divider-looking syntax:
This part is called the component script section, and the ---
is called fence.
Together, these make up an Astro component.
Let’s take the component script section for a spin.
The section’s name hints at what this section of the component does. Within the component script code fence, we may declare variables, import packages and fully take advantage of JavaScript or TypeScript.
Oh yes, TypeScript!
Let’s start by creating a variable to hold our user’s profile picture, as shown below:
We may then take advantage of the component template section to reference this image as shown below:
Note that the profilePicture
variable is referenced using curly braces { }
. This is how to reference variables from the component script in the component markup.
Now we should have the image rendered on the home page:
It’s not much, but it’s honest work, eh?
Let’s go ahead and flesh out the page to have the user’s profile markup:
As you might have noticed, we’re writing HTML looking syntax in the component markup section!
Now we should have the user photo and their bio rendered in the browser as follows:
Component styles
Styling in Astro is relatively easy to reason about. Add a <style>
tag to a component, and Astro will automatically handle its styling.
While it’s possible to select elements directly, let’s go ahead and add classes to the component markup for ease:
Add a <style>
tag, and write CSS as usual!
The user details should now be styled as expected.
If we inspect the eventual styles applied to our UI elements via the browser developer tools, we’ll notice that the style selectors look different.
For example, to style the user name, we’ve written the following CSS:
However, what’s applied in the browser looks something like this:
Why is this?
The actual style declarations for the h1
element remain unchanged. The only difference here is the selector.
The h1
element now has auto-generated class names, and the selector is now scoped via the :where
CSS selector.
This is done internally by Astro. This makes sure the styles we write don’t leak beyond our component; for example, if we styled every h1
in our component as follows:
The eventual style applied in the browser will be similar to the following:
This will ensure all other h1
in our project remains the same, and this style only applies to our specific component h1
.
Page layouts
Please look at the pages of our completed application, and realise how they all have identical forms.
There’s a navigation bar, a footer, and some container that holds the page’s main content.
Should we repeat these similar UI structures across all pages?
Most people will answer “No”. So, is there a way to share reusable UI structures across pages?
Yes, yes, yes! This is where layouts come in.
Layouts are Astro components with a twist. They are used to provide reusable UI structures across pages, e.g., navigation bars and footers.
Conventionally, layouts are placed in the src/layouts
directory. This is not compulsory but a widespread pattern.
Let’s go ahead and create our first layout in src/layouts/Main.astro
. We’ll do this by moving away all the reusable UI structures currently in index.astro
as follows:
- We’ve moved the
<html>
,<head>
and<body>
elements to theMain.astro
layout. - We’ve also introduced a new
<meta name=description />
tag for SEO. - We’ve equally introduced a
<main>
element where we want the rest of our page to go in. - Note that the file name of the layout is capitalised, i.e.,
Main.astro
, notmain.astro
.
On the one hand, layouts are unique because they mostly do one thing - provide reusable structures. But, on the other hand, they aren’t unique. They are like other Astro components and can do everything a component can!
Rendering components and slots
Rendering an Astro component is similar to how you’d attempt to render an HTML element, e.g., we’d render a div by writing the following:
The same goes for Astro components.
To render the Main.astro
component, we’d do similar:
Let’s put this into practice. We may now use the Main.astro
layout in the index.astro
page. To do this, we will do the following:
- Import the
Main.astro
layout from"../layouts/Main.astro"
- Substitute the
<html>
,<head>
and<body>
elements for the<Main>
layout inindex.astro
.
If we checked our app, we’d have a blank index
page.
Why’s that?
Unlike HTML elements, the child elements in the <Main>
tag aren’t automatically rendered.
The <Main>
layout component is rendered, and nothing else. The child components aren’t. Hence, the empty page.
To render the child elements of an Astro component, we must specify where to render these using a <slot />
element.
Let’s add a <slot>
within Main.astro
:
We should now have our page rendered with the reusable layout in place.
Capitalising component names
We’ve capitalised the file name of the Main.astro
layout component but is this important?
Theoretically, the answer to that is no.
We could create a file with a lower cased name, e.g., mainLayout.astro
and import the component as follows:
This is perfectly correct.
However, where we encounter issues is if we name the imported component with a lowercase:
In this case, we’ll encounter issues when we attempt to render the component as the name collides with the standard HTML main
element.
For this reason, it’s common practice to capitalise both component file names and the imported variable name.
The global style directive
The Main.astro
layout is in place but doesn’t add much to our page. Let’s start by adding some styles for the headers and also centre the page’s content:
With this, we’ll have the main
element centred, but the headers, h1
and h2
remain unstyled.
This is because styles applied via the <style>
tag are locally scoped by default.
Can you tell me why?
The main
element resides in the Main.astro
layout. However, the header h1
and h2
exist in a different index.astro
component!
For our use case, we need global styles.
We need to break out of the default locally scoped styles the Astro component provides, but how do we do this?
Global styles can be a nightmare — except when truly needed. For such cases, Astro provides several solutions. The first is using what’s known as a global style template directive.
I know that sounds like a mouthful! However, in simple terms, template directives in Astro are different kinds of HTML attributes that can be used in Astro component templates3.
For example, to break out of the default locally scoped <style>
behaviour, we can add a is:global
attribute as shown below:
This will remove the local CSS scoping and make the styles available globally.
Custom fonts and global CSS
Base layout components like Main.astro
are a great place to have global properties such as global styles and custom fonts.
We’ve added global styles via the is:global
template directive, but alternatively, we could have all global styles imported into Main.astro
from a global.css
file.
In cases where a project requires importing some existing global css file, this is the more straightforward approach.
For example, let’s refactor our project to use global.css
. To do so, move the entire CSS content within the <style is:global>
element into src/styles/global.css
. Then import the styles in the Main.astro
component frontmatter:
This will load and inject style onto the page.
Now, let’s turn our attention to global fonts.
We will use the Google Inter font for the project, but how do we do this?
Technically speaking, to add Inter to our project, we must add the <link>
s to Inter on every page required.
However, instead of repeating ourselves on every page, we can leverage the shared Main.astro
layout component.
Go ahead and add the <link>
s to the Inter font as shown below:
We may now update the global.css
file to use the new font family:
And boom! We have sorted global fonts.
Independent Astro components
We’ve discussed two special types of Astro components: layouts and pages.
However, a working site is made up of more than just layouts and pages. For example, different blocks of user interfaces are typically embedded within a page. These independent and reusable blocks of user interfaces can also be represented using Astro components.
Let’s put this to practice by creating NavigationBar
and Footer
components to be used in the Main.astro
layout.
When creating components, a standard convention is to have them in the src/components
directory. Let’s go ahead and create one.
Let’s also create a NavigationBar
component:
Now render the NavigationBar
and Footer
as shown below:
Adding interactive scripts
An integral part of Astro’s philosophy is shipping zero JavaScript by default to the browser.
This means our pages get compiled into HTML pages with all JavaScript stripped away by default.
You might ask, what about all the JavaScript written in the component script section of an Astro component?
The component script and markup will be used to generate the eventual HTML page(s) sent to the browser.
For example, go ahead and add a simple console.log
to the frontmatter of the index.astro
page:
Inspect the browser console and notice how the log never makes it to the browser!
So, where’s the log?
Astro runs on the server. In our case, this represents our local development server. So, the console.log
will appear in the terminal where Astro serves our local application.
When we eventually build our application for production with npm run build
, Astro will output HTML files corresponding to our pages in src/pages
.
In this example, the Hello world!
message will be logged but not get into the compiled HTML pages.
To add interactive scripts, i.e., scripts that make it into the final HTML page build output, add a <script>
element in the component markup section.
For example, let’s move the console.log
from the frontmatter to the markup via a <script>
element:
We should have Hello world!
logged in the browser console!
Interactive theme toggle
Let’s put our newly found knowledge of client-side scripts to good use.
Create a new ThemeToggler.astro
component in the src/components
directory.
Add the following markup:
- For accessibility, the button has an
aria-label
ofTheme toggler
. - The SVG has a fixed width of
25px
, rendering two<path>
elements. - The first
<path>
visually represents a sun icon. The second is a moon icon. - By default, both icons (sun and moon) are rendered. Our goal is to toggle the displayed icon based on the active theme.
Then import the component and render it in the NavigationBar
:
Let’s add some <style>
to ThemeToggler
:
Now, we should have a decent-looking theme toggler.
The :global() selector
Let’s take a moment to consider the strategy we’ll use for toggling the theme.
We’ll toggle a CSS class on the root element whenever a user clicks the toggle.
For example, if the user was viewing the site in light mode and clicked to toggle, we’ll add a .dark
class to the root element and, based on that, apply dark-themed styles.
If the user is in dark mode, clicking the toggle will remove the .dark
class. We’ll refer to this as a class strategy for toggling dark mode.
Based on this strategy, we must update our local ThemeToggler
style to display the relevant icon depending on the global .dark
class.
To do this, we will leverage the :global
selector.
Here’s how we’d achieve this:
To see this at work, inspect the page via the developer tools, and add a dark
class to the root element. The toggle icon will be appropriately changed.
In practice, limit :global
only to appropriate use cases because mixing global and locally scoped component styles will become challenging to debug. However, this is permissible, given our use case.
Event Handling
We’ve handled the styles for our toggle, assuming a .dark
root class. Now, Let’s go ahead and handle the toggle click event with a <script>
element.
Notice that this is standard JavaScript. Nothing fancy going on here.
- The toggle is selected via
document.querySelector("button")
. - To set up an event listener, we use the
.addEventListener
method on the button. - On clicking the button, we toggle the class list on the root element: adding or removing the “dark” class.
With this in place, the toggle icon changes when clicked to either that of the sun or moon.
Excellent!
Theming via CSS variables
CSS variables4 are outstanding, and we’ll leverage them for theming our application.
Firstly, let’s go ahead and define the colour variables we’ll use in the project.
- Set the variables on the root HTML element to be globally scoped.
- A CSS variable is a property that begins with two dashes,
--
e.g.,--background
. - For simplicity, we’ll stick to the minimal grey palette above.
The first visual change we’ll make is to add the following color
and background
style declarations to the body
element:
With this seemingly simple change, we should now have the text and background colour of the body
react to clicking the toggle.
Finally, update the navigation links in NavigationBar
to reflect theme preferences:
Accessing global client objects
Question! 🙋🏼
Where should we access global objects such as window.localStorage
? Within an Astro component frontmatter or an interactive <script>
?
At this point, I hope the answer to the question is clear from previous examples.
Since Astro runs on the server, attempting to access a window
property within the frontmatter of a component will result in an error.
To access window
properties, we need the script to run on the client, i.e., in the browser. So, we must leverage one or more client-side scripts.
A good use case for this is remembering the user’s theme choice.
If users toggle their theme from light to dark and refresh the browser, they lose the selected theme state.
How about we save this state to the browser’s local storage and restore the selected theme upon refresh?
Well, let’s do that!
Here are the first steps we’ll take:
- Grab the current state of the theme, i.e., dark or light, when the theme toggle is clicked.
- Save the theme value to the browser’s local storage in the form:
Here’s that translated in code:
We have saved the theme to local storage but must now set the active theme as soon as the page is loaded and the script
is executed.
Here’s the annotated code required to achieve this:
Now, give this a try. First, toggle the theme and refresh to see the theme choice preserved!
The magic of scripts
Client-side scripts added via a <script>
may seem like your typical JavaScript vanilla JS, but they’re more capable in specific ways.
The most crucial point is that Astro processes these. This means within a <script>
, we can import other scripts or import npm packages, and Astro will resolve and package the script for use in the browser.
Another critical point is the <script>
fully supports TypeScript. For example, in our solution, we typed the parameter for the setInitialColourMode
function:
We don’t have to sacrifice type safety within the client <script>
elements and can go on to write standard TypeScript code. Astro will strip out the types at build time and only serve the processed JavaScript to the browser.
Here’s a summary of what Astro does:
- NPM packages and local files can be imported and will be bundled.
- TypeScript is fully supported within the
<script>
. - If a single
Astro
component with a<script>
is used multiple times on a page, the<script>
is processed and only included once. - Astro will process and insert the script in the
<head>
of the page with atype=module
attribute.❗️The implication of
type=module
is that the browser will defer the script, i.e., load in parallel and execute it only after the page’s parsed.
Leveraging inline scripts
By default, Astro processes <script>
s. However, to opt out of Astro’s default script processing, we may pass a is:inline
directive as shown below:
In the real world, we quickly realise that the defaults don’t always satisfy every project requirement.
For example, consider the unstyled flash of content when we refresh our home page. For a user who chose the dark theme previously, refreshing the page shows light-themed rendered content before changing to dark after the script is parsed.
This occurs because we restore the user-chosen theme only after the page’s HTML has been parsed, i.e, the default behaviour of processed Astro scripts.
To prevent this, we will use the is:inline
directive, which will make the script blocking, i.e., executed immediately and stops parsing until completed.
Since scripts with the is:inline
attribute aren’t processed, they’ll be added multiple times if used in reusable components that appear more than once on the page.
So, let’s go ahead and move the theme restoration code bit into Main.astro
— because the Main.astro
layout is only included once per page.
We’ll also make sure to add this within the <head>
of the layout, as shown below:
We’re explicitly adding this to the <head>
because Astro will not process the is:inline
script. As such, it won’t be moved to the head
by Astro.
Be careful with is:inline
as it removes the default non-blocking nature of scripts. But it’s ideal for this use case.
Open your developer tools and throttle the network. Then go ahead and refresh after toggling dark mode. We should have eradicated the flash of unstyled content!
Global selectors in scripts
Understanding how Astro processes the <script>
in our components helps us make informed decisions.
We know the <script>
will eventually be bundled and injected into our page’s <head>
.
However, consider our selector for registering the theme toggle clicks:
The problem with this seemingly harmless code is that document.querySelector
will return the first element that matches the selector — a button element.
This will be selected if we add a random button somewhere on the page before our theme toggle button.
This button, which has nothing to do with theme toggling, will now be responsible for toggling the user’s theme.
Clicking “donate to charity” now toggles the theme. This is unacceptable.
The lesson here is to be mindful of your DOM selectors and be specific where possible, e.g., via ids or classes:
Let’s refactor our solution to use a data attribute.
With the more specific selector, only an element with the data attribute theme-toggle
will be selected, leaving <button>Donate to charity</button>
out of our theme toggle business.
Markdown pages
We’ve established that not all file types are valid pages in Astro. We’ve seen Astro components as pages, but allow me to introduce markdown pages!
Markdown5 is a popular, easy-to-use markup language for creating formatted text. I’m sure my nan does not know markdown, so it’s safer to say it’s a famous text format among developers.
It’s no surprise Astro supports creating pages via markdown. So, let’s put this to the test.
We’ll create two new pages to replace our dead Philosophies
and Beyond technology
navigation links.
Create the pages with the following content:
These files are written in markdown syntax6.
As with Astro component pages, markdown pages eventually get compiled to standard HTML pages rendered in the browser. The same file-based routing is also used. For example, to access the philosophies
and beyond-tech
pages, visit the /philosophies
and /beyond-tech
routes, respectively.
Navigating between pages
Navigating between pages in Astro requires no magic wand. Surprise!
Astro uses the standard <a>
element to navigate between pages. This makes sense as each page is a separate HTML page.
Let’s update the navigation links to point to the new markdown pages as shown below:
Clicking any of these links should now lead us to their appropriate pages.
Markdown layouts
Let’s face it; we won’t be winning any design awards for our current markdown pages. This is because they seem off and don’t share the same layout as our existing page. Can we fix this?
You’ve probably realised I ask questions and then provide answers. All right, you’ve got me. So that’s my trick to make you think about a problem — hoverer brief — before explaining the solution.
Believe it or not, Astro component frontmatter was inspired by markdown! The original markdown syntax supports frontmatter for providing metadata about the document. For example, we could add a title
metadata as shown below:
This is excellent news because Astro leverages this to provide layouts for markdown pages!
Instead of the so dull I can’t take it page, we can utilise a layout to bring some reusable structure to all our markdown pages.
Let’s get started.
With Astro markdown pages, we can provide layouts for a markdown page by providing a layout frontmatter metadata as shown below:
First, let’s reuse the same Main.astro
layout by adding the following to both markdown pages:
The markdown pages should now reuse our existing layout with the theming, navigation and footer all set in place!
Since Main.astro
includes our global.css
files, let’s go ahead and provide some default global styles for paragraphs and lists:
We should now have these styles take effect on our markdown pages! Isn’t life better with shared layout components? 😉
Composing layouts
Layouts are Astro components, meaning we can compose them, i.e., render a layout in another.
For example, let’s create a separate Blog.astro
layout that composes our base Main.astro
layout.
Composing the layouts in this way means we can reuse all the good stuff in Main.astro
while extending Blog.astro
to include only blog-specific elements.
The separation of concern significantly improves legibility and forces each layout to have a single responsibility.
Now, at this point, the markdown pages have the same layout markup and styles from Main.astro
. We’ve made no customisations.
Component props
As we build reusable components, we often find situations where we must customise certain values within a component. For example, consider the <title>
in our Main.astro
layout component:
A hardcoded title
on every page where the Main.astro
layout is used is ridiculous.
To foster reusability, components can accept properties. These are commonly known as props.
Props are passed to components as attributes.
The prop values are then accessed via Astro.props
. This is better explained with an example.
Go ahead and update Main.astro
to accept a title
prop as shown below:
To enforce TypeScript checks, define the Props
type alias or interface.
For simplicity, I’ll stick to a type alias for the Main.astro
layout:
With the type declared, we’ll have TypeScript error(s) in files where we’ve used <Main>
without the required title
prop.
Update the index.astro
and Blog.astro
pages to pass a title
prop to Main.astro
:
Leveraging markdown frontmatter properties
All markdown pages in our application will have a title, subtitle and poster. Luckily, a great way to represent these is via frontmatter properties.
Update the markdown pages to now include these properties, as shown below.
Note that poster
points to image paths. These paths reference the public
directory. So /images/philosophies.jpg
points to an image in public/images/philosophies.jpg
.
If you’re coding along, feel free to download any image from Unsplash and move them to the public
directory.
Adding metadata to our markdown pages doesn’t do us any good if we can use them.
Luckily, markdown layouts have a unique superpower — they can access markdown frontmatter via Astro.props.frontmatter
.
Let’s go ahead and globally handle this in our Blog.astro
layout component. Below’s the component script section:
- The
MarkdownLayoutProps
utility type accepts a generic and returns the type for all the properties available to a markdown layout. So feel free to inspect the entire shape7. MarkdownLayoutProps
accepts our frontmatter property type definition as a generic, i.e.,title
,poster
andsubtitle
. These are properties we’ve added in the frontmatter of our Markdown pages.type Props = ...
orinterface Props {}
is how we provide types for an Astro component.- The final line deconstructs the properties from
Astro.props.frontmatter
with full TypeScript support.
Equally update the layout markup to render the image, title and subtitle:
Most of the markup is arguably standard. However, note the title.toLowerCase()
call for the poster image caption. This is possible because any valid JavaScript expression can be evaluated within curly braces { }
in the component markup.
Our markdown pages will now have styled titles, subtitles and poster images! With all this handled in one place — the markdown layout.
Interactive navigation state
Now that we’re pros at handling interactive scripts in Astro let’s go ahead and make sure that we style our active navigation links differently.
As with all things programming, there are different ways to achieve this, but we will go ahead and script this.
- Get the
pathname
from thelocation
object. This will be in the form"/beyond-tech"
,"/philosophies
or"/"
. - Since the
pathname
corresponds to thehref
on the anchor tag element, we may select the active anchor tag via:document.querySelector(`nav a[href="${pathname}"]`).
- Finally, we add the
active
class to the active anchor tag.
Finally, add the relevant style for the active tag:
Viola! We should now have the active anchor tag styled differently.
Component composition
Our first look at component composition was with the Main.astro
and Blog.astro
layouts. Let’s take this further.
Our goal is to create a set of different yet identical cards. Each card acts as a link to a blog and will have a title and some background gradient.
To achieve this, we’ll have a Cards.astro
component that renders multiple Card.astro
components.
Let’s start by creating Card.astro
and Cards.astro
.
To see the fruits of our labour, we must now import and render Cards
in the index.astro
page component.
Clicking any of the links will point to the respective blog page.
Let’s not forget to add the new work-summary.md
page:
There we go!
The template flow of data
As we’ve discussed, the data in the frontmatter runs on the server and is not available in the browser.
As we’ve built our application, we’ve frequently leveraged data in the frontmatter in the template section, as shown below:
This is easy to reason about for our static website. We know this will eventually be compiled into HTML.
However, consider a more robust markup that includes <style>
and <script>
elements. How do we reference data from the frontmatter in these markup sections?
One answer is via the define:vars
template directive.
define:vars
will pass our variables from the frontmatter into the client <script>
or <style>
. It’s important to note that only JSON serialisable values work here.
Let’s give this a shot.
We must reference the gradientFrom
and gradientTo
variables passed as props in our <style>
.
First, to make the variables available within <style>
, we’ll go ahead and use define:vars
as follows:
define:vars
accepts an object of variables we want available within <style>
.
The variables are defined but not used yet!
Now, we can reference the variables via custom properties (aka css variables) as shown below:
And voila!
Our cards are now more beautiful than ever.
The dark side of define:vars
We’ve seen define:vars
come in handy for using variables from the frontmatter of an Astro component. However, be careful when using define:vars
with scripts.
Using define:vars
with a <script>
is similar to using the is:inline
directive.
Astro will not bundle the script and will be added multiple times if the same component is rendered more than once on a page.
Here’s an example to make this clear.
In Card.astro
go ahead and add a <script>
with the define:vars
directive as follows:
Inspect the elements via the developer tools. You’ll notice that the <script>
is inlined and unprocessed, i.e., just as we’ve written it, apart from being wrapped in an immediately invoked function execution (IIFE).
The script is also added three times — with a different value of gradientFrom
for each rendered card.
With scripts, a better solution (except the inline behaviour is ideal for your use case) is to pass the data from the component frontmatter to the rendered element via data-
attributes and then access these via JavaScript.
For example, we may rewrite the previous solution as shown below:
Note that this is a contrived example and only retrieves the first card element with its associated gradientfrom
data. Still, this demonstrates how to prevent unwanted behaviours with define:vars
in <script>
s.
Loading multiple local files
Let’s go ahead and create a new blog
directory to hold some more markdown pages. The pages and their content are shown below:
We aim to list these blog titles on our home page. One way to do this would be to render all link elements in index.astro
manually:
This isn’t necessarily a wrong approach to getting this done. We will now have a list of the blogs, as expected.
A better solution is to use Astro.glob()
to load multiple files.
Astro.glob()
accepts a single URL
glob parameter of the files we’d like to import. glob()
will then return an array of the exports from the matching file.
Talk is cheap, so let’s put this into action.
Instead of manually writing out the list of blog articles, we will use Astro.glob()
to fetch all the blog posts:
-
Note the argument passed to
.glob
, i.e.,../pages/blogs/*.md
. This relative glob path represents all markdown files in theblogs
directory. -
Also note the typing provided.
.glob
implements a generic, which, in this case, represents the markdown frontmatter object type.
Now, we may replace the manual list with a dynamically rendered list, as shown below:
- Dynamically render the blog list using the
.map
array function. Astro.glob()
returns markdown properties including frontmatter andurl
whereblog.url
refers to the browser url path for the markdown file.
And voila! Same result with a much neater implementation.
Deploying a static Astro site
We’ve come a long way! Now, let’s deploy this baby to the wild.
Deploying a static website is relatively the same regardless of the technology used to create the site.
At the end of your deployment build, we’ll have static assets to deploy to any service we choose.
Once this is done, we must wire up a static web server to serve this content when your users visit the deployed site.
NB: a static web server is a web server that serves static content. It essentially serves any files (e.g., HTML, CSS, JS) the client requests.
This breaks down the process of deploying a static website into two:
(1) Create the static production assets (2) Serve the static assets via a static web server
Let’s do these.
1. Create static production assets
To build our application for production, run the command:
This will internally run the astro build
command and build our application production static assets.
By default, these assets will exist in the dist
folder.
2. Serve the static assets via a static web server
Choosing a web server will come down to your choice. I’ll go ahead and explain how to use Netlify. However, the steps you must take with your web server provider will look similar.
Go over to Netlify and create an account.
Once you create an account and sign in, you’ll find a manual section to deploy a site.
Now, click browse to upload
and upload the dist
folder containing our static production assets.
Once the upload is completed, you’ll have your site deployed with a random public URL, as shown below:
Visit the URL to view your newly deployed website!
The problem with manual deployments
Manual deployments are great for conceptually breaking down the process of deploying a static website.
However, in the real world, you may find this less optimal.
The main challenge here is that every change made to your website requires you to build the application and re-upload it to your server manually.
This is a well-known problem with a standardised solution. The solution involves automating the entire process of deploying static websites by connecting your website to a git provider.
Automating the deployment of a static website
Automating the deployment of a static website looks something like this:
Step 1: Write and push your code to a Git provider like GitHub.
Step 2: Connect the GitHub project to your static web server provider, e.g., Netlify.
Step 3: You provide your website’s build
command and the location of the built assets to your web server provider, e.g., Netlify.
Step 4: Your web server provider automatically runs the build command and serves your static assets.
Step 5: Anytime you make changes to the GitHub project, your web server provider picks up the changes and reruns step 4, i.e., automatically deploying your website changes.
To see this process in practice with Netlify, go over to your dashboard and connect a Git provider (step 1).
I’ll go ahead to select Github, authorise Netlify and select the GitHub project (step 2).
Once that’s selected, provide the settings for your application deployment (Step 3). By default, Netlify will suggest the build
and publish directory
. Check these to make sure there are no errors.
Hit deploy, and your site will be live in seconds (step 4).
To see the redeployment after a new change, push a new change to the connected git repository.
How fast is our Astro website?
Astro boasts of insanely fast websites compared to frameworks like React or Vue.
Let’s put this to the test by following the steps below:
- Visit the newly deployed website on Chrome.
- Open the Chrome developer tools.
- Go to the Lighthouse tab.
- Analyse the page load.
Here’s my result running the test:
If this were a school examination, we would have just scored A+ on performance without trying!
This is a fast website!
Feel free to run the test on other pages!
Conclusion
This has been a lengthy discourse on Astro! We’ve delved into building a project and learned a handful of Astro’s capabilities, from installation to project structure to the nuances of inline scripts and, eventually, project deployment.
Why stop here? We’ve only just scratched the surface.
Footnotes
-
For other editors, please see the official Astro site https://docs.astro.build/en/editor-setup/ ↩
-
What is a “.d.ts” file in TypeScript? https://medium.com/@ohansemmanuel/what-is-a-d-ts-file-in-typescript-2e2d90d58eca ↩
-
As we’ll see later, they can also be used in
.mdx
files. ↩ -
Don’t know CSS variables? Read my guide https://medium.com/free-code-camp/everything-you-need-to-know-about-css-variables-c74d922ea855 ↩
-
What is Markdown? https://en.wikipedia.org/wiki/Markdown ↩
-
The markdown syntax cheatsheet https://www.markdownguide.org/cheat-sheet/ ↩
-
Markdown layout properties: https://docs.astro.build/en/core-concepts/layouts/#markdown-layout-props ↩