Astro Learning Guide

Table of Contents

  1. Creating Your First Astro Project
  2. Basic Page Structure
  3. Creating Pages
  4. Working with Markdown
  5. Styling in Astro
  6. Component Composition
  7. CSS Concepts in Astro
  8. Dynamic Content
  9. Client-Side JavaScript
  10. Astro Islands
  11. Layouts and Slots
  12. TypeScript in Astro

Creating Your First Astro Project

Project Setup

The preferred way to create a new Astro site is through the create astro setup wizard.

Run the following command in your terminal using your preferred package manager:

# create a new project with npm
npm create astro@latest -- --template minimal

Follow the setup wizard prompts:

  1. Confirm y to install create-astro
  2. Enter a folder name for your project (e.g., ./tutorial)
  3. Type y to install dependencies
  4. Type y to initialize a new git repository

Setting Up Your Development Environment

  1. Open VS Code

    • Open your project folder in VS Code
    • Install recommended extensions when prompted
    • Make sure to install the Astro language support extension for syntax highlighting and autocompletion
  2. Start the Development Server

    npm run dev
    • Astro will start running in development mode
    • The server typically runs at http://localhost:4321 (or port 3000 for older versions)
    • Hot module reloading is enabled by default
  3. View Your Website

    • Click the localhost link in your terminal
    • You should see the Astro “Empty Project” starter website
    • The initial page will show “Astro” at the top of a blank white page

Development Server Tips

Basic Page Structure

A basic Astro page consists of two main parts:

  1. Frontmatter (between ---)
  2. HTML template
---
// Frontmatter (JavaScript/TypeScript)
const pageTitle = "My Page";
---

<!-- HTML template -->
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta name="generator" content={Astro.generator} />
    <title>{pageTitle}</title>
  </head>
  <body>
    <h1>{pageTitle}</h1>
  </body>
</html>

Creating Pages

File Structure

<a href="/">Home</a>
<a href="/about/">About</a>
<a href="/blog/">Blog</a>

Working with Markdown

Creating Blog Posts

  1. Create .md files in src/pages/posts/
  2. Add frontmatter with metadata:
---
title: 'My First Blog Post'
pubDate: 2022-07-01
description: 'Description here'
author: 'Author Name'
image:
    url: 'image-url'
    alt: 'Image description'
tags: ["tag1", "tag2"]
---

Linking to Posts

<ul>
  <li><a href="/posts/post-1/">Post 1</a></li>
</ul>

Styling in Astro

Local Styles

<style>
  h1 {
    color: purple;
    font-size: 4rem;
  }
</style>

CSS Variables

---
const skillColor = "navy";
const fontWeight = "bold";
const textCase = "uppercase";
---

<style define:vars={{skillColor, fontWeight, textCase}}>
  .skill {
    color: var(--skillColor);
    font-weight: var(--fontWeight);
    text-transform: var(--textCase);
  }
</style>

Global Styles

  1. Create src/styles/global.css:
html {
  background-color: #f1f5f9;
  font-family: sans-serif;
}

body {
  margin: 0 auto;
  width: 100%;
  max-width: 80ch;
  padding: 1rem;
  line-height: 1.5;
}
  1. Import in pages:
---
import '../styles/global.css';
---

Component Composition

Nested Components

Components can be nested within other components. For example:

// Footer.astro imports and uses Social.astro
import Social from './Social.astro';

<footer>
  <Social platform="twitter" username="astrodotbuild" />
</footer>

Component Hierarchy

Components can be nested within other components:

index.astro
  └── Footer.astro
       └── Social.astro

CSS Concepts in Astro

Scoped Styles

Astro automatically scopes styles to their components:

<!-- Social.astro -->
<style>
  a {
    color: white;    /* Only affects links in this component */
    background: purple;
  }
</style>

Benefits:

Interactive Styling

Add hover effects and transitions:

