---
title: "Posts view model and helpers"
description: "PostView, part resolution and listing helpers (sizesForLayout, splitGridItems, listing*)."
requested_language: nl
language: en
translation_notice: "This page isn't translated yet"
url: https://docs.systhema.app/nl/components/posts-helpers
version: unreleased (main)
docs_index: https://docs.systhema.app/nl/llms.txt
---
> This page isn't translated yet. Showing English.


The posts components share one data shape, `PostView`, and a handful of pure helpers. Use them to feed the components from any data source, and to make a custom card (an island) behave exactly like the default one. Everything on this page is exported from `@systhemaui/react` and `@systhemaui/next`, and none of it depends on Payload or Next.js.

```tsx
import {
  defaultMediaAspect,
  formatPostDate,
  listingPageCount,
  listingPageOffset,
  postByline,
  resolvePartClassName,
  showPart,
  sizesForLayout,
  splitGridItems,
  type PostView,
} from '@systhemaui/next'
```

## `PostView`

```ts
interface PostView {
  id: string | number
  title: string
  href: string
  excerpt?: string
  label?: string
  category?: { name: string; slug: string } | null
  tags: { name: string; slug: string }[]
  postType: string
  author?: { name: string; avatar?: { url: string; alt?: string }; role?: string } | null
  publishedAt?: string
  locale?: string
  media?: {
    url: string
    alt?: string
    width?: number
    height?: number
    srcset?: string
    isPlaceholder?: boolean
  } | null
  layout?: PostLayout
  config?: ResolvedUnitConfig
  doc: any
}
```

The resolved, serializable data for one post. Every posts component and every island takes it as `post`. Because it holds only plain data, it crosses the server-to-client boundary as a prop.

