Skip to content

fix: server-side content list search + locale-aware list indexes#1226

Draft
scottbuscemi wants to merge 1 commit into
mainfrom
fix/1219-content-list-search
Draft

fix: server-side content list search + locale-aware list indexes#1226
scottbuscemi wants to merge 1 commit into
mainfrom
fix/1219-content-list-search

Conversation

@scottbuscemi
Copy link
Copy Markdown
Contributor

@scottbuscemi scottbuscemi commented May 29, 2026

Addresses #1219.

Problem

On collections with 1000+ entries:

  • Search only matched rows already loaded on the current page. An entry far back in the collection couldn't be found from the first page — it became searchable only after you navigated near it. (ContentList filtered data.pages.flatMap(...).filter(byTitle) client-side; the API client never sent a search term; the list query/handler/repository had no search filter.)
  • Navigation was slow on large, i18n-enabled collections partly because locale-filtered ordered lists weren't covered by an index.

Fix

Server-side search

  • contentListQuery schema gains an optional q (trimmed, 1–200 chars).
  • ContentRepository.findMany/count apply a case-insensitive substring filter across handler-resolved searchColumns, OR'd. lower() on both sides for SQLite/Postgres parity; LIKE wildcards (% _ \) in the query are escaped with ESCAPE '\'.
  • handleContentList resolves the searchable columns from the collection's fields — always slug, plus title/name when defined (mirrors the admin's title resolution) — so collections without those columns don't error.
  • Admin: fetchContentList sends q; ContentList debounces the search box (300ms) and reports it up via a new onSearchChange prop; the route feeds it into the infinite query key + fetch. Client-side filtering is retained only as legacy behavior when onSearchChange isn't supplied.

Navigation

  • New migration 041_content_locale_list_index adds idx_{table}_deleted_locale_updated_id and …_created_id across all ec_* tables (idempotent, forward-only), mirrored in SchemaRegistry for new collections.

Deliberately out of scope

The research flagged the UI's page-number pagination over forward-only cursors as the bigger driver of deep-navigation latency; that's a larger UX refactor and is deferred. I also did not skip the per-page COUNT — there's an explicit regression guard requiring total on every page, so that optimization would break documented behavior.

