# Introduction Source: https://pagescms.org/docs/ What Pages CMS is, who it is for, and how it works. ## What Pages CMS is [Pages CMS](https://pagescms.org) is an open-source CMS for static sites stored in GitHub repositories. It edits files in your repository directly. There is no separate CMS database for content. ## Why it exists Most static sites do not need a database-backed CMS. They already have: - content in files, - media in the repository, - Git history, - a deployment flow. The missing piece is usually the editing experience. Pages CMS gives teams a UI for editing content and media without asking every editor to learn Git. ## How it works 1. Add a `.pages.yml` file to the repository. 2. Define `content`, `media`, and any optional `components` or `settings`. 3. Sign in to Pages CMS. 4. Edit content in the UI. 5. Save changes back to GitHub. ## What Pages CMS does not do Pages CMS does not replace your site generator, deployment platform, or repository workflow. It only provides the editing layer on top of your existing Git-based project.
Quick start {% lucide "arrow-right" %} Configuration overview {% lucide "arrow-right" %}
--- # Quick start Source: https://pagescms.org/docs/quick-start/ The fastest path from zero to a working Pages CMS setup. ## Quick start If you just want to try Pages CMS, use the hosted app. 1. Go to [app.pagescms.org](https://app.pagescms.org). 2. Sign in with GitHub. 3. Install the GitHub App on the account or organization that owns your repository. 4. Open the repository you want to edit. 5. Create `.pages.yml` when prompted. 6. Start editing. ## Minimal config Use this as a first working config: ```yaml media: media content: - name: pages label: Pages type: collection path: docs fields: - name: title type: string - name: body type: rich-text ``` This gives you: - one media folder at `media/`, - one editable collection at `docs/`, - a `title` field, - a rich-text `body` field. ## What to do next 1. Add more fields. 2. Configure media storage. 3. Adjust filenames and collection view.
Configuration overview {% lucide "arrow-right" %} Deploy your own instance {% lucide "arrow-right" %}
--- # Media Source: https://pagescms.org/docs/configuration/media/ Configure where uploaded files are stored and what URLs are written. ## What `media` does `media` defines file storage for: - [image](/docs/configuration/fields/image/) fields, - [file](/docs/configuration/fields/file/) fields, - [rich-text](/docs/configuration/fields/rich-text/) image uploads. Use it to answer two questions: 1. Where should uploaded files be saved in the repository? 2. What public path should be written into content? Media sources can also define `actions`. See [`actions`](/docs/configuration/actions/). ## Value You can define `media` as: - a string, - one object, - an array of named media sources. ### String form ```yaml media: media ``` Equivalent to: ```yaml media: input: media output: /media ``` ### Single media object ```yaml media: input: src/media output: /media rename: random categories: [image] ``` ### Multiple media sources Use an array when different field types should write to different folders. ```yaml media: - name: images label: Images input: media/images output: /media/images rename: safe extensions: [png, jpg, webp] actions: - name: optimize-images label: Optimize images workflow: pages-cms-media-action.yml - name: docs label: Documents input: media/docs output: /media/docs categories: [document] ``` ## Keys Key | Description --- | --- name ** | Internal media source name. Required when using an array (e.g. `"images"`). label | UI label for the media source (e.g. `"Product images"`). input * | Repository path where files are stored (e.g. `"src/media"`). output * | Public path written into content (e.g. `"/media"`). extensions | Allowed extensions (e.g. `["png", "webp"]`). categories | Category-based extension sets. Values: `image`, `document`, `video`, `audio`, `compressed`, `code`, `font`, `spreadsheet`. rename | Controls upload renaming. Use `false` to keep the original filename (default behavior), `true` or `safe` to slugify it, or `random` for a generated name. commit | Per-media commit settings. [See `settings.commit`](#/docs/configuration/settings/) . actions | Adds media action buttons. [See `actions`](/docs/configuration/actions/). *: Required **: Required with multiple sources Field-level media options can override the media source defaults. ## Commit templates `media[].commit.templates` overrides the global commit templates for that media source. ```yaml media: - name: images input: media/images output: /media/images commit: templates: create: "chore(media): add {filename}" update: "chore(media): update {filename}" delete: "chore(media): remove {filename}" rename: "chore(media): rename {oldFilename} -> {newFilename}" ```
Content {% lucide "arrow-right" %} Actions {% lucide "arrow-right" %} Image field {% lucide "arrow-right" %} File field {% lucide "arrow-right" %}
--- # Editors Source: https://pagescms.org/docs/configuration/content/editors/ Choose between structured fields, raw files, code, and datagrid editors. Pages CMS can edit files in a few different ways depending on the content shape. ## Structured fields Use `fields` when the file or collection should be modeled as structured content. ```yaml content: - name: posts type: collection path: content/posts fields: - name: title type: string - name: body type: rich-text ``` ## Raw file editor If `fields` is omitted or empty, Pages CMS falls back to a raw file editor. Use this for files that should not be modeled as structured fields, for example: - `robots.txt` - redirect files - small JSON or YAML config files - snippets or templates ```yaml content: - name: robots label: robots.txt type: file path: public/robots.txt ``` ## Code editor Use `format: code` when you want a code-oriented editor for a single file. ```yaml content: - name: redirects type: file path: public/_redirects format: code ``` ## Datagrid editor Use `format: datagrid` for CSV-style tables. For `.csv` files, Pages CMS can infer this automatically. ```yaml content: - name: pricing type: file path: data/pricing.csv ``` Or set it explicitly: ```yaml content: - name: pricing type: file path: data/pricing format: datagrid ``` --- # Filename Source: https://pagescms.org/docs/configuration/content/filename/ Control how new collection entries are named. `filename` only applies to collections. Use it to control how new files are named. ## Value You can either use a string: ```yaml filename: "{primary}.md" ``` Or use the object form when you also want a filename field in the editor: ```yaml filename: template: "{year}-{month}-{day}-{primary}.md" field: create ``` `filename.field` can be: - `false`: Hide the filename input. - `create`: Show it only when creating a new entry. - `true`: Show it when creating and editing. ## Tokens Token | Description --- | --- `{primary}` | Primary field from `view.primary`, or `title`, or the first field. `{slug}` | Alias for `{primary}`. `{year}` | Current year. `{month}` | Current month, zero-padded. `{day}` | Current day, zero-padded. `{hour}` | Current hour, zero-padded. `{minute}` | Current minute, zero-padded. `{second}` | Current second, zero-padded. `{fields.}` | Field value from the current entry, slugified. `{}` | Shorthand for `{fields.}`. ## Examples ### Date-based post filenames ```yaml filename: "{year}-{month}-{day}-{title}.md" ``` ### Editable filename on create ```yaml filename: template: "{primary}.md" field: create ``` --- # List Source: https://pagescms.org/docs/configuration/content/list/ Configure repeated fields and top-level arrays. `list` is used in two places: - on fields, to repeat a field as an array - on `type: file` content entries, to store the whole file as a top-level array ## Field lists Set `list: true` to repeat a field. ```yaml - name: tags type: string list: true ``` You can also use the object form: Key | Description --- | --- min | Minimum number of items. max | Maximum number of items. collapsible | Make object or block items collapsible. Values: `true`, `false`, or an object. ### `collapsible` Use `collapsible` on `object` or `block` lists to collapse each item in the editor. You can use a boolean: ```yaml - name: sections type: object list: collapsible: true fields: - name: title type: string ``` Or the object form: Key | Description --- | --- collapsed | Values: `true`, `false`. Whether items start collapsed by default. summary | Summary text shown for each collapsed item. `summary` supports: Token | Description --- | --- `{index}` | 1-based list item index. `{fields.}` | Field value from the current object item (e.g. `{fields.title}`). `{}` | Shorthand for `{fields.}` (e.g. `{title}`). ## Top-level arrays in files For `type: file`, set `list: true` when the file content itself is an array. ```yaml content: - name: authors label: Authors type: file path: data/authors.json format: json list: true fields: - name: name type: string - name: email type: string - name: avatar type: image ``` For content entries, the documented form is `list: true`. ## Example ```yaml - name: sections type: object list: min: 1 max: 6 collapsible: collapsed: true summary: "{title} ({index})" fields: - name: title type: string - name: body type: rich-text ``` --- # Operations Source: https://pagescms.org/docs/configuration/content/operations/ Control create, rename, and delete behavior for content entries. Use `operations` to allow or block create, rename, and delete in the UI for a specific `content` entry. ## Keys Key | Description --- | --- create | Allow creating new entries/files for this content entry. rename | Allow renaming entries/files for this content entry. delete | Allow deleting entries/files for this content entry. ## Defaults Defaults depend on `type`: Type | create | rename | delete --- | --- | --- | --- `collection` | `true` | `true` | `true` `file` | `true` | `false` | `true` ## Notes - `settings` (`.pages.yml`) are separate from `content`; deleting settings is disabled by default. - Any key can be set to `false` to block that operation for the entry. ## Examples ### Disable delete for a single file ```yaml content: - name: site label: Site settings type: file path: data/site.yml operations: delete: false ``` ### Disable rename and delete for a collection ```yaml content: - name: posts type: collection path: content/posts operations: rename: false delete: false ``` --- # Fields Source: https://pagescms.org/docs/configuration/content/fields/ Define the editor schema for collections and files. `fields` defines the editor schema. ## Keys These keys apply to all field types unless noted otherwise. Key | Description --- | --- name * | Field key used in stored data. label | UI label for the field. Set `false` to hide it. type * | Field type. Use `type` or `component`. component | Reuse a field definition from `components`. Use `component` or `type`. required | Marks the field as required. pattern | Regex validation for supported field types. hidden | Hides the field from the editor. readonly | Shows the field value but prevents editing it. Inherited by nested object, block, and list fields. description | Helper text shown below the field. options | Field-specific options. See each field page for details. *: Required ## `pattern` `pattern` validates a field value against a regex. Currently, it is supported by: - `string` - `text` You can use either a string: ```yaml pattern: "^[a-z0-9-]+$" ``` Or an object with a custom message: ```yaml pattern: regex: "^[A-Z]{3}-\\d{4}$" message: "Use format ABC-1234" ``` ## `component` Use `component` to reference a reusable field definition from [`components`](/docs/configuration/components/). A field must use exactly one of: - `type` - `component` Example: ```yaml components: seo: type: object label: SEO fields: - name: title type: string - name: description type: text content: - name: pages type: collection path: content/pages fields: - name: seo component: seo label: Meta ``` ## `body` is a special key For frontmatter formats, `body` maps to the file content below the frontmatter. All other fields stay in frontmatter. ```yaml fields: - name: title type: string - name: body type: rich-text ``` ## Field types Type | Description --- | --- [`block`](/docs/configuration/fields/block/) | Multiple object shapes in one list. [`boolean`](/docs/configuration/fields/boolean/) | True/false toggle. [`code`](/docs/configuration/fields/code/) | Code editor with syntax highlighting. [`date`](/docs/configuration/fields/date/) | Date or date-time input. [`file`](/docs/configuration/fields/file/) | File picker or uploader. [`image`](/docs/configuration/fields/image/) | Image picker or uploader. [`number`](/docs/configuration/fields/number/) | Numeric input. [`object`](/docs/configuration/fields/object/) | Nested group of fields. [`reference`](/docs/configuration/fields/reference/) | Link to another collection. [`rich-text`](/docs/configuration/fields/rich-text/) | Rich text editor. [`select`](/docs/configuration/fields/select/) | Fixed local options. [`string`](/docs/configuration/fields/string/) | Single-line text input. [`text`](/docs/configuration/fields/text/) | Multi-line plain text input. [`uuid`](/docs/configuration/fields/uuid/) | UUID v4 field. ## Examples ### Frontmatter with body content ```yaml fields: - name: title type: string - name: published type: boolean - name: body type: rich-text ``` ### Nested object field ```yaml fields: - name: author type: object fields: - name: name type: string - name: email type: string ``` ### String field with pattern ```yaml fields: - name: slug type: string required: true pattern: "^[a-z0-9-]+$" options: minlength: 3 maxlength: 80 ``` ### Text field with custom pattern message ```yaml fields: - name: summary type: text required: true pattern: regex: "^(?s).{20,500}$" message: "Summary must be between 20 and 500 characters" options: minlength: 20 maxlength: 500 ``` --- # View Source: https://pagescms.org/docs/configuration/content/view/ Control how collections are listed in the editor. `view` only applies to collections. It controls how the collection list is displayed. ## Keys Key | Description --- | --- fields | Fields shown in the list, in order (e.g. `["title", "published", "author.name"]`). primary | Field used as the main label. Defaults to `title` if present. sort | Fields available for sorting. search | Fields indexed for search. default.search | Default search query. default.sort | Default sort field. default.order | Values: `asc`, `desc`. layout | Values: `list`, `tree`. node | Tree node config, as a string or object. node.filename | Node filename in tree mode. node.hideDirs | Values: `all`, `nodes`, `others`. ## Examples ### Basic list view ```yaml view: fields: [title, published, date] primary: title sort: [date, title] default: sort: date order: desc ``` ### Tree view ```yaml view: layout: tree node: filename: index.md hideDirs: others fields: [title] primary: title ``` --- # Components Source: https://pagescms.org/docs/configuration/components/ Reuse field definitions across multiple content models. ## What `components` does Use `components` when the same field group appears in multiple places. Typical examples: - SEO fields, - author objects, - call-to-action blocks, - repeated metadata groups. Define the field once, then reference it from `content`. ## Example ```yaml components: seo: type: object label: SEO fields: - name: title type: string - name: description type: text content: - name: pages type: collection path: content/pages fields: - name: heading type: string - name: seo component: seo label: Meta ``` ## Override behavior When you reference a component, field-level values can override component values. In the example above: - the component label is `SEO`, - the field label overrides it to `Meta`. The resolved field behaves like this: ```yaml - name: seo type: object label: Meta fields: - name: title type: string - name: description type: text ``` --- # Actions Source: https://pagescms.org/docs/configuration/actions/ Add custom GitHub Actions buttons to Pages CMS. ## What actions are Actions allow you to add custom buttons that trigger GitHub Actions. They can appear: - at the repository level in the sidebar, - in the header of collection pages, collection entry pages, file pages and media pages. These actions start a GitHub Actions workflow with `workflow_dispatch` and [a `payload` input](#configuration-on-github) that contains contextual information about the trigger (e.g. sha, path of the entry, custom user inputs, etc). ## Keys Each action may use the following keys: Key | Description --- | --- name * | Internal action name. label * | Button label shown in the UI. workflow * | Workflow file name in `.github/workflows/`. ref | Git ref used to dispatch the workflow. Use `current` to use the branch currently open in Pages CMS. scope | Collection-only. Values: `collection`, `entry`. cancelable | Whether the run can be cancelled from Pages CMS. Defaults to `true`. confirm | Confirmation dialog config. Use `false` to skip confirmation. fields | Extra input fields collected before dispatch. *: Required ## Confirmation Actions show a confirmation dialog by default when triggered. Set `confirm: false` to skip it or customize the title, message and button label of the dialog: ```yaml actions: - name: deploy-site label: Deploy site workflow: pages-cms-action.yml confirm: title: Deploy site? message: This will trigger the deployment workflow. button: Deploy ``` ## Extra fields Use `fields` to collect extra values that will be passed to the GitHub Actions workflow via `payload.inputs`. Each field may use the following keys: - `name`: field name (used in `payload.inputs`). - `label`: field label. - `type`: one of `text`, `textarea`, `select`, `checkbox`, or `number`. - `required`: whether the field is required. - `default`: default value for the field. - `options`: an array of label/value objects for `select` fields. The form for these values is shown in the same dialog before the action is dispatched. If `fields` are defined, the dialog is still shown even when `confirm: false`. Example: ```yaml actions: - name: deploy-site label: Deploy site workflow: pages-cms-action.yml fields: - name: environment label: Environment type: select required: true default: staging options: - label: Staging value: staging - label: Production value: production - name: force label: Force deploy type: checkbox default: false ``` ## Configuration on GitHub GitHub Actions need to be enabled for the repository. You will need to create a workflow that accepts a `payload` input: ```yaml on: workflow_dispatch: inputs: payload: description: Pages CMS payload as JSON required: true type: string ``` Pages CMS will send one JSON object inside `inputs.payload` which includes: - action metadata, - repository metadata, - triggering user, - context, - extra field values in `payload.inputs`. Example shape: ```json { "source": "pages-cms", "action": { "name": "deploy-site", "label": "Deploy site" }, "repository": { "owner": "pagescms", "repo": "website", "ref": "main", "workflowRef": "main", "sha": "abc123..." }, "triggeredAt": "2026-03-30T12:00:00.000Z", "triggerType": "rerun", "rerunOfActionRunId": 42, "triggeredBy": { "userId": "...", "name": "Ronan Berder", "email": "...", "githubUsername": "hunvreus", "image": "..." }, "context": { "type": "entry", "name": "posts", "path": "content/posts/hello.md", "data": {} }, "inputs": { "environment": "staging", "force": false } } ``` Use `jq`, `node`, or `actions/github-script` inside the workflow to parse `payload.inputs`. ## Permissions and cancellation - `Run again` is only available to GitHub users. - `Cancel run` is available to GitHub users for any active run. - Collaborators can only cancel their own active runs. - `Cancel run` is only available once the GitHub workflow run exists. - Set `cancelable: false` to disable cancellation for an action. ## Examples ### Root action ```yaml actions: - name: deploy-site label: Deploy site workflow: pages-cms-action.yml ref: current cancelable: false ``` ### Collection action ```yaml content: - name: posts label: Posts type: collection path: content/posts fields: - name: title type: string actions: - name: rebuild-posts label: Rebuild posts scope: collection workflow: pages-cms-collection-action.yml - name: preview-post label: Preview scope: entry workflow: pages-cms-entry-action.yml ``` ### File and media ```yaml content: - name: site label: Site settings type: file path: data/site.yml fields: - name: title type: string actions: - name: validate-config label: Validate config workflow: pages-cms-file-action.yml media: - name: images label: Images input: media/images output: /media/images actions: - name: optimize-images label: Optimize images workflow: pages-cms-media-action.yml ``` --- # Collaborators Source: https://pagescms.org/docs/configuration/collaborators/ What collaborators can do and how their changes are committed. ## What collaborators are Collaborators are invited by email. Use them when someone needs to edit content or media but does not have a GitHub account. ## What collaborators can do Collaborators can: - open repositories they were invited to, - edit content, - edit media. Content operations (`create`, `rename`, `delete`) still follow the repository configuration for each `content` entry. See [`content.operations`](/docs/configuration/content/operations/). ## What collaborators cannot do Collaborators cannot: - manage `.pages.yml`, - manage collaborators, - access cache admin features. Those actions stay limited to GitHub users with repository access. ## How collaborator commits work Collaborator writes use the GitHub App installation token for the target repository. By default, Pages CMS does not send the collaborator's name and email as committer metadata. That means: - the write is authorized by the GitHub App, - the commit uses the authenticated app/installation identity unless commit identity is explicitly set to `user`. If you want collaborator writes to include the collaborator's name and email as committer metadata, set `settings.commit.identity: user` or override a specific schema with `commit.identity: user`. ## Migration Collaborators live in the database, not in `.pages.yml`. If you move to a new Pages CMS install, export and import collaborators separately. --- # Settings Source: https://pagescms.org/docs/configuration/settings/ Configure repository-wide behavior in Pages CMS. ## What `settings` does Use `settings` for behavior that applies across the whole repository. Typical uses: - hide admin pages, - preserve unmanaged keys when saving structured content, - define default commit messages, - choose commit identity behavior. ## Keys Key | Description --- | --- hide | If `true`, hides the Settings page in the UI. content | Controls how structured content is saved. [See `Content`](#content). commit | Controls commit settings. [See `Commit`](#commit). ## Content For now, `settings.content` only supports `merge`. Value | Behavior --- | --- `false` | Default. Rewrite the file from the configured schema only. Anything outside the submitted editor output is removed. `true` | Merge the submitted fields into the existing file before saving. Keys outside the schema are preserved unless the editor overwrites them. ## Commit ### Commit templates `settings.commit.templates` defines the default commit message format for content and media changes. Key | Default value --- | --- `create` | `Create {path} (via Pages CMS)` `update` | `Update {path} (via Pages CMS)` `delete` | `Delete {path} (via Pages CMS)` `rename` | `Rename {oldPath} to {newPath} (via Pages CMS)` `content[].commit.templates` and `media[].commit.templates` override these global templates. Within these templates, you can use any of the following tokens: Token | Description --- | --- {action} | Current action: `create`, `update`, `delete`, or `rename`. {path} | File path. {filename} | File name only. {name} | Content or media entry name. {owner} | Repository owner. {repo} | Repository name. {branch} | Current branch. {user} | Current user identifier. Kept for compatibility. {userName} | Current user display name when available. {userEmail} | Current user email when available. {oldPath} | Previous path. Rename only. {newPath} | New path. Rename only. {oldFilename} | Previous file name. Rename only. {newFilename} | New file name. Rename only. Prefer `{userName}` and `{userEmail}` in new templates. `{user}` remains available as a legacy fallback. ### Commit identity `settings.commit.identity` controls whether Pages CMS sends explicit committer metadata on writes. Value | Behavior --- | --- `app` | Default. Do not send explicit committer metadata. GitHub uses the authenticated writer identity for the request. `user` | Send the current user's name and email as committer metadata when available. `content[].commit.identity` and `media[].commit.identity` override the global setting for a specific schema. ## Examples ### Global commit templates and identity ```yaml settings: commit: identity: app templates: create: "content(create): {path}" update: "content(update): {path}" delete: "content(delete): {path}" rename: "content(rename): {oldPath} -> {newPath}" ``` ### Global default with a media override ```yaml settings: commit: identity: app media: - name: assets input: public/uploads output: /uploads commit: identity: user templates: update: "media(update): {path} by {userEmail}" ``` --- # Block field Source: https://pagescms.org/docs/configuration/fields/block/ Let editors choose between multiple object shapes. Use `type: block` for page-builder style sections where each item can be a different schema. Each block item is stored as an object containing the selected block type and that block's fields. ## Options Key | Description --- | --- blocks | List of available block definitions (e.g. `[{ name: "hero", component: "hero" }]`). blockKey | Key used to store the selected block type. Defaults to `_block` (e.g. `blockKey: type` saves `type: hero`). ## Examples ### Simple block list ```yaml - name: sections label: Sections type: block list: true blockKey: type blocks: - name: hero component: hero - name: text fields: - name: body type: rich-text ``` Saved output: ```yaml sections: - type: hero heading: Welcome image: /images/hero.jpg - type: text body: Hello world ``` ### Block with a nested object ```yaml - name: sections type: block list: true blockKey: type blocks: - name: cta fields: - name: button type: object fields: - name: label type: string - name: url type: string ``` Saved output: ```yaml sections: - type: cta button: label: Read more url: /about ``` ### Block with a nested list ```yaml - name: sections type: block list: true blockKey: type blocks: - name: faqs fields: - name: items type: object list: true fields: - name: heading type: string - name: text type: rich-text ``` Saved output: ```yaml sections: - type: faqs items: - heading: What is Pages CMS? text: A Git-backed CMS. - heading: Is it open source? text: Yes. ``` ### Nested repeated data inside a block ```yaml - name: faqs fields: - name: items type: object list: true fields: - name: heading type: string - name: text type: rich-text ``` Saved output: ```yaml sections: - type: faqs items: - heading: What is Pages CMS? text: A Git-backed CMS. - heading: Is it open source? text: Yes. ``` This is not supported as a block root shape: ```yaml - name: faqs list: true component: faqs ``` If you need repeated structured data inside a block, nest a list field inside the block object. --- # Boolean field Source: https://pagescms.org/docs/configuration/fields/boolean/ True/false toggle. Use for flags like `published`, `featured`, or `archived`. ## Options This field has no field-specific options. ## Examples ### Basic boolean ```yaml - name: published type: boolean default: true ``` --- # Code field Source: https://pagescms.org/docs/configuration/fields/code/ Code editor with syntax highlighting. Use for snippets, templates, or small config blocks. ## Options Key | Description --- | --- format | Values: `yaml`, `yml`, `javascript`, `js`, `jsx`, `typescript`, `ts`, `tsx`, `json`, `html`, `htm`, `markdown`, `mdx`. ## Examples ### JavaScript snippet ```yaml - name: snippet type: code options: format: javascript ``` ### MDX snippet ```yaml - name: article type: code options: format: mdx ``` --- # Date field Source: https://pagescms.org/docs/configuration/fields/date/ Date or date-time input with formatting and bounds. Use for publish dates, events, or scheduling metadata. ## Options Key | Description --- | --- time | If `true`, includes a time picker. format | Output format string, using [`date-fns`](https://date-fns.org/) tokens (e.g. `"yyyy-MM-dd"`). min | Lower allowed value (e.g. `"2025-01-01"`). max | Upper allowed value (e.g. `"2025-12-31"`). step | Input step value (e.g. `60`). ## Notes - Date fields initialize to the current local date. - Datetime fields (`time: true`) initialize to the current local date and time. - To keep a date field empty by default, set `default: ""`. ## Examples ### Date and time ```yaml - name: publish_at type: date default: "" options: time: true format: yyyy-MM-dd'T'HH:mm ``` --- # File field Source: https://pagescms.org/docs/configuration/fields/file/ Select or upload non-image files from media. Use for PDFs, ZIPs, docs, audio, and similar files. ## Options Key | Description --- | --- media | Named media config to use (e.g. `"docs"`). path | Default browsing folder (e.g. `"contracts"`). multiple | Allow multiple files (e.g. `{ max: 5 }`). extensions | Allowed file extensions (e.g. `["pdf", "zip"]`). categories | Allowed file categories. Values: `image`, `document`, `video`, `audio`, `compressed`. unique | If `true`, disallows duplicate file paths when `multiple` is enabled. rename | Controls upload renaming. Use `false` to keep the original filename, `true` or `safe` to slugify it, or `random` for a generated name. ## Examples ### Named media config and default folder ```yaml - name: brochure label: Brochure type: file options: media: docs path: public/files/brochures ``` ### Multiple downloadable resources ```yaml - name: resources label: Resources type: file options: categories: [document] multiple: max: 5 unique: true rename: true ``` ### Specific file extensions ```yaml - name: archive label: Archive type: file options: extensions: [zip, tar, gz] ``` --- # Image field Source: https://pagescms.org/docs/configuration/fields/image/ Select or upload images from media. Use for cover images, thumbnails, galleries, logos. ## Options Key | Description --- | --- media | Named media config to use (e.g. `"images"`). path | Default browsing folder (e.g. `"blog"`). multiple | Allow multiple images (e.g. `{ max: 6 }`). extensions | Allowed image extensions (e.g. `["jpg", "png", "webp"]`). categories | Allowed image categories. Values: `image`. unique | If `true`, disallows duplicate image paths when `multiple` is enabled. rename | Controls upload renaming. Use `false` to keep the original filename, `true` or `safe` to slugify it, or `random` for a generated name. ## Examples ### Named media config and default folder ```yaml - name: cover label: Cover image type: image options: media: images path: public/images/posts ``` ### Multiple images ```yaml - name: gallery label: Gallery type: image options: multiple: max: 6 unique: true extensions: [jpg, png, webp] rename: true ``` ### Category-based restriction ```yaml - name: thumbnail label: Thumbnail type: image options: categories: [image] ``` --- # Number field Source: https://pagescms.org/docs/configuration/fields/number/ Numeric input for integers and decimals. Use for prices, weights, scores, or rankings. ## Options Key | Description --- | --- min | Minimum value (e.g. `0`). max | Maximum value (e.g. `100`). ## Examples ### Basic number ```yaml - name: price type: number options: min: 0 ``` --- # Object field Source: https://pagescms.org/docs/configuration/fields/object/ Group nested fields under one key. Use for structured data like addresses, SEO, or author profiles. ## Options Key | Description --- | --- None | This field has no field-specific options. Define nested `fields` instead. ## Behavior - `required` applies to the object itself, not automatically to every child. - For optional objects, child `required` rules apply only once the object has meaningful content. - `readonly` is inherited by the whole nested subtree. ### Readonly object list ```yaml - name: authors type: object readonly: true list: true fields: - name: name type: string - name: email type: string ``` In this example, `name` and `email` behave as readonly without needing their own `readonly: true`. ## List summary tokens If the object field is also a list and uses `list.collapsible.summary`, the summary string supports tokens. Without a custom summary, Pages CMS falls back to `Item #n`. Token | Description --- | --- `{index}` | 1-based list item index. `{fields.}` | Field value from the current object item (e.g. `{fields.title}`). `{}` | Shorthand alias for `{fields.}` when no direct token resolves (e.g. `{title}`). Notes: - `{index}` is always available. - `{fields.}` reads from the current object item, so nested paths like `{fields.seo.title}` also work. - `{}` first tries the token as written, then falls back to `fields.*`. - Missing values resolve to an empty string. ### List summary example ```yaml - name: sections type: object list: collapsible: collapsed: true summary: "{title} ({index})" fields: - name: title type: string ``` ## Examples ### Basic object ```yaml - name: contact type: object fields: - name: name type: string - name: email type: string ``` --- # Reference field Source: https://pagescms.org/docs/configuration/fields/reference/ Search and link entries from another collection. Use when one content type needs to point to another collection (posts -> authors, products -> categories). `reference` is collection-backed. It searches another configured collection and saves the selected entry value. ## Options Key | Description --- | --- collection | Target collection name (e.g. `"authors"`). multiple | If `true`, allows many references. min | Minimum number of selected references when `multiple` is enabled. max | Maximum number of selected references when `multiple` is enabled. search | Comma-separated fields used for lookup (e.g. `"name,email"`). value | Template for the stored value (e.g. `"{path}"`). label | Template for the displayed label (e.g. `"{name}"`). ## Template tokens `value` and `label` support template strings. Token | Description --- | --- `{path}` | Entry path. `{name}` | Entry name. `{primary}` | Primary field value from the referenced collection. Uses `view.primary`, otherwise `title`, otherwise the first field. `{fields.}` | Field value from the referenced entry. Nested paths are supported, for example `{fields.author.name}`. `{}` | Shorthand alias for `{fields.}` when no direct token resolves, for example `{author.name}`. Pages CMS resolves reference templates in this order: 1. Try the token as written. 2. If nothing resolves, try the same token under `fields.*`. ## Examples ### Basic reference ```yaml - name: author type: reference options: collection: authors ``` ### Multiple references ```yaml - name: categories type: reference options: collection: categories multiple: true min: 1 max: 5 ``` ### Search across multiple fields ```yaml - name: author type: reference options: collection: authors search: "name,email,fields.role" ``` ### Custom stored value and label ```yaml - name: author type: reference options: collection: authors value: "{primary}" label: "{primary}" ``` This stores and displays the referenced entry's primary field value. ### Multiple references with nested labels ```yaml - name: speakers type: reference options: collection: people multiple: true search: "name,fields.profile.title,fields.company.name" value: "{path}" label: "{profile.name} ({company.name})" ``` --- # Rich-text field Source: https://pagescms.org/docs/configuration/fields/rich-text/ WYSIWYG editor for formatted content. Use for article bodies, long descriptions, and page content. ## Options Key | Description --- | --- format | Values: `markdown`, `html`. switcher | Show or hide the `Editor` / `Source` mode switch. Defaults to `true`. media | Media source for inserted images (e.g. `"content_images"`). Set to `false` to disable media. path | Default image folder (e.g. `"blog"`). extensions | Allowed image extensions (e.g. `["png", "webp"]`). categories | Allowed image categories. Values: `image`. rename | Controls upload renaming. Use `false` to keep the original filename, `true` or `safe` to slugify it, or `random` for a generated name. ## Examples ### Markdown with media ```yaml - name: body label: Body type: rich-text options: media: content_images path: public/images/blog rename: true switcher: true ``` ### HTML output ```yaml - name: content label: Content type: rich-text options: format: html ``` ### Hide the mode switch ```yaml - name: excerpt label: Excerpt type: rich-text options: switcher: false ``` ### Disable media ```yaml - name: notes label: Notes type: rich-text options: media: false ``` --- # Select field Source: https://pagescms.org/docs/configuration/fields/select/ Choose from a predefined list of local options. Use for tags, categories, statuses, and other fixed option lists. ## Options Key | Description --- | --- values | Required. Static options (e.g. `["Draft", "Review", "Published"]` or `[{ name: draft, label: Draft }]`). multiple | If `true`, allows multiple values. min | Minimum number of selected values when `multiple` is enabled. max | Maximum number of selected values when `multiple` is enabled. placeholder | Custom placeholder text (e.g. `"Select a status"`). `select` only supports predefined local options. If you need to load entries dynamically or from another source, use [reference](/docs/configuration/fields/reference/) instead. ## Examples ### Single value ```yaml - name: status type: select options: values: [Draft, Review, Published] ``` ### Multiple + named values ```yaml - name: categories type: select options: values: - name: art label: Art - name: fashion label: Fashion - name: movies label: Movies - name: music label: Music ``` --- # String field Source: https://pagescms.org/docs/configuration/fields/string/ Single-line text input. Use for short values like titles, slugs, and names. ## Options Key | Description --- | --- minlength | Minimum allowed length (e.g. `3`). maxlength | Maximum allowed length (e.g. `120`). ## Examples ### Basic string ```yaml - name: title type: string options: maxlength: 120 ``` --- # Text field Source: https://pagescms.org/docs/configuration/fields/text/ Multi-line plain text. Use for summaries, excerpts, and notes. ## Options Key | Description --- | --- minlength | Minimum allowed length (e.g. `20`). maxlength | Maximum allowed length (e.g. `280`). ## Examples ### Basic text ```yaml - name: excerpt type: text options: maxlength: 280 ``` --- # UUID field Source: https://pagescms.org/docs/configuration/fields/uuid/ Generate and store a UUID v4. Use for stable IDs independent from filenames or titles. ## Options Key | Description --- | --- editable | If `true`, users can type a custom UUID value. generate | If `false`, hides the "generate new UUID" button. ## Notes - A UUID is auto-generated when the field has no explicit `default`. - To force an empty initial value, set `default: ""`. ## Examples ### Generated UUID ```yaml - name: id type: uuid options: editable: false ``` --- # Deploy on Vercel Source: https://pagescms.org/docs/guides/installing/vercel/ Deploy Pages CMS on Vercel with PostgreSQL and a GitHub App. ## Step 1: Create a PostgreSQL database You will need a `DATABASE_URL`.
{% lucide "triangle-alert" %}

