Back to articles
8 min readastro-ssg-aws-s3-cloudfront

Written by Rodrigo de Miguel

Preparing an Astro blog with good judgment (2/5)

(SSG, sitemap, and not overcomplicating things)

How to prepare an Astro blog starting from the official template, making small tweaks to keep it cleaner and more consistent: static HTML, sitemap, and consistent URLs—without unnecessary complexity or infra.

Astro
Static Site Generation SSG
SEO
Core Web Vitals CWV
Web Performance Optimization WPO

Context: we’re not building anything big (and that’s totally fine)

This post is the second in the series, and it’s worth leaving something clear right from the start.

We’re not building:

  • a SaaS
  • a platform
  • or a complex system

We’re starting from the Astro blog template and making a couple of small, conscious tweaks—exactly the same ones I applied to this personal portfolio—to leave it tidy, predictable, and easier to reason about. Plain, SEO-friendly basics.

Nothing fancy.

The idea is simple:

  • know what each piece is doing
  • avoid accepting configs “because that’s how the template comes”
  • and leave the project in a comfortable place to keep shipping without friction

What we’re aiming for in this post

By the end of this article we should have:

  • An Astro blog running
  • Static HTML generated (SSG)
  • Consistent URLs
  • Automatic sitemap
  • Everything tested locally

This is not “final production”.
It’s a solid starting point: clean, boring in the good sense, and with no surprises waiting for you later.


Step 1 — Create the project using the blog template

We’ll move quickly here, because Astro already nails this part and there’s plenty of material out there explaining the basics.

BASH
npm create astro@latest

During the wizard:

  • Choose the blog template
  • TypeScript: yes (recommended)
  • Extra frameworks: not needed for now
  • Install dependencies: yes

At this point you already get:

  • pages
  • Markdown posts
  • basic navigation

All the essentials are there.

Quick sanity check

BASH
npm run dev

Open the browser and check that:

  • the blog loads
  • you can open a post
  • nothing explodes in the console

If everything looks fine, we keep going.


Step 2 — What kind of blog are we building (SSG)

Before touching any config, it’s worth stopping for a moment.

This blog:

  • has no backend
  • has no dynamic data
  • doesn’t personalize content

That puts it squarely in Static Site Generation territory.

SSG simply means:

generate the HTML once and serve it as-is

For a blog or a portfolio, this is usually the most straightforward and sensible path. Fewer moving parts, fewer things to break, fewer headaches later.


Step 3 — Make it explicit that we use SSG

Astro uses SSG by default, but I like to make it explicit. Not because it’s required, but for mental clarity and future-you sanity.

JS
// astro.config.mjs import { defineConfig } from 'astro/config' export default defineConfig({ output: 'static', })

This doesn’t change behavior, but when you come back to this project months later, you instantly know what kind of site you’re dealing with.

Test the build

BASH
npm run build

If you see a dist/ folder full of HTML, you’re on the right track.

Quick note: Make sure dist/ is in your .gitignore. It’s generated content and shouldn’t be committed to the repo.


Step 4 — A small but important detail: URLs

This is one of those choices that feels trivial at first and painful if you ignore it.

I’ve been burned by this in other projects: duplicated URLs, canonical messes, subtle SEO issues that are annoying to clean up later.

Some platforms force you to make this decision (e.g., WordPress). Here, you can choose. Personally, I prefer not to have slashes at the end.

Google prefers slashes. However, the important thing is not preference, but consistency.

For this case we’ll use:

JS
trailingSlash: 'always'

Which means:

  • /contact/ is the final URL
  • it generates /contact/index.html

It’s not “better” than other options. It’s just coherent, and search engines care a lot about that.

Later on, with CloudFront Functions, we’ll make sure users never have to type index.html and everything resolves cleanly from S3. If you skip this step, the site starts to feel oddly retro.

While you’re using the Astro server (or npm run preview), URLs look clean by default. Once you move to S3 + CloudFront, you’re serving static files, so you need to be intentional about how those files map to URLs.

Make sure internal links follow the same pattern (with trailing slash). Astro will complain if they don’t, which is actually helpful.

Full example:

JS
export default defineConfig({ output: 'static', // default value, Static Site Generation trailingSlash: 'always', // Manage trailing slashes in URLs for SEO and serve HTML from CloudFront. We will discuss this later. site: SEO_BASE_URL, // The base URL of your website for sitemap, canonical URLs, internal linking, etc. Should be 'https://yourdomain.com' })

Note: Google doesn’t care if it’s always or never. It cares that you pick one and stick to it. Mixing both is where problems start.


Step 5 — Add a sitemap

Google can crawl your site without a sitemap, but having one is still good hygiene. You’re giving hints about structure, priorities, and change frequency.

Astro keeps this simple with @astrojs/sitemap. The template includes it, but here’s how to wire it manually.

Install

BASH
npm install @astrojs/sitemap

Simple configuration

JS
import sitemap from '@astrojs/sitemap' export default defineConfig({ output: 'static', trailingSlash: 'always', site: SEO_BASE_URL, integrations: [ sitemap({ changefreq: 'weekly', priority: 1, // Google is king, and the truth is that it ignores this. lastmod: new Date(), // Enough to get started i18n: { // If you have multiple languages defaultLocale: 'es', locales: { es: 'es-ES', en: 'en-US', }, }, }), ], })

Is it perfect? No. Is it good enough to start? Yes.

And that’s the point here.


Step 6 — Test everything locally, calmly

Before moving on, run:

BASH
npm run build npm run preview

Check that:

  • navigation works
  • URLs look right
  • /sitemap.xml exists

No need to overthink it. Just confirm nothing weird is happening.


Things we are NOT going to do

For this tutorial we’re skipping:

  • a CMS
  • databases
  • SSR
  • heavy analytics
  • extra plugins “just in case”

Not because those things are wrong, but because they’re not needed here.

Fewer moving parts means:

  • fewer decisions
  • less maintenance
  • less mental noise

Quick checklist before moving forward

  • The blog works with the base template
  • The build generates HTML
  • URLs are consistent
  • The sitemap is generated
  • Everything is tested locally

If all of this checks out, you already have a very solid foundation.


What we actually did

If you zoom out, nothing here is flashy:

  • Use the official template
  • Make SSG explicit
  • Define URL behavior
  • Add a sitemap

But the result is:

  • a cleaner structure
  • easier long-term maintenance
  • a blog that’s more SEO-friendly from day one

Small steps, good judgment.


You don’t need complexity to do things properly.

A handful of deliberate decisions early on save you a lot of friction later.

That’s what this series is about.


We're going to serve it properly.

Posts in this series

1️⃣ Part 1: A blog shouldn’t be a SaaS
👉 Part 2: Preparing an Astro blog with good judgment
3️⃣ Part 3: S3 + CloudFront to serve a fast and cheap static blog
4️⃣ Part 4: Custom domain with Route 53 and CloudFront for an Astro SSG blog
5️⃣ Part 5: Automate Astro SSG blog deployment with GitHub Actions and AWS
🐙 GitHub Repo: demo-astro-ssg-s3-cloudfront

Let's talk?

Looking for someone who understands product as much as code?

Start a conversation

© 2026 Rodrigo de Miguel. All rights reserved.