Appearance
CMS Static Pages
Flow ID: AD-48 Module(s): category (legacy), Cms/Page (modern) Complexity: Medium Last Updated: 2026-04-04
Business Overview
CMS Pages are slug-routed static content pages that serve informational purposes across the storefront -- about us, terms & conditions, privacy policy, FAQ, contact, and similar content. They are distinct from product categories despite sharing the same categories / categories_mui database tables. Pages support a hierarchical parent-child tree structure, per-language MUI translations, optional banner images, SEO metadata, custom URL overrides, redirect rules, and integration with the page builder for visual layout composition. Approximately 19 pages exist in a typical production database.
Each page is assigned a page view template (configured in application/config/app.php) and a menu group that controls its navigation placement. The storefront resolves pages by slug via the Adv_category controller with a _remap() method. SEO-friendly URL routing is managed through auto-generated route files that map custom URLs to category slugs.
API Reference
REST Endpoints
| Method | Path | Action | Auth |
|---|---|---|---|
GET | /rest/cms/page | List pages (paginated, filterable) | JWT |
GET | /rest/cms/page/item | Get single page by filters | JWT |
GET | /rest/cms/page/{id} | Get page by ID | JWT |
POST | /rest/cms/page | Create page | JWT |
POST | /rest/cms/page/{id} | Update page | JWT |
DELETE | /rest/cms/page/{id} | Delete page | JWT |
All REST routes support locale-prefixed variants (/{locale}/rest/cms/page).
Filters (REST)
| Filter | Type | Description |
|---|---|---|
id | Exact | Page ID(s) |
parent | Exact | Parent page ID |
bannerImage | Partial | Banner image filename |
menu | Exact | Menu group value |
pageView | Exact | Page view template ID |
order | Exact | Sort order position |
isPublished | Exact | Published status (0/1) |
createdAt | Exact | Creation date |
name.{locale} | Partial | Page name by locale |
slug.{locale} | Exact | Slug by locale |
metaTitle.{locale} | Partial | Meta title by locale |
metaDescription.{locale} | Partial | Meta description by locale |
metaKeywords.{locale} | Partial | Meta keywords by locale |
url.{locale} | Partial | Custom URL by locale |
redirectUrl.{locale} | Partial | Redirect URL by locale |
notEmptyContent.{locale} | NotEmpty | Has non-empty content body |
Relations
| Relation | Type | Description |
|---|---|---|
translations | One-to-Many | MUI translation rows |
parent | Belongs-To | Parent page (recursive) |
children | One-to-Many | Child pages (recursive) |
Sorts
Root: id, parent, bannerImage, menu, pageView, order, isPublished, createdAt Translation: name.{locale}, slug.{locale}, metaTitle.{locale}, metaDescription.{locale}, metaKeywords.{locale}, url.{locale}, redirectUrl.{locale} Relation (children): id, parent, order, isPublished, createdAt, menu, pageView
Admin Panel Endpoints (Legacy HMVC)
| URL | Method | Action |
|---|---|---|
category/category_admin | index() | Sortable page listing |
category/category_admin/add | add() | Create page form + handler |
category/category_admin/edit/{id} | edit($id) | Edit page form + handler |
category/category_admin/delete/{id} | delete($id) | Delete page |
category/category_admin/updateorder | updateorder() | AJAX drag-and-drop reorder |
Code Flow
Storefront Page Rendering
Browser request: /category/{slug} (or custom URL via SEO route)
|
v
Adv_category::_remap($method, $params)
| Builds slug from method + params
| Queries categories_model::get_content($slug)
| Returns 404 if slug not found or unpublished
v
Adv_category::index($slug)
| Loads: product categories, page content, parent, sibling menu
| Builds: SEO metatags, breadcrumbs, language URLs
| Checks: builder block ID for visual layout
|
+-- If builder_block_id set --> renders 'cmsBuilder' layout
+-- Else --> renders template from categoryPageViews config
|
v
load->view(template_view_front, render)Admin Create Flow
Admin: category/category_admin/add
|
v
Adv_category_admin::add()
| Validates: parent_category, menu, page_view, name per language
| Handles: banner_image upload via advuploader
| Auto-generates: slug from name + parent slug
|
+-- beforeAddRecord($data, $dataMui) [client hook]
|
v
categories_model::add_record($data, $dataMui)
| Inserts master record with date_created
| Inserts MUI rows per language (name, fulltext, slug, SEO, URL fields)
|
+-- afterAdd($id) [client hook]
|
v
seoroutes::save_routes(ROUTE_CAT)
| Regenerates category_routes.php with URL/redirect mappings
|
v
redirect -> category/category_adminREST Create Flow
POST /rest/cms/page
|
v
Page::store() -> doStore()
| Parses JSON body
v
WriteService::create($data)
| Builds WriteData from input (parentId, bannerImage, menu, pageView, order, published)
| Validates: parentCategory required
| Parses translations array -> MuiWriteData per language
| Validates: lang required per translation
|
v (transactional)
WriteRepository::insert($data + date_created)
MuiWriteRepository::insertForEntity($id, $translations)
|
v
Repository::get($id, ['translations'])
| Returns created entity with translations
v
Resource -> JSON response (201)Domain Layer
File Structure
src/Domains/Cms/Page/
Repository/
Entity.php -- id, parent_category, banner_image, menu, page_view, order, is_published, date_created
MuiEntity.php -- id, category_id, slug, name, meta_keywords, meta_description, meta_title, url, redirect_url, fulltext, builder_block_id
Repository.php -- table: categories
RepositoryConfigurator.php -- translations (ONE_TO_MANY), parent (BELONGS_TO, recursive), children (ONE_TO_MANY, recursive)
MuiRepository.php -- table: categories_mui
MuiRepositoryConfigurator.php -- builder relation (BELONGS_TO -> Builder)
WriteRepository.php
MuiWriteRepository.php
Specification/
FilterByTranslation.php
SortByTranslation.php
Service.php -- ReadService: all(), item(), get()
WriteService.php -- create(), update(), delete() with transactional writes
ListRequest.php -- Allowed filters/sorts/relation sorts
WriteData.php -- parentCategory, bannerImage, menu, pageView, order, isPublished
MuiWriteData.php -- lang, slug, name, metaKeywords, metaDescription, metaTitle, url, redirectUrl, fulltext, builderBlockId
Validator.php -- Create: parentCategory required. Update: all optional. Translations: lang required.REST Layer
src/Rest/Cms/Controllers/Page.php -- HandlesRestfulActions + HandlesWriteActions; full CRUD
src/Rest/Cms/Resources/Page/
Resource.php -- id, parentId, bannerImage (formatted), menu, pageView, order, published, createdAt; backend-only fields gated by context
Collection.php -- Collects Resource
MuiResource.php -- id, pageId, slug, name, metaKeywords, metaDescription, metaTitle, url, redirectUrl, content, builderId, lang; includes builder relation
MuiCollection.php -- Collects MuiResource; auto-includes 'builder' relationLegacy Layer
ecommercen/category/
controllers/
Adv_category.php -- Storefront: _remap() slug routing, index() page rendering
Adv_category_admin.php -- Admin CRUD: index, add, edit, delete, updateorder
models/
Adv_categories_model.php -- All DB operations: CRUD, tree traversal, slug generation, content lookup
helpers/
category_helper.php -- pageViewsCombo(), pageViewsFiles(), pageMenuCombo()Data Model
categories (Master Table)
| Column | Type | Description |
|---|---|---|
id | int, PK, AI | Page ID |
parent_category | int | Parent page ID (0 = root) |
banner_image | varchar | Banner image filename (stored in files/categories/) |
menu | int | Menu group assignment (configurable via categoryMenuTypes) |
page_view | int | View template ID (maps to categoryPageViews config) |
order | int | Sort order within sibling group |
is_published | tinyint(1) | Published status (storefront only shows is_published = 1) |
date_created | datetime | Creation timestamp |
categories_mui (MUI Table)
| Column | Type | Description |
|---|---|---|
id | int, PK, AI | MUI row ID |
category_id | int, FK | References categories.id |
lang | varchar(5) | Language code (e.g., el, en) |
slug | varchar | URL slug (hierarchical: parent-slug/page-slug) |
name | varchar | Page title |
fulltext | text | Rich HTML content body |
meta_title | varchar | SEO title |
meta_keywords | varchar | SEO keywords |
meta_description | text | SEO description |
url | varchar | Custom URL override (takes precedence over category/{slug}) |
redirect_url | varchar | Redirect URL (generates redirect route) |
builder_block_id | int, nullable | FK to builder blocks for visual layout |
Entity Relationships
categories (Page)
|-- 1:N --> categories_mui (translations, FK: category_id)
|-- N:1 --> categories (parent, FK: parent_category, recursive)
|-- 1:N --> categories (children, FK: parent_category, recursive)
|
categories_mui
|-- N:1 --> builder blocks (FK: builder_block_id)Configuration
Page View Templates
Defined in application/config/app.php:
php
$config['categoryPageViews'] = [
'0' => ['key' => 'cms.page_types.default', 'file' => 'page']
];Client repos can extend this array to add custom view templates (e.g., blog-style, FAQ accordion, landing page).
Menu Groups
php
$config['categoryMenuTypes'] = [
0 => 'cms.menu_types.general'
];Clients extend to add menu groups like header, footer, sidebar.
SEO Route Generation
The Seoroutes library regenerates application/config/category_routes.php on every page create/edit/delete. For each published page with a custom url field, it writes:
php
$route["{lang}/custom-url"] = "{lang}/category/page-slug";For pages with redirect_url, it writes redirect rules pointing to the redirect controller.
Constants
ROUTE_CAT='category'(defined inapplication/config/constants.php)
Authorization
Admin Panel
| Role | Constant | Access |
|---|---|---|
| Advisable | AUTH_ROLE_ADVISABLE | Full CRUD + slug editing + URL/redirect fields + metatag editing |
| Admin | AUTH_ROLE_ADMIN | Full CRUD |
| CMS | AUTH_ROLE_CMS | Full CRUD |
Role-gated field access:
- Metatags (
meta_title,meta_keywords,meta_description): RequirescanAccess('metatags', $sessionRoles) - URL fields (
slug,url,redirect_url): RequirescanAccess('urlFields', $sessionRoles)-- only ADVISABLE role can edit slugs on existing records
REST API
JWT authentication required. All endpoints are backend-only. The Resource class gates menu, pageView, order, published, and createdAt fields behind $context->isBackend().
Client Extension Points
Admin Controller Hooks
The admin controller provides empty protected methods for client-specific logic:
| Hook | When Called | Signature |
|---|---|---|
beforeAddRecord() | Before insert | (array $data, array $dataMui): array -- return modified arrays |
beforeEditRecord() | Before update | (int $id, array $data, array $dataMui): array |
afterAdd() | After insert | (int $id): void |
afterEdit() | After update | (int $id): void |
afterDelete() | After delete | (int $id): void |
indexExtras() | During storefront render | (): void -- add extra render data |
View Templates
Clients extend categoryPageViews config to register custom view files. The storefront controller selects the template based on the page's page_view field, looking in {client_views}/cms/{file}.
Builder Block Integration
Pages can use the page builder instead of the fulltext content field. When builder_block_id is set on a translation, the storefront renders the cmsBuilder layout instead of the configured page view template.
Domain Layer Override
Client repos can override the repository configurator via DI alias:
php
$services->alias(
\Advisable\Domains\Cms\Page\Repository\RepositoryConfigurator::class,
\Custom\Domains\Cms\Page\Repository\RepositoryConfigurator::class
);Business Rules
- Slug generation: On create, slugs are auto-generated from the page name combined with the parent's slug (hierarchical:
parent-slug/page-slug). On update, only ADVISABLE role can modify slugs directly. - Publishing: Only pages with
is_published = 1are visible on the storefront. Theget_content()model method filters by published status. - Hierarchy: Pages support unlimited nesting via
parent_category. Root pages haveparent_category = 0. - Deletion guard: A page cannot be deleted if it has child pages (
can_delete_record()checks for children). - URL override: If a page has a custom
urlfield, the SEO route system creates a mapping from that URL to the category slug. ThegetCmsUri()helper returns the custom URL when set, falling back tocategory/{slug}. - Redirect: Pages with a
redirect_urlgenerate a redirect route that passes through the redirect controller. - SEO route regeneration: Every admin CRUD operation triggers
seoroutes::save_routes(ROUTE_CAT)to rebuildcategory_routes.php. - Banner image: Uploaded to
files/categories/viaadvuploader. Image deletion removes the physical file viastorage()->delete(). - Ordering: Drag-and-drop reorder via AJAX updates the
ordercolumn for sibling pages. - Builder vs content: If
builder_block_idis set, the builder layout takes precedence over thefulltextfield and the page view template.
Related Flows
- AD-05 Category Management -- Product categories (separate from CMS pages but similar admin patterns)
- AD-12 Blog Management -- Blog articles with CMS-like content editing
- AD-14 SEO Management -- SEO routes, metatag conventions
- AD-29 Page Builder -- Builder blocks referenced by
builder_block_id - AD-49 Subcontent Blocks -- Embeddable content blocks used alongside CMS pages
- SY-23 MUI Translation Pattern --
categories_muicompanion table stores per-language names, slugs, content, and SEO fields - SY-25 File Upload & Storage --
advuploaderhandles banner image uploads tofiles/categories/; physical file deletion routes throughstorage()->delete()