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