How to start an Astro project — the way I actually do it
My opinionated setup for new Astro projects: folder structure, CSS, one layout, and Content Collections — without over-engineering it.
Every time I start a new Astro project I end up making the same decisions. This is my reference for those decisions — not the official quickstart, but the setup I actually reach for when I want something structured, typed, and content-ready without over-engineering it.
Scaffolding
npm create astro@latest
At the prompts I pick:
- Empty template — I don’t want to spend time removing things I didn’t ask for
- TypeScript: strict — always
- No to Git init if I’m already inside a repo
Folder structure
I stick close to Astro’s defaults with a few deliberate additions:
src/
components/
structure/ ← Nav, Footer — things that frame every page
content/
blog/ ← Markdown posts
data/ ← typed arrays (projects, experience…)
layouts/
MainLayout.astro
pages/
styles/
base/
_variables.scss
types/ ← shared TypeScript interfaces
The structure/ subfolder inside components/ is a small call that pays off quickly — layout-level pieces (Nav, Footer, Header) stay separate from the components you drop into page content. Everything else stays flat until there’s a real reason to nest it.
CSS approach
I use two layers:
- Global CSS files in
public/styles/— a reset, base typography, CSS custom properties for theming. These load unconditionally via<link>in the layout. - Scoped
<style lang="scss">per component — for anything that needs design tokens (breakpoints, border-radius scale) I import a single_variables.scssfile:
@use '../styles/base/_variables.scss' as *;
The variables file holds breakpoints, a border-radius scale, and a handful of shared values. That’s it — no full SCSS architecture, just one file that keeps magic numbers out of components.
One layout to start
MainLayout.astro wraps every page. It handles the <head>, renders Nav and Footer, and slots the page content in between:
---
import Nav from '../components/structure/Nav.astro'
import Footer from '../components/structure/Footer.astro'
interface Props {
title?: string
description?: string
}
const {
title = 'My site',
description = 'A short description',
} = Astro.props
---
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>{title}</title>
<meta name="description" content={description} />
</head>
<body>
<Nav />
<main>
<slot />
</main>
<Footer />
</body>
</html>
One layout covers most projects. You can always add a BlogLayout.astro or ProjectLayout.astro later if a section genuinely needs different chrome — but don’t create them until you need them.
Content Collections
Even if I’m not sure I’ll use a blog, I add the collection setup early. The cost is low and retrofitting it later is annoying.
// 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(),
description: z.string(),
date: z.date(),
tags: z.array(z.string()).default([]),
draft: z.boolean().default(false),
}),
})
export const collections = { blog }
Filtering drafts and sorting by date in the blog index:
const posts = (await getCollection('blog'))
.filter(p => !p.data.draft)
.sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf())
Add draft: true to a post’s frontmatter to keep it out of the index without deleting it.
Data files for structured content
For anything that isn’t prose — projects, work experience, links — I use a typed array in src/data/:
// src/data/projects.ts
import type { Project } from '../types/Project'
export const projects: Project[] = [
{
title: 'My thing',
description: 'What it does.',
tags: ['TypeScript', 'Astro'],
url: 'https://...',
},
]
This keeps content out of components and makes it easy to loop, filter, or slice in any page without duplicating data.
Icons (nice to have)
If the project needs icons, I reach for astro-icon backed by Iconify. It’s not an MVP requirement, but when you do need it the setup is minimal and it covers a huge range of icon sets with a consistent API. Worth knowing about for when the time comes.
That’s the foundation
No router config, no state management, no build plugins. From npm create to a typed, content-ready site is one afternoon — and these patterns hold up as the project grows.