This repository was archived by the owner on May 15, 2026. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 3.4k
MKT-74: Add blog PostHog analytics events and fix acquisition attribution #11021
Closed
Closed
Changes from all commits
Commits
Show all changes
14 commits
Select commit
Hold shift + click to select a range
b7fb063
feat(blog): add markdown content pipeline with PT scheduling
roomote e0c0dd6
content(blog): add first blog post - PRDs Are Becoming Artifacts of t…
roomote fff8e13
Merge branch 'feature/MKT-73-first-blog-post' into feature/MKT-68-blo…
roomote 1bfa919
feat(blog): add /blog index page with dynamic rendering
roomote aa4c2d9
feat(blog): add /blog/[slug] post page with dynamic rendering
roomote 6f78205
feat(blog): add Schema.org JSON-LD structured data for SEO
roomote bd1b3ad
feat(blog): add PostHog analytics events for blog pages
roomote bfa4c67
fix: add attribution tracking and correct blog event names
roomote 9dabb71
Update apps/web-roo-code/src/app/blog/[slug]/page.tsx
mp-roocode d23681a
Update apps/web-roo-code/src/components/blog/blog-analytics.tsx
mp-roocode dacbccb
Update apps/web-roo-code/src/app/blog/[slug]/page.tsx
mp-roocode 610599f
Update apps/web-roo-code/src/components/blog/blog-analytics.tsx
mp-roocode 2528775
fix(blog): prevent duplicate blog_post_time_spent events
roomote 26a47e1
fix(blog): use direct imports in client component to avoid Node.js fs…
roomote File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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. |
78 changes: 78 additions & 0 deletions
78
apps/web-roo-code/content/blog/prds-are-becoming-artifacts-of-the-past.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,206 @@ | ||
| 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, | ||
| publish_time_pt: post.publish_time_pt, | ||
| }} | ||
| /> | ||
|
mp-roocode marked this conversation as resolved.
|
||
|
|
||
| <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> | ||
| </> | ||
| ) | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.