Skip to content
Open
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
2 changes: 2 additions & 0 deletions apps/web-roo-code/content/blog/.gitkeep
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# This directory contains blog post markdown files.
# See docs/blog.md for the specification.
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
---
title: "PRDs Are Becoming Artifacts of the Past"
slug: prds-are-becoming-artifacts-of-the-past
description: "The velocity of AI change has outpaced the traditional PRD's shelf life. Teams are shifting to prototype-first workflows where documentation emerges from shipped work rather than preceding it."
tags:
- product-management
- ai-workflows
- engineering-velocity
status: published
publish_date: 2026-01-12
publish_time_pt: 9:00am
---

Forty-seven pages. Three months of stakeholder reviews. One product requirements document.

By the time it shipped, the model it described was two generations behind.

## The document that ages out

You've seen this loop. A PM spends weeks gathering requirements, aligning stakeholders, formatting sections. The PRD becomes a ceremony: cover page, executive summary, user stories, acceptance criteria, and fourteen appendices.

Then Claude 4 ships. Or the API you were planning around deprecates. Or your team discovers a workflow that makes half the document irrelevant.

The PRD that was perfect in January is not perfect in February.

This is what Paige Bailey observed at Google: teams are abandoning the rigorous PRD process not because they've gotten sloppy, but because the velocity of change has outpaced the document's shelf life.

