MDLR.STREAM

Building the blog with Astro Content Collections and Markdown

Last updated on

I stream ambient music generated in real time on a modular synth as background music for working on 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
  • Dark mode / light mode
  • Multiple languages
  • Markdown-based content / Content Collections
  • No ads1
    • Publish free or at low cost
  • Try anime.js

Concrete tech stack

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

Rationale per service is summarized below.

Why Cloudflare Domains

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.

Why Cloudflare Workers

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.

Why Astro / Content Collections

For content I considered headless CMS options such as Emdash CMS or Payload CMS, 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.

Why tailwindcss

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-friendly2.

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.

Why aube

I’d been using pnpm / bun a lot lately, but the maintainer of mise shipped a new package manager called aube, 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.

Why Cloudflare KV

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.

Why Phosphor Icons
  • SVG assets are distributed too
  • MIT license
  • Consistent design and quality across the set
  • Easy to use with astro-icon

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:

# 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 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/***.

directory
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:

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.

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.

Notes

  1. Instead, tips are welcome on Ko-fi.

  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.