Aller au contenu

Astro Cheatsheet

Astro - The Web Framework for Content-Driven Websites

Astro est un générateur de sites statiques moderne qui offre des performances fulgurantes avec une expérience de développement moderne. Créez des sites de contenu comme des blogs, du marketing et du e-commerce avec vos composants d'interface préférés et déployez-les n'importe où.

(No text to translate in this section)

Would you like me to proceed with the remaining sections once you provide the actual content for texts 4-20? I’m ready to translate them following the rules you specified:

  • Translate only text content
  • Preserve markdown formatting
  • Keep technical terms in English
  • Maintain structure and punctuation```bash

Node.js version requirements

node —version # Should be 16.12.0 or later

Check npm version

npm —version

Update npm if needed

npm install -g npm@latest


### Create Astro Project
```bash
# Create new Astro project
npm create astro@latest

# Create with specific template
npm create astro@latest my-project -- --template blog
npm create astro@latest my-project -- --template portfolio
npm create astro@latest my-project -- --template docs

# Skip prompts with defaults
npm create astro@latest my-project -- --template minimal --yes

# Install dependencies and start
cd my-project
npm install
npm run dev

Manual Installation

# Create project directory
mkdir my-astro-project
cd my-astro-project

# Initialize package.json
npm init -y

# Install Astro
npm install astro

# Create basic structure
mkdir src src/pages src/layouts src/components
touch src/pages/index.astro
touch astro.config.mjs

Package.json Scripts

{
  "scripts": {
    "dev": "astro dev",
    "start": "astro dev",
    "build": "astro build",
    "preview": "astro preview",
    "astro": "astro"
  }
}

Project Creation

Available Templates

# Official templates
npm create astro@latest -- --template minimal
npm create astro@latest -- --template blog
npm create astro@latest -- --template portfolio
npm create astro@latest -- --template docs
npm create astro@latest -- --template with-tailwindcss
npm create astro@latest -- --template with-react
npm create astro@latest -- --template with-vue
npm create astro@latest -- --template with-svelte
npm create astro@latest -- --template with-preact
npm create astro@latest -- --template with-solid-js

# Community templates
npm create astro@latest -- --template cassidoo/shopify-react-astro
npm create astro@latest -- --template withastro/astro-theme-creek
npm create astro@latest -- --template satnaing/astro-paper

Development Commands

# Start development server
npm run dev

# Start with specific port
npm run dev -- --port 3000

# Start with specific host
npm run dev -- --host 0.0.0.0

# Start with open browser
npm run dev -- --open

# Start with verbose logging
npm run dev -- --verbose

Build Commands

# Build for production
npm run build

# Preview production build
npm run preview

# Check for issues
npx astro check

# Add missing integrations
npx astro add tailwind
npx astro add react
npx astro add vue

Project Structure

Basic Structure

my-astro-project/
├── public/                # Static assets
   ├── favicon.svg
   └── images/
├── src/
   ├── components/        # Reusable components
   ├── layouts/          # Layout components
   ├── pages/            # File-based routing
   ├── styles/           # CSS files
   └── content/          # Content collections
├── astro.config.mjs      # Astro configuration
├── package.json
└── tsconfig.json

Advanced Structure

src/
├── components/
   ├── ui/              # Basic UI components
   ├── layout/          # Layout-specific components
   └── content/         # Content-related components
├── layouts/
   ├── BaseLayout.astro
   ├── BlogLayout.astro
   └── DocsLayout.astro
├── pages/
   ├── index.astro      # Homepage
   ├── about.astro      # About page
   ├── blog/
   ├── index.astro  # Blog index
   └── [slug].astro # Blog post
   └── api/             # API routes
├── content/
   ├── blog/            # Blog posts
   └── docs/            # Documentation
├── styles/
   ├── global.css
   └── components.css
└── utils/
    ├── helpers.ts
    └── constants.ts

Configuration

// astro.config.mjs
import { defineConfig } from 'astro/config';
import tailwind from '@astrojs/tailwind';
import react from '@astrojs/react';
import mdx from '@astrojs/mdx';

export default defineConfig({
  site: 'https://example.com',
  base: '/',
  
  integrations: [
    tailwind(),
    react(),
    mdx()
  ],
  
  markdown: {
    shikiConfig: {
      theme: 'github-dark',
      wrap: true
    }
  },
  
  vite: {
    css: {
      preprocessorOptions: {
        scss: {
          additionalData: `@import "src/styles/variables.scss";`
        }
      }
    }
  },
  
  server: {
    port: 3000,
    host: true
  },
  
  build: {
    assets: 'assets'
  }
});

Components

Astro Components

---
// src/components/Card.astro
export interface Props {
  title: string;
  description: string;
  href?: string;
  image?: string;
}

const { title, description, href, image } = Astro.props;
---

<div class="card">
  {image && (
    <img src={image} alt={title} class="card-image" />
  )}
  
  <div class="card-content">
    <h3 class="card-title">{title}</h3>
    <p class="card-description">{description}</p>
    
    {href && (
      <a href={href} class="card-link">
        Read more →
      </a>
    )}
  </div>
</div>

<style>
  .card {
    background: white;
    border-radius: 8px;
    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
    overflow: hidden;
    transition: transform 0.2s ease;
  }
  
  .card:hover {
    transform: translateY(-2px);
  }
  
  .card-image {
    width: 100%;
    height: 200px;
    object-fit: cover;
  }
  
  .card-content {
    padding: 1.5rem;
  }
  
  .card-title {
    margin: 0 0 0.5rem 0;
    font-size: 1.25rem;
    font-weight: 600;
  }
  
  .card-description {
    margin: 0 0 1rem 0;
    color: #666;
    line-height: 1.6;
  }
  
  .card-link {
    color: #3b82f6;
    text-decoration: none;
    font-weight: 500;
  }
  
  .card-link:hover {
    text-decoration: underline;
  }
</style>

Component with Slots

---
// src/components/Modal.astro
export interface Props {
  isOpen: boolean;
  title: string;
}

const { isOpen, title } = Astro.props;
---

