Skip to content

feat: Add public list pages with search, filters and pagination - #891

Merged
aXenDeveloper merged 6 commits into
canaryfrom
claude/blog-public-list
Oct 20, 2026
Merged

aXenDeveloper merged 6 commits into
canaryfrom
claude/blog-public-list

Conversation

@aXenDeveloper

Copy link
Copy Markdown
Owner

Description

What?

Implements a complete public list system for the Content Engine, enabling plugins to create searchable, filterable, paginated list pages (like /blog) with URL-based state management. This includes:

  • Core list utilities (defineContentList, contentListQueryOptions, contentListPageHead): Framework-agnostic helpers for declaring sorts, filters, page size and building normalized URL state
  • UI components (ContentListResults, ContentListPagination, useContentListSearchInput): Reusable React components for rendering lists with proper accessibility and pagination
  • Blog plugin integration: Complete /blog page implementation with search, category filtering, sort options and pagination
  • API support: New categories endpoint and listFields configuration to optimize list row payloads
  • Documentation: Comprehensive guides for building list pages, embedding lists in other pages, and API reference

Key features:

  • URL-based state (search term, sort, filters, page) that works before hydration
  • Normalized search parsing that keeps cache keys and canonical links stable
  • Proper SEO: plain lists are index, follow with canonical links; filtered/paginated views are noindex, follow
  • Locale-aware pagination and content delivery
  • Accessible pagination with previous/next buttons and page numbers
  • Debounced search input with configurable delay

Why?

The Content Engine needed a reference implementation for public list pages. This enables:

  • Plugins to expose their content through discoverable, searchable list pages
  • Consistent UX across different content types with reusable components
  • Proper SEO handling for list pages and their variations
  • Type-safe URL state management that integrates with TanStack Router
  • Optimized API responses via listFields to avoid sending unnecessary data to list views

The blog plugin demonstrates the full pattern: declaring sorts/filters, fetching paginated data, rendering cards, and handling search/filtering in the UI.

https://claude.ai/code/session_01B89NT73f4bRH7pk5MU6XiU

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 19, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-20T13:07:42.068869Z e32015f New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@aXenDeveloper aXenDeveloper changed the title Add public list pages with search, filters and pagination feat: Add public list pages with search, filters and pagination Oct 19, 2026
@github-actions github-actions Bot added the 💡 Feature A new feature label Oct 19, 2026
@github-actions

github-actions Bot commented Oct 19, 2026 •

Copy link
Copy Markdown

React Doctor found 2 new issues in 2 files · 2 warnings · score 83 / 100 (Needs work) · 0 fixed · vs canary

2 warnings

src/pages/blog-category-page.tsx

  • ⚠️ L35 Array lookup inside a loop js-set-map-lookups

src/pages/blog-index.tsx

  • ⚠️ L50 Heading has no content heading-has-content

Reviewed by React Doctor for commit e32015f. See inline comments for fixes.

…imitives

- Content Engine: `publicApi.listFields` now shapes the list route's response
  schema (`schemas.publicListSelectObject`) and `ContentPublicListRow`, so list
  types and OpenAPI match the runtime projection.
- `@vitnode/core/tanstack/content`: `defineContentList`,
  `contentListQueryOptions`, `contentListPageHead`, `ContentListResults`,
  `ContentListPagination` and `useContentListSearchInput`.
- `@vitnode/core/content/server`: export `resolveContentRequestLocale`,
  `contentLocaleHeaders`, `rememberContentPublicRead` and the page cache tag
  helpers; the public routes use the shared locale helper.
- Plugin route robots accept `noindex, follow` for list variants.
- Blog: `/blog` with search, category filter, sort and numbered pagination in
  the URL, cards from a lightweight `listFields` projection, and a public
  `GET /categories` summary route cached per locale.
- Docs: public lists guide, embedding guide, API reference and blog guide.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B89NT73f4bRH7pk5MU6XiU
- `listFields` narrows `ContentPublicListRow`, `publicListSelectObject` and the
  list route's OpenAPI schema while the detail keeps every field.
- `defineContentList` URL normalization, page reset, API query and href;
  `contentListQueryOptions` key; `contentListPageHead` robots and alternates.
- `ContentListResults` states and `useContentListSearchInput` debouncing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B89NT73f4bRH7pk5MU6XiU
…ublic routes

- `fetcher` / `fetcher/server`: a JSON body is typed with Hono's `JSONParsed`,
  the same transform `c.json()` applies, so a `z.date()` field reads as its
  ISO `string` instead of a `Date` the wire never carries.
- Content Engine public list, detail and preview routes answer with the typed
  `publicListSelect` / `publicSelect` schemas instead of the erased
  `ZodObject<ZodRawShape>`, so `fetcher({ module: "content/blog", path: "/" })`
  infers each row from `publicApi.listFields` rather than `Record<string, unknown>`.
- Blog: drop the hand-written zod copies of the article, card and category
  shapes; their types come from the routes.
- Forum: the approval notice reads only the fields it renders, which fits the
  edit route's JSON answer.
- Docs: fetcher response typing and the content list examples.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B89NT73f4bRH7pk5MU6XiU
- A `z.date()` field in a JSON response reads as `string`, nested and
  nullable ones too.
