Skip to content

Powered by Grav + Helios

Pages

Pages

Endpoints for managing Grav pages including listing, creating, updating, deleting, copying, moving, and reordering, plus page translations, batch operations, draft preview tokens, taxonomy, and the site's language list.

Endpoints for managing Grav pages including listing, creating, updating, deleting, copying, moving, and reordering, plus page translations, batch operations, draft preview tokens, taxonomy, and the site's language list.

List Pages

GET /pages
List pages with filtering, sorting, and pagination. Backed by the Flex page directory when available (indexed, cached), falling back to the standard Pages service otherwise. When `translations=true`, each item includes `has_default_file` and `explicit_language_files` so clients can disambiguate real translation files from default-language fallbacks.

Parameters

Name Type Description
page optional integer Page number for pagination (default 1).
per_page optional integer Number of results per page (default 20, capped at `plugins.api.pagination.max_per_page`, 1000 by default).
sort optional string Sort field: `date`, `title`, `slug`, `modified`, `order`, or `default`. When omitted, pages are sorted by `date` descending. `default` with `children_of` uses the parent's native page ordering; without `children_of` it sorts by `order` ascending. An unknown field returns 422.
order optional string Sort direction: `asc` (default) or `desc`. Ignored when `sort` is omitted.
search optional string Full-text search across indexed page fields (Flex backend only).
published optional boolean Filter by published state.
visible optional boolean Filter by visible state.
routable optional boolean Filter by routable state.
template optional string Filter by page template name.
parent optional string Filter to descendants whose route starts with this parent path (e.g. `/blog` returns `/blog/post-1`, `/blog/sub/post-2`).
children_of optional string Filter to direct children of a given route.
root optional boolean `true` returns only top-level pages (direct children of the pages root, so children of the home page are not counted). `false` returns every page that is not top-level.
locate optional string Route of a page to jump to. When it is in the filtered, sorted results, the server returns the chunk that contains it (overriding `page`) and reports its position in `meta.pagination.located_at_index`.
lang optional string List pages in a specific language. A code that is not a configured language returns 422.
translations optional boolean Include translation metadata on each item: `translated_languages`, `untranslated_languages`, `has_default_file`, `explicit_language_files`.
JSON
{"data": [{"route": "/blog", "slug": "blog", "title": "Blog", "template": "blog", "published": true, "visible": true, "routable": true, "has_children": true, "permissions": {"create": true, "read": true, "update": true, "delete": true, "publish": true, "list": true}}], "meta": {"pagination": {"page": 1, "per_page": 20, "total": 42, "total_pages": 3}}, "links": {"self": "/api/v1/pages?page=1&per_page=20", "next": "/api/v1/pages?page=2&per_page=20", "last": "/api/v1/pages?page=3&per_page=20"}}

Response Codes

200 Success.
401 Unauthorized.
403 Missing `api.pages.read` permission.
422 Unknown sort field or invalid language code.

Get Page

GET /pages/{route}
Get a single page with full content, metadata, and media, plus a `permissions` object with the caller's capabilities on this page. Returns an ETag; use `If-None-Match` for conditional fetches. Requires `api.pages.read`, subject to the page's own `read` rule. A page that has `process.twig: true` also needs the Twig-in-content permission.

Parameters

