---
title: "Building the blog with Astro Content Collections and Markdown"
description: "How this blog was built with Astro Content Collections and Markdown. Covers the tech stack and rationale."
canonical: "https://mdlr.stream/en/blog/building-blog-with-astro-content-collections-markdown/"
pubDate: "2026-04-26"
updatedDate: "2026-05-03"
---

I stream ambient music generated in real time on a modular synth as background music for working on [Modular Synth Ambient](https://www.youtube.com/@modular_synth_ambient), a YouTube channel.

Streaming is largely a side effect of generating my own work BGM, but I launched this blog so I can gather favourite archive streams as notes, introduce gear, and more.

For this very first post on the blog, I want to cover something unrelated to modular synths or ambient: the technical stack behind the blog.

## Goals and highlights

- Try [aube](https://aube.en.dev/)
- Dark mode / light mode
- Multiple languages
- Markdown-based content / Content Collections
- No ads[^1]
	- Publish free or at low cost
- Try `anime.js`

## Concrete tech stack

Frameworks and services used (versions reflect those at blog launch):

- **Domain:** [Cloudflare Domains](https://domains.cloudflare.com/)
- **Hosting:** Cloudflare Workers
- **Static site generator (SSG):** [Astro](https://astro.build/) 6.1.9
- **CMS:** Astro Content Collections
- **CSS framework:** [tailwindcss](https://tailwindcss.com/) 4.2.4
- **Package manager:** [aube](https://aube.en.dev/)
- **Database:** Cloudflare KV
- **Icons:** [Phosphor Icons](https://phosphoricons.com/)

Rationale per service is summarized below.

<details>
<summary>Why Cloudflare Domains</summary>

Cloudflare's registrar. Pricing is close to wholesale, so renewal stays very cheap.

Main reasons: hosting on Cloudflare Workers and using a `.stream` domain for about $5/year. With the yen weak, I wanted to keep costs down.
</details>

<details>
<summary>Why Cloudflare Workers</summary>

Again Cloudflare. For a static site it should still be free (as far as I know).

Cloudflare also offers Pages for static sites, but lately they steer static sites toward Workers, so I followed that.

Deployments are built locally and shipped via `wrangler`.
</details>

<details>
<summary>Why Astro / Content Collections</summary>

For content I considered headless CMS options such as [Emdash CMS](https://github.com/emdash-cms/emdash) or [Payload CMS](https://github.com/payloadcms/payload), but I skipped a CMS because I wanted to ship quickly.

In practice:

- A rich WYSIWYG editor isn't necessary when you manage everything solo
- With multilingual needs in the picture, CMS setup felt too heavy

Managing and rendering Markdown with Astro Content Collections looked fastest to wire up; I figured I could launch before I lost interest.
</details>

<details>
<summary>Why tailwindcss</summary>

At first I resisted long `class` strings and repeating the same utilities everywhere — but I'm a fan now.

The “verbose classes” issue doesn't show up as strongly in Astro where styles live per component, and you get the benefit of not bouncing between distant HTML and CSS. Narrow-scope styling is also AI-friendly[^2].

Design tokens make layout and media-query work flow straight from thought to output. Once you internalize tailwind's notation, it feels like it frees mental bandwidth.

</details>

<details>
<summary>Why aube</summary>

I'd been using pnpm / bun a lot lately, but the maintainer of [mise](https://mise.en.dev/) shipped a new package manager called [aube](https://aube.en.dev/), so I'm trying it.

They pitch it as faster than bun and it really is. So far I haven't hit a moment in development where speed bothered me. Adoption was straightforward too.
</details>

<details>
<summary>Why Cloudflare KV</summary>

When showing the latest YouTube videos, fetching the channel RSS from each visitor's browser and rendering HTML dynamically turned out to be awkward because of CORS.

So I run a Worker that pulls YouTube RSS once per day on a cron schedule and stores it in KV. Visitors hit the Worker via a reverse proxy and load from KV to generate HTML dynamically.

Writes and payload size sit comfortably inside the free tier, so adopting KV was an easy choice.
</details>

<details>
<summary>Why Phosphor Icons</summary>

- SVG assets are distributed too
- MIT license
- Consistent design and quality across the set
- Easy to use with [astro-icon](https://github.com/natemoo-re/astro-icon)
</details>


## Astro Content Collections

With Content Collections you can manage content like a flat-file CMS using Markdown files.

### Setup

During Astro setup you can pick a template that includes sample Markdown files:

```sh
# Set up with aube
aube create astro@latest

...

# In the wizard, when choosing a template,
# select `Use blog template`
tmpl   How would you like to start your new project?
 (o)   Use blog template

...

# Install Tailwind with commands
aube install tailwindcss @tailwindcss/vite

# After setup, start dev like this
aube run dev
```

See the official docs [Install Tailwind CSS with Astro](https://tailwindcss.com/docs/installation/framework-guides/astro) as well—you'll need things beyond install, such as placing `global.css`.

### Directory layout

Because I planned multilingual routing with Astro's i18n feature, the layout looks like this. Deployed URLs follow something like `https://example.com/ja/***`.

```ini :directory layout
src/
├── assets/
│   ├── icons/ # Phosphor Icons SVGs
│   └── images/ # Site images
├── components/ # Astro components
├── content/ # Markdown lives here
│   └── blog/
│       ├── en/
│       └── ja/
├── layouts/ # Astro layouts
├── pages/ # Route files
│   ├── en/
│   │   └── blog/
│   └── ja/
│       └── blog/
└── styles/ # Tailwind global.css
```

### Astro config

The i18n portion of `astro.config.mjs` is edited like this:

```js :astro.config.mjs
export default defineConfig({
    ...
    i18n: {
        defaultLocale: 'ja',
        locales: ['ja', 'en'],
        routing: {
			// Include locale in URLs even for the default language.
			// Set to false if you don't want that.
            prefixDefaultLocale: true,
            redirectToDefaultLocale: true,
        },
	},
	...
});
```

### Routing

Below is an example of routing against the default project template. For code that fits your own layout, generating it with an AI agent works fine.

```astro :src/pages/ja/blog/[slug].astro
---
import { type CollectionEntry, getCollection, render } from 'astro:content';
import BlogPost from '/src/layouts/BlogPost.astro';

export async function getStaticPaths() {
	const posts = await getCollection('blog');
	return posts
		.filter((post) => post.id.startsWith('ja/'))
		.map((post) => {
			const [, slug] = post.id.split('/');
			return {
				params: { slug },
				props: post,
			};
		});
}

type Props = CollectionEntry<'blog'>;

const post = Astro.props;
const { Content } = await render(post);
---

<BlogPost {...post.data} slug={Astro.params.slug!}>
	<Content />
</BlogPost>
```


From here, tweak the samples generated at setup and shape the site the way you like.



[^1]: Instead, tips are welcome on [Ko-fi](https://ko-fi.com/manage/mypage).
[^2]: That said, with Content Collections like this you can't apply tailwind utilities directly to parsed Markdown; the narrow-scope styling advantage is weaker.
