Create PagibleAI Content Elements

Define custom content elements in a theme package's schema.json, then add one Blade view with the same element name. PagibleAI merges the schema when the theme service provider calls Schema::register().

This guide uses a theme named mytheme. Its quote entry is registered as mytheme::quote, so it cannot collide with an element from another theme.

Create an element

Add the element below the top-level content key in your theme's schema.json. The surrounding theme file also contains metadata and page-type definitions:

{
    "label": "My Theme",
    "description": "Theme for the company website",
    "maintainer": "My Company",
    "email": "info@example.com",
    "website": "https://example.com",
    "types": {
        "page": {
            "sections": ["main", "footer"]
        }
    },
    "content": {
        "quote": {
            "label": "Quote",
            "group": "content",
            "description": "Highlighted quotation with its author",
            "icon": "<svg viewBox=\"0 0 24 24\" fill=\"currentColor\"><path d=\"M7 7h4v10H5V9a2 2 0 0 1 2-2Zm8 0h4v10h-6V9a2 2 0 0 1 2-2Z\"/></svg>",
            "fields": {
                "text": {
                    "type": "text",
                    "label": "Quote",
                    "hint": "Quotation shown in large letters",
                    "required": true,
                    "default": "Add the quotation."
                },
                "author": {
                    "type": "string",
                    "label": "Author",
                    "hint": "Person the quotation is attributed to",
                    "max": 100
                }
            }
        }
    }
}

Create views/quote.blade.php in the same theme package:

<figure class="quote">
    <blockquote>@text($data->text ?? '')</blockquote>

    @if($data->author ?? null)
        <figcaption>{{ $data->author }}</figcaption>
    @endif
</figure>

After Composer discovers the theme provider, select the theme on a page. The editor will offer Quote in the content-element dialog. The stored type is mytheme::quote; the renderer tries mytheme::quote, then the base cms::quote view, and finally cms::invalid.

Always set a label for theme elements. Without it, the admin panel shows the prefixed type, e.g. "mytheme quote".

Element properties

Each entry below content uses:

  • group: category shown as a tab in the content-element dialog, such as basic, content, media or forms; elements without a group appear under "uncategorized"
  • icon: SVG markup for the chooser; use fill="currentColor" so it follows light and dark modes
  • description: optional one-sentence summary shown below the element name in the content-element dialog
  • label: display name for the element dialog; without it, the prefixed type such as mytheme quote is shown
  • fields: object keyed by the names stored below the element's data property

The search field of the content-element dialog matches the type, label, description and group name, so editors find elements across all tabs.

The admin panel translates element names with the st context, descriptions with sd and group names with sg, using the theme's admin catalog. The st key is the label, e.g. "Quote": {"st": "Zitat"}. If an element has no label, the key is the full prefixed type such as mytheme::quote, never the bare schema key quote. Keep the description short and explain what the element is for, e.g. "Highlighted quotation with its author".

Every field requires a type. label, default, validation settings and selector options depend on that type. Reusing familiar names such as title, text and file also lets PagibleAI carry compatible values across when an editor changes an element's type.

Base theme elements

Check the base theme's elements before you create your own. A theme view with the same name, e.g. views/hero.blade.php, overrides the base view without a new schema entry.

  • basic: heading, text, image-text, code, table, html
  • media: image, slideshow, video, audio, file, map
  • content: hero, cta, cards, testimonial, article, blog, news, heros, pricing, questions, toc
  • forms: contact

PagibleAI 0.13 added these elements and fields:

  • map shows an OpenStreetMap location with title, text and an optional button
  • cta is a call to action with text and buttons
  • news lists pages of type news, like blog does for articles
  • contact has configurable mandatory and optional form fields
  • article has author-name and author-url fields for the byline and structured data
  • cards in cards have an optional url
  • the website config element stores the site name used in the layout and structured data; security and robots-txt configure /security.txt and /robots.txt

Common field settings

  • label: translated label shown in the admin panel
  • hint: translated help text shown below the control while it is focused; AI tools and the JSON:API schema receive it as the field description
  • default: value used before an editor stores a value
  • required: the field must not be empty; for items and images, at least one entry is needed
  • min and max: length, item-count, column-count or numeric bounds depending on the field
  • placeholder: example value shown in an empty control

For text fields, min only applies to non-empty values. Use required: true to make a field mandatory instead of min: 1. required is supported by the string, plaintext, text, markdown, html, url, number, date, color, radio, select, autocomplete, combobox, map, items, images and all single-file fields.

Defaults must match the value type. Use a JSON array for items, images, range and multiple-choice fields, an object for one selected file or map location, and a scalar for ordinary text or number fields.

Keep a hint to one short sentence that explains what the value is used for instead of repeating the label, e.g. "Shown in the browser tab and search results". Every field type supports hint. Translate labels, hints and option labels in the theme's admin catalog as described in Create a PagibleAI Theme Package.

Text fields

string

Short text control with min, max, placeholder, required, default and an optional class. Use pattern for a regular-expression check and uppercase: true to normalize stored input.

plaintext

Multi-line plain text with min, max, placeholder, required, default and an optional class applied to the control.

text

CKEditor with limited inline formatting. It supports min, max, required and default.

markdown

Full Markdown editor with min, max, required and default.

html

Raw HTML textarea with min, max, placeholder, required and default. The value may be empty unless you set required: true.

url

URL input with allowed, placeholder, required and default. allowed is an array of scheme names. The field also suggests matching CMS pages while the editor types.

  • absolute: true accepts only absolute URLs with one of the allowed schemes and a host name. Relative paths, #fragment and ?query links are rejected and page suggestions are disabled.
  • rel: true shows a link-attribute selector next to the field when the value is an external link. Editors can choose none, sponsored or nofollow. The selection is stored in a sibling <field>-rel property, e.g. url-rel, so the URL itself stays a string. Output it in the view with rel="{{ $data->{'url-rel'} ?? '' }}".

Combined text-field schema example:

{
    "title": {
        "type": "string",
        "label": "Title",
        "hint": "Headline shown above the text",
        "min": 5,
        "max": 100,
        "placeholder": "Enter a title"
    },
    "summary": {
        "type": "text",
        "label": "Summary",
        "hint": "Short introduction shown in article teasers",
        "max": 500
    },
    "body": {
        "type": "markdown",
        "label": "Body",
        "required": true
    },
    "url": {
        "type": "url",
        "label": "Link",
        "hint": "Page or link the button leads to",
        "rel": true
    },
    "website": {
        "type": "url",
        "label": "Website",
        "allowed": ["https"],
        "absolute": true,
        "required": false
    }
}

Choice fields

checkbox

One checkbox with optional on, off and default values.

switch

Toggle with optional on, off and default values.

radio

Selects one value from options; supports required and default.

select

Dropdown using options; supports multiple, placeholder, required and default.

combobox

Searchable dropdown that also accepts typed input. It supports required and the remote-data settings documented below. Set multiple: true to let editors select or type several values, shown as chips and stored as an array of strings, and limit their number with max. Use an array as default for a multiple combobox.

autocomplete

Suggestion picker populated from initial options, GraphQL or REST. Like combobox, it accepts multiple, max and required.

options is an array of objects containing label and value. Combined choice-field example:

{
    "category": {
        "type": "select",
        "label": "Category",
        "options": [
            {"label": "Technology", "value": "tech"},
            {"label": "Design", "value": "design"}
        ],
        "required": true,
        "default": "tech"
    },
    "featured": {
        "type": "switch",
        "label": "Featured",
        "on": true,
        "off": false,
        "default": false
    },
    "tags": {
        "type": "combobox",
        "label": "Tags",
        "hint": "Keywords shown below the article",
        "options": [
            {"label": "News", "value": "news"},
            {"label": "Guide", "value": "guide"}
        ],
        "multiple": true,
        "max": 10,
        "default": []
    }
}

Remote autocomplete and combobox

Set api-type to REST or GQL. Use _term_ in the URL or trusted GraphQL query; PagibleAI replaces it with the escaped search term. list-key, item-title and item-value are slash-separated paths into the returned JSON.

REST example:

{
    "repository": {
        "type": "autocomplete",
        "label": "Repository",
        "api-type": "REST",
        "url": "/api/repos?filter=_term_",
        "list-key": "items",
        "item-title": "name",
        "item-value": "url",
        "placeholder": "Start typing",
        "empty-text": "No repositories found",
        "required": true
    }
}

GraphQL example:

{
    "parent-page": {
        "type": "autocomplete",
        "label": "Parent page",
        "api-type": "GQL",
        "query": "query { pages(filter: {title: _term_}) { data { id title } } }",
        "list-key": "pages/data",
        "item-title": "title",
        "item-value": "id"
    }
}

Number, range, date and color fields

number

Numeric input with min, max, step, precision, placeholder, required and default.

slider

One numeric value with min, max, step and default.

range

Two numeric values with min, max, step and an array default.

date

One or multiple dates with allowed, min, max, multiple, placeholder, required and default.

color

Hexadecimal color value with required and default.

Combined numeric, date and color example:

