Skip to content

feat(api): add GET /content/post/:slug endpoint#174

Merged
fennifith merged 4 commits into
playfulprogramming:mainfrom
bbornino:feat/post-by-slug-endpoint
Jul 4, 2026
Merged

feat(api): add GET /content/post/:slug endpoint#174
fennifith merged 4 commits into
playfulprogramming:mainfrom
bbornino:feat/post-by-slug-endpoint

Conversation

@bbornino

@bbornino bbornino commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Closes #80.

Adds GET /content/post/:slug?locale=en (default en), returning a single post's data along with its author list and "chapter list."

Response shape

  • Post fields: slug, title, description, banner/social image URLs, word count, published date
  • authors: id, name, profile image URL (joined via post_authors)
  • chapters: sibling posts in the same collection, ordered by collectionOrder, each flagged with isCurrent

There's no chapters table in the schema — "chapters" are just sibling posts sharing a collectionSlug, which is how collections.ts already treats them (chapterCount).

publishedAt filtering

post_data.publishedAt is nullable at the schema level, but every current write path (sync-post) always sets it, so there's no existing filtering precedent anywhere else in the codebase. I added filtering here anyway:

  • The main post returns 404 if its post_data row has publishedAt: null for the requested locale (same 404 as "no row for locale").
  • The chapters list excludes any sibling whose own post_data row has publishedAt: null — otherwise a published chapter's response would leak the title/slug of unpublished siblings in the same collection, which nothing else in the codebase currently exposes.

This is filtered at the app level (post-fetch) rather than in the Drizzle where clause — fine for a single-row + small-sibling-list endpoint like this; could move to a DB-level filter later if collections grow large enough for it to matter.

Flagging this as a judgment call in case the intended gate is different (e.g. drafts visible to authenticated authors) — happy to adjust based on feedback.

Testing

  • New post.test.ts: 6 tests covering happy path (with chapters + isCurrent), no-collection case, missing-locale-data 404, unpublished-main-post 404, unpublished-sibling-excluded, and post-not-found 404.
  • pnpm test:unit: all 24 targets pass.
  • pnpm prettier (check): clean except a pre-existing, unrelated flag on .claude/settings.local.json.

Summary by CodeRabbit

  • New Features
    • Added a new GET /content/post/:slug endpoint that returns a localized post, author details, and (when applicable) the collection’s chapter list.
  • Bug Fixes
    • Returns 404 { error: "Post not found" } when the post is missing or unpublished for the requested locale, and omits collection data when the post isn’t in a collection.
    • Only includes published chapters and orders them by collectionOrder.
  • Tests
    • Added route-level tests for the endpoint and updated test mocks to support post lookups.

Closes playfulprogramming#80. Returns a single post's data, author list, and collection
chapter list (sibling posts ordered by collectionOrder, with the
current post flagged via isCurrent), following collections.ts's
schema and query conventions.

Filters out unpublished posts/chapters (post_data.publishedAt: null)
at both the main lookup and the chapter list, to avoid leaking draft
slugs and titles via the chapter list to anyone who doesn't already
know they exist.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 30, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@fennifith, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 55 minutes

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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 55c40482-e5d0-4a9a-be88-ec71ff4129f8

📥 Commits

Reviewing files that changed from the base of the PR and between 01fb43e and 20d8cc1.

📒 Files selected for processing (1)
  • apps/api/test-utils/setup.ts
📝 Walkthrough

Walkthrough

Adds a new Fastify GET /content/post/:slug endpoint that returns localized post data with authors and collection chapters, registers it in the app, and adds tests plus a new mock for the post lookup query.

Changes

Post endpoint

Layer / File(s) Summary
Request/response schemas
apps/api/src/routes/content/post.ts
TypeBox schemas define the slug params, locale query, 200 response body, and 404 error body.
Route registration
apps/api/src/routes/content/post.ts
Defines the Fastify plugin and registers GET /content/post/:slug with schema metadata.
Handler logic
apps/api/src/routes/content/post.ts
Loads localized post data, authors, and collection posts; returns 404 for missing or unpublished posts; formats the response with image URLs, ISO timestamps, and sorted collection chapters.
App registration
apps/api/src/createApp.ts
Imports postRoutes and registers it with the Fastify app.
Tests and mock setup
apps/api/src/routes/content/post.test.ts, apps/api/test-utils/setup.ts
Extends the database mock with posts.findFirst and adds route tests covering success, filtering, sorting, and 404 responses.

Estimated code review effort: 2 (Simple) | ~15 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant postRoutes
  participant Database

  Client->>postRoutes: GET /content/post/:slug?locale
  postRoutes->>Database: db.query.posts.findFirst(...)
  Database-->>postRoutes: post record or undefined
  postRoutes->>postRoutes: validate publishedAt and build chapters
  postRoutes-->>Client: 200 post data or 404 Post not found
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly names the new GET /content/post/:slug endpoint.
Linked Issues check ✅ Passed The PR implements the post lookup, author list, chapter list, locale handling, and 404 behavior requested by the linked issue.
Out of Scope Changes check ✅ Passed The changes are limited to the new route, its tests, and supporting test setup with no unrelated additions.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@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: 2

