agent-rules-kit
Version:
Bootstrap of **Cursor** rules (`.mdc`) and mirror documentation (`.md`) for AI agent-guided projects.
855 lines (715 loc) • 18.7 kB
Markdown
title: Astro v4 Specific Features
description: Key features and implementation details specific to Astro version 4
tags: [Astro, v4, Features, AstroDB, Actions, i18n]
globs: <root>/src/content/**/*.md,<root>/src/content/**/*.mdx,<root>/astro.config.js,<root>/astro.config.mjs,<root>/astro.config.ts,<root>/db/**/*.ts
# Astro v4 Specific Features
## Overview
Astro 4.0 introduces revolutionary features for building dynamic web applications, including Astro DB for database management, Astro Actions for type-safe server functions, enhanced internationalization, and significantly improved development experience.
## Astro DB
Astro 4.0 introduces Astro DB, a SQL database designed specifically for Astro. It provides a simple, lightweight database solution with excellent TypeScript integration.
### Database Schema Definition
```typescript
// db/config.ts
import { defineDb, defineTable, column } from 'astro:db';
const User = defineTable({
columns: {
id: column.number({ primaryKey: true }),
name: column.text(),
email: column.text({ unique: true }),
createdAt: column.date({ default: new Date() }),
isActive: column.boolean({ default: true }),
},
});
const Post = defineTable({
columns: {
id: column.number({ primaryKey: true }),
title: column.text(),
content: column.text(),
slug: column.text({ unique: true }),
authorId: column.number({ references: () => User.columns.id }),
publishedAt: column.date({ optional: true }),
tags: column.json({ optional: true }),
},
});
const Comment = defineTable({
columns: {
id: column.number({ primaryKey: true }),
content: column.text(),
postId: column.number({ references: () => Post.columns.id }),
authorId: column.number({ references: () => User.columns.id }),
createdAt: column.date({ default: new Date() }),
},
});
export default defineDb({
tables: { User, Post, Comment },
});
```
### Database Seeding
```typescript
// db/seed.ts
import { db, User, Post, Comment } from 'astro:db';
export default async function seed() {
// Seed users
await db.insert(User).values([
{
id: 1,
name: 'John Doe',
email: 'john@example.com',
},
{
id: 2,
name: 'Jane Smith',
email: 'jane@example.com',
},
]);
// Seed posts
await db.insert(Post).values([
{
id: 1,
title: 'Getting Started with Astro DB',
content: 'Learn how to use Astro DB...',
slug: 'getting-started-astro-db',
authorId: 1,
publishedAt: new Date('2024-01-01'),
tags: ['astro', 'database', 'tutorial'],
},
{
id: 2,
title: 'Advanced Database Patterns',
content: 'Explore advanced patterns...',
slug: 'advanced-database-patterns',
authorId: 2,
publishedAt: new Date('2024-01-15'),
tags: ['astro', 'database', 'advanced'],
},
]);
// Seed comments
await db.insert(Comment).values([
{
id: 1,
content: 'Great article!',
postId: 1,
authorId: 2,
},
{
id: 2,
content: 'Very helpful, thanks!',
postId: 1,
authorId: 1,
},
]);
}
```
### Using the Database
```typescript
// src/pages/blog/[slug].astro
import { db, Post, User, Comment } from 'astro:db';
import Layout from '../../layouts/Layout.astro';
const { slug } = Astro.params;
// Query post with author information
const post = await db
.select({
id: Post.id,
title: Post.title,
content: Post.content,
publishedAt: Post.publishedAt,
tags: Post.tags,
authorName: User.name,
authorEmail: User.email,
})
.from(Post)
.innerJoin(User, eq(Post.authorId, User.id))
.where(eq(Post.slug, slug))
.get();
if (!post) {
return Astro.redirect('/404');
}
// Query comments for this post
const comments = await db
.select({
id: Comment.id,
content: Comment.content,
createdAt: Comment.createdAt,
authorName: User.name,
})
.from(Comment)
.innerJoin(User, eq(Comment.authorId, User.id))
.where(eq(Comment.postId, post.id))
.orderBy(desc(Comment.createdAt));
<Layout title={post.title}>
<article>
<header>
<h1>{post.title}</h1>
<p>By {post.authorName} on {post.publishedAt?.toLocaleDateString()}</p>
{post.tags && (
<div class="tags">
{post.tags.map((tag) => (
<span class="tag">{tag}</span>
))}
</div>
)}
</header>
<div class="content">
{post.content}
</div>
<section class="comments">
<h2>Comments ({comments.length})</h2>
{comments.map((comment) => (
<div class="comment">
<p>{comment.content}</p>
<small>By {comment.authorName} on {comment.createdAt.toLocaleDateString()}</small>
</div>
))}
</section>
</article>
</Layout>
```
### Configuration
```javascript
// astro.config.mjs
import { defineConfig } from 'astro/config';
import db from '@astrojs/db';
export default defineConfig({
integrations: [db()],
output: 'server', // Required for Astro DB
});
```
## Astro Actions
Astro Actions provide a type-safe way to handle server-side logic with automatic input validation and error handling.
### Defining Actions
```typescript
// src/actions/index.ts
import { defineAction } from 'astro:actions';
import { z } from 'astro:schema';
import { db, User, Post } from 'astro:db';
export const server = {
// User registration action
registerUser: defineAction({
input: z.object({
name: z.string().min(2, 'Name must be at least 2 characters'),
email: z.string().email('Invalid email address'),
password: z
.string()
.min(8, 'Password must be at least 8 characters'),
}),
handler: async (input) => {
try {
const hashedPassword = await hashPassword(input.password);
const user = await db
.insert(User)
.values({
name: input.name,
email: input.email,
password: hashedPassword,
})
.returning();
return {
success: true,
user: user[0],
};
} catch (error) {
throw new ActionError({
code: 'BAD_REQUEST',
message: 'Email already exists',
});
}
},
}),
// Create post action
createPost: defineAction({
input: z.object({
title: z.string().min(5, 'Title must be at least 5 characters'),
content: z
.string()
.min(10, 'Content must be at least 10 characters'),
slug: z.string().regex(/^[a-z0-9-]+$/, 'Invalid slug format'),
tags: z.array(z.string()).optional(),
}),
handler: async (input, context) => {
// Check if user is authenticated
const user = await getCurrentUser(context);
if (!user) {
throw new ActionError({
code: 'UNAUTHORIZED',
message: 'You must be logged in to create a post',
});
}
const post = await db
.insert(Post)
.values({
title: input.title,
content: input.content,
slug: input.slug,
authorId: user.id,
tags: input.tags,
publishedAt: new Date(),
})
.returning();
return {
success: true,
post: post[0],
};
},
}),
// Like post action
likePost: defineAction({
input: z.object({
postId: z.number(),
}),
handler: async (input, context) => {
const user = await getCurrentUser(context);
if (!user) {
throw new ActionError({
code: 'UNAUTHORIZED',
message: 'You must be logged in to like a post',
});
}
// Toggle like logic here
const likes = await togglePostLike(input.postId, user.id);
return { likes };
},
}),
};
```
### Using Actions in Components
```typescript
// src/pages/create-post.astro
import { actions } from 'astro:actions';
import Layout from '../layouts/Layout.astro';
const result = await Astro.callAction(actions.createPost);
if (result && !result.error) {
return Astro.redirect(`/blog/${result.data.post.slug}`);
}
<Layout title="Create Post">
<form method="POST" action={actions.createPost}>
<div>
<label for="title">Title:</label>
<input
type="text"
id="title"
name="title"
required
value={result?.error?.fields?.title?.[0] || ''}
/>
{result?.error?.fieldErrors?.title && (
<p class="error">{result.error.fieldErrors.title[0]}</p>
)}
</div>
<div>
<label for="content">Content:</label>
<textarea
id="content"
name="content"
required
>{result?.error?.fields?.content?.[0] || ''}</textarea>
{result?.error?.fieldErrors?.content && (
<p class="error">{result.error.fieldErrors.content[0]}</p>
)}
</div>
<div>
<label for="slug">Slug:</label>
<input
type="text"
id="slug"
name="slug"
required
value={result?.error?.fields?.slug?.[0] || ''}
/>
{result?.error?.fieldErrors?.slug && (
<p class="error">{result.error.fieldErrors.slug[0]}</p>
)}
</div>
<button type="submit">Create Post</button>
</form>
{result?.error?.message && (
<p class="error">{result.error.message}</p>
)}
</Layout>
```
### Client-Side Action Usage
```typescript
// src/components/LikeButton.astro
interface Props {
postId: number;
initialLikes: number;
}
const { postId, initialLikes } = Astro.props;
<button
class="like-button"
data-post-id={postId}
data-likes={initialLikes}
>
<span class="likes-count">{initialLikes}</span> ❤️
</button>
<script>
import { actions } from 'astro:actions';
document.addEventListener('DOMContentLoaded', () => {
const likeButtons = document.querySelectorAll('.like-button');
likeButtons.forEach(button => {
button.addEventListener('click', async (e) => {
e.preventDefault();
const postId = parseInt(button.dataset.postId);
const likesSpan = button.querySelector('.likes-count');
try {
const result = await actions.likePost({ postId });
if (!result.error) {
likesSpan.textContent = result.data.likes;
} else {
console.error('Error liking post:', result.error.message);
}
} catch (error) {
console.error('Network error:', error);
}
});
});
});
</script>
<style>
.like-button {
background: none;
border: none;
cursor: pointer;
font-size: 1rem;
transition: transform 0.2s;
}
.like-button:hover {
transform: scale(1.1);
}
</style>
```
## Enhanced Internationalization (i18n)
Astro 4.0 significantly improves internationalization support with routing, locale detection, and translation helpers.
### Configuration
```javascript
// astro.config.mjs
import { defineConfig } from 'astro/config';
export default defineConfig({
i18n: {
defaultLocale: 'en',
locales: ['en', 'es', 'fr', 'de'],
routing: {
prefixDefaultLocale: false,
redirectToDefaultLocale: true,
},
domains: {
fr: 'https://fr.example.com',
es: 'https://es.example.com',
},
},
});
```
### Routing with Locales
```typescript
// src/pages/[...locale]/blog/[slug].astro
import { getStaticPaths } from 'astro:i18n';
import Layout from '../../../layouts/Layout.astro';
export async function getStaticPaths() {
const posts = await getCollection('blog');
return posts.flatMap((post) => {
return ['en', 'es', 'fr', 'de'].map((locale) => ({
params: {
locale,
slug: post.slug
},
props: { post, locale }
}));
});
}
const { post, locale } = Astro.props;
const { slug } = Astro.params;
<Layout title={post.data.title} locale={locale}>
<article>
<h1>{post.data.title}</h1>
<div set:html={post.render()} />
</article>
</Layout>
```
### Translation Utilities
```typescript
// src/utils/i18n.ts
const translations = {
en: {
'nav.home': 'Home',
'nav.about': 'About',
'nav.blog': 'Blog',
'nav.contact': 'Contact',
'blog.readMore': 'Read more',
'blog.publishedOn': 'Published on',
},
es: {
'nav.home': 'Inicio',
'nav.about': 'Acerca de',
'nav.blog': 'Blog',
'nav.contact': 'Contacto',
'blog.readMore': 'Leer más',
'blog.publishedOn': 'Publicado el',
},
fr: {
'nav.home': 'Accueil',
'nav.about': 'À propos',
'nav.blog': 'Blog',
'nav.contact': 'Contact',
'blog.readMore': 'Lire la suite',
'blog.publishedOn': 'Publié le',
},
};
export function t(locale: string, key: string): string {
return translations[locale]?.[key] || translations.en[key] || key;
}
export function getLocaleFromUrl(url: URL): string {
const segments = url.pathname.split('/');
const potentialLocale = segments[1];
if (['en', 'es', 'fr', 'de'].includes(potentialLocale)) {
return potentialLocale;
}
return 'en';
}
```
## Development Toolbar Enhancements
Astro 4.0 introduces a powerful development toolbar with built-in apps and the ability to create custom ones.
### Custom Toolbar App
```typescript
// src/dev-toolbar/my-app.ts
import type { DevToolbarApp } from 'astro';
export default {
id: 'my-custom-app',
name: 'My Custom App',
icon: '🛠️',
init(canvas, eventTarget) {
// Create UI elements
const button = document.createElement('button');
button.textContent = 'Toggle Feature';
button.style.cssText = `
background: #7c3aed;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
`;
button.addEventListener('click', () => {
// Toggle some development feature
document.body.classList.toggle('debug-mode');
eventTarget.dispatchEvent(
new CustomEvent('astro:dev-toolbar:toggled', {
detail: {
enabled: document.body.classList.contains('debug-mode'),
},
})
);
});
canvas.appendChild(button);
},
} satisfies DevToolbarApp;
```
```javascript
// astro.config.mjs
import { defineConfig } from 'astro/config';
export default defineConfig({
devToolbar: {
enabled: true,
apps: ['./src/dev-toolbar/my-app.ts'],
},
});
```
## Request Rewriting
Astro 4.13 introduces request rewriting capabilities for advanced routing scenarios.
### Using Rewrite in Middleware
```typescript
// src/middleware.ts
import { defineMiddleware } from 'astro:middleware';
export const onRequest = defineMiddleware(async (context, next) => {
const url = new URL(context.request.url);
// Rewrite API routes to versioned endpoints
if (url.pathname.startsWith('/api/')) {
const version = context.request.headers.get('API-Version') || 'v1';
return context.rewrite(`/api/${version}${url.pathname.slice(4)}`);
}
// A/B testing rewrites
if (url.pathname === '/pricing') {
const variant = Math.random() > 0.5 ? 'a' : 'b';
return context.rewrite(`/pricing-${variant}`);
}
// Redirect old blog structure
if (url.pathname.startsWith('/blog/')) {
const slug = url.pathname.slice(6);
return context.rewrite(`/posts/${slug}`);
}
return next();
});
```
### Using Rewrite in Pages
```typescript
// src/pages/legacy/[...path].astro
// Rewrite legacy URLs to new structure
const { path } = Astro.params;
if (path?.startsWith('product/')) {
const productId = path.split('/')[1];
return Astro.rewrite(`/products/${productId}`);
}
if (path?.startsWith('category/')) {
const category = path.split('/')[1];
return Astro.rewrite(`/shop/${category}`);
}
return Astro.redirect('/404');
```
## CSS Imports and Asset Handling
Astro 4.0 improves CSS handling and asset processing.
### CSS Modules
```typescript
// src/components/Card.astro
interface Props {
title: string;
content: string;
}
const { title, content } = Astro.props;
<div class="card">
<h3 class="title">{title}</h3>
<p class="content">{content}</p>
</div>
<style module>
.card {
border: 1px solid #e2e8f0;
border-radius: 8px;
padding: 1rem;
background: white;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.title {
margin: 0 0 0.5rem 0;
color: #1a202c;
font-size: 1.25rem;
}
.content {
margin: 0;
color: #4a5568;
line-height: 1.5;
}
</style>
```
### Asset Imports
```typescript
// src/pages/gallery.astro
import heroImage from '../assets/hero.jpg?url';
import iconSprite from '../assets/icons.svg?raw';
import configData from '../data/config.json';
<div class="gallery">
<img src={heroImage} alt="Hero" />
<!-- Inline SVG -->
<div set:html={iconSprite} />
<!-- Use imported JSON data -->
<h1>{configData.siteTitle}</h1>
</div>
```
## Environment Variables and Security
Astro 4.0 enhances security with CSRF protection and improved environment variable handling.
### CSRF Protection
```javascript
// astro.config.mjs
import { defineConfig } from 'astro/config';
export default defineConfig({
security: {
checkOrigin: true,
},
output: 'server',
});
```
### Environment Variables
```typescript
// src/utils/env.ts
import { z } from 'astro:schema';
const envSchema = z.object({
DATABASE_URL: z.string().url(),
API_KEY: z.string().min(32),
NODE_ENV: z
.enum(['development', 'production', 'test'])
.default('development'),
PORT: z.coerce.number().default(3000),
});
export const env = envSchema.parse(process.env);
```
```typescript
// Using environment variables safely
import { env } from '../utils/env';
// This will be validated at build/runtime
const apiResponse = await fetch(`${env.API_URL}/data`, {
headers: {
'Authorization': `Bearer ${env.API_KEY}`
}
});
```
## Performance Optimizations
Astro 4.0 includes numerous performance improvements and new optimization features.
### Bundle Analysis
```javascript
// astro.config.mjs
import { defineConfig } from 'astro/config';
export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks: {
'react-vendor': ['react', 'react-dom'],
utils: ['lodash', 'date-fns'],
},
},
},
},
vite: {
build: {
rollupOptions: {
external: ['some-large-library'],
},
},
},
});
```
### Preloading Strategies
```typescript
// src/layouts/BaseLayout.astro
interface Props {
title: string;
preloadImages?: string[];
preloadScripts?: string[];
}
const { title, preloadImages = [], preloadScripts = [] } = Astro.props;
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{title}</title>
<!-- Preload critical images -->
{preloadImages.map(src => (
<link rel="preload" as="image" href={src} />
))}
<!-- Preload critical scripts -->
{preloadScripts.map(src => (
<link rel="modulepreload" href={src} />
))}
</head>
<body>
<slot />
</body>
</html>
```
This comprehensive guide covers the major features introduced in Astro 4.0, providing practical examples and implementation patterns for building modern web applications with enhanced database capabilities, type-safe server actions, improved internationalization, and better development experience.