a {
  padding: 0.5rem 1rem;
  border: 2px solid white;
  border-radius: 8px;
  transition: all 0.3s ease;
}

a:hover {
  background-color: #7c3aed;
  transform: translateY(-2px);
}

Note: When using scoped styles in Astro components, you can opt-out of scoping using the is:global directive:

<style is:global>
  /* These styles will be applied globally */
  a {
    color: blue;
  }
</style>

<style>
  /* These styles will be scoped to the component */
  a {
    color: red;
  }
</style>

Dynamic Content

Conditional Rendering

  1. Using && operator:
{happy && <p>I am happy!</p>}
  1. Using ternary operator:
{happy ? <p>I am happy!</p> : <p>I am not happy</p>}
  1. Using IIFE:
{(() => {
    if (happy) {
        return <p>I am happy!</p>;
    }
    return null;
})()}

Working with Data

---
const skills = ["HTML", "CSS", "JavaScript"];
const identity = {
  firstName: "Sarah",
  country: "Canada"
};
---

<ul>
  {skills.map((skill) => <li>{skill}</li>)}
</ul>
<p>Name: {identity.firstName}</p>

Best Practices

  1. Code Organization

    • Use descriptive names
    • Comment complex logic
    • Keep components modular
  2. Styling

    • Use scoped styles
    • Leverage CSS variables
    • Follow consistent patterns
  3. Performance

    • Minimize client-side JS
    • Optimize images
    • Use static rendering when possible

Common Patterns

  1. Page layout structure
  2. Navigation links
  3. Blog post structure
  4. Styling organization
  5. Dynamic content rendering

Tips and Tricks

  1. Use CSS variables for consistent styling
  2. Leverage Markdown for blog posts
  3. Keep styles scoped when possible
  4. Use comments to explain complex logic
  5. Structure your project folders clearly

Client-Side JavaScript

Understanding Build Time vs Client-Side JavaScript

Astro handles JavaScript in two ways:

  1. Build Time JavaScript (in frontmatter):
    • Runs during site building
    • Used for generating static HTML
    • “Thrown away” after build
  2. Client-Side JavaScript (in <script> tags):
    • Sent to the browser
    • Runs on user interaction
    • Handles dynamic functionality

Creating an Interactive Hamburger Menu

1. Component Structure

Create the Hamburger component (src/components/Hamburger.astro):

---
---
<div class="hamburger">
  <span class="line"></span>
  <span class="line"></span>
  <span class="line"></span>
</div>

2. Styling

Add to src/styles/global.css:

/* Hamburger styles */
.hamburger {
  padding-right: 20px;
  cursor: pointer;
}

.hamburger .line {
  display: block;
  width: 40px;
  height: 5px;
  margin-bottom: 10px;
  background-color: #ff9776;
}

/* Navigation styles */
.nav-links {
  width: 100%;
  top: 5rem;
  left: 48px;
  background-color: #ff9776;
  display: none;
  margin: 0;
}

/* Responsive design */
@media screen and (min-width: 636px) {
  .nav-links {
    margin-left: 5em;
    display: block;
    position: static;
    width: auto;
    background: none;
  }

  .hamburger {
    display: none;
  }
}

/* Utility classes */
.expanded {
  display: unset;
}

3. Adding Interactivity

Option 1: Inline Script

Add directly to your page:

<script>
  document.querySelector('.hamburger')?.addEventListener('click', () => {
    document.querySelector('.nav-links')?.classList.toggle('expanded');
  });
</script>
  1. Create src/scripts/menu.js:
document.querySelector('.hamburger').addEventListener('click', () => {
  document.querySelector('.nav-links').classList.toggle('expanded');
});
  1. Import in your pages:
<script>
  import "../scripts/menu.js";
</script>