Using Supabase?

If you run migrations during deployment, prefer the Supabase Session Pooler for DATABASE_URL. Direct connections can cause connectivity issues on some hosting providers.

On Vercel with a pooled Supabase connection string, set POSTGRES_MAX_CONNECTIONS=1. The limit is per Vercel instance, so higher values can exhaust database connections quickly.

## Step 2: Choose your public URL Use one of these: - your custom domain, for example `https://cms.example.com` - your Vercel production domain, for example `https://my-pages-cms.vercel.app` Use the same URL everywhere: - `BASE_URL` - GitHub App callback URL - GitHub App webhook URL - GitHub App setup URL If you use the default Vercel domain, make sure it matches the project name you create in the next steps. ## Step 3: Create the GitHub App Use [the helper](/docs/guides/installing/github-app/#using-the-helper): ```bash npm run setup:github-app -- --base-url https://cms.example.com --env .env ``` If you need other helper options, see [GitHub App helper](/docs/guides/installing/github-app/#using-the-helper). This writes the GitHub App environment variables to `.env`. ## Step 4: Create the Vercel project Use the deploy button: [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fpages-cms%2Fpages-cms%2Ftree%2Fmain&project-name=pages-cms&repository-name=pages-cms&redirect-url=https%3A%2F%2Fpagescms.org%2Fdocs%2Fguides%2Finstall-vercel%2F&env=BASE_URL,DATABASE_URL,BETTER_AUTH_SECRET,CRYPTO_KEY,GITHUB_APP_ID,GITHUB_APP_NAME,GITHUB_APP_PRIVATE_KEY,GITHUB_APP_WEBHOOK_SECRET,GITHUB_APP_CLIENT_ID,GITHUB_APP_CLIENT_SECRET&envDescription=Enter%20the%20required%20environment%20variables%20for%20Pages%20CMS.&envLink=https%3A%2F%2Fpagescms.org%2Fdocs%2Fdevelopment%2Fenvironment-variables%2F) If you prefer, you can also create the project manually in Vercel. Use the GitHub App environment variables from the previous step, plus: ```bash DATABASE_URL=postgresql://... POSTGRES_MAX_CONNECTIONS=1 BETTER_AUTH_SECRET=your-random-secret CRYPTO_KEY=your-random-secret BASE_URL=https://cms.example.com ``` You can generate secrets with: ```bash openssl rand -base64 32 ``` For the full list, see [Environment variables](/docs/development/environment-variables/). ## Step 5: Deploy your app Deploy the app in Vercel. If you change the production URL later, update `BASE_URL` and the GitHub App URLs to match it. ## Step 6: Run migrations Run the migration against your production database: ```bash npm run db:migrate ``` --- # Self-host Source: https://pagescms.org/docs/guides/installing/self-host/ Run Pages CMS on your own infrastructure. ## Step 1: Create a PostgreSQL database You will need a `DATABASE_URL`.
{% lucide "triangle-alert" %}

