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

  1. Install node or any other package manager you like, use their set up wizard. create astro
  2. Designate your directory, should be empty.
  3. Choose template and let it install dependencies.
  4. Run using npm run dev to launch development server. This is what index.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.

  1. The frontmatter block designated with the ---, Astro will make use of this info. The tutorial will cover it later.
  2. 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 the src/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 any Component.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:

  1. Import the wrapper-layout (let’s call it layout A) into the layout that’s to be wrapped, layout B.
  2. Then wrap the content of B with A
  3. 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 project
  • getStaticPaths() 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() function

It 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 and Astro.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 outside getStaticPaths().”

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 .

  1. Verify that all the blog posts MD files have tags array property.
  2. 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 the uniqueTags array, even if empty, with the elements in the new Set that’s mapping all the tags in the allPosts array.
  3. 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

  1. Create a new file index.astro in the directory src/pages/tags/.
  2. 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.
  3. Create a minimal page at src/pages/tags/index.astro that uses your layout. You have done this before!
    1. Create a new page component src/pages/tags called index.astro.
    2. Import and use the Base Layout
    3. Define a page title, and pass as component attribute to the layout.
  4. 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

  1. Give the toggle button an SVG icon with a specific ID.
  2. Create classes for this SVG for both Dark and Light mode because the icon should also change.
  3. Add the icon to the navbar
  4. Set up the styles for the Dark mode (or Light mode if it’s not default).
  5. 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>