Content Pipeline

How Markdown, drivers, caching, and theming turn slugs into rendered pages.

Category: Content Pipeline

High-level flow #

  1. Routing resolves /aboutPageController (see HTTP docs) which calls PageService::load('about').
  2. Content driver (file/db/hybrid/auto) fetches the Page model’s metadata + Markdown.
  3. MarkdownService parses Markdown → HTML and caches it (default 10 minutes).
  4. PageService fires lifecycle events (page.loading, page.loaded, etc.) and caches hydrated pages via CacheDriver + CacheTagManager.
  5. ThemeService hands the Page to your layout, injecting helper functions (asset(), url(), partial()), and builds the final HTML response.

PageService responsibilities #

VelvetCMS\Services\PageService is the orchestrator:

  • Uses the configured ContentDriver for storage. The driver implementation is injected via the container, so swapping drivers is instant.
  • Wraps expensive operations in CacheDriver::remember() with per-page keys (page:about) and invalidates the cache on save/delete.
  • Exposes helpers like list(), published(), drafts(), and exists() which all go through the driver.
  • Publishes lifecycle events that modules can hook:
    • page.loading before hitting storage.
    • page.loaded once a Page object exists.
    • page.saving/page.saved, page.deleting/page.deleted for write operations.
  • Collaborates with CacheTagManager to tag aggregated queries (e.g., pages:list) so any write flushes the relevant tag.

Page model #

VelvetCMS\Models\Page is a lightweight data class:

  • Public promoted properties (slug, title, content, status, layout, meta, timestamps).
  • Page::fromArray() builds instances from driver payloads.
  • setHtml() stores the rendered HTML alongside raw Markdown.
  • Convenience helpers: html(), getExcerpt(), publish(), isPublished(), etc.

MarkdownService #

VelvetCMS\Services\MarkdownService wraps league/commonmark with YAML frontmatter parsing and caching:

  • Reads parser config + extensions from config/content.php (content.markdown).
  • parse() accepts Markdown, an optional TTL, and toggles caching with cache boolean.
  • Emits events markdown.parsing and markdown.parsed so modules can rewrite content or inject shortcodes.
  • parseFrontmatter() splits YAML frontmatter and Markdown body. Drivers reuse it to hydrate metadata.

TemplateEngine & ThemeService #

This stage just hands pages to the theming layer. For directory layout, helpers, and template syntax, see Theme Basics and Template Engine. The TL;DR: ThemeService loads themes/<name>, injects helpers like $asset()/$url(), and caches compiled .velvet.php templates under storage/cache/templates. Modules can call addPath() to contribute their own layouts or partials.

Content drivers #

All drivers conform to VelvetCMS\Contracts\ContentDriver which defines CRUD + list() + count().

Driver Best for Storage Highlights
file Git-centric sites, docs, small marketing sites Markdown files in content/pages YAML frontmatter parsed via MarkdownService. Maintains an index in storage/cache/file-driver-index.json for speedy listings and automatic Markdown → HTML caching.
db Content-heavy CRUD, dashboards pages table via VelvetCMS\Database\Connection Stores Markdown directly in the DB, JSON-encodes meta, supports filters/order/pagination server-side.
hybrid Large catalogs with editorial workflows Metadata in DB + Markdown files Keeps content files for Git diffing but adds indexed metadata for fast queries. Automatically JSON-encodes meta, expects file + DB to stay in sync.
auto Progressive scaling without manual switches Delegates to file or hybrid Reads current page count (DB or file fallback) and flips when content.drivers.auto.threshold is exceeded; re-evaluates after writes.

Configure the driver via CONTENT_DRIVER env or config/content.php. All drivers share validation rules (slug/title required, status must be draft/published).

Caching strategy #

  • Per-page cache: CacheDriver::remember("page:$slug", 300, ...) stores hydrated Page objects.
  • Collection cache: CacheTagManager tags the aggregated list results (pages:list). When any page is saved/deleted, tags are flushed, ensuring fresh listings.
  • Markdown cache: MarkdownService optionally caches rendered HTML keyed by the Markdown hash. TTL defaults to content.markdown.cache_ttl.

Backends for CacheDriver can be file, Redis, etc. Pick them in config/cache.php; the interface requires get/set/has/delete/remember.

Events worth subscribing to #

  • page.loading / page.loaded – instrumentation, audit, on-the-fly metadata.
  • page.saving / page.saved – validations, queueing background tasks.
  • markdown.parsing / markdown.parsed – custom syntax, embed processing.
  • page.rendering / page.rendered – theme-related injections, analytics beacons.

Register listeners inside a module boot() or via a service provider binding the dispatcher.

CLI + automation touch points #

  • velvet page:list, page:create, page:publish, etc., are thin wrappers around PageService; they ensure the same driver + validation as the HTTP layer.
  • velvet cache:clear --tag=pages:list flushes the tagged list cache, great for CI/CD hooks.
  • velvet diagnose checks that the selected driver can read/write (file permissions, DB tables, etc.) and reports Markdown cache health.

Extending the pipeline #

  • Custom drivers: implement ContentDriver, bind it with $app->bind(ContentDriver::class, MyDriver::class) in a module’s register().
  • Shortcodes/embeds: listen to markdown.parsing (raw Markdown) or markdown.parsed (HTML) to run DOM transforms.
  • Theme variables: call $theme->set('featureFlags', [...]) in middleware or controllers, and read via {{ $featureFlags['beta'] }}.
  • Headless use: skip ThemeService and return Page::toArray() or Page::html() from an API controller—nothing in the pipeline forces server-side rendering.

With these building blocks you can own the entire content lifecycle, from Markdown authoring to cached HTML responses and module-level overrides.