Skip to content

Powered by Grav + Helios

Pages

Pages

Endpoints for managing Grav pages including listing, creating, updating, deleting, moving, and reordering.

Endpoints for managing Grav pages including listing, creating, updating, deleting, moving, and reordering.

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, max 100).
sort optional string Sort field: `date`, `title`, `slug`, `modified`, `order`, or `default`. `default` with `children_of` uses native page ordering.
order optional string Sort direction: `asc` or `desc`.
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 string Restrict the listing to a subtree root.
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}], "meta": {"total": 42, "page": 1, "per_page": 20}}

Response Codes

200 Success.
401 Unauthorized.
403 Missing `api.pages.read` permission.

Get Page

GET /pages/{route}
Get a single page with full content, metadata, and media. Returns an ETag; use `If-None-Match` for conditional fetches.

Parameters

Name Type Description
route required string The page route (e.g. `/blog/my-post`).
summary optional boolean Include page summary in the response.
render optional boolean Return rendered HTML content instead of raw markdown.
children optional boolean Include child pages in the response.
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.
404 Page not found.

Create Page

POST /pages
Create a new page.

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
lang optional string Language code for multi-language sites
JSON
{"route": "/blog/new-post", "title": "New Post", "template": "post", "content": "# New Post\nContent here"}
JSON
{"data": {"route": "/blog/new-post", "title": "New Post"}}

Response Codes

201 Page created
401 Unauthorized
422 Validation error

Update Page

PATCH /pages/{route}
Partial update of a page. Only provided fields are changed.

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
template optional string Change the page template
published optional boolean Set the published state
visible optional boolean Set the visible state
JSON
{"title": "Updated Title", "header": {"subtitle": "New subtitle"}}

Response Codes

200 Success
401 Unauthorized
404 Page not found
409 Conflict (ETag mismatch)
422 Validation error

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 optionally its children.

Parameters

Name Type Description
route required string The page route to delete
children optional boolean Also delete child pages
Default: true
lang optional string Delete only a specific language version

Response Codes

204 Page deleted
401 Unauthorized
404 Page not found
422 Validation error

Move Page

POST /pages/{route}/move
Move a page to a new parent location.

Parameters

Name Type Description
route required string The current page route (path parameter)
parent required string The target parent route
slug optional string Optionally rename the slug during the move
order optional integer Numeric ordering prefix at the new location
JSON
{"parent": "/blog", "slug": "moved-post"}

Response Codes

200 Page moved
401 Unauthorized
404 Page not found
422 Validation error

Reorder Pages

POST /pages/{route}/reorder
Reorder child pages under a parent.

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"]}

Response Codes

200 Pages reordered
401 Unauthorized
404 Parent page not found
422 Validation error

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. Returns 201 with the new page. Emits `pages:create:` and `pages:list` cache invalidation tags.

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.
400 Missing destination `route`, destination parent not found, or destination already exists.
401 Unauthorized.
403 Missing `api.pages.write` permission.
404 Source page not found.

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.

Parameters

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

Response Codes

200 Language status returned.
401 Unauthorized.
403 Missing `api.pages.read` permission.
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. Fires `onApiBeforePageTranslate` (mutable), `onAdminSave`/`onAdminAfterSave`, and `onApiPageTranslated`. Returns 201 with the translated page.

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).
JSON
{"lang": "fr", "title": "Mon article", "content": "# Bonjour"}
JSON
{"data": {"route": "/blog/my-post", "lang": "fr", "title": "Mon article", "content": "# Bonjour"}}

Response Codes

201 Translation created.
400 Missing `lang`, invalid language code, or a translation already exists for that language (use PATCH to update).
401 Unauthorized.
403 Missing `api.pages.write` permission.
404 Source page not found.

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

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", "lang": "en"}}

Response Codes

200 Base file renamed to the language-specific filename.
400 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.
401 Unauthorized.
403 Missing `api.pages.write` permission.
404 Page not found.
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. Fires `onApiBeforePageSync` (mutable header/content) and `onApiPageSynced`.

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", "lang": "fr", "title": "My Post", "content": "# Hello"}}

Response Codes

200 Target translation overwritten with source content.
400 Missing fields, same source/target, target translation file does not exist (create it with `/translate` first), or invalid language code.
401 Unauthorized.
403 Missing `api.pages.write` permission.
404 Page not found for one of the supplied languages.

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. Missing translations return `exists: false` rather than 404 so clients can still show "target missing" states.

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`).
400 Missing `source` / `target` query param, or invalid language code.
401 Unauthorized.
403 Missing `api.pages.read` permission.

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 — one failure does not abort the batch. Limited to `plugins.api.batch.max_items` (default 50). Emits per-page cache invalidation tags matching the operation.

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 Operation-specific options (e.g., destination parent for `copy`).
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 not found"}], "total": 2, "successful": 1, "failed": 1}}

Response Codes

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

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. Limited to `plugins.api.batch.max_items` (default 50). Fires `onApiBeforePagesReorganize` and `onApiPagesReorganized`.

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) and `position` (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.
400 Validation failed — empty operations, duplicate routes, missing page, position conflict, attempt to move a page into its own subtree, or filesystem error during execution (partial rollback attempted).
401 Unauthorized.
403 Missing `api.pages.write` permission.

List Taxonomy

GET /taxonomy
Return every taxonomy type (e.g. `category`, `tag`) with the full list of values used across all pages. Internal file paths are stripped — only the term values are returned.
JSON
{"data": {"category": ["blog", "docs"], "tag": ["php", "grav", "api"]}}

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` with empty `languages` 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.