Pages
Content in Tiger is data, not files. A page is a row in the page store, rendered live and edited from the admin — publishing is a row update, not a deploy. The same table backs the site's public pages, the chrome that wraps them, and the reusable fragments they're built from.
Three primitives, one store
Every content row is one of three kinds, set by its type column (Tiger_Model_Page):
type |
What it is | Routed by | Rendered |
|---|---|---|---|
page |
a routable page (Home, About, a landing page) | its slug (/about) |
on its own, optionally wrapped in a layout |
layout |
the chrome around a page (header/footer/nav shell) | a stable page_key |
wraps a page's body via |
partial |
a reusable fragment (a hero, a CTA, a section) | a page_key |
pulled in with |
A page names the layout that wraps it via its layout_key (pointing at a layout row's page_key); the layout emits the page body wherever it places . Layouts and partials aren't publish-gated — they're infrastructure, fetched whenever something references them. See Layouts & partials for composition.
There's a fourth type,
block— a builder-only library fragment placed by copy (its HTML is inlined into a page, detached from the source), as opposed to a partial placed by reference. Blocks are covered under the visual builder; they're never resolved at render time.
Body formats
Each row declares how its body is rendered, via the format column. Tiger_Cms_Renderer turns the body into HTML accordingly:
format |
Rendered as | Safe for untrusted editors? |
|---|---|---|
html |
output as-is, then the shortcode pass | yes |
markdown |
Parsedown → HTML, then the shortcode pass | yes |
phtml |
rendered as a Zend_View template with view vars + all helpers |
no — trusted code |
builder |
GrapesJS output (self-contained <style> + markup), rendered like html |
yes (<script> is stripped on save) |
html/markdownare the safe authoring formats: their only dynamic mechanism is the shortcode registry (below), so a content editor can't inject code.phtmlis trusted — the body is executed as a view template (it can loop a$postsarray, call helpers, etc.), so it's for developer-authored pages only.builderis the visual builder's output; it stays safe because any<script>is stripped when the design is saved. See The visual builder.
Shortcodes
For the safe formats, dynamic bits are [shortcode]s — a registry of named handlers substituted at render time. Built-ins include , , and ; a module can register its own. Full details in Shortcodes.
Slug dispatch & 301 redirects
Public URLs reach CMS pages through a front-controller plugin, Tiger_Controller_Plugin_PageDispatch, that runs after routing. For any URL no real controller claims:
- It looks up a published
pagewhose slug matches (Tiger_Model_Page::resolveBySlug). A match wins — even over a shipped route, so you can replace a built-in landing page (/vibe) with editable CMS content just by giving a page that slug. - If nothing matches and nothing else will dispatch the URL, it checks
page_redirectand issues a 301 to the new slug. - Otherwise the request is left untouched → a clean 404.
Reserved namespaces are never CMS-claimable: every registered module prefix (/admin, /api, /docs, /pay, …) plus the core system controllers, built fresh from the live module list each request. The site root / is left to the admin-chosen home page (set in Content settings — see below), not to slug dispatch.
Redirects are automatic. When you change a published page's slug, Tiger_Model_Page::save() records a page_redirect from the old slug to the new one, so existing links and SEO survive. Reclaiming a slug clears any stale redirect from it, so redirects can't loop.
Per-org cascade & per-language rows
The store is multi-tenant and localized, and both resolve as a cascade (the same live-override philosophy as config and translations — the more specific row wins):
- Per-org. Content is scoped by
org_id. A lookup reads[<current org>, '']and a tenant's own row wins over the shared global ('') one (ORDER BY org_id DESC). So one install can serve different content per tenant, falling back to shared content where a tenant hasn't overridden it. A blank org resolves to the site's own org (Tiger_Model_Org::siteOrgId()). - Per-language. Each page is one row per locale.
resolveBySlugwalks a locale cascade: an exact-locale row wins; else the default-locale row answers (so an English-only/why-tigerserves every language rather than 404ing — English content under a translated shell); else a locale-neutral ('') row. Locales are language-only (en,es).
Publish now / schedule
A page's status is draft, published, or archived — only published rows are served publicly. Combined with the published_at timestamp, that gives scheduling for free:
- Publish now — status
published,published_atempty (or in the past). - Schedule — status
publishedwith a futurepublished_at. The dispatch gate isstatus = 'published' AND (published_at IS NULL OR published_at <= NOW()), so the page goes live automatically when its time arrives — no separate workflow, no cron. In the editor a scheduled page is flagged in the content list until it's live.
Versioning & restore
Every save snapshots the result. Tiger_Model_Page::save() writes an immutable page_version row (title, body, format, meta, status) with an incrementing version number, so the latest version always mirrors the live page. You can restore any prior version — restoreVersion() copies that version's content back onto the page (which itself snapshots as a new version, so nothing is lost). See Versioning.
Soft-delete
Deleting a page is a soft-delete — the row is flagged (deleted = 1), not dropped, and finders exclude it by default (activeSelect()). It's recoverable. This is the standard Tiger_Model_Table behavior every domain table shares.
Authoring
Pages are managed in the first-party cms module, in the admin shell:
- Content list (
/cms/page) — a server-side DataTables grid (search, per-column sort, status/type filters) of every page, layout, and partial. - Page editor (
/cms/page/edit) — create/edit, choose type + format + layout, set status +published_at, edit SEO (meta_description) and raw<head>/ end-of-body injection, and view + restore the version history. - Visual builder (
/cms/page/design) — the full-screen GrapesJS drag-drop editor forbuilder-format pages (The visual builder).
All writes go through the /api service Cms_Service_Page (validate → transaction), never a page-POST. The model's save() is itself transactional — it writes the row, snapshots the version, and records any slug redirect in one transaction.
Content settings
An admin Content settings screen (/cms/settings) sets the site name and the home page (which CMS page is served at /, else the built-in landing). These are stored in the config table (live-override, per-org-capable) — no separate settings table.
See also
- Layouts & partials — composition, "everything is a labeled partial"
- Shortcodes — the registry and built-ins
- The visual builder — GrapesJS drag-drop authoring
- Versioning — snapshots, restore, scheduled publish
- Menus · Theming