Docs

This page isn't translated yet

Archive pages

The archive page template, lead split, filters and building a custom archive.

On this page

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 fieldsLink to this section

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). When search is off and no filters are enabled the control renders nothing. Placement follows the hero type (see Search/filter 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 (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), 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); 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'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 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, 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 splitLink to this section

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'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. 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: 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.

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 placementLink to this section

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 typePlacement
simple / featureinside the hero (its append/content slot)
backgrounda standalone <Section> below the hero, above the listing
nonea 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 styleLink to this section

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):

filterStyleRendering
dropdown (default)One <select> dropdown per taxonomy (the Figma archive shape). No regression — this is the prior behaviour.
chipsEach taxonomy renders as a wrap row of toggle Chips (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's filterStyle prop; a Tier-2 custom archive can pass filterStyle to ArchiveFilterControl / ArchiveSearchFilter directly.

Listing gapLink to this section

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'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) 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 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)Link to this section

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-cached) / 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) 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: 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), 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 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.

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).

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)Link to this section

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

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:
injectItems={({ section, isLeadEnd }) => (section === 'lead' && isLeadEnd ? adNode : null)}