Best Practices for Client-Side JavaScript

  1. Script Placement:

    • Place scripts before closing </body> tag
    • Use external files for reusable code
  2. Performance:

    • Keep client-side JavaScript minimal
    • Use build-time JavaScript when possible
  3. Organization:

    • Keep scripts in a dedicated /scripts directory
    • Use meaningful file names
  4. Error Handling:

    • Use optional chaining (?.) for DOM queries
    • Add error handling for critical functionality

Common Use Cases for Client-Side JavaScript

  1. User Interaction:

    • Toggle menus
    • Form validation
    • Click handlers
  2. DOM Manipulation:

    • Adding/removing classes
    • Updating content
    • Showing/hiding elements
  3. Dynamic Updates:

    • Fetching data
    • Updating UI states
    • Handling user input

Astro Islands

Astro Islands (also known as Component Islands) are a key feature that allows you to add interactive UI elements to your static Astro site.

Understanding Astro Islands

  1. What are Islands?

    • Isolated pieces of interactive UI within a static HTML page
    • Can be built using different UI frameworks (React, Preact, Vue, Svelte, etc.)
    • Only hydrated when needed using client directives
  2. Key Characteristics

    • Default to static HTML with zero client-side JavaScript
    • Become interactive only when explicitly hydrated
    • Can coexist with other framework components on the same page
    • Allow mixing different frameworks in one project

Client Directives

Client directives control how UI framework components are hydrated:

<!-- Static HTML only, no JavaScript -->
<PreactComponent />

<!-- Hydrates immediately when page loads -->
<PreactComponent client:load />

<!-- Hydrates when component becomes visible -->
<PreactComponent client:visible />

Framework Components vs Astro Scripts

  1. Framework Components

    • Require explicit client directives for interactivity

    • Ship framework-specific JavaScript

    • Good for complex interactive features

    • Example:

      ---
      import PreactCounter from '../components/PreactCounter';
      ---
      <PreactCounter client:load />
  2. Astro <script> Tags

    • Always send JavaScript to the browser

    • Use vanilla JavaScript

    • Good for simple interactions

    • Example:

      <button id="themeToggle">Toggle Theme</button>
      <script>
        document.getElementById('themeToggle')
          .addEventListener('click', () => {
            document.body.classList.toggle('dark');
          });
      </script>

When to Use Islands

  1. Use Framework Components When:

    • Need complex state management
    • Building reusable interactive components
    • Want to leverage framework features
    • Porting existing framework components
  2. Use Astro Scripts When:

    • Need simple DOM manipulation
    • Want to avoid framework overhead
    • Don’t need complex state management
    • Want to minimize JavaScript sent to client

Example: Interactive Greeting Component

// src/components/Greeting.jsx
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>
  );
}

Using the component:

---
import BaseLayout from '../layouts/BaseLayout.astro';
import Greeting from '../components/Greeting';
---
<BaseLayout>
  <Greeting client:load messages={["Hi", "Hello", "Howdy", "Hey there"]} />
</BaseLayout>

Best Practices

  1. Performance Optimization

    • Only use islands where interactivity is needed
    • Choose appropriate client directives
    • Minimize framework JavaScript
  2. Component Design

    • Keep interactive components focused
    • Split complex UI into smaller islands
    • Use static components where possible
  3. Framework Selection

    • Choose frameworks based on needs
    • Consider bundle size impact
    • Use consistent frameworks when possible

Layouts and Slots

Creating Layout Components

Layouts are special components that provide a reusable page structure. They live in src/layouts/ and help reduce duplicate code across pages.

Basic Layout Structure

// src/layouts/BaseLayout.astro
---
import Header from '../components/Header.astro';
import Footer from '../components/Footer.astro';
import '../styles/global.css';
const { pageTitle } = Astro.props;  // Receive props from pages
---
<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>{pageTitle}</title>
  </head>
  <body>
    <Header />
    <h1>{pageTitle}</h1>
    <slot />  <!-- Child content will be inserted here -->
    <Footer />
    <script>
      import "../scripts/menu.js";
    </script>
  </body>