🤖 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 `@apps/api/src/routes/content/post.ts`:
- Line 128: The authors mapping in the post response is currently relying on
database return order, which is not stable. Update the post retrieval/mapping in
the post route to add an explicit orderBy for authors (or otherwise define a
deterministic sort) when populating post.authors, using the existing authors
relation in the response shape so the author order is consistent.
- Around line 114-143: The post query is pulling `data` rows only by locale,
which can return multiple versions for the same slug and make
`post.data[0]`/`chapter.data[0]` nondeterministic. Update the
`db.query.posts.findFirst` relations in `post.ts` so both the top-level `data`
and nested `collection.posts.data` filters include the intended `version`
alongside `locale`. Use the existing `post.data` and `chapter.data` relation
selectors to locate the fix.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 165375d1-9075-4126-b152-8e80943bf7e4

📥 Commits

Reviewing files that changed from the base of the PR and between 5b1c106 and 27a34d4.

📒 Files selected for processing (4)
  • apps/api/src/createApp.ts
  • apps/api/src/routes/content/post.test.ts
  • apps/api/src/routes/content/post.ts
  • apps/api/test-utils/setup.ts

Comment on lines +114 to +143
const post = await db.query.posts.findFirst({
where: { slug },
with: {
data: {
columns: {
title: true,
description: true,
bannerImage: true,
socialImage: true,
wordCount: true,
publishedAt: true,
},
where: { locale },
},
authors: { columns: { slug: true, name: true, profileImage: true } },
collection: {
with: {
posts: {
columns: { slug: true, collectionOrder: true },
with: {
data: {
columns: { title: true, publishedAt: true },
where: { locale },
},
},
},
},
},
},
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show the relevant route file with line numbers
sed -n '1,240p' apps/api/src/routes/content/post.ts

echo '--- FILES ---'
# Locate post_data schema and related usages
git ls-files | rg 'apps/api/src/.*(schema|db|table|drizzle|content).*|apps/api/src/routes/content|post_data|postData|version'

Repository: playfulprogramming/hoof

Length of output: 5021


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the route file structure to identify relevant functions/relations
ast-grep outline apps/api/src/routes/content/post.ts --view expanded || true

echo '--- SEARCH version filters/usages ---'
rg -n --hidden --glob '!**/node_modules/**' --glob '!**/dist/**' --glob '!**/build/**' \
  '\bversion\b|post_data|postData|published version|draft' apps/api/src || true

echo '--- SEARCH data relation patterns ---'
rg -n --hidden --glob '!**/node_modules/**' --glob '!**/dist/**' --glob '!**/build/**' \
  'with:\s*\{\s*data:\s*\{' apps/api/src || true

Repository: playfulprogramming/hoof

Length of output: 1863


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the schema and the route body in smaller slices.
echo '--- post.ts lines 100-170 ---'
sed -n '100,170p' apps/api/src/routes/content/post.ts

echo '--- schema candidates ---'
fd -a 'post*' apps/api/src || true
fd -a '*schema*' apps/api/src || true
fd -a '*table*' apps/api/src || true

Repository: playfulprogramming/hoof

Length of output: 2748


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- locate db schema and relation definitions ---'
git ls-files | rg '(^|/)(schema|schemas|table|tables|db|drizzle).*|packages/.*/src/.*(schema|table|db).*|.*post_data.*|.*postData.*|.*version.*'

echo '--- search for post_data / version definitions ---'
rg -n --hidden --glob '!**/node_modules/**' --glob '!**/dist/**' --glob '!**/build/**' \
  'post_data|postData|version: text\("version"\)|primaryKey\(\{ columns: \[table\.slug, table\.locale, table\.version\]\}\)|where: \{ locale \}|orderBy' .

echo '--- search for data relation in db package ---'
rg -n --hidden --glob '!**/node_modules/**' --glob '!**/dist/**' --glob '!**/build/**' \
  'relations\(|data:\s*\(|many\(.*post_data|one\(.*post_data|posts.*data' .

Repository: playfulprogramming/hoof

Length of output: 12920


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- packages/db/src/schema/posts.ts ---'
sed -n '1,220p' packages/db/src/schema/posts.ts

echo '--- packages/db/src/relations.ts ---'
sed -n '1,220p' packages/db/src/relations.ts

echo '--- apps/worker/src/tasks/sync-post/processor.ts (relevant slice) ---'
sed -n '120,220p' apps/worker/src/tasks/sync-post/processor.ts

echo '--- version assignments across repo ---'
rg -n --hidden --glob '!**/node_modules/**' --glob '!**/dist/**' --glob '!**/build/**' \
  '\bversion\s*:\s*["'\'']' apps packages | sed -n '1,200p'

Repository: playfulprogramming/hoof

Length of output: 5752


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- post.test.ts slices around fixtures and assertions ---'
sed -n '1,340p' apps/api/src/routes/content/post.test.ts

echo '--- search for postData version in tests/fixtures ---'
rg -n --hidden --glob '!**/node_modules/**' --glob '!**/dist/**' --glob '!**/build/**' \
  'postData|version:' apps/api/src/routes/content/post.test.ts apps/worker/src/tasks/sync-post/processor.test.ts apps/worker/test-utils/setup.ts

Repository: playfulprogramming/hoof

Length of output: 7989


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- search for parsed.version and content version semantics ---'
rg -n --hidden --glob '!**/node_modules/**' --glob '!**/dist/**' --glob '!**/build/**' \
  'parsed\.version|frontmatter.*version|version\s*:' apps/worker apps/api packages/post-images packages/db | sed -n '1,220p'

echo '--- inspect sync-post parser-related code ---'
fd -a 'processor.ts' apps/worker/src/tasks/sync-post
fd -a '*parse*' apps/worker/src/tasks/sync-post apps/worker/src | sed -n '1,120p'

Repository: playfulprogramming/hoof

Length of output: 1297


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- locate markdown parser / sync-post inputs ---'
rg -n --hidden --glob '!**/node_modules/**' --glob '!**/dist/**' --glob '!**/build/**' \
  'sync-post|parsed\.title|parsed\.published|parsed\.version|upToDateSlug|noindex|license' apps/worker/src | sed -n '1,260p'

Repository: playfulprogramming/hoof

Length of output: 1905


Filter post_data by version here. The worker writes parsed.version into post_data, so where: { locale } can return multiple rows for the same slug/locale. post.data[0] and chapter.data[0] then become order-dependent; add version: "" (or the published version you intend) to both data relations.

🤖 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 `@apps/api/src/routes/content/post.ts` around lines 114 - 143, The post query
is pulling `data` rows only by locale, which can return multiple versions for
the same slug and make `post.data[0]`/`chapter.data[0]` nondeterministic. Update
the `db.query.posts.findFirst` relations in `post.ts` so both the top-level
`data` and nested `collection.posts.data` filters include the intended `version`
alongside `locale`. Use the existing `post.data` and `chapter.data` relation
selectors to locate the fix.

},
where: { locale },
},
authors: { columns: { slug: true, name: true, profileImage: true } },

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if there's an order/position column on the post_authors join table
ast-grep run --pattern 'export const postAuthors = pgTable($_, $_, $_)' --lang typescript packages/db
rg -n -A 15 'postAuthors' packages/db/src/schema --type ts

