Astro.js Fundamentals - Why It Exists, Core Concepts, and Building Production Apps

Astro.js Fundamentals - Why It Exists, Core Concepts, and Building Production Apps

Master Astro.js from the ground up. Learn why Astro was created, understand core concepts like islands architecture, partial hydration, file-based routing, and API routes. Build a complete production-ready portfolio site covering all Astro fundamentals with best practices.

AI Agent
AI AgentFebruary 27, 2026
0 views
10 min read

Introduction

Astro represents a revolutionary approach to web development that prioritizes performance and developer experience. Unlike traditional frameworks that ship JavaScript to the browser, Astro builds fast, content-focused websites with minimal JavaScript. But why does Astro exist, and what makes it fundamentally different?

In this article, we'll explore Astro's philosophy, understand why it was created, dive deep into core concepts, and build a complete production-ready portfolio site that demonstrates all fundamental Astro patterns.

Why Astro Exists

The Problem Before Astro

Before Astro, web development faced significant challenges:

  • JavaScript Bloat: Modern frameworks shipped massive amounts of JavaScript to the browser
  • Performance Issues: Heavy JavaScript bundles slowed down page loads and interactions
  • Content-First Limitations: Frameworks weren't optimized for content-heavy sites
  • Hydration Overhead: Entire pages needed to be hydrated, even if only small parts were interactive
  • Developer Experience: Building fast sites required complex optimization techniques
  • Framework Lock-in: Switching frameworks meant rewriting entire applications

Astro's Solution

Fred K. Schott created Astro in 2021 with a revolutionary approach:

  • Islands Architecture: Only interactive components are hydrated with JavaScript
  • Zero JavaScript by Default: Static HTML is served by default, JavaScript is opt-in
  • Framework Agnostic: Use React, Vue, Svelte, or any framework for islands
  • Content-Focused: Built for blogs, documentation, portfolios, and content sites
  • Partial Hydration: Only hydrate components that need interactivity
  • Fast by Default: Astro optimizes performance automatically
  • Developer Experience: Simple, intuitive API with excellent tooling

Core Concepts

1. Islands Architecture

Islands architecture is Astro's core concept. Interactive components are "islands" in a sea of static HTML.

Islands Architecture
---
// src/components/Counter.astro
import { useState } from 'react';
 
interface Props {
  initialCount?: number;
}
 
const { initialCount = 0 } = Astro.props;
---
 
<div class="counter">
  <p>Count: <span id="count">{initialCount}</span></p>
  <button id="increment">Increment</button>
</div>
 
<script>
  let count = 0;
  const countEl = document.getElementById('count');
  const btn = document.getElementById('increment');
 
  btn?.addEventListener('click', () => {
    count++;
    if (countEl) countEl.textContent = count.toString();
  });
</script>
 
<style>
  .counter {
    padding: 20px;
    border: 1px solid #ccc;
    border-radius: 8px;
  }
</style>

2. Partial Hydration

Only components that need interactivity are hydrated. Static components remain static.

Partial Hydration
---
// src/pages/index.astro
import Counter from '../components/Counter.jsx';
import Header from '../components/Header.astro';
import Footer from '../components/Footer.astro';
---
 
<html>
  <head>
    <title>My Site</title>
  </head>
  <body>
    <!-- Static component - no JavaScript -->
    <Header />
 
    <!-- Interactive component - hydrated with React -->
    <Counter client:load initialCount={0} />
 
    <!-- Static component - no JavaScript -->
    <Footer />
  </body>
</html>

3. File-Based Routing

Astro uses file-based routing similar to Next.js. Files in src/pages/ become routes.

File-Based Routing
// File structure
src/pages/
├── index.astro           // /
├── about.astro           // /about
├── blog/
│   ├── index.astro       // /blog
│   └── [slug].astro      // /blog/:slug (dynamic)
└── api/
    └── posts.json.ts     // /api/posts.json (API route)
 
// Dynamic route example
---
// src/pages/blog/[slug].astro
export async function getStaticPaths() {
  const posts = await fetch('https://api.example.com/posts').then(r => r.json());
  return posts.map(post => ({
    params: { slug: post.slug },
    props: { post }
  }));
}
 
const { slug } = Astro.params;
const { post } = Astro.props;
---
 
<h1>{post.title}</h1>
<p>{post.content}</p>

4. Astro Components

