From 3555320eb7a80264851277d67efbcfb7dfc3f6cb Mon Sep 17 00:00:00 2001 From: Kaylee <65376239+KayleeWilliams@users.noreply.github.com> Date: Sun, 12 Jul 2026 15:55:09 +0100 Subject: [PATCH 1/3] feat: add markdown blog with rich embeds --- PRODUCT.md | 33 ++ content/blog/code-as-an-experiment.md | 31 ++ lib/agent-markdown.ts | 28 +- lib/get-blog-posts.ts | 11 + lib/rich-markdown.test.ts | 35 ++ lib/rich-markdown.ts | 573 ++++++++++++++++++++++++++ lib/seo.ts | 7 +- package.json | 3 +- public/AGENTS.md | 2 +- src/components/Nav.astro | 1 + src/content.config.ts | 14 +- src/pages/blog.md.ts | 32 ++ src/pages/blog/[slug].astro | 67 +++ src/pages/blog/[slug].md.ts | 42 ++ src/pages/blog/index.astro | 69 ++++ src/pages/sitemap.md.ts | 4 +- src/pages/sitemap.xml.ts | 6 +- src/styles/globals.css | 331 +++++++++++++++ 18 files changed, 1281 insertions(+), 8 deletions(-) create mode 100644 PRODUCT.md create mode 100644 content/blog/code-as-an-experiment.md create mode 100644 lib/get-blog-posts.ts create mode 100644 lib/rich-markdown.test.ts create mode 100644 lib/rich-markdown.ts create mode 100644 src/pages/blog.md.ts create mode 100644 src/pages/blog/[slug].astro create mode 100644 src/pages/blog/[slug].md.ts create mode 100644 src/pages/blog/index.astro diff --git a/PRODUCT.md b/PRODUCT.md new file mode 100644 index 0000000..3402a0d --- /dev/null +++ b/PRODUCT.md @@ -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. diff --git a/content/blog/code-as-an-experiment.md b/content/blog/code-as-an-experiment.md new file mode 100644 index 0000000..8d4c4bd --- /dev/null +++ b/content/blog/code-as-an-experiment.md @@ -0,0 +1,31 @@ +--- +title: "Code as an experiment" +description: "Coding models make experiments cheap enough to answer questions that were previously too expensive to ask." +publishedAt: 2026-07-12 +tags: + - AI + - engineering + - experiments +draft: false +--- +Coding models make code cheap enough to experiment with. The code does not need to ship. It just needs to answer the question. + +At [AI Engineer Miami](https://www.ai.engineer/miami) and [React Miami](https://www.reactmiami.com/) in April, a conversation with [https://x.com/tannerlinsley](https://x.com/tannerlinsley) left me with a question: what if we used coding models to test new shapes for a product, not just clear its existing backlog? + +For c15t v3, I wanted to know whether the manifest system I built for scripts in v2 could be used for data fetching. The hypothesis was that this would improve performance enough to justify the added complexity. I knew how to measure the result, but not whether the idea would work. Proving it by hand would have meant committing real time to an uncertain direction. That is usually where an experiment gets pushed behind work with a clearer payoff. + +This time, I wrote down the hypothesis, the rough shape of the implementation, and how I would judge the result. I set an agent loop running, then spent the day cleaning my apartment. + +The first benchmarks were promising enough to keep investigating. So I tried the same approach on other parts of c15t and on a few of my other projects. Some ideas worked. Others failed miserably. + +The failures were useful. I had spent a few moments describing the idea, then reviewed the result when it was ready. While the model explored a dead end, I worked on something else. An experiment no longer needed to win a place on my roadmap before it could teach me anything. + +[https://x.com/theo/status/2074094418798485814](https://x.com/theo/status/2074094418798485814) + +I think we underuse coding models this way. A generated implementation can test an assumption, expose a constraint, or give an idea enough shape to evaluate properly. + +[https://x.com/rauchg/status/2070923036988248248](https://x.com/rauchg/status/2070923036988248248) + +More possible implementations do not create more time to evaluate them. Every branch still asks for attention. That is why I start with a question and a way to measure the answer. Otherwise, cheap code just creates more code to review. + +Code is cheap. The hard part is asking a useful question and knowing what the answer proves. \ No newline at end of file diff --git a/lib/agent-markdown.ts b/lib/agent-markdown.ts index 8f037fb..3303aec 100644 --- a/lib/agent-markdown.ts +++ b/lib/agent-markdown.ts @@ -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"; @@ -12,6 +13,7 @@ const PAGE_PATHS: Record = { 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" }, }; @@ -143,6 +145,20 @@ function connectBody(): string { return lines.join("\n").trimEnd(); } +async function blogBody(base: string): Promise { + 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, @@ -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(); } @@ -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).`, @@ -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", @@ -211,9 +231,10 @@ export async function renderLlmsFull(): Promise { } /** sitemap.md — Markdown sitemap mirroring the site hierarchy. */ -export function renderSitemapMarkdown(baseUrl: string): string { +export async function renderSitemapMarkdown(baseUrl: string): Promise { const base = normalizeBase(baseUrl); const dateModified = new Date().toISOString(); + const posts = await getBlogPosts(); return `${[ "---", `title: "Sitemap — ${personConfig.name}"`, @@ -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", "", diff --git a/lib/get-blog-posts.ts b/lib/get-blog-posts.ts new file mode 100644 index 0000000..63505e5 --- /dev/null +++ b/lib/get-blog-posts.ts @@ -0,0 +1,11 @@ +import { type CollectionEntry, getCollection } from "astro:content"; + +export type BlogPost = CollectionEntry<"blog">; + +export async function getBlogPosts(): Promise { + const posts = await getCollection("blog", ({ data }) => !data.draft); + + return posts.sort( + (a, b) => b.data.publishedAt.getTime() - a.data.publishedAt.getTime() + ); +} diff --git a/lib/rich-markdown.test.ts b/lib/rich-markdown.test.ts new file mode 100644 index 0000000..bd42246 --- /dev/null +++ b/lib/rich-markdown.test.ts @@ -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(); + }); +}); diff --git a/lib/rich-markdown.ts b/lib/rich-markdown.ts new file mode 100644 index 0000000..9ff32cc --- /dev/null +++ b/lib/rich-markdown.ts @@ -0,0 +1,573 @@ +import { mkdir, readFile, stat, writeFile } from "node:fs/promises"; +import { join } from "node:path"; +import { Marked, type Token, type Tokens } from "marked"; + +interface LinkPreview { + description?: string; + image?: string; + kind: "link" | "profile" | "tweet"; + profileUrl?: string; + site: string; + title: string; + tweetHtml?: string; +} + +interface OEmbedResponse { + author_name?: string; + author_url?: string; + html?: string; +} + +const EXTERNAL_URL_REGEX = /^https?:\/\//; +const TITLE_TAG_REGEX = /]*>([\s\S]*?)<\/title>/i; +const TWEET_BODY_REGEX = /]*>([\s\S]*?)<\/p>/i; +const WWW_PREFIX_REGEX = /^www\./; +const BR_TAG_REGEX = //gi; +const PROFILE_IMAGE_URL_REGEX = + /https:\/\/pbs\.twimg\.com\/profile_images\/[^"\\]+/i; +const AVATAR_CACHE_MAX_AGE_MS = 1000 * 60 * 60 * 24 * 7; +const AVATAR_CACHE_DIRECTORY = join( + process.cwd(), + ".astro", + "blog-avatar-cache" +); + +const PROFILE_NAMES: Record = { + burnedchris: "Christopher Burns", + kaylee_dev: "Kaylee Williams", + tannerlinsley: "Tanner Linsley", +}; +const previewCache = new Map>(); +const avatarCache = new Map>(); + +const ENTITY_MAP: Record = { + "&": "&", + "'": "'", + "'": "'", + ">": ">", + "<": "<", + """: '"', +}; + +function escapeHtml(value: string): string { + return value.replace(/[&<>"]/g, (character) => { + const entities: Record = { + "&": "&", + "<": "<", + ">": ">", + '"': """, + }; + return entities[character] ?? character; + }); +} + +function decodeEntities(value: string): string { + return value.replace( + /&(amp|apos|#39|gt|lt|quot);/g, + (entity) => ENTITY_MAP[entity] ?? entity + ); +} + +function plainText(value: string): string { + return decodeEntities(value.replace(/<[^>]+>/g, "").trim()); +} + +export function normalizeXProfileTitle( + title: string | undefined, + handle: string +): string | undefined { + if (!title) { + return; + } + const escapedHandle = handle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const suffix = new RegExp(`\\s+\\(@${escapedHandle}\\)\\s+on X$`, "i"); + return title.replace(suffix, "").trim() || undefined; +} + +function metaContent(html: string, key: string): string | undefined { + const escapedKey = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const patterns = [ + new RegExp( + `]+(?:property|name)=["']${escapedKey}["'][^>]+content=["']([^"']+)["'][^>]*>`, + "i" + ), + new RegExp( + `]+content=["']([^"']+)["'][^>]+(?:property|name)=["']${escapedKey}["'][^>]*>`, + "i" + ), + ]; + + for (const pattern of patterns) { + const match = html.match(pattern)?.[1]; + if (match) { + return plainText(match); + } + } + return; +} + +function fetchWithTimeout(url: string): Promise { + return fetch(url, { + headers: { "User-Agent": "kaylee.dev link preview" }, + signal: AbortSignal.timeout(3500), + }); +} + +async function responseDataUrl( + response: Response +): Promise { + if (!response.ok) { + return; + } + const contentType = response.headers.get("content-type") ?? "image/jpeg"; + if (!contentType.startsWith("image/")) { + return; + } + const bytes = await response.arrayBuffer(); + if (bytes.byteLength > 200_000) { + return; + } + return `data:${contentType};base64,${Buffer.from(bytes).toString("base64")}`; +} + +function avatarCachePath(handle: string): string { + const safeHandle = handle.toLowerCase().replace(/[^a-z0-9_-]/g, "_"); + return join(AVATAR_CACHE_DIRECTORY, `${safeHandle}.txt`); +} + +async function readCachedAvatar(handle: string): Promise { + try { + const path = avatarCachePath(handle); + const file = await stat(path); + if (Date.now() - file.mtimeMs > AVATAR_CACHE_MAX_AGE_MS) { + return; + } + return await readFile(path, "utf8"); + } catch { + return; + } +} + +async function fetchAvatar(handle: string): Promise { + const cached = await readCachedAvatar(handle); + if (cached) { + return cached; + } + + let image: string | undefined; + try { + const response = await fetchWithTimeout( + `https://unavatar.io/x/${encodeURIComponent(handle)}?fallback=false` + ); + image = await responseDataUrl(response); + } catch { + image = undefined; + } + + if (!image) { + try { + const profileResponse = await fetchWithTimeout(`https://x.com/${handle}`); + const profileHtml = profileResponse.ok + ? await profileResponse.text() + : ""; + const imageUrl = profileHtml.match(PROFILE_IMAGE_URL_REGEX)?.[0]; + if (imageUrl) { + image = await responseDataUrl(await fetchWithTimeout(imageUrl)); + } + } catch { + image = undefined; + } + } + + if (image) { + try { + await mkdir(AVATAR_CACHE_DIRECTORY, { recursive: true }); + await writeFile(avatarCachePath(handle), image, "utf8"); + } catch { + // A read-only build filesystem still gets the in-memory cached image. + } + } + return image; +} + +function avatarDataUrl(handle: string): Promise { + const key = handle.toLowerCase(); + const cached = avatarCache.get(key); + if (cached) { + return cached; + } + const avatar = fetchAvatar(key); + avatarCache.set(key, avatar); + return avatar; +} + +function parseXUrl( + url: URL +): + | { handle: string; kind: "profile" } + | { handle: string; id: string; kind: "tweet" } + | undefined { + if ( + !["x.com", "www.x.com", "twitter.com", "www.twitter.com"].includes( + url.hostname + ) + ) { + return; + } + const parts = url.pathname.split("/").filter(Boolean); + if (parts.length === 1) { + return { handle: parts[0], kind: "profile" }; + } + if (parts.length >= 3 && parts[1] === "status") { + return { handle: parts[0], id: parts[2], kind: "tweet" }; + } + return; +} + +async function profilePreview( + handle: string, + linkLabel: string +): Promise { + const explicitLabel = linkLabel.startsWith("http") ? undefined : linkLabel; + const image = avatarDataUrl(handle); + let resolvedName: string | undefined; + if (!explicitLabel) { + try { + const response = await fetchWithTimeout(`https://x.com/${handle}`); + const html = response.ok ? await response.text() : ""; + resolvedName = normalizeXProfileTitle( + metaContent(html, "og:title"), + handle + ); + } catch { + resolvedName = undefined; + } + } + return { + image: await image, + kind: "profile", + profileUrl: `https://x.com/${handle}`, + site: `@${handle}`, + title: + explicitLabel || + resolvedName || + PROFILE_NAMES[handle.toLowerCase()] || + `@${handle}`, + }; +} + +async function tweetPreview(url: string, handle: string): Promise { + try { + const endpoint = new URL("https://publish.twitter.com/oembed"); + endpoint.searchParams.set("url", url); + endpoint.searchParams.set("dnt", "true"); + endpoint.searchParams.set("omit_script", "true"); + const response = await fetchWithTimeout(endpoint.toString()); + if (!response.ok) { + throw new Error("X did not return this post"); + } + const data = (await response.json()) as OEmbedResponse; + const tweetHtml = data.html?.match(TWEET_BODY_REGEX)?.[1]; + const resolvedHandle = + data.author_url?.split("/").filter(Boolean).at(-1) || handle; + return { + image: await avatarDataUrl(resolvedHandle), + kind: "tweet", + profileUrl: data.author_url || `https://x.com/${resolvedHandle}`, + site: `@${resolvedHandle}`, + title: + data.author_name || + PROFILE_NAMES[resolvedHandle] || + `@${resolvedHandle}`, + tweetHtml, + }; + } catch { + return { + image: await avatarDataUrl(handle), + kind: "tweet", + profileUrl: `https://x.com/${handle}`, + site: `@${handle}`, + title: PROFILE_NAMES[handle.toLowerCase()] || `@${handle}`, + }; + } +} + +async function websitePreview( + url: URL, + linkLabel: string +): Promise { + const fallbackTitle = linkLabel.startsWith("http") + ? url.hostname.replace(WWW_PREFIX_REGEX, "") + : linkLabel; + try { + const response = await fetchWithTimeout(url.toString()); + if (!response.ok) { + throw new Error("Preview request failed"); + } + const html = await response.text(); + const rawTitle = + metaContent(html, "og:title") || + plainText(html.match(TITLE_TAG_REGEX)?.[1] ?? ""); + return { + description: + metaContent(html, "og:description") || metaContent(html, "description"), + kind: "link", + site: + metaContent(html, "og:site_name") || + url.hostname.replace(WWW_PREFIX_REGEX, ""), + title: rawTitle || fallbackTitle, + }; + } catch { + return { + kind: "link", + site: url.hostname.replace(WWW_PREFIX_REGEX, ""), + title: fallbackTitle, + }; + } +} + +function createPreview(href: string, label: string): Promise { + const url = new URL(href); + const xLink = parseXUrl(url); + if (xLink?.kind === "profile") { + return profilePreview(xLink.handle, label); + } + if (xLink?.kind === "tweet") { + return tweetPreview(url.toString(), xLink.handle); + } + return websitePreview(url, label); +} + +function cachedPreview(href: string, label: string): Promise { + const cached = previewCache.get(href); + if (cached) { + return cached; + } + const preview = createPreview(href, label).catch((error: unknown) => { + previewCache.delete(href); + throw error; + }); + previewCache.set(href, preview); + return preview; +} + +function standaloneLink(token: Tokens.Paragraph): Tokens.Link | undefined { + if (token.tokens.length !== 1 || token.tokens[0].type !== "link") { + return; + } + const link = token.tokens[0] as Tokens.Link; + return EXTERNAL_URL_REGEX.test(link.href) ? link : undefined; +} + +function avatarMarkup(preview: LinkPreview): string { + if (preview.image) { + return ``; + } + return ``; +} + +function inlineAvatarMarkup(preview: LinkPreview): string { + if (preview.image) { + return ``; + } + return ``; +} + +function renderInlineProfile(href: string, preview: LinkPreview): string { + return `${inlineAvatarMarkup(preview)}${escapeHtml(preview.title)}`; +} + +function renderPreview(href: string, preview: LinkPreview): string { + const safeHref = escapeHtml(href); + if (preview.kind === "profile") { + return `${avatarMarkup(preview)}${escapeHtml(preview.title)}${escapeHtml(preview.site)}`; + } + if (preview.kind === "tweet") { + const body = preview.tweetHtml + ? `
${preview.tweetHtml}
` + : '

View this post on X.

'; + const profileUrl = escapeHtml(preview.profileUrl || href); + return ``; + } + return `${escapeHtml(preview.site)}${escapeHtml(preview.title)}${preview.description ? `${escapeHtml(preview.description)}` : ""}Visit site `; +} + +function agentProfileLink(href: string, preview: LinkPreview): string { + const identity = + preview.title === preview.site + ? preview.title + : `${preview.title} (${preview.site})`; + return `[${identity}](${href})`; +} + +function agentTweetQuote(href: string, preview: LinkPreview): string { + const text = preview.tweetHtml + ? plainText(preview.tweetHtml.replace(BR_TAG_REGEX, "\n")) + : "Post text unavailable. View the original on X."; + const quote = text + .split("\n") + .map((line) => `> ${line}`) + .join("\n"); + return `${quote}\n>\n> ${agentProfileLink(href, preview)}`; +} + +function linksInToken(parser: Marked, token: Token): Tokens.Link[] { + const links: Tokens.Link[] = []; + parser.walkTokens([token], (child) => { + if (child.type === "link") { + links.push(child); + } + }); + return links; +} + +function replaceAgentLinks( + raw: string, + links: Tokens.Link[], + previews: Map +): { markdown: string; tweetQuotes: string[] } { + let cursor = 0; + let markdown = ""; + const tweetQuotes: string[] = []; + + for (const link of links) { + const preview = previews.get(link.href); + if (!(preview && ["profile", "tweet"].includes(preview.kind))) { + continue; + } + const index = raw.indexOf(link.raw, cursor); + if (index < 0) { + continue; + } + markdown += raw.slice(cursor, index); + if (preview.kind === "profile") { + markdown += agentProfileLink(link.href, preview); + } else { + markdown += `[X post by ${preview.title} (${preview.site})](${link.href})`; + tweetQuotes.push(agentTweetQuote(link.href, preview)); + } + cursor = index + link.raw.length; + } + + markdown += raw.slice(cursor); + return { markdown, tweetQuotes }; +} + +export async function renderAgentMarkdown(source: string): Promise { + const parser = new Marked({ gfm: true }); + const tokens = parser.lexer(source); + const xLinks: Tokens.Link[] = []; + parser.walkTokens(tokens, (token) => { + if (token.type !== "link" || !EXTERNAL_URL_REGEX.test(token.href)) { + return; + } + try { + if (parseXUrl(new URL(token.href))) { + xLinks.push(token); + } + } catch { + // Malformed links remain unchanged in the Markdown mirror. + } + }); + + const previews = new Map(); + await Promise.all( + xLinks.map(async (link) => { + try { + previews.set(link.href, await cachedPreview(link.href, link.text)); + } catch { + // Preserve the author's original Markdown when metadata is unavailable. + } + }) + ); + + const rendered = tokens.map((token) => { + if (token.type === "code" || token.type === "html") { + return token.raw; + } + if (token.type === "paragraph") { + const standalone = standaloneLink(token); + const preview = standalone ? previews.get(standalone.href) : undefined; + if (standalone && preview?.kind === "tweet") { + return `${agentTweetQuote(standalone.href, preview)}\n\n`; + } + if (standalone && preview?.kind === "profile") { + return `${agentProfileLink(standalone.href, preview)}\n\n`; + } + } + + const { markdown, tweetQuotes } = replaceAgentLinks( + token.raw, + linksInToken(parser, token), + previews + ); + if (tweetQuotes.length === 0) { + return markdown; + } + return `${markdown.trimEnd()}\n\n${tweetQuotes.join("\n\n")}\n\n`; + }); + + return rendered.join("").trimEnd(); +} + +export async function renderRichMarkdown(source: string): Promise { + const parser = new Marked({ gfm: true }); + const tokens = parser.lexer(source); + const standaloneLinks = tokens + .filter((token): token is Tokens.Paragraph => token.type === "paragraph") + .map(standaloneLink) + .filter((link): link is Tokens.Link => Boolean(link)); + const inlineProfileLinks: Tokens.Link[] = []; + parser.walkTokens(tokens, (token) => { + if (token.type !== "link" || !EXTERNAL_URL_REGEX.test(token.href)) { + return; + } + try { + if (parseXUrl(new URL(token.href))?.kind === "profile") { + inlineProfileLinks.push(token); + } + } catch { + // Malformed links retain Marked's normal link rendering. + } + }); + const links = [...standaloneLinks, ...inlineProfileLinks].filter( + (link, index, allLinks) => + allLinks.findIndex((candidate) => candidate.href === link.href) === index + ); + const previews = new Map(); + + await Promise.all( + links.map(async (link) => { + try { + previews.set(link.href, await cachedPreview(link.href, link.text)); + } catch { + // The normal Markdown link remains the fallback for malformed URLs. + } + }) + ); + + parser.use({ + renderer: { + link({ href, title, tokens: linkTokens }) { + const preview = previews.get(href); + if (preview?.kind === "profile") { + return renderInlineProfile(href, preview); + } + const text = this.parser.parseInline(linkTokens); + const titleAttribute = title ? ` title="${escapeHtml(title)}"` : ""; + const external = EXTERNAL_URL_REGEX.test(href); + return `${text}`; + }, + paragraph(token) { + const link = standaloneLink(token); + const preview = link ? previews.get(link.href) : undefined; + if (link && preview) { + return `${renderPreview(link.href, preview)}\n`; + } + return `

${this.parser.parseInline(token.tokens)}

\n`; + }, + }, + }); + + return parser.parser(tokens) as string; +} diff --git a/lib/seo.ts b/lib/seo.ts index 15eb158..d3bd228 100644 --- a/lib/seo.ts +++ b/lib/seo.ts @@ -1,6 +1,6 @@ import { personConfig } from "@/lib/site-config"; -export type SeoKey = "home" | "about" | "projects" | "connect"; +export type SeoKey = "home" | "about" | "projects" | "blog" | "connect"; /** * Single source of truth for per-page (≤60 chars) and meta description @@ -22,6 +22,11 @@ export const pageSeo: Record<SeoKey, { title: string; description: string }> = { description: "Open-source tools for consent, privacy, and developer experience by Kaylee Williams at Inth: c15t, Cookiebench, DSAR, and Leadtype.", }, + blog: { + title: "Blog | Kaylee Williams", + description: + "Notes from Kaylee Williams on open-source compliance, developer tooling, privacy, and the details that make software feel finished.", + }, connect: { title: "Connect — Kaylee Williams", description: diff --git a/package.json b/package.json index 14e2dce..69b9558 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,8 @@ "build:node": "ASTRO_ADAPTER=node ASTRO_TELEMETRY_DISABLED=1 astro build", "start": "bun run build:node && HOST=127.0.0.1 PORT=4321 node dist/server/entry.mjs", "curate": "bun run scripts/curate.ts", - "fmt": "biome check --write" + "fmt": "biome check --write", + "test": "bun test" }, "dependencies": { "@astrojs/svelte": "^9.0.0", diff --git a/public/AGENTS.md b/public/AGENTS.md index 438bb58..2cee763 100644 --- a/public/AGENTS.md +++ b/public/AGENTS.md @@ -9,7 +9,7 @@ stack: consent, data rights, performance, and agent-readable docs, in code. - `/llms.txt` — concise index (llmstxt.org format) - `/llms-full.txt` — the full profile inlined, including live OSS activity - `/sitemap.md` — Markdown sitemap; `/sitemap.xml` — XML sitemap -- Every page has a Markdown mirror: `/index.md`, `/about.md`, `/projects.md`, `/connect.md` +- Every page has a Markdown mirror: `/index.md`, `/about.md`, `/projects.md`, `/blog.md`, `/connect.md`, and `/blog/{slug}.md` - Pages also answer `Accept: text/markdown` with their Markdown mirror ## Usage diff --git a/src/components/Nav.astro b/src/components/Nav.astro index 1e991d5..cc67712 100644 --- a/src/components/Nav.astro +++ b/src/components/Nav.astro @@ -5,6 +5,7 @@ const links = [ { href: "/", label: "Home" }, { href: "/about", label: "About" }, { href: "/projects", label: "Projects" }, + { href: "/blog", label: "Blog" }, { href: "/connect", label: "Connect" }, ]; diff --git a/src/content.config.ts b/src/content.config.ts index ef98807..b8a3a4f 100644 --- a/src/content.config.ts +++ b/src/content.config.ts @@ -48,4 +48,16 @@ const experience = defineCollection({ }), }); -export const collections = { experience, projects, site }; +const blog = defineCollection({ + loader: glob({ base: "./content/blog", pattern: "**/*.{md,mdx}" }), + schema: z.object({ + title: z.string(), + description: z.string(), + publishedAt: z.coerce.date(), + updatedAt: z.coerce.date().optional(), + tags: z.array(z.string()).default([]), + draft: z.boolean().default(false), + }), +}); + +export const collections = { blog, experience, projects, site }; diff --git a/src/pages/blog.md.ts b/src/pages/blog.md.ts new file mode 100644 index 0000000..43d2aa3 --- /dev/null +++ b/src/pages/blog.md.ts @@ -0,0 +1,32 @@ +import { markdownResponse } from "@/lib/agent-markdown"; +import { getBlogPosts } from "@/lib/get-blog-posts"; +import { pageSeo } from "@/lib/seo"; + +export const prerender = true; + +export async function GET({ url }: { url: URL }): Promise<Response> { + const posts = await getBlogPosts(); + const body = [ + "---", + `title: "${pageSeo.blog.title}"`, + `description: "${pageSeo.blog.description}"`, + `canonical_url: "${url.origin}/blog"`, + "---", + "", + "# Blog", + "", + ...posts.flatMap((post) => [ + `## [${post.data.title}](${url.origin}/blog/${post.id}.md)`, + "", + post.data.description, + "", + `Published: ${post.data.publishedAt.toISOString().slice(0, 10)}`, + "", + ]), + ].join("\n"); + + return markdownResponse(`${body.trimEnd()}\n`, { + base: url.origin, + canonicalPath: "/blog", + }); +} diff --git a/src/pages/blog/[slug].astro b/src/pages/blog/[slug].astro new file mode 100644 index 0000000..bdfcf62 --- /dev/null +++ b/src/pages/blog/[slug].astro @@ -0,0 +1,67 @@ +--- +import BaseLayout from "@/src/layouts/BaseLayout.astro"; +import { getBlogPosts, type BlogPost } from "@/lib/get-blog-posts"; +import { renderRichMarkdown } from "@/lib/rich-markdown"; + +export const prerender = true; + +export async function getStaticPaths() { + const posts = await getBlogPosts(); + return posts.map((post) => ({ params: { slug: post.id }, props: { post } })); +} + +type Props = { post: BlogPost }; + +const { post } = Astro.props; +const html = await renderRichMarkdown(post.body ?? ""); +const dateFormat = new Intl.DateTimeFormat("en-GB", { + day: "numeric", + month: "long", + year: "numeric", +}); +--- + +<BaseLayout + title={`${post.data.title} | Kaylee Williams`} + description={post.data.description} +> + <article> + <a + class="mb-7 inline-flex items-center gap-1.5 rounded-md text-muted-foreground text-sm transition-colors hover:text-primary focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-primary" + href="/blog" + > + <span aria-hidden="true">←</span> + All posts + </a> + + <header class="mb-9 space-y-4 border-b border-border pb-7"> + <h1 class="font-bold text-3xl tracking-tight sm:text-4xl">{post.data.title}</h1> + <p class="max-w-prose text-lg text-muted-foreground"> + {post.data.description} + </p> + <div class="flex flex-wrap items-center gap-x-3 gap-y-2 text-muted-foreground text-sm"> + <time datetime={post.data.publishedAt.toISOString()}> + {dateFormat.format(post.data.publishedAt)} + </time> + { + post.data.updatedAt ? ( + <> + <span aria-hidden="true">·</span> + <span>Updated {dateFormat.format(post.data.updatedAt)}</span> + </> + ) : null + } + { + post.data.tags.length > 0 ? ( + <> + <span aria-hidden="true">·</span> + <span>{post.data.tags.join(" · ")}</span> + </> + ) : null + } + </div> + </header> + + <div class="blog-prose" set:html={html} /> + </article> +</BaseLayout> diff --git a/src/pages/blog/[slug].md.ts b/src/pages/blog/[slug].md.ts new file mode 100644 index 0000000..fe2d392 --- /dev/null +++ b/src/pages/blog/[slug].md.ts @@ -0,0 +1,42 @@ +import { markdownResponse } from "@/lib/agent-markdown"; +import { type BlogPost, getBlogPosts } from "@/lib/get-blog-posts"; +import { renderAgentMarkdown } from "@/lib/rich-markdown"; + +export const prerender = true; + +export async function getStaticPaths() { + const posts = await getBlogPosts(); + return posts.map((post) => ({ params: { slug: post.id }, props: { post } })); +} + +export async function GET({ + props, + url, +}: { + props: { post: BlogPost }; + url: URL; +}): Promise<Response> { + const { post } = props; + const content = await renderAgentMarkdown(post.body ?? ""); + const body = [ + "---", + `title: ${JSON.stringify(post.data.title)}`, + `description: ${JSON.stringify(post.data.description)}`, + `canonical_url: "${url.origin}/blog/${post.id}"`, + `published: "${post.data.publishedAt.toISOString()}"`, + ...(post.data.updatedAt + ? [`updated: "${post.data.updatedAt.toISOString()}"`] + : []), + "---", + "", + `# ${post.data.title}`, + "", + content, + "", + ].join("\n"); + + return markdownResponse(body, { + base: url.origin, + canonicalPath: `/blog/${post.id}`, + }); +} diff --git a/src/pages/blog/index.astro b/src/pages/blog/index.astro new file mode 100644 index 0000000..ba5dcbf --- /dev/null +++ b/src/pages/blog/index.astro @@ -0,0 +1,69 @@ +--- +import BaseLayout from "@/src/layouts/BaseLayout.astro"; +import { getBlogPosts } from "@/lib/get-blog-posts"; +import { pageSeo } from "@/lib/seo"; + +export const prerender = true; + +const posts = await getBlogPosts(); +const dateFormat = new Intl.DateTimeFormat("en-GB", { + day: "numeric", + month: "short", + year: "numeric", +}); +--- + +<BaseLayout title={pageSeo.blog.title} description={pageSeo.blog.description}> + <header class="space-y-2"> + <h2 class="font-bold text-3xl tracking-tight">Blog</h2> + <p class="max-w-prose text-muted-foreground"> + Notes on compliance, developer tools, and the small decisions that make software + hold together. + </p> + </header> + + { + posts.length > 0 ? ( + <ol class="divide-y divide-border" aria-label="Blog posts"> + {posts.map((post) => ( + <li class="py-5 first:pt-0"> + <article class="flex flex-col gap-2"> + <div class="flex items-baseline justify-between gap-4"> + <a + class="group font-semibold text-lg text-primary" + href={`/blog/${post.id}`} + > + <span class="group-hover:underline">{post.data.title}</span> + </a> + <time + class="shrink-0 text-muted-foreground text-sm tabular-nums" + datetime={post.data.publishedAt.toISOString()} + > + {dateFormat.format(post.data.publishedAt)} + </time> + </div> + <p class="max-w-prose text-muted-foreground text-sm"> + {post.data.description} + </p> + {post.data.tags.length > 0 ? ( + <ul + class="flex flex-wrap gap-x-3 gap-y-1 text-muted-foreground text-xs" + aria-label="Topics" + > + {post.data.tags.map((tag) => ( + <li>{tag}</li> + ))} + </ul> + ) : null} + </article> + </li> + ))} + </ol> + ) : ( + <div class="rounded-lg border border-dashed border-border px-6 py-10 text-center"> + <p class="font-semibold">Nothing published yet.</p> + <p class="mt-1 text-muted-foreground text-sm">The drafts are behaving themselves.</p> + </div> + ) + } +</BaseLayout> diff --git a/src/pages/sitemap.md.ts b/src/pages/sitemap.md.ts index 48225ba..af001c4 100644 --- a/src/pages/sitemap.md.ts +++ b/src/pages/sitemap.md.ts @@ -3,6 +3,6 @@ import { markdownResponse, renderSitemapMarkdown } from "@/lib/agent-markdown"; export const prerender = true; -export function GET({ url }: APIContext): Response { - return markdownResponse(renderSitemapMarkdown(url.origin)); +export async function GET({ url }: APIContext): Promise<Response> { + return markdownResponse(await renderSitemapMarkdown(url.origin)); } diff --git a/src/pages/sitemap.xml.ts b/src/pages/sitemap.xml.ts index 3ea5c0d..26293d2 100644 --- a/src/pages/sitemap.xml.ts +++ b/src/pages/sitemap.xml.ts @@ -1,16 +1,20 @@ import type { APIContext } from "astro"; +import { getBlogPosts } from "@/lib/get-blog-posts"; export const prerender = true; const TRAILING_SLASHES_REGEX = /\/+$/; -export function GET({ url }: APIContext): Response { +export async function GET({ url }: APIContext): Promise<Response> { const base = url.origin.replace(TRAILING_SLASHES_REGEX, ""); const now = new Date().toISOString(); + const posts = await getBlogPosts(); const routes = [ { path: "/", priority: "1" }, { path: "/about", priority: "0.8" }, { path: "/projects", priority: "0.8" }, + { path: "/blog", priority: "0.7" }, + ...posts.map((post) => ({ path: `/blog/${post.id}`, priority: "0.6" })), { path: "/connect", priority: "0.5" }, { path: "/records", priority: "0.4" }, ]; diff --git a/src/styles/globals.css b/src/styles/globals.css index a1064a1..d604740 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -258,6 +258,337 @@ h3 { text-decoration: underline; } +.blog-prose { + max-width: 70ch; + font-size: 1.0625rem; + line-height: 1.75; +} + +.blog-prose > * + * { + margin-top: 1.35em; +} + +.blog-prose h2, +.blog-prose h3 { + line-height: 1.2; + letter-spacing: -0.025em; +} + +.blog-prose h2 { + margin-top: 2.4em; + font-size: 1.65rem; + font-weight: 750; +} + +.blog-prose h3 { + margin-top: 2em; + font-size: 1.3rem; + font-weight: 700; +} + +.blog-prose :is(ul, ol) { + padding-left: 1.4em; +} + +.blog-prose ul { + list-style: disc; +} + +.blog-prose ol { + list-style: decimal; +} + +.blog-prose li + li { + margin-top: 0.4em; +} + +.blog-prose :not(.rich-profile, .rich-link, .rich-tweet) a { + color: var(--primary); + text-decoration-line: underline; + text-decoration-thickness: 0.08em; + text-underline-offset: 0.2em; +} + +.blog-prose :not(.rich-profile, .rich-link, .rich-tweet) a:hover { + text-decoration-thickness: 0.13em; +} + +.blog-prose .inline-profile { + display: inline-flex; + gap: 0.32em; + align-items: center; + max-width: 100%; + padding: 0.08em 0.48em 0.08em 0.12em; + margin-inline: 0.08em; + font-size: 0.9em; + line-height: 1.4; + vertical-align: -0.18em; + color: var(--foreground); + white-space: nowrap; + text-decoration: none; + background: color-mix(in srgb, var(--primary) 8%, var(--card)); + border: 1px solid color-mix(in srgb, var(--primary) 24%, var(--border)); + border-radius: 9999px; +} + +.blog-prose .inline-profile:hover { + color: var(--primary); + border-color: color-mix(in srgb, var(--primary) 55%, var(--border)); +} + +.blog-prose .inline-profile:focus-visible { + outline: 2px solid var(--primary); + outline-offset: 2px; +} + +.inline-profile__avatar, +.inline-profile > span { + flex: none; + width: 1.25rem; + height: 1.25rem; + object-fit: cover; + border-radius: 9999px; +} + +.inline-profile > span { + display: inline-grid; + place-items: center; + font-size: 0.7em; + font-weight: 800; + color: var(--primary); + background: color-mix(in srgb, var(--primary) 14%, var(--muted)); +} + +.inline-profile strong { + overflow: hidden; + text-overflow: ellipsis; +} + +.blog-prose blockquote { + padding: 1.1rem 1.25rem; + color: var(--muted-foreground); + background: color-mix(in srgb, var(--muted) 65%, transparent); + border: 1px solid var(--border); + border-radius: 0.75rem; +} + +.blog-prose :is(code, pre) { + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; +} + +.blog-prose :not(pre) > code { + padding: 0.14em 0.35em; + font-size: 0.9em; + background: var(--muted); + border-radius: 0.3rem; +} + +.blog-prose pre { + padding: 1rem 1.15rem; + overflow-x: auto; + font-size: 0.9rem; + line-height: 1.65; + background: var(--muted); + border: 1px solid var(--border); + border-radius: 0.75rem; +} + +.blog-prose img:not(.rich-embed__avatar, .inline-profile__avatar) { + width: 100%; + border-radius: 0.75rem; +} + +.rich-profile, +.rich-link, +.rich-tweet { + display: block; + background: var(--card); + border: 1px solid var(--border); + border-radius: 0.8rem; + box-shadow: 0 1px 2px color-mix(in srgb, var(--foreground) 6%, transparent); +} + +.rich-profile, +.rich-link { + color: var(--card-foreground); + text-decoration: none; + transition: + border-color 160ms ease-out, + transform 160ms ease-out, + box-shadow 160ms ease-out; +} + +.rich-profile:hover, +.rich-link:hover { + border-color: color-mix(in srgb, var(--primary) 48%, var(--border)); + box-shadow: 0 8px 24px color-mix(in srgb, var(--foreground) 8%, transparent); + transform: translateY(-2px); +} + +.rich-profile:focus-visible, +.rich-link:focus-visible, +.rich-tweet a:focus-visible { + outline: 2px solid var(--primary); + outline-offset: 3px; +} + +.rich-profile { + display: flex; + gap: 0.8rem; + align-items: center; + padding: 0.85rem 1rem; +} + +.rich-profile > span:nth-child(2), +.rich-tweet__author > span { + display: flex; + flex: 1; + flex-direction: column; + min-width: 0; + line-height: 1.3; +} + +.rich-profile strong, +.rich-tweet strong { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.rich-profile small, +.rich-tweet small { + font-size: 0.78rem; + color: var(--muted-foreground); +} + +.rich-embed__avatar { + flex: none; + width: 2.75rem; + height: 2.75rem; + object-fit: cover; + border-radius: 9999px; +} + +.rich-embed__avatar--fallback { + display: grid; + place-items: center; + font-weight: 800; + color: var(--primary); + background: color-mix(in srgb, var(--primary) 14%, var(--muted)); +} + +.rich-embed__mark { + font-size: 1.15rem; + color: var(--muted-foreground); +} + +.rich-tweet { + padding: 1rem 1.1rem; +} + +.rich-tweet header { + display: flex; + gap: 0.75rem; + align-items: center; +} + +.rich-tweet__author { + display: flex; + flex: 1; + gap: 0.75rem; + align-items: center; + min-width: 0; + color: var(--foreground); + text-decoration: none; +} + +.blog-prose a.rich-tweet__author, +.blog-prose a.rich-tweet__author:hover { + text-decoration: none; +} + +.rich-tweet__author:hover strong { + color: var(--primary); +} + +.rich-tweet__body, +.rich-tweet__fallback { + margin-top: 1rem; + font-size: 1rem; + line-height: 1.65; +} + +.rich-tweet__body a { + color: var(--primary); +} + +.rich-tweet__source { + display: inline-flex; + gap: 0.25rem; + align-items: center; + margin-top: 0.8rem; + font-size: 0.8rem; + font-weight: 700; + color: var(--muted-foreground); + text-decoration: none; +} + +.rich-tweet__source:hover { + color: var(--primary); +} + +.rich-link { + padding: 1rem 1.1rem 1.05rem; +} + +.rich-link > span, +.rich-link > strong { + display: block; +} + +.rich-link__site { + margin-bottom: 0.3rem; + font-size: 0.75rem; + font-weight: 750; + color: var(--primary); + letter-spacing: 0.04em; +} + +.rich-link > strong { + font-size: 1.05rem; + line-height: 1.35; +} + +.rich-link__description { + display: -webkit-box !important; + -webkit-box-orient: vertical; + margin-top: 0.35rem; + overflow: hidden; + -webkit-line-clamp: 2; + font-size: 0.88rem; + line-height: 1.5; + color: var(--muted-foreground); +} + +.rich-link__action { + margin-top: 0.7rem; + font-size: 0.78rem; + font-weight: 700; + color: var(--muted-foreground); +} + +@media (prefers-reduced-motion: reduce) { + .rich-profile, + .rich-link { + transition: none; + } + + .rich-profile:hover, + .rich-link:hover { + transform: none; + } +} + /* Staggered entrance for the /connect surfaces (delay set inline per element). */ @keyframes rise { from { From 17f21d435ba178ccbca8549512362d51bbc16db4 Mon Sep 17 00:00:00 2001 From: Kaylee <65376239+KayleeWilliams@users.noreply.github.com> Date: Sun, 12 Jul 2026 17:27:55 +0100 Subject: [PATCH 2/3] feat(blog): publish code doesn't have to ship --- content/blog/code-as-an-experiment.md | 31 ------------ content/blog/code-doesnt-have-to-ship.md | 61 ++++++++++++++++++++++++ src/styles/globals.css | 4 +- 3 files changed, 63 insertions(+), 33 deletions(-) delete mode 100644 content/blog/code-as-an-experiment.md create mode 100644 content/blog/code-doesnt-have-to-ship.md diff --git a/content/blog/code-as-an-experiment.md b/content/blog/code-as-an-experiment.md deleted file mode 100644 index 8d4c4bd..0000000 --- a/content/blog/code-as-an-experiment.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -title: "Code as an experiment" -description: "Coding models make experiments cheap enough to answer questions that were previously too expensive to ask." -publishedAt: 2026-07-12 -tags: - - AI - - engineering - - experiments -draft: false ---- -Coding models make code cheap enough to experiment with. The code does not need to ship. It just needs to answer the question. - -At [AI Engineer Miami](https://www.ai.engineer/miami) and [React Miami](https://www.reactmiami.com/) in April, a conversation with [https://x.com/tannerlinsley](https://x.com/tannerlinsley) left me with a question: what if we used coding models to test new shapes for a product, not just clear its existing backlog? - -For c15t v3, I wanted to know whether the manifest system I built for scripts in v2 could be used for data fetching. The hypothesis was that this would improve performance enough to justify the added complexity. I knew how to measure the result, but not whether the idea would work. Proving it by hand would have meant committing real time to an uncertain direction. That is usually where an experiment gets pushed behind work with a clearer payoff. - -This time, I wrote down the hypothesis, the rough shape of the implementation, and how I would judge the result. I set an agent loop running, then spent the day cleaning my apartment. - -The first benchmarks were promising enough to keep investigating. So I tried the same approach on other parts of c15t and on a few of my other projects. Some ideas worked. Others failed miserably. - -The failures were useful. I had spent a few moments describing the idea, then reviewed the result when it was ready. While the model explored a dead end, I worked on something else. An experiment no longer needed to win a place on my roadmap before it could teach me anything. - -[https://x.com/theo/status/2074094418798485814](https://x.com/theo/status/2074094418798485814) - -I think we underuse coding models this way. A generated implementation can test an assumption, expose a constraint, or give an idea enough shape to evaluate properly. - -[https://x.com/rauchg/status/2070923036988248248](https://x.com/rauchg/status/2070923036988248248) - -More possible implementations do not create more time to evaluate them. Every branch still asks for attention. That is why I start with a question and a way to measure the answer. Otherwise, cheap code just creates more code to review. - -Code is cheap. The hard part is asking a useful question and knowing what the answer proves. \ No newline at end of file diff --git a/content/blog/code-doesnt-have-to-ship.md b/content/blog/code-doesnt-have-to-ship.md new file mode 100644 index 0000000..6d76914 --- /dev/null +++ b/content/blog/code-doesnt-have-to-ship.md @@ -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 +--- +At [AI Engineer Miami](https://www.ai.engineer/miami) and [React Miami](https://www.reactmiami.com/) in April, a conversation with [https://x.com/tannerlinsley](https://x.com/tannerlinsley) 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. diff --git a/src/styles/globals.css b/src/styles/globals.css index d604740..f3c5934 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -313,7 +313,7 @@ h3 { text-decoration-thickness: 0.13em; } -.blog-prose .inline-profile { +.blog-prose a.inline-profile { display: inline-flex; gap: 0.32em; align-items: center; @@ -323,7 +323,7 @@ h3 { font-size: 0.9em; line-height: 1.4; vertical-align: -0.18em; - color: var(--foreground); + color: var(--primary); white-space: nowrap; text-decoration: none; background: color-mix(in srgb, var(--primary) 8%, var(--card)); From 12bf00099047d08c56d6d354251d05c1d3c40eea Mon Sep 17 00:00:00 2001 From: Kaylee <65376239+KayleeWilliams@users.noreply.github.com> Date: Sun, 12 Jul 2026 21:53:09 +0100 Subject: [PATCH 3/3] fix(blog): improve opening line wrap --- content/blog/code-doesnt-have-to-ship.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/content/blog/code-doesnt-have-to-ship.md b/content/blog/code-doesnt-have-to-ship.md index 6d76914..ff6f6c1 100644 --- a/content/blog/code-doesnt-have-to-ship.md +++ b/content/blog/code-doesnt-have-to-ship.md @@ -8,7 +8,7 @@ tags: - experiments draft: false --- -At [AI Engineer Miami](https://www.ai.engineer/miami) and [React Miami](https://www.reactmiami.com/) in April, a conversation with [https://x.com/tannerlinsley](https://x.com/tannerlinsley) left me with a question: what if we used coding models to test new shapes for a product, not just clear its existing backlog? +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.