---
title: "Posts module"
description: "Enable the module, its collections, featured media, the editing screen, SEO and the content editor."
requested_language: hu
language: en
translation_notice: "This page isn't translated yet"
url: https://docs.systhema.app/hu/next/payload/posts
version: unreleased (main)
docs_index: https://docs.systhema.app/hu/next/llms.txt
---
> This page isn't translated yet. Showing English.


Systhema ships an optional **Posts module** — a reusable blog/posts capability for `@systhemaui/payload`, with matching presentational components in `@systhemaui/react` / `@systhemaui/next`. It adds `Posts`, `Categories`, and `Tags` collections, a WordPress-style permalink engine, a post-template registry that mirrors page templates, an editor-created Archive PAGE template, a `posts` Lexical block, share-to-network buttons, and two extra built-in roles (Publisher / Author).

The module is **disabled by default**. Everything it adds — collections, capabilities, roles, the General Settings → Posts tab, the sitemap resolver, the routing branch — only exists when you turn it on, so projects that don't blog pay nothing for it.

## At a glance

- **Disabled by default.** Enable with `posts: true` (or a `PostsOptions` object) in `withSysthema()`. `undefined` ⇒ disabled.
- **Three collections.** `Posts` (drafts + autosave + scheduled publish), and the toggleable `Categories` / `Tags` taxonomy collections. Author is a relationship to the existing `users` collection — no separate Authors collection. All three share a **Posts** admin sidebar group so they read as a distinct module (override with a standard collection `admin.group` override).
- **Independent type + template.** A `postType` taxonomy select (for filtering/archives) and a `postTemplate` rendering registry (mirrors `customPageTemplates`) are chosen **independently** on each post. Each registered post template contributes a conditional field group.
- **WordPress-style permalinks.** A configurable pattern (`/posts/{slug}` by default) drives single-post URLs. Posts slot into the `SysthemaPage` resolution chain **after** pages — pages always win at an exact path.
- **Editor-made archives.** `/blog`, `/reviews`, `/videos` are real Pages using the built-in **Archive** PAGE template. No auto-generated archive routes.
- **One listing component.** Latest posts, archive listings, related posts, and the `posts` block all render through the single `PostsList` component (`@systhemaui/react`).
- **Share + related, editor-controlled.** Share-to-network buttons and the related-posts strategy are configured in General Settings → Posts and overridable per post.
- **Two extra roles.** **Publisher** (full posts + taxonomies + publish) and **Author** (write own posts + SEO, no publish/delete) register only when the module is on.

## Enabling it

```ts title="src/payload.config.ts"
import { withSysthema } from '@systhemaui/payload'

export default buildConfig(
  withSysthema(
    {
      posts: true, // shorthand for { enabled: true }
    },
    { /* your base Payload config */ },
  ),
)
```

The object form gives finer control:

```ts
posts: {
  enabled: true,
  categories: true,                 // register the Categories collection (default true)
  tags: true,                       // register the Tags collection (default true)
  permalink: '/{category}/{slug}',  // code-level default; editors can override in General Settings
  types: ['Article', 'Video'],      // post types (default false). Titles → kebab slugs (article, video)
                                    // — or [{ slug, title }] for explicit slugs.
  templates: [/* SysthemaPostTemplate[] — see "Custom post templates" */],
  share: {
    enabled: true,
    networks: ['facebook', 'x', 'linkedin', 'nativeShare'],
    position: 'inline',   // 'inline' | 'sticky-bottom' | 'floating-sidebar'
    style: 'icon',        // 'icon' | 'icon-label' | 'pills'
  },
}
```

`PostsOptions`:

