Skip to content

Add /blog with initial positioning posts - #800

Merged
kody-bot merged 5 commits into
mainfrom
cursor/kody-blog-positioning-posts-5c02
Jul 20, 2026
Merged

kody-bot merged 5 commits into
mainfrom
cursor/kody-blog-positioning-posts-5c02

Conversation

@kentcdodds

@kentcdodds kentcdodds commented Jul 20, 2026 •

Copy link
Copy Markdown
Member

Adds a public /blog to heykody.dev and publishes seven initial positioning posts by Kent C. Dodds.

What's here

Engineering

  • Public /blog index and /blog/:slug post pages in the Remix 3 client, following the existing public-route patterns (privacy for the static index shape, community-detail for dynamic pages with 404 + OgHead + loaderData hydration). .json API endpoints back SPA navigation like community does.
  • Posts are markdown files with frontmatter in packages/worker/src/blog/posts/, bundled into the Worker via a Wrangler Text module rule (plus a tiny Vite plugin so vitest resolves .md as text). A small typed catalog (catalog.ts) parses frontmatter (title, date, description, order) and sorts date desc, then order asc. Adding a post = drop a .md file + one import/entry in the catalog.
  • Per-post Open Graph images at /blog/:slug/og.png using the existing Satori primitives in packages/worker/src/og/ (lazy-imported renderer per the sanctioned cold-start pattern), and a static blog entry in the OG page registry for the index.
  • RSS 2.0 feed at /blog/rss.xml (escaped, summary descriptions, <link rel="alternate"> on the index).
  • "Blog" nav link next to Community.
  • Unit tests for frontmatter parsing, catalog enumeration (no hardcoded slugs), RSS escaping, and post OG handler; e2e/og-images.spec.ts extended for /blog and post OG images.

Content (7 posts, author Kent C. Dodds)

  1. Your assistant's home - the manifesto: what accumulates (memory, keys, packages, jobs, inbox) and why portability across MCP hosts matters.
  2. Gateways connect. Homes accumulate. - respectful contrast with MCP gateways: gateways solve tool connection for teams; Kody is the durable personal runtime.
  3. The personal assistant that can't leak your keychain - security architecture: sandboxed execute, {{secret:name}} resolution at the fetch gateway, per-user Durable Object isolation, honest limits.
  4. Why your assistant's home should make zero inference calls - free-agent economics: the host you pay for does the reasoning; Kody sells durable capability, not tokens.
  5. Every install is a fork you own - the ownership ladder: install, read, change, schedule, publish back.
  6. The automations you never got around to building - the real competitor is non-consumption; delegate the recurring thing in one conversation.
  7. Self-service means ownership - why you bring your own OAuth apps and keys, with the trade-offs named honestly.

All posts were drafted against the Kent writing voice skill (blog register, ASCII punctuation, no em dashes) and reviewed against the positioning canon. No fabricated quotes, users, or metrics; the only numbers cited are Kent's public dogfooding stats from the June 2026 "I built my own OpenClaw" video (42 jobs, 55 packages, 51 secrets, 11 OAuth integrations, 72 built-in capabilities) and the $0/$5 plan prices already shown in the product UI. Every post links to the signup/waitlist page and relevant docs.

Orchestration note

Work was delegated per instructions: implementation to a Grok 4.5 subagent (cursor-grok-4.5-high) and writing to Sonnet 5 subagents. "Sonnet 5" was not an exact available model slug in this environment; the nearest available slug used was claude-fable-5-thinking-high.

Validation

