Skip to content
Draft
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
33 changes: 33 additions & 0 deletions PRODUCT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Product

## Register

brand

## Users

Peer engineers evaluating Kaylee's craft or c15t, recruiters sizing up her scope, founders working in privacy and consent, and AI agents summarizing her work. Visitors skim quickly and need to understand what she builds, see proof of the work, and remember the site as distinctly hers.

## Product Purpose

Kaylee's personal site explains and demonstrates her work as a founding engineer at Inth building the open-source compliance stack: c15t for consent, DSAR for data rights, Cookiebench for performance, and Leadtype for agent-readable docs. Each surface should reinforce the thesis that compliance belongs in code.

## Brand Personality

Precise, playful, and design-minded. The tone is clean, warm, and self-assured. Personality comes through specific earned details rather than labels or hype.

## Anti-references

Generic AI-generated portfolio templates, clones of burnedchris.com, try-hard quirk, and corporate or sterile agency styling. Avoid static metrics in prose, self-mythologizing career narration, disconnected project lists, and decorative personality labels.

## Design Principles

1. Show, do not announce. Use real artifacts and earned details as proof.
2. Practice what you preach. The site itself should demonstrate privacy, accessibility, and performance.
3. Let the work carry the weight. Prefer live evidence and shipped work to adjectives.
4. Connect every project to the same causal thesis: compliance belongs in code.
5. Use whimsy sparingly and execute it well.

## Accessibility & Inclusion