Using Supabase?

If you run migrations during deployment, prefer the Supabase Session Pooler for DATABASE_URL. Direct connections can cause connectivity issues on some hosting providers.

## Step 2: Create `.env` Add at least: ```bash DATABASE_URL=postgresql://... BETTER_AUTH_SECRET=your-random-secret CRYPTO_KEY=your-random-secret BASE_URL=https://cms.example.com ADMIN_EMAILS=admin@example.com ``` `ADMIN_EMAILS` is optional and controls access to the admin panel. You can generate secrets with: ```bash openssl rand -base64 32 ``` ## Step 3: Create the GitHub App Use [the helper](/docs/guides/installing/github-app/#using-the-helper): ```bash npm run setup:github-app -- --base-url https://cms.example.com --env .env ``` This writes the GitHub App environment variables into `.env.production`. If you need other helper options, see [GitHub App helper](/docs/guides/installing/github-app/#using-the-helper). ## Step 4: Install dependencies ```bash npm install ``` ## Step 5: Run migrations ```bash npm run db:migrate ``` ## Step 6: Build and run ```bash npm run build npm run start ``` ## Step 7: Put HTTPS in front of the app Use a stable public HTTPS URL in front of the app. Typical setup: - Nginx or Caddy as reverse proxy - TLS at the proxy or platform edge - the app behind it on an internal port --- # GitHub App Source: https://pagescms.org/docs/guides/installing/github-app/ What the Pages CMS GitHub App does, how to create it with the helper, and how to configure it manually. Pages CMS uses a GitHub App for repository access, user sign-in, webhook delivery, and installation-scoped repository operations. ## Using the helper The fastest path is the built-in helper: ```bash npm run setup:github-app -- --base-url http://localhost:3000 ``` The helper creates the app from a manifest and prints the GitHub App environment variables for you. If you pass `--env `, it will also write them to that file. Options: | Option | What it does | | --- | --- | | `--base-url` | Sets the public app URL used for the callback URL, webhook URL, and setup URL. | | `--env` | Writes the generated GitHub App environment variables to a file instead of only printing them. | | `--owner-type` | Creates the app under a personal account or an organization. | | `--org` | Sets the organization slug when `--owner-type org` is used. | | `--app-name` | Sets the GitHub App display name. | | `--no-open` | Does not try to open the browser automatically. | Example: ```bash npm run setup:github-app -- \ --base-url https://cms.example.com \ --env .env \ --owner-type org \ --org my-company \ --app-name "Pages CMS" \ --no-open ``` You will need to manually disable `User-to-server token expiration` if GitHub shows that option. This will avoid your users to be periodically logged out. ## Manually Open GitHub App settings: - personal apps: `https://github.com/settings/apps` - org apps: `https://github.com/organizations//settings/apps` Match the permissions and events below: | Section | Name | Value | | --- | --- | --- | | Account permissions | Email addresses | Read only | | Repository permissions | Administration | Read and write | | Repository permissions | Actions | Read and write | | Repository permissions | Checks | Read only | | Repository permissions | Commit statuses | Read only | | Repository permissions | Contents | Read and write | | Repository permissions | Metadata | Read only | | Events | Installation target | Enabled | | Events | Repository | Enabled | | Events | Push | Enabled | | Events | Delete | Enabled | | Events | Check run | Enabled | | Events | Check suite | Enabled | | Events | Status | Enabled | | Events | Workflow run | Enabled | Finally: - generate and download a private key (for `GITHUB_APP_PRIVATE_KEY`), - set a webhook secret (for `GITHUB_APP_WEBHOOK_SECRET`), - disable `User-to-server token expiration` if GitHub offers that setting (in "Optional features"), --- # Upgrading to 2.x Source: https://pagescms.org/docs/guides/upgrading-to-2/ Upgrade an existing Pages CMS 1.x deployment to 2.x. ## Upgrade checklist 1. **Back up the deployment.** Save the current database and environment configuration before changing anything. 2. **Optional: Migrate to PostgreSQL.** Pages CMS 2.x expects PostgreSQL for normal deployments. If you still use SQLite or legacy libSQL/Turso, migrate collaborators first: [Migrating collaborators](/docs/guides/migrating-collaborators/). 3. [**Update environment variables (see below).**](#update-environment-variables). 4. [**Update the GitHub App (see below).**](#update-github-app) 5. **Run migrations.** Apply the 2.x database migrations before serving traffic: `npm run db:migrate`. 6. **Clear cache.** Remove old cache data once after upgrading: `npm run db:clear-cache`. 7. **Redeploy the app.** Restart the app with the new code and environment. 8. **Verify the upgrade.** Confirm GitHub sign-in, repository installation, webhook delivery, build status display, and GitHub Actions triggering if you use it. ## Add environment variables - `BETTER_AUTH_SECRET`: A random secret for the new auth library (Better Auth). - `BASE_URL`: This is now required. More info: [Environment variables](/docs/development/environment-variables/) ## Clear cache after upgrading Recommended once after upgrading to 2.x: ```bash npm run db:clear-cache ``` This removes old cache data from previous versions and lets Pages CMS rebuild it with the 2.x cache logic. ## Update GitHub App More info: [GitHub App](/docs/guides/installing/github-app/) ### Add account permissions | Permission | Value | | --- | --- | | Email addresses | Read only | ### Add repository permissions | Permission | Value | | --- | --- | | Actions | Read and write | | Checks | Read only | | Commit statuses | Read only | ## Add webhook events | Event | | --- | | Check run | | Check suite | | Status | | Workflow run | ### Confirm URLs and install behavior | Setting | Value | | --- | --- | | User authorization callback URL | `/api/auth/callback/github` | | Webhook URL | `/api/webhook/github` | | Setup URL | `/` | | Redirect on update | Enabled | | Request user authorization during installation | Disabled | --- # Migrating collaborators Source: https://pagescms.org/docs/guides/migrating-collaborators/ Export and import collaborators when moving between Pages CMS installs. ## When you need this Collaborators are stored in the database, not in `.pages.yml`. If you move to a new database or a new Pages CMS install, migrate collaborators separately. ## Export from the current database ```bash npm run db:collaborators:export -- --output=collaborators.csv ``` ## Import into the new database ```bash npm run db:collaborators:import -- --input=collaborators.csv ``` ## Optional import flags - `--replace`: remove current collaborators before import. - `--default-invited-by-user-id=` - `--default-invited-by-email=` ## Export from legacy SQLite or libSQL If you are migrating from an older Pages CMS install that used SQLite or libSQL/Turso, first export collaborators with the legacy exporter script included in the Pages CMS repo: `db/scripts/export-collaborators-legacy-libsql.mjs` Run it from the Pages CMS repository root: ```bash SQLITE_URL="libsql://..." SQLITE_AUTH_TOKEN="..." \ npx -y -p @libsql/client node db/scripts/export-collaborators-legacy-libsql.mjs --out=collaborators.csv ``` You can also pass credentials as flags instead of environment variables: ```bash npx -y -p @libsql/client node db/scripts/export-collaborators-legacy-libsql.mjs \ --url="libsql://..." \ --token="..." \ --out=collaborators.csv ``` Once done, import the resulting CSV with the normal importer. --- # Creating a custom field Source: https://pagescms.org/docs/guides/creating-custom-field/ Build a custom field and let Pages CMS register it automatically. ## When to create one Create a custom field when the built-in fields are not enough. Typical cases: - a custom picker UI, - a custom storage format, - field-specific validation, - a reusable editing pattern. ## Fastest path Start from a simple existing field such as: - `fields/core/boolean` - `fields/core/string` - `fields/core/select` Avoid starting from a complex field unless you need that behavior. ## Folder structure Create a folder under `fields/custom`: ```text fields/ ├─ core/ ├─ custom/ │ └─ my-field/ │ ├─ edit-component.tsx │ ├─ view-component.tsx │ └─ index.tsx └─ registry.ts ``` ## What a field can export Your `index.tsx` can export: - `label` - `schema` - `defaultValue` - `read` - `write` - `EditComponent` - `ViewComponent` You usually only need some of them. ## What each export does ### `label` Human-readable field name. ### `schema` Returns the Zod schema for this field. ### `defaultValue` Fallback value when there is no stored value and no explicit field default. ### `read` Transforms stored data into editor data. ### `write` Transforms editor data back into stored data. ### `EditComponent` The editing UI. It receives the current value and calls `onChange` with the next value. ### `ViewComponent` Compact display used in lists or read-only contexts. ## Minimal example ```tsx import { z } from "zod"; import { Input } from "@/components/ui/input"; const schema = () => z.string().min(1, "Required"); const EditComponent = ({ value, onChange }: any) => ( onChange(event.target.value)} /> ); const ViewComponent = ({ value }: { value: unknown }) => { if (!value) return null; return {String(value)}; }; const label = "My field"; export { label, schema, EditComponent, ViewComponent }; ``` ## How registration works Pages CMS loads fields through `fields/registry.ts`. Core fields are registered directly. Custom fields under `fields/custom//index.ts` or `index.tsx` are auto-registered at startup and build time. The folder name becomes the field `type`. ```text fields/custom/my-field/index.tsx ``` registers: ```yaml type: my-field ``` If you add, remove, or rename a custom field while the dev server is running, restart it. ## Test with a minimal config ```yaml fields: - name: promo type: my-field ``` Check four things: 1. it renders, 2. it saves, 3. it reloads, 4. validation behaves correctly. ## When to add `read` and `write` Add them only when the editor value and stored value are different. Examples: - editor uses objects but storage uses strings, - editor uses local dates but storage uses ISO strings, - editor uses relative media paths but storage wants normalized paths. --- # Authentication Source: https://pagescms.org/docs/development/authentication/ How Pages CMS chooses between a GitHub user token and a GitHub App installation token. ## Authentication model Pages CMS can operate with either: - a GitHub user token, - a GitHub App installation token. The token choice depends on who is signed in and what access they have. ## Token selection order For repository reads and writes, Pages CMS follows this order: 1. Check whether the signed-in user has a GitHub token. 2. If access checks are enabled, verify that token can access the target repository. 3. If yes, use the GitHub user token. 4. If not, check for collaborator access for the current `owner/repo`. 5. If a collaborator record exists, use the repository's GitHub App installation token. 6. If neither path succeeds, deny access. ## Collaborator scope Collaborator access is scoped to one repository. The fallback check matches: - collaborator email, - repository owner, - repository name. ## Commits and attribution When Pages CMS writes with a GitHub user token, GitHub handles attribution normally. When Pages CMS writes with the GitHub App installation token, the default behavior is to omit explicit committer metadata: - with `settings.commit.identity: app` or no setting, GitHub uses the authenticated app/installation identity for the write, - with `settings.commit.identity: user`, Pages CMS sends the current user's name and email as committer metadata when available. Per-schema overrides are also available through `content[].commit.identity` and `media[].commit.identity`. ## Routes that require a GitHub user Collaborator fallback does not apply everywhere. The following areas require a real GitHub identity: - configuration management, - collaborator management, - cache management. --- # Caching Source: https://pagescms.org/docs/development/caching/ How Pages CMS caches config, collections, media, and permission data. Pages CMS uses GitHub as the source of truth and PostgreSQL as a read cache. ## Cache layers | Layer | Scope | Purpose | | --- | --- | --- | | `config` table | Per `owner/repo/branch` | Stores parsed `.pages.yml` and its SHA. | | `cache_file` table | Per cached row | Stores collection/media rows, file metadata, and cached content. | | `cache_file_meta` table | Per branch and per folder scope | Tracks trusted branch state and trusted folder snapshots. | | `cache_permission` table | Per user/repo | Caches access checks used by file cache endpoints. | | In-memory TTL caches | Per server process | Short-lived branch HEAD and repository metadata lookups. | ## Config lifecycle 1. UI/API requests call `getConfig`. 2. If DB has config, it is returned immediately. 3. If DB is missing config and a GitHub token is available, Pages CMS fetches `.pages.yml` from GitHub, parses it, upserts DB, then returns it. 4. Optional sync mode (`sync: true`) can compare DB SHA with GitHub SHA using a TTL gate. ## Collection and media lifecycle 1. Requests read cached rows by folder path. 2. A folder snapshot is trusted only when folder meta is `ok`, has a `commitSha`, and matches branch state when branch meta is available. 3. If the folder snapshot is missing, expired, syncing, errored, empty, or otherwise untrusted, Pages CMS fetches the whole folder from GitHub. 4. That authoritative fetch replaces folder rows and folder meta together. 5. Empty folders are not cached as trusted snapshots. ## Incremental updates Direct CMS writes and small webhook pushes can preserve an already-verified direct folder cache. That means: - an already-cached leaf folder can stay hot after a file add/update/delete, - cold folders are never promoted from partial deltas, - ancestor folders are invalidated and repopulated on the next read, - if incremental preservation is incomplete or uncertain, the folder falls back to invalidation. This keeps large hot folders fast without trusting partial snapshots. ## Staleness behavior - **Client cache** uses SWR on top of the API responses. - **Backend cache** trusts only verified folder snapshots. - **Branch reconcile** is non-destructive: it updates branch-head state, and later folder reads decide whether a refetch is needed. ## Webhook push behavior GitHub `push` webhooks use a tiered strategy to keep webhook responses fast while avoiding broad cache resets on normal commits: 1. For small pushes, Pages CMS applies incremental updates only to already-verified direct folders and invalidates the rest. 2. For medium pushes, Pages CMS skips per-file patching and invalidates only affected cache paths. 3. For very large pushes, Pages CMS invalidates the full branch cache as a safety fallback. All fallback modes repopulate data on demand. ## Environment variables | Variable | Unit | Default | Purpose | | --- | --- | --- | --- | | `CACHE_CHECK_MIN` | minutes | `5` | Reconcile interval for branch cache freshness checks. | | `CONFIG_CHECK_MIN` | minutes | `5` | TTL gate for config SHA checks when sync is enabled. | | `FILE_TTL_MIN` | minutes | `1440` | Max age for `cache_file` rows (`-1` no expiry, `0` no cache). | | `PERMISSIONS_TTL_MIN` | minutes | `60` | Max age for `cache_permission` rows (`0` always recheck GitHub). | | `BRANCH_HEAD_TTL_MS` | milliseconds | `15000` | In-memory TTL for branch HEAD lookups. | | `REPO_META_TTL_MS` | milliseconds | `15000` | In-memory TTL for repo metadata snapshot lookups. | | `WEBHOOK_PUSH_INCREMENTAL_MAX_FILES` | files | `120` | Max changed paths for incremental push processing. | | `WEBHOOK_PUSH_SCOPED_INVALIDATION_MAX_FILES` | files | `800` | Max changed paths for scoped invalidation before full branch invalidation. | --- # Database Source: https://pagescms.org/docs/development/database/ Migrations, operational scripts, and database maintenance for Pages CMS. Pages CMS stores app state in PostgreSQL: auth data, collaborators, parsed config, and cache metadata. Content itself stays in GitHub. ## Migrations Generate a migration when the schema changes: ```bash npm run db:generate ``` Apply migrations with: ```bash npm run db:migrate ``` `npm run build` also runs migrations through `postbuild`. Use manual `npm run db:migrate` when: - setting up local development, - deploying on platforms where `postbuild` is skipped, - applying migrations before switching traffic to a new deployment. ## Cache-related tables | Table | Purpose | | --- | --- | | `config` | Parsed `.pages.yml` plus its SHA. | | `cache_file` | Cached collection/media rows. | | `cache_file_meta` | Branch and folder snapshot state. | | `cache_permission` | Cached repo permission checks. | ## Database scripts | Script | Purpose | Typical use | | --- | --- | --- | | `npm run db:clear-cache` | Clears cache tables used for repository/config/permission caching. | Safe when cache state is stale or corrupted. GitHub remains the source of truth. | | `npm run db:collaborators:export -- --output=collaborators.csv` | Exports collaborators to CSV. | Move collaborators to a new deployment/database. | | `npm run db:collaborators:import -- --input=collaborators.csv` | Imports collaborators from CSV. | Restore/migrate collaborator assignments. | ## Operational note After cache-logic upgrades, it can be worth running `npm run db:clear-cache` once so old cached rows do not survive into the new behavior. --- # Fields Source: https://pagescms.org/docs/development/fields/ How field registration, validation, transforms, and rendering work internally. ## Main pieces The field system is built from: - field modules under `fields/core` and `fields/custom`, - the registry in `fields/registry.ts`, - schema assembly in `lib/schema.ts`, - field rendering in the entry form. ## Field modules A field module can export: - `label` - `schema` - `defaultValue` - `read` - `write` - `EditComponent` - `ViewComponent` The registry collects these exports and exposes them to the rest of the app. ## Registry `fields/registry.ts` registers core fields directly and then registers custom fields from the generated manifest in `fields/custom.generated.ts`. That generated file is written by `next.config.mjs` by scanning `fields/custom/*/index.ts(x)`. The custom field folder name becomes the field type. For example: ```text fields/custom/my-field/index.tsx ``` registers the field as `type: my-field`. That produces shared maps for: - labels, - schemas, - default values, - read functions, - write functions, - edit components, - view components. These maps are then used throughout the app. ## Validation pipeline Form validation is assembled in `lib/schema.ts`. High-level flow: 1. `generateZodSchema(fields)` walks the configured field tree. 2. For each field type, it looks up the registered `schema`. 3. Object and block fields are wrapped recursively. 4. List behavior is applied on top when `field.list` is enabled. 5. Required/optional handling is applied around the result. This means your field-level `schema` usually only needs to describe the field itself, not the whole surrounding object structure. ## Default values Initial editor state is built with `initializeState(...)` in `lib/schema.ts`. Resolution order is: 1. explicit field `default`, 2. list default if present, 3. registered `defaultValue`, 4. fallback empty value. ## Read and write transforms `read` and `write` are for storage/UI conversion. Typical pattern: - `read` runs when content is loaded into the editor, - `write` runs when editor values are serialized back to content. Use them when stored values should not match the editor representation one-to-one. ## Rendering pipeline The entry form resolves the field type to its registered `EditComponent`. That component receives props such as: - `value` - `onChange` - `field` The component is responsible for: - rendering the editing UI, - converting browser events into field values, - calling `onChange` with the value expected by the field schema and write pipeline. `ViewComponent` is the compact display version used outside the main form editing flow. ## Lists vs field-specific multiple behavior There are two different concepts: - `field.list` This means the field itself is repeated as a list item by the form system. - field-specific `options.multiple` This means a single field manages multiple selections internally, such as select/reference. These are separate layers and should not be conflated. ## Good design rules for field authors - keep `schema` narrow and explicit, - keep `EditComponent` dumb where possible, - use `read` / `write` only when representation actually differs, - avoid leaking transport or API concerns into generic field logic, - copy a simple field first, then add complexity. --- # Environment variables Source: https://pagescms.org/docs/development/environment-variables/ Required and optional environment variables for running Pages CMS. ## Required variables Variable | Description --- | --- `DATABASE_URL` | PostgreSQL connection string. `BETTER_AUTH_SECRET` | Secret used by Better Auth. `AUTH_SECRET` is also accepted. `CRYPTO_KEY` | Key used to encrypt GitHub tokens in the database. `GITHUB_APP_ID` | GitHub App ID. `GITHUB_APP_NAME` | GitHub App slug used in GitHub URLs. `GITHUB_APP_PRIVATE_KEY` | GitHub App private key. `GITHUB_APP_WEBHOOK_SECRET` | Secret used to verify GitHub webhooks. `GITHUB_APP_CLIENT_ID` | GitHub App client ID. `GITHUB_APP_CLIENT_SECRET` | GitHub App client secret. ## Common optional variables Variable | Description --- | --- `BASE_URL` | Canonical public URL for the app. Required in production. `ADMIN_EMAILS` | Comma-separated allowlist for admin panel access. `EMAIL_PROVIDER` | `resend` or `smtp`. `EMAIL_FROM` | Sender address for auth and invitation emails. `RESEND_API_KEY` | Required when using Resend. `SMTP_HOST` | Required when using SMTP. `SMTP_PORT` | SMTP port. Defaults to `587`. `SMTP_SECURE` | Defaults to `true` when `SMTP_PORT=465`, otherwise `false`. `SMTP_USER` | SMTP username. `SMTP_PASSWORD` | SMTP password. ## Cache variables Variable | Description --- | --- `CACHE_CHECK_MIN` | Branch cache reconcile interval in minutes. Default `5`. `CONFIG_CHECK_MIN` | Config sync check interval in minutes. Default `5`. `FILE_TTL_MIN` | File cache TTL in minutes. Default `1440`. Use `-1` to disable expiry. `PERMISSIONS_TTL_MIN` | Permission cache TTL in minutes. Default `60`. `BRANCH_HEAD_TTL_MS` | Branch HEAD cache TTL in milliseconds. Default `15000`. `REPO_META_TTL_MS` | Repository metadata cache TTL in milliseconds. Default `15000`. `WEBHOOK_PUSH_INCREMENTAL_MAX_FILES` | Max changed-file count for incremental webhook processing. Default `120`. `WEBHOOK_PUSH_SCOPED_INVALIDATION_MAX_FILES` | Max changed-file count for scoped invalidation fallback before full branch invalidation. Default `800`. Only these canonical names are supported. ## Generate secrets ```bash openssl rand -base64 32 ``` ## Notes - If you use the GitHub App helper, it writes the GitHub App variables for you. - `GITHUB_APP_NAME` must be the app slug, not the display name. - `BASE_URL` is required in production. In development, it defaults to `http://localhost:3000`. - `BASE_URL`, callback URL, setup URL, and webhook URL must all point to the same app instance. - Use one canonical production URL only. Do not mix a custom domain and a `*.netlify.app` URL for the same install. --- # Overview Source: https://pagescms.org/docs/configuration/ Understand the structure of the `.pages.yml` file. ## What `.pages.yml` does `.pages.yml` is the single source of truth for Pages CMS configuration. Place it at the repository root. Pages CMS reads it per repository and per branch. ## Top-level keys Key | Description --- | --- media | Defines where uploaded files are stored and what URLs are written. [See `media`](/docs/configuration/media/). content | Defines editable collections and files. [See `content`](/docs/configuration/content/). components | Reuses shared field definitions. [See `components`](/docs/configuration/components/). settings | Sets repository-wide behavior such as merge mode and commit templates. [See `settings`](/docs/configuration/settings/). actions | Adds repository-level GitHub Actions buttons. [See `actions`](/docs/configuration/actions/). ## Read order Start with this order: 1. Define `media`. 2. Define `content`. 3. Add `components` if fields repeat. 4. Add `settings` if you need global behavior. 5. Add `actions` if you want custom workflow buttons. ## Minimal example ```yaml media: media content: - name: posts label: Posts type: collection path: content/posts fields: - name: title type: string - name: body type: rich-text ``` ## Example with a collection and a single file ```yaml media: input: src/media output: /media content: - name: posts label: Posts type: collection path: src/posts fields: - name: title type: string - name: body type: rich-text - name: site label: Site settings type: file path: src/_data/site.json fields: - name: title type: string - name: description type: text - name: url type: string actions: - name: deploy-site label: Deploy site workflow: pages-cms-action.yml ``` --- # Overview Source: https://pagescms.org/docs/configuration/content/ Define editable collections and files in your repository. ## Overview `content` defines what editors can edit. Each entry is either: - a `collection` for many files with the same schema - a `file` for one file with its own schema - a `group` for organizing files and collections in the sidebar `group` is navigation-only. It can contain nested `group`, `collection`, and `file` entries, but it does not create its own editor route. Collections and files can also define `actions`. See [`actions`](/docs/configuration/actions/). ## Keys Each `content` entry may use the following keys: Key | Description --- | --- name * | Unique internal name (e.g. `"posts"`). label | UI label (e.g. `"Blog posts"`). type * | Values: `collection`, `file`, `group`. path * | Folder for collections or file path for single files (e.g. `"content/posts"`, `"data/site.yml"`). Not used by `group`. fields | Field definitions shown in the editor (e.g. `[{ name: "title", type: "string" }]`). Fields support options like `required`, `hidden`, and `readonly`. Read more about: [`fields`](/docs/configuration/content/fields/), [editors](/docs/configuration/content/editors/) filename | Collection filename template or object config (e.g. `"{primary}.md"` or `{ template: "{year}-{month}-{day}-{primary}.md", field: create }`). [Read more about `filename`](/docs/configuration/content/filename/) exclude | Files to ignore in a collection (e.g. `["README.md"]`). format | File format. Values include `yaml-frontmatter`, `json-frontmatter`, `toml-frontmatter`, `yaml`, `json`, `toml`, `datagrid`, `code`, `raw`. [Read more about editors](/docs/configuration/content/editors/) delimiters | Custom frontmatter delimiters (e.g. `"+++"`, `[""]`). subfolders | Values: `true`, `false`. Enables or disables nested folders in collections. list | Repeat a field as an array, or for `type: file`, store the whole file as a top-level array. [Read more about `list`](/docs/configuration/content/list/) view | Collection list settings for fields, sorting, search, and tree mode (e.g. `{ primary: "title", sort: "date", order: "desc" }`). [Read more about `view`](/docs/configuration/content/view/) operations | Per-entry create/rename/delete controls (e.g. `{ delete: false }`). [Read more about `operations`](/docs/configuration/content/operations/). commit | Per-entry commit settings (e.g. `{ identity: "user" }`). [See `settings`](/docs/configuration/settings/). actions | Adds collection or file action buttons (e.g. `[{ name: "preview", label: "Preview", workflow: "pages-cms-file-action.yml" }]`). [See `actions`](/docs/configuration/actions/). items | Child entries inside a `group` (e.g. nested `group`, `collection`, or `file` entries). *: Required ## Examples ### Collection ```yaml content: - name: posts label: Posts type: collection path: content/posts fields: - name: title type: string - name: body type: rich-text actions: - name: rebuild-posts label: Rebuild posts scope: collection workflow: pages-cms-collection-action.yml - name: preview-post label: Preview scope: entry workflow: pages-cms-entry-action.yml ``` ### Single file ```yaml content: - name: site label: Site settings type: file path: data/site.yml fields: - name: title type: string - name: description type: text actions: - name: validate-config label: Validate config workflow: pages-cms-file-action.yml ``` ### Nested groups Use `type: group` to organize large content menus without changing how content is stored internally. ```yaml content: - name: docs label: Docs type: group items: - name: pages label: Pages type: collection path: content/pages fields: - name: title type: string - name: references label: References type: group items: - name: publications label: Publications type: file path: data/publications.json format: json list: true fields: - name: title type: string ``` --- # Install locally Source: https://pagescms.org/docs/guides/installing/ Run Pages CMS locally with PostgreSQL, environment variables, and a GitHub App. ## Step 1: Start PostgreSQL ```bash docker run --name pagescms-db -e POSTGRES_USER=pagescms -e POSTGRES_PASSWORD=pagescms -e POSTGRES_DB=pagescms -p 5432:5432 -d postgres:16 ``` ## Step 2: Install dependencies ```bash npm install ``` ## Step 3: Create `.env.local` ```bash DATABASE_URL=postgresql://pagescms:pagescms@localhost:5432/pagescms BETTER_AUTH_SECRET=your-random-secret CRYPTO_KEY=your-random-secret ``` You can generate secrets with: ```bash openssl rand -base64 32 ``` ## Step 4: Create the GitHub App Use [the helper](/docs/guides/installing/github-app/#using-the-helper): ```bash npm run setup:github-app -- --base-url http://localhost:3000 --env .env.local ``` If you need other helper options, see [GitHub App helper](/docs/guides/installing/github-app/#using-the-helper). ## 5. Run migrations ```bash npm run db:migrate ``` ## 6. Start the app ```bash npm run dev ``` If you need GitHub webhooks to hit your local app, use a public tunnel URL (e.g. [ngrok](https://ngrok.com/)) as the helper `--base-url`. ---