</html>

Using Layouts

Basic Usage

// src/pages/index.astro
---
import BaseLayout from '../layouts/BaseLayout.astro';
const pageTitle = "Home Page";
---
<BaseLayout pageTitle={pageTitle}>
  <h2>My awesome blog subtitle</h2>
</BaseLayout>

Understanding Slots

The <slot /> element is a placeholder for child content:

Example with fallback:

<slot>
  <p>Default content if no child content provided</p>
</slot>

Passing Props to Layouts

  1. From Page to Layout:
// Page
<BaseLayout pageTitle="About Me" customClass="about-page">
  <p>Page content</p>
</BaseLayout>

// Layout
const { pageTitle, customClass } = Astro.props;
  1. Using Props in Layout:
<title>{pageTitle}</title>
<div class={customClass}>
  <slot />
</div>

Best Practices for Layouts

  1. Component Organization:

    • Keep layouts in src/layouts/
    • Use descriptive names (e.g., BaseLayout.astro, BlogPost.astro)
  2. Style Management:

    • Move common styles to layout
    • Use is:global for page-specific styles that affect layout elements
    <style is:global define:vars={{skillColor}}>
      h1 { color: var(--skillColor); }
    </style>
  3. Refactoring Tips:

    • Move common elements to layout
    • Keep page-specific styles in pages
    • Pass dynamic content as props
    • Use slots for flexible content insertion
  4. Common Elements to Include:

    • <html>, <head>, and <body> tags
    • Meta tags and common scripts
    • Header and footer components
    • Global styles

Layout Inheritance

Layouts can be nested for more complex page structures:

// BlogLayout.astro
---
import BaseLayout from './BaseLayout.astro';
---
<BaseLayout>
  <div class="blog-container">
    <slot />  <!-- Blog content goes here -->
  </div>
</BaseLayout>

TypeScript in Astro

Type Safety in Components

Astro provides built-in TypeScript support. Here’s how to add type safety to your components:

---
// Define the props interface
interface Props {
  title: string;
  items: string[];
  optional?: boolean;
}

// Destructure with type checking
const { title, items, optional = false } = Astro.props;
---

<div>
  <h1>{title}</h1>
  <ul>
    {items.map((item) => <li>{item}</li>)}
  </ul>
  {optional && <p>Optional content</p>}
</div>

Error Handling

When working with dynamic data or external APIs, always implement proper error handling:

---
// Example of fetching data with error handling
let data;
try {
  const response = await fetch('https://api.example.com/data');
  if (!response.ok) {
    throw new Error(`HTTP error! status: ${response.status}`);
  }
  data = await response.json();
} catch (error) {
  console.error('Error fetching data:', error);
  return Astro.redirect('/error');
}
---

{data ? (
  <DisplayData data={data} />
) : (
  <p>Error loading data. Please try again later.</p>
)}

Creating Blog Post Layouts

Setting Up a Markdown Post Layout

  1. Create a new layout file for blog posts:
