Reference

Astro Cheatsheet

Production cheatsheet from ActiveWizards Astro + Cloudflare Pages deployment (147 pages).


Content Collections (src/content.config.ts)

Astro 5 uses glob loaders (replaces the old src/content/ magic folder convention).

import { defineCollection, z } from 'astro:content';
import { glob } from 'astro/loaders';

const blog = defineCollection({
  loader: glob({ pattern: '**/*.md', base: './src/content/blog' }),
  schema: z.object({
    title: z.string(),
    description: z.string(),
    publishedAt: z.coerce.date(),
    updatedAt: z.coerce.date().optional(),
    tags: z.array(z.string()).default([]),
    metaTitle: z.string().optional(),
    metaDescription: z.string().optional(),
    ogImage: z.string().optional(),
    readingTime: z.number().optional(),
  }),
});

export const collections = { blog };

Key points:

  • z.coerce.date() handles ISO date strings from frontmatter
  • .default([]) prevents undefined on optional arrays
  • Run astro sync after changing schemas to regenerate types


Dynamic Routes ([...slug].astro)

Rest parameter [...slug] catches nested paths. Requires getStaticPaths() for SSG.

---
import { getCollection, render } from 'astro:content';
import BlogLayout from '../../layouts/BlogLayout.astro';

export async function getStaticPaths() {
  const posts = await getCollection('blog');
  return posts.map((post) => ({
    params: { slug: post.id },
    props: { post },
  }));
}

const { post } = Astro.props;
const { Content } = await render(post);
---

<BlogLayout title={post.data.title}>
  <Content />
</BlogLayout>
  • post.id is the filename without extension (e.g., my-post from my-post.md)
  • post.data contains validated frontmatter fields
  • render() returns { Content } component for markdown body


Pagination

Manual pagination (AW approach)

Page 1 handled by blog/index.astro, pages 2+ by blog/[page].astro:

---
export async function getStaticPaths() {
  const POSTS_PER_PAGE = 12;
  const allPosts = await getCollection('blog');
  const totalPages = Math.ceil(allPosts.length / POSTS_PER_PAGE);

  return Array.from({ length: totalPages - 1 }, (_, i) => ({
    params: { page: String(i + 2) },
    props: { currentPage: i + 2 },
  }));
}

const { currentPage } = Astro.props;
const allPosts = await getCollection('blog');
const sorted = allPosts.sort((a, b) =>
  new Date(b.data.publishedAt).getTime() - new Date(a.data.publishedAt).getTime()
);
const startIndex = (currentPage - 1) * POSTS_PER_PAGE;
const paginatedPosts = sorted.slice(startIndex, startIndex + POSTS_PER_PAGE);
---

Built-in paginate() (alternative)

---
export async function getStaticPaths({ paginate }) {
  const posts = await getCollection('blog');
  return paginate(posts, { pageSize: 12 });
}
const { page } = Astro.props;
// page.data = current page items
// page.currentPage, page.lastPage, page.url.prev, page.url.next
---

Layout Chaining

<!-- BaseLayout.astro -->
<html>
  <head>...</head>
  <body>
    <Nav />
    <main><slot /></main>
    <Footer />
  </body>
</html>

<!-- BlogLayout.astro -->
---
import BaseLayout from './BaseLayout.astro';
interface Props { title: string; description: string; }
const { title, description } = Astro.props;
---
<BaseLayout title={title} description={description}>
  <article class="prose">
    <h1>{title}</h1>
    <slot />
  </article>
</BaseLayout>

Component Props

---
interface Props {
  title: string;
  variant?: 'primary' | 'secondary';
  items: string[];
}

const { title, variant = 'primary', items } = Astro.props;
---

Slots (Default + Named)

<!-- Card.astro -->
<div class="card">
  <div class="card-header"><slot name="header" /></div>
  <div class="card-body"><slot /></div>
  <div class="card-footer"><slot name="footer" /></div>
</div>

<!-- Usage -->
<Card>
  <h2 slot="header">Title</h2>
  <p>Body content goes in default slot</p>
  <button slot="footer">Action</button>
</Card>

Styles

Scoped (default):