{isOpen && (
  <div class="modal-overlay">
    <div class="modal">
      <header class="modal-header">
        <h2>{title}</h2>
        <button class="modal-close" onclick="closeModal()">×</button>
      </header>
      
      <div class="modal-body">
        <slot />
      </div>
      
      <footer class="modal-footer">
        <slot name="footer" />
      </footer>
    </div>
  </div>
)}

<script>
  function closeModal() {
    document.querySelector('.modal-overlay').style.display = 'none';
  }
</script>

<style>
  .modal-overlay {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    background: rgba(0, 0, 0, 0.5);
    display: flex;
    align-items: center;
    justify-content: center;
    z-index: 1000;
  }
  
  .modal {
    background: white;
    border-radius: 8px;
    max-width: 500px;
    width: 90%;
    max-height: 80vh;
    overflow: auto;
  }
  
  .modal-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 1rem;
    border-bottom: 1px solid #e5e7eb;
  }
  
  .modal-close {
    background: none;
    border: none;
    font-size: 1.5rem;
    cursor: pointer;
  }
  
  .modal-body {
    padding: 1rem;
  }
  
  .modal-footer {
    padding: 1rem;
    border-top: 1px solid #e5e7eb;
  }
</style>

Framework Components

---
// src/components/Counter.astro
---

<div id="counter-container">
  <h3>Counter Example</h3>
  
  <!-- React Component -->
  <div class="framework-example">
    <h4>React Counter</h4>
    <ReactCounter client:load />
  </div>
  
  <!-- Vue Component -->
  <div class="framework-example">
    <h4>Vue Counter</h4>
    <VueCounter client:load />
  </div>
  
  <!-- Svelte Component -->
  <div class="framework-example">
    <h4>Svelte Counter</h4>
    <SvelteCounter client:load />
  </div>
</div>

<style>
  .framework-example {
    margin: 1rem 0;
    padding: 1rem;
    border: 1px solid #e5e7eb;
    border-radius: 8px;
  }
</style>
// src/components/ReactCounter.jsx
import { useState } from 'react';

export default function ReactCounter() {
  const [count, setCount] = useState(0);
  
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>
        Increment
      </button>
      <button onClick={() => setCount(count - 1)}>
        Decrement
      </button>
    </div>
  );
}

Component Scripts

---
// src/components/InteractiveCard.astro
export interface Props {
  title: string;
  content: string;
}

const { title, content } = Astro.props;
---

<div class="interactive-card" data-title={title}>
  <h3>{title}</h3>
  <p>{content}</p>
  <button class="toggle-btn">Toggle Details</button>
  <div class="details" style="display: none;">
    <p>Additional details about {title}</p>
  </div>
</div>

<script>
  // Client-side script
  document.addEventListener('DOMContentLoaded', () => {
    const cards = document.querySelectorAll('.interactive-card');
    
    cards.forEach(card => {
      const button = card.querySelector('.toggle-btn');
      const details = card.querySelector('.details');
      
      button.addEventListener('click', () => {
        const isVisible = details.style.display !== 'none';
        details.style.display = isVisible ? 'none' : 'block';
        button.textContent = isVisible ? 'Show Details' : 'Hide Details';
      });
    });
  });
</script>

<style>
  .interactive-card {
    border: 1px solid #e5e7eb;
    border-radius: 8px;
    padding: 1rem;
    margin: 1rem 0;
  }
  
  .toggle-btn {
    background: #3b82f6;
    color: white;
    border: none;
    padding: 0.5rem 1rem;
    border-radius: 4px;
    cursor: pointer;
  }
  
  .details {
    margin-top: 1rem;
    padding: 1rem;
    background: #f9fafb;
    border-radius: 4px;
  }
</style>

Pages and Routing

File-based Routing

# Route structure
src/pages/
├── index.astro           # / (homepage)
├── about.astro           # /about
├── contact.astro         # /contact
├── blog/
   ├── index.astro       # /blog
   ├── [slug].astro      # /blog/[slug]
   └── tag/
       └── [tag].astro   # /blog/tag/[tag]
├── products/
   ├── index.astro       # /products
   ├── [id].astro        # /products/[id]
   └── [...path].astro   # /products/[...path] (catch-all)
└── api/
    ├── posts.json.ts     # /api/posts.json
    └── contact.ts        # /api/contact

Basic Page

---
// src/pages/about.astro
import Layout from '../layouts/Layout.astro';

const pageTitle = 'About Us';
const description = 'Learn more about our company and mission.';
---

<Layout title={pageTitle} description={description}>
  <main>
    <h1>About Us</h1>
    
    <section class="hero">
      <h2>Our Mission</h2>
      <p>
        We're dedicated to creating amazing web experiences
        that delight users and drive business results.
      </p>
    </section>
    
    <section class="team">
      <h2>Our Team</h2>
      <div class="team-grid">
        <div class="team-member">
          <img src="/team/john.jpg" alt="John Doe" />
          <h3>John Doe</h3>
          <p>CEO & Founder</p>
        </div>
        <div class="team-member">
          <img src="/team/jane.jpg" alt="Jane Smith" />
          <h3>Jane Smith</h3>
          <p>CTO</p>
        </div>
      </div>
    </section>
  </main>
</Layout>

<style>
  .hero {
    text-align: center;
    padding: 4rem 0;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
    color: white;
    border-radius: 8px;
    margin: 2rem 0;
  }
  
  .team-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
    gap: 2rem;
    margin: 2rem 0;
  }
  
  .team-member {
    text-align: center;
    padding: 1rem;
    border: 1px solid #e5e7eb;
    border-radius: 8px;
  }
  
  .team-member img {
    width: 150px;
    height: 150px;
    border-radius: 50%;
    object-fit: cover;
    margin-bottom: 1rem;
  }
</style>

Dynamic Pages

---
// src/pages/blog/[slug].astro
import Layout from '../../layouts/BlogLayout.astro';
import { getCollection } from 'astro:content';

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

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

const { title, description, publishDate, author, tags } = entry.data;
---

