Framework

Astro Setup

# Astro 5.x Project Setup Playbook

Production-tested patterns from the ActiveWizards site build (147 pages, 115 blog posts, 12 case studies, 5 service pages). Every code example is extracted from a running production site.


Table of Contents

  • Project Initialization
  • Tailwind v4 Integration
  • Content Collections
  • Layout Hierarchy
  • Core Components
  • Global CSS with @theme Tokens
  • Dynamic Routes
  • Pagination
  • Astro Config
  • TypeScript Config
  • Project Structure

  • 1. Project Initialization

    ``bash

    npm create astro@latest my-site

    `

    Recommended options:

    • Template: Empty (start clean, not a starter)
    • TypeScript: Yes, strict
    • Install dependencies: Yes

    After scaffolding:

    `bash

    cd my-site

    npm install tailwindcss @tailwindcss/vite @astrojs/sitemap

    npm install astro-robots-txt astro-compress astro-expressive-code

    npm install astro-icon astro-pagefind

    npm install rehype-slug rehype-autolink-headings rehype-external-links

    npm install remark-reading-time mdast-util-to-string

    npm install medium-zoom

    `

    Production package.json dependencies (proven stable):

    `json

    {

    "name": "site-aw",

    "type": "module",

    "version": "0.0.1",

    "scripts": {

    "dev": "astro dev",

    "build": "astro build",

    "preview": "astro preview",

    "astro": "astro"

    },

    "dependencies": {

    "@astrojs/check": "^0.9.6",

    "@astrojs/rss": "^4.0.15",

    "@astrojs/sitemap": "^3.7.0",

    "@tailwindcss/vite": "^4.2.1",

    "astro": "^5.17.1",

    "astro-compress": "^2.3.9",

    "astro-expressive-code": "^0.41.7",

    "astro-icon": "^1.1.5",

    "astro-pagefind": "^1.8.5",

    "astro-robots-txt": "^1.0.0",

    "mdast-util-to-string": "^4.0.0",

    "medium-zoom": "^1.1.0",

    "rehype-autolink-headings": "^7.1.0",

    "rehype-external-links": "^3.0.0",

    "rehype-slug": "^6.0.0",

    "remark-reading-time": "^2.0.2",

    "tailwindcss": "^4.2.1"

    }

    }

    `


    2. Tailwind v4 Integration

    Tailwind v4 uses Vite plugin instead of PostCSS. No tailwind.config.js needed — tokens go in CSS @theme blocks.

    Do NOT use @astrojs/tailwind. Use the Vite plugin directly:

    `bash

    npm install tailwindcss @tailwindcss/vite

    `

    In astro.config.mjs:

    `js

    import tailwindcss from '@tailwindcss/vite';

    export default defineConfig({

    vite: {

    plugins: [tailwindcss()],

    },

    });

    `

    In src/styles/global.css:

    `css

    @import "tailwindcss";

    @theme {

    --color-bg-primary: #0A0A0F;

    --color-text-primary: #EDEDED;

    --color-accent: #00D4AA;

    --font-sans: 'Geist Sans', system-ui, sans-serif;

    --font-mono: 'Geist Mono', monospace;

    }

    `

    Import the CSS in your base layout:

    `astro


    import '../styles/global.css';


    `

    Key difference from Tailwind v3: No tailwind.config.js. All tokens live in CSS @theme blocks. Utility classes reference tokens via var() — e.g., bg-[var(--color-bg-primary)].

    3. Content Collections

    Astro 5.x uses src/content.config.ts (not src/content/config.ts from v4).

    src/content.config.ts

    `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(),

    description: z.string(),

    publishedAt: z.coerce.date(),

    updatedAt: z.coerce.date().optional(),

    tags: z.array(z.string()).default([]),

    // GEO fields (for Generative Engine Optimization)

    problem: z.string().optional(),

    technology: z.string().optional(),

    technologyVersion: z.string().optional(),

    persona: z.string().optional(),

    // SEO overrides

    metaTitle: z.string().optional(),

    metaDescription: z.string().optional(),

    ogImage: z.string().optional(),

    // Content

    tldr: z.string().optional(),

    relatedPosts: z.array(z.string()).default([]),

    readingTime: z.number().optional(),

    }),

    });

    const cases = defineCollection({

    loader: glob({ pattern: '**/*.md', base: './src/content/cases' }),

    schema: z.object({

    title: z.string(),

    description: z.string(),

    publishedAt: z.coerce.date(),

    client: z.string().optional(),

    industry: z.string().optional(),

    technologies: z.array(z.string()).default([]),

    metrics: z.array(z.object({

    label: z.string(),

    value: z.string(),

    })).default([]),

    image: z.string().optional(),

    metaTitle: z.string().optional(),

    metaDescription: z.string().optional(),

    }),

    });

    const services = defineCollection({

    loader: glob({ pattern: '**/*.md', base: './src/content/services' }),

    schema: z.object({

    title: z.string(),

    description: z.string(),

    icon: z.string().optional(),

    order: z.number().default(0),

    technologies: z.array(z.string()).default([]),

    caseStudies: z.array(z.string()).default([]),

    metaTitle: z.string().optional(),

    metaDescription: z.string().optional(),

    }),

    });

    export const collections = { blog, cases, services };

    `

    Content file structure

    `

    src/content/

    blog/

    my-first-post.md

    another-post.md

    cases/

    project-alpha.md

    services/

    ai-agent-engineering.md

    `

    Frontmatter example (blog)

    `yaml


    title: "Building Production RAG Pipelines with LangChain"

    description: "A complete guide to deploying retrieval-augmented generation in production."

    publishedAt: 2025-11-15

    updatedAt: 2026-01-20

    tags: ["RAG", "LangChain", "Vector DB"]

    problem: "How to build a production RAG pipeline"

    technology: "LangChain"

    technologyVersion: "0.3.x"

    tldr: "

      • Use chunking with overlap
      • Embed with ada-002
    "


    Article body in Markdown...

    `

    Key patterns

    • Use z.coerce.date() for dates — handles both 2025-11-15 and "2025-11-15T00:00:00Z" strings
    • Use .default([]) for array fields to avoid undefined checks everywhere
    • Use .optional() for fields that not every post will have
    • The glob loader in v5 replaces the implicit directory-based loader from v4

    4. Layout Hierarchy

    `

    BaseLayout (HTML shell, SEO, fonts, Nav, Footer)

    └── BlogLayout (article header, TL;DR, article body, CTA, related posts)

    └── CaseStudyLayout (metrics bar, hero image, prose body)

    └── [page].astro (direct use for index pages, about, contact)

    `

    BaseLayout — src/layouts/BaseLayout.astro

    The HTML shell. Every page uses this.

    `astro


    import '../styles/global.css';

    import Nav from '../components/Nav.astro';

    import Footer from '../components/Footer.astro';

    import SEO from '../components/SEO.astro';

    export interface Props {

    title: string;

    description: string;

    canonical?: string;

    ogImage?: string;

    ogType?: 'website' | 'article';

    publishedAt?: Date;

    updatedAt?: Date;

    jsonLd?: Record;

    }

    const props = Astro.props;


    Skip to content