Astro + Atomic Design
General Principles
Section titled “General Principles”Write performant, maintainable Astro sites that leverage the framework’s strengths: content-focused, islands architecture, and excellent developer experience.
Project Structure
Section titled “Project Structure”Recommended Layout
Section titled “Recommended Layout”Organize projects with clear separation of concerns:
src/├── components/│ ├── ui/ # Reusable UI components│ ├── layout/ # Layout components│ └── islands/ # Interactive islands├── content/│ ├── blog/ # Blog posts│ └── config.ts # Content collections├── layouts/│ └── Layout.astro # Base layout├── pages/│ ├── index.astro│ └── blog/│ └── [...slug].astro├── styles/│ └── global.css└── env.d.tsNote: For component organization using Atomic Design methodology, see the ATOMIC DESIGN METHODOLOGY section.
File Naming
Section titled “File Naming”Use consistent naming conventions:
✅ BlogPost.astro # Component✅ BaseLayout.astro # Layout✅ [slug].astro # Dynamic route✅ [...path].astro # Rest parameter✅ api/posts.json.ts # Endpoint
❌ blogPost.astro❌ blog_post.astroComponent Syntax
Section titled “Component Syntax”Component Structure
Section titled “Component Structure”Structure components with clear sections:
---// 1. Importsimport BaseLayout from '@/layouts/BaseLayout.astro';import Card from '@/components/ui/Card.astro';
// 2. Props interfaceinterface Props { title: string; description?: string;}
// 3. Props destructuring with defaultsconst { title, description = 'Default description' } = Astro.props;
// 4. Data fetchingconst posts = await getPosts();
// 5. Logicconst formattedTitle = title.toUpperCase();---
<!-- 6. Template --><BaseLayout title={formattedTitle}> <h1>{title}</h1> {description && <p>{description}</p>}
{posts.map(post => ( <Card title={post.title} /> ))}</BaseLayout>
<!-- 7. Scoped styles --><style> h1 { color: var(--accent); }</style>
<!-- 8. Scripts --><script> console.log('Page loaded');</script>Props and TypeScript
Section titled “Props and TypeScript”Always type component props:
---interface Props { title: string; tags?: string[]; publishedAt: Date; featured?: boolean;}
const { title, tags = [], publishedAt, featured = false} = Astro.props;---
<article class:list={{ featured }}> <h2>{title}</h2> <time datetime={publishedAt.toISOString()}> {publishedAt.toLocaleDateString()} </time> {tags.length > 0 && ( <ul> {tags.map(tag => <li>{tag}</li>)} </ul> )}</article>Conditional Rendering
Section titled “Conditional Rendering”Use clear conditional patterns:
---const { user, isLoggedIn } = Astro.props;---
<!-- Short-circuit for simple cases -->{isLoggedIn && <p>Welcome back!</p>}
<!-- Ternary for either/or -->{isLoggedIn ? ( <Dashboard user={user} />) : ( <LoginForm />)}
<!-- Multiple conditions -->{status === 'loading' && <Spinner />}{status === 'error' && <Error message={error} />}{status === 'success' && <Content data={data} />}Content Collections
Section titled “Content Collections”Schema Definition
Section titled “Schema Definition”Define strict schemas for content:
import { defineCollection, z } from 'astro:content';
const blog = defineCollection({ type: 'content', schema: z.object({ title: z.string(), description: z.string(), publishedAt: z.date(), updatedAt: z.date().optional(), tags: z.array(z.string()).default([]), featured: z.boolean().default(false), draft: z.boolean().default(false), }),});
export const collections = { blog };Content Queries
Section titled “Content Queries”Use type-safe content queries:
---import { getCollection } from 'astro:content';
// Get all published postsconst posts = await getCollection('blog', ({ data }) => { return !data.draft;});
// Sort by dateconst sortedPosts = posts.sort((a, b) => b.data.publishedAt.getTime() - a.data.publishedAt.getTime());
// Get single entryconst post = await getEntry('blog', 'my-post-slug');---
{sortedPosts.map(post => ( <article> <h2>{post.data.title}</h2> <a href={`/blog/${post.slug}`}>Read more</a> </article>))}Dynamic Routes with Content
Section titled “Dynamic Routes with Content”Create type-safe dynamic routes:
---import { getCollection, type CollectionEntry } 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.slug }, props: { post }, }));}
interface Props { post: CollectionEntry<'blog'>;}
const { post } = Astro.props;const { Content } = await post.render();---
<BlogLayout title={post.data.title}> <article> <h1>{post.data.title}</h1> <Content /> </article></BlogLayout>Styling
Section titled “Styling”Scoped Styles
Section titled “Scoped Styles”Prefer scoped styles for components:
---const { variant = 'primary' } = Astro.props;---
<button class={`btn btn-${variant}`}> <slot /></button>
<style> .btn { padding: 0.5rem 1rem; border-radius: 0.25rem; border: none; cursor: pointer; }
.btn-primary { background: var(--color-primary); color: white; }
.btn-secondary { background: var(--color-secondary); color: white; }</style>Global Styles
Section titled “Global Styles”Use global styles for design tokens:
:root { /* Colors */ --color-primary: hsl(220 90% 56%); --color-secondary: hsl(280 70% 60%); --color-background: hsl(0 0% 100%); --color-text: hsl(0 0% 10%);
/* Spacing */ --spacing-xs: 0.25rem; --spacing-sm: 0.5rem; --spacing-md: 1rem; --spacing-lg: 2rem;
/* Typography */ --font-sans: system-ui, sans-serif; --font-mono: 'Fira Code', monospace;}
* { box-sizing: border-box; margin: 0; padding: 0;}
body { font-family: var(--font-sans); color: var(--color-text); background: var(--color-background);}CSS Frameworks
Section titled “CSS Frameworks”Integrate CSS frameworks properly:
---// Using Tailwind CSSimport '@/styles/global.css';---
<div class="container mx-auto px-4"> <h1 class="text-4xl font-bold">Hello</h1></div>Islands Architecture
Section titled “Islands Architecture”Client Directives
Section titled “Client Directives”Use appropriate client directives:
---import Counter from '@/components/islands/Counter.svelte';import Search from '@/components/islands/Search.tsx';import Analytics from '@/components/islands/Analytics.vue';---
<!-- Load immediately --><Counter client:load />
<!-- Load when idle --><Search client:idle />
<!-- Load when visible --><Analytics client:visible />
<!-- Load on media query --><MobileMenu client:media="(max-width: 768px)" />
<!-- Only render on client --><ClientOnly client:only="react" />Passing Props to Islands
Section titled “Passing Props to Islands”Pass serializable data to islands:
---const initialCount = 0;const config = { theme: 'dark', locale: 'en-US'};---
<!-- Good - serializable data --><Counter client:load initialCount={initialCount} config={config}/>
<!-- Bad - functions/symbols not serializable --><Counter client:load onClick={() => console.log('click')}/>Data Fetching
Section titled “Data Fetching”Server-Side Fetching
Section titled “Server-Side Fetching”Fetch data in component frontmatter:
---// Good - runs at build timeconst response = await fetch('https://api.example.com/posts');const posts = await response.json();
// Good - with error handlinglet data = null;let error = null;
try { const res = await fetch('https://api.example.com/data'); data = await res.json();} catch (e) { error = e.message;}---
{error ? ( <p>Error: {error}</p>) : ( <ul> {data.map(item => <li>{item.name}</li>)} </ul>)}Environment Variables
Section titled “Environment Variables”Use environment variables securely:
---// Good - server-only secretsconst API_KEY = import.meta.env.SECRET_API_KEY;
// Good - public variablesconst PUBLIC_URL = import.meta.env.PUBLIC_API_URL;---
<div data-api-url={PUBLIC_URL}> <!-- Never expose SECRET_API_KEY to client --></div>API Routes
Section titled “API Routes”Endpoint Structure
Section titled “Endpoint Structure”Create type-safe API endpoints:
import type { APIRoute } from 'astro';
export const GET: APIRoute = async ({ params, request }) => { try { const posts = await getPosts();
return new Response(JSON.stringify(posts), { status: 200, headers: { 'Content-Type': 'application/json', }, }); } catch (error) { return new Response(JSON.stringify({ error: 'Failed to fetch posts' }), { status: 500, headers: { 'Content-Type': 'application/json', }, }); }};Form Handling
Section titled “Form Handling”Handle form submissions with Actions:
import { defineAction } from 'astro:actions';import { z } from 'astro:schema';
export const server = { newsletter: defineAction({ input: z.object({ email: z.string().email(), }), handler: async (input) => { // Subscribe to newsletter await subscribeToNewsletter(input.email); return { success: true }; }, }),};---import { actions } from 'astro:actions';---
<form method="POST" action={actions.newsletter}> <input type="email" name="email" required /> <button type="submit">Subscribe</button></form>View Transitions
Section titled “View Transitions”Basic Implementation
Section titled “Basic Implementation”Add smooth page transitions:
---import { ViewTransitions } from 'astro:transitions';---
<html> <head> <ViewTransitions /> </head> <body> <slot /> </body></html>Transition Directives
Section titled “Transition Directives”Control transition behavior:
---import { fade, slide } from 'astro:transitions';---
<!-- Persist across navigation --><header transition:persist> <nav><!-- Navigation --></nav></header>
<!-- Named transition --><img src={hero} alt="Hero" transition:name="hero" transition:animate={fade({ duration: '0.3s' })}/>
<!-- Animate on entry --><article transition:animate={slide({ duration: '0.5s' })}> <h1>{title}</h1></article>Performance
Section titled “Performance”Image Optimization
Section titled “Image Optimization”Use Astro’s Image component:
---import { Image } from 'astro:assets';import heroImage from '@/assets/hero.jpg';---
<!-- Optimized with automatic formats --><Image src={heroImage} alt="Hero image" width={800} height={600} loading="lazy"/>
<!-- Remote images --><Image src="https://example.com/image.jpg" alt="Remote" width={400} height={300} inferSize/>Prefetching
Section titled “Prefetching”Prefetch pages for faster navigation:
---import { prefetch } from 'astro:prefetch';---
<!-- Prefetch on hover --><a href="/about" data-astro-prefetch>About</a>
<!-- Prefetch on visibility --><a href="/blog" data-astro-prefetch="viewport">Blog</a>
<!-- Programmatic prefetch --><script> import { prefetch } from 'astro:prefetch';
document.addEventListener('DOMContentLoaded', () => { prefetch('/important-page'); });</script>Best Practices
Section titled “Best Practices”✅ Use content collections for content management ✅ Type all component props with TypeScript ✅ Leverage server-side rendering by default ✅ Use islands for interactive components ✅ Optimize images with Image component ✅ Use View Transitions for smooth navigation ✅ Keep client-side JavaScript minimal ✅ Use environment variables properly ✅ Implement proper error handling ✅ Follow semantic HTML practices
Don’ts
Section titled “Don’ts”❌ Hydrate entire page with client:load ❌ Fetch data client-side when avoidable ❌ Expose secrets to client ❌ Use unoptimized images ❌ Skip TypeScript types ❌ Create deeply nested component trees ❌ Mix server and client state ❌ Ignore accessibility ❌ Use inline styles excessively
Configuration
Section titled “Configuration”TypeScript Config
Section titled “TypeScript Config”{ "extends": "astro/tsconfigs/strict", "compilerOptions": { "baseUrl": ".", "paths": { "@/*": ["src/*"], "@/components/*": ["src/components/*"], "@/layouts/*": ["src/layouts/*"] } }}Astro Config
Section titled “Astro Config”import { defineConfig } from 'astro/config';import tailwind from '@astrojs/tailwind';import sitemap from '@astrojs/sitemap';
export default defineConfig({ site: 'https://example.com', integrations: [ tailwind(), sitemap(), ], output: 'static', // or 'server' or 'hybrid' vite: { optimizeDeps: { exclude: ['@node-rs/argon2'], }, },});Testing
Section titled “Testing”Component Testing
Section titled “Component Testing”Test components with Vitest:
import { experimental_AstroContainer as AstroContainer } from 'astro/container';import { expect, test } from 'vitest';import Card from './Card.astro';
test('Card renders with title', async () => { const container = await AstroContainer.create(); const result = await container.renderToString(Card, { props: { title: 'Test Card' }, });
expect(result).toContain('Test Card');});