Skip to content

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 / markdown are the safe authoring formats: their only dynamic mechanism is the shortcode registry (below), so a content editor can't inject code.
  • phtml is trusted — the body is executed as a view template (it can loop a $posts array, call helpers, etc.), so it's for developer-authored pages only.
  • builder is 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:

  1. It looks up a published page whose 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.
  2. If nothing matches and nothing else will dispatch the URL, it checks page_redirect and issues a 301 to the new slug.
  3. 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. resolveBySlug walks a locale cascade: an exact-locale row wins; else the default-locale row answers (so an English-only /why-tiger serves 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_at empty (or in the past).
  • Schedule — status published with a future published_at. The dispatch gate is status = '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 for builder-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