
Building bdbch.com: How I built my new site in a day
How I rebuilt my personal website using Astro, Motion, some AI agents, and an embarrassingly small token bill.
I had a website before this one. It was fine. A static HTML page with some text, a few links, the absolute minimum. It worked, but it didn’t feel like me. It was flat and kind of boring. I wanted a place that actually represents what I do. A place to show my projects, the tools I use and love, and somewhere to write down my thoughts for whoever wants to read them.
So I built this.
Tech stack
I’ve been a fan of Astro for static sites for a while now. I think it’s one of the cleverest SSG solutions out there. The islands architecture, the zero-JS-by-default approach, the way it lets you bring your own framework. It just clicks for me.
For this site, I paired it with Tailwind CSS using a custom theme built on content-semantic CSS variables, Motion (the new standalone library from the Framer Motion team) for animations, and MDX for content.
Content collections and Zod
The part I enjoyed most was Astro’s content collection system. Defining a schema with Zod gives you typed, validated content out of the box. No manual type assertions, no runtime surprises.
const blog = defineCollection({
loader: glob({ pattern: '**/*.mdx', base: './src/content/blog' }),
schema: z.object({
title: z.string(),
excerpt: z.string(),
category: z.string(),
tags: z.array(z.string()),
published: z.string(),
updated: z.string().optional(),
cover: z.string().optional(),
}),
});
Building a paginated blog page with category filters was almost trivial. Astro’s getStaticPaths makes pagination straightforward, and having typed collections means I get full autocompletion when accessing post data. The helpers for sorting and grouping stay short:
export function sortPostsByDate(posts: BlogPost[]): BlogPost[] {
return posts.sort(
(a, b) => new Date(b.data.published).getTime() - new Date(a.data.published).getTime(),
);
}
export function getCategories(posts: BlogPost[]): string[] {
const categories = new Set(posts.map((post) => post.data.category));
return Array.from(categories).sort();
}
Design
I wanted something minimal that still had some personality. I’m a big fan of dark sites with good typography. I’m not a designer by trade, but I think I nailed the vibe I was going for.
I originally planned a purple/indigo accent palette, but the colors felt weak in practice. They didn’t pop enough against the dark background. So I switched to a gold accent. That fixed it. Warm enough to notice, not loud enough to fight the text.
I’m using content-semantic CSS variables for the theme. Instead of naming colors --blue-500, everything is --accent, --muted-foreground, --surface. It makes theming consistent and swapping values painless:
--accent: oklch(0.80 0.18 90);
--accent-hover: oklch(0.70 0.20 90);
--accent-foreground: oklch(0.05 0.005 285.823);
A few gradients keep it from looking flat, and Motion handles the entrance animations. Nothing fancy:
import { animate } from "motion";
animate(
content,
{ opacity: [0, 1], y: [24, 0] },
{ duration: 0.7, ease: "easeOut" }
);
It’s not perfect. There’s plenty I’d still improve. If you have feedback, hit me up on Bluesky. I’m always open to hearing what works and what doesn’t.
The AI part
I wanted to test out Opencode Go, the $10/month subscription (I got it for $5 for the first month). It gives you access to a bunch of pretty cheap models. I’ll write a more detailed post about Opencode later, but for this project, the setup was dead simple.
I started by defining the whole project in my AGENTS.md: the stack, the conventions, the goals. Then I used a custom agent I call “Peer Programming” for incremental, back-and-forth coding. I definitely prefer this over just vibecoding everything with no control. It let me steer the project at every step and review changes as they came in.
Every now and then I’d pull in a few subagents, a design reviewer, a code reviewer, and an SEO optimizer, and gave them browser access via the Chrome MCP tools. Each of them would constantly check for inconsistencies, potential refactors, or SEO gaps. The results were honestly crazy good.
I used Kimi K2.6 for planning and DeepSeek V4 Flash on max settings for the code. The whole thing took about a day and cost me around $2 to $3 in tokens. A few bucks for an entire site.
Right now the site scores 100 on Lighthouse, mobile and desktop.
This isn’t sponsored or anything. I’m just genuinely impressed with the workflow.
My AI coding stack
If you’re wondering what tools I actually used:
- Opencode Go ($5/mo intro) runs everything. This is where I define my custom agents and route tasks.
- Kimi K2.6 for planning, architecture decisions, and content outlines. Cheap, fast, and better at reasoning than I expected.
- DeepSeek V4 Flash (max) for almost all code generation. At max settings the output needed few corrections.
- Claude for polishing copy and content. I prefer it for writing tasks.
- GitHub Copilot for inline completions during manual edits. Not part of the agentic workflow, but still in my daily setup.
Outro
This was a really fun project. The only caveat I’d add: I don’t use Opencode Go models on private or client projects because I’m not entirely sure how they handle code sharing with third-party providers. For personal stuff? Absolutely worth it.
If you’re curious about my exact Opencode setup, you can find my config and agents over on github.com/bdbch/ai-dotfiles.
I had fun building this, and I’m happy I finally have a website that actually looks and feels like me.