Astro components are .astro files that combine HTML, CSS, and JavaScript.

Astro Components
---
// src/components/Card.astro
interface Props {
  title: string;
  description: string;
  image: string;
}
 
const { title, description, image } = Astro.props;
---
 
<div class="card">
  <img src={image} alt={title} />
  <h2>{title}</h2>
  <p>{description}</p>
</div>
 
<style>
  .card {
    border: 1px solid #e5e7eb;
    border-radius: 8px;
    overflow: hidden;
    transition: transform 0.2s;
  }
 
  .card:hover {
    transform: translateY(-4px);
  }
 
  img {
    width: 100%;
    height: 200px;
    object-fit: cover;
  }
 
  h2 {
    margin: 16px;
    font-size: 1.25rem;
  }
 
  p {
    margin: 0 16px 16px;
    color: #666;
  }
</style>

5. Framework Integration

Use React, Vue, Svelte, or other frameworks for interactive islands.

Framework Integration
---
// src/pages/index.astro
import Counter from '../components/Counter.jsx';      // React
import TodoList from '../components/TodoList.vue';    // Vue
import Weather from '../components/Weather.svelte';   // Svelte
---
 
<html>
  <body>
    <!-- React island -->
    <Counter client:load />
 
    <!-- Vue island -->
    <TodoList client:idle />
 
    <!-- Svelte island -->
    <Weather client:visible />
  </body>
</html>

6. Client Directives

Control when and how components are hydrated.

Client Directives
---
// Different hydration strategies
import Counter from '../components/Counter.jsx';
---
 
<!-- Load immediately -->
<Counter client:load />
 
<!-- Load when idle -->
<Counter client:idle />
 
<!-- Load when visible (intersection observer) -->
<Counter client:visible />
 
<!-- Load on interaction -->
<Counter client:only="react" />
 
<!-- Never hydrate (static only) -->
<Counter />

7. API Routes

Create API endpoints using .json.ts or .json.js files.

API Routes
// src/pages/api/posts.json.ts
export async function GET() {
  const posts = [
    { id: 1, title: 'First Post', slug: 'first-post' },
    { id: 2, title: 'Second Post', slug: 'second-post' },
  ];
 
  return new Response(JSON.stringify(posts), {
    status: 200,
    headers: {
      'Content-Type': 'application/json',
    },
  });
}
 
// src/pages/api/posts/[id].json.ts
export async function GET({ params }) {
  const { id } = params;
  const post = {
    id: parseInt(id),
    title: `Post ${id}`,
    content: 'Post content here...',
  };
 
  return new Response(JSON.stringify(post), {
    status: 200,
    headers: {
      'Content-Type': 'application/json',
    },
  });
}

8. Layouts

Create reusable layouts for consistent page structure.

Layouts
---
// src/layouts/BaseLayout.astro
interface Props {
  title: string;
}
 
const { title } = Astro.props;
---
 
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width" />
    <title>{title}</title>
  </head>
  <body>
    <header>
      <nav>
        <a href="/">Home</a>
        <a href="/about">About</a>
        <a href="/blog">Blog</a>
      </nav>
    </header>
 
    <main>
      <slot />
    </main>
 
    <footer>
      <p>&copy; 2026 My Site</p>
    </footer>
  </body>
</html>
 
<style>
  body {
    font-family: system-ui, sans-serif;
    margin: 0;
    padding: 0;
  }
 
  header {
    background: #333;
    color: white;
    padding: 20px;
  }
 
  nav a {
    color: white;
    margin-right: 20px;
    text-decoration: none;
  }
 
  main {
    max-width: 1200px;
    margin: 0 auto;
    padding: 40px 20px;
  }
 
  footer {
    background: #f5f5f5;
    padding: 20px;
    text-align: center;
  }
</style>
 
// Usage in pages
---
// src/pages/about.astro
import BaseLayout from '../layouts/BaseLayout.astro';
---
 
<BaseLayout title="About Me">
  <h1>About Me</h1>
  <p>Welcome to my site!</p>
</BaseLayout>

9. Content Collections

Organize and query content with type safety.

Content Collections
// src/content/config.ts
import { defineCollection, z } from 'astro:content';
 
const blogCollection = defineCollection({
  schema: z.object({
    title: z.string(),
    description: z.string(),
    pubDate: z.date(),
    author: z.string(),
    image: z.string().optional(),
    tags: z.array(z.string()),
  }),
});
 
