Skip to main content
โšก Calmops

Astro Complete Guide: Build Faster Websites with Less JavaScript

Introduction

Astro is a web framework designed specifically for building content-driven websites. It pioneered the “islands architecture” concept and ships zero JavaScript to the client by default. This comprehensive guide covers everything you need to build blazing-fast websites with Astro.

Understanding Astro

What is Astro?

Astro is a static-first web framework that renders HTML on the server and only sends JavaScript when absolutely necessary. It’s perfect for blogs, documentation, marketing sites, and e-commerce.

graph TB
    subgraph "Traditional SPA"
        JS1[React App]
        User1[User]
        Load[Initial Load: 150KB+ JS]
        
        User1 --> Load
        Load --> JS1
    end
    
    subgraph "Astro"
        HTML[HTML Only]
        Islands[Interactive Islands]
        User2[User]
        Load2[Initial Load: 0-10KB JS]
        
        User2 --> Load2
        Load2 --> HTML
        HTML -.->|hydrate| Islands
    end

Key Concepts

Concept Description
Zero JS by Default No JavaScript sent to client unless needed
Islands Architecture Interactive components isolated in islands
Server-First HTML generated on server
Framework Agnostic Use React, Vue, Svelte together
Content Collections Type-safe content management

When to Use Astro

Use Case Why Astro?
Blog Static generation, fast loads
Documentation MDX support, great DX
Marketing Site Fast, SEO-friendly
Portfolio Simple, beautiful output
E-commerce Fast product pages

Getting Started

Installation

# Create new Astro project
npm create astro@latest my-website

# Or with specific template
npm create astro@latest my-blog -- --template blog
npm create astro@latest my-docs -- --template docs

# Add to existing project
npx astro add react
npx astro add tailwind
npx astro add svelte

Project Structure

my-astro-project/
โ”œโ”€โ”€ src/
โ”‚   โ”œโ”€โ”€ components/
โ”‚   โ”‚   โ”œโ”€โ”€ Header.astro
โ”‚   โ”‚   โ””โ”€โ”€ Counter.jsx    # React component
โ”‚   โ”œโ”€โ”€ layouts/
โ”‚   โ”‚   โ””โ”€โ”€ Layout.astro
โ”‚   โ”œโ”€โ”€ pages/
โ”‚   โ”‚   โ”œโ”€โ”€ index.astro
โ”‚   โ”‚   โ””โ”€โ”€ blog/
โ”‚   โ”‚       โ””โ”€โ”€ [slug].astro
โ”‚   โ”œโ”€โ”€ content/
โ”‚   โ”‚   โ””โ”€โ”€ blog/
โ”‚   โ”‚       โ”œโ”€โ”€ post-1.md
โ”‚   โ”‚       โ””โ”€โ”€ post-2.md
โ”‚   โ””โ”€โ”€ styles/
โ”‚       โ””โ”€โ”€ global.css
โ”œโ”€โ”€ public/
โ”‚   โ””โ”€โ”€ images/
โ”œโ”€โ”€ astro.config.mjs
โ””โ”€โ”€ package.json

Basic Page

---
// src/pages/index.astro
// This is the "frontmatter" - server-side JavaScript

// Import components
import Layout from '../layouts/Layout.astro';
import Header from '../components/Header.astro';
import Footer from '../components/Footer.astro';

// This runs at build time
const title = "My Awesome Site";
const posts = await fetch('https://api.example.com/posts').then(r => r.json());
---

<!-- This is the template - renders to HTML -->

<Layout title={title}>
  <Header />
  
  <main>
    <h1>Welcome to {title}</h1>
    
    <ul>
      {posts.map(post => (
        <li>
          <a href={`/blog/${post.slug}`}>{post.title}</a>
        </li>
      ))}
    </ul>
  </main>
  
  <Footer />
</Layout>

<style>
  /* Scoped CSS */
  main {
    max-width: 800px;
    margin: 0 auto;
  }
  
  h1 {
    color: blue;
  }
</style>

Components

Astro Components

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

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

<article class="card">
  <h2>
    <a href={href}>{title}</a>
  </h2>
  {description && <p>{description}</p>}
  <a href={href}>Read more โ†’</a>
</article>

<style>
  .card {
    padding: 1rem;
    border: 1px solid #ccc;
    border-radius: 8px;
  }
</style>

Using the Component

---
import Card from '../components/Card.astro';
---

<Card 
  title="My Post"
  description="A great article"
  href="/blog/my-post"
/>

React/Vue/Svelte Components

---
// Import any framework component
import Counter from '../components/Counter.jsx';
import TodoList from '../components/TodoList.svelte';
import Header from '../components/Header.vue';
---

<!-- Static HTML -->
<Header />

<!-- 
  Interactive Island!
  Only this component sends JavaScript to client
-->
<Counter client:visible />

<!-- Load when visible (lazy) -->
<TodoList client:visible />

<!-- Load immediately -->
<Counter client:load />

<!-- Only on mobile -->
<Counter client:only="mobile" />

Client Directives

Directive Description
client:load Load immediately on page load
client:visible Load when element enters viewport
client:idle Load when browser is idle
client:media Load when media query matches
client:only Skip server rendering, client only

Content Collections

Defining Collections

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

const blog = defineCollection({
  type: 'content', // v2.0+ API
  schema: z.object({
    title: z.string(),
    description: z.string(),
    pubDate: z.date(),
    updatedDate: z.date().optional(),
    heroImage: z.string().optional(),
    tags: z.array(z.string()).default([]),
    draft: z.boolean().default(false),
  }),
});

