# Builderly API reference

Base URL: http://smartman.max-media.io
Generated from the running server on 2026-11-09T17:13:29.513Z. Every mounted route is listed with the access it needs, the parameters its handler reads and the data models it uses.

## Authentication

Send `Authorization: Bearer <token>` where a route needs a token. Add `?ionic=true` to get JSON from routes that also render pages.

- **Admin token** (`admin`): Bearer token of a site admin (Authorization: Bearer <token>).
- **User token** (`user`): Bearer token of a signed-in user of the site.
- **Optional user** (`optional-user`): Works without a token; a user token personalises the answer.
- **User with access** (`user-priv`): A signed-in user whose plan grants access to the item.
- **Page access** (`page-priv`): Pages limited to some users check the token when one is sent.
- **Webhook signature** (`webhook`): Called by a provider; verified by its signature.
- **reCAPTCHA** (`recaptcha`): Needs a reCAPTCHA token from the site form.
- **System** (`system`): Internal - called by the platform itself.

### For AI assistants: getting a token

Public endpoints (Access: Public) need no token. Everything else - changing the site, or reading its orders, bookings, customers and other private data - needs the site owner's API token.

You cannot create a token yourself, and you must not ask for the owner's password. Ask the user for a token instead - you can send them this message as it is:

> To work with your site through its API, I need an API token from you:
> 1. Sign in to your Builderly admin: https://admin.max-media.io
> 2. Open Developers > API reference (or go straight to https://admin.max-media.io/tabs/site-builder/api-docs).
> 3. Press "Generate token", copy the token and paste it here.
> The token acts as you, so only share it in this chat. You can cancel it at any time by signing out of all devices.

Then send it with every request as `Authorization: Bearer <token>`, and add `?ionic=true` to get JSON. Calls go to the site's own address (its domain), not to the reference's.

The token acts as the user: say what you will change and wait for their yes before any request that creates, changes or deletes something, never repeat the token back, and never put it in a link or a file you share.

## Status codes

| Code | Name | Meaning |
| --- | --- | --- |
| 200 | OK | It worked. The answer is the object or list asked for. |
| 201 | Created | The object was created. Some endpoints send it back; others send an empty body. |
| 202 | Accepted | The work was started and finishes in the background - for example a video being processed ({ "status": "processing" }). Check back on the object. |
| 204 | No Content | It worked, and there is nothing to send back - for example after removing a staff member. |
| 301 | Moved Permanently | The address changed; follow the Location header. |
| 400 | Bad Request | The request was refused as sent: a required field is missing, a value has the wrong type or fails validation. The body says what - often as plain text. |
| 401 | Unauthorized | No token, or one that is invalid or signed out. Without ?ionic=true, some endpoints answer this with the HTML login page instead of JSON. |
| 402 | Payment Required | The content needs a subscription: the user has no price plan that unlocks it. |
| 403 | Forbidden | The token is valid but may not do this - the record belongs to someone else, or the user is not an admin. AI builder endpoints also answer 403 when the account is out of credits. |
| 404 | Not Found | No object with that ID or URL slug, or no such endpoint. |
| 409 | Conflict | It clashes with the current state: the item is already being processed, a version is already applied, or an AI plan or preview is no longer available. |
| 410 | Gone | It existed but is no longer there - for example a comment already deleted on the social network. |
| 412 | Precondition Failed | A rule the request must meet is not met - for example push notifications of the same type at least 48 hours apart. |
| 418 | I'm a teapot | An account confirmation or password reset link was used with an email that does not match it. |
| 421 | Misdirected Request | The domain is not a Builderly site. Call the site's own address. |
| 422 | Unprocessable Content | The request was understood but could not be done - for example the AI could not read the website it was asked to clone. The message says what to try instead. |
| 429 | Too Many Requests | Slow down: the site's request limit was reached, an AI chat already has a request running, or the hourly limit of AI search answers is used up. Wait and retry. |
| 500 | Internal Server Error | Something failed on the server. Retrying later may work; if it keeps failing, the request may be one the endpoint does not handle. |
| 502 | Bad Gateway | A service the request depends on failed - for example the address a redirect forwards to. |
| 503 | Service Unavailable | Used by the lead form when an order is below its minimum value, not only for outages: read the message. |

Errors do not share one shape: depending on the endpoint the body is plain text, { "error": "..." }, { "message": "..." } or empty. Rely on the status code, and show the body to the user as the reason.

## Endpoints

### Design & components

The site's look: the theme (colours, fonts, dark mode), header, footer, popups and reusable custom components built with HTML, CSS and JS.

#### Custom Component

##### List all custom components
`GET /custom-components`
The custom code components. Answers an array of every record.
- Access: Public
- Returns: a list of CustomComponent objects.
- Status codes: 200, 500
- Models: CustomComponent

##### Create a custom component
`POST /custom-components`
Creates a custom code component. Answers 201 with no body.
- Access: Admin token
- Returns: the CustomComponent object.
- Status codes: 201, 400, 401, 500
- Models: CustomComponent

##### Retrieve a custom component
`GET /custom-components/:id`
One custom code component. 404 when there is no such record.
- Access: Public
- Path:
  - `id` (string): The ID of the custom component.
- Returns: the CustomComponent object.
- Status codes: 200, 404, 500
- Models: CustomComponent

##### Update a custom component
`PATCH /custom-components/:id`
Updates a custom code component. Fields outside the allowed list are silently ignored. Answers the updated record, or 404.
- Access: Admin token
- Path:
  - `id` (string): The ID of the custom component.
- Returns: the CustomComponent object.
- Status codes: 200, 400, 401, 404, 500
- Models: CustomComponent

##### Delete a custom component
`DELETE /custom-components/:id`
Deletes a custom code component. Answers the deleted record, or 404. Records that point to it are not changed.
- Access: Admin token
- Path:
  - `id` (string): The ID of the custom component.
- Returns: an empty answer on success.
- Status codes: 200, 401, 404, 500
- Models: CustomComponent

#### Custom Font

##### Retrieve the custom font
`GET /custom-font`
The uploaded fonts. Answers an array of every record.
- Access: Admin token
- Returns: the CustomFont object.
- Status codes: 200, 401, 500
- Models: CustomFont

##### Create a custom font
`POST /custom-font`
Import a Google font. Takes `{ query }` — a family name, a Google. Fonts URL, a <link> tag or a `font-family:` declaration — and          resolves its real weights against Google's font metadata. The older          `{ family, link }` body still works unchanged.
- Access: Admin token
- Body:
  - `query` (string): The custom font's query.
- Returns: the CustomFont object.
- Status codes: 200, 201, 400, 401, 500
- Models: CustomFont

##### Retrieve a custom font
`GET /custom-font/:id`
One uploaded font. 404 when there is no such record.
- Access: Admin token
- Path:
  - `id` (string): The ID of the custom font.
- Returns: the CustomFont object.
- Status codes: 200, 401, 404, 500
- Models: CustomFont

##### Update a custom font
`PATCH /custom-font/:id`
Updates an uploaded font. Fields outside the allowed list are silently ignored. Answers the updated record, or 404.
- Access: Admin token
- Path:
  - `id` (string): The ID of the custom font.
- Returns: the CustomFont object.
- Status codes: 200, 400, 401, 404, 500
- Models: CustomFont

##### Delete a custom font
`DELETE /custom-font/:id`
Deletes an uploaded font. Answers the deleted record, or 404. Records that point to it are not changed.
- Access: Admin token
- Path:
  - `id` (string): The ID of the custom font.
- Returns: an empty answer on success.
- Status codes: 200, 401, 404, 500
- Models: CustomFont

##### Retrieve the font catalogue
`GET /font-catalogue`
Every font the theme picker may offer — the built-in library plus. this tenant's own imports, in one shape.
- Access: Admin token
- Status codes: 200, 401, 500
- Models: CustomFont

##### Search google fonts
`GET /google-fonts/search`
Typeahead over the whole Google Fonts library, for the admin's font. picker. `q` is whatever the user has typed so far.
- Access: Admin token
- Query:
  - `q` (string): The words to search for.
- Status codes: 200, 401, 500

#### Editor

##### Add an add locale
`POST /add-locale`
Adds a word key to the site's locales (locale), for every language. locale is the wording to add; it becomes both the key and the text until translated. Answers 201 when added, 200 when the key already exists. Reads no token.
- Access: Public
- Body:
  - `locale` (string): The add locale's locale.
- Status codes: 200

##### Retrieve the 404
`GET /get-404`
The legacy site file templates/views/404.hbs, as text. Answers the file's text as it is on the server now.
- Access: Admin token
- Status codes: 200, 401

##### Retrieve the app scss
`GET /get-app-scss`
The legacy site file public/css/app-stylesheet.css, as text. Answers the file's text as it is on the server now.
- Access: Public
- Status codes: 200

##### Retrieve the blog
`GET /get-blog`
The legacy site file templates/views/blogpost.hbs, as text. Answers the file's text as it is on the server now.
- Access: Admin token
- Status codes: 200, 401

##### List all blogs
`GET /get-blogs`
The legacy site file templates/views/blogposts.hbs, as text. Answers the file's text as it is on the server now.
- Access: Admin token
- Status codes: 200, 401

##### Retrieve the bs css
`GET /get-bs-css`
The legacy site file public/css/bs-theme.css, as text. Answers the file's text as it is on the server now.
- Access: Admin token
- Status codes: 200, 401

##### Retrieve the cart
`GET /get-cart`
The legacy site file templates/views/my_cart.hbs, as text. Answers the file's text as it is on the server now.
- Access: Admin token
- Status codes: 200, 401

##### List all contacts
`GET /get-contacts`
The legacy site file templates/views/contacts.hbs, as text. Answers the file's text as it is on the server now.
- Access: Admin token
- Status codes: 200, 401

##### Retrieve the css
`GET /get-css`
The legacy site file public/css/stylesheet.css, as text. Answers the file's text as it is on the server now.
- Access: Admin token
- Status codes: 200, 401

##### Retrieve the footer
`GET /get-footer`
The legacy site file templates/partials/footer.hbs, as text. Answers the file's text as it is on the server now.
- Access: Admin token
- Status codes: 200, 401

##### Retrieve the footer css
`GET /get-footer-css`
The legacy site file public/css/footer.css, as text. Answers the file's text as it is on the server now.
- Access: Admin token
- Status codes: 200, 401

##### Retrieve the header
`GET /get-header`
The legacy site file templates/partials/header.hbs, as text. Answers the file's text as it is on the server now.
- Access: Admin token
- Status codes: 200, 401

##### Retrieve the header css
`GET /get-header-css`
The legacy site file public/css/header.css, as text. Answers the file's text as it is on the server now.
- Access: Admin token
- Status codes: 200, 401

##### Retrieve the index
`GET /get-index`
The legacy site file templates/views/index.hbs, as text. Answers the file's text as it is on the server now.
- Access: Admin token
- Status codes: 200, 401

##### List all locales
`GET /get-locales`
The site's words by language, as JSON. Answers the site's words as { <key>: <text shown> }, from the newest locales record.
- Access: Public
- Returns: a list of Locales objects.
- Status codes: 200, 404, 500
- Models: Locales

##### Retrieve the main css
`GET /get-main-css`
The legacy site file public/css/main.css, as text. Answers the file's text as it is on the server now.
- Access: Admin token
- Status codes: 200, 401

##### List all partners
`GET /get-partners`
The legacy site file templates/views/partners.hbs, as text. Answers the file's text as it is on the server now.
- Access: Admin token
- Status codes: 200, 401

##### Retrieve the payment
`GET /get-payment`
The legacy site file templates/views/payment.hbs, as text. Answers the file's text as it is on the server now.
- Access: Admin token
- Status codes: 200, 401

##### Retrieve the product
`GET /get-product`
The legacy site file templates/views/product.hbs, as text. Answers the file's text as it is on the server now.
- Access: Admin token
- Status codes: 200, 401

##### List all products
`GET /get-products`
The legacy site file templates/views/products.hbs, as text. Answers the file's text as it is on the server now.
- Access: Admin token
- Status codes: 200, 401

##### Retrieve the service
`GET /get-service`
The legacy site file templates/views/service.hbs, as text. Answers the file's text as it is on the server now.
- Access: Admin token
- Status codes: 200, 401

##### List all services
`GET /get-services`
The legacy site file templates/views/services.hbs, as text. Answers the file's text as it is on the server now.
- Access: Admin token
- Status codes: 200, 401

##### Retrieve the store
`GET /get-store`
The legacy site file templates/views/store.hbs, as text. Answers the file's text as it is on the server now.
- Access: Admin token
- Status codes: 200, 401

##### Retrieve the theme
`GET /get-theme`
The site's theme file (colours, fonts) from cloud storage. Answers the theme JSON straight from cloud storage, or 404 when the site has none yet.
- Access: Public
- Status codes: 200, 400, 404, 500

##### Retrieve the vlog
`GET /get-vlog`
The legacy site file templates/views/video.hbs, as text. Answers the file's text as it is on the server now.
- Access: Admin token
- Status codes: 200, 401

##### List all vlogs
`GET /get-vlogs`
The legacy site file templates/views/videos.hbs, as text. Answers the file's text as it is on the server now.
- Access: Admin token
- Status codes: 200, 401

##### Set a 404
`POST /set-404`
Overwrites the legacy site file templates/views/404.hbs with content and restarts the site. Send the whole file as content; it replaces the old one and the site restarts to pick it up. A broken template or stylesheet breaks the site's pages until it is fixed.
- Access: Admin token
- Body:
  - `content` (string): The 404's content.
- Status codes: 200, 401

##### Set a blog
`POST /set-blog`
Overwrites the legacy site file templates/views/blogpost.hbs with content and restarts the site. Send the whole file as content; it replaces the old one and the site restarts to pick it up. A broken template or stylesheet breaks the site's pages until it is fixed.
- Access: Admin token
- Body:
  - `content` (string): The blog's content.
- Status codes: 200, 401

##### Set blogs
`POST /set-blogs`
Overwrites the legacy site file templates/views/blogposts.hbs with content and restarts the site. Send the whole file as content; it replaces the old one and the site restarts to pick it up. A broken template or stylesheet breaks the site's pages until it is fixed.
- Access: Admin token
- Body:
  - `content` (string): The blog's content.
- Status codes: 200, 401

##### Set bs css
`POST /set-bs-css`
Overwrites the legacy site file public/css/bs-theme.css with content and restarts the site. Send the whole file as content; it replaces the old one and the site restarts to pick it up. A broken template or stylesheet breaks the site's pages until it is fixed.
- Access: Admin token
- Body:
  - `content` (string): The bs css's content.
- Status codes: 200, 401

##### Set a cart
`POST /set-cart`
Overwrites the legacy site file templates/views/my_cart.hbs with content and restarts the site. Send the whole file as content; it replaces the old one and the site restarts to pick it up. A broken template or stylesheet breaks the site's pages until it is fixed.
- Access: Admin token
- Body:
  - `content` (string): The cart's content.
- Status codes: 200, 401

##### Set contacts
`POST /set-contacts`
Overwrites the legacy site file templates/views/contacts.hbs with content and restarts the site. Send the whole file as content; it replaces the old one and the site restarts to pick it up. A broken template or stylesheet breaks the site's pages until it is fixed.
- Access: Admin token
- Body:
  - `content` (string): The contact's content.
- Status codes: 200, 401

##### Set a css
`POST /set-css`
Overwrites the legacy site file public/css/stylesheet.css with content and restarts the site. Send the whole file as content; it replaces the old one and the site restarts to pick it up. A broken template or stylesheet breaks the site's pages until it is fixed.
- Access: Admin token
- Body:
  - `content` (string): The css's content.
- Status codes: 200, 401

##### Set a footer
`POST /set-footer`
Overwrites the legacy site file templates/partials/footer.hbs with content and restarts the site. Send the whole file as content; it replaces the old one and the site restarts to pick it up. A broken template or stylesheet breaks the site's pages until it is fixed.
- Access: Admin token
- Body:
  - `content` (string): The footer's content.
- Status codes: 200, 401

##### Set a footer css
`POST /set-footer-css`
Overwrites the legacy site file public/css/footer.css with content and restarts the site. Send the whole file as content; it replaces the old one and the site restarts to pick it up. A broken template or stylesheet breaks the site's pages until it is fixed.
- Access: Admin token
- Body:
  - `content` (string): The footer css's content.
- Status codes: 200, 401

##### Set a header
`POST /set-header`
Overwrites the legacy site file templates/partials/header.hbs with content and restarts the site. Send the whole file as content; it replaces the old one and the site restarts to pick it up. A broken template or stylesheet breaks the site's pages until it is fixed.
- Access: Admin token
- Body:
  - `content` (string): The header's content.
- Status codes: 200, 401

##### Set a header css
`POST /set-header-css`
Overwrites the legacy site file public/css/header.css with content and restarts the site. Send the whole file as content; it replaces the old one and the site restarts to pick it up. A broken template or stylesheet breaks the site's pages until it is fixed.
- Access: Admin token
- Body:
  - `content` (string): The header css's content.
- Status codes: 200, 401

##### Set an index
`POST /set-index`
Overwrites the legacy site file templates/views/index.hbs with content and restarts the site. Send the whole file as content; it replaces the old one and the site restarts to pick it up. A broken template or stylesheet breaks the site's pages until it is fixed.
- Access: Admin token
- Body:
  - `content` (string): The index's content.
- Status codes: 200, 401

##### Set locales
`POST /set-locales`
Replaces the site's words by language (content). Send the whole map as content (JSON text): keys left out are removed. Older locales records are deleted and the app re-reads its startup data.
- Access: Admin token
- Body:
  - `content` (string): The locale's content.
- Status codes: 200, 401

##### Set a main css
`POST /set-main-css`
Overwrites the legacy site file public/css/main.css with content and restarts the site. Send the whole file as content; it replaces the old one and the site restarts to pick it up. A broken template or stylesheet breaks the site's pages until it is fixed.
- Access: Admin token
- Body:
  - `content` (string): The main css's content.
- Status codes: 200, 401

##### Set partners
`POST /set-partners`
Overwrites the legacy site file templates/views/partners.hbs with content and restarts the site. Send the whole file as content; it replaces the old one and the site restarts to pick it up. A broken template or stylesheet breaks the site's pages until it is fixed.
- Access: Admin token
- Body:
  - `content` (string): The partner's content.
- Status codes: 200, 401

##### Set a payment
`POST /set-payment`
Overwrites the legacy site file templates/views/payment.hbs with content and restarts the site. Send the whole file as content; it replaces the old one and the site restarts to pick it up. A broken template or stylesheet breaks the site's pages until it is fixed.
- Access: Admin token
- Body:
  - `content` (string): The payment's content.
- Status codes: 200, 401

##### Set a product
`POST /set-product`
Overwrites the legacy site file templates/views/product.hbs with content and restarts the site. Send the whole file as content; it replaces the old one and the site restarts to pick it up. A broken template or stylesheet breaks the site's pages until it is fixed.
- Access: Admin token
- Body:
  - `content` (string): The product's content.
- Status codes: 200, 401

##### Set products
`POST /set-products`
Overwrites the legacy site file templates/views/products.hbs with content and restarts the site. Send the whole file as content; it replaces the old one and the site restarts to pick it up. A broken template or stylesheet breaks the site's pages until it is fixed.
- Access: Admin token
- Body:
  - `content` (string): The product's content.
- Status codes: 200, 401

##### Set a service
`POST /set-service`
Overwrites the legacy site file templates/views/service.hbs with content and restarts the site. Send the whole file as content; it replaces the old one and the site restarts to pick it up. A broken template or stylesheet breaks the site's pages until it is fixed.
- Access: Admin token
- Body:
  - `content` (string): The service's content.
- Status codes: 200, 401

##### Set services
`POST /set-services`
Overwrites the legacy site file templates/views/services.hbs with content and restarts the site. Send the whole file as content; it replaces the old one and the site restarts to pick it up. A broken template or stylesheet breaks the site's pages until it is fixed.
- Access: Admin token
- Body:
  - `content` (string): The service's content.
- Status codes: 200, 401

##### Set a store
`POST /set-store`
Overwrites the legacy site file templates/views/store.hbs with content and restarts the site. Send the whole file as content; it replaces the old one and the site restarts to pick it up. A broken template or stylesheet breaks the site's pages until it is fixed.
- Access: Admin token
- Body:
  - `content` (string): The store's content.
- Status codes: 200, 401

##### Set a theme
`POST /set-theme`
Saves the site's theme file (content), marks the theme step done and re-renders every page. Send the whole theme as content. Every page is rendered again with it on its next visit.
- Access: Admin token
- Body:
  - `content` (string): The theme's content.
- Status codes: 200, 401

##### Set a vlog
`POST /set-vlog`
Overwrites the legacy site file templates/views/video.hbs with content and restarts the site. Send the whole file as content; it replaces the old one and the site restarts to pick it up. A broken template or stylesheet breaks the site's pages until it is fixed.
- Access: Admin token
- Body:
  - `content` (string): The vlog's content.
- Status codes: 200, 401

##### Set vlogs
`POST /set-vlogs`
Overwrites the legacy site file templates/views/videos.hbs with content and restarts the site. Send the whole file as content; it replaces the old one and the site restarts to pick it up. A broken template or stylesheet breaks the site's pages until it is fixed.
- Access: Admin token
- Body:
  - `content` (string): The vlog's content.
- Status codes: 200, 401

#### Index Component

##### List all index components
`GET /index-components`
The home page's blocks. Answers an array of every record.
- Access: Admin token
- Returns: a list of IndexComponent objects.
- Status codes: 200, 401, 500
- Models: IndexComponent

##### Create an index component
`POST /index-components`
Replaces every home page block with the list sent, in order. The body is the WHOLE list (an array of blocks): every block not in it is removed. To add one block and keep the rest, use POST /index-components/append. The list is checked before anything is removed - an empty or invalid one answers 400 and changes nothing. A page block names its page by url in about_page, not by id. The home page is rendered again on its next visit. Answers 201.
- Access: Admin token
- Body:
  - `map` (string): The index component's map.
- Returns: the IndexComponent object.
- Status codes: 201, 400, 401, 500
- Models: IndexComponent

##### Retrieve an index component
`GET /index-components/:id`
One home page block. 404 when there is no such record.
- Access: Admin token
- Path:
  - `id` (string): The ID of the index component.
- Returns: the IndexComponent object.
- Status codes: 200, 401, 404, 500
- Models: IndexComponent

##### Update an index component
`PATCH /index-components/:id`
Updates a home page block. The home page is rendered again on its next visit. Answers the block, or 404.
- Access: Admin token
- Path:
  - `id` (string): The ID of the index component.
- Returns: the IndexComponent object.
- Status codes: 200, 400, 401, 404, 500
- Models: IndexComponent

##### Delete an index component
`DELETE /index-components/:id`
Deletes a home page block. Answers the deleted record, or 404. Records that point to it are not changed.
- Access: Admin token
- Path:
  - `id` (string): The ID of the index component.
- Returns: an empty answer on success.
- Status codes: 200, 401, 404, 500
- Models: IndexComponent

##### Create an index component
`POST /index-components/append`
Adds one block to the home page, keeping the others - e.g. a page shown on the home page. For a page: { about_page: its id or its url } (type defaults to "about_page"). Other types show a list instead (products_latest, services, blogposts...). position puts it there and moves the blocks from there on down; without it, it goes last. isAppComponent: true adds it to the mobile app's home instead of the website's. 404 when the page does not exist. Answers 201 with the new block.
- Access: Admin token
- Body:
  - `type` (string): What the block shows: about_page (a page), or a list - products, products_latest, products_top, services, services_all, services_latest, services_top, blogposts, blogposts_latest, blogposts_top, videos, videos_latest, price_plans.
  - `isFluid` (boolean): Spans the full width of the screen.
  - `isAppComponent` (boolean): Shown in the mobile app's home instead of the website's.
  - `about_page` (string): For an about_page block: the URL of the page it shows (AboutPage.url, e.g. "our-story"), not its id. POST /index-components/append takes either and stores the url.
  - `position` (number): Order on the home page; lower first.
- Returns: the IndexComponent object.
- Status codes: 201, 400, 401, 404, 500
- Models: AboutPage, IndexComponent

#### Popup

##### Retrieve the popup
`GET /popup`
The site's popups. Answers an array.
- Access: Admin token
- Returns: the Popup object.
- Status codes: 200, 401, 500
- Models: Popup

##### Create a popup
`POST /popup`
Creates a popup. Answers 201 with no body.
- Access: Admin token
- Returns: the Popup object.
- Status codes: 201, 400, 401, 500
- Models: Popup

##### Retrieve a popup
`GET /popup/:id`
One popup. 404 when there is no such record.
- Access: Admin token
- Path:
  - `id` (string): The ID of the popup.
- Returns: the Popup object.
- Status codes: 200, 401, 404, 500
- Models: Popup

#### Wildcard

##### Retrieve a g
`GET /g/:url(*)`
Serves what a wildcard route points to (a product, service, post or program) at /g/<url>. 404 when no wildcard route has that url; 502 when the page it points to cannot be loaded. Cached for an hour.
- Access: Public
- Path:
  - `url` (string): The g's URL slug, as in its address on the site.
- Status codes: 200, 404, 500, 502
- Models: Wildcard

##### List all wildcards
`GET /wildcards`
The wildcard routes. Answers an array of every record.
- Access: Admin token
- Query:
  - `authenticated` (string): Only wildcards with this authenticated.
- Returns: a list of Wildcard objects.
- Status codes: 200, 401, 500
- Models: Wildcard

##### Create a wildcard
`POST /wildcards`
Creates a wildcard route. Answers 201 { id }.
- Access: Admin token
- Returns: the Wildcard object.
- Status codes: 201, 400, 401, 500
- Models: Wildcard

##### Retrieve a wildcard
`GET /wildcards/:id`
One wildcard route. 404 when there is no such record.
- Access: Admin token
- Path:
  - `id` (string): The ID of the wildcard.
- Returns: the Wildcard object.
- Status codes: 200, 401, 404, 500
- Models: Wildcard

##### Update a wildcard
`PATCH /wildcards/:id`
Updates a wildcard route. Fields outside the allowed list are silently ignored. Answers the updated record, or 404.
- Access: Admin token
- Path:
  - `id` (string): The ID of the wildcard.
- Returns: the Wildcard object.
- Status codes: 200, 400, 401, 404, 500
- Models: Wildcard

##### Delete a wildcard
`DELETE /wildcards/:id`
Deletes a wildcard route. Answers the deleted record, or 404. Records that point to it are not changed.
- Access: Admin token
- Path:
  - `id` (string): The ID of the wildcard.
- Returns: an empty answer on success.
- Status codes: 200, 401, 404, 500
- Models: Wildcard

#### AI builder

##### Create a popup with AI
`POST /create-new-popup`
Create a popup from a prompt, written by the AI builder.
- Access: Admin token
- Body:
  - `prompt` (string): What the AI should do with the popup, in plain words - any language.
  - `chatId` (string): The AI chat this request belongs to; its history and language carry over. Leave it out to start a new chat.
  - `aiVersion` (string): Which version of the AI builder to use; leave it out for the current one.
  - `item_id` (string): The ID of the popup to change.
- Status codes: 200, 401, 500
- Models: ChatMessage

##### Generate a popup with AI
`POST /generate-popup`
Use it to create or set up a site popup or modal.
- Access: Admin token
- Body:
  - `prompt` (string): What the AI should do with the popup, in plain words - any language.
  - `chatId` (string): The AI chat this request belongs to; its history and language carry over. Leave it out to start a new chat.
- Status codes: 200, 401, 500

## Objects

### AboutPage

A page built in the editor or by the AI builder. Its content is in containers: each container is a full-width section with columns, and each column has components, laid out on a 12-column grid by their size.

- `requiresLogin`: Boolean, default false - Only signed-in users can see the page.
- `requiresSubscription`: Boolean, default false - Only subscribers of one of price_plan_ids can see the page.
- `price_plan_ids`: String[] - The price plans whose subscribers can see the page, with requiresSubscription.
- `title`: String, required - The page's title, shown in the browser tab and in the admin.
- `url`: String, required - The page's address on the site: /<url>. Unique.
- `redirectUrl`: String - When set, visitors who open the page are sent to this address instead.
- `legal_kind`: String - For legal pages: which document it is, in English (Privacy Policy, Terms of Service...).
- `isPartial`: Boolean, default false - A part of other pages (a banner, a block inside a product page), not a page of its own.
- `showTo`: String, default "0" - Who sees the page: "0" everyone, "1" signed-in users only, "2" visitors who are not signed in.
- `showHeader`: Boolean, default true - Show the site's header on the page.
- `showFooter`: Boolean, default true - Show the site's footer on the page.
- `ai_context_v2`: Mixed - Legacy, no longer written: the AI context is built on demand. Old copies are not selected and are removed by npm run migrate:strip-about-page-ai-context.
- `ai_surfaces_v2`: Mixed - The editable regions the AI page modifier found on the page last time it read it; written by the AI builder.
- `ai_overview`: String - A short summary of the page the AI builder keeps for its own use.
- `ai_generation_metadata`: Mixed - How the AI builder generated the page (mode, prompt, timings); written by the AI builder.
- `containers`: Object[] - The page's content: sections, each with columns of components.
  - `isFluid`: Boolean, default false
  - `parallaxEnabled`: Boolean
  - `parallaxAmount`: Number
  - `parallaxZoom`: Number
  - `continuousEnabled`: Boolean, default false
  - `continuousEffect`: String, default "none"
  - `continuousDuration`: Number, default 2400
  - `continuousIntensity`: Number, default 4
  - `backgroundImage`: String
  - `backgroundSize`: String
  - `backgroundPosition`: String
  - `backgroundRepeat`: String
  - `backgroundVideo`: String
  - `backgroundColor`: String
  - `backgroundGradient.enabled`: Boolean, default false
  - `backgroundGradient.type`: String
  - `backgroundGradient.degrees`: Number
  - `backgroundGradient.radialShape`: String
  - `backgroundGradient.radialPosition`: String
  - `backgroundGradient.colors`: Object[]
    - `color`: String
    - `percentage`: Number
    - `_id`: ObjectId
  - `backgroundGradient.css`: String
  - `backgroundParallax`: Boolean, default false
  - `minHeight`: Number
  - `minHeightMobile`: Number
  - `padding_top.value`: String, default ""
  - `padding_top.unit`: String, default "px"
  - `padding_top.mobile_value`: String, default ""
  - `padding_top.mobile_unit`: String, default "px"
  - `padding_right.value`: String, default ""
  - `padding_right.unit`: String, default "px"
  - `padding_right.mobile_value`: String, default ""
  - `padding_right.mobile_unit`: String, default "px"
  - `padding_bottom.value`: String, default ""
  - `padding_bottom.unit`: String, default "px"
  - `padding_bottom.mobile_value`: String, default ""
  - `padding_bottom.mobile_unit`: String, default "px"
  - `padding_left.value`: String, default ""
  - `padding_left.unit`: String, default "px"
  - `padding_left.mobile_value`: String, default ""
  - `padding_left.mobile_unit`: String, default "px"
  - `allowOffset`: Boolean, default false
  - `offsetX`: Number, default 0
  - `offsetY`: Number, default 0
  - `offsetMobileX`: Number, default 0
  - `offsetMobileY`: Number, default 0
  - `zIndex`: Number, default 0
  - `wrap_reverse`: Boolean, default false
  - `borderRadius`: Number, default 0
  - `marginX`: Number, default 0
  - `editorName`: String
  - `horizontalScroll`: Boolean, default false
  - `horizontalScrollWidth`: Number, default 100
  - `overflowX`: String, default "hidden"
  - `columns`: Object[]
    - `size`: String
    - `hoverGroup`: Boolean
    - `parallaxEnabled`: Boolean
    - `parallaxAmount`: Number
    - `parallaxZoom`: Number
    - `continuousEnabled`: Boolean, default false
    - `continuousEffect`: String, default "none"
    - `continuousDuration`: Number, default 2400
    - `continuousIntensity`: Number, default 4
    - `sticky`: Boolean, default false
    - `justify`: String
    - `align`: String
    - `onHoverActions`: Boolean, default false
    - `onHoverTransition`: Number, default 0.3
    - `backgroundColor`: String
    - `backgroundGradient.enabled`: Boolean, default false
    - `backgroundGradient.type`: String
    - `backgroundGradient.degrees`: Number
    - `backgroundGradient.radialShape`: String
    - `backgroundGradient.radialPosition`: String
    - `backgroundGradient.colors`: Object[]
      - `color`: String
      - `percentage`: Number
      - `_id`: ObjectId
    - `backgroundGradient.css`: String
    - `backgroundColorOnHover`: String
    - `innerBackgroundColor`: String
    - `innerBackgroundGradient.enabled`: Boolean, default false
    - `innerBackgroundGradient.type`: String
    - `innerBackgroundGradient.degrees`: Number
    - `innerBackgroundGradient.radialShape`: String
    - `innerBackgroundGradient.radialPosition`: String
    - `innerBackgroundGradient.colors`: Object[]
      - `color`: String
      - `percentage`: Number
      - `_id`: ObjectId
    - `innerBackgroundGradient.css`: String
    - `innerBackgroundColorOnHover`: String
    - `backgroundImage`: String
    - `backgroundSize`: String
    - `backgroundPosition`: String
    - `backgroundRepeat`: String
    - `backgroundVideo`: String
    - `paddingX`: Number, default 0
    - `paddingXOnHover`: Number, default 0
    - `paddingY`: Number, default 0
    - `paddingYOnHover`: Number, default 0
    - `innerPaddingX`: Number, default 0
    - `innerPaddingXOnHover`: Number, default 0
    - `innerPaddingY`: Number, default 0
    - `allowOffset`: Boolean, default false
    - `offsetX`: Number, default 0
    - `offsetY`: Number, default 0
    - `offsetMobileX`: Number, default 0
    - `offsetMobileY`: Number, default 0
    - `zIndex`: Number, default 0
    - `innerPaddingYOnHover`: Number, default 0
    - `paddingOptions`: String
    - `paddingOptionsOnHover`: String
    - `innerPaddingOptions`: String
    - `borderOptions`: String
    - `borderOptionsOnHover`: String
    - `card`: Boolean, default false
    - `borderRadius`: Number, default 0
    - `borderRadiusOnHover`: Number, default 0
    - `borderType`: String, default "none"
    - `borderTypeOnHover`: String, default "none"
    - `borderWidth`: Number, default 0
    - `borderWidthOnHover`: Number, default 0
    - `borderColor`: String, default ""
    - `borderColorOnHover`: String, default ""
    - `components`: Object[]
      - `type`: String
      - `size`: String, default "12"
      - `sticky`: Boolean, default false
      - `paddingX`: Number, default 0
      - `paddingY`: Number, default 0
      - `allowOffset`: Boolean, default false
      - `offsetX`: Number, default 0
      - `offsetY`: Number, default 0
      - `offsetMobileX`: Number, default 0
      - `offsetMobileY`: Number, default 0
      - `zIndex`: Number, default 0
      - `position`: String, one of static | relative | absolute | fixed | sticky, default "static"
      - `top.value`: String, default "0"
      - `top.unit`: String, default "px"
      - `top.mobile_value`: String, default ""
      - `top.mobile_unit`: String, default "px"
      - `left.value`: String, default "0"
      - `left.unit`: String, default "px"
      - `left.mobile_value`: String, default ""
      - `left.mobile_unit`: String, default "px"
      - `right.value`: String, default "0"
      - `right.unit`: String, default "px"
      - `right.mobile_value`: String, default ""
      - `right.mobile_unit`: String, default "px"
      - `bottom.value`: String, default "0"
      - `bottom.unit`: String, default "px"
      - `bottom.mobile_value`: String, default ""
      - `bottom.mobile_unit`: String, default "px"
      - `custom_measurements`: Boolean, default false
      - `edit_type`: String, default "simple"
      - `custom_width.value`: Mixed
      - `custom_width.unit`: String, default "px"
      - `custom_width.mobile_value`: Mixed
      - `custom_width.mobile_unit`: String, default "px"
      - `custom_min_width.value`: Mixed
      - `custom_min_width.unit`: String, default "px"
      - `custom_min_width.mobile_value`: Mixed
      - `custom_min_width.mobile_unit`: String, default "px"
      - `custom_max_width.value`: Mixed
      - `custom_max_width.unit`: String, default "px"
      - `custom_max_width.mobile_value`: Mixed
      - `custom_max_width.mobile_unit`: String, default "px"
      - `custom_height.value`: Mixed
      - `custom_height.unit`: String, default "px"
      - `custom_height.mobile_value`: Mixed
      - `custom_height.mobile_unit`: String, default "px"
      - `custom_min_height.value`: Mixed
      - `custom_min_height.unit`: String, default "px"
      - `custom_min_height.mobile_value`: Mixed
      - `custom_min_height.mobile_unit`: String, default "px"
      - `custom_max_height.value`: Mixed
      - `custom_max_height.unit`: String, default "px"
      - `custom_max_height.mobile_value`: Mixed
      - `custom_max_height.mobile_unit`: String, default "px"
      - `object_fit`: String, default "none"
      - `aspect_ratio.custom`: Boolean, default false
      - `aspect_ratio.ratio`: String, default ""
      - `min_height`: Number, default 0
      - `margin_left.value`: Mixed
      - `margin_left.unit`: String, default "px"
      - `margin_left.mobile_value`: Mixed
      - `margin_left.mobile_unit`: String, default "px"
      - `margin_right.value`: Mixed
      - `margin_right.unit`: String, default "px"
      - `margin_right.mobile_value`: Mixed
      - `margin_right.mobile_unit`: String, default "px"
      - `margin_top.value`: Mixed
      - `margin_top.unit`: String, default "px"
      - `margin_top.mobile_value`: Mixed
      - `margin_top.mobile_unit`: String, default "px"
      - `margin_bottom.value`: Mixed
      - `margin_bottom.unit`: String, default "px"
      - `margin_bottom.mobile_value`: Mixed
      - `margin_bottom.mobile_unit`: String, default "px"
      - `padding_left.value`: Mixed
      - `padding_left.unit`: String, default "px"
      - `padding_left.mobile_value`: Mixed
      - `padding_left.mobile_unit`: String, default "px"
      - `padding_right.value`: Mixed
      - `padding_right.unit`: String, default "px"
      - `padding_right.mobile_value`: Mixed
      - `padding_right.mobile_unit`: String, default "px"
      - `padding_top.value`: Mixed
      - `padding_top.unit`: String, default "px"
      - `padding_top.mobile_value`: Mixed
      - `padding_top.mobile_unit`: String, default "px"
      - `padding_bottom.value`: Mixed
      - `padding_bottom.unit`: String, default "px"
      - `padding_bottom.mobile_value`: Mixed
      - `padding_bottom.mobile_unit`: String, default "px"
      - `border_left.color`: String
      - `border_left.width.value`: Mixed
      - `border_left.width.unit`: String, default "px"
      - `border_left.style`: String
      - `border_right.color`: String
      - `border_right.width.value`: Mixed
      - `border_right.width.unit`: String, default "px"
      - `border_right.style`: String
      - `border_top.color`: String
      - `border_top.width.value`: Mixed
      - `border_top.width.unit`: String, default "px"
      - `border_top.style`: String
      - `border_bottom.color`: String
      - `border_bottom.width.value`: Mixed
      - `border_bottom.width.unit`: String, default "px"
      - `border_bottom.style`: String
      - `border_top_left_radius.value`: Mixed
      - `border_top_left_radius.unit`: String, default "px"
      - `border_top_right_radius.value`: Mixed
      - `border_top_right_radius.unit`: String, default "px"
      - `border_bottom_left_radius.value`: Mixed
      - `border_bottom_left_radius.unit`: String, default "px"
      - `border_bottom_right_radius.value`: Mixed
      - `border_bottom_right_radius.unit`: String, default "px"
      - `custom_border_radius.value`: Number
      - `custom_border_radius.unit`: String, default "px"
      - `specific_border_radius`: Boolean, default false
      - `box_shadows`: Object[]
      - `mix_blend_mode`: String, one of normal | multiply | screen | overlay | darken | lighten | color-dodge | color-burn | hard-light | soft-light | difference | exclusion | hue | saturation | color | luminosity, default "normal"
      - `paddingOptions`: String
      - `backgroundColor`: String
      - `backgroundGradient.enabled`: Boolean, default false
      - `backgroundGradient.type`: String
      - `backgroundGradient.degrees`: Number
      - `backgroundGradient.radialShape`: String
      - `backgroundGradient.radialPosition`: String
      - `backgroundGradient.colors`: Object[]
      - `backgroundGradient.css`: String
      - `card`: Boolean, default false
      - `borderRadius`: Number, default 0
      - `borderType`: String, default "none"
      - `borderWidth`: Number, default 0
      - `borderColor`: String, default ""
      - `borderOptions`: String
      - `showOnMobile`: Boolean, default true
      - `showOnDesktop`: Boolean, default true
      - `icon`: String
      - `iconSettings.size`: Number, default 40
      - `iconSettings.color`: String, default "var(--ion-color-primary)"
      - `heading`: String
      - `subHeading`: String
      - `heading_alignment`: String
      - `innerHTML`: String
      - `innerCSS`: String
      - `innerJS`: String
      - `lineHeight`: String, default "normal"
      - `medias`: Object[]
      - `products`: ObjectId[], → Product
      - `product_gallery.design_layout`: String, default ""
      - `product_gallery.mode`: String, default "card"
      - `product_gallery.columns`: Number, default 3
      - `product_gallery.placeholderItems`: Object[]
      - `product_gallery.placeholderPlateColor`: String, default ""
      - `product_gallery.showName`: Boolean, default true
      - `product_gallery.showPrice`: Boolean, default true
      - `product_gallery.showDescription`: Boolean, default false
      - `product_gallery.showBrand`: Boolean, default false
      - `product_gallery.showRibbon`: Boolean, default false
      - `product_gallery.showRating`: Boolean, default false
      - `product_gallery.showCart`: Boolean, default false
      - `product_gallery.showLink`: Boolean, default false
      - `product_gallery.linkLabel`: String, default "View product"
      - `product_gallery.imageAspectRatio`: String, default "1/1"
      - `product_gallery.imageHeight`: Number, default 260
      - `product_gallery.imageRadius`: Number, default 20
      - `product_gallery.imageObjectFit`: String, default "cover"
      - `product_gallery.cardBackgroundColor`: String, default "var(--ion-card-background, #ffffff)"
      - `product_gallery.cardBorderRadius`: Number, default 28
      - `product_gallery.cardBorderWidth`: Number, default 0
      - `product_gallery.cardBorderColor`: String, default "transparent"
      - `product_gallery.cardPaddingX`: Number, default 20
      - `product_gallery.cardPaddingY`: Number, default 20
      - `product_gallery.gap`: Number, default 24
      - `product_gallery.boxShadowColor`: String, default "rgba(var(--ion-color-primary-rgb, 56,128,255), 0.22)"
      - `product_gallery.boxShadowOffsetY`: Number, default 20
      - `product_gallery.boxShadowBlurRadius`: Number, default 40
      - `product_gallery.boxShadowSpreadRadius`: Number, default -22
      - `product_gallery.fieldBackground`: String, default "transparent"
      - `product_gallery.hairlineColor`: String, default "transparent"
      - `product_gallery.hairlineWidth`: Number, default 0
      - `product_gallery.maxWidth`: Number, default 0
      - `product_gallery.scrimGradient`: String, default ""
      - `product_gallery.align`: String, default "left"
      - `product_gallery.accentColor`: String, default "var(--ion-color-primary, #3880ff)"
      - `product_gallery.nameColor`: String, default "var(--ion-text-color, #1e293b)"
      - `product_gallery.nameFontSize`: String, default "1em"
      - `product_gallery.nameFontWeight`: String, default "700"
      - `product_gallery.nameFontFamily`: String, default ""
      - `product_gallery.nameTransform`: String, default "none"
      - `product_gallery.priceColor`: String, default "var(--ion-color-primary, #3880ff)"
      - `product_gallery.priceFontSize`: String, default "0.94em"
      - `product_gallery.priceFontWeight`: String, default "600"
      - `product_gallery.priceFontFamily`: String, default ""
      - `product_gallery.priceFontStyle`: String, default "normal"
      - `product_gallery.metaColor`: String, default "rgba(var(--ion-text-color-rgb, 30,41,59), 0.66)"
      - `product_gallery.titlePosition`: String, default "bottom"
      - `product_gallery.imageSide`: String, default "left"
      - `product_gallery.imagePadding`: Number, default 0
      - `product_gallery.boxShadowOffsetX`: Number, default 0
      - `product_gallery.descFontSize`: String, default "13px"
      - `product_gallery.descFontWeight`: String, default "400"
      - `product_gallery.animation`: String, default "none"
      - `product_gallery.animationDuration`: Number, default 0
      - `product_gallery.animationDelay`: Number, default 0
      - `product_gallery.showCustomFields`: Boolean, default false
      - `product_gallery.cardDesign`: String, default ""
      - `product_gallery.carouselLoop`: Boolean, default false
      - `product_gallery.carouselControls`: Boolean, default false
      - `product_gallery.customCss`: String, default ""
      - `services`: ObjectId[], → Service
      - `service_gallery.design_layout`: String, default ""
      - `service_gallery.mode`: String, default "card"
      - `service_gallery.columns`: Number, default 3
      - `service_gallery.placeholderItems`: Object[]
      - `service_gallery.placeholderPlateColor`: String, default ""
      - `service_gallery.showTitle`: Boolean, default true
      - `service_gallery.showPrice`: Boolean, default true
      - `service_gallery.showDescription`: Boolean, default false
      - `service_gallery.showDuration`: Boolean, default false
      - `service_gallery.durationLabel`: String, default "min"
      - `service_gallery.showImage`: Boolean, default true
      - `service_gallery.showLink`: Boolean, default false
      - `service_gallery.linkLabel`: String, default "View service"
      - `service_gallery.imageAspectRatio`: String, default "1/1"
      - `service_gallery.imageHeight`: Number, default 260
      - `service_gallery.imageRadius`: Number, default 20
      - `service_gallery.imageObjectFit`: String, default "cover"
      - `service_gallery.cardBackgroundColor`: String, default "var(--ion-card-background, #ffffff)"
      - `service_gallery.cardBorderRadius`: Number, default 28
      - `service_gallery.cardBorderWidth`: Number, default 0
      - `service_gallery.cardBorderColor`: String, default "transparent"
      - `service_gallery.cardPaddingX`: Number, default 20
      - `service_gallery.cardPaddingY`: Number, default 20
      - `service_gallery.gap`: Number, default 24
      - `service_gallery.boxShadowColor`: String, default "rgba(var(--ion-color-primary-rgb, 56,128,255), 0.22)"
      - `service_gallery.boxShadowOffsetY`: Number, default 20
      - `service_gallery.boxShadowBlurRadius`: Number, default 40
      - `service_gallery.boxShadowSpreadRadius`: Number, default -22
      - `service_gallery.fieldBackground`: String, default "transparent"
      - `service_gallery.hairlineColor`: String, default "transparent"
      - `service_gallery.hairlineWidth`: Number, default 0
      - `service_gallery.rowRuleColor`: String, default "transparent"
      - `service_gallery.rowRuleWidth`: Number, default 0
      - `service_gallery.maxWidth`: Number, default 0
      - `service_gallery.scrimGradient`: String, default ""
      - `service_gallery.align`: String, default "left"
      - `service_gallery.accentColor`: String, default "var(--ion-color-primary, #3880ff)"
      - `service_gallery.titleColor`: String, default "var(--ion-text-color, #1e293b)"
      - `service_gallery.titleFontSize`: String, default "1em"
      - `service_gallery.titleFontWeight`: String, default "700"
      - `service_gallery.titleFontFamily`: String, default ""
      - `service_gallery.titleTransform`: String, default "none"
      - `service_gallery.priceColor`: String, default "var(--ion-color-primary, #3880ff)"
      - `service_gallery.priceFontSize`: String, default "0.94em"
      - `service_gallery.priceFontWeight`: String, default "600"
      - `service_gallery.priceFontFamily`: String, default ""
      - `service_gallery.priceFontStyle`: String, default "normal"
      - `service_gallery.metaColor`: String, default "rgba(var(--ion-text-color-rgb, 30,41,59), 0.66)"
      - `service_gallery.titlePosition`: String, default "bottom"
      - `service_gallery.imageSide`: String, default "left"
      - `service_gallery.imagePadding`: Number, default 0
      - `service_gallery.boxShadowOffsetX`: Number, default 0
      - `service_gallery.descFontSize`: String, default "13px"
      - `service_gallery.descFontWeight`: String, default "400"
      - `service_gallery.animation`: String, default "none"
      - `service_gallery.animationDuration`: Number, default 0
      - `service_gallery.animationDelay`: Number, default 0
      - `service_gallery.showCustomFields`: Boolean, default false
      - `service_gallery.cardDesign`: String, default ""
      - `service_gallery.carouselLoop`: Boolean, default false
      - `service_gallery.carouselControls`: Boolean, default false
      - `service_gallery.customCss`: String, default ""
      - `blog_gallery.design_layout`: String, default ""
      - `blog_gallery.mode`: String, default "card"
      - `blog_gallery.columns`: Number, default 3
      - `blog_gallery.leadLayout`: String, default "beside"
      - `blog_gallery.leadWidth`: Number, default 58
      - `blog_gallery.placeholderItems`: Object[]
      - `blog_gallery.placeholderPlateColor`: String, default ""
      - `blog_gallery.showTitle`: Boolean, default true
      - `blog_gallery.showExcerpt`: Boolean, default false
      - `blog_gallery.showDate`: Boolean, default true
      - `blog_gallery.showImage`: Boolean, default true
      - `blog_gallery.showLink`: Boolean, default false
      - `blog_gallery.linkLabel`: String, default "Read more"
      - `blog_gallery.showCustomFields`: Boolean, default false
      - `blog_gallery.imageAspectRatio`: String, default "1/1"
      - `blog_gallery.imageHeight`: Number, default 260
      - `blog_gallery.imageRadius`: Number, default 20
      - `blog_gallery.imageObjectFit`: String, default "cover"
      - `blog_gallery.imagePadding`: Number, default 0
      - `blog_gallery.imageSide`: String, default "left"
      - `blog_gallery.cardBackgroundColor`: String, default "var(--ion-card-background, #ffffff)"
      - `blog_gallery.cardBorderRadius`: Number, default 28
      - `blog_gallery.cardBorderWidth`: Number, default 0
      - `blog_gallery.cardBorderColor`: String, default "transparent"
      - `blog_gallery.cardPaddingX`: Number, default 20
      - `blog_gallery.cardPaddingY`: Number, default 20
      - `blog_gallery.gap`: Number, default 24
      - `blog_gallery.boxShadowColor`: String, default "rgba(var(--ion-color-primary-rgb, 56,128,255), 0.22)"
      - `blog_gallery.boxShadowOffsetX`: Number, default 0
      - `blog_gallery.boxShadowOffsetY`: Number, default 20
      - `blog_gallery.boxShadowBlurRadius`: Number, default 40
      - `blog_gallery.boxShadowSpreadRadius`: Number, default -22
      - `blog_gallery.fieldBackground`: String, default "transparent"
      - `blog_gallery.hairlineColor`: String, default "transparent"
      - `blog_gallery.hairlineWidth`: Number, default 0
      - `blog_gallery.rowRuleColor`: String, default "transparent"
      - `blog_gallery.rowRuleWidth`: Number, default 0
      - `blog_gallery.maxWidth`: Number, default 0
      - `blog_gallery.scrimGradient`: String, default ""
      - `blog_gallery.align`: String, default "left"
      - `blog_gallery.accentColor`: String, default "var(--ion-color-primary, #3880ff)"
      - `blog_gallery.titleColor`: String, default "var(--ion-text-color, #1e293b)"
      - `blog_gallery.titleFontSize`: String, default "1em"
      - `blog_gallery.titleFontWeight`: String, default "700"
      - `blog_gallery.titleFontFamily`: String, default ""
      - `blog_gallery.titleTransform`: String, default "none"
      - `blog_gallery.dateColor`: String, default "var(--ion-color-primary, #3880ff)"
      - `blog_gallery.dateFontSize`: String, default "0.81em"
      - `blog_gallery.dateFontWeight`: String, default "600"
      - `blog_gallery.dateFontFamily`: String, default ""
      - `blog_gallery.dateFontStyle`: String, default "normal"
      - `blog_gallery.metaColor`: String, default "rgba(var(--ion-text-color-rgb, 30,41,59), 0.66)"
      - `blog_gallery.excerptFontSize`: String, default "0.88em"
      - `blog_gallery.excerptFontWeight`: String, default "400"
      - `blog_gallery.excerptFontFamily`: String, default ""
      - `blog_gallery.titlePosition`: String, default "bottom"
      - `blog_gallery.animation`: String, default "none"
      - `blog_gallery.animationDuration`: Number, default 0
      - `blog_gallery.animationDelay`: Number, default 0
      - `blog_gallery.cardDesign`: String, default ""
      - `blog_gallery.customCss`: String, default ""
      - `multi_booking.layout`: String, default "chips"
      - `multi_booking.columns`: Number, default 3
      - `multi_booking.pickerBackground`: String, default "transparent"
      - `multi_booking.gap`: Number, default 10
      - `multi_booking.align`: String, default "left"
      - `multi_booking.showPrice`: Boolean, default false
      - `multi_booking.showDuration`: Boolean, default false
      - `multi_booking.showDescription`: Boolean, default false
      - `multi_booking.showImage`: Boolean, default false
      - `multi_booking.itemPaddingX`: Number, default 18
      - `multi_booking.itemPaddingY`: Number, default 10
      - `multi_booking.itemRadius`: Number, default 100
      - `multi_booking.itemBorderWidth`: Number, default 0
      - `multi_booking.itemBorderColor`: String, default "rgba(var(--ion-color-primary-rgb, 56,128,255), 0.12)"
      - `multi_booking.itemBackground`: String, default "rgba(var(--ion-color-primary-rgb, 56,128,255), 0.10)"
      - `multi_booking.itemTextColor`: String, default "var(--ion-text-color, inherit)"
      - `multi_booking.itemSubTextColor`: String, default "var(--ion-color-medium, rgba(0,0,0,0.55))"
      - `multi_booking.selectedBackground`: String, default "var(--ion-color-primary)"
      - `multi_booking.selectedTextColor`: String, default "var(--ion-color-primary-contrast, #ffffff)"
      - `multi_booking.selectedSubTextColor`: String, default "rgba(var(--ion-color-primary-contrast-rgb, 255,255,255), 0.75)"
      - `multi_booking.selectedBorderColor`: String, default "var(--ion-color-primary)"
      - `multi_booking.hoverBackground`: String, default "rgba(var(--ion-color-primary-rgb, 56,128,255), 0.18)"
      - `multi_booking.titleFontSize`: Number, default 14
      - `multi_booking.titleFontWeight`: String, default "500"
      - `multi_booking.subFontSize`: Number, default 13
      - `multi_booking.dividerColor`: String, default "rgba(var(--ion-color-primary-rgb, 56,128,255), 0.12)"
      - `multi_booking.dividerWidth`: Number, default 1
      - `multi_booking.autoScroll`: Boolean, default true
      - `multi_booking.cardBackground`: String, default "var(--ion-card-color, #ffffff)"
      - `multi_booking.cardRadius`: Number, default 28
      - `multi_booking.cardBorderWidth`: Number, default 0
      - `multi_booking.cardBorderColor`: String, default "rgba(var(--ion-color-primary-rgb, 56,128,255), 0.12)"
      - `multi_booking.cardMaxWidth`: Number, default 820
      - `multi_booking.cardLayout`: String, default "stack"
      - `multi_booking.splitWidth`: Number, default 340
      - `multi_booking.shadowColor`: String, default "rgba(var(--ion-color-primary-rgb, 56,128,255), 0.18)"
      - `multi_booking.shadowOffsetY`: Number, default 20
      - `multi_booking.shadowBlur`: Number, default 40
      - `multi_booking.shadowSpread`: Number, default -22
      - `multi_booking.sectionPaddingX`: Number, default 40
      - `multi_booking.sectionPaddingY`: Number, default 32
      - `multi_booking.sectionDividerColor`: String, default "rgba(var(--ion-color-primary-rgb, 56,128,255), 0.12)"
      - `multi_booking.sectionDividerWidth`: Number, default 1
      - `multi_booking.showStepLabels`: Boolean, default true
      - `multi_booking.serviceStepLabel`: String, default "Step 1 — Service"
      - `multi_booking.dayStepLabel`: String, default "Step 2 — Day"
      - `multi_booking.staffStepLabel`: String, default "Step 3 — Staff member"
      - `multi_booking.timeStepLabel`: String, default "Step 4 — Time"
      - `multi_booking.stepLabelColor`: String, default "var(--ion-color-medium, rgba(0,0,0,0.55))"
      - `multi_booking.stepLabelSize`: Number, default 13
      - `multi_booking.stepLabelWeight`: String, default "700"
      - `multi_booking.stepLabelSpacing`: Number, default 0.04
      - `multi_booking.stepLabelUppercase`: Boolean, default true
      - `multi_booking.dayCount`: Number, default 6
      - `multi_booking.dayLayout`: String, default "strip"
      - `multi_booking.dayBorderColor`: String, default "rgba(var(--ion-color-primary-rgb, 56,128,255), 0.15)"
      - `multi_booking.dayShape`: String, default "circle"
      - `multi_booking.daySize`: Number, default 64
      - `multi_booking.dayGap`: Number, default 10
      - `multi_booking.dayShowWeekday`: Boolean, default false
      - `multi_booking.dayBackground`: String, default "rgba(var(--ion-color-primary-rgb, 56,128,255), 0.10)"
      - `multi_booking.dayTextColor`: String, default "var(--ion-text-color, inherit)"
      - `multi_booking.daySelectedBackground`: String, default "var(--ion-color-primary)"
      - `multi_booking.daySelectedTextColor`: String, default "var(--ion-color-primary-contrast, #ffffff)"
      - `multi_booking.dayDisabledOpacity`: Number, default 0.35
      - `multi_booking.timeGap`: Number, default 10
      - `multi_booking.timeRadius`: Number, default 100
      - `multi_booking.timeBorderWidth`: Number, default 0
      - `multi_booking.timeBorderColor`: String, default "rgba(var(--ion-color-primary-rgb, 56,128,255), 0.15)"
      - `multi_booking.timeSelectedBorderColor`: String, default "var(--ion-color-primary)"
      - `multi_booking.timePaddingX`: Number, default 18
      - `multi_booking.timePaddingY`: Number, default 10
      - `multi_booking.timeBackground`: String, default "rgba(var(--ion-color-primary-rgb, 56,128,255), 0.10)"
      - `multi_booking.timeTextColor`: String, default "var(--ion-text-color, inherit)"
      - `multi_booking.timeSelectedBackground`: String, default "var(--ion-color-primary)"
      - `multi_booking.timeSelectedTextColor`: String, default "var(--ion-color-primary-contrast, #ffffff)"
      - `multi_booking.buttonText`: String, default "Confirm my booking"
      - `multi_booking.buttonBackground`: String, default "var(--ion-color-primary)"
      - `multi_booking.buttonTextColor`: String, default "var(--ion-color-primary-contrast, #ffffff)"
      - `multi_booking.buttonRadius`: Number, default 100
      - `multi_booking.buttonPaddingY`: Number, default 16
      - `multi_booking.buttonFontSize`: Number, default 15
      - `multi_booking.buttonFontWeight`: String, default "700"
      - `multi_booking.buttonFullWidth`: Boolean, default true
      - `blogposts`: ObjectId[], → BlogPost
      - `videos`: ObjectId[], → Video
      - `price_plans`: ObjectId[], → PricePlan
      - `href`: String, default "#"
      - `router_link`: String
      - `buttonOptions.fill`: String
      - `buttonOptions.expand`: String
      - `buttonOptions.size`: String
      - `buttonOptions.color`: String
      - `buttonOptions.class`: String
      - `buttonOptions.customColor`: String
      - `buttonOptions.customColorContrast`: String
      - `buttonOptions.customColorHover`: String
      - `buttonOptions.hoverRoll`: Boolean
      - `buttonOptions.fontWeight`: Number
      - `buttonOptions.letterSpacing`: Number
      - `buttonOptions.fontSize`: Number, default 14
      - `buttonOptions.borderRadius`: Number, default 6
      - `buttonOptions.customColorA`: String
      - `buttonOptions.customColorB`: String
      - `buttonOptions.customColorC`: String
      - `buttonOptions.custom_measurements`: Boolean, default false
      - `buttonOptions.height.value`: String
      - `buttonOptions.height.unit`: String
      - `buttonOptions.width.value`: String
      - `buttonOptions.width.unit`: String
      - `carousel.showArrows`: Boolean, default true
      - `carousel.showIndicators`: Boolean, default true
      - `carousel.height`: Number, default 90
      - `carousel.automaticHeight`: Boolean, default true
      - `carousel.carouselMedias`: Object[]
      - `map`: String
      - `contactForm.name`: String
      - `contactForm.email`: String
      - `contactForm.subject`: String
      - `contactForm.body`: String
      - `contactForm.button`: String
      - `contactForm.hideNewsletter`: Boolean
      - `contactForm.placeholders.name`: String
      - `contactForm.placeholders.email`: String
      - `contactForm.placeholders.subject`: String
      - `contactForm.placeholders.body`: String
      - `contactForm.style.backgroundColor`: String
      - `contactForm.style.textColor`: String
      - `contactForm.style.accentColor`: String
      - `contactForm.style.border.color`: String
      - `contactForm.style.border.width.value`: Mixed
      - `contactForm.style.border.width.unit`: String
      - `contactForm.style.border.style`: String
      - `contactForm.style.borderRadius.value`: Mixed
      - `contactForm.style.borderRadius.unit`: String
      - `contactForm.style.padding.value`: Mixed
      - `contactForm.style.padding.unit`: String
      - `contactForm.style.labelFontSize.value`: Mixed
      - `contactForm.style.labelFontSize.unit`: String
      - `contactForm.style.consentFontSize.value`: Mixed
      - `contactForm.style.consentFontSize.unit`: String
      - `contactForm.style.inputBackgroundColor`: String
      - `contactForm.style.inputTextColor`: String
      - `contactForm.style.placeholderColor`: String
      - `contactForm.style.inputFocusColor`: String
      - `contactForm.style.inputFontSize.value`: Mixed
      - `contactForm.style.inputFontSize.unit`: String
      - `contactForm.style.inputPadding.value`: Mixed
      - `contactForm.style.inputPadding.unit`: String
      - `contactForm.style.textareaMinHeight.value`: Mixed
      - `contactForm.style.textareaMinHeight.unit`: String
      - `contactForm.style.inputMargin.value`: Mixed
      - `contactForm.style.inputMargin.unit`: String
      - `contactForm.style.inputBorder.color`: String
      - `contactForm.style.inputBorder.width.value`: Mixed
      - `contactForm.style.inputBorder.width.unit`: String
      - `contactForm.style.inputBorder.style`: String
      - `contactForm.style.inputBorderRadius.value`: Mixed
      - `contactForm.style.inputBorderRadius.unit`: String
      - `contactForm.style.buttonBackgroundColor`: String
      - `contactForm.style.buttonTextColor`: String
      - `contactForm.style.buttonAlign`: String
      - `contactForm.style.buttonFontSize.value`: Mixed
      - `contactForm.style.buttonFontSize.unit`: String
      - `contactForm.style.buttonBorder.color`: String
      - `contactForm.style.buttonBorder.width.value`: Mixed
      - `contactForm.style.buttonBorder.width.unit`: String
      - `contactForm.style.buttonBorder.style`: String
      - `contactForm.style.buttonBorderRadius.value`: Mixed
      - `contactForm.style.buttonBorderRadius.unit`: String
      - `contactForm.style.buttonWidth.value`: Mixed
      - `contactForm.style.buttonWidth.unit`: String
      - `contactForm.additional`: Object[]
      - `instagram.columns`: Number
      - `instagram.first`: Number
      - `instagram.skip`: Number
      - `instagram.showInfo`: Boolean
      - `instagram.gridType`: Number
      - `instagram.profile`: String
      - `facebook.url`: String
      - `facebook.width`: String
      - `facebook.height`: String
      - `animation`: String, default "none"
      - `animationDuration`: Number, default 0
      - `animationStrength`: Number
      - `parallaxEnabled`: Boolean
      - `parallaxAmount`: Number
      - `parallaxZoom`: Number
      - `textReveal`: String
      - `animationDelay`: Number, default 0
      - `animationReplay`: String, default "once"
      - `animationEasing`: String, default "power3.out"
      - `animationBreakpoint`: String, default "all"
      - `scrollScrubEnabled`: Boolean, default false
      - `scrollScrubAxis`: String, default "y"
      - `scrollScrubDistance`: Number, default 80
      - `motionOrigin`: String, default "center center"
      - `hoverEnabled`: Boolean, default false
      - `hoverEffect`: String, default "none"
      - `hoverDuration`: Number, default 240
      - `hoverIntensity`: Number, default 4
      - `continuousEnabled`: Boolean, default false
      - `continuousEffect`: String, default "none"
      - `continuousDuration`: Number, default 2400
      - `continuousIntensity`: Number, default 4
      - `statefulEnabled`: Boolean, default false
      - `statefulEffect`: String, default "none"
      - `statefulDuration`: Number, default 260
      - `statefulStagger`: Number, default 60
      - `transform.enabled`: Boolean, default false
      - `transform.translateX.value`: Number, default 0
      - `transform.translateX.unit`: String, default "px"
      - `transform.translateY.value`: Number, default 0
      - `transform.translateY.unit`: String, default "px"
      - `transform.scaleX`: Number, default 1
      - `transform.scaleY`: Number, default 1
      - `transform.rotate`: Number, default 0
      - `transform.rotateX`: Number, default 0
      - `transform.rotateY`: Number, default 0
      - `transform.skewX`: Number, default 0
      - `transform.skewY`: Number, default 0
      - `transform.perspective`: Number, default 0
      - `transform.originX`: String, default "center"
      - `transform.originY`: String, default "center"
      - `opacity`: Mixed
      - `transitionDuration`: Mixed
      - `transitionEasing`: String
      - `states`: Mixed
      - `useIframe`: Boolean, default false
      - `popup.text`: String
      - `popup.backgroundColor`: String
      - `popup.fullScreen`: Boolean, default false
      - `popup.about_page`: ObjectId, → AboutPage
      - `custom_component.html`: String
      - `custom_component.compiled_html`: String
      - `custom_component.css`: String
      - `custom_component.compiled_css`: String
      - `custom_component.js`: String
      - `custom_component.iframe`: Boolean, default false
      - `custom_component.variables`: Object[]
      - `hoverable_media.before_hover_media.media`: ObjectId, → Media
      - `hoverable_media.before_hover_media.gcs_url`: String
      - `hoverable_media.before_hover_media.vimeo_uri`: String
      - `hoverable_media.before_hover_media.vimeo_picture`: String
      - `hoverable_media.before_hover_media.youtube_url`: String
      - `hoverable_media.before_hover_media.vimeo_url`: String
      - `hoverable_media.before_hover_media.href`: String
      - `hoverable_media.before_hover_media.router_link`: String
      - `hoverable_media.before_hover_media.thumbnail`: String
      - `hoverable_media.before_hover_media.autoplay`: Boolean, default false
      - `hoverable_media.before_hover_media.loop`: Boolean, default false
      - `hoverable_media.before_hover_media.show_controls`: Boolean, default true
      - `hoverable_media.after_hover_media.media`: ObjectId, → Media
      - `hoverable_media.after_hover_media.gcs_url`: String
      - `hoverable_media.after_hover_media.vimeo_uri`: String
      - `hoverable_media.after_hover_media.vimeo_picture`: String
      - `hoverable_media.after_hover_media.youtube_url`: String
      - `hoverable_media.after_hover_media.vimeo_url`: String
      - `hoverable_media.after_hover_media.href`: String
      - `hoverable_media.after_hover_media.router_link`: String
      - `hoverable_media.after_hover_media.thumbnail`: String
      - `hoverable_media.after_hover_media.autoplay`: Boolean, default false
      - `hoverable_media.after_hover_media.loop`: Boolean, default false
      - `hoverable_media.after_hover_media.show_controls`: Boolean, default true
      - `collapsible_container`: Object[]
      - `collapsible_container_settings.enable_customization`: Boolean, default false
      - `collapsible_container_settings.title_color`: String
      - `collapsible_container_settings.title_color_checked`: String
      - `collapsible_container_settings.title_background`: String
      - `collapsible_container_settings.title_background_checked`: String
      - `collapsible_container_settings.title_font_size`: String
      - `collapsible_container_settings.title_border_radius`: String
      - `collapsible_container_settings.title_border_color`: String
      - `collapsible_container_settings.title_border_width`: String
      - `collapsible_container_settings.body_color`: String
      - `collapsible_container_settings.body_background`: String
      - `collapsible_container_settings.body_border_radius`: String
      - `collapsible_container_settings.body_border_color`: String
      - `collapsible_container_settings.body_border_width`: String
      - `collapsible_container_settings.body_font_size`: String
      - `collapsible_container_settings.icon_color`: String
      - `collapsible_container_settings.icon_glyph_color`: String
      - `collapsible_container_settings.icon_background_color`: String
      - `quiz.title`: String
      - `quiz.header`: String
      - `quiz.questions`: Object[]
      - `quiz.score_results`: Object[]
      - `quiz.show_right_answers`: Boolean, default false
      - `quiz.mutliple_choice`: Boolean, default false
      - `partners.defaultCollection`: String, default ""
      - `partners.defaultView`: String, default "locations"
      - `scroll_sequence.media`: ObjectId, → Media
      - `scroll_sequence.scroll_length`: Number, default 3
      - `scroll_sequence.fit`: String, one of cover | contain, default "cover"
      - `scroll_sequence.background_color`: String, default "#000000"
      - `scroll_sequence.overlay_color`: String, default ""
      - `scroll_sequence.overlay_opacity`: Number, default 0
      - `scroll_sequence.rows_over`: Boolean, default true
      - `scroll_sequence.rows_effect`: String, one of slide | pop, default "slide"
      - `scroll_sequence.snap_points`: Number[]
      - `scroll_sequence.snap_scroll_speed`: Number, default 1.5
      - `scroll_sequence.snap_down`: Boolean, default true
      - `scroll_sequence.snap_up`: Boolean, default false
      - `scroll_sequence.stop_momentum`: Boolean, default false
      - `smart_search.placeholder`: String, default ""
      - `smart_search.kinds`: String[]
      - `smart_search.collections`: String[]
      - `smart_search.items`: Object[]
      - `smart_search.showKindFilter`: Boolean, default true
      - `smart_search.showPriceFilter`: Boolean, default false
      - `smart_search.showDateFilter`: Boolean, default false
      - `smart_search.showSort`: Boolean, default true
      - `smart_search.dateMode`: String, one of created | updated | availability, default "created"
      - `smart_search.layout`: String, one of grid | list, default "grid"
      - `smart_search.columns`: Number, default 3
      - `smart_search.perPage`: Number, default 12
      - `smart_search.instant`: Boolean, default true
      - `smart_search.showImages`: Boolean, default true
      - `smart_search.showPrices`: Boolean, default true
      - `smart_search.showInitial`: Boolean, default false
      - `smart_search.showAnswer`: Boolean, default false
      - `countdown.targetDate`: String, default ""
      - `countdown.expiredText`: String, default "Offer has ended"
      - `countdown.hideOnExpire`: Boolean, default false
      - `countdown.textColor`: String, default ""
      - `countdown.boxColor`: String, default ""
      - `countdown.align`: String, one of  | left | center | right, default ""
      - `swiper.slides`: Object[]
      - `swiper.autoplay`: Boolean, default false
      - `swiper.loop`: Boolean, default false
      - `swiper.show_controls`: Boolean, default true
      - `swiper.dynamic_bullets`: Boolean, default false
      - `swiper.pagination`: Boolean, default true
      - `dynamicField`: String
      - `useDynamicField`: Boolean, default false
      - `text_auto_slider.speed`: Number
      - `text_auto_slider.aspect_ratio`: String
      - `text_auto_slider.text_color`: String
      - `text_auto_slider.background_color`: String
      - `text_auto_slider.delimiter_color`: String
      - `text_auto_slider.direction`: String
      - `text_auto_slider.width`: Number
      - `text_auto_slider.font_size`: Number
      - `text_auto_slider.text_transform`: String, one of none | uppercase, default "none"
      - `text_auto_slider.texts`: Object[]
      - `image_auto_slider.speed`: Number
      - `image_auto_slider.aspect_ratio`: String
      - `image_auto_slider.direction`: String
      - `image_auto_slider.width`: Number
      - `image_auto_slider.medias`: Object[]
      - `image_dragger.aspect_ratio`: String
      - `image_dragger.media_1.media`: ObjectId, → Media
      - `image_dragger.media_1.gcs_url`: String
      - `image_dragger.media_1.vimeo_uri`: String
      - `image_dragger.media_1.vimeo_picture`: String
      - `image_dragger.media_1.vimeo_url`: String
      - `image_dragger.media_1.youtube_url`: String
      - `image_dragger.media_2.media`: ObjectId, → Media
      - `image_dragger.media_2.gcs_url`: String
      - `image_dragger.media_2.vimeo_uri`: String
      - `image_dragger.media_2.vimeo_picture`: String
      - `image_dragger.media_2.vimeo_url`: String
      - `image_dragger.media_2.youtube_url`: String
      - `image_dragger.media_2.title`: String
      - `image_dragger.media_2.body`: String
      - `image_dragger.media_2.textColor`: String
      - `image_dragger.media_2.href`: String
      - `image_dragger.media_2.router_link`: String
      - `_id`: ObjectId
    - `sections`: Object[]
      - `position`: String, one of static | relative | absolute | fixed | sticky, default "static"
      - `overflow`: String, one of visible | hidden | scroll | auto, default "visible"
      - `editorName`: String
      - `top.value`: String, default "0"
      - `top.unit`: String, default "px"
      - `top.mobile_value`: String, default ""
      - `top.mobile_unit`: String, default "px"
      - `left.value`: String, default "0"
      - `left.unit`: String, default "px"
      - `left.mobile_value`: String, default ""
      - `left.mobile_unit`: String, default "px"
      - `right.value`: String, default "0"
      - `right.unit`: String, default "px"
      - `right.mobile_value`: String, default ""
      - `right.mobile_unit`: String, default "px"
      - `bottom.value`: String, default "0"
      - `bottom.unit`: String, default "px"
      - `bottom.mobile_value`: String, default ""
      - `bottom.mobile_unit`: String, default "px"
      - `display`: String, one of block | inline | flex, default "block"
      - `flex_direction`: String, one of row | row-reverse | column | column-reverse, default "row"
      - `flex_direction_mobile`: String, default ""
      - `backdropFilter`: String, default ""
      - `justify`: String, one of start | end | center | space-between | space-around | stretch, default "start"
      - `justify_mobile`: String, default ""
      - `align`: String, one of start | end | center | space-between | space-around | stretch, default "start"
      - `align_mobile`: String, default ""
      - `animation`: String, default "none"
      - `animationDuration`: Number, default 0
      - `animationStrength`: Number
      - `parallaxEnabled`: Boolean
      - `parallaxAmount`: Number
      - `parallaxZoom`: Number
      - `animationDelay`: Number, default 0
      - `animationReplay`: String, default "once"
      - `animationEasing`: String, default "power3.out"
      - `animationBreakpoint`: String, default "all"
      - `scrollScrubEnabled`: Boolean, default false
      - `scrollScrubAxis`: String, default "y"
      - `scrollScrubDistance`: Number, default 80
      - `motionOrigin`: String, default "center center"
      - `hoverEnabled`: Boolean, default false
      - `hoverEffect`: String, default "none"
      - `hoverDuration`: Number, default 240
      - `hoverIntensity`: Number, default 4
      - `continuousEnabled`: Boolean, default false
      - `continuousEffect`: String, default "none"
      - `continuousDuration`: Number, default 2400
      - `continuousIntensity`: Number, default 4
      - `statefulEnabled`: Boolean, default false
      - `statefulEffect`: String, default "none"
      - `statefulDuration`: Number, default 260
      - `statefulStagger`: Number, default 60
      - `transform.enabled`: Boolean, default false
      - `transform.translateX.value`: Number, default 0
      - `transform.translateX.unit`: String, default "px"
      - `transform.translateY.value`: Number, default 0
      - `transform.translateY.unit`: String, default "px"
      - `transform.scaleX`: Number, default 1
      - `transform.scaleY`: Number, default 1
      - `transform.rotate`: Number, default 0
      - `transform.rotateX`: Number, default 0
      - `transform.rotateY`: Number, default 0
      - `transform.skewX`: Number, default 0
      - `transform.skewY`: Number, default 0
      - `transform.perspective`: Number, default 0
      - `transform.originX`: String, default "center"
      - `transform.originY`: String, default "center"
      - `opacity`: Mixed
      - `transitionDuration`: Mixed
      - `transitionEasing`: String
      - `states`: Mixed
      - `hoverGroup`: Boolean
      - `flex_wrap`: String, one of nowrap | wrap | wrap-reverse, default "wrap"
      - `flex_wrap_mobile`: String, default ""
      - `width.value`: String, default ""
      - `width.unit`: String, default "px"
      - `width.mobile_value`: String, default ""
      - `width.mobile_unit`: String, default "px"
      - `min_width.value`: String, default ""
      - `min_width.unit`: String, default "px"
      - `min_width.mobile_value`: String, default ""
      - `min_width.mobile_unit`: String, default "px"
      - `max_width.value`: String, default ""
      - `max_width.unit`: String, default "px"
      - `max_width.mobile_value`: String, default ""
      - `max_width.mobile_unit`: String, default "px"
      - `height.value`: String, default ""
      - `height.unit`: String, default "px"
      - `height.mobile_value`: String, default ""
      - `height.mobile_unit`: String, default "px"
      - `min_height.value`: String, default ""
      - `min_height.unit`: String, default "px"
      - `min_height.mobile_value`: String, default ""
      - `min_height.mobile_unit`: String, default "px"
      - `max_height.value`: String, default ""
      - `max_height.unit`: String, default "px"
      - `max_height.mobile_value`: String, default ""
      - `max_height.mobile_unit`: String, default "px"
      - `object_fit`: String
      - `aspect_ratio.custom`: Boolean, default false
      - `aspect_ratio.ratio`: String, default ""
      - `margin_left.value`: String, default "0"
      - `margin_left.unit`: String, default "px"
      - `margin_left.mobile_value`: String, default ""
      - `margin_left.mobile_unit`: String, default "px"
      - `margin_right.value`: String, default "0"
      - `margin_right.unit`: String, default "px"
      - `margin_right.mobile_value`: String, default ""
      - `margin_right.mobile_unit`: String, default "px"
      - `margin_top.value`: String, default "0"
      - `margin_top.unit`: String, default "px"
      - `margin_top.mobile_value`: String, default ""
      - `margin_top.mobile_unit`: String, default "px"
      - `margin_bottom.value`: String, default "0"
      - `margin_bottom.unit`: String, default "px"
      - `margin_bottom.mobile_value`: String, default ""
      - `margin_bottom.mobile_unit`: String, default "px"
      - `padding_left.value`: String, default "0"
      - `padding_left.unit`: String, default "px"
      - `padding_left.mobile_value`: String, default ""
      - `padding_left.mobile_unit`: String, default "px"
      - `padding_right.value`: String, default "0"
      - `padding_right.unit`: String, default "px"
      - `padding_right.mobile_value`: String, default ""
      - `padding_right.mobile_unit`: String, default "px"
      - `padding_top.value`: String, default "0"
      - `padding_top.unit`: String, default "px"
      - `padding_top.mobile_value`: String, default ""
      - `padding_top.mobile_unit`: String, default "px"
      - `padding_bottom.value`: String, default "0"
      - `padding_bottom.unit`: String, default "px"
      - `padding_bottom.mobile_value`: String, default ""
      - `padding_bottom.mobile_unit`: String, default "px"
      - `border_left.style`: String, default "none"
      - `border_left.color`: String
      - `border_left.width.value`: String, default "0"
      - `border_left.width.unit`: String, default "px"
      - `border_right.style`: String, default "none"
      - `border_right.color`: String
      - `border_right.width.value`: String, default "0"
      - `border_right.width.unit`: String, default "px"
      - `border_top.style`: String, default "none"
      - `border_top.color`: String
      - `border_top.width.value`: String, default "0"
      - `border_top.width.unit`: String, default "px"
      - `border_bottom.style`: String, default "none"
      - `border_bottom.color`: String
      - `border_bottom.width.value`: String, default "0"
      - `border_bottom.width.unit`: String, default "px"
      - `border_top_left_radius.value`: String, default "0"
      - `border_top_left_radius.unit`: String, default "px"
      - `border_top_right_radius.value`: String, default "0"
      - `border_top_right_radius.unit`: String, default "px"
      - `border_bottom_left_radius.value`: String, default "0"
      - `border_bottom_left_radius.unit`: String, default "px"
      - `border_bottom_right_radius.value`: String, default "0"
      - `border_bottom_right_radius.unit`: String, default "px"
      - `border_radius.value`: String, default "0"
      - `border_radius.unit`: String, default "px"
      - `specific_border_radius`: Boolean, default false
      - `box_shadows`: Object[]
      - `backgroundColor`: String
      - `backgroundGradient.enabled`: Boolean, default false
      - `backgroundGradient.type`: String
      - `backgroundGradient.degrees`: Number
      - `backgroundGradient.radialShape`: String
      - `backgroundGradient.radialPosition`: String
      - `backgroundGradient.colors`: Object[]
      - `backgroundGradient.css`: String
      - `backgroundVideo`: String
      - `backgroundImage`: String
      - `backgroundSize`: String
      - `backgroundPosition`: String
      - `backgroundRepeat`: String
      - `showOnMobile`: Boolean, default true
      - `showOnDesktop`: Boolean, default true
      - `components`: Object[]
      - `sections`: Object[]
      - `_id`: ObjectId
    - `_id`: ObjectId
  - `ai_meta`: Mixed
  - `_id`: ObjectId
- `suitability_price_plans`: ObjectId[], → PricePlan - PricePlan ids.
- `isSuitable`: Boolean - Set per request, not stored: whether the signed-in user's price plans match the page's suitability plans.
- `archived`: Boolean, default false - Hidden from the site and the admin's page list without being deleted.
- `archivedAt`: Date - When the page was archived.
- `_id`: ObjectId
- `updatedAt`: Date
- `createdAt`: Date

### ChatMessage

- `chat_id`: ObjectId, required, → ChatHistory - ChatHistory.id.
- `role`: String, one of user | model - One of: user, model.
- `text`: String - The message's text.
- `target_url`: String - Admin page the message links to.
- `action`: String, one of RESPONSE | REDIRECT | OBJECT_RESPONSE | ERROR | CLARIFY | PROPOSED_ACTION - One of: RESPONSE, REDIRECT, OBJECT_RESPONSE, ERROR, CLARIFY, PROPOSED_ACTION.
- `options`: String[] - Quick replies offered under the message.
- `data.object`: Mixed
- `data.object_type`: String
- `data.edit_summary`: Mixed
- `data.edit_diff`: Mixed
- `data.undo_record`: Mixed
- `data.hero_animation_offer`: Mixed
- `data.preview_state`: String
- `data.pending_credit_cost`: Number
- `data.target_restore`: Mixed
- `data.media_request`: Mixed
- `data.item_request`: Mixed
- `data.reference_request`: Mixed
- `data.section_options`: Mixed
- `data.section_blueprint`: Mixed
- `data.section_style_options`: Mixed
- `data.section_style_blueprint`: Mixed
- `data.section_role_picker`: Mixed
- `data.small_section_regeneration_options`: Mixed
- `data.small_section_regeneration_target`: Mixed
- `data.small_section_regeneration_prompt`: String
- `data.selected_small_section_action`: Mixed
- `data.component_regeneration_input`: Mixed
- `data.selected_section_role`: Mixed
- `data.selected_section_option`: Mixed
- `data.selected_section_style`: Mixed
- `data.section_options_target`: Mixed
- `data.section_options_prompt`: String
- `data.style_options_target`: Mixed
- `data.style_options_prompt`: String
- `data.previous_container`: Mixed
- `data.preserved_custom_components`: Mixed[]
- `data.selection_request`: Mixed
- `data.proposed_action`: Mixed
- `data.plan_result`: Mixed
- `data.parsed_intent`: Mixed
- `data.parsed_intent_version`: Number
- `data.parsed_intent_at`: Date
- `data.item_id`: ObjectId
- `data.design_archetype`: String
- `data.design_palette`: Mixed
- `data.out_of_credits`: Boolean
- `data.site_wide_edit`: Mixed
- `data.redesign`: Mixed
- `medias`: Object[] - Pictures attached to the message.
  - `media`: ObjectId, → Media
  - `gcs_url`: String
  - `gemini_uri`: String
  - `gemini_interaction_id`: String
  - `_id`: ObjectId
- `products`: ObjectId[], → Product - Product ids.
- `services`: ObjectId[], → Service - Service ids.
- `blogposts`: ObjectId[], → BlogPost - BlogPost ids.
- `programs`: ObjectId[], → Program - Program ids.
- `suggestions`: Object[] - Next steps suggested under the message.
  - `text`: String
  - `route`: String
  - `item_id`: ObjectId
  - `medias`: Mixed[]
  - `picture`: ObjectId
  - `_id`: ObjectId
- `is_system`: Boolean, default false - Written by the platform, not by the owner or the AI.
- `_id`: ObjectId
- `updatedAt`: Date
- `createdAt`: Date

### CustomComponent

- `name`: String - Name of the component, shown in the editor.
- `html`: String - The component's HTML as written.
- `compiled_html`: String - HTML with its variables filled in, as rendered.
- `css`: String - The component's CSS as written.
- `compiled_css`: String - CSS as rendered.
- `js`: String - The component's script.
- `variables`: Object[] - Values the owner can set in the editor without touching the code.
  - `value`: String
  - `_id`: ObjectId
- `iframe`: Boolean, default false - Rendered in an iframe, isolated from the page.
- `media`: ObjectId, → Media - Media.id.
- `_id`: ObjectId
- `updatedAt`: Date
- `createdAt`: Date

### CustomFont

- `family`: String, required - CSS font-family value, e.g. "'Comic Neue', sans-serif".
- `link`: String, required - Google Fonts stylesheet URL.
- `name`: String - Bare family name as Google spells it, e.g. "Comic Neue".
- `weights`: String - Roman weights, comma-separated, e.g. "300,400,700".
- `italics`: String[] - Italic weights as the picker spells them, e.g. ["i400"].
- `subsets`: String[] - Unicode subsets Google serves, e.g. ["latin","cyrillic"].
- `generic`: String - CSS generic fallback: sans-serif | serif | cursive.
- `_id`: ObjectId
- `updatedAt`: Date
- `createdAt`: Date

### IndexComponent

- `type`: String - What the block shows: about_page (a page), or a list - products, products_latest, products_top, services, services_all, services_latest, services_top, blogposts, blogposts_latest, blogposts_top, videos, videos_latest, price_plans.
- `position`: Number - Order on the home page; lower first.
- `about_page`: String - For an about_page block: the URL of the page it shows (AboutPage.url, e.g. "our-story"), not its id. POST /index-components/append takes either and stores the url.
- `isFluid`: Boolean, default false - Spans the full width of the screen.
- `isAppComponent`: Boolean, default false - Shown in the mobile app's home instead of the website's.
- `_id`: ObjectId
- `updatedAt`: Date
- `createdAt`: Date

### Locales

The words of the site's interface in each of its languages.

- `locales_object`: Mixed - The site's words as JSON text: { <key>: <text shown> }, the key being the English source wording. Only the newest record is used.
- `_id`: ObjectId
- `updatedAt`: Date
- `createdAt`: Date

### Popup

A popup shown on the site, with the landing page behind its accept button.

- `title`: String, required - Internal admin label for this popup (e.g. "Welcome discount", "Newsletter signup", "Cookie consent").
- `body`: ObjectId, required, → AboutPage - The full landing page the user sees after clicking accept.
- `decline_button_text`: String, required - Text on the dismiss button (e.g. "No thanks", "Maybe later", "Close").
- `accept_button_text`: String, required - Text on the primary CTA button of the popup modal (e.g. "Sign up", "Learn more", "Claim offer").
- `accept_button_link`: String - Where the popup's button leads.
- `wide_modal`: Boolean, default false - True for wider popup modal, false (default) for compact.
- `_id`: ObjectId
- `updatedAt`: Date
- `createdAt`: Date

### Wildcard

- `url`: String - Custom url of wildcard.
- `points_to`: String - Url to override.
- `_id`: ObjectId
- `updatedAt`: Date
- `createdAt`: Date