Name Type Description
route required string The page route (e.g. `/blog/my-post`).
summary optional boolean Leave out the raw `content` and return a plain-text `summary` (rendered, tags stripped, truncated) in its place.
summary_size optional integer Maximum length of `summary` in characters (default 300). Only used with `summary=true`.
render optional boolean Also return the rendered HTML in `content_html`.
children optional boolean Include child pages in the response.
children_depth optional integer How many levels of children to include (default 1). Only used with `children=true`.
translations optional boolean Include translation metadata: `translated_languages`, `untranslated_languages`, `has_default_file` (whether an untyped `{template}.md` exists), `explicit_language_files` (the subset of languages backed by a real `{template}.{lang}.md` on disk).
lang optional string Return the page in a specific language (overrides the request's active language).
JSON
{"data": {"route": "/blog/my-post", "slug": "my-post", "title": "My Post", "template": "post", "content": "# Hello World", "header": {"published": true}, "media": [], "has_default_file": true, "explicit_language_files": ["fr"]}}

Response Codes

200 Success.
304 Not modified (ETag match).
401 Unauthorized.
403 Missing `api.pages.read` permission, denied by the page's own rules, or the page uses Twig in content and the caller lacks that permission.
404 Page not found.
422 Invalid language code.

Create Page

POST /pages
Create a new page. The parent page must already exist (except for top-level pages), and the target folder must be free. The frontmatter is built from the template blueprint's defaults, then `title`, then the supplied `header`. Returns 201 with the new page and a `Location` header. Requires `api.pages.write`, subject to the parent page's own `create` rule; setting `process.twig: true` also needs the Twig-in-content permission.

Parameters

Name Type Description
route required string The route where the page will be created
title required string The page title
template optional string Page template to use
Default: default
content optional string Markdown content for the page body
header optional object Page header/frontmatter values
order optional integer Numeric ordering prefix for the page folder. Pass `"auto"` to use the next free number among the siblings (or no prefix when no sibling has one).
kind optional string One of `page` (default), `folder` (a folder with no `.md` file) or `module` (a modular section, its slug is prefixed with `_`). For `folder`, the response may carry only `route` and `kind`.
Default: page
lang optional string Language code for multi-language sites (body field, or `?lang=` query parameter). Writes a `{template}.{lang}.md` file.
JSON
{"route": "/blog/new-post", "title": "New Post", "template": "post", "content": "# New Post\nContent here"}
JSON
{"data": {"route": "/blog/new-post", "slug": "new-post", "title": "New Post", "template": "post", "published": true, "content": "# New Post\nContent here", "media": [], "permissions": {"create": true, "read": true, "update": true, "delete": true, "publish": true, "list": true}}}

Response Codes

201 Page created
401 Unauthorized
403 Missing `api.pages.write` permission, denied by the parent page's rules, or Twig-in-content not allowed
422 Missing `route` or `title`, invalid `kind`, parent page not found, a page already exists at that route, invalid language code, or blueprint validation failed

Update Page

PATCH /pages/{route}
Partial update of a page. Only provided fields are changed, and the result is validated against the page blueprint. Returns the updated page with a new ETag. Requires `api.pages.write`, subject to the page's own `update` rule; enabling `process.twig`, or editing a page that already has it on, also needs the Twig-in-content permission.

Parameters

Name Type Description
route required string The page route (path parameter)
title optional string Updated page title
content optional string Updated markdown content
header optional object Header values to merge into existing frontmatter. A `null` value removes that key.
header_mode optional string Set to `replace` to replace the whole frontmatter with `header` instead of merging (used by raw frontmatter editors).
template optional string Change the page template
published optional boolean Set the published state
visible optional boolean Set the visible state
lang optional string Query parameter. Update a specific language version; that translation must already exist (create it with `POST /pages/{route}/translate`).
JSON
{"title": "Updated Title", "header": {"subtitle": "New subtitle"}}
JSON
{"data": {"route": "/blog/my-post", "slug": "my-post", "title": "Updated Title", "template": "post", "header": {"title": "Updated Title", "subtitle": "New subtitle"}, "content": "# Hello World", "media": [], "permissions": {"create": true, "read": true, "update": true, "delete": true, "publish": true, "list": true}}}

Response Codes

200 Success
401 Unauthorized
403 Missing `api.pages.write` permission, denied by the page's rules, or Twig-in-content not allowed
404 Page not found
409 Conflict (`If-Match` does not match the current ETag)
422 Blueprint validation failed, invalid language code, or the requested translation does not exist

Supports optimistic concurrency control via the If-Match header. Include the ETag from your last GET request to prevent overwriting concurrent changes.

Delete Page

DELETE /pages/{route}
Delete a page and its folder. Child pages are deleted too unless `children=false`, in which case a page with children is refused with 422. With `?lang=` on a multi-language site only that language's file is removed; when it is the page's only translation the whole folder goes, and `children=false` is honored the same way. Requires `api.pages.write`, subject to the page's own `delete` rule.

Parameters

Name Type Description
route required string The page route to delete
children optional boolean Also delete child pages. `false` refuses to delete a page that has children.
Default: true
lang optional string Delete only a specific language version (404 if the page has no such translation)

Response Codes

204 Page deleted
401 Unauthorized
403 Missing `api.pages.write` permission or denied by the page's rules
404 Page not found, or no translation for the requested language
422 The page has children and `children=false`, or invalid language code

Move Page

POST /pages/{route}/move
Move a page to a new parent, optionally renaming its slug or changing its ordering prefix. Returns the moved page with an ETag; if the page cannot be resolved at its new route, only `route` and `slug` are returned. Requires `api.pages.write`, subject to the page's own `update` rule and the destination parent's `create` rule.

Parameters

Name Type Description
route required string The current page route (path parameter)
parent required string The target parent route (`/` for the top level)
slug optional string Optionally rename the slug during the move. Must be a single path segment.
order optional integer Numeric ordering prefix at the new location. Defaults to the current prefix; `null` removes it.
JSON
{"parent": "/blog", "slug": "moved-post"}
JSON
{"data": {"route": "/blog/moved-post", "slug": "moved-post", "title": "My Post", "template": "post", "content": "# Hello World", "media": []}}

Response Codes

200 Page moved
401 Unauthorized
403 Missing `api.pages.write` permission or denied by page rules
404 Page not found
422 Missing `parent`, invalid slug, destination parent not found, destination already exists, or the page is already at that location

Reorder Pages

POST /pages/{route}/reorder
Reorder child pages under a parent. The listed slugs get numeric folder prefixes 1, 2, 3 in the order given, keeping the widest prefix width already used by the siblings. Children not listed keep their current folder name. Returns every child of the page in its new order. Requires `api.pages.write`, subject to the parent page's own `update` rule.

Parameters

Name Type Description
route required string The parent page route (path parameter)
order required array Array of child slugs in the desired order
JSON
{"order": ["first-post", "second-post", "third-post"]}
JSON
{"data": [{"route": "/blog/first-post", "slug": "first-post", "title": "First Post", "order": "01."}, {"route": "/blog/second-post", "slug": "second-post", "title": "Second Post", "order": "02."}, {"route": "/blog/third-post", "slug": "third-post", "title": "Third Post", "order": "03."}]}

Response Codes

200 Pages reordered
401 Unauthorized
403 Missing `api.pages.write` permission or denied by the parent page's rules
404 Parent page not found
422 `order` missing or not an array, or a slug is not a child of this page

Copy Page

POST /pages/{route}/copy
Duplicate a page (including all content and media) to a new route. The destination parent must exist and the destination path must be free. If the source sets a `slug:` in its frontmatter, the copy's is rewritten to the new folder name. Returns 201 with the new page (only `route` and `slug` if the copy cannot be resolved afterwards). Emits `pages:create:` and `pages:list` cache invalidation tags. Requires `api.pages.write`, subject to the source page's `read` rule and the destination parent's `create` rule.

Parameters

Name Type Description
route required string Source page route (path param).
route required string Destination route for the copy (body field).
JSON
{"route": "/blog/my-post-copy"}
JSON
{"data": {"route": "/blog/my-post-copy", "slug": "my-post-copy", "title": "My Post", "template": "post"}}

Response Codes

201 Page copied; `Location` header points to the new page.
401 Unauthorized.
403 Missing `api.pages.write` permission, or denied by the source or destination page rules.
404 Source page not found.
422 Missing destination `route`, destination parent not found, or destination already exists.

List Page Languages

GET /pages/{route}/languages
List translated and untranslated languages for a page. Use this to drive the language switcher in page editors — `translated` are languages with content, `untranslated` are languages configured for the site but missing this page. `translated` maps each language code to the page's route in that language. On a single-language site both lists are empty. Requires `api.pages.read`, subject to the page's own `read` rule.

Parameters

Name Type Description
route required string The page route (path param).
JSON
{"data": {"route": "/blog/my-post", "default_language": "en", "translated": {"en": "/blog/my-post", "fr": "/blog/mon-article"}, "untranslated": ["de", "es"]}}

Response Codes

200 Language status returned.
401 Unauthorized.
403 Missing `api.pages.read` permission or denied by the page's rules.
404 Page not found.

Note

When the site has only a bare default.md (no language suffix) and multilang is enabled, Grav will report the default language in translated as a fallback. Use GET /pages/{route}?translations=true to get the has_default_file / explicit_language_files fields that disambiguate which languages are backed by a real on-disk file.

Create Translation

POST /pages/{route}/translate
Create a new translation of a page in the specified language. Writes a new `{template}.{lang}.md` file alongside the existing page. The result is validated against the page blueprint. Fires `onApiBeforePageTranslate` (mutable), `onAdminSave`/`onAdminAfterSave`, and `onApiPageTranslated`. Returns 201 with the page in the new language; the `Location` header has no `lang` query, so add `?lang=` to fetch the translation. Requires multi-language to be enabled and `api.pages.write`, subject to the page's own `update` rule. Enabling `process.twig` in `header` also needs the Twig-in-content permission.

Parameters

Name Type Description
route required string The page route (path param).
lang required string Target language code (must match a configured site language).
title optional string Title for the translation (defaults to the source page title).
content optional string Raw markdown content (defaults to the source page content).
header optional object Frontmatter object (defaults to a copy of the source page header, with `title` merged in). Template blueprint defaults fill in anything missing.
JSON
{"lang": "fr", "title": "Mon article", "content": "# Bonjour"}
JSON
{"data": {"route": "/blog/my-post", "slug": "my-post", "title": "Mon article", "template": "post", "language": "fr", "header": {"title": "Mon article"}, "content": "# Bonjour", "media": []}}

Response Codes

201 Translation created.
401 Unauthorized.
403 Missing `api.pages.write` permission, denied by the page's rules, or Twig-in-content not allowed.
404 Source page not found.
422 Missing `lang`, a language code that is not a string or not configured, multi-language not enabled, a translation already exists for that language (use PATCH to update), or blueprint validation failed.

Adopt Language

POST /pages/{route}/adopt-language
Claim an untyped base page file (e.g. `default.md`) as a specific language by renaming it in-place to `{template}.{lang}.md`. Pure filesystem rename + cache bust — content is untouched. Designed for sites that started single-language and later enabled multilang: lets the operator declare "this existing content is the English version" without editing YAML. Fires `onApiBeforePageAdoptLanguage` and `onApiPageLanguageAdopted`. Returns the page as loaded in the adopted language. Requires `api.pages.write`, subject to the page's own `update` rule.

Parameters

Name Type Description
route required string The page route (path param).
lang required string Language code to adopt the base file as.
JSON
{"lang": "en"}
JSON
{"data": {"route": "/blog/my-post", "slug": "my-post", "title": "My Post", "template": "default", "language": "en", "content": "# Hello", "media": []}}

Response Codes

200 Base file renamed to the language-specific filename.
401 Unauthorized.
403 Missing `api.pages.write` permission or denied by the page's rules.
404 Page not found.
422 Missing or invalid `lang` (including a code that is not a string), multi-language not enabled, no untyped base file exists (page already uses language-suffixed files, use `/translate` instead), a translation file for that language already exists, or target filename collides with the base filename.
500 Filesystem rename failed.

Sync Translation

POST /pages/{route}/sync
Overwrite one language's content + header with another language's. Useful for "reset this French page back to the English version" workflows. Both the source and target translation files must already exist. The same Twig-content and blueprint validation checks as a normal page save apply. Fires `onApiBeforePageSync` (mutable header/content) and `onApiPageSynced`. Returns the updated target page. Requires multi-language to be enabled and `api.pages.write`, subject to the page's own `update` rule.

Parameters

Name Type Description
route required string The page route (path param).
source_lang required string Language code to copy content from.
target_lang required string Language code to overwrite.
JSON
{"source_lang": "en", "target_lang": "fr"}
JSON
{"data": {"route": "/blog/my-post", "slug": "my-post", "title": "My Post", "template": "post", "language": "fr", "content": "# Hello", "media": []}}

Response Codes

200 Target translation overwritten with source content.
401 Unauthorized.
403 Missing `api.pages.write` permission, denied by the page's rules, or Twig-in-content not allowed.
404 Page not found for one of the supplied languages.
422 Missing fields, same source/target, target translation file does not exist (create it with `/translate` first), a language code that is not a string or not configured, multi-language not enabled, or blueprint validation failed.

Compare Translations

GET /pages/{route}/compare
Return side-by-side title/content/header/modified for two language versions of a page. Drives translation diff UIs in Admin2. A side is `null` when the page does not resolve in that language, and `exists` is `false` when it resolves only through the default-language fallback rather than its own file, so clients can still show "target missing" states. Requires multi-language to be enabled and `api.pages.read`, and neither the source nor the target version may deny the caller read access.

Parameters

Name Type Description
route required string The page route (path param).
source required string Source language code (query param).
target required string Target language code (query param).
JSON
{"data": {"route": "/blog/my-post", "source": {"lang": "en", "exists": true, "title": "My Post", "content": "# Hello", "header": {"title": "My Post"}, "modified": "2026-04-17T10:00:00+00:00"}, "target": {"lang": "fr", "exists": false, "title": "My Post", "content": "# Hello", "header": {"title": "My Post"}, "modified": null}}}

Response Codes

200 Comparison returned (source/target may be null or `exists: false`).
401 Unauthorized.
403 Missing `api.pages.read` permission, or the source or target version denies read access.
422 Missing `source` / `target` query param, a language code that is not a string (for example `source[]=`) or not configured, or multi-language not enabled.

Batch Page Operations

POST /pages/batch
Run the same operation (`publish`, `unpublish`, `delete`, or `copy`) across multiple pages in one request. Per-page results are returned individually, so one failure does not abort the batch; a page whose own rules deny the action is reported as an `error` result. Every route must resolve to a page before anything runs. Limited to `plugins.api.batch.max_items` (default 50). Emits per-page cache invalidation tags matching the operation. Requires `api.pages.write`.

Parameters

Name Type Description
operation required string One of: `publish`, `unpublish`, `delete`, `copy`.
routes required array Non-empty array of page routes.
options optional object Used by `copy` only: `destination` (parent route to copy into, `/` for the top level, defaults to each page's own parent) and `suffix` (appended to each slug, default `-copy`, must be a single path segment).
JSON
{"operation": "publish", "routes": ["/blog/post-1", "/blog/post-2"]}
JSON
{"data": {"operation": "publish", "results": [{"route": "/blog/post-1", "status": "success"}, {"route": "/blog/post-2", "status": "error", "message": "Page permissions deny 'publish' on this page."}], "total": 2, "successful": 1, "failed": 1}}

Response Codes

200 Batch processed; individual results in `results[]`.
401 Unauthorized.
403 Missing `api.pages.write` permission.
422 Invalid operation, empty routes array, batch limit exceeded, or a route does not exist.

Reorganize Pages

POST /pages/reorganize
Atomically move and/or reorder multiple pages in a single request. All operations are validated before any filesystem changes are applied, then executed via a two-phase temp-rename strategy with best-effort rollback on failure. Cannot move a page into its own subtree, or into a parent that is itself being moved in the same request. Limited to `plugins.api.batch.max_items` (default 50). Fires `onApiBeforePagesReorganize`, `onApiPagesReorganized`, and `onApiPageMoved` for each page that actually moved. Requires `api.pages.write`; a page whose own rules deny `update`, or a destination parent that denies `create`, fails the whole request with 403.

Parameters

Name Type Description
operations required array Non-empty array of operation objects. Each must have a `route`; optional fields are `parent` (new parent route, `/` for the top level; omit to stay under the current parent) and `position` (1-based integer, controls the numeric prefix on the folder).
JSON
{"operations": [{"route": "/blog/post-1", "parent": "/archive", "position": 1}, {"route": "/blog/post-2", "position": 3}]}
JSON
{"data": [{"route": "/archive/post-1", "slug": "post-1", "title": "Post 1", "order": 1, "parent": "/archive"}, {"route": "/blog/post-2", "slug": "post-2", "title": "Post 2", "order": 3, "parent": "/blog"}]}

Response Codes

200 All operations succeeded; response lists every child of every affected parent.
401 Unauthorized.
403 Missing `api.pages.write` permission, or a page or destination parent denies the action.
422 Validation failed (empty operations, too many operations, duplicate routes, missing page or parent, position conflict, a move into the page's own subtree, or a target parent that is also being moved), or a filesystem error during execution (best-effort rollback attempted).

List Taxonomy

GET /taxonomy
Return every taxonomy type (e.g. `category`, `tag`) with the full list of values used across all pages. Every type declared in `site.taxonomies` is included, with an empty list when no page uses it yet. Internal file paths are stripped, so only the term values are returned.
JSON
{"data": {"category": ["blog", "docs"], "tag": ["php", "grav", "api"], "author": []}}

Response Codes

200 Taxonomy map returned.
401 Unauthorized.
403 Missing `api.pages.read` permission.

List Site Languages

GET /languages
Return the site's configured languages with display names, RTL flags, and the default/active codes. Returns `enabled: false`, an empty `languages` list and `null` for `default`/`active` when multilang is off. Drives language switchers and translation UIs.
JSON
{"data": {"enabled": true, "languages": [{"code": "en", "name": "English", "native_name": "English", "rtl": false, "is_default": true}, {"code": "fr", "name": "French", "native_name": "Français", "rtl": false, "is_default": false}], "default": "en", "active": "en"}}

Response Codes

200 Languages returned.
401 Unauthorized.
403 Missing `api.pages.read` permission.

Get Preview Token

POST /pages/{route}/preview-token
Mint a short-lived signed token that lets the frontend render this page even when it is unpublished, for draft previews. Load the page's frontend URL with `?admin_preview=1&preview_token=`; the token unlocks only this page. For a modular section, `route` is the parent page to load and `anchor` suggests an element id to scroll to (it is `null` otherwise). The token lives for `plugins.api.preview_token_ttl` seconds (default 300, minimum 30). Requires `api.pages.read` and read access to the page, and to its parent page when previewing a module.

Parameters

Name Type Description
route required string The page route (path param).
lang optional string Resolve the page in a specific language (query param).
JSON
{"data": {"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "expires_in": 300, "route": "/blog/my-post", "anchor": null}}

Response Codes

200 Preview token returned.
401 Unauthorized.
403 Draft preview is turned off (`plugins.api.allow_draft_preview: false`), missing `api.pages.read` permission, or denied by the page's rules.
404 Page not found.
422 Invalid language code.