Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Template Context API

Every admin page receives a structured context object built by the ContextBuilder. When overriding templates, you can access any of these variables in your Handlebars templates.

Top-Level Keys

KeyTypePagesDescription
crapobjectallApp metadata (version, dev mode, auth status)
pageobjectallCurrent page info (title, type, breadcrumbs)
navobjectall (except auth)Navigation data for sidebar
userobjectauthenticatedCurrent user (email, id, collection)
collectionobjectcollection pagesFull collection definition with metadata
globalobjectglobal pagesFull global definition with metadata
documentobjectedit pagesCurrent document with raw data
fieldsarrayedit/create/global editProcessed field contexts for main content area
sidebar_fieldsarrayedit/create/global editField contexts for sidebar panel (fields with admin.position = "sidebar")
docsarraycollection itemsDocument list with enriched data
editingbooleanedit/createtrue when editing, false when creating
paginationobjectitems, versionsPagination state
versionsarrayedit (versioned)Recent version entries
has_more_versionsbooleanedit (versioned)Whether more versions exist beyond the shown 3
uploadobjectupload collectionsUpload file metadata and preview
collection_cardsarraydashboardCollection summary cards with counts
global_cardsarraydashboardGlobal summary cards
searchstringitemsCurrent search query
customobjectallCustom data injected by before_render hooks

Base Context (Every Page)

crap — App Metadata

{{crap.version}}      {{!-- "0.1.0" --}}
{{crap.build_hash}}   {{!-- Git commit hash at build time --}}
{{crap.dev_mode}}     {{!-- true/false --}}
{{crap.auth_enabled}} {{!-- true if any auth collection exists --}}

page — Page Info

{{page.title}}  {{!-- "Edit Post", "Dashboard", etc. --}}
{{page.type}}   {{!-- "collection_edit", "dashboard", etc. --}}