<Layout title={title} description={description}>
  <article class="blog-post">
    <header class="post-header">
      <h1>{title}</h1>
      <div class="post-meta">
        <time datetime={publishDate.toISOString()}>
          {publishDate.toLocaleDateString('en-US', {
            year: 'numeric',
            month: 'long',
            day: 'numeric'
          })}
        </time>
        <span class="author">by {author}</span>
      </div>
      
      {tags && (
        <div class="tags">
          {tags.map(tag => (
            <a href={`/blog/tag/${tag}`} class="tag">
              #{tag}
            </a>
          ))}
        </div>
      )}
    </header>
    
    <div class="post-content">
      <Content />
    </div>
  </article>
</Layout>

<style>
  .blog-post {
    max-width: 800px;
    margin: 0 auto;
    padding: 2rem;
  }
  
  .post-header {
    margin-bottom: 3rem;
    text-align: center;
  }
  
  .post-meta {
    color: #666;
    margin: 1rem 0;
  }
  
  .author {
    margin-left: 1rem;
  }
  
  .tags {
    margin-top: 1rem;
  }
  
  .tag {
    display: inline-block;
    background: #f3f4f6;
    color: #374151;
    padding: 0.25rem 0.5rem;
    border-radius: 4px;
    text-decoration: none;
    margin: 0 0.25rem;
    font-size: 0.875rem;
  }
  
  .tag:hover {
    background: #e5e7eb;
  }
  
  .post-content {
    line-height: 1.8;
  }
  
  .post-content h2 {
    margin: 2rem 0 1rem 0;
    color: #1f2937;
  }
  
  .post-content p {
    margin: 1rem 0;
  }
  
  .post-content pre {
    background: #f9fafb;
    padding: 1rem;
    border-radius: 4px;
    overflow-x: auto;
  }
</style>

Catch-all Routes

---
// src/pages/docs/[...path].astro
import Layout from '../../layouts/DocsLayout.astro';
import { getCollection } from 'astro:content';

export async function getStaticPaths() {
  const docs = await getCollection('docs');
  
  return docs.map(doc => ({
    params: { path: doc.slug },
    props: { doc }
  }));
}

const { doc } = Astro.props;
const { Content, headings } = await doc.render();
---

<Layout title={doc.data.title}>
  <div class="docs-layout">
    <aside class="sidebar">
      <nav class="toc">
        <h3>Table of Contents</h3>
        <ul>
          {headings.map(heading => (
            <li class={`toc-${heading.depth}`}>
              <a href={`#${heading.slug}`}>
                {heading.text}
              </a>
            </li>
          ))}
        </ul>
      </nav>
    </aside>
    
    <main class="content">
      <h1>{doc.data.title}</h1>
      <Content />
    </main>
  </div>
</Layout>

<style>
  .docs-layout {
    display: grid;
    grid-template-columns: 250px 1fr;
    gap: 2rem;
    max-width: 1200px;
    margin: 0 auto;
    padding: 2rem;
  }
  
  .sidebar {
    position: sticky;
    top: 2rem;
    height: fit-content;
  }
  
  .toc ul {
    list-style: none;
    padding: 0;
  }
  
  .toc li {
    margin: 0.5rem 0;
  }
  
  .toc-2 {
    padding-left: 1rem;
  }
  
  .toc-3 {
    padding-left: 2rem;
  }
  
  .content {
    min-width: 0;
  }
</style>

Layouts

Base Layout

---
// src/layouts/Layout.astro
export interface Props {
  title: string;
  description?: string;
  image?: string;
}

const { title, description = 'Default description', image = '/og-image.jpg' } = Astro.props;
const canonicalURL = new URL(Astro.url.pathname, Astro.site);
---

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="description" content={description} />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
    <meta name="generator" content={Astro.generator} />
    
    <!-- Canonical URL -->
    <link rel="canonical" href={canonicalURL} />
    
    <!-- Open Graph -->
    <meta property="og:type" content="website" />
    <meta property="og:url" content={Astro.url} />
    <meta property="og:title" content={title} />
    <meta property="og:description" content={description} />
    <meta property="og:image" content={new URL(image, Astro.url)} />
    
    <!-- Twitter -->
    <meta property="twitter:card" content="summary_large_image" />
    <meta property="twitter:url" content={Astro.url} />
    <meta property="twitter:title" content={title} />
    <meta property="twitter:description" content={description} />
    <meta property="twitter:image" content={new URL(image, Astro.url)} />
    
    <title>{title}</title>
  </head>
  
  <body>
    <header class="site-header">
      <nav class="nav">
        <a href="/" class="logo">
          <img src="/logo.svg" alt="Logo" />
        </a>
        
        <ul class="nav-links">
          <li><a href="/">Home</a></li>
          <li><a href="/about">About</a></li>
          <li><a href="/blog">Blog</a></li>
          <li><a href="/contact">Contact</a></li>
        </ul>
        
        <button class="mobile-menu-toggle" aria-label="Toggle menu">
          <span></span>
          <span></span>
          <span></span>
        </button>
      </nav>
    </header>
    
    <slot />
    
    <footer class="site-footer">
      <div class="footer-content">
        <p>&copy; 2024 My Website. All rights reserved.</p>
        <div class="social-links">
          <a href="https://twitter.com" aria-label="Twitter">Twitter</a>
          <a href="https://github.com" aria-label="GitHub">GitHub</a>
        </div>
      </div>
    </footer>
  </body>
</html>