- A content type's public list route infers rows from `listFields`, while
  the detail route keeps every exposed field.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B89NT73f4bRH7pk5MU6XiU
@aXenDeveloper
aXenDeveloper force-pushed the claude/blog-public-list branch from 1e218b5 to b426eb7 Compare October 19, 2026 17:39

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b426eb7239

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

...blogPostsQueryOptions({ locale, search }),
meta: LOADER_DATA_QUERY_META,
})
.catch(() => null),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Prevent failed list loads from entering the page cache

When the initial posts request fails during a production cache miss, this catch converts the loader failure into null, so /blog still returns a cacheable 200 error screen. Since the route caches /blog for 300 seconds plus 60 seconds of stale-while-revalidate and the failed API read contributes no content tags, a transient outage can make the error screen persist for every visitor after the API recovers. Let the shared render fail or otherwise mark this degraded response as non-cacheable; the categories fallback below has the same problem.

Useful? React with 👍 / 👎.

Comment on lines +211 to +214
if (committed !== value) {
setCommitted(value);
if ((value ?? "") !== draft.trim()) setDraft(value ?? "");
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Cancel pending search commits after URL changes

When value changes while a debounce is pending—for example, a reader edits an existing query and then clicks Clear filters or navigates Back before 500 ms—this synchronization updates the displayed draft but leaves the discarded term scheduled. The callback later invokes onCommit with that stale term and navigates back to the search the reader just left, so clearing or history navigation does not stick. Cancel the pending callback when an external value change is adopted.

Useful? React with 👍 / 👎.

… pages

- Implemented `public-posts.ts` for handling published post conditions and translations.
- Created `authors.module.ts` to manage authors of blog posts with caching.
- Developed blog category pages with sidebar navigation and chips for better UX.
- Introduced skeleton loading states for blog pages to improve user experience during data fetching.
- Added transitions for blog categories and articles to enhance visual feedback.
- Implemented legacy post redirection for improved SEO and user navigation.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 158662519d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

routePaths: {
pl: {
'/blog/:slug': '/wpisy/:slug',
'/blog/article/:slug': '/blog/artykul/:slug',

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve the retired Polish article route

When a previously published Polish URL such as /pl/wpisy/my-post is requested, the checked web app no longer rewrites it to the new legacy handler: this replacement removes the old /blog/:slug → /wpisy/:slug mapping, while the redirect page added in plugins/blog/src/routes.ts is still registered only at the internal /blog/:slug path. Those existing localized links therefore become 404s instead of receiving the promised permanent redirect; retain a Polish mapping for the legacy route in both the web and API i18n configurations.

Useful? React with 👍 / 👎.

Comment on lines +49 to +50
inArray(blog_posts_author_id.itemId, postIds),
publishedPostCondition(),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Gate author lookups on localized publication

When an article's base row is published but all of its translation rows are draft or unpublished, this anonymous endpoint still returns the article ID and its authors because publishedPostCondition() checks only the base table. The normal localized public list additionally requires a published translation, so callers can enumerate IDs here and disclose authorship for an article that has no public page; apply the localized publication predicate, either for a requested locale or for at least one enabled locale.

Useful? React with 👍 / 👎.

return parseSearch({
...current,
...patch,
page: resetsPage ? undefined : (patch.page ?? current.page),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Honor explicit page clearing in withSearch

When a consumer calls withSearch(current, { page: undefined }) to return from a later page to the first page, the nullish coalescing expression restores current.page, so the URL remains on the old page. This contradicts the helper's documented rule that undefined clears a value; distinguish an absent page property from an explicitly supplied undefined instead of using ??.

Useful? React with 👍 / 👎.


if (!selected) {
const translated = categories.items.find(category =>
Object.values(category.slugs).includes(slug),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

React Doctor · react-doctor/js-set-map-lookups (warning)

This scales poorly because array.includes() inside a loop scans the whole list every time. Use a Set for constant-time lookups.

Fix → Use a Set or Map when you check for the same items over and over. Array.includes/find scans the whole list each time

Docs

<Empty className="border">
<EmptyHeader>
<EmptyMedia variant="icon">{icon}</EmptyMedia>
<EmptyTitle render={<h2 />}>{title}</EmptyTitle>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

React Doctor · react-doctor/heading-has-content (warning)

Blind users can't use this heading to navigate because screen readers skip it empty, so add text, aria-label, or aria-labelledby.

Fix → Put readable text in every heading.

Docs

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e32015f067

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +77 to +82
const overlay = await loadContentPublicPeopleOverlay(
c,
definition,
people,
rows,
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Hydrate collection-backed people in live previews

When publicApi.people names a to-many user field and a preview token has revisionId: 0 (the supported path for records that predate editorial revisions), readPreviewRow obtains a live row through contentPublicSelection, which deliberately omits junction-backed fields. This call therefore receives no IDs and replaces the field with an empty array, so previews silently lose authors even though ordinary detail and list reads load them through advanced.loadMany; the live-preview path needs the collection-aware hydrator before resolving people.

Useful? React with 👍 / 👎.

@aXenDeveloper
aXenDeveloper merged commit 4ebacf5 into canary Oct 20, 2026
7 checks passed
@aXenDeveloper
aXenDeveloper deleted the claude/blog-public-list branch October 20, 2026 14:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

💡 Feature A new feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants