コンテンツにスキップ

Astro Cheatsheet

Astro - The Web Framework for Content-Driven Websites

Astro is a modern static site generator that delivers lightning-fast performance with a modern developer experience. Build content sites like blogs, marketing, and e-commerce with your favorite UI components and deploy anywhere.

Table of Contents

Installation

Prerequisites

# 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

# 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:

```bash
npm create astro@latest
cd my-project
npm install
npm run dev

Your First Component

Here's a simple Astro component:

---
const greeting = "Hello, 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
```astro
---
// 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
function greet(name) {
  return `Hello, ${name}!`;
}

And we can continue with regular markdown content...


## Islands Architecture

### Client Directives
```astro
---
// 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

// 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

// 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.