Skip to content
Astro projects 4 min read

Project Ideas

The fastest way to internalize Astro is to ship real sites with it. This roadmap walks you from a tiny static blog up to a server-rendered storefront, introducing one major concept per stage: content collections, islands, view transitions, API endpoints, and finally full server-side rendering with an adapter. Each project is small enough to finish in a sitting but deliberately stacks on the previous one so you keep practicing the zero-JS-by-default mindset that makes Astro fast.

How this roadmap works

Astro’s superpower is that it renders to HTML at build time and ships no client JavaScript unless you explicitly opt in with a client:* directive. Every project below is chosen to push on a different part of that model. Start at the top even if it feels easy — the muscle memory of content collections and layouts pays off in every later project.

StageProjectNew concepts you practice
1Personal blogContent collections, Markdown, layouts, dynamic routes
2Portfolio siteIslands, client:* hydration, image optimization
3Docs siteNested collections, search, view transitions
4E-commerce storefrontSSR, API endpoints, adapters, sessions

Tip: Build each project in its own repository. Resisting the urge to cram everything into one codebase keeps the concept you’re learning isolated and easy to revisit.

Stage 1 — Personal blog (static)

This is the canonical first Astro project. You define a content collection, render Markdown to fully static HTML, and generate one route per post. There is zero client JavaScript, so the entire site can be hosted for free on any static host.

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

const blog = defineCollection({
  loader: glob({ pattern: "**/*.md", base: "./src/content/blog" }),
  schema: z.object({
    title: z.string(),
    pubDate: z.coerce.date(),
    draft: z.boolean().default(false),
  }),
});

export const collections = { blog };

Then a dynamic route turns every entry into a page:

---
// src/pages/blog/[...slug].astro
import { getCollection, render } from "astro:content";

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

const { post } = Astro.props;
const { Content } = await render(post);
---
<article>
  <h1>{post.data.title}</h1>
  <time>{post.data.pubDate.toLocaleDateString()}</time>
  <Content />
</article>

Stage 2 — Portfolio site (your first island)

A portfolio is mostly static, which makes it the perfect place to learn islands architecture. Render the page as HTML, then hydrate a single interactive widget — a theme toggle or a contact form — without shipping a framework to the whole page.

---
// src/pages/index.astro
import ThemeToggle from "../components/ThemeToggle.svelte";
import { Image } from "astro:assets";
import avatar from "../assets/avatar.png";
---
<header>
  <Image src={avatar} alt="Portrait" width={96} height={96} />
  <ThemeToggle client:idle />
</header>

The client:idle directive defers hydration until the browser is idle, so the rest of the page paints instantly. Try the directive options to feel the difference:

DirectiveWhen it hydrates
client:loadImmediately on page load
client:idleWhen the main thread is idle
client:visibleWhen the element scrolls into view
client:mediaWhen a CSS media query matches

Stage 3 — Docs site (structure and navigation)

A documentation site teaches you to model nested content and smooth navigation. Group pages into sections, build a sidebar from the collection, and add view transitions so moving between pages feels like an SPA while staying server-rendered.

---
// src/layouts/Docs.astro
import { ClientRouter } from "astro:transitions";
const { frontmatter } = Astro.props;
---
<html lang="en">
  <head>
    <title>{frontmatter.title}</title>
    <ClientRouter />
  </head>
  <body><slot /></body>
</html>

Adding <ClientRouter /> enables cross-page view transitions with no extra JavaScript framework. Pair it with astro add integrations like @astrojs/sitemap to round the site out.

npx astro add sitemap mdx

Output:

✔ Resolving packages...
  Astro will run the following command:
    npm install @astrojs/sitemap @astrojs/mdx
✔ Continue? … yes
✔ Successfully updated astro.config.mjs

Stage 4 — E-commerce storefront (SSR)

The capstone moves from static output to server-side rendering. You add an adapter, switch routes to prerender = false, and write API endpoints that respond to form posts — the real backend work that static sites can’t do.

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

export default defineConfig({
  output: "server",
  adapter: node({ mode: "standalone" }),
});
// src/pages/api/cart.ts
import type { APIRoute } from "astro";

export const POST: APIRoute = async ({ request }) => {
  const data = await request.formData();
  const productId = data.get("productId");
  if (!productId) {
    return new Response("Missing productId", { status: 400 });
  }
  return Response.json({ added: productId }, { status: 201 });
};

Warning: Once a route is server-rendered, it runs on every request. Keep prerender = true on pages that don’t need live data so you still get static-speed responses where it counts.

Best Practices

  • Build each stage as a standalone repo so the concept you are learning stays isolated.
  • Default to zero JavaScript; only reach for a client:* directive when interactivity is genuinely required.
  • Validate all collection frontmatter with a Zod schema so broken content fails the build, not production.
  • Prefer client:visible or client:idle over client:load to protect your time-to-interactive.
  • Keep pages static (prerender = true) unless they truly need per-request data.
  • Add integrations with astro add rather than editing config by hand — it wires up dependencies and config together.
  • Deploy early and often; shipping each project teaches you the adapter and hosting story for real.
Last updated June 14, 2026
Was this helpful?