Repository: playfulprogramming/hoof

Length of output: 2118


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== apps/api/src/routes/content/post.ts =="
ast-grep outline apps/api/src/routes/content/post.ts --view expanded
echo
sed -n '1,240p' apps/api/src/routes/content/post.ts

echo
echo "== packages/db/src/schema/posts.ts =="
ast-grep outline packages/db/src/schema/posts.ts --view expanded
echo
sed -n '1,220p' packages/db/src/schema/posts.ts

echo
echo "== search for authors ordering / post.authors usage =="
rg -n "post\.authors|authors: \{|orderBy:|author.*order" apps packages --type ts

Repository: playfulprogramming/hoof

Length of output: 7859


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== relation definitions around posts =="
rg -n -A 80 -B 20 'relations\(|postAuthors|authors:' packages/db/src/schema/posts.ts packages/db/src/schema/*.ts

echo
echo "== relation helpers / docs references to orderBy in db schema =="
rg -n "orderBy" packages/db/src/schema packages/db/src -g '*.ts'

Repository: playfulprogramming/hoof

Length of output: 6930


Add an explicit author sort If author order matters in the response, add an explicit orderBy for authors; post.authors is mapped in whatever order the DB returns, and post_authors has no position column to make that stable.

🤖 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 `@apps/api/src/routes/content/post.ts` at line 128, The authors mapping in the
post response is currently relying on database return order, which is not
stable. Update the post retrieval/mapping in the post route to add an explicit
orderBy for authors (or otherwise define a deterministic sort) when populating
post.authors, using the existing authors relation in the response shape so the
author order is consistent.

Comment thread apps/api/src/routes/content/post.ts Outdated
Comment on lines +34 to +35
collectionOrder: Type.Number(),
isCurrent: Type.Boolean(),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Two requests:

  • Could we remove these two properties (collectionOrder and isCurrent)? The frontend should be able to infer both from the data.
  • Can we add the collection slug & title to the response? (I would group these in an object; i.e. collection.slug, collection.title, collection.chapters are all required when the collection is present)

Otherwise, this looks great!

Drop collectionOrder and isCurrent from each chapter (the frontend can
derive both from array position and slug comparison), and restructure
the response to nest collection info under collection: { slug, title,
chapters } instead of a flat chapters array.
@fennifith fennifith enabled auto-merge July 4, 2026 00:41
@fennifith fennifith added this pull request to the merge queue Jul 4, 2026
Merged via the queue into playfulprogramming:main with commit 086e182 Jul 4, 2026
4 checks passed
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.

Endpoint for get post by ID

2 participants