Project Commands
# Dev server
npm run dev
npx astro dev -- --port 3000
# Build
npm run build
npx astro build
# Preview build locally
npm run preview
# Check types
npx astro check
# Add integration
npx astro add cloudflare
npx astro add tailwind
Component Basics
---
// Frontmatter (runs at build/request time, never in browser)
import Header from '../components/Header.astro';
interface Props {
title: string;
count?: number;
}
const { title, count = 0 } = Astro.props;
const items = await fetch('https://api.example.com/items').then(r => r.json());
---
<Header />
<h1>{title}</h1>
<p>Count: {count}</p>
{items.length > 0 && (
<ul>
{items.map((item) => (
<li>{item.name}</li>
))}
</ul>
)}
<style>
/* Scoped to this component by default */
h1 { color: navy; }
</style>
<script>
// Runs in the browser
console.log('client-side');
</script>
Slots
<!-- Layout.astro -->
<div class="layout">
<header><slot name="header" /></header>
<main><slot /></main> <!-- default slot -->
<footer><slot name="footer">Default footer</slot></footer>
</div>
<!-- Usage -->
<Layout>
<h1 slot="header">Title</h1>
<p>Main content goes in default slot</p>
<nav slot="footer">Custom footer</nav>
</Layout>
Styles
<!-- Scoped (default) -->
<style>
h1 { color: red; }
</style>
<!-- Global -->
<style is:global>
body { margin: 0; }
</style>
<!-- Target child component elements -->
<style>
.content :global(h2) {
margin-top: 2rem;
}
</style>
<!-- Inline via define:vars -->
---
const color = 'red';
---
<style define:vars={{ color }}>
h1 { color: var(--color); }
</style>
<!-- External stylesheet -->
<link rel="stylesheet" href="/styles/global.css" />
<!-- Import in frontmatter -->
---
import '../styles/global.css';
---
Content Collections
// src/content/config.ts
import { defineCollection, z } from 'astro:content';
const posts = defineCollection({
type: 'content', // markdown/mdx
schema: z.object({
title: z.string(),
description: z.string(),
pubDate: z.coerce.date(),
tags: z.array(z.string()).default([]),
draft: z.boolean().default(false),
}),
});
const authors = defineCollection({
type: 'data', // json/yaml
schema: z.object({
name: z.string(),
avatar: z.string().optional(),
}),
});
export const collections = { posts, authors };
---
// Query collections
import { getCollection, getEntry } from 'astro:content';
// All entries
const posts = await getCollection('posts');
// Filtered
const published = await getCollection('posts', ({ data }) => !data.draft);
// Single entry
const post = await getEntry('posts', 'my-post-slug');
// Render content
const { Content } = await post.render();
---
<Content />
Routing
src/pages/
index.astro -> /
about.astro -> /about/
posts/
index.astro -> /posts/
[slug].astro -> /posts/:slug/
[...slug].astro -> /posts/* (catch-all)
tags/
[tag].astro -> /tags/:tag/
---
// Static paths (SSG)
export async function getStaticPaths() {
const posts = await getCollection('posts');
return posts.map((post) => ({
params: { slug: post.slug },
props: post,
}));
}
const post = Astro.props;
---
---
// Dynamic (SSR) - no getStaticPaths needed
export const prerender = false;
const { slug } = Astro.params;
---
SSR vs SSG
---
// Per-page SSR opt-in
export const prerender = false;
// Access request data (SSR only)
const url = Astro.url;
const params = Astro.url.searchParams;
const cookie = Astro.cookies.get('session');
const headers = Astro.request.headers;
---
---
// Per-page SSG opt-in (when default is SSR)
export const prerender = true;
---
// astro.config.mjs
import { defineConfig } from 'astro/config';
import cloudflare from '@astrojs/cloudflare';
export default defineConfig({
output: 'server', // default SSR
// output: 'static', // default SSG (default)
// output: 'hybrid', // default SSG, opt-in SSR
adapter: cloudflare(),
});
Data Fetching
---
// Fetch at build/request time
const res = await fetch('https://api.example.com/data', {
headers: { 'Authorization': `Bearer ${import.meta.env.API_KEY}` }
});
if (!res.ok) {
return Astro.redirect('/error');
}
const data = await res.json();
---
Environment Variables
# .env
PUBLIC_SITE_URL=https://example.com # available client-side
API_KEY=secret123 # server only
---
// Server-side (frontmatter)
const key = import.meta.env.API_KEY;
// Platform-specific (Cloudflare, Vercel, etc.)
const apiKey = Astro.locals.runtime?.env?.API_KEY;
---
<script>
// Client-side (only PUBLIC_ prefixed)
console.log(import.meta.env.PUBLIC_SITE_URL);
</script>
Redirects and Responses
---
// Redirect
return Astro.redirect('/login', 302);
// Custom response (API route)
// src/pages/api/data.ts
export async function GET({ request }) {
return new Response(JSON.stringify({ ok: true }), {
headers: { 'Content-Type': 'application/json' }
});
}
export async function POST({ request }) {
const body = await request.json();
return new Response(JSON.stringify({ received: body }), { status: 201 });
}
---
Script Handling
<!-- Bundled (default) - processed, deduped -->
<script>
import { Poline } from 'poline';
const p = new Poline({ numPoints: 5 });
</script>
<!-- Inline (no processing) -->
<script is:inline>
alert('runs exactly as written');
</script>
<!-- External module -->
<script src="../scripts/main.ts"></script>
Built-in Components
---
import { Image } from 'astro:assets';
import myImage from '../assets/photo.png';
---
<!-- Optimized image -->
<Image src={myImage} alt="Description" width={600} />
<!-- Remote image -->
<Image src="https://example.com/img.png" alt="Remote" width={400} height={300} />
Integrations Config
// astro.config.mjs
import { defineConfig } from 'astro/config';
import cloudflare from '@astrojs/cloudflare';
import sitemap from '@astrojs/sitemap';
import mdx from '@astrojs/mdx';
export default defineConfig({
site: 'https://example.com',
output: 'hybrid',
adapter: cloudflare({
imageService: 'compile',
}),
integrations: [sitemap(), mdx()],
markdown: {
shikiConfig: {
theme: 'github-dark',
},
},
});