diff --git a/apps/web/content/docs/dev/content-engine/content-delivery-and-seo.mdx b/apps/web/content/docs/dev/content-engine/content-delivery-and-seo.mdx index c412c407d2..5a52d7d288 100644 --- a/apps/web/content/docs/dev/content-engine/content-delivery-and-seo.mdx +++ b/apps/web/content/docs/dev/content-engine/content-delivery-and-seo.mdx @@ -283,4 +283,4 @@ Pages hold 1,000 entries by default (`?limit=` up to 50,000), and `nextCursor` f Every URL delivery builds follows the app's [localized URL rules](/docs/dev/i18n/localized-urls): language prefixes, per-language domains and translated route segments. With [localization](/docs/dev/content-engine/localization) and a localized slug, and an app that translates `/recipes` in its `routePaths`, the Polish recipe can live at `/pl/przepisy/szakszuka-dla-dwojga` while the English one stays at `/recipes/shakshuka-for-two`. Only published translations become alternate links. -Every `delivery` option, with its default and requirements, is in the [content type reference](/docs/dev/content-engine/reference#delivery). +To list every recipe on one page with search and filters, continue with [Show a list of records](/docs/dev/content-engine/list-pages). Every `delivery` option, with its default and requirements, is in the [content type reference](/docs/dev/content-engine/reference#delivery). diff --git a/apps/web/content/docs/dev/content-engine/index.mdx b/apps/web/content/docs/dev/content-engine/index.mdx index 9336cd1df1..dc8a958ac7 100644 --- a/apps/web/content/docs/dev/content-engine/index.mdx +++ b/apps/web/content/docs/dev/content-engine/index.mdx @@ -40,7 +40,7 @@ export const recipeContentType = defineContentType({ | Every content type | Staff routes and `can_view`, `can_create`, `can_edit` and `can_delete` permissions | | `publication` | Draft and published states, `can_publish`, and Publish buttons | | `publicApi` | Read-only public routes that return only the fields you list | -| `delivery` | Page URL resolution, SEO metadata, slug redirects and sitemap entries | +| `delivery` | Page URL resolution, SEO metadata, slug redirects, sitemap entries and an optional list page | | `editorial` | Revision history with restore, signed preview links and scheduled publishing | | `localization` | A translations table, per-language fields and per-language publishing | | `search` | Records in the site [search](/docs/dev/search) index | @@ -54,6 +54,7 @@ Content types live in code and in your migrations, not in the database. There is | Build a content type from scratch, with AdminCP screens | [Create your first content type](/docs/dev/content-engine/defining-a-content-type) | | Add drafts and a public, read-only API | [Publish content with a public API](/docs/dev/content-engine/public-api-and-caching) | | Give each record its own page with SEO metadata | [Show content on a public page](/docs/dev/content-engine/content-delivery-and-seo) | +| List published records with pagination, search and filters | [Show a list of records](/docs/dev/content-engine/list-pages) | | Link records together, group fields or repeat rows | [Relations and advanced fields](/docs/dev/content-engine/relations-and-advanced-modeling) | | Translate content into several languages | [Translate content](/docs/dev/content-engine/localization) | | Keep revisions, share draft previews or schedule publishing | [Revisions, previews and scheduling](/docs/dev/content-engine/publication-and-editorial) | diff --git a/apps/web/content/docs/dev/content-engine/list-pages-reference.mdx b/apps/web/content/docs/dev/content-engine/list-pages-reference.mdx new file mode 100644 index 0000000000..9b82bf3101 --- /dev/null +++ b/apps/web/content/docs/dev/content-engine/list-pages-reference.mdx @@ -0,0 +1,81 @@ +--- +title: List page reference +description: Every helper and component VitNode provides for Content Engine list pages, from @vitnode/core/tanstack/content, with parameters, defaults and return values. +icon: BookOpen +--- + +Everything a [list page](/docs/dev/content-engine/list-pages) uses comes from `@vitnode/core/tanstack/content`. The helpers read a content type's resolved `delivery.list`, so any definition returned by `defineContentType` can be passed where a `definition` is expected. + +```ts +import { + contentListPage, + contentListPageHead, + contentListQuery, + contentListSearch, +} from "@vitnode/core/tanstack/content"; +``` + +## `ContentListSearch` + +The page's URL after `contentListSearch` has read it. + +| Key | Type | Description | +| ------------------ | -------------------------------- | ----------------------------------------------------------- | +| `page` | `number` | Set from 2 upwards. Page 1 has no `?page=`, so it has one URL | +| `q` | `string` | The trimmed search term, at most 256 characters | +| any declared filter | `boolean` \| `number` \| `string` | One key per field in `delivery.list.filters` | + +## Helpers + +| Helper | Returns | Behavior | +| ------ | ------- | -------- | +| `contentListSearch(definition)` | `(input) => ContentListSearch` | The route's `search` validator. Drops a value it cannot read instead of failing | +| `contentListQuery(definition, search)` | `ContentListQuery` | The `args.query` of the public list route: `first`, plus `page`, `search` and each filter as strings | +| `contentListPage({ list, search })` | the `list` it was given | Throws `notFound()` when `search.page` is past the last page | +| `contentListPageHead(definition, options)` | `PluginRouteHead` | The route's `head`. See below | +| `nextContentListSearch(search, changes)` | `ContentListSearch` | The search of a link that changes the list. `undefined` removes a key, and any change except `page` goes back to page 1 | +| `isContentListIndexable(search)` | `boolean` | `true` only for the plain first page | + +### How filters are read + +`contentListSearch` reads each filter by the kind of its field: + +| Field kind | URL value | Search value | +| ------------------------------ | --------------------- | ------------ | +| `field.boolean()` | `true` or `false` | `boolean` | +| `field.enum()` | one of its `values` | `string` | +| `field.number()` | a finite number | `number` | +| `field.relation()`, `field.user()` | a positive whole number | `number` | +| `field.text()`, `field.slug()` | up to 255 characters | `string` | + +### `contentListPageHead` options + +| Name | Type | Required | Default | Description | +| ------------- | --------------------------------- | -------- | ---------------- | --------------------------------- | +| `search` | `ContentListSearch \| undefined` | Yes | | The `search` that `head` receives | +| `title` | `string` | No | | The page title, already translated | +| `description` | `string` | No | | The meta description | +| `locales` | `readonly string[]` | No | every app locale | Languages that get an `hreflang` link | + +The plain first page gets a canonical link and one alternate per language, all pointing at `delivery.list.path`. Any other page returns `robots: "noindex, follow"` and no alternates. + +## Components + +All components render semantic HTML, take a `className`, and translate their own labels through the `core.global` namespace. + +| Component | Renders | Props | +| --------- | ------- | ----- | +| `ContentList` | A `