Skip to content

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

MethodPathActionAuth
GET/rest/cms/pageList pages (paginated, filterable)JWT
GET/rest/cms/page/itemGet single page by filtersJWT
GET/rest/cms/page/{id}Get page by IDJWT
POST/rest/cms/pageCreate pageJWT
POST/rest/cms/page/{id}Update pageJWT
DELETE/rest/cms/page/{id}Delete pageJWT

All REST routes support locale-prefixed variants (/{locale}/rest/cms/page).

Filters (REST)

FilterTypeDescription
idExactPage ID(s)
parentExactParent page ID
bannerImagePartialBanner image filename
menuExactMenu group value
pageViewExactPage view template ID
orderExactSort order position
isPublishedExactPublished status (0/1)
createdAtExactCreation date
name.{locale}PartialPage name by locale
slug.{locale}ExactSlug by locale
metaTitle.{locale}PartialMeta title by locale
metaDescription.{locale}PartialMeta description by locale
metaKeywords.{locale}PartialMeta keywords by locale
url.{locale}PartialCustom URL by locale
redirectUrl.{locale}PartialRedirect URL by locale
notEmptyContent.{locale}NotEmptyHas non-empty content body

Relations

RelationTypeDescription
translationsOne-to-ManyMUI translation rows
parentBelongs-ToParent page (recursive)
childrenOne-to-ManyChild 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)

URLMethodAction
category/category_adminindex()Sortable page listing
category/category_admin/addadd()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/updateorderupdateorder()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_admin

REST 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' relation

Legacy 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)

ColumnTypeDescription
idint, PK, AIPage ID
parent_categoryintParent page ID (0 = root)
banner_imagevarcharBanner image filename (stored in files/categories/)
menuintMenu group assignment (configurable via categoryMenuTypes)
page_viewintView template ID (maps to categoryPageViews config)
orderintSort order within sibling group
is_publishedtinyint(1)Published status (storefront only shows is_published = 1)
date_createddatetimeCreation timestamp

categories_mui (MUI Table)

ColumnTypeDescription
idint, PK, AIMUI row ID
category_idint, FKReferences categories.id
langvarchar(5)Language code (e.g., el, en)
slugvarcharURL slug (hierarchical: parent-slug/page-slug)
namevarcharPage title
fulltexttextRich HTML content body
meta_titlevarcharSEO title
meta_keywordsvarcharSEO keywords
meta_descriptiontextSEO description
urlvarcharCustom URL override (takes precedence over category/{slug})
redirect_urlvarcharRedirect URL (generates redirect route)
builder_block_idint, nullableFK 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).

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 in application/config/constants.php)

Authorization

Admin Panel

RoleConstantAccess
AdvisableAUTH_ROLE_ADVISABLEFull CRUD + slug editing + URL/redirect fields + metatag editing
AdminAUTH_ROLE_ADMINFull CRUD
CMSAUTH_ROLE_CMSFull CRUD

Role-gated field access:

  • Metatags (meta_title, meta_keywords, meta_description): Requires canAccess('metatags', $sessionRoles)
  • URL fields (slug, url, redirect_url): Requires canAccess('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:

HookWhen CalledSignature
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

  1. 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.
  2. Publishing: Only pages with is_published = 1 are visible on the storefront. The get_content() model method filters by published status.
  3. Hierarchy: Pages support unlimited nesting via parent_category. Root pages have parent_category = 0.
  4. Deletion guard: A page cannot be deleted if it has child pages (can_delete_record() checks for children).
  5. URL override: If a page has a custom url field, the SEO route system creates a mapping from that URL to the category slug. The getCmsUri() helper returns the custom URL when set, falling back to category/{slug}.
  6. Redirect: Pages with a redirect_url generate a redirect route that passes through the redirect controller.
  7. SEO route regeneration: Every admin CRUD operation triggers seoroutes::save_routes(ROUTE_CAT) to rebuild category_routes.php.
  8. Banner image: Uploaded to files/categories/ via advuploader. Image deletion removes the physical file via storage()->delete().
  9. Ordering: Drag-and-drop reorder via AJAX updates the order column for sibling pages.
  10. Builder vs content: If builder_block_id is set, the builder layout takes precedence over the fulltext field and the page view template.