🎯 Recommended Samples
Balanced sample collections from various categories for you to explore
Astro Samples
Astro framework examples - Modern static site builder with island architecture, partial hydration, and multi-framework support
💻 Astro Hello World typescript
🟢 simple
⭐
Basic Astro pages, components, file-based routing, and core syntax
⏱️ 15 min
🏷️ astro, static site, components, routing
Prerequisites:
HTML/CSS basics, JavaScript fundamentals, Basic understanding of static site generators
// Astro Hello World Examples
// 1. Basic Astro page (src/pages/index.astro)
---
// Frontmatter - server-side JavaScript code
const title = "Welcome to Astro";
const message = "Modern web development simplified";
const features = [
"Zero JS by default",
"Islands Architecture",
"Multi-framework support",
"Built-in performance optimizations"
];
const currentDate = new Date().toLocaleDateString();
---
<!-- HTML template -->
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width" />
<title>{title}</title>
<style>
:global(body) {
font-family: system-ui, -apple-system, sans-serif;
margin: 0;
padding: 40px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
}
.container {
max-width: 800px;
margin: 0 auto;
background: white;
padding: 40px;
border-radius: 12px;
box-shadow: 0 20px 40px rgba(0,0,0,0.1);
}
.features {
list-style: none;
padding: 0;
}
.features li {
padding: 12px 0;
border-bottom: 1px solid #eee;
}
.features li:before {
content: "✨ ";
color: #667eea;
}
</style>
</head>
<body>
<div class="container">
<h1>{title}</h1>
<p>{message}</p>
<p>Generated on: {currentDate}</p>
<h2>Key Features:</h2>
<ul class="features">
{features.map(feature => <li>{feature}</li>)}
</ul>
</div>
</body>
</html>
// 2. Astro component (src/components/Card.astro)
---
interface Props {
title: string;
description: string;
imageUrl?: string;
featured?: boolean;
}
const { title, description, imageUrl, featured = false } = Astro.props;
---
<div class={`card ${featured ? 'featured' : ''}`}>
{imageUrl && <img src={imageUrl} alt={title} class="card-image" />}
<div class="card-content">
<h3>{title}</h3>
<p>{description}</p>
<slot />
</div>
</div>
<style>
.card {
border: 1px solid #e2e8f0;
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
background: white;
transition: transform 0.2s;
}
.card:hover {
transform: translateY(-2px);
box-shadow: 0 8px 16px rgba(0,0,0,0.1);
}
.card.featured {
border-color: #667eea;
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.2);
}
.card-image {
width: 100%;
height: 200px;
object-fit: cover;
border-radius: 4px 4px 0 0;
margin: -20px -20px 20px -20px;
}
.card-content h3 {
margin-top: 0;
color: #2d3748;
}
</style>
// 3. Layout component (src/layouts/MainLayout.astro)
---
export interface Props {
title: string;
}
const { title } = Astro.props;
const currentYear = new Date().getFullYear();
---
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width" />
<title>{title} - My Astro Site</title>
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<style is:global>
:root {
--primary-color: #667eea;
--text-color: #2d3748;
--bg-color: #f7fafc;
--border-color: #e2e8f0;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
font-family: system-ui, -apple-system, sans-serif;
line-height: 1.6;
color: var(--text-color);
background: var(--bg-color);
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 20px;
}
</style>
</head>
<body>
<header>
<nav class="container">
<div class="nav-brand">
<a href="/">Astro Site</a>
</div>
<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>
</nav>
</header>
<main>
<slot />
</main>
<footer>
<div class="container">
<p>© {currentYear} My Astro Site. All rights reserved.</p>
</div>
</footer>
<style>
header {
background: white;
border-bottom: 1px solid var(--border-color);
padding: 1rem 0;
position: sticky;
top: 0;
z-index: 100;
}
nav {
display: flex;
justify-content: space-between;
align-items: center;
}
.nav-brand a {
font-size: 1.5rem;
font-weight: bold;
color: var(--primary-color);
text-decoration: none;
}
.nav-links {
display: flex;
list-style: none;
margin: 0;
padding: 0;
gap: 2rem;
}
.nav-links a {
color: var(--text-color);
text-decoration: none;
font-weight: 500;
transition: color 0.2s;
}
.nav-links a:hover {
color: var(--primary-color);
}
main {
min-height: calc(100vh - 120px);
padding: 2rem 0;
}
footer {
background: white;
border-top: 1px solid var(--border-color);
padding: 2rem 0;
text-align: center;
color: #718096;
}
@media (max-width: 768px) {
nav {
flex-direction: column;
gap: 1rem;
}
.nav-links {
gap: 1rem;
}
}
</style>
</body>
</html>
// 4. Blog listing page (src/pages/blog/index.astro)
---
import Card from '../../components/Card.astro';
// Mock blog data
const posts = [
{
title: "Getting Started with Astro",
description: "Learn the basics of Astro and how to build fast websites.",
date: "2024-01-15",
slug: "getting-started",
featured: true,
image: "/blog-astro.jpg"
},
{
title: "Islands Architecture Explained",
description: "Understanding Astro's islands architecture for partial hydration.",
date: "2024-01-10",
slug: "islands-architecture",
featured: false,
image: "/blog-islands.jpg"
},
{
title: "Performance Optimization in Astro",
description: "Tips and tricks for making your Astro site lightning fast.",
date: "2024-01-05",
slug: "performance-optimization",
featured: false,
image: "/blog-performance.jpg"
}
];
const pageTitle = "Blog - My Astro Site";
---
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width" />
<title>{pageTitle}</title>
<link rel="stylesheet" href="/styles/global.css" />
</head>
<body>
<div class="container">
<h1>Latest Blog Posts</h1>
<div class="posts-grid">
{posts.map(post => (
<Card
title={post.title}
description={post.description}
imageUrl={post.image}
featured={post.featured}
>
<div class="post-meta">
<span class="date">{post.date}</span>
<a href={`/blog/${post.slug}`} class="read-more">Read More →</a>
</div>
</Card>
))}
</div>
</div>
</body>
</html>
<style>
.container {
max-width: 1200px;
margin: 0 auto;
padding: 40px 20px;
}
h1 {
text-align: center;
margin-bottom: 3rem;
color: #2d3748;
}
.posts-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
gap: 2rem;
}
.post-meta {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 1rem;
padding-top: 1rem;
border-top: 1px solid #e2e8f0;
}
.date {
color: #718096;
font-size: 0.9rem;
}
.read-more {
color: #667eea;
text-decoration: none;
font-weight: 500;
transition: color 0.2s;
}
.read-more:hover {
color: #5a67d8;
}
</style>
// 5. API endpoint (src/pages/api/posts.json.ts)
---
export async function GET() {
const posts = [
{
id: 1,
title: "Getting Started with Astro",
content: "Learn the basics of Astro and how to build fast websites...",
excerpt: "Learn the basics of Astro and how to build fast websites.",
author: "John Doe",
date: "2024-01-15",
tags: ["astro", "tutorial", "beginner"]
},
{
id: 2,
title: "Islands Architecture Explained",
content: "Understanding Astro's islands architecture for partial hydration...",
excerpt: "Understanding Astro's islands architecture for partial hydration.",
author: "Jane Smith",
date: "2024-01-10",
tags: ["astro", "islands", "performance"]
}
];
return new Response(JSON.stringify(posts), {
headers: {
'Content-Type': 'application/json'
}
});
}
---
// 6. Dynamic route page (src/pages/blog/[slug].astro)
---
export async function getStaticPaths() {
const posts = [
{ slug: "getting-started", title: "Getting Started with Astro" },
{ slug: "islands-architecture", title: "Islands Architecture Explained" },
{ slug: "performance-optimization", title: "Performance Optimization in Astro" }
];
return posts.map(post => ({
params: { slug: post.slug },
props: { title: post.title }
}));
}
const { title } = Astro.props;
const { slug } = Astro.params;
---
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width" />
<title>{title}</title>
<link rel="stylesheet" href="/styles/global.css" />
</head>
<body>
<article class="container">
<header>
<h1>{title}</h1>
<nav>
<a href="/blog">← Back to Blog</a>
</nav>
</header>
<main>
<p>This is the {title} article.</p>
<p>Article slug: {slug}</p>
<!-- In a real application, you would fetch and display the full article content here -->
</main>
<footer>
<p>Published on January {slug.includes('performance') ? '5' : slug.includes('islands') ? '10' : '15'}, 2024</p>
</footer>
</article>
</body>
</html>
<style>
.container {
max-width: 800px;
margin: 0 auto;
padding: 40px 20px;
}
header {
margin-bottom: 2rem;
padding-bottom: 1rem;
border-bottom: 1px solid #e2e8f0;
}
header h1 {
margin: 0 0 1rem 0;
color: #2d3748;
}
header nav a {
color: #667eea;
text-decoration: none;
font-weight: 500;
}
header nav a:hover {
text-decoration: underline;
}
main {
line-height: 1.8;
font-size: 1.1rem;
}
main p {
margin-bottom: 1.5rem;
}
footer {
margin-top: 3rem;
padding-top: 1rem;
border-top: 1px solid #e2e8f0;
color: #718096;
font-size: 0.9rem;
}
</style>
💻 Astro Islands and Framework Integration typescript
🟡 intermediate
⭐⭐⭐⭐
Islands architecture with React, Vue, Svelte components and partial hydration strategies
⏱️ 30 min
🏷️ astro, islands, react, vue, svelte, hydration
Prerequisites:
Astro basics, React/Vue/Svelte fundamentals, Understanding of hydration
// Astro Islands and Framework Integration Examples
// 1. React island component (src/components/ReactCounter.tsx)
import React, { useState, useEffect } from 'react';
export default function ReactCounter({ initialCount = 0 }) {
const [count, setCount] = useState(initialCount);
const [mounted, setMounted] = useState(false);
// Track when component is mounted on client
useEffect(() => {
setMounted(true);
}, []);
const increment = () => setCount(count + 1);
const decrement = () => setCount(count - 1);
const reset = () => setCount(initialCount);
return (
<div className="react-counter">
<h3>React Counter Island</h3>
{!mounted ? (
<p>Loading React component...</p>
) : (
<>
<p>Count: {count}</p>
<div className="counter-controls">
<button onClick={decrement}>-</button>
<button onClick={reset}>Reset</button>
<button onClick={increment}>+</button>
</div>
<p className="hydration-status">
Status: <span className="success">Hydrated ✅</span>
</p>
</>
)}
</div>
);
}
<style jsx>{`
.react-counter {
padding: 20px;
border: 2px solid #61dafb;
border-radius: 8px;
background: #f0f9ff;
margin: 20px 0;
text-align: center;
}
.counter-controls {
display: flex;
gap: 10px;
justify-content: center;
margin: 15px 0;
}
.counter-controls button {
padding: 8px 16px;
border: none;
border-radius: 4px;
background: #61dafb;
color: white;
cursor: pointer;
font-weight: bold;
}
.counter-controls button:hover {
background: #4fa8c5;
}
.hydration-status {
font-size: 0.9rem;
color: #059669;
margin-top: 10px;
}
.success {
color: #059669;
font-weight: bold;
}
`}</style>
// 2. Vue island component (src/components/VueTodo.vue)
<template>
<div class="vue-todo">
<h3>Vue Todo Island</h3>
<div class="todo-input">
<input
v-model="newTodo"
@keyup.enter="addTodo"
placeholder="Add new todo..."
/>
<button @click="addTodo">Add</button>
</div>
<ul class="todo-list">
<li
v-for="todo in todos"
:key="todo.id"
:class="{ completed: todo.completed }"
>
<input
type="checkbox"
v-model="todo.completed"
@change="updateTodo(todo.id)"
/>
<span>{{ todo.text }}</span>
<button @click="removeTodo(todo.id)" class="remove">×</button>
</li>
</ul>
<div class="todo-stats">
<p>Total: {{ todos.length }}</p>
<p>Completed: {{ completedCount }}</p>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue';
interface Todo {
id: number;
text: string;
completed: boolean;
}
const todos = ref<Todo[]>([]);
const newTodo = ref('');
const completedCount = computed(() =>
todos.value.filter(todo => todo.completed).length
);
const addTodo = () => {
if (newTodo.value.trim()) {
todos.value.push({
id: Date.now(),
text: newTodo.value.trim(),
completed: false
});
newTodo.value = '';
}
};
const updateTodo = (id: number) => {
// Vue's reactivity handles this automatically
console.log('Todo updated:', id);
};
const removeTodo = (id: number) => {
todos.value = todos.value.filter(todo => todo.id !== id);
};
onMounted(() => {
console.log('Vue Todo component mounted');
});
</script>
<style scoped>
.vue-todo {
padding: 20px;
border: 2px solid #42b883;
border-radius: 8px;
background: #f0fdf4;
margin: 20px 0;
max-width: 400px;
}
.todo-input {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
.todo-input input {
flex: 1;
padding: 8px 12px;
border: 1px solid #d1d5db;
border-radius: 4px;
}
.todo-input button {
padding: 8px 16px;
border: none;
border-radius: 4px;
background: #42b883;
color: white;
cursor: pointer;
}
.todo-list {
list-style: none;
padding: 0;
margin: 0;
}
.todo-list li {
display: flex;
align-items: center;
gap: 10px;
padding: 8px 0;
border-bottom: 1px solid #d1d5db;
}
.todo-list li.completed span {
text-decoration: line-through;
color: #6b7280;
}
.todo-list .remove {
margin-left: auto;
background: #ef4444;
color: white;
border: none;
border-radius: 50%;
width: 24px;
height: 24px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
}
.todo-stats {
display: flex;
justify-content: space-between;
margin-top: 20px;
padding-top: 20px;
border-top: 1px solid #d1d5db;
font-size: 0.9rem;
color: #6b7280;
}
</style>
// 3. Svelte island component (src/components/SvelteTimer.svelte)
<script lang="ts">
let time = 0;
let isRunning = false;
let interval: NodeJS.Timeout;
function start() {
if (!isRunning) {
isRunning = true;
interval = setInterval(() => {
time += 1;
}, 1000);
}
}
function pause() {
if (isRunning) {
isRunning = false;
clearInterval(interval);
}
}
function reset() {
isRunning = false;
clearInterval(interval);
time = 0;
}
function formatTime(seconds: number): string {
const mins = Math.floor(seconds / 60);
const secs = seconds % 60;
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
}
// Cleanup on component destruction
onDestroy(() => {
clearInterval(interval);
});
</script>
<div class="svelte-timer">
<h3>Svelte Timer Island</h3>
<div class="timer-display">
{formatTime(time)}
</div>
<div class="timer-controls">
<button on:click={start} disabled={isRunning}>
Start
</button>
<button on:click={pause} disabled={!isRunning}>
Pause
</button>
<button on:click={reset}>
Reset
</button>
</div>
<div class="timer-status">
Status: {isRunning ? 'Running 🏃' : 'Stopped ⏸️'}
</div>
</div>
<style>
.svelte-timer {
padding: 20px;
border: 2px solid #ff3e00;
border-radius: 8px;
background: #fff7ed;
margin: 20px 0;
text-align: center;
max-width: 300px;
}
.timer-display {
font-size: 3rem;
font-weight: bold;
color: #ff3e00;
margin: 20px 0;
font-family: monospace;
}
.timer-controls {
display: flex;
gap: 10px;
justify-content: center;
margin: 20px 0;
}
.timer-controls button {
padding: 10px 20px;
border: none;
border-radius: 4px;
background: #ff3e00;
color: white;
cursor: pointer;
font-weight: bold;
transition: opacity 0.2s;
}
.timer-controls button:hover:not(:disabled) {
opacity: 0.8;
}
.timer-controls button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.timer-status {
margin-top: 15px;
color: #ea580c;
font-weight: 500;
}
</style>
// 4. Astro page with islands (src/pages/islands.astro)
---
import ReactCounter from '../components/ReactCounter.tsx';
import VueTodo from '../components/VueTodo.vue';
import SvelteTimer from '../components/SvelteTimer.svelte';
const pageTitle = "Astro Islands Architecture";
const description = "Experience multi-framework components with partial hydration";
---
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width" />
<title>{pageTitle}</title>
<style>
:global(body) {
font-family: system-ui, -apple-system, sans-serif;
line-height: 1.6;
margin: 0;
padding: 40px 20px;
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
min-height: 100vh;
}
.container {
max-width: 1200px;
margin: 0 auto;
}
.header {
text-align: center;
margin-bottom: 3rem;
}
.header h1 {
color: #2d3748;
margin-bottom: 1rem;
}
.header p {
color: #718096;
font-size: 1.1rem;
max-width: 600px;
margin: 0 auto;
}
.islands-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
gap: 2rem;
margin-bottom: 3rem;
}
.island-info {
background: white;
padding: 30px;
border-radius: 12px;
box-shadow: 0 10px 25px rgba(0,0,0,0.1);
}
.island-info h2 {
color: #2d3748;
margin-top: 0;
}
.client-js-info {
background: #fef3c7;
border: 1px solid #f59e0b;
border-radius: 8px;
padding: 20px;
margin-bottom: 2rem;
text-align: center;
}
.client-js-info h3 {
margin-top: 0;
color: #92400e;
}
.performance-stats {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
margin-top: 2rem;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
margin-top: 1rem;
}
.stat-item {
text-align: center;
padding: 1rem;
background: #f8fafc;
border-radius: 6px;
}
.stat-value {
font-size: 1.5rem;
font-weight: bold;
color: #667eea;
}
.stat-label {
font-size: 0.9rem;
color: #718096;
margin-top: 0.5rem;
}
@media (max-width: 768px) {
.islands-grid {
grid-template-columns: 1fr;
}
.stats-grid {
grid-template-columns: 1fr;
}
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>{pageTitle}</h1>
<p>{description}</p>
</div>
<div class="client-js-info">
<h3>🌴 Islands Architecture in Action</h3>
<p>Only the interactive components below are shipped to the browser. Everything else is pre-rendered HTML!</p>
</div>
<div class="islands-grid">
<div class="island-info">
<h2>React Counter Island</h2>
<p>A React component that's hydrated client-side. Only this component's JavaScript is loaded.</p>
<ReactCounter client:load initialCount={10} />
</div>
<div class="island-info">
<h2>Vue Todo Island</h2>
<p>A Vue.js component with local state management. Hydrated independently from other components.</p>
<VueTodo client:visible />
</div>
<div class="island-info">
<h2>Svelte Timer Island</h2>
<p>A Svelte component with reactive state. Compiled to minimal JavaScript for maximum performance.</p>
<SvelteTimer client:idle />
</div>
</div>
<div class="performance-stats">
<h2>Performance Benefits</h2>
<div class="stats-grid">
<div class="stat-item">
<div class="stat-value">0 KB</div>
<div class="stat-label">JS by Default</div>
</div>
<div class="stat-item">
<div class="stat-value">Partial</div>
<div class="stat-label">Hydration</div>
</div>
<div class="stat-item">
<div class="stat-value">Multi</div>
<div class="stat-label">Framework Support</div>
</div>
<div class="stat-item">
<div class="stat-value">Fast</div>
<div class="stat-label">Page Load</div>
</div>
</div>
</div>
</div>
</body>
</html>
// 5. Advanced island with data fetching (src/components/ReactUserList.tsx)
import React, { useState, useEffect } from 'react';
interface User {
id: number;
name: string;
email: string;
company: string;
}
export default function ReactUserList() {
const [users, setUsers] = useState<User[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
fetchUsers();
}, []);
const fetchUsers = async () => {
try {
setLoading(true);
const response = await fetch('/api/users');
if (!response.ok) {
throw new Error('Failed to fetch users');
}
const data = await response.json();
setUsers(data);
} catch (err) {
setError(err instanceof Error ? err.message : 'An error occurred');
} finally {
setLoading(false);
}
};
if (loading) {
return (
<div className="user-list loading">
<h3>React User List Island</h3>
<div className="spinner">Loading users...</div>
</div>
);
}
if (error) {
return (
<div className="user-list error">
<h3>React User List Island</h3>
<div className="error-message">Error: {error}</div>
<button onClick={fetchUsers}>Retry</button>
</div>
);
}
return (
<div className="user-list">
<h3>React User List Island</h3>
<p>Users fetched from API endpoint:</p>
<ul className="users">
{users.map(user => (
<li key={user.id} className="user-item">
<strong>{user.name}</strong>
<span className="email">{user.email}</span>
<span className="company">{user.company}</span>
</li>
))}
</ul>
<button onClick={fetchUsers} className="refresh-btn">
Refresh Users
</button>
</div>
);
}
<style jsx>{`
.user-list {
padding: 20px;
border: 2px solid #61dafb;
border-radius: 8px;
background: #f0f9ff;
margin: 20px 0;
max-width: 500px;
}
.loading, .error {
text-align: center;
}
.spinner {
padding: 20px;
color: #3b82f6;
}
.error-message {
color: #ef4444;
margin: 10px 0;
}
.users {
list-style: none;
padding: 0;
margin: 20px 0;
}
.user-item {
display: flex;
flex-direction: column;
gap: 4px;
padding: 12px;
background: white;
border-radius: 6px;
margin-bottom: 8px;
}
.email {
color: #6b7280;
font-size: 0.9rem;
}
.company {
color: #3b82f6;
font-size: 0.85rem;
font-weight: 500;
}
.refresh-btn {
background: #61dafb;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
font-weight: 500;
}
.refresh-btn:hover {
background: #4fa8c5;
}
.error button {
background: #ef4444;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
margin-top: 10px;
}
`}</style>
// 6. API endpoint for users (src/pages/api/users.json.ts)
---
export async function GET() {
const users = [
{
id: 1,
name: "John Doe",
email: "[email protected]",
company: "Tech Corp"
},
{
id: 2,
name: "Jane Smith",
email: "[email protected]",
company: "Design Studio"
},
{
id: 3,
name: "Bob Johnson",
email: "[email protected]",
company: "Startup Inc"
}
];
// Simulate network delay
await new Promise(resolve => setTimeout(resolve, 1000));
return new Response(JSON.stringify(users), {
headers: {
'Content-Type': 'application/json',
'Cache-Control': 'no-cache'
}
});
}
---
💻 Astro Content Collections and MDX typescript
🔴 complex
⭐⭐⭐⭐
Content management with collections, MDX integration, and dynamic content generation
⏱️ 35 min
🏷️ astro, content, mdx, cms, typescript
Prerequisites:
Astro fundamentals, Markdown/MDX knowledge, TypeScript basics, CMS concepts
// Astro Content Collections and MDX Examples
// 1. Content collection configuration (src/content/config.ts)
import { defineCollection, z } from 'astro:content';
// Define schema for blog posts
const blog = defineCollection({
schema: z.object({
title: z.string(),
description: z.string(),
pubDate: z.coerce.date(),
heroImage: z.string().optional(),
tags: z.array(z.string()).default([]),
author: z.object({
name: z.string(),
email: z.string().email(),
bio: z.string().optional(),
}),
draft: z.boolean().default(false),
featured: z.boolean().default(false),
readingTime: z.number().optional(),
}),
});
// Define schema for projects
const projects = defineCollection({
schema: z.object({
title: z.string(),
description: z.string(),
publishDate: z.coerce.date(),
tech: z.array(z.string()),
liveUrl: z.string().url().optional(),
githubUrl: z.string().url().optional(),
featured: z.boolean().default(false),
category: z.enum(['web', 'mobile', 'desktop', 'other']),
}),
});
export const collections = {
blog,
projects,
};
// 2. Blog post with frontmatter (src/content/blog/getting-started.md)
---
title: "Getting Started with Astro Content Collections"
description: "Learn how to use Astro's Content Collections for type-safe content management."
pubDate: 2024-01-15
heroImage: "/images/blog-content-collections.jpg"
tags: ["astro", "content-collections", "tutorial", "cms"]
author:
name: "Jane Smith"
email: "[email protected]"
bio: "Technical writer and Astro enthusiast"
draft: false
featured: true
readingTime: 5
---
import { Code } from 'astro:components';
# Getting Started with Astro Content Collections
Astro Content Collections provide a **type-safe** way to manage your content files. Let's explore how they work!
## What are Content Collections?
Content Collections are directories of content files that Astro:
- ✅ Validates your frontmatter against a schema
- ✅ Provides TypeScript types for your content
- ✅ Enables performance optimizations
- ✅ Supports various file formats (MD, MDX, JSON)
## Example Frontmatter
The frontmatter of this file is automatically validated:
<Code code={`---
title: "Getting Started with Astro Content Collections"
description: "Learn how to use Astro's Content Collections"
pubDate: 2024-01-15
tags: ["astro", "content-collections"]
---`} />
## Working with Collections
You can query collections in your Astro pages:
\```astro
---
import { getCollection } from 'astro:content';
const posts = await getCollection('blog', ({ data }) => {
return !data.draft && data.pubDate <= new Date();
});
const featuredPosts = posts.filter(post => post.data.featured);
---
<h1>Featured Posts</h1>
{featuredPosts.map(post => <article>{post.data.title}</article>)}
\```
## Benefits
1. **Type Safety**: Catch errors at build time
2. **Performance**: Optimized content loading
3. **Developer Experience**: Autocomplete and validation
4. **Flexibility**: Support for multiple content types
---
*Happy coding with Astro! 🚀*
// 3. Project documentation (src/content/projects/astro-portfolio.md)
---
title: "Astro Portfolio Website"
description: "A blazing-fast portfolio site built with Astro and Tailwind CSS."
publishDate: 2024-01-10
tech: ["Astro", "Tailwind CSS", "TypeScript", "MDX"]
liveUrl: "https://portfolio.example.com"
githubUrl: "https://github.com/user/astro-portfolio"
featured: true
category: "web"
---
# Astro Portfolio Website
A modern, lightning-fast portfolio website showcasing the power of Astro's island architecture.
## Features
- 🚀 **Blazing fast** page loads with 100+ Lighthouse score
- 🎨 Beautiful UI with Tailwind CSS
- 📝 Blog powered by MDX
- 🌴 Islands for interactive components
- 🔍 SEO optimized
## Tech Stack
This project leverages:
\```bash
astro # The main framework
tailwindcss # For styling
@astrojs/mdx # MDX support
@astrojs/react # React integration
typescript # Type safety
\```
## Performance
- First Contentful Paint: **0.8s**
- Largest Contentful Paint: **1.2s**
- Cumulative Layout Shift: **0.02**
- Total Blocking Time: **0ms**
## Live Demo
👉 [View Live Site](https://portfolio.example.com)
## Source Code
👉 [GitHub Repository](https://github.com/user/astro-portfolio)
---
*Built with ❤️ using Astro*
// 4. Dynamic blog listing page (src/pages/blog/index.astro)
---
import { getCollection } from 'astro:content';
import BlogCard from '../../components/BlogCard.astro';
// Get all blog posts, filtering out drafts
const posts = await getCollection('blog', ({ data }) => {
return !data.draft;
});
// Sort by publication date (newest first)
posts.sort((a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf());
// Get unique tags
const allTags = [...new Set(posts.flatMap(post => post.data.tags))];
// Featured posts
const featuredPosts = posts.filter(post => post.data.featured);
const pageTitle = "Blog - My Astro Site";
const pageDescription = "Latest articles about web development, Astro, and modern JavaScript.";
---
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width" />
<title>{pageTitle}</title>
<meta name="description" content={pageDescription} />
<link rel="stylesheet" href="/styles/global.css" />
</head>
<body>
<main class="container">
<header class="page-header">
<h1>Blog</h1>
<p>{pageDescription}</p>
<!-- Featured Posts -->
{featuredPosts.length > 0 && (
<section class="featured-section">
<h2>Featured Posts</h2>
<div class="featured-grid">
{featuredPosts.map(post => (
<BlogCard
post={post}
featured={true}
/>
))}
</div>
</section>
)}
</header>
<!-- Filter Tags -->
<section class="tags-section">
<h3>Filter by Tags</h3>
<div class="tags-cloud">
<a href="/blog" class="tag active">All</a>
{allTags.map(tag => (
<a href={`/blog/tag/${tag}`} class="tag">{tag}</a>
))}
</div>
</section>
<!-- All Posts -->
<section class="posts-section">
<h2>All Posts</h2>
<div class="posts-grid">
{posts.map(post => (
<BlogCard post={post} />
))}
</div>
</section>
</main>
</body>
</html>
<style>
.container {
max-width: 1200px;
margin: 0 auto;
padding: 40px 20px;
}
.page-header {
text-align: center;
margin-bottom: 3rem;
}
.page-header h1 {
font-size: 3rem;
margin-bottom: 1rem;
color: #2d3748;
}
.page-header p {
font-size: 1.2rem;
color: #718096;
max-width: 600px;
margin: 0 auto 2rem;
}
.featured-section {
margin-bottom: 3rem;
}
.featured-section h2 {
text-align: center;
margin-bottom: 2rem;
color: #2d3748;
}
.featured-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
gap: 2rem;
}
.tags-section {
margin-bottom: 3rem;
text-align: center;
}
.tags-section h3 {
margin-bottom: 1rem;
color: #2d3748;
}
.tags-cloud {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
justify-content: center;
}
.tag {
padding: 0.5rem 1rem;
background: #f7fafc;
border: 1px solid #e2e8f0;
border-radius: 9999px;
color: #4a5568;
text-decoration: none;
transition: all 0.2s;
font-size: 0.9rem;
}
.tag:hover,
.tag.active {
background: #667eea;
color: white;
border-color: #667eea;
}
.posts-section h2 {
text-align: center;
margin-bottom: 2rem;
color: #2d3748;
}
.posts-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
gap: 2rem;
}
</style>
// 5. Blog card component (src/components/BlogCard.astro)
---
export interface Props {
post: import('astro:content').CollectionEntry<'blog'>;
featured?: boolean;
}
const { post, featured = false } = Astro.props;
// Format reading time if available
const readingTime = post.data.readingTime
? `${post.data.readingTime} min read`
: '';
// Format publication date
const formatDate = (date: Date) => {
return new Intl.DateTimeFormat('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
}).format(date);
};
---
<article class={`blog-card ${featured ? 'featured' : ''}`}>
{post.data.heroImage && (
<div class="card-image">
<img
src={post.data.heroImage}
alt={post.data.title}
loading="lazy"
/>
{featured && <span class="featured-badge">Featured</span>}
</div>
)}
<div class="card-content">
<div class="card-meta">
<time datetime={post.data.pubDate.toISOString()}>
{formatDate(post.data.pubDate)}
</time>
{readingTime && (
<span class="reading-time">• {readingTime}</span>
)}
</div>
<h2 class="card-title">
<a href={`/blog/${post.slug}`}>{post.data.title}</a>
</h2>
<p class="card-description">{post.data.description}</p>
{post.data.tags.length > 0 && (
<div class="card-tags">
{post.data.tags.map(tag => (
<span class="tag">{tag}</span>
))}
</div>
)}
<div class="card-footer">
<div class="author">
<div class="author-info">
<strong>{post.data.author.name}</strong>
</div>
</div>
<a href={`/blog/${post.slug}`} class="read-more">
Read More →
</a>
</div>
</div>
</article>
<style>
.blog-card {
background: white;
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
overflow: hidden;
transition: transform 0.2s, box-shadow 0.2s;
border: 2px solid transparent;
}
.blog-card:hover {
transform: translateY(-2px);
box-shadow: 0 8px 24px rgba(0,0,0,0.15);
}
.blog-card.featured {
border-color: #667eea;
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.2);
}
.card-image {
position: relative;
height: 200px;
overflow: hidden;
}
.card-image img {
width: 100%;
height: 100%;
object-fit: cover;
}
.featured-badge {
position: absolute;
top: 1rem;
right: 1rem;
background: #667eea;
color: white;
padding: 0.25rem 0.75rem;
border-radius: 9999px;
font-size: 0.8rem;
font-weight: 600;
}
.card-content {
padding: 1.5rem;
}
.card-meta {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 1rem;
color: #718096;
font-size: 0.9rem;
}
.card-title {
margin: 0 0 1rem 0;
line-height: 1.3;
}
.card-title a {
color: #2d3748;
text-decoration: none;
font-size: 1.25rem;
font-weight: 600;
}
.card-title a:hover {
color: #667eea;
}
.card-description {
color: #718096;
line-height: 1.6;
margin-bottom: 1rem;
}
.card-tags {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-bottom: 1.5rem;
}
.tag {
background: #f7fafc;
color: #4a5568;
padding: 0.25rem 0.75rem;
border-radius: 9999px;
font-size: 0.8rem;
font-weight: 500;
}
.card-footer {
display: flex;
justify-content: space-between;
align-items: center;
padding-top: 1rem;
border-top: 1px solid #e2e8f0;
}
.author-info {
font-size: 0.9rem;
color: #4a5568;
}
.read-more {
color: #667eea;
text-decoration: none;
font-weight: 500;
font-size: 0.9rem;
transition: color 0.2s;
}
.read-more:hover {
color: #5a67d8;
}
</style>
// 6. Dynamic blog post page (src/pages/blog/[slug].astro)
---
import { getCollection, getEntry } from 'astro:content';
import { type CollectionEntry } from 'astro:content';
export async function getStaticPaths() {
const posts = await getCollection('blog', ({ data }) => {
return !data.draft;
});
return posts.map((post) => ({
params: { slug: post.slug },
props: { post },
}));
}
const { post } = Astro.props;
// Get related posts (same tags, not current post)
const relatedPosts = await getCollection('blog', ({ data }) => {
return !data.draft &&
data.tags.some(tag => post.data.tags.includes(tag));
});
const filteredRelated = relatedPosts
.filter(relatedPost => relatedPost.slug !== post.slug)
.slice(0, 3);
const pageTitle = post.data.title;
const pageDescription = post.data.description;
---
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width" />
<title>{pageTitle}</title>
<meta name="description" content={pageDescription} />
{/* Open Graph tags */}
<meta property="og:title" content={pageTitle} />
<meta property="og:description" content={pageDescription} />
<meta property="og:type" content="article" />
{/* Twitter Card tags */}
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={pageTitle} />
<meta name="twitter:description" content={pageDescription} />
<link rel="stylesheet" href="/styles/global.css" />
<style>
.container {
max-width: 800px;
margin: 0 auto;
padding: 40px 20px;
}
.post-header {
text-align: center;
margin-bottom: 3rem;
}
.post-header h1 {
font-size: 2.5rem;
line-height: 1.2;
margin-bottom: 1rem;
color: #2d3748;
}
.post-meta {
display: flex;
justify-content: center;
align-items: center;
gap: 1rem;
margin-bottom: 1.5rem;
color: #718096;
font-size: 0.95rem;
}
.author-info {
display: flex;
align-items: center;
gap: 0.5rem;
}
.post-hero {
margin-bottom: 3rem;
}
.post-hero img {
width: 100%;
height: auto;
border-radius: 12px;
box-shadow: 0 10px 30px rgba(0,0,0,0.1);
}
.post-tags {
display: flex;
justify-content: center;
flex-wrap: wrap;
gap: 0.5rem;
margin-bottom: 2rem;
}
.tag {
background: #f7fafc;
color: #4a5568;
padding: 0.5rem 1rem;
border-radius: 9999px;
font-size: 0.9rem;
font-weight: 500;
}
.post-content {
line-height: 1.8;
font-size: 1.1rem;
color: #2d3748;
}
.post-content h2,
.post-content h3 {
margin-top: 2.5rem;
margin-bottom: 1rem;
color: #1a202c;
}
.post-content p {
margin-bottom: 1.5rem;
}
.post-content pre {
background: #f7fafc;
border: 1px solid #e2e8f0;
border-radius: 8px;
padding: 1.5rem;
overflow-x: auto;
margin: 1.5rem 0;
}
.post-content code {
background: #f7fafc;
padding: 0.2rem 0.4rem;
border-radius: 4px;
font-size: 0.9em;
}
.related-posts {
margin-top: 4rem;
padding-top: 2rem;
border-top: 1px solid #e2e8f0;
}
.related-posts h2 {
margin-bottom: 1.5rem;
color: #2d3748;
}
.related-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1.5rem;
}
.related-card {
background: white;
padding: 1.5rem;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
border: 1px solid #e2e8f0;
}
.related-card h3 {
margin: 0 0 0.5rem 0;
font-size: 1.1rem;
}
.related-card h3 a {
color: #2d3748;
text-decoration: none;
}
.related-card h3 a:hover {
color: #667eea;
}
.related-card p {
color: #718096;
font-size: 0.9rem;
margin: 0;
}
.navigation {
margin-top: 3rem;
text-align: center;
}
.navigation a {
color: #667eea;
text-decoration: none;
font-weight: 500;
margin: 0 1rem;
}
.navigation a:hover {
text-decoration: underline;
}
@media (max-width: 768px) {
.post-header h1 {
font-size: 2rem;
}
.post-meta {
flex-direction: column;
gap: 0.5rem;
}
.related-grid {
grid-template-columns: 1fr;
}
}
</style>
</head>
<body>
<div class="container">
<article>
<header class="post-header">
<h1>{post.data.title}</h1>
<div class="post-meta">
<div class="author-info">
<span>By {post.data.author.name}</span>
</div>
<span>•</span>
<time datetime={post.data.pubDate.toISOString()}>
{post.data.pubDate.toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
})}
</time>
{post.data.readingTime && (
<>
<span>•</span>
<span>{post.data.readingTime} min read</span>
</>
)}
</div>
{post.data.tags.length > 0 && (
<div class="post-tags">
{post.data.tags.map(tag => (
<span class="tag">{tag}</span>
))}
</div>
)}
</header>
{post.data.heroImage && (
<div class="post-hero">
<img src={post.data.heroImage} alt={post.data.title} />
</div>
)}
<div class="post-content">
<post.Content />
</div>
{filteredRelated.length > 0 && (
<section class="related-posts">
<h2>Related Posts</h2>
<div class="related-grid">
{filteredRelated.map(relatedPost => (
<div class="related-card">
<h3>
<a href={`/blog/${relatedPost.slug}`}>
{relatedPost.data.title}
</a>
</h3>
<p>{relatedPost.data.description}</p>
</div>
))}
</div>
</section>
)}
<nav class="navigation">
<a href="/blog">← Back to Blog</a>
</nav>
</article>
</div>
</body>
</html>