<style is:global>
  html {
    font-family: system-ui, sans-serif;
    scroll-behavior: smooth;
  }
  
  body {
    margin: 0;
    min-height: 100vh;
    display: flex;
    flex-direction: column;
  }
  
  main {
    flex: 1;
  }
  
  .site-header {
    background: white;
    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
    position: sticky;
    top: 0;
    z-index: 100;
  }
  
  .nav {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 1rem 2rem;
    max-width: 1200px;
    margin: 0 auto;
  }
  
  .logo img {
    height: 40px;
  }
  
  .nav-links {
    display: flex;
    list-style: none;
    margin: 0;
    padding: 0;
    gap: 2rem;
  }
  
  .nav-links a {
    text-decoration: none;
    color: #374151;
    font-weight: 500;
    transition: color 0.2s;
  }
  
  .nav-links a:hover {
    color: #3b82f6;
  }
  
  .mobile-menu-toggle {
    display: none;
    flex-direction: column;
    background: none;
    border: none;
    cursor: pointer;
    padding: 0.5rem;
  }
  
  .mobile-menu-toggle span {
    width: 25px;
    height: 3px;
    background: #374151;
    margin: 3px 0;
    transition: 0.3s;
  }
  
  .site-footer {
    background: #1f2937;
    color: white;
    padding: 2rem;
    margin-top: auto;
  }
  
  .footer-content {
    max-width: 1200px;
    margin: 0 auto;
    display: flex;
    justify-content: space-between;
    align-items: center;
  }
  
  .social-links {
    display: flex;
    gap: 1rem;
  }
  
  .social-links a {
    color: white;
    text-decoration: none;
  }
  
  @media (max-width: 768px) {
    .nav-links {
      display: none;
    }
    
    .mobile-menu-toggle {
      display: flex;
    }
    
    .footer-content {
      flex-direction: column;
      gap: 1rem;
      text-align: center;
    }
  }
</style>

<script>
  // Mobile menu toggle
  document.addEventListener('DOMContentLoaded', () => {
    const toggle = document.querySelector('.mobile-menu-toggle');
    const navLinks = document.querySelector('.nav-links');
    
    toggle?.addEventListener('click', () => {
      navLinks?.classList.toggle('active');
    });
  });
</script>

Blog Layout

---
// src/layouts/BlogLayout.astro
import Layout from './Layout.astro';

export interface Props {
  title: string;
  description?: string;
  publishDate?: Date;
  author?: string;
  image?: string;
}

const { title, description, publishDate, author, image } = Astro.props;
---

<Layout title={title} description={description} image={image}>
  <main class="blog-main">
    <div class="container">
      <article class="blog-article">
        <slot />
      </article>
      
      <aside class="blog-sidebar">
        <div class="sidebar-section">
          <h3>Recent Posts</h3>
          <ul class="recent-posts">
            <!-- Recent posts would be populated here -->
          </ul>
        </div>
        
        <div class="sidebar-section">
          <h3>Categories</h3>
          <ul class="categories">
            <li><a href="/blog/category/web-development">Web Development</a></li>
            <li><a href="/blog/category/javascript">JavaScript</a></li>
            <li><a href="/blog/category/css">CSS</a></li>
          </ul>
        </div>
        
        <div class="sidebar-section">
          <h3>Newsletter</h3>
          <form class="newsletter-form">
            <input type="email" placeholder="Your email" required />
            <button type="submit">Subscribe</button>
          </form>
        </div>
      </aside>
    </div>
  </main>
</Layout>

<style>
  .blog-main {
    padding: 2rem 0;
  }
  
  .container {
    max-width: 1200px;
    margin: 0 auto;
    padding: 0 2rem;
    display: grid;
    grid-template-columns: 1fr 300px;
    gap: 3rem;
  }
  
  .blog-article {
    min-width: 0;
  }
  
  .blog-sidebar {
    position: sticky;
    top: 2rem;
    height: fit-content;
  }
  
  .sidebar-section {
    background: #f9fafb;
    padding: 1.5rem;
    border-radius: 8px;
    margin-bottom: 2rem;
  }
  
  .sidebar-section h3 {
    margin: 0 0 1rem 0;
    color: #1f2937;
  }
  
  .recent-posts,
  .categories {
    list-style: none;
    padding: 0;
    margin: 0;
  }
  
  .recent-posts li,
  .categories li {
    margin: 0.5rem 0;
  }
  
  .recent-posts a,
  .categories a {
    color: #374151;
    text-decoration: none;
  }
  
  .recent-posts a:hover,
  .categories a:hover {
    color: #3b82f6;
  }
  
  .newsletter-form {
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
  }
  
  .newsletter-form input {
    padding: 0.5rem;
    border: 1px solid #d1d5db;
    border-radius: 4px;
  }
  
  .newsletter-form button {
    background: #3b82f6;
    color: white;
    border: none;
    padding: 0.5rem;
    border-radius: 4px;
    cursor: pointer;
  }
  
  @media (max-width: 768px) {
    .container {
      grid-template-columns: 1fr;
      gap: 2rem;
    }
    
    .blog-sidebar {
      position: static;
    }
  }
</style>

Styling

Global Styles

/* src/styles/global.css */
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');

:root {
  --color-primary: #3b82f6;
  --color-secondary: #64748b;
  --color-success: #10b981;
  --color-warning: #f59e0b;
  --color-error: #ef4444;
  
  --color-text: #1f2937;
  --color-text-light: #6b7280;
  --color-background: #ffffff;
  --color-surface: #f9fafb;
  
  --spacing-xs: 0.25rem;
  --spacing-sm: 0.5rem;
  --spacing-md: 1rem;
  --spacing-lg: 1.5rem;
  --spacing-xl: 2rem;
  --spacing-2xl: 3rem;
  
  --border-radius: 0.375rem;
  --border-radius-lg: 0.5rem;
  
  --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
  --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
  --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
}

* {
  box-sizing: border-box;
}

html {
  font-family: 'Inter', system-ui, sans-serif;
  scroll-behavior: smooth;
}

body {
  margin: 0;
  color: var(--color-text);
  background-color: var(--color-background);
  line-height: 1.6;
}

/* Typography */
h1, h2, h3, h4, h5, h6 {
  margin: 0 0 var(--spacing-md) 0;
  font-weight: 600;
  line-height: 1.3;
}

h1 { font-size: 2.5rem; }
h2 { font-size: 2rem; }
h3 { font-size: 1.5rem; }
h4 { font-size: 1.25rem; }
h5 { font-size: 1.125rem; }
h6 { font-size: 1rem; }

p {
  margin: 0 0 var(--spacing-md) 0;
}

/* Links */
a {
  color: var(--color-primary);
  text-decoration: none;
  transition: color 0.2s ease;
}

a:hover {
  color: #2563eb;
  text-decoration: underline;
}

/* Buttons */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: var(--spacing-sm) var(--spacing-md);
  border: none;
  border-radius: var(--border-radius);
  font-weight: 500;
  text-decoration: none;
  cursor: pointer;
  transition: all 0.2s ease;
}

.btn-primary {
  background-color: var(--color-primary);
  color: white;
}

.btn-primary:hover {
  background-color: #2563eb;
  text-decoration: none;
}

.btn-secondary {
  background-color: var(--color-secondary);
  color: white;
}

.btn-outline {
  background-color: transparent;
  color: var(--color-primary);
  border: 1px solid var(--color-primary);
}

/* Utilities */
.container {
  max-width: 1200px;
  margin: 0 auto;
  padding: 0 var(--spacing-md);
}

.text-center { text-align: center; }
.text-left { text-align: left; }
.text-right { text-align: right; }

.mt-0 { margin-top: 0; }
.mt-1 { margin-top: var(--spacing-xs); }
.mt-2 { margin-top: var(--spacing-sm); }
.mt-4 { margin-top: var(--spacing-md); }
.mt-6 { margin-top: var(--spacing-lg); }
.mt-8 { margin-top: var(--spacing-xl); }

.mb-0 { margin-bottom: 0; }
.mb-1 { margin-bottom: var(--spacing-xs); }
.mb-2 { margin-bottom: var(--spacing-sm); }
.mb-4 { margin-bottom: var(--spacing-md); }
.mb-6 { margin-bottom: var(--spacing-lg); }
.mb-8 { margin-bottom: var(--spacing-xl); }

/* Responsive */
@media (max-width: 768px) {
  h1 { font-size: 2rem; }
  h2 { font-size: 1.75rem; }
  h3 { font-size: 1.25rem; }
  
  .container {
    padding: 0 var(--spacing-sm);
  }
}

Component Styles

---
// src/components/Hero.astro
export interface Props {
  title: string;
  subtitle: string;
  backgroundImage?: string;
}

const { title, subtitle, backgroundImage } = Astro.props;
---

<section class="hero" style={backgroundImage ? `background-image: url(${backgroundImage})` : ''}>
  <div class="hero-content">
    <h1 class="hero-title">{title}</h1>
    <p class="hero-subtitle">{subtitle}</p>
    <div class="hero-actions">
      <a href="/get-started" class="btn btn-primary">Get Started</a>
      <a href="/learn-more" class="btn btn-outline">Learn More</a>
    </div>
  </div>
</section>

<style>
  .hero {
    position: relative;
    min-height: 60vh;
    display: flex;
    align-items: center;
    justify-content: center;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
    background-size: cover;
    background-position: center;
    color: white;
    text-align: center;
  }
  
  .hero::before {
    content: '';
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    background: rgba(0, 0, 0, 0.4);
    z-index: 1;
  }
  
  .hero-content {
    position: relative;
    z-index: 2;
    max-width: 800px;
    padding: 2rem;
  }
  
  .hero-title {
    font-size: 3.5rem;
    font-weight: 700;
    margin-bottom: 1rem;
    text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
  }
  
  .hero-subtitle {
    font-size: 1.25rem;
    margin-bottom: 2rem;
    opacity: 0.9;
  }
  
  .hero-actions {
    display: flex;
    gap: 1rem;
    justify-content: center;
    flex-wrap: wrap;
  }
  
  @media (max-width: 768px) {
    .hero-title {
      font-size: 2.5rem;
    }
    
    .hero-subtitle {
      font-size: 1.125rem;
    }
    
    .hero-actions {
      flex-direction: column;
      align-items: center;
    }
  }
</style>

Tailwind CSS Integration

# Install Tailwind CSS
npx astro add tailwind
---
// src/components/Card.astro with Tailwind
export interface Props {
  title: string;
  description: string;
  image?: string;
}

const { title, description, image } = Astro.props;
---

<div class="bg-white rounded-lg shadow-md overflow-hidden hover:shadow-lg transition-shadow duration-300">
  {image && (
    <img 
      src={image} 
      alt={title} 
      class="w-full h-48 object-cover"
    />
  )}
  
  <div class="p-6">
    <h3 class="text-xl font-semibold text-gray-900 mb-2">{title}</h3>
    <p class="text-gray-600 leading-relaxed">{description}</p>
    
    <div class="mt-4">
      <a 
        href="#" 
        class="inline-flex items-center text-blue-600 hover:text-blue-800 font-medium"
      >
        Read more
        <svg class="ml-1 w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
          <path fill-rule="evenodd" d="M10.293 3.293a1 1 0 011.414 0l6 6a1 1 0 010 1.414l-6 6a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-4.293-4.293a1 1 0 010-1.414z" clip-rule="evenodd"></path>
        </svg>
      </a>
    </div>
  </div>
</div>

Content Collections

Configuration

// src/content/config.ts
import { defineCollection, z } from 'astro:content';

const blog = defineCollection({
  type: 'content',
  schema: z.object({
    title: z.string(),
    description: z.string(),
    publishDate: z.date(),
    author: z.string(),
    image: z.string().optional(),
    tags: z.array(z.string()).optional(),
    featured: z.boolean().default(false),
    draft: z.boolean().default(false)
  })
});

const docs = defineCollection({
  type: 'content',
  schema: z.object({
    title: z.string(),
    description: z.string(),
    category: z.string(),
    order: z.number().optional(),
    lastUpdated: z.date().optional()
  })
});

const team = defineCollection({
  type: 'data',
  schema: z.object({
    name: z.string(),
    role: z.string(),
    bio: z.string(),
    avatar: z.string(),
    social: z.object({
      twitter: z.string().optional(),
      github: z.string().optional(),
      linkedin: z.string().optional()
    }).optional()
  })
});

export const collections = {
  blog,
  docs,
  team
};

Content Files

---
# src/content/blog/getting-started-with-astro.md
title: "Getting Started with Astro"
description: "Learn how to build fast, content-focused websites with Astro"
publishDate: 2024-01-15
author: "John Doe"
image: "/blog/astro-getting-started.jpg"
tags: ["astro", "web-development", "static-sites"]
featured: true
---

# Getting Started with Astro

Astro is a modern static site generator that delivers lightning-fast performance with a modern developer experience. In this guide, we'll explore how to get started with Astro and build your first website.

## What is Astro?

Astro is designed for building content-rich websites like blogs, marketing sites, and documentation. It uses a unique "Islands Architecture" that allows you to use your favorite UI framework while shipping minimal JavaScript to the browser.

## Key Features

- **Zero JavaScript by default**: Astro generates static HTML with no client-side JavaScript unless you explicitly opt-in
- **Framework agnostic**: Use React, Vue, Svelte, or any other framework
- **File-based routing**: Create pages by adding files to the `src/pages` directory
- **Content collections**: Organize and validate your content with TypeScript

## Installation

Getting started with Astro is simple:
npm create astro@latest
cd my-project
npm install
npm run dev

## Your First Component

Here's a simple Astro component:

```astro
```astro
---
const greeting = "Bonjour, Astro !";
---

<div class="greeting">
  <h1>{greeting}</h1>
  <p>Welcome to the future of web development.</p>
</div>

<style>
  .greeting {
    text-align: center;
    padding: 2rem;
  }
</style>

## Conclusion

Astro provides an excellent developer experience while delivering exceptional performance. Its unique approach to JavaScript hydration makes it perfect for content-focused websites.

Using Collections

---
// src/pages/blog/index.astro
import Layout from '../../layouts/Layout.astro';
import Card from '../../components/Card.astro';
import { getCollection } from 'astro:content';

const allBlogPosts = await getCollection('blog', ({ data }) => {
  return !data.draft;
});

// Sort by publish date
const sortedPosts = allBlogPosts.sort((a, b) => 
  b.data.publishDate.valueOf() - a.data.publishDate.valueOf()
);

// Get featured posts
const featuredPosts = sortedPosts.filter(post => post.data.featured);
---

<Layout title="Blog" description="Read our latest blog posts">
  <main class="blog-index">
    <div class="container">
      <header class="page-header">
        <h1>Blog</h1>
        <p>Insights, tutorials, and updates from our team</p>
      </header>
      
      {featuredPosts.length > 0 && (
        <section class="featured-posts">
          <h2>Featured Posts</h2>
          <div class="posts-grid">
            {featuredPosts.map(post => (
              <Card
                title={post.data.title}
                description={post.data.description}
                href={`/blog/${post.slug}`}
                image={post.data.image}
              />
            ))}
          </div>
        </section>
      )}
      
      <section class="all-posts">
        <h2>All Posts</h2>
        <div class="posts-grid">
          {sortedPosts.map(post => (
            <Card
              title={post.data.title}
              description={post.data.description}
              href={`/blog/${post.slug}`}
              image={post.data.image}
            />
          ))}
        </div>
      </section>
    </div>
  </main>
</Layout>

<style>
  .blog-index {
    padding: 2rem 0;
  }
  
  .page-header {
    text-align: center;
    margin-bottom: 3rem;
  }
  
  .page-header h1 {
    font-size: 3rem;
    margin-bottom: 1rem;
  }
  
  .featured-posts,
  .all-posts {
    margin-bottom: 3rem;
  }
  
  .posts-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
    gap: 2rem;
    margin-top: 2rem;
  }
</style>

Integrations

React Integration

# Add React integration
npx astro add react
// src/components/ReactCounter.jsx
import { useState } from 'react';

export default function ReactCounter({ initialCount = 0 }) {
  const [count, setCount] = useState(initialCount);
  
  return (
    <div className="counter">
      <h3>React Counter</h3>
      <p>Count: {count}</p>
      <div className="counter-buttons">
        <button onClick={() => setCount(count - 1)}>-</button>
        <button onClick={() => setCount(count + 1)}>+</button>
        <button onClick={() => setCount(0)}>Reset</button>
      </div>
    </div>
  );
}
---
// Using React component in Astro
import Layout from '../layouts/Layout.astro';
import ReactCounter from '../components/ReactCounter.jsx';
---

<Layout title="Interactive Demo">
  <main>
    <h1>Interactive Components</h1>
    
    <!-- Hydrate on page load -->
    <ReactCounter client:load initialCount={5} />
    
    <!-- Hydrate when visible -->
    <ReactCounter client:visible initialCount={10} />
    
    <!-- Hydrate on idle -->
    <ReactCounter client:idle />
    
    <!-- Hydrate on media query -->
    <ReactCounter client:media="(max-width: 768px)" />
  </main>
</Layout>

Vue Integration

# Add Vue integration
npx astro add vue
<!-- src/components/VueCounter.vue -->
<template>
  <div class="counter">
    <h3>Vue Counter</h3>
    <p>Count: {{ count }}</p>
    <div class="counter-buttons">
      <button @click="decrement">-</button>
      <button @click="increment">+</button>
      <button @click="reset">Reset</button>
    </div>
  </div>
</template>

<script setup>
import { ref } from 'vue';

const props = defineProps({
  initialCount: {
    type: Number,
    default: 0
  }
});

const count = ref(props.initialCount);

const increment = () => count.value++;
const decrement = () => count.value--;
const reset = () => count.value = 0;
</script>

<style scoped>
.counter {
  border: 1px solid #e5e7eb;
  border-radius: 8px;
  padding: 1rem;
  margin: 1rem 0;
}

.counter-buttons {
  display: flex;
  gap: 0.5rem;
  margin-top: 1rem;
}

button {
  padding: 0.5rem 1rem;
  border: 1px solid #d1d5db;
  border-radius: 4px;
  background: white;
  cursor: pointer;
}

button:hover {
  background: #f9fafb;
}
</style>

MDX Integration

# Add MDX integration
npx astro add mdx
---
# src/pages/interactive-post.mdx
title: "Interactive Blog Post"
description: "A blog post with interactive components"
---

import ReactCounter from '../components/ReactCounter.jsx';
import VueCounter from '../components/VueCounter.vue';

# Interactive Blog Post

This is a regular markdown paragraph. But we can also include interactive components:

<ReactCounter client:load initialCount={5} />

Here's another component with different hydration:

<VueCounter client:visible initialCount={10} />

## Code Examples

```javascript
```javascript
function greet(name) {
  return `Hello, ${name}!`;
}