Testing

  • New packages/core/tests/integration/content/content-list-search.test.ts (describeEachDialect): finds a deep entry, case-insensitive, slug match, wildcard-escape ("50%" matches only the literal), and unfiltered fallback. Passing on SQLite locally; Postgres in CI.
  • New admin test in ContentList.test.tsx: server mode reports the debounced query and does not client-filter.
  • Full core suite green (3486 tests); pnpm lint:json clean; emdash + @emdash-cms/admin typecheck pass. Admin browser tests run in CI (couldn't launch a local browser here).

Manual verification

  1. Collection with 1000+ entries.
  2. Search a term that only matches a deep entry → it appears (previously didn't until you paged near it).
  3. With i18n enabled, confirm locale-filtered lists still page correctly.

Try this PR

Open a fresh playground →

A full working EmDash site, deployed from this branch. Each visit gets its own session-scoped sandbox: no login needed and no shared state. Try the admin, edit content, hit the public site.

Tracks fix/1219-content-list-search. Updated automatically when the playground redeploys.

The admin content list filtered only the rows already loaded on the
current page, so entries far back in a large collection were unfindable
until you navigated near them. Add a `q` query param to the content
list endpoint that performs a case-insensitive substring search across
the collection's title/name/slug columns (LIKE wildcards escaped), and
wire the admin search box to drive it (debounced) instead of filtering
in memory. Add locale-aware composite indexes so i18n-filtered lists stay
index-served on large tables. Closes #1219.
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 29, 2026

🦋 Changeset detected

Latest commit: 7545b03

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 14 packages
Name Type
emdash Patch
@emdash-cms/admin Patch
@emdash-cms/cloudflare Patch
@emdash-cms/sandbox-workerd Patch
@emdash-cms/fixture-perf-site Patch
@emdash-cms/perf-demo-site Patch
@emdash-cms/cache-demo-site Patch
@emdash-cms/auth Patch
@emdash-cms/blocks Patch
@emdash-cms/gutenberg-to-portable-text Patch
@emdash-cms/x402 Patch
create-emdash Patch
@emdash-cms/auth-atproto Patch
@emdash-cms/plugin-embeds Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions
Copy link
Copy Markdown
Contributor

PR template validation failed

Please fix the following issues by editing your PR description:

  • This PR does not use the required PR template. Please edit the description to use the PR template. Copy it into your PR description and fill out all sections.

See CONTRIBUTING.md for the full contribution policy.

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented May 29, 2026

Open in StackBlitz

@emdash-cms/admin

npm i https://pkg.pr.new/@emdash-cms/admin@1226

@emdash-cms/auth

npm i https://pkg.pr.new/@emdash-cms/auth@1226

@emdash-cms/blocks

npm i https://pkg.pr.new/@emdash-cms/blocks@1226

@emdash-cms/cloudflare

npm i https://pkg.pr.new/@emdash-cms/cloudflare@1226

emdash

npm i https://pkg.pr.new/emdash@1226

create-emdash

npm i https://pkg.pr.new/create-emdash@1226

@emdash-cms/gutenberg-to-portable-text

npm i https://pkg.pr.new/@emdash-cms/gutenberg-to-portable-text@1226

@emdash-cms/x402

npm i https://pkg.pr.new/@emdash-cms/x402@1226

@emdash-cms/plugin-ai-moderation

npm i https://pkg.pr.new/@emdash-cms/plugin-ai-moderation@1226

@emdash-cms/plugin-atproto

npm i https://pkg.pr.new/@emdash-cms/plugin-atproto@1226

@emdash-cms/plugin-audit-log

npm i https://pkg.pr.new/@emdash-cms/plugin-audit-log@1226

@emdash-cms/plugin-color

npm i https://pkg.pr.new/@emdash-cms/plugin-color@1226

@emdash-cms/plugin-embeds

npm i https://pkg.pr.new/@emdash-cms/plugin-embeds@1226

@emdash-cms/plugin-forms

npm i https://pkg.pr.new/@emdash-cms/plugin-forms@1226

@emdash-cms/plugin-webhook-notifier

npm i https://pkg.pr.new/@emdash-cms/plugin-webhook-notifier@1226

commit: 7545b03

@cloudflare-workers-and-pages
Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
emdash-playground 7545b03 May 29 2026, 11:03 PM

@cloudflare-workers-and-pages
Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
docs 7545b03 May 29 2026, 11:03 PM

@cloudflare-workers-and-pages
Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
emdash-demo-cache 7545b03 May 29 2026, 11:05 PM

@github-actions
Copy link
Copy Markdown
Contributor

Overlapping PRs

This PR modifies files that are also changed by other open PRs:

This may cause merge conflicts or duplicated work. A maintainer will coordinate.

@ascorbic ascorbic added the bot:review Trigger an emdashbot code review on this PR label May 31, 2026
Copy link
Copy Markdown
Contributor

@emdashbot emdashbot Bot left a comment

Choose a reason for hiding this comment

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

This is the right fix for the stated problem: moving content-list search server-side so large collections are fully searchable, plus locale-aware composite indexes to keep list queries index-served. The implementation follows EmDash conventions well—Lingui-wrapped labels, sql.ref() / validateIdentifier() for SQL safety, a proper changeset, describeEachDialect integration tests, and admin component tests. The handler-resolved searchColumns approach keeps the repository generic while avoiding missing-column errors.

However, the PR introduces three regressions that should be fixed before merge:

  1. Postgres index-name truncation / collision. The new index names idx_{table}_deleted_locale_updated_id and idx_{table}_deleted_locale_created_id differ only after a 26-character suffix. Postgres truncates identifiers to 63 bytes, so for any collection slug ≥ 40 characters (ec_ + slug = table name ≥ 43), the distinguishing part is truncated away and both names collide. In the migration IF NOT EXISTS silently skips the second index; in SchemaRegistry (no IF NOT EXISTS) collection creation hard-fails with “relation already exists”. The names need to be shortened so the divergence survives truncation.

  2. Search box disappears on empty server-side results. The search <Input> is guarded by {items.length > 0 && ...}. With server-side search, a query that returns zero matches makes items empty, unmounting the search box. The user then cannot edit or clear their query.

  3. Pagination count shows loaded page size instead of total during server search. renderItemCount receives filteredCount: filteredItems.length. In server-search mode filteredItems is just the currently loaded API page, so the count text displays e.g. “20 items matching …” even when the server reports a total of 100+. The caller already passes the correct total prop; the count text should use it in server mode.

@ascorbic
Copy link
Copy Markdown
Collaborator

@scottbuscemi tell ya boy to use the PR template!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/admin area/core bot:review Trigger an emdashbot code review on this PR overlap size/L

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants