Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# dbtlr.com

Personal site for [dbtlr.com](https://dbtlr.com) — a calm, typography-led dark-mode
portfolio and blog. Built as a fully static site with **Astro 6** and **Tailwind 4**,
deployed to **Cloudflare Workers** static assets.

No client framework ships: the only browser JS is a ⌘K search palette and code-copy
buttons (`src/scripts/`). Code blocks are highlighted at build time with Shiki using a
custom theme mapped to the design tokens.

## Requirements

- **Node 24**
- **pnpm** (the package manager — never npm; pinned via `packageManager` in `package.json`)

## Development

```sh
pnpm install
pnpm dev # local dev server with HMR
pnpm build # static build to ./dist
pnpm preview # serve the built ./dist locally
pnpm check # astro check (type checking)
```

## Workflow

Work happens on feature branches and lands through pull requests:

```
feature branch → PR to main → preview deploy → merge to main → production
```

- **PR previews** (`.github/workflows/preview.yml`): every PR against `main` runs
`pnpm check` + `pnpm build`, then `wrangler versions upload` publishes an ephemeral
version of the `dbtlr-com` Worker **without** touching production. The preview URL is
posted (and kept updated) as a comment on the PR.
- **Production** (`.github/workflows/deploy.yml`): pushing to `main` runs the same
checks and `wrangler deploy`, publishing the `dbtlr-com` Worker that serves
[dbtlr.com](https://dbtlr.com).

Both workflows use Node 24 + pnpm and require `CLOUDFLARE_API_TOKEN` and
`CLOUDFLARE_ACCOUNT_ID` repository secrets.

## Checks

Run before pushing — these are exactly what CI runs:

```sh
pnpm check && pnpm build
```

## Project structure

```
astro.config.mjs Astro config — Shiki theme + code-block chrome transformer
wrangler.jsonc Cloudflare Worker (static assets from ./dist)
src/
pages/ routes — home, about, projects, writing, feed.xml, search.json, 404
layouts/Base.astro shared page shell
components/ SectionHead, PostRow
content/ Markdown content collections (projects/ populated; writing/ pending)
content.config.ts collection schemas (zod) — writing + projects
scripts/ vanilla JS — search.js (⌘K palette), copy.js (code copy)
styles/global.css Tailwind 4 theme tokens + component CSS
.github/workflows/ deploy.yml (production), preview.yml (PR previews)
```
4 changes: 2 additions & 2 deletions src/pages/about.astro
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ const elsewhere = [
---
<Base title="About — Drew Butler" description="Drew Butler — a product-focused engineer and engineering leader building open-source tooling for agentic, AI-native development. Based in Brooklyn." current="about">
<div class="col">
<div class="pt-18 max-md:pt-12">
<div class="pt-12">
<h1 class="m-0 mb-4.5 text-[34px] font-semibold tracking-[-0.015em] max-md:text-[28px]">Hi, I’m Drew.</h1>
<div class="body max-w-[64ch]">
<div class="body">
<p>I’m a product-focused engineer and engineering leader. Twenty years in, I’ve never stopped writing code — I think the best technical leaders keep their hands on the work, and it keeps me honest about the trade-offs. Lately I’m focused on agentic engineering: building the open-source tooling and workflows for how software gets made next.</p>
<p>I care about <b>why</b> something is being built, not just how. My best work happens close to the problem — talking to users, learning the workflow, making architectural calls that serve the product rather than the tech. I’ve led distributed teams since before remote was normal, and I’d still rather reach for the right boring tool than the trendy one.</p>
<p>Right now that’s <a href="/projects/">norn, mimir, saga, and skald</a> — a Norse-named family of terminal tools for agentic development. (I’ve named a few products in my time; the habit stuck.) I’m based in Brooklyn.</p>
Expand Down
10 changes: 6 additions & 4 deletions src/pages/index.astro
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const projects = (await getCollection('projects')).sort((a, b) => a.data.order -
<div class="col">
<section class="pt-22 pb-16 max-md:pt-13 max-md:pb-10">
<h1 class="m-0 mb-5 text-[40px] leading-[1.18] font-semibold tracking-[-0.015em] max-md:text-3xl">I build tools for AI-native development.</h1>
<p class="m-0 text-[17px] leading-[1.65] text-text-2 max-w-[56ch] max-md:text-[15.5px]">
<p class="m-0 text-[17px] leading-[1.65] text-text-2 max-md:text-[15.5px]">
I’m Drew Butler — a product-focused engineer and engineering leader, twenty years in and still
shipping code every day. Lately that means open-source tooling for agentic development —
<a class="text-accent-cool border-b border-transparent hover:border-cool-dim" href="/projects/norn/">norn</a>, <a class="text-accent-cool border-b border-transparent hover:border-cool-dim" href="/projects/mimir/">mimir</a>,
Expand All @@ -21,10 +21,12 @@ const projects = (await getCollection('projects')).sort((a, b) => a.data.order -

<SectionHead label="Selected projects" href="/projects/" linkText="All projects →" />
{projects.map((project) => (
<a class="group flex items-baseline gap-3.5 py-4 border-b border-line max-md:flex-wrap max-md:gap-y-1" href={`/projects/${project.id}/`}>
<a class="group grid grid-cols-[96px_1fr] gap-3.5 items-baseline py-4 border-b border-line max-md:grid-cols-1 max-md:gap-y-1" href={`/projects/${project.id}/`}>
<b class="text-base font-semibold transition-colors duration-120 group-hover:text-accent-cool">{project.data.name}</b>
<span class="text-sm text-text-2">{project.data.blurb}</span>
<em class="ml-auto not-italic font-mono text-[11.5px] text-accent-warm shrink-0 max-md:ml-0 max-md:w-full">{project.data.lang}</em>
<span>
<span class="block text-sm text-text-2">{project.data.blurb}</span>
<em class="block not-italic font-mono text-[11.5px] text-accent-warm mt-1.5">{project.data.lang}</em>
</span>
</a>
))}
</div>
Expand Down
8 changes: 4 additions & 4 deletions src/pages/projects/[slug].astro
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,15 @@ const buttons = [
---
<Base title={`${project.data.name} — Drew Butler`} description={project.data.blurb} current="projects">
<div class="col">
<section class="pt-18 max-md:pt-12">
<div class="flex gap-3 items-center mt-14 text-[13px]">
<section class="pt-12">
<div class="flex gap-3 items-center text-[13px]">
<a class="text-accent-cool" href="/projects/">← Projects</a>
</div>
<div class="flex items-baseline gap-3.5 flex-wrap mt-4.5">
<h1 class="m-0 text-[38px] font-[650] tracking-[-0.02em] max-md:text-3xl">{project.data.name}</h1>
<span class="font-mono text-xs text-accent-warm">{meta}</span>
</div>
<p class="m-0 mt-3 text-[17px] leading-[1.6] text-text-2 max-w-[54ch]">{project.data.tagline}</p>
<p class="m-0 mt-3 text-[17px] leading-[1.6] text-text-2">{project.data.tagline}</p>
{buttons.length > 0 && (
<div class="flex gap-3 mt-5.5 flex-wrap">
{buttons.map((b) =>
Expand All @@ -50,7 +50,7 @@ const buttons = [

<dl class="grid grid-cols-3 gap-px bg-line border-y border-line my-10 max-md:grid-cols-1">
{project.data.facts.map((fact) => (
<div class="bg-bg py-4.5 pr-5 max-md:py-3.5 max-md:pr-0">
<div class="bg-bg py-4.5 px-2 max-md:py-3.5 max-md:px-0">
<dt class="text-[11.5px] tracking-[0.08em] uppercase text-text-3 m-0 mb-1.5">{fact.label}</dt>
<dd class:list={['m-0', fact.mono ? 'font-mono text-[13px] text-text' : 'text-[15px] text-text font-medium']}>{fact.value}</dd>
</div>
Expand Down
6 changes: 3 additions & 3 deletions src/pages/projects/index.astro
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@ const projects = (await getCollection('projects')).sort((a, b) => a.data.order -
---
<Base title="Projects — Drew Butler" description="A Norse-named family of terminal tools for agentic development — norn (knowledge), mimir (work), saga (orchestration), and skald, an AI git CLI." current="projects">
<div class="col">
<section class="pt-18 pb-10 max-md:pt-12">
<section class="pt-12 pb-10">
<h1 class="m-0 mb-3.5 text-[34px] font-semibold tracking-[-0.015em] max-md:text-[28px]">Projects</h1>
<p class="m-0 text-base leading-[1.6] text-text-2 max-w-[60ch]">A Norse-named family of terminal tools for agentic development. <b class="text-text font-medium">norn</b> keeps knowledge, <b class="text-text font-medium">mimir</b> holds work, <b class="text-text font-medium">saga</b> weaves them into a session — and <b class="text-text font-medium">skald</b> writes your git prose. Open-source, built in the open.</p>
<p class="m-0 text-base leading-[1.6] text-text-2">A Norse-named family of terminal tools for agentic development. <b class="text-text font-medium">norn</b> keeps knowledge, <b class="text-text font-medium">mimir</b> holds work, <b class="text-text font-medium">saga</b> weaves them into a session — and <b class="text-text font-medium">skald</b> writes your git prose. Open-source, built in the open.</p>
</section>

{projects.map((project) => (
<a class="group grid grid-cols-[180px_1fr] gap-7 items-start py-7 border-b border-line max-md:grid-cols-1 max-md:gap-3.5" href={`/projects/${project.id}/`}>
<div class="h-27.5 max-md:h-35 overflow-hidden rounded-md border border-line-strong bg-bg-sunk px-3.5 py-3">
<div class="h-27.5 max-md:hidden overflow-hidden rounded-md border border-line-strong bg-bg-sunk px-3.5 py-3">
<pre class="m-0 font-mono text-[11px] leading-[1.6] text-text-2 whitespace-pre overflow-hidden">{project.data.demo}</pre>
</div>
<span>
Expand Down