- `href` is the post's URL, `label` the text above the title (Payload uses the category name), and `publishedAt` an ISO date that the cards format in `locale` (en-US by default).
- `author` is the public byline only. Payload never puts an email in it.
- `media` is `null` when the post has no image. When Payload substitutes a configured placeholder image, `isPlaceholder` is `true`.
- `layout` (`grid2`, `grid3`, `grid4` or `row`) is set on cards by `PostsList`; it is absent on a single-post hero.
- `config` carries the resolved `show` and `classNames` maps for the unit, so an island can honour them.
- `doc` is the full source document, for an island that needs a field the view does not surface. Payload builds a `PostView` from a post with `mapPostToView`; see [Customizing the presentation](https://docs.systhema.app/nl/payload/posts/presentation.md#the-postview-an-island-receives).

Related types: `PostLayout`, `ResolvedUnitConfig`, the island types `PostCardComponent`, `PostHeroComponent` and `PostSlotComponent` (all `ComponentType<{ post: PostView }>`), the config shapes `CardConfig`, `HighlightCardConfig` and `PostHeroConfig` (`{ show?, classNames?, island? }`), and `PostLayoutConfig` (`{ readingWidth?, bylineAvatarSize? }`, see [Single-post layout](https://docs.systhema.app/nl/payload/posts/presentation.md#single-post-layout-post)).

## Part-resolution helpers

The default cards resolve each part (media, label, title and so on) with these helpers. Use them in an island to get the same defaults and override behaviour.

### `resolvePartClassName`

```ts
resolvePartClassName(defaultClassName: string, override?: string): string
```

Merges an override over a part's default classes with tailwind-merge: a conflicting utility in the override wins (`'aspect-square'` replaces `'aspect-[4/3]'`) and other classes are added.

### `showPart`

```ts
showPart(show: Partial<Record<string, boolean>> | undefined, part: string, fallback: boolean): boolean
```

A part shows unless `show[part]` is `false`; a missing entry keeps `fallback`. So a `show` map only lists what differs from the defaults.

### `defaultMediaAspect`

```ts
defaultMediaAspect(layout: PostLayout | undefined): string
```

The default media aspect classes for a card layout: `'aspect-[4/3] md:aspect-video'` for `grid2`, `'aspect-[4/3] md:aspect-[3/2]'` for `row`, and `'aspect-[4/3]'` otherwise.

### `sizesForLayout`

```ts
sizesForLayout(layout: string | null | undefined): string
```

The `sizes` attribute for a card image in a layout. Every layout is `100vw` up to 768px; above that `grid2` and `row` are `50vw`, `grid4` is `25vw`, and `grid3` (and anything else) `33vw`. The Next.js `PostsList` and `PostCard` pass it to `next/image` for you.

```tsx
<Image src={post.media.url} alt="" fill sizes={sizesForLayout(post.layout)} />
```

### `postByline` and `formatPostDate`

```ts
postByline(post: Pick<PostView, 'author' | 'publishedAt' | 'locale'>): {
  author: string | null
  date: string | null
  dateTime: string | undefined
}
formatPostDate(iso: string | undefined, locale?: string): string | null
```

`postByline` returns the props [`PostMeta`](https://docs.systhema.app/nl/components/post-meta.md) needs. `formatPostDate` formats an ISO date as a long date in `locale` (default `'en-US'`, so `'2026-09-14'` becomes "September 14, 2026"), in UTC so the server and the browser agree on the day. It returns `null` for a missing or invalid date. `PartConfig` is the `{ show?, classNames? }` type these helpers read.

### `splitGridItems`

```ts
splitGridItems<T>(items: T[], leadCount?: number): { lead: T[]; tail: T[] }
```

Splits a list into the lead grid and the tail, as `PostsList` does for `leadCount`. The split applies only when `leadCount` is positive and smaller than the list; otherwise everything is in `lead` and `tail` is empty.

```ts
splitGridItems(['a', 'b', 'c', 'd'], 2) // { lead: ['a', 'b'], tail: ['c', 'd'] }
splitGridItems(['a', 'b'], 5) // { lead: ['a', 'b'], tail: [] }
```

## Listing helpers

A listing with a highlight card or a lead grid has uneven pages. `limit` (the "posts per page") counts the main list only; the highlight and the lead cards are extra on page 1. So page 1 holds `limit + extra` posts and every later page holds `limit`. These helpers do that arithmetic, so your `fetchMore` asks for the right slice. For a listing without extras (`extra` is `0`) they reduce to plain page-based paging.

| Helper                                    | Returns                                                                               |
| ----------------------------------------- | ------------------------------------------------------------------------------------- |
| `listingExtra(highlightFirst, leadCount)` | The page-1 extras: 1 for the highlight plus `leadCount` (if > 0)                      |
| `listingPageOneCount(limit, extra)`       | Posts on page 1: `limit + extra`                                                      |
| `listingTotalPages(total, limit, extra)`  | The page count: 1 if everything fits on page 1, else 1 + the rest in pages of `limit` |
| `listingPageOffset(page, limit, extra)`   | The offset of a 1-based page: 0 for page 1, then `limit + extra + (page - 2) * limit` |
| `listingPageCount(page, limit, extra)`    | How many posts to fetch for a page: `limit + extra` on page 1, `limit` after          |

With a highlight, a lead grid of 6 and 12 posts per page, 40 posts in total:

```ts
const extra = listingExtra(true, 6) // 7
listingPageOneCount(12, extra) // 19
listingTotalPages(40, 12, extra) // 3 (19, 12, 9)
listingPageOffset(2, 12, extra) // 19
listingPageCount(2, 12, extra) // 12
```

A `fetchMore` built on them:

```ts
const extra = listingExtra(highlightFirst, leadCount)

async function fetchMore(page: number): Promise<PostView[]> {
  const offset = listingPageOffset(page, limit, extra)
  const count = listingPageCount(page, limit, extra)
  const response = await fetch(`/api/journal?offset=${offset}&limit=${count}`)
  return response.json()
}
```

Payload's archive templates and the `posts` block use the same helpers. See [Archive pages](https://docs.systhema.app/nl/payload/posts/archives.md#lead-posts-split).

## Next.js

Every helper and type on this page is re-exported unchanged from `@systhemaui/next`.

## Related

- [PostsList](https://docs.systhema.app/nl/components/posts-list.md)
- [PostCard and HighlightCard](https://docs.systhema.app/nl/components/post-card.md)
- [Customizing the presentation](https://docs.systhema.app/nl/payload/posts/presentation.md)