{{#each page.breadcrumbs}}
  {{#if this.url}}
    <a href="{{this.url}}">{{this.label}}</a>
  {{else}}
    <span>{{this.label}}</span>
  {{/if}}
{{/each}}

Page Types

TypeRoute
dashboard/admin
collection_list/admin/collections
collection_items/admin/collections/{slug}
collection_edit/admin/collections/{slug}/{id}
collection_create/admin/collections/{slug}/create
collection_delete/admin/collections/{slug}/{id}/delete
collection_versions/admin/collections/{slug}/{id}/versions
global_edit/admin/globals/{slug}
global_versions/admin/globals/{slug}/versions
auth_login/admin/login
auth_forgot/admin/forgot-password
auth_reset/admin/reset-password
error_403(forbidden)
error_404(not found)
error_500(server error)

Available on all authenticated pages. Auth pages use ContextBuilder::auth() which does not include nav.

{{#each nav.collections}}
  <a href="/admin/collections/{{this.slug}}">{{this.display_name}}</a>
  {{!-- Also available: this.is_auth, this.is_upload --}}
{{/each}}

{{#each nav.globals}}
  <a href="/admin/globals/{{this.slug}}">{{this.display_name}}</a>
{{/each}}

Each nav collection entry:

FieldTypeDescription
slugstringCollection slug
display_namestringHuman-readable name
is_authbooleanWhether this is an auth collection
is_uploadbooleanWhether this is an upload collection

Each nav global entry:

FieldTypeDescription
slugstringGlobal slug
display_namestringHuman-readable name

user — Current User

Present when the user is authenticated (JWT session valid). Absent on auth pages and error pages.

{{#if user}}
  Logged in as {{user.email}} ({{user.collection}})
{{/if}}
FieldTypeDescription
emailstringUser’s email address
idstringUser document ID
collectionstringAuth collection slug (e.g., "users")

Collection Pages

collection — Collection Definition

Available on all collection page types (collection_items, collection_edit, collection_create, collection_delete, collection_versions).

{{collection.slug}}
{{collection.display_name}}
{{collection.singular_name}}
{{collection.title_field}}
{{collection.timestamps}}       {{!-- boolean --}}
{{collection.is_auth}}          {{!-- boolean --}}
{{collection.is_upload}}        {{!-- boolean --}}
{{collection.has_drafts}}       {{!-- boolean --}}
{{collection.has_versions}}     {{!-- boolean --}}

collection.admin

{{collection.admin.use_as_title}}          {{!-- field name or null --}}
{{collection.admin.default_sort}}          {{!-- e.g., "-created_at" or null --}}
{{collection.admin.hidden}}                {{!-- boolean --}}
{{collection.admin.list_searchable_fields}} {{!-- array of field names --}}

collection.upload

null unless the collection has upload enabled.

{{#if collection.upload}}
  Accepts: {{collection.upload.mime_types}}
  Max size: {{collection.upload.max_file_size}}
  Thumbnail: {{collection.upload.admin_thumbnail}}
{{/if}}

collection.versions

null unless the collection has versioning enabled.

{{#if collection.versions}}
  Drafts: {{collection.versions.drafts}}
  Max versions: {{collection.versions.max_versions}}
{{/if}}

collection.auth

null unless the collection is an auth collection.

{{#if collection.auth}}
  Local login: {{#if (not collection.auth.disable_local)}}enabled{{/if}}
  Email verification: {{collection.auth.verify_email}}
{{/if}}

collection.fields_meta

Array of raw field definitions. Useful for JavaScript conditional logic or embedding metadata.

{{#each collection.fields_meta}}
  {{this.name}} — {{this.field_type}}
  Required: {{this.required}}, Localized: {{this.localized}}
  Label: {{this.admin.label}}
{{/each}}

{{!-- Serialize to JSON for JavaScript --}}
<script>
  const fields = {{{json collection.fields_meta}}};
</script>

Each entry in fields_meta:

FieldTypeDescription
namestringField name
field_typestringtext, number, select, relationship, etc.
requiredbooleanWhether the field is required
uniquebooleanWhether the field has a unique constraint
localizedbooleanWhether the field is localized
admin.labelstring/nullDisplay label
admin.hiddenbooleanWhether the field is hidden in admin
admin.readonlybooleanWhether the field is readonly
admin.widthnumber/nullColumn width hint
admin.descriptionstring/nullHelp text
admin.placeholderstring/nullPlaceholder text

Dashboard

Page type: dashboard

Additional keys:

KeyTypeDescription
collection_cardsarrayOne entry per collection with document count
global_cardsarrayOne entry per global
{{#each collection_cards}}
  <a href="/admin/collections/{{this.slug}}">
    {{this.display_name}} ({{this.count}} items)
  </a>
{{/each}}

{{#each global_cards}}
  <a href="/admin/globals/{{this.slug}}">{{this.display_name}}</a>
{{/each}}

Collection card fields: slug, display_name, singular_name, count, last_updated, is_auth, is_upload, has_versions.

Global card fields: slug, display_name, last_updated, has_versions.


Collection Items (List)

Page type: collection_items

Additional keys beyond collection:

KeyTypeDescription
docsarrayDocument list
searchstring/nullCurrent search query
paginationobjectPagination state
has_draftsbooleanShorthand for collection.has_drafts

docs

{{#each docs}}
  <tr>
    <td>{{this.title_value}}</td>
    <td>{{this.status}}</td>
    <td>{{this.created_at}}</td>
    <td>{{this.updated_at}}</td>
    {{#if this.thumbnail_url}}
      <td><img src="{{this.thumbnail_url}}" /></td>
    {{/if}}
  </tr>
{{/each}}

Each doc:

FieldTypeDescription
idstringDocument ID
title_valuestringValue of the title field (falls back to filename for uploads, then ID)
created_atstring/nullCreation timestamp
updated_atstring/nullLast update timestamp
thumbnail_urlstring/nullThumbnail URL (upload collections with images only)

pagination

{{#if pagination.has_prev}}
  <a href="{{pagination.prev_url}}">Previous</a>
{{/if}}
<span>Page {{pagination.page}} of {{pagination.total_pages}}</span>
{{#if pagination.has_next}}
  <a href="{{pagination.next_url}}">Next</a>
{{/if}}
FieldTypeDescription
pageintegerCurrent page number (1-based)
per_pageintegerItems per page
totalintegerTotal document count
total_pagesintegerTotal number of pages
has_prevbooleanWhether a previous page exists
has_nextbooleanWhether a next page exists
prev_urlstringURL for the previous page
next_urlstringURL for the next page

Collection Edit

Page type: collection_edit

Additional keys beyond collection:

KeyTypeDescription
documentobjectCurrent document data
fieldsarrayProcessed field contexts for form rendering
editingbooleanAlways true
has_draftsbooleanShorthand for collection.has_drafts
has_versionsbooleanShorthand for collection.has_versions
versionsarrayUp to 3 most recent version entries
has_more_versionsbooleantrue if more than 3 versions exist
uploadobjectUpload context (upload collections only)

document

{{document.id}}
{{document.created_at}}
{{document.updated_at}}
{{document.status}}        {{!-- "published" or "draft" --}}

{{!-- Raw field values --}}
{{document.data.title}}
{{document.data.category}}
FieldTypeDescription
idstringDocument ID
created_atstring/nullCreation timestamp
updated_atstring/nullLast update timestamp
statusstring"published" or "draft" (when drafts enabled)
dataobjectRaw field values as key-value pairs

Draft loading: When a collection has drafts enabled and the latest version is a draft, the edit page loads the document from the draft version snapshot. This means document.data contains the draft values, including block and array data — not the published main-table values.

fields

Processed field context objects used by the {{render_field}} helper or custom form rendering. See Field Context below.

versions

{{#each versions}}
  v{{this.version}} — {{this.status}}
  {{#if this.latest}} (latest) {{/if}}
  Created: {{this.created_at}}
{{/each}}
{{#if has_more_versions}}
  <a href="/admin/collections/{{collection.slug}}/{{document.id}}/versions">View all</a>
{{/if}}

Each version entry: id, version (number), status ("published" or "draft"), latest (boolean), created_at.

upload

Present only on upload collection edit/create pages.

{{#if collection.is_upload}}
  {{#if upload.preview}}
    <img src="{{upload.preview}}" />
  {{/if}}
  {{#if upload.info}}
    {{upload.info.filename}}
    {{upload.info.filesize_display}}  {{!-- e.g., "2.4 MB" --}}
    {{upload.info.dimensions}}        {{!-- e.g., "1920x1080" --}}
  {{/if}}
  {{#if upload.accept}}
    <input type="file" accept="{{upload.accept}}" />
  {{/if}}
{{/if}}
FieldTypeDescription
acceptstring/nullMIME type filter for file input (e.g., "image/*")
previewstring/nullPreview image URL (images only, uses admin_thumbnail)
infoobject/nullFile info for existing uploads
info.filenamestringOriginal filename
info.filesize_displaystringHuman-readable file size
info.dimensionsstring/nullImage dimensions (e.g., "1920x1080")

Collection Create

Page type: collection_create

Same structure as collection edit, with these differences:

  • editing is false
  • document is absent
  • versions, has_more_versions are absent
  • Password field is added for auth collections (required)

Collection Delete

Page type: collection_delete

KeyTypeDescription
collectionobjectCollection definition
document_idstringID of the document to delete
title_valuestring/nullDisplay title of the document

Collection Versions

Page type: collection_versions

Full version history page with pagination.

KeyTypeDescription
collectionobjectCollection definition
documentobjectStub with id only
doc_titlestringDocument title for breadcrumbs
versionsarrayPaginated version entries
paginationobjectPagination state

Collection List

Page type: collection_list

KeyTypeDescription
collectionsarrayAll registered collections

Each entry: slug, display_name, field_count.


Global Edit

Page type: global_edit

KeyTypeDescription
globalobjectGlobal definition
fieldsarrayProcessed field contexts (main area)
sidebar_fieldsarrayProcessed field contexts (sidebar)
has_draftsbooleanWhether the global has drafts enabled
has_versionsbooleanWhether the global has versions enabled
versionsarrayRecent version entries (up to 3)
has_more_versionsbooleantrue if more than 3 versions exist
restore_url_prefixstringURL prefix for version restore actions
versions_urlstringURL to the full versions list page
doc_statusstringDocument status ("published" or "draft")

global

{{global.slug}}
{{global.display_name}}
{{#each global.fields_meta}}
  {{this.name}} — {{this.field_type}}
{{/each}}
FieldTypeDescription
slugstringGlobal slug
display_namestringHuman-readable name
fields_metaarraySame structure as collection.fields_meta

Auth Pages

Auth pages use a minimal context builder (ContextBuilder::auth()) — no nav or user.

Login (auth_login)

KeyTypeDescription
collectionsarrayAuth collections (slug + display_name)
show_collection_pickerbooleantrue if more than one auth collection
disable_localbooleantrue if all auth collections disable local login
show_forgot_passwordbooleantrue if email is configured and any collection enables forgot password
errorstring/nullError message (e.g., “Invalid email or password”)
successstring/nullSuccess message (e.g., after password reset)
emailstring/nullPre-filled email (on error re-render)

Forgot Password (auth_forgot)

KeyTypeDescription
collectionsarrayAuth collections
show_collection_pickerbooleantrue if more than one auth collection
successboolean/nulltrue after form submission (always, to avoid leaking user existence)

Reset Password (auth_reset)

KeyTypeDescription
tokenstring/nullReset token (if valid)
errorstring/nullError message (invalid/expired token, validation errors)

Error Pages

Error pages receive the base context (crap, nav, page) plus:

KeyTypeDescription
messagestringError description

Page types: error_403, error_404, error_500.

<h1>{{page.title}}</h1>
<p>{{message}}</p>

Locale Context

When localization is enabled in crap.toml, edit/create pages receive additional keys merged into the top level:

KeyTypeDescription
has_localesbooleanAlways true when locale is enabled
current_localestringCurrently selected locale (e.g., "en")
localesarrayAll configured locales with selection state
{{#if has_locales}}
  {{#each locales}}
    <option value="{{this.value}}" {{#if this.selected}}selected{{/if}}>
      {{this.label}}
    </option>
  {{/each}}
{{/if}}

Each locale entry: value (e.g., "en"), label (e.g., "EN"), selected (boolean).

When editing a non-default locale, non-localized fields are rendered as readonly (locale-locked).

Editor Locale (Content Locale)

In addition to the UI translation locale (has_locales/current_locale/locales), edit/create pages also receive content editor locale variables when locale is enabled:

KeyTypeDescription
has_editor_localesbooleanAlways true when locale is enabled
editor_localestringCurrently selected content locale (e.g., "en")
editor_localesarrayAll configured locales with selection state

These control which content locale is being edited, separate from the admin UI translation locale.


Field Context

The fields array contains processed field context objects, one per field. These are used by {{{render_field field}}} or can be iterated manually.

Common Fields

Every field context object has:

FieldTypeDescription
namestringField name (HTML form input name)
field_typestringType identifier (e.g., "text", "select", "blocks")
labelstringDisplay label (from admin.label or auto-generated)
requiredbooleanWhether the field is required
valuestringCurrent value (stringified)
placeholderstring/nullPlaceholder text
descriptionstring/nullHelp text
readonlybooleanWhether the field is readonly
localizedbooleanWhether the field is localized
locale_lockedbooleantrue when editing a non-default locale and the field is not localized
errorstring/nullValidation error message (on re-render after failed save)
positionstring/nullField position: "main" (default) or "sidebar". Fields with "sidebar" appear in sidebar_fields instead of fields
condition_visibleboolean/nullInitial visibility from display condition evaluation. false = hidden on page load
condition_jsonobject/nullClient-side condition table (JSON). Present for condition functions that return a table
condition_refstring/nullServer-side condition function reference. Present for condition functions that return a boolean

Select Fields

Additional keys:

FieldTypeDescription
optionsarrayAvailable options

Each option: label, value, selected (boolean).

Checkbox Fields

FieldTypeDescription
checkedbooleanWhether the checkbox is checked

Date Fields

FieldTypeDescription
picker_appearancestring"dayOnly", "dayAndTime", "timeOnly", or "monthOnly"
date_only_valuestringDate portion only, e.g., "2026-01-15" (dayOnly)
datetime_local_valuestringDate+time, e.g., "2026-01-15T09:00" (dayAndTime)

Relationship Fields

FieldTypeDescription
relationship_collectionstringRelated collection slug
has_manybooleanWhether this is a has-many relationship
relationship_optionsarrayAvailable documents from the related collection

Each option: value (document ID), label (title field value), selected (boolean).

Upload Fields

FieldTypeDescription
relationship_collectionstringUpload collection slug
relationship_optionsarrayAvailable uploads with thumbnail info
selected_preview_urlstring/nullPreview URL of the currently selected upload
selected_filenamestring/nullFilename of the currently selected upload

Each option: value, label, selected, thumbnail_url (if image), is_image (boolean), filename.

Group Fields

FieldTypeDescription
sub_fieldsarraySub-field contexts (same structure as top-level fields)
collapsedbooleanWhether the group starts collapsed

Sub-field name is formatted as group__subfield (double underscore).

Array Fields

FieldTypeDescription
sub_fieldsarraySub-field definitions (template for new rows)
rowsarrayExisting row data
row_countintegerNumber of existing rows
template_idstringUnique ID for DOM targeting (count badge, row container, templates)
label_fieldstring/nullSub-field name used for dynamic row labels (from admin.label_field)
max_rowsinteger/nullMaximum number of rows allowed (from max_rows on field definition)
min_rowsinteger/nullMinimum number of rows required (from min_rows on field definition)
init_collapsedbooleanWhether existing rows render collapsed by default (from admin.collapsed, default: true)
add_labelstring/nullCustom singular label for the add button (from admin.labels.singular, e.g., “Slide” → “Add Slide”)

Each row: index (integer), sub_fields (array of field contexts with indexed names like items[0][title]), custom_label (string/null — computed label from row_label or label_field).

The <fieldset> element includes a data-label-field attribute when label_field is set, enabling JavaScript to update row titles live as the user types.

Blocks Fields

FieldTypeDescription
block_definitionsarrayAvailable block types with their fields
rowsarrayExisting block instances
row_countintegerNumber of existing blocks
template_idstringUnique ID for DOM targeting (count badge, row container, templates)
label_fieldstring/nullField-level admin.label_field (shared fallback for all block types)
max_rowsinteger/nullMaximum number of blocks allowed (from max_rows on field definition)
min_rowsinteger/nullMinimum number of blocks required (from min_rows on field definition)
init_collapsedbooleanWhether existing block rows render collapsed by default (from admin.collapsed, default: true)
add_labelstring/nullCustom singular label for the add button (from admin.labels.singular, e.g., “Section” → “Add Section”)

Each block definition: block_type, label, label_field (string/null — per-block-type label field), fields (array of sub-field contexts).

Each row: index, _block_type, block_label, custom_label (string/null — computed label from row_label, block label_field, or field label_field), sub_fields (array of field contexts with indexed names like content[0][heading]).


Handlebars Helpers

In addition to the standard Handlebars helpers, these custom helpers are available:

Logic Helpers

HelperUsageDescription
eq{{#if (eq a b)}}Equality check (any types)
not{{#if (not val)}}Boolean negation
and{{#if (and a b)}}Logical AND
or{{#if (or a b)}}Logical OR

Comparison Helpers

HelperUsageDescription
gt{{#if (gt a b)}}Greater than (numeric)
lt{{#if (lt a b)}}Less than (numeric)
gte{{#if (gte a b)}}Greater than or equal (numeric)
lte{{#if (lte a b)}}Less than or equal (numeric)
contains{{#if (contains haystack needle)}}Array/string contains

Utility Helpers

HelperUsageDescription
json{{{json value}}}Serialize to JSON string (use triple braces)
default{{default val "fallback"}}Fallback for falsy values
concat{{concat a b c}}String concatenation (variadic)
t{{t "key"}}Translation lookup
render_field{{{render_field field}}}Render a field partial (use triple braces)

Translation Helper

Supports interpolation:

{{t "welcome"}}                     {{!-- simple lookup --}}
{{t "greeting" name="World"}}       {{!-- replaces {{name}} in translation string --}}

Truthiness

Helpers like not, and, or, and default use Handlebars truthiness:

  • Falsy: null, false, 0, "" (empty string), [] (empty array)
  • Truthy: everything else (including empty objects {})

Composition

Helpers can be composed as sub-expressions:

{{#if (and (not collection.is_auth) (gt pagination.total 0))}}
  Showing {{pagination.total}} items
{{/if}}

{{#if (or collection.has_drafts collection.has_versions)}}
  This collection supports versioning
{{/if}}

<a href="{{concat "/admin/collections/" collection.slug "/create"}}">New</a>

before_render Hook

You can inject custom data into every admin page context using the before_render hook:

-- init.lua
crap.hooks.register("before_render", function(context)
  context.custom = {
    announcement = "Maintenance tonight at 10pm",
    feature_flags = { new_editor = true },
  }
  return context
end)

Then in your templates:

{{#if custom.announcement}}
  <div class="announcement">{{custom.announcement}}</div>
{{/if}}

{{#if custom.feature_flags.new_editor}}
  {{!-- show new editor --}}
{{/if}}

The before_render hook:

  • Fires on every admin page render (GET and POST error re-renders)
  • Receives the full template context as a Lua table
  • Must return the (possibly modified) context
  • Has no CRUD access (no database operations) — keeps it fast
  • Use the custom key by convention for injected data
  • Can read and modify any context key (not just custom), but modifying built-in keys may break default templates
  • On error: logs a warning and returns the original context unmodified

Example: Conditional Navigation

crap.hooks.register("before_render", function(context)
  -- Add environment indicator
  local env = crap.env.get("APP_ENV") or "development"
  context.custom = context.custom or {}
  context.custom.environment = env
  context.custom.is_production = (env == "production")
  return context
end)
{{#if custom.is_production}}
  <div class="env-badge env-badge--production">PRODUCTION</div>
{{else}}
  <div class="env-badge">{{custom.environment}}</div>
{{/if}}

Full Example: Custom List Template

A complete example overriding the items list to add a custom column:

{{!-- <config_dir>/templates/collections/items.hbs --}}
{{#> layout/base}}

<h1>{{page.title}}</h1>

{{#if custom.announcement}}
  <div class="alert">{{custom.announcement}}</div>
{{/if}}

<table>
  <thead>
    <tr>
      <th>Title</th>
      <th>Status</th>
      {{#if collection.is_upload}}<th>Preview</th>{{/if}}
      <th>Updated</th>
    </tr>
  </thead>
  <tbody>
    {{#each docs}}
      <tr>
        <td>
          <a href="/admin/collections/{{../collection.slug}}/{{this.id}}">
            {{this.title_value}}
          </a>
        </td>
        <td>{{this.status}}</td>
        {{#if ../collection.is_upload}}
          <td>
            {{#if this.thumbnail_url}}
              <img src="{{this.thumbnail_url}}" width="40" />
            {{/if}}
          </td>
        {{/if}}
        <td>{{this.updated_at}}</td>
      </tr>
    {{/each}}
  </tbody>
</table>

{{#if pagination.has_prev}}
  <a href="{{pagination.prev_url}}">Previous</a>
{{/if}}
{{#if pagination.has_next}}
  <a href="{{pagination.next_url}}">Next</a>
{{/if}}

{{!-- Embed field metadata for client-side use --}}
<script>
  const collectionDef = {{{json collection}}};
</script>

{{/layout/base}}