Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
fd178ec
feat: Add Content Engine Stage 3
aXenDeveloper Aug 4, 2026
7ffb794
fix: Preserve search document plugin ownership
aXenDeveloper Aug 4, 2026
93ad8ff
fix: Make search rebuild pagination source-aware
aXenDeveloper Aug 4, 2026
c894b74
fix: Report over-indexed search collections as stale
aXenDeveloper Aug 4, 2026
5090364
fix: Preserve literal search enabled types
aXenDeveloper Aug 4, 2026
c2f6e43
fix: Isolate search synchronization logging failures
aXenDeveloper Aug 4, 2026
bb47b4c
docs: Harden Content Engine search documentation
aXenDeveloper Aug 4, 2026
5b74a1e
fix: Keep the array result of SearchIndexer.load working
aXenDeveloper Aug 4, 2026
ebf5bd5
fix: Keep the stored owner of an orphaned search collection
aXenDeveloper Aug 4, 2026
8830b5c
docs: Document both search indexer load results
aXenDeveloper Aug 4, 2026
73d2c2f
fix: Report a search collection with no indexer as orphaned
aXenDeveloper Aug 4, 2026
efbaf05
fix: Refuse a scoped search rebuild with no registered indexer
aXenDeveloper Aug 4, 2026
c491187
feat: Add an explicit cleanup action for orphaned search collections
aXenDeveloper Aug 4, 2026
7f6af03
docs: Document orphaned search collections
aXenDeveloper Aug 4, 2026
15f9bcc
fix: Call a collection with no rebuild indexer unmanaged, not orphaned
aXenDeveloper Aug 4, 2026
90ead27
fix: Make the search cleanup audit log best effort
aXenDeveloper Aug 4, 2026
4917636
docs: Separate live indexing from rebuild support
aXenDeveloper Aug 4, 2026
1a23614
docs: Improve search docs
aXenDeveloper Aug 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 50 additions & 6 deletions apps/docs/content/docs/dev/content-engine/limitations.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ other 20%, so you find out here rather than halfway through building.
| Deep relation nesting in a public response | One level, one key. Anything more is a custom route |
| Origin `Cache-Control` headers | [Tags](/docs/dev/content-engine/caching) handle Next-side caching; add headers in your own route |
| Per-route rate limits | The global IP bucket applies; anything finer is yours |
| Search indexing | Register a `SearchIndexer` yourself |
| Search: author facet, per-locale documents, relation containers | [`search`](/docs/dev/content-engine/search) indexes public text fields. Anything richer needs your own `SearchIndexer` |
| Automatic field renames | See below |
| Unique constraints on more than one column | Declare them in `indexes`, not on the field |
| Uniqueness on non-`text` fields | Declare the index in `indexes` |
Expand Down Expand Up @@ -132,16 +132,60 @@ suffixes in the [slug backfill
recipe](/docs/dev/content-engine/slug-field#migrations) are a one-off,
deterministic migration device for rows that predate the column.

## Direct service calls invalidate no cache
## Direct service calls invalidate no cache and index nothing

`service.publish()` and friends change rows and return the result. They emit no
event and expire no cache tag: they may be inside an uncommitted transaction,
they may be running in `apps/api` where there is no Next runtime at all, and the
Next cache APIs need a request scope a repository does not own.
event, expire no cache tag and touch no search document: they may be inside an
uncommitted transaction, they may be running in `apps/api` where there is no Next
runtime at all, and the Next cache APIs need a request scope a repository does
not own.

Every generated write path goes through an AdminCP server action, which does all
three. A direct caller does its own follow-up, after committing - see
[Caching](/docs/dev/content-engine/caching#writing-who-expires-what).
[Caching](/docs/dev/content-engine/caching#writing-who-expires-what) and
[Search](/docs/dev/content-engine/search#direct-service-calls-do-not-synchronize).

## A search engine outage does not undo a write

Search synchronization is best effort. A content mutation succeeds, the failure is
logged with a `[content-search]` prefix and surfaced in the AdminCP, and a rebuild
repairs the drift. There is no automatic retry and no durable outbox, so the index
is eventually consistent - bounded by the next publish or the next rebuild.

Logging is best effort in the same way: it writes to the database, so it can fail
alongside the search engine. When it does, the message falls back to the console
and the mutation still succeeds. Neither failure can turn a committed write into a
failed request.

## A collection with no rebuild indexer cannot be rebuilt

A collection with indexed documents but no registered `SearchIndexer` is labelled
**Unmanaged**. That happens when a content type drops its `search` block or its
plugin goes away - and also when a plugin simply indexes through `search.index()`
and never registers an indexer, which is supported and may be perfectly healthy.
The label therefore says only what is known: there is no way to rebuild it.

The AdminCP names the owner stored on those documents rather than reassigning them
to core, and falls back to `unknown` only when neither a registered indexer nor the
stored rows know. It reports no coverage, because there is no source to compare
against.

A scoped rebuild of it is refused - it would clear the documents and refill nothing
- so the only action offered is an explicit, confirmed **Remove documents**. That
clears the current indexed state and nothing more: a live-writing plugin may
recreate those documents immediately. A full rebuild clears them too, as part of
clearing everything, and recreates only collections that have an indexer.

Registering an indexer for the same `itemType` makes the collection ordinary again.
Nothing recovers it automatically.

## Malformed published data reads as under-indexed

A published record the search mapper cannot project - a title that is blank in the
database, say - counts toward a collection's source total and produces no document.
The AdminCP shows that as **stale** rather than hiding it. Extra documents, for
records that no longer qualify, are stale too: the counts have to match exactly for
a collection to read as indexed.

## The public cursor is always the row id

Expand Down
1 change: 1 addition & 0 deletions apps/docs/content/docs/dev/content-engine/meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"slug-field",
"public-api",
"public-service",
"search",
"caching",
"admincp",
"permissions",
Expand Down
7 changes: 7 additions & 0 deletions apps/docs/content/docs/dev/content-engine/public-api.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,13 @@ response schema generated from the projection. Only `get` operations are ever
built - there is no public create, update, delete, publish or unpublish, and no
flag that would add one.

## The allowlist bounds search too

[`search`](/docs/dev/content-engine/search) can only index fields that are in
`fields`, and that is a compile error rather than a convention - a private value
must not surface through a result snippet, a highlighted match or an exact-match
probe either.

## What this is not

No public writes, ever. No `Cache-Control` headers - HTTP caching is handled
Expand Down
11 changes: 11 additions & 0 deletions apps/docs/content/docs/dev/content-engine/publication.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,17 @@ path - which is exactly what the
[public field allowlist](/docs/dev/content-engine/public-api#the-field-allowlist)
does for you on the generated one.

## Publishing can do more than flip a column

Two things hang off the lifecycle, and both are opt-in:

- [`publicApi`](/docs/dev/content-engine/public-api) makes a published record
readable over HTTP.
- [`search`](/docs/dev/content-engine/search) keeps a published record in the
site-wide search index - added on publish, removed on unpublish.

Enabling publication on its own still publishes nothing anywhere.

## What this is not

Scheduled publishing, approval workflows and revisions are not here. The
Expand Down
Loading
Loading