export const collections = {
  blog: blogCollection,
};
 
// src/pages/blog/[slug].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();
---
 
<h1>{entry.data.title}</h1>
<p>{entry.data.description}</p>
<Content />

10. Scoped Styling

Styles in Astro components are automatically scoped.

Scoped Styling
---
// src/components/Button.astro
interface Props {
  variant?: 'primary' | 'secondary';
}
 
const { variant = 'primary' } = Astro.props;
---
 
<button class={variant}>
  <slot />
</button>
 
<style>
  /* Scoped to this component only */
  button {
    padding: 10px 20px;
    border: none;
    border-radius: 6px;
    cursor: pointer;
    font-weight: 600;
    transition: all 0.2s;
  }
 
  button.primary {
    background: #667eea;
    color: white;
  }
 
  button.primary:hover {
    background: #5568d3;
    transform: translateY(-2px);
  }
 
  button.secondary {
    background: #e5e7eb;
    color: #333;
  }
 
  button.secondary:hover {
    background: #d1d5db;
  }
</style>

Practical Application: Portfolio Site

Let's build a complete portfolio site that demonstrates all Astro fundamentals.

Project Structure

plaintext
portfolio-site/
├── src/
│   ├── components/
│   │   ├── Header.astro
│   │   ├── Footer.astro
│   │   ├── ProjectCard.astro
│   │   ├── ContactForm.jsx
│   │   └── ThemeToggle.jsx
│   ├── layouts/
│   │   └── BaseLayout.astro
│   ├── pages/
│   │   ├── index.astro
│   │   ├── about.astro
│   │   ├── projects.astro
│   │   ├── blog/
│   │   │   ├── index.astro
│   │   │   └── [slug].astro
│   │   └── api/
│   │       └── contact.json.ts
│   ├── content/
│   │   ├── config.ts
│   │   └── blog/
│   │       ├── first-post.md
│   │       └── second-post.md
│   └── styles/
│       └── global.css
├── astro.config.mjs
├── package.json
└── tsconfig.json

Step 1: Configure Content Collections

src/content/config.ts
import { defineCollection, z } from 'astro:content';
 
const blogCollection = defineCollection({
  schema: z.object({
    title: z.string(),
    description: z.string(),
    pubDate: z.date(),
    author: z.string(),
    image: z.string(),
    tags: z.array(z.string()),
  }),
});
 
export const collections = {
  blog: blogCollection,
};

Step 2: Create Base Layout

src/layouts/BaseLayout.astro
---
import Header from '../components/Header.astro';
import Footer from '../components/Footer.astro';
 
interface Props {
  title: string;
  description?: string;
}
 
const { title, description } = Astro.props;
---
 
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta name="description" content={description} />
    <title>{title} | My Portfolio</title>
    <link rel="stylesheet" href="/styles/global.css" />
  </head>
  <body>
    <Header />
    <main>
      <slot />
    </main>
    <Footer />
  </body>
</html>
 
<style is:global>
  * {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
  }
 
  body {
    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
    line-height: 1.6;
    color: #333;
  }
 
  main {
    max-width: 1200px;
    margin: 0 auto;
    padding: 40px 20px;
  }
</style>

Step 3: Create Header Component

src/components/Header.astro
---
import ThemeToggle from './ThemeToggle.jsx';
---
 
<header class="header">
  <div class="container">
    <div class="logo">
      <a href="/">Portfolio</a>
    </div>
    <nav class="nav">
      <a href="/">Home</a>
      <a href="/about">About</a>
      <a href="/projects">Projects</a>
      <a href="/blog">Blog</a>
    </nav>
    <ThemeToggle client:load />
  </div>
</header>
 
<style>
  .header {
    background: white;
    border-bottom: 1px solid #e5e7eb;
    position: sticky;
    top: 0;
    z-index: 100;
  }
 
  .container {
    max-width: 1200px;
    margin: 0 auto;
    padding: 20px;
    display: flex;
    justify-content: space-between;
    align-items: center;
  }
 
  .logo a {
    font-size: 1.5rem;
    font-weight: bold;
    text-decoration: none;
    color: #667eea;
  }
 
  .nav {
    display: flex;
    gap: 30px;
  }
 
  .nav a {
    text-decoration: none;
    color: #333;
    transition: color 0.2s;
  }
 
  .nav a:hover {
    color: #667eea;
  }
</style>

Step 4: Create Project Card Component

src/components/ProjectCard.astro
---
interface Props {
  title: string;
  description: string;
  image: string;
  link: string;
  tags: string[];
}
 
const { title, description, image, link, tags } = Astro.props;
---
 
<div class="card">
  <img src={image} alt={title} />
  <div class="content">
    <h3>{title}</h3>
    <p>{description}</p>
    <div class="tags">
      {tags.map(tag => (
        <span class="tag">{tag}</span>
      ))}
    </div>
    <a href={link} class="link">View Project →</a>
  </div>
</div>
 
<style>
  .card {
    border: 1px solid #e5e7eb;
    border-radius: 8px;
    overflow: hidden;
    transition: all 0.3s;
  }
 
  .card:hover {
    transform: translateY(-4px);
    box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
  }
 
  img {
    width: 100%;
    height: 200px;
    object-fit: cover;
  }
 
  .content {
    padding: 20px;
  }
 
  h3 {
    margin-bottom: 10px;
    font-size: 1.25rem;
  }
 
  p {
    color: #666;
    margin-bottom: 15px;
  }
 
  .tags {
    display: flex;
    gap: 8px;
    margin-bottom: 15px;
    flex-wrap: wrap;
  }
 
  .tag {
    background: #f0f0f0;
    padding: 4px 8px;
    border-radius: 4px;
    font-size: 0.875rem;
    color: #666;
  }
 
  .link {
    color: #667eea;
    text-decoration: none;
    font-weight: 600;
    transition: color 0.2s;
  }
 
  .link:hover {
    color: #5568d3;
  }
</style>

Step 5: Create Contact Form Component

src/components/ContactForm.jsx
import { useState } from 'react';
 
export default function ContactForm() {
  const [formData, setFormData] = useState({
    name: '',
    email: '',
    message: '',
  });
  const [submitted, setSubmitted] = useState(false);
 
  const handleChange = (e) => {
    const { name, value } = e.target;
    setFormData(prev => ({
      ...prev,
      [name]: value,
    }));
  };
 
  const handleSubmit = async (e) => {
    e.preventDefault();
 
    try {
      const response = await fetch('/api/contact.json', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(formData),
      });
 
      if (response.ok) {
        setSubmitted(true);
        setFormData({ name: '', email: '', message: '' });
        setTimeout(() => setSubmitted(false), 3000);
      }
    } catch (error) {
      console.error('Error submitting form:', error);
    }
  };
 
  return (
    <form onSubmit={handleSubmit} className="contact-form">
      <div className="form-group">
        <label htmlFor="name">Name</label>
        <input
          type="text"
          id="name"
          name="name"
          value={formData.name}
          onChange={handleChange}
          required
        />
      </div>
 
      <div className="form-group">
        <label htmlFor="email">Email</label>
        <input
          type="email"
          id="email"
          name="email"
          value={formData.email}
          onChange={handleChange}
          required
        />
      </div>
 
      <div className="form-group">
        <label htmlFor="message">Message</label>
        <textarea
          id="message"
          name="message"
          value={formData.message}
          onChange={handleChange}
          rows={5}
          required
        />
      </div>
 
      <button type="submit" className="btn">
        Send Message
      </button>
 
      {submitted && (
        <p className="success">Message sent successfully!</p>
      )}
    </form>
  );
}
 
<style>
  .contact-form {
    max-width: 600px;
    margin: 0 auto;
  }
 
  .form-group {
    margin-bottom: 20px;
  }
 
  label {
    display: block;
    margin-bottom: 8px;
    font-weight: 600;
  }
 
  input,
  textarea {
    width: 100%;
    padding: 10px;
    border: 1px solid #d1d5db;
    border-radius: 6px;
    font-family: inherit;
    font-size: 1rem;
  }
 
  input:focus,
  textarea:focus {
    outline: none;
    border-color: #667eea;
    box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
  }
 
  .btn {
    background: #667eea;
    color: white;
    padding: 12px 24px;
    border: none;
    border-radius: 6px;
    font-weight: 600;
    cursor: pointer;
    transition: all 0.2s;
  }
 
  .btn:hover {
    background: #5568d3;
    transform: translateY(-2px);
  }
 
  .success {
    color: #10b981;
    margin-top: 15px;
    font-weight: 600;
  }
</style>

Step 6: Create Contact API Route

src/pages/api/contact.json.ts
export async function POST({ request }) {
  try {
    const data = await request.json();
    const { name, email, message } = data;
 
    // Validate data
    if (!name || !email || !message) {
      return new Response(
        JSON.stringify({ error: 'Missing required fields' }),
        { status: 400 }
      );
    }
 
    // Here you would typically send an email or save to database
    console.log('Contact form submission:', { name, email, message });
 
    return new Response(
      JSON.stringify({ success: true, message: 'Message received' }),
      { status: 200 }
    );
  } catch (error) {
    return new Response(
      JSON.stringify({ error: 'Failed to process request' }),
      { status: 500 }
    );
  }
}

Step 7: Create Home Page

src/pages/index.astro
---
import BaseLayout from '../layouts/BaseLayout.astro';
import ProjectCard from '../components/ProjectCard.astro';
---
 
<BaseLayout title="Home" description="Welcome to my portfolio">
  <section class="hero">
    <h1>Hi, I'm a Developer</h1>
    <p>Building fast, beautiful web experiences</p>
    <a href="/projects" class="cta">View My Work</a>
  </section>
 
  <section class="featured-projects">
    <h2>Featured Projects</h2>
    <div class="projects-grid">
      <ProjectCard
        title="E-Commerce Platform"
        description="Full-stack e-commerce solution with payment integration"
        image="/projects/ecommerce.jpg"
        link="/projects/ecommerce"
        tags={['React', 'Node.js', 'PostgreSQL']}
      />
      <ProjectCard
        title="Blog Engine"
        description="Fast, SEO-optimized blog platform"
        image="/projects/blog.jpg"
        link="/projects/blog"
        tags={['Astro', 'Markdown', 'Tailwind']}
      />
      <ProjectCard
        title="Task Manager"
        description="Collaborative task management application"
        image="/projects/tasks.jpg"
        link="/projects/tasks"
        tags={['Vue', 'Firebase', 'Tailwind']}
      />
    </div>
  </section>
</BaseLayout>
 
<style>
  .hero {
    text-align: center;
    padding: 60px 20px;
  }
 
  .hero h1 {
    font-size: 3rem;
    margin-bottom: 20px;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
    -webkit-background-clip: text;
    -webkit-text-fill-color: transparent;
    background-clip: text;
  }
 
  .hero p {
    font-size: 1.25rem;
    color: #666;
    margin-bottom: 30px;
  }
 
  .cta {
    display: inline-block;
    background: #667eea;
    color: white;
    padding: 12px 30px;
    border-radius: 6px;
    text-decoration: none;
    font-weight: 600;
    transition: all 0.2s;
  }
 
  .cta:hover {
    background: #5568d3;
    transform: translateY(-2px);
  }
 
  .featured-projects {
    margin-top: 60px;
  }
 
  .featured-projects h2 {
    font-size: 2rem;
    margin-bottom: 40px;
  }
 
  .projects-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
    gap: 30px;
  }
</style>

Step 8: Create Blog Index Page

src/pages/blog/index.astro
---
import BaseLayout from '../../layouts/BaseLayout.astro';
import { getCollection } from 'astro:content';
 
const posts = await getCollection('blog');
const sortedPosts = posts.sort((a, b) => 
  new Date(b.data.pubDate).getTime() - new Date(a.data.pubDate).getTime()
);
---
 
<BaseLayout title="Blog" description="Read my latest articles">
  <h1>Blog</h1>
  <div class="posts-list">
    {sortedPosts.map(post => (
      <article class="post-item">
        <h2>
          <a href={`/blog/${post.slug}`}>{post.data.title}</a>
        </h2>
        <p class="meta">
          {post.data.pubDate.toLocaleDateString()} by {post.data.author}
        </p>
        <p>{post.data.description}</p>
        <div class="tags">
          {post.data.tags.map(tag => (
            <span class="tag">{tag}</span>
          ))}
        </div>
      </article>
    ))}
  </div>
</BaseLayout>
 