// src/layouts/MarkdownPostLayout.astro
---
const { frontmatter } = Astro.props;
---
<h1>{frontmatter.title}</h1>
<p>Published on: {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 />

Using the Layout in Blog Posts

Add the layout to your Markdown files:

// src/pages/posts/post-1.md
---
layout: ../../layouts/MarkdownPostLayout.astro
title: 'My First Blog Post'
pubDate: 2022-07-01
description: 'This is the first post of my new Astro blog.'
author: 'Astro Learner'
image:
    url: 'https://docs.astro.build/assets/rose.webp'
    alt: 'The Astro logo on a dark background with a pink glow.'
tags: ["astro", "blogging", "learning in public"]
---

Welcome to my _new blog_ about learning Astro!

Best Practices for Blog Layouts

  1. Avoid Duplication

    • Move common elements to the layout
    • Remove duplicated content from Markdown files
    • Use frontmatter for metadata
  2. Common Layout Elements

    • Title
    • Publication date
    • Author information
    • Description
    • Featured image
    • Tags
  3. Data Flow

    • YAML frontmatter values are passed as props
    • Access via frontmatter object in layout
    • Use <slot /> for unique blog content
  4. Tips

    • Format dates using toString().slice(0,10)
    • Use conditional rendering for optional elements
    • Style common elements consistently across posts

Example: Progressive Enhancement

Start with a basic layout:

---
const { frontmatter } = Astro.props;
---
<h1>{frontmatter.title}</h1>
<p>Written by {frontmatter.author}</p>
<slot />

Then enhance with more features:

---
const { frontmatter } = Astro.props;
---
<article class="blog-post">
  <header>
    <h1>{frontmatter.title}</h1>
    <div class="metadata">
      <p>Published on: {frontmatter.pubDate.toString().slice(0,10)}</p>
      <p>Written by: {frontmatter.author}</p>
    </div>
    {frontmatter.image && (
      <img src={frontmatter.image.url} width="300" alt={frontmatter.image.alt} />
    )}
    <p class="description"><em>{frontmatter.description}</em></p>
  </header>
  
  <div class="content">
    <slot />
  </div>

  {frontmatter.tags && (
    <footer>
      <h2>Tags</h2>
      <ul class="tags">
        {frontmatter.tags.map((tag: string) => (
          <li>{tag}</li>
        ))}
      </ul>
    </footer>
  )}
</article>

Tag System Implementation

Creating Dynamic Tag Pages

Astro allows creating dynamic routes using square brackets notation. Here’s how to implement a tag system:

1. Tag Page Structure

// src/pages/tags/[tag].astro
---
import BaseLayout from '../../layouts/BaseLayout.astro';
import BlogPost from '../../components/BlogPost.astro';

export async function getStaticPaths() {
  const allPosts = Object.values(import.meta.glob('../posts/*.md', { eager: true }));
  const uniqueTags = [...new Set(allPosts.map((post: any) => post.frontmatter.tags).flat())];
  
  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>

2. Tag Index Page

// src/pages/tags/index.astro
---
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())];
---
<BaseLayout pageTitle="Tags">
  <div class="tags">
    {tags.map((tag) => (
      <p class="tag">
        <a href={`/tags/${tag}`}>{tag}</a>
      </p>
    ))}
  </div>
</BaseLayout>

<style>
  .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;
  }
  a {
    color: #00539F;
  }
</style>

3. Adding Tags to Blog Posts

In your MarkdownPostLayout:

// src/layouts/MarkdownPostLayout.astro
---
const { frontmatter } = Astro.props;
---
<BaseLayout pageTitle={frontmatter.title}>
  <!-- existing content -->
  
  <div class="tags">
    {frontmatter.tags.map((tag: string) => (
      <p class="tag">
        <a href={`/tags/${tag}`}>{tag}</a>
      </p>
    ))}
  </div>
</BaseLayout>

<style>
  .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;
  }
  a {
    color: #00539F;
  }
</style>

Key Concepts

  1. Dynamic Routes

    • Use [tag].astro for dynamic page generation
    • getStaticPaths() defines all possible routes
    • Each route gets its own props
  2. Data Processing

    • Extract unique tags using Set
    • Filter posts by tag
    • Pass filtered posts as props
  3. Component Reuse

    • Use same tag styling across components
    • Maintain consistent UI for tags
    • Share BlogPost component for listings

Best Practices

  1. Tag Management

    • Always use arrays for tags in frontmatter
    • Keep tags consistent across posts
    • Use lowercase for tag values
  2. Styling

    • Keep tag styles consistent
    • Use flexbox for responsive layouts
    • Maintain visual hierarchy
  3. Performance

    • Generate routes at build time
    • Filter posts before passing as props
    • Reuse components for consistency