Blog

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:

  1. Global CSS files in public/styles/ — a reset, base typography, CSS custom properties for theming. These load unconditionally via <link> in the layout.
  2. Scoped <style lang="scss"> per component — for anything that needs design tokens (breakpoints, border-radius scale) I import a single _variables.scss file:
@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.