Astro is a fast and performant framework that supports components from other frameworks, crazy! It’s fast because of it’s IslandArchitecture. Docs.
New project
- Install node or any other package manager you like, use their set up wizard.
create astro
- Designate your directory, should be empty.
- Choose template and let it install dependencies.
- Run using
npm run dev
to launch development server. This is whatindex.astro
will look like:
---
---
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width" />
<meta name="generator" content={Astro.generator} >
<title>Astro</title>
</head>
<body>
<h1>Astro</h1>
</body>
</html>
Basics
Covering the basics of the Astro framework, check out the tutorial.
Completed Tutorial Code
“You can find the code for the project in this tutorial on GitHub or open a working version in an online code environment like IDX or StackBlitz.”
Astro pages
These files are .astro
and live in src/pages/
have two portions.
- The frontmatter block designated with the
---
, Astro will make use of this info. The tutorial will cover it later. - The HTML markup. These work like regular pages, all the HTML elements work as expected.
Dynamic content
The data in the frontmatter can be used in the HTML, we can also write JS between the ---
“Code fences” .
We can write any JS expression in the code fences because Astro’s frontmatter is in JavaScript not YAML or TOML like Markdown.
To add the dynamic data or JS expressions in the HTML, simply write it in {}
for example: <h1>{pageTitle}</h1>
.
Conditional rendering
When you’ve predicates or values to check you can conditionally render markup like so:
{happy && well && <p>I am excited to be learning Astro!</p>}
{well && <p>I am ready to be learning Astro!</p>}
{finished && <p>I finished this tutorial!</p>}
{goal === 3 ? <p>My goal is to finish in 3 days.</p> : <p>My goal is not 3 days.</p>}
Breakdown:
- In the
{}
a predicate is followed by&&
then another one and then the markup to be rendered. - To evaluate a condition we use a Ternary style operator using
?
and:
.
Tip
“Astro’s templating syntax is similar to JSX syntax. If you’re ever wondering how to use your script in your HTML, then searching for how it is done in JSX is probably a good starting point!”
Styling pages
Astro like most component-based frameworks allow us to style single pages or import from a CSS sheet.
Single page
Using <style></style>
tags, in the HTML header.
We can define styles for elements, classes, and IDs as we’d regularly do.
The cool part is that the styles in the <style>
tag can reference any variable in the Astro frontmatter, and then use them the same way you’d use CSS variables --var(varName)
.
Global styles
These will live in the src/styles/
directory, whether you have one global sheet or more is up to your design choices.
To import the style sheet into a page use the conventional JS syntax:
import '.../styles/styles.css'
and voila just like that your styles are in, and can be combined with in-page styles, allowing site-wide styles and page specific ones too.
Info
The local styles will override the global ones if there are conflicts.
Components
A Component is a reusable block of UI code, and often includes the markup, styles, and logic.
Components should live in the src/components/
directory.
Components can be imported using the import
keyword in the importers code fences.
Note
The key distinction between a component and a page is where it lives in your
src
folder, Astro takes note of this. Astro components will automatically create a new page on your site when they’re in thesrc/pages/
Astro Props
Like with React and Svelte Astro also supports props for its components. //likely because it supports all these frameworks as well...
To assign (receive) props in your components unpack them into variables or constants and use Astro.props
:
---
const { platform, username } = Astro.props; // this unpacks the props
---
<a href={`https://www.${platform}.com/${username}`}>{platform}</a>
To pass them simply set them similar to how you would an attribute, if they’re expressions then place them in {}
:
<Atrocomp platform="twitter" username="astrodotbuild" />
Scripts
In any .astro
file, we typically write the JS expressions in the code fences. But when we want client-side JS we need to write it in <script>
tags.
Like traditional vanilla setups, these script tags go inside the HTML <body>
write before its closing tag that way the page loads up before the script does.
Importing JS
Most prefer to write in JavaScript modules, add them to src/scripts/
and then import them IN the script tag.
Info
The JS expressions in the code fences are executed at build time to create the static HTML including the dynamic data in them then it’s discarded so not great for interactivity. Hence the need to import JS modules in the
<script>
tag, so they’re sent TO the browser.
Layouts
We use Layouts to share the reused components and styles that will be common throughout a website. To do this, you will:
- Create reusable layout components
- Pass content to your layouts with
- Pass data from Markdown frontmatter to your layouts
- Nest multiple layouts
Creating layouts
These live in src/layouts/
folder, create layouts the same way you’d create a regular page.
The interesting part, the one that involves modularity regarding the content that goes into the layout is powered by Astro’s <slot />
tag.
Info
“The
<slot />
allows you to inject (or “slot in”) child content written between opening and closing<Component></Component>
tags to anyComponent.astro
file.”
Passing props
Done the same way you’d pass them to any component.
Layouts and Frontmatter
Adding a layout
frontmatter property to a Markdown file ensures that ALL the values in the MD’s frontmatter are available to the layout. //Astronomical!
Tip
“When using layouts, you now have the option of including elements, like a page title, in the Markdown content or in the layout. Remember to visually inspect your page preview and make any adjustments necessary to avoid duplicated elements”
Nesting Layouts
To nest layouts:
- Import the wrapper-layout (let’s call it layout A) into the layout that’s to be wrapped, layout B.
- Then wrap the content of B with A
- Refactor and clean up accordingly, remove any HTML boiler plate if present as it’s likely included in A. Example of nesting a layout within another:
<BaseLayout pageTitle={frontmatter.title}>
<p>{frontmatter.pubDate.toString().slice(0,10)}</p>
<p><em>{frontmatter.description}</em></p>
<p>Written by: {frontmatter.author}</p>
<img src={frontmatter.image.url} width="300" alt={frontmatter.image.alt} />
<slot />
</BaseLayout>
Notice how it looks like nesting any other component/element in another.
Astro API
The Astro API enable us to work with our files and do lots of cool stuff like tagging pages, RSS feeds, and more. Key points:
import.meta.glob()
to access data from files in your projectgetStaticPaths()
to create multiple pages (routes) at once- The Astro RSS package to create an RSS feed #RSSFeeds are pretty neat and are a great way to create automatically updated curated feeds from different websites. Wikipedia
Import Global meta data from MD files for a blog
We can import the metadata (located in their frontmatter) for a bunch of pages. using:
const allPosts = Object.values(import.meta.glob('./posts/*.md', { eager: true }));
then to render them in a dynamic list we map them:
---
import BaseLayout from '../layouts/BaseLayout.astro'
const allPosts = Object.values(import.meta.glob('./posts/*.md', { eager: true }));
const pageTitle = "My Astro Learning Blog";
---
<BaseLayout pageTitle={pageTitle}>
<p>This is where I will post about my journey learning Astro.</p>
<ul>
<!-- This line maps the objects' titles into list items -->
{allPosts.map((post: any) => <li><a href={post.url}>{post.frontmatter.title}</a></li>)}
</ul>
</BaseLayout>
The list blog posts is now being generated dynamically using Astro’s built-in TypeScript support, by mapping over the array returned by import.meta.glob()
.
Now if a new blog post MD file is added to src/pages/posts/
it’ll automatically be included in this dynamic list, but be sure that it has all the necessary metadata in its frontmatter properties.
Generating tag pages
You can create sets of pages dynamically using .astro
files that export a getStaticPaths()
function.
The
getStaticPaths()
functionIt exports an array of page routes, and can include props as well.
Create pages dynamically
Create a file at src/pages/tags/[tag].astro
, which is peculiar due to the []
…
Example:
---
import BaseLayout from '../../layouts/BaseLayout.astro';
export async function getStaticPaths() {
return [
{ params: { tag: "Astro" } },
{ params: { tag: "successes" } },
{ params: { tag: "community" } },
{ params: { tag: "blogging" } },
{ params: { tag: "setbacks" } },
{ params: { tag: "Webdev" } },
{ params: { tag: "Software" } },
{ params: { tag: "learning in public" } },
];
}
const { tag } = Astro.params;
---
<BaseLayout pageTitle={tag}>
<p>Posts tagged with {tag}</p>
</BaseLayout>
The getStaticPaths
function returns an array of page routes, and all of the pages at those routes will use the same template defined in the file.
// We're building a template for dynamically generated files (pages).
Now ensure that your blog posts include at least one tag in their frontmatter. Tags are written in an array.
E.g. tags: ["software", "webdev", "Astro"]
.
Tip
Best to keep tags lower case. If they contain spaces then sub those spaces with the escape character
%20
in the browser’s search bar.
To make data from pages (blog posts in this example) available to the generated tag pages, we need to pass the pass the props using the following syntax: {params: {tag: "tagName"}, props: {posts: allProps}}
.
For Example:
---
import BaseLayout from '../../layouts/BaseLayout.astro';
export async function getStaticPaths() {
const allPosts = Object.values(import.meta.glob('../posts/*.md', { eager: true }));
return [
{ params: { tag: "astro" }, props: {posts: allPosts}},
{ params: { tag: "successes" }, props: {posts: allPosts} },
{ params: { tag: "community" }, props: {posts: allPosts}},
{ params: { tag: "blogging" }, props: {posts: allPosts} },
{ params: { tag: "setbacks" }, props: {posts: allPosts} },
{ params: { tag: "webdev" }, props: {posts: allPosts} },
{ params: { tag: "software" }, props: {posts: allPosts} },
{ params: { tag: "learning in public" } },
];
}
const { tag } = Astro.params;
const {posts} = Astro.props
const filteredPosts = posts.filter((post : any) => post.frontmatter.tags.includes(tag))
---
<BaseLayout pageTitle={tag}>
<p>Posts tagged with {tag}</p>
<ul>
{filteredPosts.map((post:any) =>{
<li><a href={post.frontmatter.title}></a></li>
})}
</ul>
</BaseLayout>
Breakdown:
- Define and export the
getStaticPaths()
function. Which is async btw - We import all the metadata using
import.meta.glob()
- Define the tag params and their props.
- Unpack the tag and props using
Astro.params
andAstro.props
respectively. - Use them to build the structure of the generated pages.
Takeaway
“If you need information to construct the page routes, write it inside
getStaticPaths()
. To receive information in the HTML template of a page route, write it outsidegetStaticPaths()
.”
Advanced JS to generate pages from tags
Previously the tags were statically defined in the the src/pages/tags/[tag].astro
but we can use some JS to generate pages for each tag used in the blogs, in Astro this is called DynamicRouting .
- Verify that all the blog posts MD files have
tags
array property. - Create a new array of all the tags, preferably a Set to avoid duplicates.
const uniqueTags = [...new Set(allPosts.map((post: any) => post.frontmatter.tags).flat())];
This line uses the spread operator to dynamically update theuniqueTags
array, even if empty, with the elements in the new Set that’s mapping all the tags in theallPosts
array. - Now we update the tags in the exported function:
---
import BaseLayout from '../../layouts/BaseLayout.astro';
export async function getStaticPaths() {
const allPosts = Object.values(import.meta.glob('../posts/*.md', { eager: true }));
return uniqueTags.map((tag) => {
const filteredPosts = allPosts.filter((post: any) => post.frontmatter.tags.includes(tag));
return {
params: { tag },
props: { posts: filteredPosts },
};
});
}
const { tag } = Astro.params;
const {posts} = Astro.props
---
<BaseLayout pageTitle={tag}>
<p>Posts tagged with {tag}</p>
<ul>
{posts.map((post: any) => <BlogPost url={post.url} title={post.frontmatter.title}/>)}
</ul>
</BaseLayout>
Breakdown:
- Replaced the static tags with a mapping function to get all tags and respective props, while also filtering.
- Then updated the rendered list with another map function to render blog posts.
Tag Index
The /pages/folder/index.astro
routing pattern
- Create a new file
index.astro
in the directorysrc/pages/tags/
. - Navigate to
http://localhost:4321/tags
and verify that your site now contains a page at this URL. It will be empty, but it will exist. - Create a minimal page at
src/pages/tags/index.astro
that uses your layout. You have done this before!- Create a new page component
src/pages/tags
calledindex.astro
. - Import and use the Base Layout
- Define a page title, and pass as component attribute to the layout.
- Create a new page component
- Check your browser preview again and you should have a formatted page, ready to add content to!
This is rather example specific so follow along here docs. But the final result is something like:
---
import BaseLayout from '../../layouts/BaseLayout.astro';
const allPosts = Object.values(import.meta.glob('../posts/*.md', { eager: true }));
const tags = [...new Set(allPosts.map((post: any) => post.frontmatter.tags).flat())];
const pageTitle = "Tag Index";
---
<BaseLayout pageTitle={pageTitle}>
<div class="tags">
{tags.map((tag) => (
<p class="tag"><a href={`/tags/${tag}`}>{tag}</a></p>
))}
</div>
</BaseLayout>
<style>
a {
color: #00539F;
}
.tags {
display: flex;
flex-wrap: wrap;
}
.tag {
margin: 0.25em;
border: dotted 1px #a1a1a1;
border-radius: .5em;
padding: .5em 1em;
font-size: 1.15em;
background-color: #F8FCFD;
}
</style>
Astro Islands
This enables us to bring frontend framework components into our Astro sites. Allows us to:
- Add a UI framework, Preact, to your Astro project
- Use Preact to create an interactive greeting component
- Learn when you might not choose islands for interactivity
Astro Integrations
Adding Preact
To add Preact to our Astro project we need to run npx astro add preact
.
Now we can add the thinnest possible React alternative, Preact, there is to make fast and lightweight frontend components, in a bit…
Now we can build Preact components in .JSX
files in scr/components/
Example:
import { useState } from 'preact/hooks';
export default function Greeting({messages}) {
const randomMessage = () => messages[(Math.floor(Math.random() * messages.length))];
const [greeting, setGreeting] = useState(messages[0]);
return (
<div>
<h3>{greeting}! Thank you for visiting!</h3>
<button onClick={() => setGreeting(randomMessage())}>
New Greeting
</button>
</div>
);
}
// it's like baby React heheh Then import and use it wherever you like.
Astor Directives
AstroDirectives tell Astro to send and rerun its JavaScript on the client side.
client:load
directive
Reruns JS at the client side when the page loads, making the component interactive. This is called a HydratedComponent.
A component with the client:load
directive will re-render after the page is loaded, and any interactive elements that it has will work.
Astro Directives
“There are other client: directives to explore. Each sends the JavaScript to the client at a different time.
client:visible
, for example, will only send the component’s JavaScript when it is visible on the page.”
Switching Themes with vanilla JS and CSS
- Give the toggle button an SVG icon with a specific ID.
- Create classes for this SVG for both Dark and Light mode because the icon should also change.
- Add the icon to the navbar
- Set up the styles for the Dark mode (or Light mode if it’s not default).
- Now add the JS logic to your toggle component. Ex:
---
---
---
<button id="themeToggle">
<svg></svg>
</button>
<style>
.sun { fill: black; }
.moon { fill: transparent; }
:global(.dark) .sun { fill: transparent; }
:global(.dark) .moon { fill: white; }
</style>
<script is:inline>
const theme = (() => {
const localStorageTheme = localStorage?.getItem("theme") ?? '';
if (['dark', 'light'].includes(localStorageTheme)) {
return localStorageTheme;
}
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
return 'dark';
}
return 'light';
})();
if (theme === 'light') {
document.documentElement.classList.remove('dark');
} else {
document.documentElement.classList.add('dark');
}
window.localStorage.setItem('theme', theme);
const handleToggleClick = () => {
const element = document.documentElement;
element.classList.toggle("dark");
const isDark = element.classList.contains("dark");
localStorage.setItem("theme", isDark ? "dark" : "light");
}
document.getElementById("themeToggle")?.addEventListener("click", handleToggleClick);
</script>