And we can continue with regular markdown content...

Islands Architecture

Client Directives

---
// src/pages/islands-demo.astro
import Layout from '../layouts/Layout.astro';
import ReactCounter from '../components/ReactCounter.jsx';
import VueChart from '../components/VueChart.vue';
import SvelteWidget from '../components/SvelteWidget.svelte';
---

<Layout title="Islands Architecture Demo">
  <main>
    <h1>Islands Architecture</h1>
    
    <!-- Load immediately -->
    <ReactCounter client:load />
    
    <!-- Load when component becomes visible -->
    <VueChart client:visible />
    
    <!-- Load when browser is idle -->
    <SvelteWidget client:idle />
    
    <!-- Load only on mobile -->
    <ReactCounter client:media="(max-width: 768px)" />
    
    <!-- Load only on specific event -->
    <VueChart client:only="vue" />
    
    <!-- Never hydrate (static only) -->
    <ReactCounter />
  </main>
</Layout>

Performance Optimization

---
// Optimize component loading
import { Image } from 'astro:assets';
import HeavyComponent from '../components/HeavyComponent.jsx';
---

<div class="optimized-page">
  <!-- Optimized images -->
  <Image 
    src="/hero.jpg" 
    alt="Hero image"
    width={800}
    height={600}
    loading="eager"
  />
  
  <!-- Lazy load heavy components -->
  <HeavyComponent client:visible />
  
  <!-- Preload critical resources -->
  <link rel="preload" href="/fonts/inter.woff2" as="font" type="font/woff2" crossorigin />
</div>

API Routes

Basic API Routes

// src/pages/api/posts.json.ts
import type { APIRoute } from 'astro';
import { getCollection } from 'astro:content';

export const GET: APIRoute = async ({ params, request }) => {
  const posts = await getCollection('blog');
  
  return new Response(JSON.stringify(posts), {
    status: 200,
    headers: {
      'Content-Type': 'application/json'
    }
  });
};

export const POST: APIRoute = async ({ request }) => {
  const data = await request.json();
  
  // Process the data
  console.log('Received:', data);
  
  return new Response(JSON.stringify({ success: true }), {
    status: 201,
    headers: {
      'Content-Type': 'application/json'
    }
  });
};

Dynamic API Routes

Would you like me to fill in the translations for the specific sections you want translated? Some sections appear to be empty, so I’ll need more context to provide accurate translations.```typescript // src/pages/api/posts/[id].json.ts import type { APIRoute } from ‘astro’; import { getCollection } from ‘astro:content’;

export const GET: APIRoute = async ({ params }) => { const { id } = params;

if (!id) { return new Response(JSON.stringify({ error: ‘ID is required’ }), { status: 400, headers: { ‘Content-Type’: ‘application/json’ } }); }

const posts = await getCollection(‘blog’); const post = posts.find(p => p.slug === id);

if (!post) { return new Response(JSON.stringify({ error: ‘Post not found’ }), { status: 404, headers: { ‘Content-Type’: ‘application/json’ } }); }

return new Response(JSON.stringify(post), { status: 200, headers: { ‘Content-Type’: ‘application/json’ } }); };


### Form Handling
```typescript
// src/pages/api/contact.ts
import type { APIRoute } from 'astro';

export const POST: APIRoute = async ({ request }) => {
  try {
    const formData = await request.formData();
    const name = formData.get('name') as string;
    const email = formData.get('email') as string;
    const message = formData.get('message') as string;
    
    // Validate data
    if (!name || !email || !message) {
      return new Response(JSON.stringify({ 
        error: 'All fields are required' 
      }), {
        status: 400,
        headers: { 'Content-Type': 'application/json' }
      });
    }
    
    // Process form (send email, save to database, etc.)
    await sendEmail({ name, email, message });
    
    return new Response(JSON.stringify({ 
      success: true,
      message: 'Thank you for your message!' 
    }), {
      status: 200,
      headers: { 'Content-Type': 'application/json' }
    });
    
  } catch (error) {
    return new Response(JSON.stringify({ 
      error: 'Internal server error' 
    }), {
      status: 500,
      headers: { 'Content-Type': 'application/json' }
    });
  }
};

async function sendEmail(data: { name: string; email: string; message: string }) {
  // Email sending logic
  console.log('Sending email:', data);
}

Image Optimization

Astro Assets

---
// src/components/OptimizedImages.astro
import { Image } from 'astro:assets';
import heroImage from '../assets/hero.jpg';
import profileImage from '../assets/profile.png';
---

<div class="image-gallery">
  <!-- Local images with optimization -->
  <Image 
    src={heroImage}
    alt="Hero image"
    width={800}
    height={600}
    loading="eager"
    class="hero-image"
  />
  
  <!-- Responsive images -->
  <Image 
    src={profileImage}
    alt="Profile"
    widths={[240, 540, 720]}
    sizes="(max-width: 360px) 240px, (max-width: 720px) 540px, 720px"
    class="profile-image"
  />
  
  <!-- Remote images -->
  <Image 
    src="https://example.com/image.jpg"
    alt="Remote image"
    width={400}
    height={300}
    loading="lazy"
  />
  
  <!-- Background images -->
  <div class="hero-section">
    <Image 
      src={heroImage}
      alt=""
      width={1200}
      height={600}
      class="background-image"
    />
    <div class="hero-content">
      <h1>Hero Title</h1>
      <p>Hero description</p>
    </div>
  </div>
</div>

<style>
  .image-gallery {
    display: grid;
    gap: 2rem;
    padding: 2rem;
  }
  
  .hero-image {
    width: 100%;
    height: auto;
    border-radius: 8px;
  }
  
  .profile-image {
    width: 200px;
    height: 200px;
    border-radius: 50%;
    object-fit: cover;
  }
  
  .hero-section {
    position: relative;
    height: 400px;
    border-radius: 8px;
    overflow: hidden;
  }
  
  .background-image {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
    z-index: 1;
  }
  
  .hero-content {
    position: relative;
    z-index: 2;
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    height: 100%;
    color: white;
    text-align: center;
    background: rgba(0, 0, 0, 0.4);
  }
</style>

