Report Date: November 30, 2025 Framework Version: Astro 5.x Research Focus: Current best practices for production-ready Astro development
Table of Contents #
- Project Structure and Organization Patterns
- Content Collections Best Practices
- TypeScript Configuration Recommendations
- Performance Optimization Techniques
- Security Best Practices
- Testing Approaches for Astro Projects
- Static Site Generation Patterns
1. Project Structure and Organization Patterns #
Standard Directory Layout #
Astro projects follow an opinionated folder structure that balances convention with flexibility:
project-root/
├── src/
│ ├── pages/ # Required - defines routes
│ ├── components/ # Reusable components (Astro, React, Vue, etc.)
│ ├── layouts/ # Page layout templates
│ ├── styles/ # CSS/Sass files
│ ├── content/ # Content collections (Markdown, MDX, etc.)
│ └── middleware/ # Request/response processing
├── public/ # Static assets (copied as-is)
├── astro.config.mjs # Astro configuration
├── tsconfig.json # TypeScript configuration
└── package.json # Dependencies and scripts
Key Directory Guidelines #
src/pages/ - Required Directory
- The only mandatory directory in Astro projects
- Files here automatically become routes based on file-based routing
- Without it, your site will have no pages or routes
- Supports
.astro,.md,.mdx, and framework component files
src/components/ - Recommended Convention
- Store reusable UI components (Astro and framework components)
- Optional but highly recommended for organization
- Can be organized by feature, type, or framework
src/layouts/ - Layout Templates
- Define shared UI structures across multiple pages
- Common layouts: BaseLayout, BlogPostLayout, DocumentationLayout
- Conventional but not mandatory
src/content/ - Content Collections
- Organize collections with each subdirectory representing a named collection
- While Astro 5's Content Layer API allows collections anywhere, this structure maintains clarity
- Example:
src/content/blog/,src/content/docs/
public/ - Static Assets
- Files served untouched during builds
- Not bundled or optimized by Astro
- Ideal for:
robots.txt,favicon.ico, manifests, pre-optimized images - Referenced in HTML with root-relative paths:
/favicon.ico
File Naming Conventions #
Component Files:
- Use
.astrofor reusable components and page layouts - Only
.astrofiles can handle Astro API calls likegetStaticPaths() - Framework components (
.jsx,.vue,.svelte) for interactive elements
Consistent Naming:
- Use lowercase with dashes instead of spaces:
blog-post.astro,user-profile.tsx - Makes content easier to find and organize
- Improves cross-platform compatibility
Project Organization Recommendations #
- Modular Development: Organize components by feature or domain
- Code Splitting: Astro handles this by default for optimal loading
- Import Aliases: Configure path aliases in
tsconfig.json:
1{
2 "compilerOptions": {
3 "baseUrl": ".",
4 "paths": {
5 "@components/*": ["src/components/*"],
6 "@layouts/*": ["src/layouts/*"],
7 "@content/*": ["src/content/*"]
8 }
9 }
10}
2. Content Collections Best Practices #
Content Layer API (Astro 5.0) #
Astro 5.0 introduces the Content Layer API, a fundamental improvement for content management:
- Unified, type-safe API for defining, loading, and accessing content
- Works with any source: local files, APIs, databases, CMS platforms
- Performance gains: 5x faster builds for Markdown, 2x faster for MDX
- Memory efficiency: 25-50% reduction in memory usage
Setting Up Content Collections #
1. Create Configuration File
Create src/content.config.ts (or .js, .mjs):
1import { defineCollection, z } from 'astro:content';
2import { glob } from 'astro/loaders';
3
4const blog = defineCollection({
5 loader: glob({ pattern: '**/*.md', base: './src/content/blog' }),
6 schema: z.object({
7 title: z.string(),
8 description: z.string(),
9 publishDate: z.date(),
10 author: z.string(),
11 tags: z.array(z.string()).optional(),
12 draft: z.boolean().default(false),
13 image: z.object({
14 src: z.string(),
15 alt: z.string()
16 }).optional()
17 })
18});
19
20export const collections = { blog };
2. TypeScript Requirements
Ensure these settings in tsconfig.json:
1{
2 "compilerOptions": {
3 "strictNullChecks": true,
4 "allowJs": true
5 }
6}
Recommended: Use "strict": true or extend astro/tsconfigs/strict for full type safety.
Schema Validation Best Practices #
Always Define Schemas
While optional, schemas are highly recommended because they:
- Enforce consistent frontmatter structure
- Provide automatic TypeScript typings
- Enable property autocompletion in editors
- Catch validation errors during builds
Use Zod Effectively
1import { z } from 'astro:content';
2
3// Make fields optional explicitly
4const schema = z.object({
5 title: z.string(), // Required
6 subtitle: z.string().optional(), // Optional
7 publishDate: z.date().transform((date) => new Date(date)), // Transform
8 status: z.enum(['draft', 'published', 'archived']), // Enum
9 readingTime: z.number().positive(), // Validated number
10 tags: z.array(z.string()).default([]) // Default value
11});
Key Zod Patterns:
- Everything is required by default - use
.optional()explicitly - Use
.transform()for data conversion (strings to dates, etc.) - Use
.default()to provide fallback values - Use
.enum()for constrained string values
Collection Organization #
Separate Collections by Content Type
If content represents different structures, use separate collections:
src/content/
├── blog/ # Blog posts
├── docs/ # Documentation
├── authors/ # Author profiles
└── projects/ # Project showcases
Cross-Collection References
Use the reference() function for relationships:
1import { defineCollection, z, reference } from 'astro:content';
2
3const authors = defineCollection({
4 schema: z.object({
5 name: z.string(),
6 bio: z.string()
7 })
8});
9
10const blog = defineCollection({
11 schema: z.object({
12 title: z.string(),
13 author: reference('authors'), // Single reference
14 relatedPosts: z.array(reference('blog')).optional() // Multiple
15 })
16});
Querying Content Collections #
Type-Safe Queries
1import { getCollection, getEntry } from 'astro:content';
2
3// Get all entries
4const allPosts = await getCollection('blog');
5
6// Filter entries
7const publishedPosts = await getCollection('blog', ({ data }) => {
8 return data.draft !== true;
9});
10
11// Get single entry
12const post = await getEntry('blog', 'my-first-post');
Typing Component Props
1---
2import type { CollectionEntry } from 'astro:content';
3
4interface Props {
5 post: CollectionEntry<'blog'>;
6}
7
8const { post } = Astro.props;
9---
Content Collection Maintenance #
Development Workflow
- Restart dev server or use sync command (
s + enter) after schema changes - Add
.astroto.gitignore- contains generated types - Use consistent naming for file slugs to ensure predictable URLs
Production Considerations
- Filter draft content in production builds
- Leverage incremental content caching for faster builds
- Use loaders for remote content sources (CMS, APIs)
3. TypeScript Configuration Recommendations #
Recommended Strictness Levels #
Astro provides three TypeScript configuration presets:
base- Minimal TypeScript support, permissivestrict- Recommended for TypeScript projectsstrictest- Maximum type safety
Recommendation: Use strict or strictest for production projects.
Strict Configuration #
Extending Astro's Strict Preset:
1{
2 "extends": "astro/tsconfigs/strict",
3 "compilerOptions": {
4 "baseUrl": ".",
5 "paths": {
6 "@components/*": ["src/components/*"],
7 "@layouts/*": ["src/layouts/*"],
8 "@utils/*": ["src/utils/*"]
9 },
10 "verbatimModuleSyntax": true
11 }
12}
Key Compiler Options #
Essential Settings:
1{
2 "compilerOptions": {
3 "strict": true, // Enable all strict checks
4 "strictNullChecks": true, // Required for content collections
5 "allowJs": true, // Support .js files
6 "verbatimModuleSyntax": true, // Enforce type imports
7 "isolatedModules": true, // Ensure file-level transpilation
8 "skipLibCheck": true, // Speed up compilation
9 "moduleResolution": "bundler", // Modern module resolution
10 "jsx": "react-jsx" // For JSX support
11 }
12}
Verbatim Module Syntax
Enabled by default in Astro's presets, this setting enforces import type for types:
1// Correct
2import type { CollectionEntry } from 'astro:content';
3import { getCollection } from 'astro:content';
4
5// Error - type should use 'import type'
6import { CollectionEntry, getCollection } from 'astro:content';
Multiple JSX Framework Configuration #
When using multiple frameworks (React, Preact, Solid), configure per-file JSX:
tsconfig.json:
1{
2 "compilerOptions": {
3 "jsx": "react-jsx",
4 "jsxImportSource": "react" // Default framework
5 }
6}
Per-File Override:
1/** @jsxImportSource preact */
2import { h } from 'preact';
3
4export function PreactComponent() {
5 return <div>Preact Component</div>;
6}
Path Aliases Best Practices #
Organize imports with clear aliases:
1{
2 "compilerOptions": {
3 "baseUrl": ".",
4 "paths": {
5 "@/*": ["src/*"],
6 "@components/*": ["src/components/*"],
7 "@layouts/*": ["src/layouts/*"],
8 "@lib/*": ["src/lib/*"],
9 "@utils/*": ["src/utils/*"],
10 "@content/*": ["src/content/*"],
11 "@assets/*": ["src/assets/*"]
12 }
13 }
14}
Usage:
1// Instead of: import { formatDate } from '../../../utils/date';
2import { formatDate } from '@utils/date';
Type Generation and Validation #
Automatic Type Generation
Astro automatically generates types for:
- Content collections schemas
- Environment variables (via
astro:env) - Route parameters
Manual Sync
Force type regeneration:
1npx astro sync
Do this when:
- Adding new content collections
- Modifying collection schemas
- After pulling changes from version control
Editor Integration #
VS Code Configuration
Install the official Astro extension for:
- Syntax highlighting for
.astrofiles - TypeScript IntelliSense
- Code formatting
- Error checking
- Debugging support
Why tsconfig.json Matters
Even if not writing TypeScript, tsconfig.json enables:
- NPM package imports in the editor
- Framework component type checking
- Better autocompletion
- Import path resolution
4. Performance Optimization Techniques #
Astro 5.0 Built-in Improvements #
Content Layer Performance
Astro 5.0 delivers significant build performance improvements:
- 5x faster for Markdown pages on content-heavy sites
- 2x faster for MDX content
- 25-50% reduction in memory usage
Server Islands Architecture
Server Islands extend the Islands Architecture to the server, enabling:
- Static HTML caching on Edge CDNs
- Independent loading of dynamic components
- Custom fallback content during island loading
- Individual cache lifetime per island
Build Optimization Strategies #
Real-world optimization can improve build speed from 35 pages/second to 127 pages/second (3.6x improvement):
1. Upgrade Node.js and Astro
1# Use latest LTS Node.js version
2node --version # Should be v20+ or v22+
3
4# Keep Astro updated
5npm update astro
Expected improvement: ~30% faster builds
2. Increase Memory Allocation
1# In package.json scripts
2{
3 "scripts": {
4 "build": "NODE_OPTIONS='--max-old-space-size=8192' astro build"
5 }
6}
Benefits:
- Reduced garbage collection pauses
- Eliminated memory-related crashes
- Faster processing of large sites
3. Configure Build Concurrency
1// astro.config.mjs
2export default defineConfig({
3 build: {
4 concurrency: 4 // Adjust based on CPU cores
5 }
6});
Optimal settings:
- 4 for most systems
- Too high causes resource contention
- Too low underutilizes CPU
4. Implement API Caching
Problem: Individual API calls per page during build significantly increase build times.
Solution: Cache API responses in getStaticPaths():
1// Bad: API call in component (runs for each page)
2---
3const data = await fetch('https://api.example.com/data');
4---
5
6// Good: Cache in getStaticPaths (runs once)
7export async function getStaticPaths() {
8 const data = await fetch('https://api.example.com/data');
9 const items = await data.json();
10
11 return items.map(item => ({
12 params: { slug: item.slug },
13 props: { item } // Pass as props
14 }));
15}
5. Move Large Assets to CDN
1// astro.config.mjs
2export default defineConfig({
3 build: {
4 assets: 'assets' // Customize asset directory
5 }
6});
Benefits:
- Reduced build time (no processing of large files)
- Faster page loads
- Lower hosting costs
Image Optimization #
Use Modern Formats
---
import { Image } from 'astro:assets';
import heroImage from '@assets/hero.jpg';
---
<Image
src={heroImage}
alt="Hero image"
format="webp" // or 'avif'
quality={80}
loading="lazy"
widths={[400, 800, 1200]}
sizes="(max-width: 800px) 100vw, 800px"
/>
Best practices:
- WebP/AVIF: Smaller file sizes, better quality
- Responsive widths: Load appropriate sizes per screen
- Lazy loading: Load images only when in viewport
- Quality settings: 80-85 is often sufficient
Image Component Benefits
Astro's <Image> component automatically:
- Optimizes images during build
- Generates multiple sizes
- Serves modern formats with fallbacks
- Adds proper width/height attributes (prevents layout shift)
CSS Optimization #
Automatic Optimizations
Astro handles CSS optimization by default:
- Minification
- Purification (removes unused styles)
- Critical CSS extraction
- Scoped styles per component
Best Practices
---
// Component-scoped styles (recommended)
---
<style>
h1 {
color: var(--primary-color);
}
</style>
<!-- Or global styles when needed -->
<style is:global>
:root {
--primary-color: #3b82f6;
}
</style>
Code Splitting
Astro automatically splits code:
- Each page gets only its required JavaScript
- Components are bundled efficiently
- Shared dependencies are extracted
Incremental Content Caching #
Enable for large content sites:
1// astro.config.mjs
2export default defineConfig({
3 experimental: {
4 contentCollectionCache: true
5 }
6});
Benefits:
- Reuses unchanged content entries
- Dramatically speeds up incremental builds
- Tracks changes via internal build manifest
Runtime Performance #
Minimize JavaScript
Astro's HTML-first philosophy:
- Zero JavaScript by default
- Add interactivity only where needed
- Framework components are islands of interactivity
View Transitions
Smooth page transitions without JavaScript overhead:
---
import { ViewTransitions } from 'astro:transitions';
---
<head>
<ViewTransitions />
</head>
Prefetching
<a href="/about" data-astro-prefetch>About</a>
5. Security Best Practices #
Content Security Policy (CSP) #
Astro 5.9 CSP Support
Astro 5.9 introduces experimental built-in CSP support - the framework's most upvoted feature request:
1// astro.config.mjs
2export default defineConfig({
3 experimental: {
4 security: {
5 csp: true
6 }
7 }
8});
Benefits:
- Ditch
unsafe-inlineworkarounds - Works with all adapters and runtimes
- Supports static sites, serverless, Node.js, edge runtimes
- Compatible with all frontend libraries
Hash-Based CSP Implementation
Astro uses hash-based CSP instead of nonces:
- Generates hashes for every script and stylesheet
- More complex but supports more use cases
- Works with static sites (nonce headers don't)
CSP via Meta Tag
For static sites and SPAs, Astro uses:
1<meta http-equiv="content-security-policy" content="...">
This approach works everywhere, not just server-rendered pages.
Static Site Security Advantages #
Inherent Security Benefits:
- Reduced Attack Surface: Pre-built HTML with minimal JavaScript
- No Server-Side Processing: Can't exploit server vulnerabilities
- XSS Protection: Static content can't inject malicious scripts
- No Database: No SQL injection risks
- Faster Patches: Rebuild and redeploy quickly
Default Security Posture
Astro renders entire pages to static HTML by default:
- Removes all JavaScript from final build (unless explicitly added)
- No runtime code execution vulnerabilities
- Combines performance with security
Environment Variable Security #
Type-Safe Environment Variables (astro:env)
1// astro.config.mjs
2import { defineConfig, envField } from 'astro/config';
3
4export default defineConfig({
5 env: {
6 schema: {
7 // Public client variable
8 PUBLIC_API_URL: envField.string({
9 context: "client",
10 access: "public",
11 default: "https://api.example.com"
12 }),
13
14 // Public server variable
15 BUILD_TIME: envField.string({
16 context: "server",
17 access: "public"
18 }),
19
20 // Secret server variable
21 API_SECRET: envField.string({
22 context: "server",
23 access: "secret"
24 }),
25
26 // Optional with validation
27 DB_URL: envField.string({
28 context: "server",
29 access: "secret",
30 optional: true
31 })
32 },
33 validateSecrets: true // Validate on start
34 }
35});
Security Rules:
- Client variables: Available in browser (public data only)
- Server variables: Server-side only, not in client bundle
- Secrets: Never exposed to client, validated at runtime
- No client secrets: Framework prevents this (no safe way to send)
Usage:
1// Server-side only
2import { API_SECRET } from 'astro:env/server';
3
4// Client and server
5import { PUBLIC_API_URL } from 'astro:env/client';
Security Best Practices Checklist #
Environment Variables:
- ✅ Never hardcode sensitive information
- ✅ Use
.envfiles, add to.gitignore - ✅ Use
astro:envfor type safety - ✅ Rotate secrets regularly
- ✅ Apply least privilege principle
- ✅ Use different secrets for dev/staging/production
Content Security:
- ✅ Enable CSP in production
- ✅ Validate user-generated content
- ✅ Sanitize Markdown/MDX inputs
- ✅ Use trusted content sources
Dependency Security:
1# Audit dependencies regularly
2npm audit
3
4# Fix vulnerabilities
5npm audit fix
6
7# Check for outdated packages
8npm outdated
Headers and Configuration:
1// astro.config.mjs
2export default defineConfig({
3 server: {
4 headers: {
5 'X-Frame-Options': 'DENY',
6 'X-Content-Type-Options': 'nosniff',
7 'Referrer-Policy': 'strict-origin-when-cross-origin',
8 'Permissions-Policy': 'geolocation=(), microphone=()'
9 }
10 }
11});
Static Site Generation Security #
Build-Time Security:
- Environment variables evaluated at build time
- Can't change after deployment
- Predictable, immutable output
Best Practices:
- Validate inputs at build time
- Use CSP to lock down allowed resources
- Implement ISR carefully - understand caching implications
- Audit third-party scripts before including
- Monitor dependencies for vulnerabilities
6. Testing Approaches for Astro Projects #
Testing Strategy Overview #
Astro supports comprehensive testing approaches:
- Unit tests: Test individual functions and utilities
- Component tests: Test Astro and framework components
- End-to-end tests: Test complete user flows
Recommended tools:
- Vitest: Unit and component testing
- Playwright: End-to-end testing
- Cypress: Alternative E2E option
Vitest Setup for Unit Testing #
Installation:
1npm install -D vitest
Configuration (vitest.config.ts):
1import { getViteConfig } from 'astro/config';
2
3export default getViteConfig({
4 test: {
5 globals: true,
6 environment: 'happy-dom' // or 'jsdom'
7 }
8});
Key Points:
- Use
getViteConfig()to integrate with Astro's settings - Auto-detects Astro config by default
- Choose DOM library:
happy-dom(faster) orjsdom(more compatible)
Custom Configuration (Astro 4.8+):
1export default getViteConfig(
2 { test: { /* ... */ } },
3 {
4 site: 'https://example.com',
5 trailingSlash: 'always'
6 }
7);
Component Testing with Container API #
Astro 4.9+ Container API
The Container API enables native Astro component testing:
1import { experimental_AstroContainer as AstroContainer } from 'astro/container';
2import { expect, test } from 'vitest';
3import Card from '../src/components/Card.astro';
4
5test('Card component renders correctly', async () => {
6 const container = await AstroContainer.create();
7 const result = await container.renderToString(Card, {
8 props: {
9 title: 'Test Card',
10 description: 'Test description'
11 }
12 });
13
14 expect(result).toContain('Test Card');
15 expect(result).toContain('Test description');
16});
Testing with Slots:
1test('Card with slot content', async () => {
2 const container = await AstroContainer.create();
3 const result = await container.renderToString(Card, {
4 slots: {
5 default: '<p>Slot content</p>'
6 }
7 });
8
9 expect(result).toContain('Slot content');
10});
Environment Configuration:
1// At top of test file
2/// @vitest-environment happy-dom
3
4import { expect, test } from 'vitest';
Vitest 2.0 Browser Mode #
Enhanced Component Testing:
Vitest 2.0 introduced Browser Mode:
- Built on Playwright
- Renders components in iframe with real browser events
- More accurate than JSDOM
- Less error-prone than snapshot testing
Configuration:
1export default {
2 test: {
3 browser: {
4 enabled: true,
5 name: 'chromium',
6 provider: 'playwright'
7 }
8 }
9};
Compatibility Considerations #
Astro 5 + Vitest Issues:
Recent compatibility notes:
- Vitest reverted Vite 6 support in v2.1.7
- Update to Vitest 3.0.5+ for Astro 5 compatibility
- "test does not exist" errors indicate version conflicts
Solution:
1npm install -D vitest@latest
Playwright Setup for E2E Testing #
Installation:
1npm init playwright@latest
Configuration (playwright.config.ts):
1import { defineConfig } from '@playwright/test';
2
3export default defineConfig({
4 testDir: './tests',
5 baseURL: 'http://localhost:4321',
6
7 webServer: {
8 command: 'npm run dev',
9 url: 'http://localhost:4321',
10 reuseExistingServer: !process.env.CI
11 },
12
13 use: {
14 trace: 'on-first-retry'
15 },
16
17 projects: [
18 {
19 name: 'chromium',
20 use: { browserName: 'chromium' }
21 },
22 {
23 name: 'firefox',
24 use: { browserName: 'firefox' }
25 },
26 {
27 name: 'webkit',
28 use: { browserName: 'webkit' }
29 }
30 ]
31});
Example E2E Test:
1import { test, expect } from '@playwright/test';
2
3test('homepage loads correctly', async ({ page }) => {
4 await page.goto('/');
5
6 // Check page title
7 await expect(page).toHaveTitle(/My Astro Site/);
8
9 // Check heading
10 const heading = page.locator('h1');
11 await expect(heading).toHaveText('Welcome to Astro');
12
13 // Test navigation
14 await page.click('a[href="/about"]');
15 await expect(page).toHaveURL(/about/);
16});
17
18test('blog posts load', async ({ page }) => {
19 await page.goto('/blog');
20
21 const articles = page.locator('article');
22 await expect(articles).toHaveCount(3);
23});
Running Tests:
1# Run all tests
2npx playwright test
3
4# Run in UI mode
5npx playwright test --ui
6
7# View test report
8npx playwright show-report
Testing Best Practices #
1. Test Production Builds
1# Build first
2npm run build
3
4# Test against preview
5npm run preview
6npx playwright test
Production builds may differ from development.
2. Test Real Deployments
1// playwright.config.ts for production
2export default defineConfig({
3 baseURL: process.env.PRODUCTION_URL || 'http://localhost:4321'
4});
3. Organize Tests
tests/
├── unit/ # Vitest unit tests
│ ├── utils.test.ts
│ └── helpers.test.ts
├── component/ # Vitest component tests
│ ├── Card.test.ts
│ └── Navigation.test.ts
└── e2e/ # Playwright E2E tests
├── homepage.spec.ts
└── blog.spec.ts
4. Test Framework Components
For React/Vue/Svelte components, use framework-specific testing libraries:
1import { render, screen } from '@testing-library/react';
2import { expect, test } from 'vitest';
3import ReactButton from '../src/components/ReactButton';
4
5test('React button renders', () => {
6 render(<ReactButton label="Click me" />);
7 expect(screen.getByRole('button')).toHaveTextContent('Click me');
8});
5. Client-Side Testing
For now, test client-side Astro component code with E2E tests (Playwright/Cypress) rather than unit tests.
Continuous Integration #
GitHub Actions Example:
1name: Test
2
3on: [push, pull_request]
4
5jobs:
6 test:
7 runs-on: ubuntu-latest
8 steps:
9 - uses: actions/checkout@v4
10 - uses: actions/setup-node@v4
11 with:
12 node-version: '20'
13
14 - name: Install dependencies
15 run: npm ci
16
17 - name: Run unit tests
18 run: npm run test:unit
19
20 - name: Install Playwright
21 run: npx playwright install --with-deps
22
23 - name: Build site
24 run: npm run build
25
26 - name: Run E2E tests
27 run: npm run test:e2e
7. Static Site Generation Patterns #
getStaticPaths() for Dynamic Routes #
Core Concept
In static mode, all routes must be determined at build time. Dynamic routes require getStaticPaths() to generate paths.
Basic Pattern:
1---
2// src/pages/blog/[slug].astro
3export async function getStaticPaths() {
4 const posts = await getCollection('blog');
5
6 return posts.map(post => ({
7 params: { slug: post.slug },
8 props: { post }
9 }));
10}
11
12const { post } = Astro.props;
13const { Content } = await post.render();
14---
15
16<h1>{post.data.title}</h1>
17<Content />
Key Rules:
- Returns array of objects with
paramsproperty - Each object generates one route
- Executes in isolated scope (can't reference parent scope except imports)
- Runs once at build time
Advanced Patterns #
Multiple Parameters:
1// src/pages/[category]/[year]/[slug].astro
2export async function getStaticPaths() {
3 const posts = await getCollection('blog');
4 const categories = ['tech', 'design', 'business'];
5 const years = ['2023', '2024', '2025'];
6
7 return categories.flatMap(category =>
8 years.flatMap(year =>
9 posts
10 .filter(p => p.data.category === category && p.data.year === year)
11 .map(post => ({
12 params: { category, year, slug: post.slug },
13 props: { post }
14 }))
15 )
16 );
17}
Generates routes like: /tech/2024/my-post
Pagination Pattern:
1---
2import { getCollection } from 'astro:content';
3
4export async function getStaticPaths({ paginate }) {
5 const posts = await getCollection('blog');
6 const sortedPosts = posts.sort(
7 (a, b) => b.data.publishDate - a.data.publishDate
8 );
9
10 return paginate(sortedPosts, { pageSize: 10 });
11}
12
13const { page } = Astro.props;
14---
15
16{page.data.map(post => (
17 <article>
18 <h2>{post.data.title}</h2>
19 </article>
20))}
21
22<!-- Pagination controls -->
23{page.url.prev && <a href={page.url.prev}>Previous</a>}
24{page.url.next && <a href={page.url.next}>Next</a>}
API Data at Build Time:
1export async function getStaticPaths() {
2 // Fetch once at build time
3 const response = await fetch('https://api.example.com/products');
4 const products = await response.json();
5
6 return products.map(product => ({
7 params: { id: product.id },
8 props: { product } // Pass data as props
9 }));
10}
Performance Optimization for getStaticPaths #
1. Limit Data Fetching
1// Bad: Fetching full product data for each page
2export async function getStaticPaths() {
3 const products = await fetchAllProductsWithFullDetails();
4 return products.map(p => ({ params: { id: p.id }, props: { p } }));
5}
6
7// Good: Fetch only IDs, get details in component
8export async function getStaticPaths() {
9 const ids = await fetchProductIds(); // Lighter query
10 return ids.map(id => ({ params: { id } }));
11}
12
13// In component
14const { id } = Astro.params;
15const product = await fetchProduct(id); // Cached/memoized
2. Implement Caching
1// utils/cache.ts
2const cache = new Map();
3
4export async function cachedFetch(url: string) {
5 if (cache.has(url)) {
6 return cache.get(url);
7 }
8
9 const response = await fetch(url);
10 const data = await response.json();
11 cache.set(url, data);
12
13 return data;
14}
15
16// Use in getStaticPaths
17export async function getStaticPaths() {
18 const data = await cachedFetch('https://api.example.com/data');
19 // ...
20}
3. Content Collections over getStaticPaths
When possible, use Content Collections instead of getStaticPaths():
1// Preferred: Content Collections
2import { getCollection } from 'astro:content';
3
4export async function getStaticPaths() {
5 const posts = await getCollection('blog');
6 return posts.map(post => ({
7 params: { slug: post.slug },
8 props: { post }
9 }));
10}
Benefits:
- Type-safe schemas
- 5x faster builds
- Better caching
- Automatic optimization
Hybrid Rendering (Astro 5.0) #
Simplified Configuration
Astro 5.0 merged hybrid and static modes:
1// astro.config.mjs
2export default defineConfig({
3 output: 'static', // Default
4 adapter: node() // For SSR routes
5});
Selective SSR:
1// src/pages/api/dynamic.ts
2export const prerender = false; // This route uses SSR
3
4export async function GET() {
5 const data = await fetchLiveData();
6 return new Response(JSON.stringify(data));
7}
Selective Static:
1// In SSR mode
2export const prerender = true; // This route is pre-rendered
Server Islands for Static Sites #
Use Case:
Combine static HTML with dynamic, server-rendered components:
---
// src/pages/product/[id].astro (static)
import DynamicPricing from '@components/DynamicPricing.astro';
import Reviews from '@components/Reviews.astro';
---
<!-- Static content -->
<h1>{product.name}</h1>
<p>{product.description}</p>
<!-- Dynamic island -->
<DynamicPricing server:defer productId={product.id}>
<div slot="fallback">Loading price...</div>
</DynamicPricing>
<!-- Another island -->
<Reviews server:defer productId={product.id}>
<div slot="fallback">Loading reviews...</div>
</Reviews>
Benefits:
- Static page cached on CDN (fast delivery)
- Dynamic data loaded independently
- Slower islands don't block fast ones
- Fallback content shows immediately
When to Use:
✅ E-commerce sites (static products, dynamic pricing/inventory) ✅ Content sites with personalization ✅ Dashboards with mostly static layout
❌ Avoid when: high ratio of dynamic to static content
Incremental Static Regeneration (ISR) #
Middleware Pattern:
1// src/middleware.ts
2const cache = new Map();
3const REVALIDATE_TIME = 60 * 1000; // 1 minute
4
5export async function onRequest({ request, next }, locals) {
6 const url = new URL(request.url);
7 const cacheKey = url.pathname;
8
9 const cached = cache.get(cacheKey);
10
11 if (cached && Date.now() - cached.timestamp < REVALIDATE_TIME) {
12 return cached.response;
13 }
14
15 const response = await next();
16
17 cache.set(cacheKey, {
18 response: response.clone(),
19 timestamp: Date.now()
20 });
21
22 return response;
23}
CDN-Level ISR:
1// astro.config.mjs
2export default defineConfig({
3 adapter: netlify(),
4 output: 'static'
5});
6
7// In page component
8---
9export const prerender = true;
10
11// Set cache headers
12Astro.response.headers.set('Cache-Control', 's-maxage=60, stale-while-revalidate=86400');
13---
Stale-While-Revalidate:
- Serves stale content immediately
- Revalidates in background
- User never waits for fresh content
- Ideal for frequently updated but not real-time content
Static Site Best Practices #
1. Build Strategy
1// astro.config.mjs
2export default defineConfig({
3 build: {
4 format: 'directory', // URLs without .html
5 inlineStylesheets: 'auto', // Inline small CSS
6 assetsPrefix: 'https://cdn.example.com' // CDN for assets
7 }
8});
2. Route Organization
src/pages/
├── index.astro # /
├── about.astro # /about
├── blog/
│ ├── index.astro # /blog
│ └── [slug].astro # /blog/[slug]
├── docs/
│ └── [...slug].astro # /docs/any/path
└── api/
└── search.json.ts # /api/search.json
3. Content Organization
Use Content Collections for type-safety:
- Better organization
- Schema validation
- Automatic type generation
- Performance optimization
4. Monitoring and Validation
1# Check for broken links
2npm run build
3npx broken-link-checker http://localhost:4321
4
5# Lighthouse CI
6npm install -g @lhci/cli
7lhci autorun
Conclusion #
Astro 5.x represents a mature, production-ready framework for building high-performance static sites with modern developer experience. Key takeaways:
Performance First:
- HTML-first architecture
- Zero JavaScript by default
- Built-in optimizations
- Incremental adoption of interactivity
Type Safety:
- Full TypeScript support
- Automatic type generation
- Content Collections schemas
- Environment variable validation
Developer Experience:
- Intuitive project structure
- Multiple testing approaches
- Framework-agnostic
- Excellent documentation
Production Ready:
- Built-in security features
- Multiple deployment targets
- Performance monitoring
- Scalability patterns
By following these best practices, teams can build fast, secure, and maintainable websites with Astro 5.x.
Sources #
Official Documentation #
- Astro 5.0 Release
- Project Structure - Astro Docs
- Content Collections - Astro Docs
- TypeScript - Astro Docs
- Testing - Astro Docs
- Routing - Astro Docs
- Environment Variables - Astro Docs
- Environment Variables API Reference
- Server Islands - Astro Docs
- Astro 5.9 Release
Tutorials and Guides #
- What is Astro? A Step-by-Step Tutorial for Beginners in 2025
- Astro Web Framework Guide 2025
- How to Use Astro Content Collections
- Getting Started with Content Collections in Astro — SitePoint
- Understanding Astro's getStaticPaths function
- Type-safe environment variables in Astro 5.0
Performance Optimization #
- Boosting Web Performance with Astro JS
- Astro Build Speed Optimization: From 35 to 127 Pages/Second
- How We Cut Astro Build Time from 30 Minutes to 5 Minutes
- How to optimize images in Astro: A step-by-step guide
- Production-Ready Astro Middleware
- How to Implement ISR in Astro
- How to do advanced caching and ISR with Astro
Security #
Testing #
Server Islands #
- Server Islands - The Future of Astro
- Astro 4.12: Server Islands
- How Astro's server islands deliver progressive rendering