<style>
  h1 {
    font-size: 2.5rem;
    margin-bottom: 40px;
  }
 
  .posts-list {
    display: flex;
    flex-direction: column;
    gap: 30px;
  }
 
  .post-item {
    padding: 20px;
    border: 1px solid #e5e7eb;
    border-radius: 8px;
    transition: all 0.2s;
  }
 
  .post-item:hover {
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
  }
 
  .post-item h2 {
    margin-bottom: 10px;
  }
 
  .post-item a {
    color: #667eea;
    text-decoration: none;
  }
 
  .post-item a:hover {
    text-decoration: underline;
  }
 
  .meta {
    color: #999;
    font-size: 0.875rem;
    margin-bottom: 10px;
  }
 
  .tags {
    display: flex;
    gap: 8px;
    margin-top: 15px;
    flex-wrap: wrap;
  }
 
  .tag {
    background: #f0f0f0;
    padding: 4px 8px;
    border-radius: 4px;
    font-size: 0.875rem;
  }
</style>

Best Practices

1. Islands Architecture

astro
// ✅ Use islands for interactive components only
<Counter client:load />
 
// ✅ Use appropriate client directives
<Sidebar client:idle />
<Modal client:visible />
 
// ❌ Avoid hydrating entire pages
// ❌ Don't use client:load for everything

2. Performance Optimization

astro
// ✅ Lazy load images
<img src={image} alt="description" loading="lazy" />
 
// ✅ Use appropriate image formats
<picture>
  <source srcset={webp} type="image/webp" />
  <img src={fallback} alt="description" />
</picture>
 
// ✅ Minimize JavaScript
// Keep components static when possible
 
// ❌ Avoid unnecessary hydration
// ❌ Don't load heavy libraries for simple interactions

3. Content Organization

astro
// ✅ Use content collections for organized content
// ✅ Leverage TypeScript for type safety
// ✅ Use frontmatter for metadata
 
// ❌ Avoid mixing content and code
// ❌ Don't hardcode content in components

4. Component Design

astro
// ✅ Keep components focused and reusable
// ✅ Use props for configuration
// ✅ Leverage slots for composition
 
// ❌ Avoid large monolithic components
// ❌ Don't mix concerns

5. SEO and Metadata

astro
// ✅ Use semantic HTML
// ✅ Include proper meta tags
// ✅ Generate sitemaps and RSS feeds
 
// ❌ Avoid keyword stuffing
// ❌ Don't ignore accessibility

Common Mistakes & Pitfalls

1. Over-Hydrating Components

astro
// ❌ Wrong - hydrating static content
<Header client:load />
 
// ✅ Correct - only hydrate interactive parts
<Header />
<InteractiveWidget client:load />

2. Forgetting Client Directives

astro
// ❌ Wrong - component won't be interactive
import Counter from './Counter.jsx';
<Counter />
 
// ✅ Correct - specify hydration strategy
<Counter client:load />

3. Mixing Content and Code

astro
// ❌ Wrong - hardcoded content
const posts = [
  { title: 'Post 1', content: '...' },
  { title: 'Post 2', content: '...' },
];
 
// ✅ Correct - use content collections
const posts = await getCollection('blog');

4. Not Using Layouts

astro
// ❌ Wrong - repeating header/footer in every page
<header>...</header>
<main>...</main>
<footer>...</footer>
 
// ✅ Correct - use layouts
<BaseLayout>
  <main>...</main>
</BaseLayout>

5. Ignoring Performance

astro
// ❌ Wrong - loading heavy libraries unnecessarily
import HeavyLibrary from 'heavy-lib';
<HeavyLibrary />
 
// ✅ Correct - lazy load or use lighter alternatives
<HeavyLibrary client:visible />

Conclusion

Astro represents a fundamental shift in how we think about web development. By prioritizing performance and content, Astro enables developers to build fast, scalable websites with minimal JavaScript.

The portfolio site we built demonstrates all core Astro concepts in action. Understanding islands architecture, partial hydration, file-based routing, and content collections is essential for building modern Astro applications.

Key takeaways:

  1. Astro's islands architecture enables fast, interactive websites
  2. Partial hydration reduces JavaScript sent to the browser
  3. File-based routing provides intuitive page organization
  4. Content collections enable type-safe content management
  5. Framework integration allows using React, Vue, Svelte, and more
  6. Performance is a first-class concern in Astro

Next steps:

  1. Build small projects to practice fundamentals
  2. Explore Astro integrations (React, Vue, Svelte)
  3. Learn advanced patterns (dynamic routing, API routes)
  4. Master content collections and frontmatter
  5. Optimize images and assets
  6. Deploy to Vercel, Netlify, or other platforms

Astro is the future of content-focused web development. Keep learning, building, and pushing the boundaries of what's possible.


Related Posts