Meet WCAG 2.1 AA. Keep the purple accent legible in both themes, respect color-scheme and reduced-motion preferences, provide keyboard navigation and visible focus states, and make controls comfortable on touch screens.
61 changes: 61 additions & 0 deletions content/blog/code-doesnt-have-to-ship.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
---
title: "Code doesn't have to ship"
description: "Coding agents make more engineering questions cheap enough to test."
publishedAt: 2026-07-12
tags:
- AI
- engineering
- experiments
draft: false
---
In April, a conversation with [https://x.com/tannerlinsley](https://x.com/tannerlinsley) at [AI Engineer Miami](https://www.ai.engineer/miami) and [React Miami](https://www.reactmiami.com/) left me with a question: what if we used coding models to test new shapes for a product, not just clear its existing backlog?

I had somewhere to test it. For c15t v3, I was working on the server-rendered path for consent experiences.

Client-side c15t has a straightforward request path:

```text
Client → c15t backend → Client
```

Server-side rendering lets c15t use the framework to prepare the geo- and locale-specific experience before the page reaches the browser. In Next.js, that path looked like this:

```text
Client → Next.js server → c15t backend → Next.js server → Client
```

This moved the work server-side, but the Next.js server still had to call the c15t backend before it could respond. That put the latency between those servers on the critical path.

I wanted to know whether the manifest system I built for scripts in v2 could also describe the data needed to render a consent experience. My hypothesis was that we could cache that manifest at the CDN edge, then let the Next.js server combine it with the request's geography and locale to produce the response itself:

```text
Client → Next.js server → Client
```

That would keep the benefits of server-side rendering while removing the trip to the c15t backend. The question was whether the performance improvement would justify moving more logic into the manifest system.

Proving it by hand would have meant giving an uncertain direction real roadmap time. That is usually where an experiment loses to work with a clearer payoff.

Instead, I wrote down the hypothesis, the rough implementation, and the benchmark that would decide whether it was worth pursuing. I set an agent loop running, then spent the day cleaning my apartment.

The experiment worked. With the manifest cached at the CDN edge, the Next.js server could produce the response without waiting for the c15t backend. The request was no longer exposed to the latency between those servers, and the improvement was large enough to keep investigating.

I started using the same approach elsewhere in c15t and across a few of my other projects. Some ideas worked. Others failed miserably.

The failures were useful. While a model explored a dead end, I worked on something else. I still had to inspect the implementation and decide what its result proved, but I did not have to build every branch myself.

Theo described the distinction neatly: code can be useful without being merged.

[https://x.com/theo/status/2074094418798485814](https://x.com/theo/status/2074094418798485814)

That is what makes this different from asking a coding agent to clear a backlog. The task is not “implement this feature.” It is “produce enough evidence to decide whether this idea deserves to become a feature.”

A prototype gives an idea shape. An experiment answers a question. Coding agents make both cheaper, but only the second starts with a hypothesis and a way to prove it wrong.

More possible implementations do not create more time to evaluate them. Every branch still asks for attention. Guillermo Rauch makes the other half of the argument: greater implementation capacity makes engineering judgment more important, not less.

[https://x.com/rauchg/status/2070923036988248248](https://x.com/rauchg/status/2070923036988248248)

That is why I start with a question and a way to measure the answer. Without both, cheap code just creates more code to review.

The code does not need to ship. It just needs to make the next decision clearer.
28 changes: 27 additions & 1 deletion lib/agent-markdown.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { getAllExperience } from "@/lib/get-all-experience";
import { getAllProjects } from "@/lib/get-all-projects";
import { getBlogPosts } from "@/lib/get-blog-posts";
import { getSiteContent } from "@/lib/get-site-content";
import { renderPortfolioMarkdown } from "@/lib/render-portfolio-markdown";
import { pageSeo, type SeoKey } from "@/lib/seo";
Expand All @@ -12,6 +13,7 @@ const PAGE_PATHS: Record<PageKey, { path: string; mdPath: string }> = {
home: { path: "/", mdPath: "/index.md" },
about: { path: "/about", mdPath: "/about.md" },
projects: { path: "/projects", mdPath: "/projects.md" },
blog: { path: "/blog", mdPath: "/blog.md" },
connect: { path: "/connect", mdPath: "/connect.md" },
};

Expand Down Expand Up @@ -143,6 +145,20 @@ function connectBody(): string {
return lines.join("\n").trimEnd();
}

async function blogBody(base: string): Promise<string> {
const posts = await getBlogPosts();
const lines = ["# Blog", ""];
for (const post of posts) {
lines.push(
`## [${post.data.title}](${base}/blog/${post.id}.md)`,
"",
post.data.description,
""
);
}
return lines.join("\n").trimEnd();
}

/** Full Markdown mirror of an HTML page: frontmatter + body + sitemap link. */
export async function renderPageMarkdown(
key: PageKey,
Expand All @@ -159,6 +175,8 @@ export async function renderPageMarkdown(
body = await aboutBody();
} else if (key === "projects") {
body = await projectsBody();
} else if (key === "blog") {
body = await blogBody(base);
} else {
body = connectBody();
}
Expand All @@ -177,6 +195,7 @@ export function renderNotFoundMarkdown(baseUrl: string): string {
`- [Home](${base}/) — [markdown](${base}/index.md)`,
`- [About](${base}/about) — [markdown](${base}/about.md)`,
`- [Projects](${base}/projects) — [markdown](${base}/projects.md)`,
`- [Blog](${base}/blog) — [markdown](${base}/blog.md)`,
`- [Connect](${base}/connect) — [markdown](${base}/connect.md)`,
"",
`See [the full sitemap](${base}/sitemap.md).`,
Expand All @@ -196,6 +215,7 @@ export function renderLlmsIndex(baseUrl: string): string {
`- [Home](${base}/index.md): Overview, current work, and selected projects`,
`- [About](${base}/about.md): Bio, experience, and appearances`,
`- [Projects](${base}/projects.md): Open-source work — c15t, Cookiebench, DSAR, Leadtype, Joyful`,
`- [Blog](${base}/blog.md): Notes on compliance, developer tooling, and software craft`,
`- [Connect](${base}/connect.md): Social and contact links`,
"",
"## More",
Expand All @@ -211,9 +231,10 @@ export async function renderLlmsFull(): Promise<string> {
}

/** sitemap.md — Markdown sitemap mirroring the site hierarchy. */
export function renderSitemapMarkdown(baseUrl: string): string {
export async function renderSitemapMarkdown(baseUrl: string): Promise<string> {
const base = normalizeBase(baseUrl);
const dateModified = new Date().toISOString();
const posts = await getBlogPosts();
return `${[
"---",
`title: "Sitemap — ${personConfig.name}"`,
Expand All @@ -228,8 +249,13 @@ export function renderSitemapMarkdown(baseUrl: string): string {
`- [Home](${base}/) — [markdown](${base}/index.md)`,
`- [About](${base}/about) — [markdown](${base}/about.md)`,
`- [Projects](${base}/projects) — [markdown](${base}/projects.md)`,
`- [Blog](${base}/blog) — [markdown](${base}/blog.md)`,
`- [Connect](${base}/connect) — [markdown](${base}/connect.md)`,
`- [The Crate](${base}/records) — [markdown](${base}/records.md)`,
...posts.map(
(post) =>
`- [${post.data.title}](${base}/blog/${post.id}) — [markdown](${base}/blog/${post.id}.md)`
),
"",
"## Agent resources",
"",
Expand Down
11 changes: 11 additions & 0 deletions lib/get-blog-posts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { type CollectionEntry, getCollection } from "astro:content";

export type BlogPost = CollectionEntry<"blog">;

export async function getBlogPosts(): Promise<BlogPost[]> {
const posts = await getCollection("blog", ({ data }) => !data.draft);

return posts.sort(
(a, b) => b.data.publishedAt.getTime() - a.data.publishedAt.getTime()
);
}
35 changes: 35 additions & 0 deletions lib/rich-markdown.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { describe, expect, test } from "bun:test";
import { normalizeXProfileTitle } from "./rich-markdown";

describe("normalizeXProfileTitle", () => {
test("removes an X suffix when the handle uses display casing", () => {
expect(
normalizeXProfileTitle(
"Christopher Burns (@BurnedChris) on X",
"burnedchris"
)
).toBe("Christopher Burns");
});

test("removes an X suffix when the handle casing already matches", () => {
expect(
normalizeXProfileTitle(
"Tanner Linsley (@tannerlinsley) on X",
"tannerlinsley"
)
).toBe("Tanner Linsley");
});

test("preserves titles belonging to a different handle", () => {
expect(
normalizeXProfileTitle(
"Christopher Burns (@someoneelse) on X",
"burnedchris"
)
).toBe("Christopher Burns (@someoneelse) on X");
});

test("handles missing metadata", () => {
expect(normalizeXProfileTitle(undefined, "burnedchris")).toBeUndefined();
});
});
Loading