export const collections = { blog };

Creating Content

---
# src/content/blog/first-post.md
title: "My First Blog Post"
description: "Hello World!"
pubDate: 2026-02-22
tags: ["getting-started"]
---

# Hello World

This is my first post in Astro!

Querying Content

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

// Get all blog posts
const posts = await getCollection('blog');

// Filter and sort
const sortedPosts = posts
  .filter(post => !post.data.draft)
  .sort((a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf());
---

<ul>
  {sortedPosts.map(post => (
    <li>
      <a href={`/blog/${post.slug}`}>{post.data.title}</a>
    </li>
  ))}
</ul>

Layouts and Templates

Base Layout

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

const { title, description = "My awesome site" } = Astro.props;
---

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width">
  <meta name="description" content={description}>
  <title>{title}</title>
</head>
<body>
  <slot /> <!-- Page content goes here -->
</body>
</html>

Blog Post Layout

---
// src/layouts/BlogPost.astro
import type { CollectionEntry } from 'astro:content';
import Layout from './Layout.astro';

interface Props {
  post: CollectionEntry<'blog'>;
}

const { post } = Astro.props;
const { title, description, pubDate, heroImage } = post.data;
---

<Layout title={title} description={description}>
  <article>
    {heroImage && <img src={heroImage} alt={title} />}
    
    <h1>{title}</h1>
    <time datetime={pubDate.toISOString()}>
      {pubDate.toLocaleDateString()}
    </time>
    
    <slot /> <!-- Content goes here -->
  </article>
</Layout>

Routing

Static Routes

---
// src/pages/about.astro
// Becomes /about
---

<h1>About Page</h1>

Dynamic Routes

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

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

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

<BlogPost {...post.data}>
  <Content />
</BlogPost>

Route Parameters

// [category]/[page].astro
export function getStaticPaths() {
  return [
    { params: { category: 'tech', page: '1' } },
    { params: { category: 'tech', page: '2' } },
    { params: { category: 'life', page: '1' } },
  ];
}

Data Fetching

Fetch in Frontmatter

---
// Fetch at build time
const response = await fetch('https://api.example.com/data');
const data = await response.json();
---

<ul>
  {data.items.map(item => (
    <li>{item.name}</li>
  ))}
</ul>

API Routes

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

export const GET: APIRoute = async () => {
  const posts = await getPosts(); // Your data fetching
  
  return new Response(JSON.stringify(posts), {
    headers: {
      'Content-Type': 'application/json',
    },
  });
}

Styling

Scoped CSS

<style>
  /* Only applies to this component */
  .highlight {
    background: yellow;
  }
</style>

Global CSS

---
// src/pages/index.astro
---
<style is:global>
  /* Applies to entire site */
  html {
    font-family: system-ui;
  }
</style>

Tailwind CSS

npx astro add tailwind
---
// Use Tailwind classes
---
<button class="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600">
  Click Me
</button>

Integrations

React

npx astro add react
---
// src/components/ReactCounter.jsx
import { useState } from 'react';

export default function Counter() {
  const [count, setCount] = useState(0);
  
  return (
    <button onClick={() => setCount(c => c + 1)}>
      Count: {count}
    </button>
  );
}

Markdown & MDX

npx astro add mdx
---
# src/content/blog/with-mdx.mdx
title: "MDX Blog Post"
---
import Counter from '../../components/Counter.jsx';

# Using Components in Markdown

<Counter client:visible />

Partytown (Analytics)

npx astro add partytown
<!-- Track page views without blocking -->
<script type="text/partytown" src="https://analytics.example.com/script.js" />

Performance Optimization

Image Optimization

npx astro add image
---
import { Image } from 'astro:assets';
import myImage from '../assets/hero.png';
---

<Image 
  src={myImage} 
  alt="Hero image"
  width={800}
  height={600}
  format="webp"
/>

Prefetching

<!-- Add to layout -->
<head>
  <link rel="prefetch" href="/blog/post-2" />
</head>

<!-- Or use Astro's built-in -->
<a href="/blog/post-2" data-astro-prefetch>Post 2</a>

View Transitions

---
// Enable in Astro 3.0+
import { ViewTransitions } from 'astro:transitions';
---

<head>
  <ViewTransitions />
</head>

Deployment

Static (SSG)

# Build to static files
npm run build
# Output: dist/

# Deploy to any static host
# Netlify, Vercel, Cloudflare Pages, etc.

Server-Side Rendering (SSR)

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

export default defineConfig({
  output: 'server',
  adapter: node({
    mode: 'standalone',
  }),
});
# Deploy with Node
node dist/server/entry.mjs

Edge

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

export default defineConfig({
  output: 'server',
  adapter: vercel({
    imageService: true,
  }),
});

Astro vs Next.js

Feature Astro Next.js
JavaScript Zero by default Always present
Islands Native Custom
Routing File-based File-based
Data Fetching Build + Runtime Server Components
React Support Yes Native
MDX Built-in Next MDX
Learning Curve Easy Moderate
Ecosystem Growing Large

Conclusion

Astro is the perfect choice for:

  • Content-focused websites that don’t need complex client-side state
  • Teams that want simplicity over feature complexity
  • Performance-critical applications where every byte matters
  • Developers who prefer HTML over JavaScript

Start with Astro’s default settings, add interactive islands only when needed, and enjoy blazing-fast websites.


External Resources

Comments