> "Now things are moving so fast that even if you had a PRD that was perfect as of January, it would not be perfect as of February."
>
> Paige Bailey, [Office Hours S01E15](https://www.youtube.com/watch?v=sAFQIqmDFL4)

## The shift: prototypes as specification

The replacement is not "no documentation." The replacement is documentation that emerges from working software.

Instead of writing a spec and then building, teams build something small and iterate against real feedback. The prototype becomes the specification. The commit history becomes the decision log. The PR comments become the rationale.

> "For software, it's much more effective to get something out and keep iterating on it really really quickly."
>
> Paige Bailey, [Office Hours S01E15](https://www.youtube.com/watch?v=sAFQIqmDFL4)

This works because AI tooling has compressed the cost of producing working code. When you can generate a functional prototype in an afternoon, the economics of "plan first, build second" flip. The sunk cost of writing a detailed PRD becomes harder to justify when you could have shipped the first version instead.

## The tradeoff is real

Abandoning upfront planning does not mean abandoning coordination. Teams still need to align on scope, surface constraints, and communicate with stakeholders.

The difference is when that alignment happens. Waterfall-style PRDs front-load alignment before any code exists. Prototype-first workflows back-load alignment: you ship something, learn what breaks, and document the decisions retrospectively.

This approach has failure modes:

**Scope creep without anchors.** If there's no initial constraint document, the prototype can drift in directions that no stakeholder wanted.

**Lost rationale.** If you don't capture why decisions were made, you lose institutional memory. This matters when teammates leave or when you need to revisit a choice six months later.

**Stakeholder whiplash.** Executives who expect a polished plan before greenlighting work may not trust a "we'll figure it out as we build" pitch.

The mitigation is lightweight decision records: ADRs, RFC-style docs, or even structured commit messages that capture the why, not just the what. The goal is not zero documentation. The goal is documentation that emerges from shipped work rather than preceding it.

## Why this matters for your team

For a Series A team with five engineers, a three-week PRD cycle is a significant tax. That's three weeks where no code ships while stakeholders negotiate requirements that will change anyway.

If your team is shipping 8-10 PRs a week, the prototype-first model lets you compress the feedback loop. Instead of aligning on a document and then discovering problems in production, you discover problems in the prototype and align on fixes that already work.

The compounding effect: teams that treat prototypes as the specification ship more iterations per quarter. Teams that cling to waterfall-style documentation lose velocity to teams that iterate against real user feedback.

> "I also feel like we don't have nearly as rigorous of a process around PRDs. Like PRDs kind of feel like an artifact of the past."
>
> Paige Bailey, [Office Hours S01E15](https://www.youtube.com/watch?v=sAFQIqmDFL4)

## The decision

The question is not "should we have documentation?" The question is "when does documentation happen?"

If your PRDs take longer to write than your prototypes take to ship, the economics have flipped. Start with a working prototype. Capture decisions as you make them. Align stakeholders around something they can touch, not something they have to imagine.

The artifact that matters is the diff, not the document.
1 change: 1 addition & 0 deletions apps/web-roo-code/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"embla-carousel-autoplay": "^8.6.0",
"embla-carousel-react": "^8.6.0",
"framer-motion": "12.15.0",
"gray-matter": "^4.0.3",
"lucide-react": "^0.518.0",
"next": "~15.2.8",
"next-themes": "^0.4.6",
Expand Down
205 changes: 205 additions & 0 deletions apps/web-roo-code/src/app/blog/[slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
import type { Metadata } from "next"
import { notFound } from "next/navigation"
import Link from "next/link"
import ReactMarkdown from "react-markdown"
import remarkGfm from "remark-gfm"
import { ArrowLeft } from "lucide-react"
import { SEO } from "@/lib/seo"
import { ogImageUrl } from "@/lib/og"
import {
getBlogPostBySlug,
formatPostDatePt,
getArticleStructuredData,
getBlogPostBreadcrumbStructuredData,
} from "@/lib/blog"
import { BlogPostAnalytics } from "@/components/blog/blog-analytics"

// Force dynamic rendering to evaluate publish gating at request-time
export const dynamic = "force-dynamic"

// Require Node.js runtime for filesystem reads
export const runtime = "nodejs"

interface BlogPostPageProps {
params: Promise<{ slug: string }>
}

export async function generateMetadata({ params }: BlogPostPageProps): Promise<Metadata> {
const { slug } = await params
const post = getBlogPostBySlug(slug)

if (!post) {
return {
title: "Post Not Found",
}
}

const PATH = `/blog/${post.slug}`

return {
title: post.title,
description: post.description,
alternates: {
canonical: `${SEO.url}${PATH}`,
},
openGraph: {
title: post.title,
description: post.description,
url: `${SEO.url}${PATH}`,
siteName: SEO.name,
images: [
{
url: ogImageUrl(post.title, post.description),
width: 1200,
height: 630,
alt: post.title,
},
],
locale: SEO.locale,
type: "article",
publishedTime: post.publish_date,
},
twitter: {
card: SEO.twitterCard,
title: post.title,
description: post.description,
images: [ogImageUrl(post.title, post.description)],
},
keywords: [...SEO.keywords, ...post.tags],
}
}

export default async function BlogPostPage({ params }: BlogPostPageProps) {
const { slug } = await params
const post = getBlogPostBySlug(slug)

// Return 404 if post not found or not published
if (!post) {
notFound()
}

// Generate structured data for SEO
const articleSchema = getArticleStructuredData(post)
const breadcrumbSchema = getBlogPostBreadcrumbStructuredData(post)

return (
<>
{/* JSON-LD Structured Data */}
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(articleSchema) }} />
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(breadcrumbSchema) }} />

{/* PostHog Analytics */}
<BlogPostAnalytics
post={{
slug: post.slug,
title: post.title,
description: post.description,
tags: post.tags,
publish_date: post.publish_date,
}}
/>

<div className="container mx-auto px-4 py-12 sm:px-6 lg:px-8">
<article className="mx-auto max-w-3xl">
{/* Back link */}
<Link
href="/blog"
className="mb-8 inline-flex items-center text-sm text-muted-foreground hover:text-foreground">
<ArrowLeft className="mr-2 size-4" />
Back to Blog
</Link>

{/* Post Header */}
<header className="mb-8">
<h1 className="text-3xl font-bold tracking-tight sm:text-4xl md:text-5xl">{post.title}</h1>
<div className="mt-4 flex flex-wrap items-center gap-4 text-sm text-muted-foreground">
<time dateTime={post.publish_date}>Posted {formatPostDatePt(post.publish_date)}</time>
{post.tags.length > 0 && (
<>
<span className="text-border">•</span>
<div className="flex flex-wrap gap-2">
{post.tags.map((tag) => (
<span key={tag} className="rounded-full bg-muted px-2 py-0.5 text-xs">
{tag}
</span>
))}
</div>
</>
)}
</div>
</header>

{/* Post Content */}
<div className="prose prose-lg max-w-none dark:prose-invert">
<ReactMarkdown
remarkPlugins={[remarkGfm]}
components={{
// Do not allow raw HTML - all components render safely
h1: ({ ...props }) => (
<h1 className="text-2xl font-bold tracking-tight sm:text-3xl" {...props} />
),
h2: ({ ...props }) => <h2 className="mt-10 text-xl font-bold sm:text-2xl" {...props} />,
h3: ({ ...props }) => <h3 className="mt-8 text-lg font-semibold" {...props} />,
a: ({ ...props }) => (
<a
className="text-primary hover:underline"
target="_blank"
rel="noopener noreferrer"
{...props}
/>
),
blockquote: ({ ...props }) => (
<blockquote
className="border-l-4 border-primary/50 pl-4 italic text-muted-foreground"
{...props}
/>
),
code: ({ className, children, ...props }) => {
// Check if this is an inline code or code block
const isInline = !className
if (isInline) {
return (
<code
className="rounded bg-muted px-1.5 py-0.5 text-sm font-mono"
{...props}>
{children}
</code>
)
}
return (
<code className={className} {...props}>
{children}
</code>
)
},
pre: ({ ...props }) => (
<pre
className="overflow-x-auto rounded-lg bg-muted p-4 text-sm font-mono"
{...props}
/>
),
ul: ({ ...props }) => <ul className="list-disc pl-6" {...props} />,
ol: ({ ...props }) => <ol className="list-decimal pl-6" {...props} />,
li: ({ ...props }) => <li className="mt-2" {...props} />,
p: ({ ...props }) => <p className="mt-4 leading-7" {...props} />,
strong: ({ ...props }) => <strong className="font-semibold" {...props} />,
hr: ({ ...props }) => <hr className="my-8 border-border" {...props} />,
}}>
{post.content}
</ReactMarkdown>
</div>

{/* Footer */}
<footer className="mt-12 border-t border-border pt-8">
<Link
href="/blog"
className="inline-flex items-center text-sm text-muted-foreground hover:text-foreground">
<ArrowLeft className="mr-2 size-4" />
Back to Blog
</Link>
</footer>
</article>
</div>
</>
)
}
Loading
Loading