~/rbh
All posts

Shipping This Site with Astro and shadcn/ui

A short tour of the stack behind this site: Astro content collections, MDX, Tailwind v4, and shadcn components on a dark, mono, terminal-flavored theme.

1 min read #astro #shadcn #tailwind

This site is intentionally boring infrastructure. Two pages, one content collection, zero client-side JavaScript on most routes. Here’s what the pieces look like.

Content collections

Posts live as MDX files in src/content/blog, and Astro’s content layer turns the folder into a typed collection:

import { defineCollection, z } from "astro:content"
import { glob } from "astro/loaders"

const blog = defineCollection({
  loader: glob({ pattern: "**/*.{md,mdx}", base: "./src/content/blog" }),
  schema: z.object({
    title: z.string(),
    description: z.string(),
    pubDate: z.coerce.date(),
    tags: z.array(z.string()).default([]),
    draft: z.boolean().default(false),
  }),
})

export const collections = { blog }

The Zod schema means typos in frontmatter fail the build instead of quietly rendering an empty page. That trade — a little friction now for no surprises later — is worth it even for a two-page site.

Tailwind v4, CSS-first

There’s no tailwind.config.js. The theme lives in CSS: design tokens are plain custom properties in global.css, exposed to Tailwind through an @theme inline block. The whole “brand” is JetBrains Mono, a near-black green-tinted background, and one terminal-green accent.

.dark {
  --background: oklch(0.14 0.005 160);
  --primary: oklch(0.77 0.17 155);
}

shadcn/ui, the Astro way

shadcn components are source code you own, not a dependency you fight. The React versions render fine inside .astro files — they become static HTML at build time and ship no JavaScript. The rest of the “design system” is a handful of hand-rolled utilities: a fading blueprint grid, a text glow, an animated nav underline. All CSS.

The result

The whole site builds to static files, so it deploys anywhere — this one is set up for Cloudflare’s static asset hosting via Wrangler. No servers, no databases, no runtime to patch.

The lesson I keep relearning: constraints make side projects finish. Two pages means two pages.