Full npm run validate passes locally (format, lint, typecheck, 1010 unit tests, Playwright e2e, MCP e2e). Also smoke-tested against npm run dev: /blog (200, lists all 7 posts), /blog/:slug (200, SSR'd body + OG meta), /blog/:slug/og.png (200, image/png, verified visually), /blog/rss.xml (200, valid escaped XML), unknown slug (404).

System recap — extends app-ui (medium risk)

Mode: recap · Base: main @ 07e85d84 · Head: a70da330

Classification: extends — the browser app / route surface gains a new public section (/blog index, post pages, post OG images, RSS). New blog content module under packages/worker/src/blog/ composes existing OG and SSR primitives; no primitives added to the map (repo-backed static content, no storage or auth changes).

Primitives touched

Primitive Group Impact
app-ui surfaces extends — new public routes blog, blogPost, blogPostOgImage, blogRss (+ .json APIs), handlers, client routes, nav link

System map

A request for a blog page flows from the route contract through new blog handlers into the markdown catalog, reusing the existing SSR document and Satori OG renderer.

Legend: green = composes (wiring only) · amber = extended by this PR · red = new primitive · gray = context (unchanged, included only when an edge crosses it).

flowchart LR
	appUi["app-ui<br/>Browser app (Remix 3)"]:::extended
	blogCatalog["blog catalog<br/>markdown posts (bundled Text modules)"]:::touched
	ogRenderer["og renderer<br/>Satori og-image primitives"]:::untouched
	appUi -->|"/blog + /blog/:slug handlers read posts"| blogCatalog
	appUi -->|"/blog/:slug/og.png + /og/blog.png"| ogRenderer
	appUi -->|"/blog/rss.xml built from catalog"| blogCatalog
	classDef touched fill:#1a7f37,color:#fff
	classDef extended fill:#9a6700,color:#fff
	classDef added fill:#cf222e,color:#fff
	classDef untouched fill:#57606a,color:#fff
Loading

Invariants

No auth or per-user data involved: all blog routes are public, read-only, and serve repo-bundled content. No userId-scoped paths touched.

Open in Web Open in Cursor 

Summary by CodeRabbit

  • New Features
    • Added a public blog experience with an index and individual post pages, including a new “Blog” navigation entry and improved post date/author display.
    • Added an RSS feed for blog posts.
    • Added Open Graph metadata and generated OG PNG images for the blog landing page and each post.
  • Bug Fixes
    • Improved robustness for missing/invalid blog slugs and error states when loading blog content and OG images.
  • Tests
    • Expanded unit, routing, and E2E coverage for blog pages, RSS output, and OG image/meta responses.

@coderabbitai

coderabbitai Bot commented Jul 20, 2026 •

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@cursor[bot], you've reached your PR review limit, so we couldn't start this review.

Next review available in: 16 seconds

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 0b80cf78-fe67-47a3-a13d-eee0ba7581da

📥 Commits

Reviewing files that changed from the base of the PR and between d56920b and 42f6c2a.

📒 Files selected for processing (1)
  • packages/worker/client/routes/blog-post.tsx
📝 Walkthrough

Walkthrough

This change adds a Markdown-backed blog with catalog parsing, seven posts, HTML and JSON routes, RSS output, post OG images, client navigation and rendering, public blog OG metadata, and E2E/unit coverage.

Changes

Blog publishing and delivery

Layer / File(s) Summary
Content catalog and feed
packages/worker/src/blog/*, packages/worker/src/blog/posts/*, tools/vite-markdown-as-text.ts, vitest*.ts, packages/worker/wrangler.jsonc
Markdown frontmatter is validated and parsed into an ordered catalog, summaries, and escaped RSS XML. Seven blog posts and Markdown loading support are added with corresponding tests.
Server blog routes
packages/worker/src/app/routes.ts, packages/worker/src/app/router.ts, packages/worker/src/app/handlers/blog.tsx, packages/worker/src/app/loader-data.ts
Blog page, API, post, RSS, and OG image routes are registered with HTML/JSON responses, SSR loader data, metadata, and cache headers.
OG image delivery
packages/worker/src/blog/og-image.ts, packages/worker/src/og/pages.ts, packages/worker/src/app/handlers/blog.tsx, e2e/og-images.spec.ts, packages/worker/src/og/page-image.node.test.ts, packages/worker/src/app/handlers/blog-post-og-image.node.test.ts
Blog page and post OG images are rendered as PNGs, exposed through public metadata and post endpoints, and verified through unit and E2E assertions.
Client blog pages
packages/worker/client/routes/blog.tsx, packages/worker/client/routes/blog-post.tsx, packages/worker/src/app/blog-display.ts, packages/worker/tsconfig-client.json
Client routes load blog data, manage loading/error/not-found states, render post lists and Markdown content, and format dates and author metadata.
Navigation and route wiring
packages/worker/client/app.tsx, packages/worker/client/routes/index.tsx
The top navigation links to /blog, and client loaders/renderers are registered for the blog index and blog post routes.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Browser
  participant BlogRoute
  participant blogRouteLoader
  participant BlogApiHandler
  participant BlogCatalog
  Browser->>BlogRoute: navigate to /blog
  BlogRoute->>blogRouteLoader: load blog data
  blogRouteLoader->>BlogApiHandler: request /blog.json
  BlogApiHandler->>BlogCatalog: listBlogPosts()
  BlogCatalog-->>BlogApiHandler: post summaries
  BlogApiHandler-->>blogRouteLoader: JSON payload
  blogRouteLoader-->>BlogRoute: loader data
  BlogRoute-->>Browser: render blog post links
Loading

Possibly related PRs

  • kentcdodds/kody#608: Introduced the loader-driven client navigation framework used by the new blog routes.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 12.90% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: adding a new /blog section with initial positioning posts.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch cursor/kody-blog-positioning-posts-5c02

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@kody-bot
kody-bot marked this pull request as ready for review July 20, 2026 06:25
@github-actions

github-actions Bot commented Jul 20, 2026 •

Copy link
Copy Markdown
Contributor

🔎 Preview deployed: https://kody-pr-800.kody-a99.workers.dev

Worker: kody-pr-800
D1: kody-pr-800-db
KV: kody-pr-800-oauth-kv

Mocks:

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/worker/client/routes/blog-post.tsx`:
- Around line 69-93: Update loadPost and its queueTask callback to accept and
propagate an AbortSignal through the blog-post request, following the existing
app.tsx session-refresh queueTask pattern. Pass the signal to fetch and ensure
superseded requests are aborted or ignored before updating post or status,
preserving the latest slug/href result.

In `@packages/worker/client/routes/blog.tsx`:
- Around line 47-98: Update packages/worker/client/routes/blog.tsx#L47-L98 and
packages/worker/client/routes/blog-post.tsx#L69-L129: accept the AbortSignal in
each handle.queueTask callback, thread it through loadBlog/loadPost into fetch,
and return immediately after fetch resolves if the signal is aborted. Ensure
aborted tasks do not modify posts, post, or status and do not mark the load
latch as loaded or failed.
- Around line 47-65: The blog route’s queued load drops cancellation, allowing
stale fetches and state updates after navigation. Update the
loadBlog/handle.queueTask flow to accept the provided AbortSignal, pass it to
fetch, and check for abortion before applying posts/status or reporting task
success/failure, preserving only updates for the active request.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 2ddc7301-8bdc-4e23-a017-002aee708fbf

📥 Commits

Reviewing files that changed from the base of the PR and between aca4cf8 and a70da33.

📒 Files selected for processing (31)
  • e2e/og-images.spec.ts
  • packages/worker/client/app.tsx
  • packages/worker/client/routes/blog-post.tsx
  • packages/worker/client/routes/blog.tsx
  • packages/worker/client/routes/index.tsx
  • packages/worker/src/app/blog-display.ts
  • packages/worker/src/app/handlers/blog-post-og-image.node.test.ts
  • packages/worker/src/app/handlers/blog.tsx
  • packages/worker/src/app/loader-data.ts
  • packages/worker/src/app/router.ts
  • packages/worker/src/app/routes.ts
  • packages/worker/src/blog/catalog.node.test.ts
  • packages/worker/src/blog/catalog.ts
  • packages/worker/src/blog/markdown.d.ts
  • packages/worker/src/blog/og-image.ts
  • packages/worker/src/blog/parse-frontmatter.ts
  • packages/worker/src/blog/posts/every-install-is-a-fork-you-own.md
  • packages/worker/src/blog/posts/gateways-connect-homes-accumulate.md
  • packages/worker/src/blog/posts/self-service-means-ownership.md
  • packages/worker/src/blog/posts/the-assistant-that-cant-leak-your-keychain.md
  • packages/worker/src/blog/posts/the-automations-you-never-built.md
  • packages/worker/src/blog/posts/your-assistants-home.md
  • packages/worker/src/blog/posts/zero-inference-calls.md
  • packages/worker/src/blog/rss.ts
  • packages/worker/src/og/page-image.node.test.ts
  • packages/worker/src/og/pages.ts
  • packages/worker/tsconfig-client.json
  • packages/worker/wrangler.jsonc
  • tools/vite-markdown-as-text.ts
  • vitest-shared.ts
  • vitest.config.ts

Comment thread packages/worker/client/routes/blog-post.tsx Outdated
Comment thread packages/worker/client/routes/blog.tsx Outdated
Comment thread packages/worker/client/routes/blog.tsx Outdated
Comment on lines +47 to +98
async function loadBlog() {
status = 'loading'
handle.update()
try {
const response = await fetch(blogApiPath, {
headers: { Accept: 'application/json' },
})
const payload = await readJson<BlogLoaderData>(response)
if (!response.ok || !payload?.ok) {
throw new Error('Unable to load blog posts.')
}
posts = payload.posts
status = 'ready'
handle.update()
} catch {
status = 'error'
handle.update()
}
}

return () => {
const currentHref = readCurrentRouterHref(handle)
if (!isBlogIndexPath(currentHref)) {
return <section mix={css(pageCss)} />
}

const routeData = tryConsumeRouteLoaderData(handle, 'blog', currentHref)
const appliedRouteData = Boolean(routeData?.ok)
if (routeData?.ok) {
posts = routeData.posts
status = 'ready'
loadLatch.markLoaded(currentHref)
}

const needsStaleRefresh = consumeStaleNavigationData(currentHref)
const needsLoad = loadLatch.needsLoad({
currentHref,
isLoading: status === 'loading',
appliedRouteData,
needsStaleRefresh,
})
if (needsLoad && typeof document !== 'undefined') {
handle.queueTask(async () => {
try {
await loadBlog()
if (status === 'ready') loadLatch.markLoaded(currentHref)
else loadLatch.markFailed(currentHref)
} catch {
loadLatch.markFailed(currentHref)
}
})
}

@coderabbitai coderabbitai Bot Jul 20, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Client-side retry loaders don't cancel superseded fetches. handle.queueTask appears to provide an AbortSignal to its callback — app.tsx's session-refresh usage (handle.queueTask(async (signal) => { ...; if (signal.aborted) return })) relies on it — but both blog route files omit it entirely, so a stale fetch can resolve after a newer navigation and overwrite posts/post/status with outdated data, or incorrectly mark a still-valid href as failed in the load latch.

  • packages/worker/client/routes/blog.tsx#L47-L98: accept signal in the queueTask callback, pass it into loadBlog's fetch, and bail out (without touching status/posts or the load latch) if signal.aborted once the fetch resolves.
  • packages/worker/client/routes/blog-post.tsx#L69-L129: same fix — thread the queueTask signal into loadPost's fetch and check signal.aborted before committing post/status/load-latch updates.
📍 Affects 2 files
  • packages/worker/client/routes/blog.tsx#L47-L98 (this comment)
  • packages/worker/client/routes/blog-post.tsx#L69-L129
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/worker/client/routes/blog.tsx` around lines 47 - 98, Update
packages/worker/client/routes/blog.tsx#L47-L98 and
packages/worker/client/routes/blog-post.tsx#L69-L129: accept the AbortSignal in
each handle.queueTask callback, thread it through loadBlog/loadPost into fetch,
and return immediately after fetch resolves if the signal is aborted. Ensure
aborted tasks do not modify posts, post, or status and do not mark the load
latch as loaded or failed.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 8697977: both queueTask callbacks now accept the provided AbortSignal, thread it into the loadBlog/loadPost fetches, and bail before committing posts/post/status or load-latch updates when aborted (including the catch paths, so an abort never sets status = 'error').

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Skipped: comment is from another GitHub bot.

Comment thread packages/worker/client/routes/blog-post.tsx

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit d56920b. Configure here.

Comment thread packages/worker/client/routes/blog-post.tsx
@kody-bot
kody-bot merged commit 8e9723d into main Jul 20, 2026
5 checks passed
@kody-bot
kody-bot deleted the cursor/kody-blog-positioning-posts-5c02 branch July 20, 2026 07:49
@coderabbitai coderabbitai Bot mentioned this pull request Jul 20, 2026
4 tasks done
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants