<

Anne Mariel

/>

Full Stack Developer

<- Back to blog
3 min readAnne Mariel Catapang

How I built this blog: a coding agent that argues back

I asked an AI agent for a blog page, and got interrogated instead. Here is what that produced: Teable as the data layer, and one bug caught before it shipped.

  • pi
  • teable
  • nextjs
How I built this blog: a coding agent that argues back

I wanted a blog page. What I got first was an interrogation, and the page was better for it.

The agent interviewed me before it wrote anything

I built this with pi, a coding agent, running a "grilling" skill. It asks one question at a time, and recommends an answer.

Here are some of the useful arguments I came across with: Where does the content live? Should drafts exist? What happens to an unfinished post? I answered maybe a dozen of these.

Teable as the data layer

Posts are not files in this repo. They are records in a Teable base called annemariel.com, in a table called Blog — title, slug, status, excerpt, content, cover image, tags, category, SEO fields, and reading time.

The appeal is that publishing is not a deploy. I flip a record's status from Draft to Published, and the site picks it up. Next.js revalidates hourly, so pages stay static and CDN-fast while the content stays editable from a spreadsheet-shaped UI.

The whole integration is one module. Nothing else in the codebase knows Teable exists:

export async function getPostBySlug(slug: string): Promise<Post | null> {
  return (await getPosts()).find((post) => post.slug === slug) ?? null;
}

Status handling has one nice property: in development every post is visible, including drafts and in-review pieces. In production, only Published. Writing in progress is always one environment away from being seen, and never one mistake away.

The bug that would have shipped

Here is the part I'm glad I caught before deploying.

Teable's attachment fields hand you a presignedUrl for each image. It works perfectly until it expires. Bake that URL into a static page and your cover images are fine on launch day and broken a few days later. Worse for Open Graph: social platforms cache the image URL they scrape, so a dead link propagates into every share of the post.

The fix is to never let the temporary URL reach the browser. Each cover is served from a stable path on my own domain, and a small route handler resolves a fresh presigned URL server-side and streams the bytes back:

const source = await getCoverSource(params.slug);
const upstream = await fetch(source);
 
return new Response(upstream.body, {
  headers: {
    'Content-Type': upstream.headers.get('content-type') ?? 'image/png',
    'Cache-Control': 'public, s-maxage=86400, stale-while-revalidate=604800',
  },
});

The expiring URL stays server-side. The public URL never changes. The CDN caches the bytes, so this is not a function call per visitor.

That is the real output of an hour of being grilled: a shorter list of things I actually built, and a much clearer idea of why each one is on it.