Image Processing

// astro.config.mjs
import { defineConfig } from 'astro/config';

export default defineConfig({
  image: {
    service: {
      entrypoint: 'astro/assets/services/sharp'
    }
  },
  
  vite: {
    optimizeDeps: {
      include: ['sharp']
    }
  }
});

Deployment

Static Deployment

# Build static site
npm run build

# Output will be in dist/ directory
# Deploy to any static hosting service

Vercel Deployment

// vercel.json
{
  "framework": "astro",
  "buildCommand": "npm run build",
  "devCommand": "npm run dev",
  "installCommand": "npm install"
}

Netlify Deployment

# netlify.toml
[build]
  command = "npm run build"
  publish = "dist"

[build.environment]
  NODE_VERSION = "18"

[[redirects]]
  from = "/*"
  to = "/404.html"
  status = 404

Docker Deployment

# Dockerfile
FROM node:18-alpine AS build

WORKDIR /app
COPY package*.json ./
RUN npm ci

COPY . .
RUN npm run build

FROM nginx:alpine
COPY --from=build /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/nginx.conf

EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

Server-Side Rendering

// astro.config.mjs
import { defineConfig } from 'astro/config';
import node from '@astrojs/node';

export default defineConfig({
  output: 'server',
  adapter: node({
    mode: 'standalone'
  })
});

Performance

Bundle Analysis

# Analyze bundle size
npm run build -- --analyze

# Check lighthouse scores
npx lighthouse http://localhost:3000

Optimization Techniques

---
// Performance optimizations
import { Image } from 'astro:assets';
---

<!-- Preload critical resources -->
<link rel="preload" href="/fonts/inter.woff2" as="font" type="font/woff2" crossorigin />

<!-- Optimize images -->
<Image 
  src="/hero.jpg"
  alt="Hero"
  width={800}
  height={600}
  loading="eager"
  decoding="async"
/>

<!-- Lazy load non-critical components -->
<HeavyComponent client:visible />

<!-- Minimize JavaScript -->
<script>
  // Only essential JavaScript
  console.log('Page loaded');
</script>

Testing

Unit Testing

# Install testing dependencies
npm install --save-dev vitest @astrojs/test-utils
// tests/components/Card.test.ts
import { experimental_AstroContainer as AstroContainer } from 'astro/container';
import { expect, test } from 'vitest';
import Card from '../src/components/Card.astro';

test('Card component renders correctly', async () => {
  const container = await AstroContainer.create();
  const result = await container.renderToString(Card, {
    props: {
      title: 'Test Card',
      description: 'Test description'
    }
  });
  
  expect(result).toContain('Test Card');
  expect(result).toContain('Test description');
});

E2E Testing

# Install Playwright
npm install --save-dev @playwright/test
// tests/e2e/homepage.spec.ts
import { test, expect } from '@playwright/test';

test('homepage loads correctly', async ({ page }) => {
  await page.goto('/');
  
  await expect(page).toHaveTitle(/My Astro Site/);
  await expect(page.getByRole('heading', { name: 'Welcome' })).toBeVisible();
});

test('navigation works', async ({ page }) => {
  await page.goto('/');
  
  await page.getByRole('link', { name: 'About' }).click();
  await expect(page).toHaveURL('/about');
});

Troubleshooting

Common Issues

Build Errors

# Clear cache and rebuild
rm -rf node_modules/.astro dist
npm install
npm run build

# Check for TypeScript errors
npx astro check

Hydration Issues

<!-- Fix hydration mismatches -->
<div class="client-only">
  <ClientComponent client:only="react" />
</div>

<!-- Use proper client directives -->
<InteractiveComponent client:load />
<LazyComponent client:visible />

Performance Issues

---
// Optimize component loading
---

<!-- Use appropriate client directives -->
<HeavyComponent client:idle />

<!-- Optimize images -->
<Image src="/large-image.jpg" width={800} height={600} loading="lazy" />

<!-- Minimize JavaScript -->
<script is:inline>
  // Inline only critical scripts
</script>

Best Practices

Project Organization

# Recommended structure
src/
├── components/
   ├── ui/           # Reusable UI components
   ├── layout/       # Layout components
   └── content/      # Content-specific components
├── layouts/          # Page layouts
├── pages/            # Routes
├── content/          # Content collections
├── assets/           # Images, fonts, etc.
├── styles/           # Global styles
└── utils/            # Utility functions

Performance Best Practices

---
// Optimize for performance
import { Image } from 'astro:assets';
---

<!-- Use appropriate loading strategies -->
<Image src="/hero.jpg" alt="Hero" loading="eager" />
<Image src="/gallery.jpg" alt="Gallery" loading="lazy" />

<!-- Minimize client-side JavaScript -->
<InteractiveComponent client:visible />

<!-- Preload critical resources -->
<link rel="preload" href="/critical.css" as="style" />

SEO Best Practices

---
// SEO optimization
const { title, description, image } = Astro.props;
const canonicalURL = new URL(Astro.url.pathname, Astro.site);
---

<head>
  <title>{title}</title>
  <meta name="description" content={description} />
  <link rel="canonical" href={canonicalURL} />
  
  <!-- Open Graph -->
  <meta property="og:title" content={title} />
  <meta property="og:description" content={description} />
  <meta property="og:image" content={image} />
  
  <!-- Structured data -->
  <script type="application/ld+json">
    {JSON.stringify({
      "@context": "https://schema.org",
      "@type": "WebPage",
      "name": title,
      "description": description
    })}
  </script>
</head>

Summary

Astro is a powerful static site generator that excels at building content-driven websites with exceptional performance. Key features include:

  • Islands Architecture: Ship minimal JavaScript with selective hydration
  • Framework Agnostic: Use React, Vue, Svelte, or any framework
  • Content Collections: Type-safe content management with validation
  • File-based Routing: Intuitive routing based on file structure
  • Image Optimization: Built-in image processing and optimization
  • Performance First: Zero JavaScript by default with opt-in interactivity

For optimal results, leverage static generation for content pages, use client directives judiciously for interactivity, optimize images with Astro’s built-in tools, and follow content collection patterns for scalable content management.