| Option       | Type                                         | Default                                                               | Purpose                                                                                                                                                                                                                                                                                                          |
| ------------ | -------------------------------------------- | --------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `enabled`    | `boolean`                                    | `false`                                                               | Turn the module on. `posts: true` is shorthand for `{ enabled: true }`.                                                                                                                                                                                                                                          |
| `categories` | `boolean`                                    | `true`                                                                | Register the `Categories` taxonomy collection.                                                                                                                                                                                                                                                                   |
| `tags`       | `boolean`                                    | `true`                                                                | Register the `Tags` taxonomy collection.                                                                                                                                                                                                                                                                         |
| `permalink`  | `string`                                     | `'/posts/{slug}'`                                                     | Code-level permalink default. The editable value lives in General Settings.                                                                                                                                                                                                                                      |
| `types`      | `false \| string[] \| { slug, title }[]`     | `false`                                                               | Post types for the `postType` select. Title array → kebab-cased slugs; or explicit `{ slug, title }`, where `title` accepts a Payload `StaticLabel` for the `postType` select option's label in the editor's Admin language (Admin-only, never rendered on the front end). `false`/single type hides the select. |
| `templates`  | `SysthemaPostTemplate[]`                     | `[]`                                                                  | Custom post templates (merged with the top-level `customPostTemplates`).                                                                                                                                                                                                                                         |
| `share`      | `{ enabled?, networks?, position?, style? }` | `{ enabled: true, networks: […], position: 'inline', style: 'icon' }` | Code-level share defaults (incl. bar **position** `inline`/`sticky-bottom`/`floating-sidebar` + button **style** `icon`/`icon-label`/`pills`). The editable values live in General Settings → Posts → Share buttons.                                                                                             |
| `media`      | `{ requirement?, placeholder? }`             | `{ requirement: 'recommended' }`                                      | Featured-media requirement model + placeholder. See [Featured media](#featured-media).                                                                                                                                                                                                                           |

The full option shape is `{ enabled?, categories?, tags?, permalink?, types?, templates?, contentEditor?, share?, media?, listing?, hero?, post?, templateSlots? }`. `undefined` ⇒ disabled (mirrors `redirect`); an object form is on unless `enabled: false`. `types` (default `false`) accepts a title array (slugs kebab-cased automatically) or `{ slug, title }[]`; an explicit `title` accepts a Payload `StaticLabel`, resolved as the `postType` select option's label in the editor's chosen Admin language — it is Admin-only and never rendered on the front end. The last five configure the [presentation/island/media model](https://docs.systhema.app/hu/next/payload/posts/presentation.md). `post` holds two single-post **layout knobs** — `readingWidth` (the body reading-column `max-width`) and `bylineAvatarSize` (the byline avatar size); both are plain CSS lengths applied as custom properties on `#post` and default to the current layout (zero regression). Exported types: `PostsOptions`, `PostLayoutConfig`, `PostTypeInput` (`{ slug, title }`), `PostTypeDef` (resolved `{ value, label }`), `SysthemaPostTemplate`.

> [!WARNING]
> **First-time install changes the schema.** Enabling posts adds tables, versioned siblings, and PostgreSQL enums. Dev push can prompt interactively and hang with stdin closed. `PAYLOAD_FORCE_DRIZZLE_PUSH=true` does not suppress those prompts. On a push-only PostgreSQL project, review and apply the additions before starting the updated application:
>
> ```bash
> NODE_ENV=production systhema migrate --schema --dry-run
> NODE_ENV=production systhema migrate --schema
> ```
>
> Projects with Payload migration history should continue generating and deploying project-owned migrations. See [Non-interactive schema migration](https://docs.systhema.app/hu/next/payload/database/schema-migration.md) for supported changes and refusal cases.

## Collections

- **`Posts`** — drafts + autosave + scheduled publish. Fields: `title`, a tabs group (per-template `Content` field groups + an SEO `meta` tab gated on `posts.seo.update`), `postType` / `postTemplate` selects, `category` (→ categories), `tags` (→ tags, hasMany), `author` (→ users), `publishedAt`, `featuredImage`, `excerpt`, `relatedPosts` (self-relation), and a unique permalink-aware `slug`. Access: `capabilityOrPublished('posts.read')`, `requireCapability('posts.create')`, `requireUpdateOrScoped('posts')`, `requireCapability('posts.delete')`.
- **`Categories`** / **`Tags`** — toggleable taxonomy collections (`name`, `slug`, Categories also has `description`). Access mirrors Posts via `publicOrCapability`.

Author is a relationship to the existing `users` collection — there is no separate Authors collection. Posts + taxonomies sit right after `pages` in the default collection order.

## Featured media

Each post has a **Featured image** upload field — the card / social-share thumbnail, **distinct from the hero** (the in-page banner). It's **optional**: when a post has no featured image, the resolved media falls back to the **post hero image** (when the hero uses an image), then a placeholder, then nothing. The `posts.media` option controls how strictly it's treated and supplies the placeholder for the in-between case:

```ts
posts: {
  enabled: true,
  media: {
    requirement: 'recommended', // 'required' | 'recommended' | 'optional'
    placeholder: '/images/post-fallback.jpg', // image URL or path
  },
}
```

| `requirement` | Field is required? | Missing-media behaviour                                                                   | OG / social-share thumbnail                  |
| ------------- | ------------------ | ----------------------------------------------------------------------------------------- | -------------------------------------------- |
| `required`    | **Yes**            | Editors must upload an image to save.                                                     | Always the post's own image.                 |
| `recommended` | No (default)       | Falls back to the hero image, then the configured `placeholder`, in cards/hero/highlight. | Post image → hero image → `placeholder`.     |
| `optional`    | No                 | Falls back to the hero image; no placeholder is injected if there's none.                 | Post image → hero image → global default OG. |

- **Resolution chain.** The card / listing media resolves as **featured image → post hero image (when it's an image) → placeholder (unless `optional`) → none**. The hero image is read from the active template's `hero` field group, so a post with a banner hero but no featured image still shows that hero in its cards.
- **OG fallback.** `generatePostMetadata` resolves the OpenGraph/Twitter image as **post SEO image → featured image → hero image → `placeholder` → site-default OG**. So `required` posts always have a thumbnail, and the hero image stands in before the placeholder.
- **Placeholder in rendering.** When a post has no featured AND no hero image, and the requirement is `required`/`recommended` with a `placeholder` set, the resolved `PostView.media` carries the placeholder URL with `media.isPlaceholder: true`, so cards/hero/highlight render it and a Tier-2 island can tell a real image apart from the fallback. Under `optional` (or with no placeholder configured) `media` is `null`.
- The field's admin description reflects the requirement (e.g. recommended → _"Optional — … falls back to the post hero image, then a placeholder…"_).

## General Settings Posts tab

When posts is enabled, a **Posts** tab is inserted right after the Pages tab: `permalink` pattern (field-level guarded by `global.general-settings.posts.update`), `showAuthor` / `showDate` byline toggles, a `share` group (`enabled` + `networks` multiselect + a `position` select — `inline` / `sticky-bottom` / `floating-sidebar` — and a `style` select — `icon` / `icon-label` / `pills`), and a `related` group (`strategy` — `manual-then-auto` / `manual-only` / `auto-only`; `autoSource` — `both` / `tags` / `category`; `limit`, default 3) that drives `resolveRelatedPosts`. The posts-option `permalink` / `share` act as code-level defaults; these fields are the runtime source of truth. The `related` defaults reproduce the original hardcoded behavior, so an un-configured project is unchanged.

## The post editing screen

The Posts edit view mirrors Pages — a `title` field, a **Content** / **SEO** tab pair, and a sidebar:

- **Content tab** — the per-template content (the default template's hero + `content` body), then **Related posts** at the bottom. Related posts is a top-level field rendered here because it's core to every post regardless of template.
- **SEO tab** — present only when the [`seo`](https://docs.systhema.app/hu/next/payload.md) feature is enabled, identical to the Pages SEO tab (Meta Title, Meta Description, Social Media Share Image, search-visibility toggle, live preview snippet). See [SEO metadata](#seo-metadata) for the fallback behaviour.
- **Sidebar**, in order: **URL path** (the editable slug) followed by a live **permalink preview** that shows the final URL with the active pattern's tokens resolved from the post's own slug / category / date (e.g. `domain/reviews/2026/my-post`), updating as you edit; **Excerpt**, **Featured image** (the optional card thumbnail — see [Featured media](#featured-media)), **Published date**, **Author**, **Category**, **Tags**, **Post Type** + **Post Template** (each hidden when only one option exists). **Category** and **Tags** create as you type: when no existing category or tag matches the typed name (ignoring case), the dropdown offers **Create "…"**, and picking it creates the document and selects it, with no drawer and no separate save. Existing matches stay first, so Enter picks one of them. The input keeps focus, so you can type the next tag straight away. In **Tags**, a comma also commits the typed name, and pasting a comma- or line-separated list selects every tag in it, creating the missing ones. Editors without the `categories.create` / `tags.create` capability get the plain select. There's no per-post "featured" flag — a listing's lead/highlight is simply its first post (`leadCount`). The preview is a read-only UI field (`permalinkPreviewField`) that fetches the resolved pattern from `/sys/permalink-pattern` and builds the URL with the same `buildPermalink` engine the server uses.

## SEO metadata

`generatePostMetadata` builds a post's `<title>`, description, OpenGraph, and Twitter tags with graceful fallbacks — nothing in the SEO tab is mandatory:

| Tag                    | Fallback chain                                                                                                                                             |
| ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Title**              | SEO Meta Title → **post title** → website name                                                                                                             |
| **Description**        | SEO Meta Description → **excerpt** → **the post body's plain text** (whitespace-collapsed, word-boundary truncated) → site SEO global description          |
| **OG / Twitter image** | SEO Social Share Image → **featured image** → **post hero image** → configured `media.placeholder` → site-default OG image → _(none — the tag is dropped)_ |

Posts go through the **same site title template as pages**: General Settings → SEO's _Title template_, _Title separator_ and _Website name_ (falling back to the `seo.defaults` plugin option) are applied to the post title, so an article ships `Ten ways to ship faster – Acme`, not a bare headline. A post that sets its own **SEO Meta Title** keeps it verbatim, exactly like a page. Change the template once and every page and post follows.

The OG image chain ties into the [`media` requirement](#featured-media): a `required` post always has a thumbnail, and a `recommended` post falls back to the placeholder before the global default. The body-text description fallback reads the resolved template body (`postData[postTemplate].content`) via `lexicalToPlainText`, so even a post with no excerpt gets a meaningful `<meta name="description">`.

## The post content editor

The default post template's **body** (`content`) field uses the **region-level** Lexical editor by default — paragraph / heading / list + all **inline blocks** (button, chip, icon, avatar) + the standard region **blocks** (image, video, youtube, carousel, accordion, quote, embed, form, stack) + **card**, **columns** and **gallery**, all gated by which `blocks.*` you've enabled. It's narrower than the page **root** editor (which also offers the page-layout `Section` / `Feature` / `Posts` / `Components` blocks).

A Gallery in a post body renders as a full-width band of the article, the same as on a page: its content and slides sit in the design-system container, and `readingWidth` does not narrow it.

Swap it per project with `posts.contentEditor`:

```ts
withSysthema(payloadConfig, {
  posts: {
    enabled: true,
    // A level string — 'root' | 'region' (default) | 'fragment' | 'slot':
    contentEditor: 'root', // give posts the full page editor
  },
})
```

Or pass a **fully custom editor** — e.g. to drop `card`/`columns`/`gallery`, or to add **posts-specific** custom blocks without affecting the region editor used elsewhere (Section bodies, etc.):

```ts
import { regionLexicalEditor } from '@systhemaui/payload' // or build your own with lexicalEditor({...})

withSysthema(payloadConfig, {
  posts: { enabled: true, contentEditor: regionLexicalEditor({ withCard: true, withColumns: true }) }, // no gallery
})
```

The editor factories are part of the public API — `regionLexicalEditor`, `rootLexicalEditor`, `fragmentLexicalEditor`, `slotLexicalEditor` (and `withLazyMount` / `richTextField`) all import from `@systhemaui/payload`. **Use `regionLexicalEditor()` for a custom post template's body** rather than a bare `lexicalEditor()` — the latter ships Payload's full, unscoped feature set (tables, all heading levels, etc.) instead of the Systhema region set.

This only swaps the **default** post template's body. Custom post templates set their own `content` editor (and ignore `posts.contentEditor`) — reuse `postBodyField` from `@systhemaui/payload` if you want the default body in a custom template.
