Astro Learning Guide
Table of Contents
- Creating Your First Astro Project
- Basic Page Structure
- Creating Pages
- Working with Markdown
- Styling in Astro
- Component Composition
- CSS Concepts in Astro
- Dynamic Content
- Client-Side JavaScript
- Astro Islands
- Layouts and Slots
- 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:
- Confirm
y
to install create-astro - Enter a folder name for your project (e.g.,
./tutorial
) - Type
y
to install dependencies - Type
y
to initialize a new git repository
Setting Up Your Development Environment
-
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
-
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
-
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
- Use
Ctrl + J
(macOS:Cmd ⌘ + J
) to toggle the terminal visibility in VS Code - Stop the dev server with
Ctrl + C
- Restart the server with
npm run dev
if it stops unexpectedly - The terminal will provide feedback while previewing your site
- You cannot run other commands while the dev server is running
Basic Page Structure
A basic Astro page consists of two main parts:
- Frontmatter (between
---
) - 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
- Pages go in
src/pages/
.astro
files automatically become pages- File path becomes the URL path
Navigation
<a href="/">Home</a>
<a href="/about/">About</a>
<a href="/blog/">Blog</a>
Working with Markdown
Creating Blog Posts
- Create
.md
files insrc/pages/posts/
- 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
- 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;
}
- 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
- Parent components import their child components
- Child component styles and functionality travel with them
- Pages only need to import their direct children
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:
- Prevents style conflicts
- Keeps styles isolated
- Allows same class names in different components
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
- Using && operator:
{happy && <p>I am happy!</p>}
- Using ternary operator:
{happy ? <p>I am happy!</p> : <p>I am not happy</p>}
- 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
-
Code Organization
- Use descriptive names
- Comment complex logic
- Keep components modular
-
Styling
- Use scoped styles
- Leverage CSS variables
- Follow consistent patterns
-
Performance
- Minimize client-side JS
- Optimize images
- Use static rendering when possible
Common Patterns
- Page layout structure
- Navigation links
- Blog post structure
- Styling organization
- Dynamic content rendering
Tips and Tricks
- Use CSS variables for consistent styling
- Leverage Markdown for blog posts
- Keep styles scoped when possible
- Use comments to explain complex logic
- Structure your project folders clearly
Client-Side JavaScript
Understanding Build Time vs Client-Side JavaScript
Astro handles JavaScript in two ways:
- Build Time JavaScript (in frontmatter):
- Runs during site building
- Used for generating static HTML
- “Thrown away” after build
- 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>
Option 2: External JavaScript File (Recommended)
- Create
src/scripts/menu.js
:
document.querySelector('.hamburger').addEventListener('click', () => {
document.querySelector('.nav-links').classList.toggle('expanded');
});
- Import in your pages:
<script>
import "../scripts/menu.js";
</script>
Best Practices for Client-Side JavaScript
-
Script Placement:
- Place scripts before closing
</body>
tag - Use external files for reusable code
- Place scripts before closing
-
Performance:
- Keep client-side JavaScript minimal
- Use build-time JavaScript when possible
-
Organization:
- Keep scripts in a dedicated
/scripts
directory - Use meaningful file names
- Keep scripts in a dedicated
-
Error Handling:
- Use optional chaining (
?.
) for DOM queries - Add error handling for critical functionality
- Use optional chaining (
Common Use Cases for Client-Side JavaScript
-
User Interaction:
- Toggle menus
- Form validation
- Click handlers
-
DOM Manipulation:
- Adding/removing classes
- Updating content
- Showing/hiding elements
-
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
-
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
-
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
-
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 />
-
-
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
-
Use Framework Components When:
- Need complex state management
- Building reusable interactive components
- Want to leverage framework features
- Porting existing framework components
-
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
-
Performance Optimization
- Only use islands where interactivity is needed
- Choose appropriate client directives
- Minimize framework JavaScript
-
Component Design
- Keep interactive components focused
- Split complex UI into smaller islands
- Use static components where possible
-
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:
- Content between layout tags gets inserted at the slot location
- Multiple slots can be used with named slots
- Slots can have fallback content
Example with fallback:
<slot>
<p>Default content if no child content provided</p>
</slot>
Passing Props to Layouts
- From Page to Layout:
// Page
<BaseLayout pageTitle="About Me" customClass="about-page">
<p>Page content</p>
</BaseLayout>
// Layout
const { pageTitle, customClass } = Astro.props;
- Using Props in Layout:
<title>{pageTitle}</title>
<div class={customClass}>
<slot />
</div>
Best Practices for Layouts
-
Component Organization:
- Keep layouts in
src/layouts/
- Use descriptive names (e.g.,
BaseLayout.astro
,BlogPost.astro
)
- Keep layouts in
-
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>
-
Refactoring Tips:
- Move common elements to layout
- Keep page-specific styles in pages
- Pass dynamic content as props
- Use slots for flexible content insertion
-
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
- 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
-
Avoid Duplication
- Move common elements to the layout
- Remove duplicated content from Markdown files
- Use frontmatter for metadata
-
Common Layout Elements
- Title
- Publication date
- Author information
- Description
- Featured image
- Tags
-
Data Flow
- YAML frontmatter values are passed as props
- Access via
frontmatter
object in layout - Use
<slot />
for unique blog content
-
Tips
- Format dates using
toString().slice(0,10)
- Use conditional rendering for optional elements
- Style common elements consistently across posts
- Format dates using
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
-
Dynamic Routes
- Use
[tag].astro
for dynamic page generation getStaticPaths()
defines all possible routes- Each route gets its own props
- Use
-
Data Processing
- Extract unique tags using Set
- Filter posts by tag
- Pass filtered posts as props
-
Component Reuse
- Use same tag styling across components
- Maintain consistent UI for tags
- Share BlogPost component for listings
Best Practices
-
Tag Management
- Always use arrays for tags in frontmatter
- Keep tags consistent across posts
- Use lowercase for tag values
-
Styling
- Keep tag styles consistent
- Use flexbox for responsive layouts
- Maintain visual hierarchy
-
Performance
- Generate routes at build time
- Filter posts before passing as props
- Reuse components for consistency