{
    "price": {
        "type": "number",
        "label": "Price",
        "min": 0,
        "step": 0.01,
        "precision": 2,
        "required": true
    },
    "price-range": {
        "type": "range",
        "label": "Price range",
        "min": 0,
        "max": 1000,
        "step": 10,
        "default": [100, 500]
    },
    "event-date": {
        "type": "date",
        "label": "Event date",
        "min": "2026-01-01",
        "max": "2026-12-31"
    },
    "accent": {
        "type": "color",
        "label": "Accent",
        "default": "#2563EB"
    }
}

Location field

map

Interactive OpenStreetMap picker. Editors search for a place or click on the map to set the location. The value is an object with latitude, longitude and zoom (1–19). It supports zoom for the initial zoom level (default 15), required and an object default.

The base theme's map content element uses this field. Its frontend view embeds OpenStreetMap, so keep https://www.openstreetmap.org in CMS_CSP_FRAME_SRC when you override that setting.

{
    "location": {
        "type": "map",
        "label": "Location",
        "hint": "Search for the address or click on the map to set the location",
        "zoom": 16,
        "required": true
    }
}

Repeated data

items

Ordered list of structured objects. Define the row fields below item; identity names an optional generated stable property. min, max, required and default apply to the outer array.

table

Editable rows and columns. min and max constrain the column count; default is a two-dimensional array.

An items default must be an array of objects. Combined repeated-data example:

{
    "members": {
        "type": "items",
        "label": "Team members",
        "identity": "id",
        "min": 1,
        "max": 6,
        "default": [
            {
                "name": "Jane Doe",
                "position": "Editor"
            }
        ],
        "item": {
            "name": {
                "type": "string",
                "label": "Name",
                "required": true
            },
            "position": {
                "type": "string",
                "label": "Position"
            }
        }
    },
    "comparison": {
        "type": "table",
        "label": "Comparison",
        "min": 2,
        "max": 4,
        "default": [
            ["Feature", "Plan"],
            ["Support", "Included"]
        ]
    }
}

File and media fields

file

Selects one file of any accepted MIME type. Supports accept and required.

image

Selects one image. Supports accept and required.

images

Selects an ordered image list. Use min and max for the allowed item count, required for at least one image and accept for MIME types.

media

Selects one image or video and exposes the media editing controls. Supports accept and required.

audio

Selects one audio file. Supports accept and required.

video

Selects one video file. Supports accept and required.

Use accept to restrict MIME types. Single-file fields and images support required; images also uses min for a minimum number of images. Combined file-field example:

{
    "document": {
        "type": "file",
        "label": "PDF",
        "accept": "application/pdf",
        "required": true
    },
    "cover": {
        "type": "image",
        "label": "Cover image",
        "accept": "image/jpeg,image/webp"
    },
    "gallery": {
        "type": "images",
        "label": "Gallery",
        "accept": "image/*",
        "min": 2,
        "max": 8
    },
    "feature-media": {
        "type": "media",
        "label": "Image or video",
        "accept": "image/*,video/*"
    }
}

Hidden fields and actions

hidden

Stores a fixed schema value without showing a control. Its primary use is selecting a trusted action handler.

{
    "action": {
        "type": "hidden",
        "value": "\\App\\Cms\\LatestArticles"
    }
}

During rendering, PagibleAI resolves the handler from the trusted registered schema for the element type. It does not execute a callable supplied only in stored page content. See Add Dynamic Content to Pages for the action class and query patterns.

Troubleshoot schema fields

  • The element is absent from the chooser: validate schema.json, confirm the theme provider is loaded and select that theme on the page. Clear cached configuration and views with php artisan optimize:clear.
  • The element name shows the theme prefix or isn't translated: add a label to the element and use that label as st key in the theme's admin catalog.
  • A field does not render: its type must match a registered component name from admin/js/fields; field names are case-sensitive after conversion to the component name.
  • An empty field passes validation despite min: min only checks non-empty values; add required: true.
  • A default is rejected: match the value shape to the field—scalar, object or array—and keep min, max and required consistent with it.
  • A remote chooser stays empty: inspect the REST or GraphQL response, CORS policy, api-type, list-key, item-title and item-value.
  • A hint stays in English: add it to the theme's admin catalog with the fh context and republish the theme assets. Element names, descriptions and groups use the st, sd and sg contexts.
  • The link-attribute selector is missing: set rel: true on the url field; the selector only appears for external http(s):// links.
  • The map stays blank on the website: allow https://www.openstreetmap.org in CMS_CSP_FRAME_SRC.
  • An older element lacks a new field: keep a default in the schema or Blade view. Existing immutable page versions are not rewritten when the schema changes.