---
title: "Archive pages"
description: "The archive page template, lead split, filters and building a custom archive."
requested_language: fr
language: en
translation_notice: "This page isn't translated yet"
url: https://docs.systhema.app/fr/next/payload/posts/archives
version: unreleased (main)
docs_index: https://docs.systhema.app/fr/next/llms.txt
---
> This page isn't translated yet. Showing English.


There are **no auto-generated archive routes**. Archives are real Pages that an editor creates and assigns the built-in **Archive** PAGE template to. The Archive template renders as its own **top-level editor tabs** — **Hero**, **Prepend**, **Posts**, **Append**, and the page's shared **SEO** tab — instead of one "Content" tab (a page template can opt into this via `editorTabs`; the archive's fields therefore live at the page root, not under an `archive` group). The listing is effectively a `posts` block "sandwiched" between two root-editor regions.

A template that is configured by one or two choices rather than a tab's worth of fields declares them as `sidebarFields`: they sit in the document sidebar under the template select, appear only while that template is selected, and — like `editorTabs` fields — live at the page root, so prefix their names.

## Template fields

The template adds the following to the Page:

- **`hero`** — a normal (optional) post hero, plus the archive's **search/filter controls**: a **`search`** toggle (default on), a **`filters`** multiselect (which taxonomy dropdowns to surface — `categories` / `tags` / `types`; default `categories` + `tags`; **empty = no filters**; the `types` option only appears on sites with more than one post type), and a **`filterStyle`** select — `Dropdowns` (default) or `Chips` — choosing how the taxonomy filters render (see [Filter control style](#filter-control-style)). When search is off **and** no filters are enabled the control renders nothing. Placement follows the hero type (see [Search/filter placement](#searchfilter-placement)).
- **`prepend`** — a root-level Lexical editor rendered **above** the listing (flexible sections/layouts before the posts).
- **`listing`** — how the unified `PostsList` is sourced and rendered. It is the **same shared field-set** as the [`posts` block](https://docs.systhema.app/fr/next/payload/posts/listing-and-sharing.md#the-posts-block) (`createPostsListingFields`), differing only in defaults: `source` (automatic query / fixed hand-picked), scoping filters (`filterCategories` / `filterTags` / `filterPostTypes` — the last shown only on multi-type sites), `order` + `direction`, `limit` / **Posts per page** (the **MAIN-list count** — default 4 for the block, **12 for the archive**; the highlight + lead grid are added on top of page 1, see [Lead posts split](#lead-posts-split)), `view` / **Layout** (`grid2`/`grid3`/`grid4`/`row`, default `grid4` — the layout of the main list; the first N posts render above in their own layout when a lead split is set), `loading` (`none`/`pagination`/`loadMore`/`infinite`, default `none`), the **Row dividers** `dividers` toggle, plus an **Advanced Settings** drawer. The drawer holds **Padding** (range 0–6, default 0); the per-axis **Items Gap X** (`gapX`) and **Items Gap Y** (`gapY`) sliders — the inter-card column-gap / row-gap as generated gap-token indices, mirroring the Columns block (see [Listing gap](#listing-gap)); the **lead posts split** controls **Lead posts count** (`leadCount`) + **Lead posts layout** (`leadView`, a `grid2`/`grid3`/`grid4`/`row` select shown only when `leadCount > 0`); and **Highlight first post** (`highlightFirst`, default `false`) — all listing-level (block + archive). When **Loading strategy** is set to **Load more button**, the drawer also reveals a **Load more button** group (listing-level, block + archive) that configures the real design-system `<Button>` the listing renders for that strategy — **Variant** (any design-system button variant; unset = the default variant), **Button title** (prefilled "Load more"), **Icon before the title** + **Icon after the title** (icon-picker fields rendered around the title), and **Alignment** (`Left` / `Center` / `Right`, default `Center`). Icons are resolved to HTML server-side and threaded to [`PostsList`](https://docs.systhema.app/fr/next/components/posts-list.md)'s `loadMoreButton` prop; the button carries the bespoke styling hook `.posts-list-load-more` (now appearance comes from the button tokens, so that class is just a target + the disabled affordance). On the **[`posts` block](https://docs.systhema.app/fr/next/payload/posts/listing-and-sharing.md#the-posts-block)** the drawer additionally exposes four block-only presentation controls: **Media aspect ratio** (`21:9` / `16:9` / `4:3` _(default)_ / `3:2` / `1:1` / `3:4` / `4:5` / `9:16` — the ImageBlock fixed ratios; the responsive `21/9-md` is excluded, since a card applies the ratio as a plain inline `aspect-ratio`); for the Row layout only, **Thumbnail width** (`Narrow` / `Default` / `Wide` / `Equal halves` — the media \| content column split) and **Body alignment** (`Top` / `Center` / `Bottom` — the body's vertical align against the taller media column, overriding the top-align default); and for grid + row, **Media ↔ body gap** (a gap-token slider — the gap between thumbnail and text inside a card, resolved to the matching `--gap-*` token, index 0 = none). All apply as inline styles so any value renders without a Tailwind content-scan, override the [card defaults](https://docs.systhema.app/fr/next/payload/posts/presentation.md#the-three-tiers-and-how-they-win), and leave existing blocks untouched (omitted ⇒ no change). The listing also carries section presentation (`theme` / `layoutBg`) so it renders inside a themeable `<Section>`.
- **`append`** — a root-level Lexical editor rendered **below** the listing.

## Lead posts split

A listing can render as a uniform single container (the default) or as a **lead posts split**: the first few posts fill a **lead grid** with its own layout, and the rest flow into the primary `view` container below. Two admin fields (both in **Advanced Settings**) drive it:

- **Lead posts count** (`leadCount`, number) — render the first N posts **after** any `highlightFirst` hero in their own **Lead posts layout** (`leadView`); the remaining posts render below in the main `view` container. Unset / `0` / a value `>=` the item count ⇒ uniform single-container behaviour (no split).
- **Lead posts layout** (`leadView`, select — `grid2`/`grid3`/`grid4`/`row`, default `'grid2'`) — the layout of the lead posts. Shown in the admin only when `leadCount > 0`. The rest use the main **Layout** (`view`).
- **Row dividers** (`dividers`, checkbox, default `false`) — see below. Shown in the admin only when a `row` container is actually rendered (the main `view` is `row`, or a lead split uses a `row` `leadView`).

**Count model — the page size adds up.** `limit` (**Posts per page**) is the **main-list count**, NOT the total. The highlight hero (1) and the lead grid (`leadCount`) are added **on top of page 1** automatically — so `Posts per page: 12` with `highlightFirst` + a 6-card lead renders **19 on page 1** (1 + 6 + 12) and **12 on every later page** (a uniform main grid — the highlight + lead are a page-1 treatment). The frontend computes every fetch count, so an editor never subtracts the highlight/lead from the page size by hand. Pages are therefore **uneven**: `totalPages = 1 + ceil((total − pageOne) / limit)` where `pageOne = limit + 1 + leadCount`. The pure helpers `listingExtra` / `listingPageOneCount` / `listingTotalPages` / `listingPageOffset` / `listingPageCount` (`@systhemaui/react`) encode this model and back both the server fetch (`resolveArchiveData` page 1, `resolvePostsPage` pages 2+ via an explicit fetch `offset`) and the client. An un-split listing (`leadCount` 0, no highlight) has `extra = 0`, so the model reduces to classic page-based pagination (byte-identical).

**Row dividers** (`dividers`, boolean, default `false`) draws a 1px `--color-separator-line` hairline between adjacent rows in the `row` (and split lead `row`) container via [`PostsList`](https://docs.systhema.app/fr/next/components/posts-list.md)'s `<Stack divider>` — the Figma archive list shape. Grids are unaffected (a row hairline doesn't apply across columns). `resolveArchiveData` reads it off the listing config (`listing.dividers`) and threads it through `ArchiveData` → `PostsListClient` → `PostsList`, and a Tier-2 custom archive can pass `dividers` to `PostsList` / `PostsListClient` directly. It is surfaced as a **Row dividers** checkbox in the admin — in both the shipped Archive template's **Posts** tab (the `listing` group) and the `posts` block.

`resolveArchiveData` resolves these into `ArchiveData` (`leadCount?`, `leadView?`, `dividers?`) and `PostsListClient` threads them into [`PostsList`](https://docs.systhema.app/fr/next/components/posts-list.md). Load-more pages append to the main `view` container automatically.

**Per-section card config.** Because the lead posts and the main list can be different layouts, each section gets its **own** layout's [Tier-1 card config](https://docs.systhema.app/fr/next/payload/posts/presentation.md): the lead cards resolve `cards[leadView]` and the main cards resolve `cards[view]`. So with `cards.grid2 = { aspectRatio: '16/9' }` and `cards.row = { aspectRatio: '4/3', bodyAlign: 'center' }`, a `grid2` lead + `row` main list render each section with its respective knobs. `resolveArchiveData` returns both `cardConfig` (the main `view`) and `leadCardConfig` (the lead `leadView`); `PostsList` applies `leadCardConfig` to the lead grid, falling back to `cardConfig` when unset (un-split listings are unaffected). A Tier-2 custom archive can pass both props to `PostsList` / `PostsListClient` directly.

A recommended split (a Figma example): `limit: 12, highlightFirst: true, leadCount: 6, view: 'row', leadView: 'grid3', loading: 'loadMore'` → page 1 = 1 hero + a 6-card grid lead + a 12-row list (19 posts), and **Load more** appends 12 more rows at a time.

> [!NOTE]
> **How pagination vs append handle the page-1 extra.** With `loading: 'pagination'`, page 1 renders the highlight + lead grid + `limit` main posts; pages 2+ render a **uniform `limit`-post grid** (no highlight, no lead split). `loadMore`/`infinite` keep page 1's highlight + lead and **append `limit` more** to the main list on each load. (Pages 2+ start at a non-page-aligned fetch offset past the page-1 extra; `resolvePostsPage` handles that offset server-side.)

The Archive template registers automatically as a PAGE template when posts is enabled (so it appears in the Page `pageTemplate` select). To build a `/reviews` archive: create a Page with slug `reviews`, pick the Archive template, and constrain the listing to the Reviews category. The `archiveTemplateFields` shape is exported for reuse/extension.

## Search/filter placement

Where the search/filter control mounts depends on the hero type — so it always reads as part of the page header regardless of the hero you pick:

| Hero type            | Placement                                                      |
| -------------------- | -------------------------------------------------------------- |
| `simple` / `feature` | **inside the hero** (its append/content slot)                  |
| `background`         | a standalone **`<Section>` below the hero**, above the listing |
| `none`               | a standalone **`<Section>`** above the listing                 |

If `search` is off **and** `filters` is empty, nothing renders — no empty hero slot and no empty section. The control itself (search input + taxonomy pills) is the same `ArchiveFilterControl` everywhere; only its mount point changes.

## Filter control style

The taxonomy filters render one of two ways, chosen by the hero's **`filterStyle`** select (only shown when at least one taxonomy `filters` dimension is enabled):

| `filterStyle`          | Rendering                                                                                                        |
| ---------------------- | ---------------------------------------------------------------------------------------------------------------- |
| `dropdown` _(default)_ | One `<select>` dropdown per taxonomy (the Figma archive shape). **No regression** — this is the prior behaviour. |
| `chips`                | Each taxonomy renders as a wrap row of toggle [`Chip`](https://docs.systhema.app/fr/next/components/chip.md)s (the podcasts wireframe).      |

Both styles share the **same** filter state, handlers, and URL sync — `filterStyle` is purely presentational. The free-text search input is unaffected. In `chips` mode, clicking a chip selects that filter (and `aria-pressed="true"` + the `.archive-filter-chip-toggle-active` class mark it active); clicking the active chip again clears it. The removable active-filter chips row renders in both styles. `resolveArchiveData` normalizes the hero choice onto `ArchiveData.filterStyle` (defaulting unknown / unset values to `'dropdown'`) and threads it through `ArchiveFilterControl` → [`ArchiveSearchFilter`](https://docs.systhema.app/fr/next/components/archive-search-filter.md)'s `filterStyle` prop; a Tier-2 custom archive can pass `filterStyle` to `ArchiveFilterControl` / `ArchiveSearchFilter` directly.

## Listing gap

The inter-card gap is a **generated gap token**, not a raw pixel value — the same mechanism `Columns` and `Stack` use. Two per-axis sliders in **Advanced Settings** — **Items Gap X** (`gapX`, column-gap) and **Items Gap Y** (`gapY`, row-gap) — each store an index into the design system's gap token set (`none` … `3xl`), mirroring the Columns block's Horizontal/Vertical Gap. `resolveArchiveData` resolves each index to a `GapSize` and threads it to [`PostsList`](https://docs.systhema.app/fr/next/components/posts-list.md)'s `gapX` / `gapY` props, which apply an inline `column-gap` / `row-gap` (`var(--gap-{size})`). When a slider is left at the design default (or omitted on existing data) the per-axis **core-CSS defaults** apply: grids keep their asymmetric column `sm` / row `md` gaps, and the `.posts-list-row` flex container carries a default `md` row gap. In the `row` layout the card body is **top-aligned** against the (taller) media column by default (the kicker tracks the media top, matching the Figma row card); re-align it with the `posts` block's **Body alignment** control (`Top` / `Center` / `Bottom` — see the [block-only presentation controls](#template-fields)) or, in code, with custom CSS on the `.post-card-row .post-card-body` hook.

The **Pagination** loading mode renders the controls as **← prev · `n / total` · next →** (centered), reusing the [Gallery](https://docs.systhema.app/fr/next/components.md) component's button + number font styling (masked-icon arrows). It **scrolls to the top of the listing section** rather than the top of the page on page change, and the **highlighted first post is page-1 only** — navigated pages render a uniform grid with no highlight.

## Building a custom archive (search + load-more + ads together)

When the shipped Archive template isn't enough — e.g. you want the search/filter control **and** interleaved ads in one listing — compose your own from the exported pieces. `@systhemaui/payload/next` exports the full filter pipeline:

- `resolveArchiveData(payload, { listing, … })` — server resolver (page-1 query + authors + filter options + Tier-1/2 config).
- `resolvePostAuthor` (single, React-`cache`d) / `resolvePostAuthors` (batched, no N+1 — use this for listings) — resolve a post's public byline as a `ResolvedPostAuthor` (`{ name, avatar?, role? }`) for **custom bylines**. They run under `overrideAccess`, so the byline (including `role`, see [the PostView byline above](https://docs.systhema.app/fr/next/payload/posts/presentation.md#the-postview-an-island-receives)) is safe on anonymous published reads. `authorRelationId` pulls the author relation id; `AuthorRelation` types the raw relation.
- `ArchiveFilterProvider` / `useArchiveFilters` — the shared client filter-state context.
- `ArchiveFilterControl` — the search + taxonomy-filter control, bound to that context. It forwards `order` / `filterStyle` / `searchPlaceholder` / `filtersLabel` to the underlying [`ArchiveSearchFilter`](https://docs.systhema.app/fr/next/components/archive-search-filter.md): `order?: readonly ArchiveFilterDimension[]` reorders the taxonomy filters (`'categories' | 'tags' | 'types'`; default category → tag → type, omitted dimensions keep their default position after the listed ones), `filterStyle?: 'dropdown' | 'chips'` chooses dropdowns (default) or a toggle-chip row (see [Filter control style](#filter-control-style)), `searchPlaceholder?` overrides the search input placeholder (default `'Search…'`), and `filtersLabel?` renders an optional leading eyebrow before the controls as `.archive-filter-label` (omitted when unset).
- `PostsListClient` — the client listing engine (load-more / pagination / infinite), which also accepts an `injectItems` render-prop for interleaving ad nodes. It threads `leadCount` / `leadView` through to `PostsList`, so a custom archive gets the [lead posts split](#lead-posts-split) too.
- `splitGridItems(items, leadCount)` — the pure helper `PostsList` uses to partition a flat list into `{ lead, tail }`. Also exported from `@systhemaui/react` and `@systhemaui/next` for custom splitting.
- `resolvePostsPage(payload, request)` — the shared "filters → `PostView` page" resolver behind both SSR page 1 and the load-more / filter fetch. Resolves the public author with `overrideAccess`, so an appended/filtered card keeps its byline. Use it to build a custom listing endpoint.

> [!NOTE]
> **Load-more bylines.** `PostsListClient` resolves load-more / filtered pages through the module's `/sys/posts-list` route (which runs `resolvePostsPage` server-side with `overrideAccess` for the author), so appended and filtered cards keep the byline name + avatar — matching the server-rendered first page. If that route isn't mounted (a project without the systhema `[route]` handler), it falls back to the public `/api/posts` REST endpoint, where anonymous reads access-gate the author relation to a bare id and the byline degrades to date-only. The shipped Payload template mounts the handler, so this is the default path.

Recipe: resolve `archiveData` server-side, then wrap the subtree in `ArchiveFilterProvider`, mount `ArchiveFilterControl` (fed `archiveData.filterOptions.*`) in the hero, and render `PostsListClient` with `injectItems={({ index }) => (index % 6 === 5 ? <YourAd /> : null)}`. Search re-fetches page 1, load-more pages the filtered set, and ads interleave — all together. The filter control must live inside the same `ArchiveFilterProvider` subtree as the listing, and `injectItems` (a function) must be defined in a `'use client'` component (it hands pre-rendered ad **elements** down — no closure crosses the RSC boundary).

> [!TIP]
> **Container edge.** Wrap your listing in a `<Section padding={0}>` (as the shipped `DefaultArchive` does) so it shares the design-system grid + gutter + `py-section` rhythm with its hero/filter siblings — that Section is the canonical full-container edge. `.posts-list`'s own `max-width: var(--container-width)` is only a last-resort safety net for a listing rendered _outside_ a `Section`/`Container`, so it doesn't bleed full-bleed.

**Wrap the sections in `<Article>`.** Like `DefaultArchive` (and `DefaultPage`), wrap your archive's `<Section>`s in `<Article>` (from `@systhemaui/react`/`next`), inside the `ArchiveFilterProvider`. The section-spacing relations — first/last-child padding suppression and the collapsing of `py-section` padding between adjacent sections (so neighbours don't double their gap) — are all CSS scoped under `.article > section`. A custom archive that renders bare `<Section>`s without the `<Article>` parent gets none of them, and the inter-section vertical rhythm will look wrong.

### Interleaving ads (the `injectItems` context)

`injectItems` receives a context that's been widened additively for the lead posts split — it stays fully backward-compatible:

```ts
injectItems?: (ctx: {
  index: number
  total: number
  section: 'lead' | 'tail'   // which container the call comes from
  isLeadEnd: boolean         // true ONLY for the single seam call between lead grid and tail list
}) => ReactNode
```

- **Per-card injection** by `index` still works exactly as before — return an element to drop an ad card after that card, `null` to skip. Use it for in-list ad cadence (`index % 6 === 5`).
- **`section`** tells you whether the call comes from the **lead** grid or the **tail** list, so an ad can target one container.
- **`isLeadEnd`** is `true` for exactly one **seam** call fired _between_ the lead grid and the tail list, only when a split is active. It renders as a **sibling between the two containers** (wrapped in `.posts-list-interspace`) — never inside the grid — so it's the place for a full-width ad at the grid→list seam:

```tsx
injectItems={({ section, isLeadEnd }) => (section === 'lead' && isLeadEnd ? adNode : null)}
```
