menu_book Navigation menu

Rendering (Pages Controllers & Templates)

Preparing Data (The Controller)

Unlike heavy MVC frameworks, Pragma CMS uses a pragmatic "Page Controller" pattern. A Page Controller is a single PHP file responsible for the entire lifecycle of a specific route (e.g., your blog index).

A Page Controller is a single PHP file mapped to a route (e.g. /blog).

It is responsible for orchestrating the entire request lifecycle.

It does not represent the page itself, but controls how the page is built and rendered.

Responsibilities of a Page Controller

  • Fetches data from Managers (business/domain logic)
  • Prepares and structures data in the $page object
  • Configures page-level settings (SEO, breadcrumbs, layout)
  • Selects and renders templates (views)
  • Returns the final HTML response

This approach avoids unnecessary abstraction layers and provides a clear, linear execution flow that is easy to understand and maintain.

To pass data to the template (the view), you use the $page object.

Example: A Blog List Controller (pages/blog/index.php)

PHP
<?php

// Retrieve the current route handle from the page context (set by router.php)
$routeHandle = $page->currentRoute["handle"];

// Fetch the page configuration/data associated with this route
$pageData = PageManager::getPageDataForRoute($routeHandle);

// If no page is found, display a 404 error and stop execution
if (!$pageData) {
    displayError(getTranslation("The page does not exist"), 404);
    exit;
}

// Get the content type handle associated with this page (e.g. "article", "recipe")
$contentTypeHandle = $pageData["page_content_type_handle"];

// Retrieve the full content type configuration (fields, taxonomies, etc.)
$contentType = ContentTypeManager::get($contentTypeHandle);

// If the content type does not exist, throw a server error
if (!$contentType) {
    displayError("Content Type '{$contentTypeHandle}' not found.", 500);
    exit;
}

// Get the current language ID from session
$langId = $_SESSION["lang_id"];

// Handle only GET requests (page display & filtering)
if ($_SERVER['REQUEST_METHOD'] === 'GET') {

    // ---------------------------------------------------------
    // 1. Load available taxonomies for filtering
    // ---------------------------------------------------------
    $taxonomies = [];

    // Loop through all taxonomies defined for this content type
    foreach ($contentType["taxonomies"] ?? [] as $taxonomyType) {
        // Retrieve taxonomy metadata (translated name, slug, etc.)
        $taxonomyData = TaxonomyManager::getTaxonomyByType($taxonomyType, $langId);

        if ($taxonomyData) {
            $taxonomies[] = $taxonomyData;
        }
    }

    // ---------------------------------------------------------
    // 2. Retrieve taxonomy filters from URL
    // Example: ?terms[]=1&terms[]=5
    // ---------------------------------------------------------
    $termIds = array_filter($_GET['terms'] ?? [], 'is_numeric');

    // Options that will be passed to the query builder
    $options = [];

    if (!empty($termIds)) {

        // Recursive query that retrieves all descendants (children, grandchildren, etc.)
        // for a set of term IDs, regardless of taxonomy structure.
        $allTermIds = TaxonomyTermManager::getTermIdsWithAllDescendants($termIds);

        if ($allTermIds) {

            // Build dynamic JOIN + parameters for filtering entries
            $options = [
                'joins' => [
                    // Join the term_relationships table to filter entries
                    "INNER JOIN term_relationships tr
                        ON e.entry_id = tr.object_id
                       AND tr.object_type = '{$contentTypeHandle}'"
                ],
                'params' => [
                    // Pass all relevant term IDs for filtering
                    'terms' => $allTermIds
                ]
            ];
        }
    }

    // ---------------------------------------------------------
    // 3. Retrieve entries (automatically hydrated with taxonomies)
    // ---------------------------------------------------------
    $articles = EntryManager::getEntries("article", $langId, $options);

    // ---------------------------------------------------------
    // 4. Mark selected terms in taxonomies (for UI state)
    // ---------------------------------------------------------

    // Retrieve full data for selected terms
    $selectedTerms = !empty($termIds)
        ? TaxonomyTermManager::getTermsByIds($termIds, $langId)
        : [];

    // Attach selected terms to their corresponding taxonomy
    foreach ($taxonomies as &$taxonomy) {
        $taxonomy['selected_terms'] = array_filter(
            $selectedTerms,
            fn($term) => $term['taxonomy_type'] === $taxonomy['type']
        );
    }
    unset($taxonomy); // break reference

    // ---------------------------------------------------------
    // 5. Configure page rendering
    // ---------------------------------------------------------

    $page->viewData = [
        // Page title (fallback if not defined)
        'title' => $pageData["title"] ?? getTranslation("articles"),

        // Filtered entries
        'articles' => $articles,

        // Taxonomies with selected state
        'taxonomies' => $taxonomies,

        // Boolean used in UI to indicate active filters
        'has_active_filters' => !empty($termIds)
    ];

    // SEO metadata
    $page->metaTitle = $pageData["meta_title"] ?? $page->viewData["title"];
    $page->metaDescription = $pageData["meta_description"] ?? "";

    // Load custom fields attached to the page entry
    $page->entry["fields"] = EntryManager::getEntryFields($pageData["page_id"], $langId);

    // Build layout components
    $page->header = renderHeader($page);
    $page->footer = renderFooter($page);

    // Render the main template for this page
    $page->main = render_template($pageData["template_path"], $page);

    // Render final layout (base template)
    echo render_template("base.php", $page);
}

Rendering Data (The View)

Templates are responsible only for displaying data.

info
Pragma CMS integrates Twig natively for those who prefer template engines. However, you can use Native PHP. If using PHP, you must be vigilant about security by manually escaping variables using htmlspecialchars().

METHOD A: Native PHP

PHP
<?= 
// Render breadcrumb navigation for current page context
BreadcrumbManager::renderBreadcrumbs(); 
?>

<section class="cms-hero-container">
    <div class="hero-title">
        <div class="container">
            <?php // Render main page title from entry fields ?>
            <h1><?= htmlspecialchars($page->entry["fields"]["hero-title"]); ?></h1>

            <?php // Render page subtitle from entry fields ?>
            <h2><?= htmlspecialchars($page->entry["fields"]["hero-subtitle"]); ?></h2>
        </div>
    </div>

    <div class="hero-image">
        <?php // Render hero image using media helper (optimized size: xlarge) ?>
        <?php renderImage($page->entry["fields"]["hero-image"]["id"], 'xlarge'); ?>

        <?php // Visual blur overlay for hero image styling ?>
        <div class="blur"></div>
    </div>
    
</section>
        
<section>
    <div class="container">

        <?php // Display filter form only if taxonomies are available ?>
        <?php if(!empty($page->viewData['taxonomies'])): ?>
            <form method="GET" id="articles-filter-form" class="filter-form">

                <?php // Loop through available taxonomies for filtering ?>
                <?php foreach($page->viewData['taxonomies'] as $taxonomy): ?>
                    <div class="flex-column items-start gap-s">

                        <?php // Display taxonomy label ?>
                        <label class="form-label">
                            <?= htmlspecialchars($taxonomy["name"]); ?>
                        </label>

                        <?php // Render reusable relation field (taxonomy terms selector) ?>
                        <?= 
                        UI::renderRelationField(
                            baseInputName:'terms',
                            currentValue:$taxonomy['selected_terms'] ?? [],
                            options:[
                                // API endpoint for loading taxonomy terms
                                'api' => 'api/taxonomies/' . $taxonomy['type'] . '/terms',
                            ],
                            isMultiple:true,
                            langId:$_SESSION['lang_id'] ?? null,
                            idKey:'term_id',
                            nameKey:'name',
                            listType:'tree-dropdown'
                        ); 
                        ?>

                    </div>
                <?php endforeach; ?>

                <?php // Submit filter form ?>
                <button class="self-center"><?= getTranslation("Search"); ?></button>
            </form>
        <?php endif; ?>

        <?php // Display articles grid if results exist ?>
        <?php if (!empty($page->viewData['articles'])) : ?>
            <div class="grid-container">

                <?php // Loop through articles collection ?>
                <?php foreach($page->viewData['articles'] as $article) : ?>
                    <div class="cms-card grid-item-3-col">

                        <div class="card-image">

                            <?php // Link to article detail page ?>
                            <a href="<?= getLink("front.blog.show", ["slug" => htmlspecialchars($article["slug"])]); ?>">

                                <?php // Render article thumbnail image ?>
                                <?php renderImage($article["image"]['id'], attributes:["class" => "image-4-3"]); ?>

                            </a>
                        </div>

                        <div class="card-content">
                            <div class="card-header">

                                <?php // Render article taxonomy terms if available ?>
                                <?php if (!empty($article['terms'])): ?>
                                    <?php foreach ($article["terms"] as $term) : ?>

                                        <?php // Link to filtered taxonomy term page ?>
                                        <a href="<?= getLink("front.taxonomies.terms.show", ["taxonomy_slug" => htmlspecialchars($term["taxonomy_slug"]), "term_slug" => htmlspecialchars($term["term_slug"])]); ?>" class="term-link">
                                            <?= htmlspecialchars($term["name"]) ?>
                                        </a>

                                    <?php endforeach; ?>
                                <?php endif; ?>

                                <?php // Article title linking to detail page ?>
                                <h3>
                                    <a href="<?= getLink("front.blog.show", ["slug" => htmlspecialchars($article["slug"])]); ?>">
                                        <?= htmlspecialchars($article["title"]); ?>
                                    </a>
                                </h3>

                            </div>

                            <?php // Article short description preview ?>
                            <div class="description">
                                <?= htmlspecialchars(truncateText($article["description"], 100)); ?>
                            </div>

                            <?php // Article publication date formatted for display and machine readability ?>
                            <time datetime="<?= date('Y-m-d\TH:i:s', strtotime($article['created_at'])); ?>">
                                <?= date('d/m/Y', strtotime($article['created_at'])); ?>
                            </time>

                        </div>
                    </div>
                <?php endforeach; ?>

            </div>
        <?php else : ?>

            <?php // Empty state depending on filter context ?>
            <?php if ($page->viewData['has_active_filters']) : ?>
                <p><?= getTranslation("No result"); ?></p>
            <?php else : ?>
                <p><?= getTranslation("No articles found yet"); ?>.</p>
            <?php endif; ?>

        <?php endif; ?>

        <?php // Show reset filter button only when filters are active ?>
        <?php if ($page->viewData['has_active_filters']) : ?>
            <a href="<?= getLink("front.blog.index") ?>" class="button-primary mt-s self-start">
                <?= getTranslation("Reset search"); ?>
            </a>
        <?php endif; ?>

    </div>

</section>

METHOD B: TWIG

PHP
{# Render breadcrumb navigation for current page context #}
{{ BreadcrumbManager.renderBreadcrumbs() }}

<section class="cms-hero-container">
    <div class="hero-title">
        <div class="container">

            {# Render main page title from entry fields #}
            <h1>{{ page.entry.fields['hero-title']|e }}</h1>

            {# Render page subtitle from entry fields #}
            <h2>{{ page.entry.fields['hero-subtitle']|e }}</h2>

        </div>
    </div>

    <div class="hero-image">

        {# Render hero image using media helper (optimized size: xlarge) #}
        {{ renderImage(page.entry.fields['hero-image'].id, 'xlarge') }}

        {# Visual blur overlay for hero image styling #}
        <div class="blur"></div>

    </div>
</section>

<section>
    <div class="container">

        {# Display filter form only if taxonomies are available #}
        {% if page.viewData.taxonomies is not empty %}

            <form method="GET" id="articles-filter-form" class="filter-form">

                {# Loop through available taxonomies for filtering #}
                {% for taxonomy in page.viewData.taxonomies %}

                    <div class="flex-column items-start gap-s">

                        {# Display taxonomy label #}
                        <label class="form-label">
                            {{ taxonomy.name|e }}
                        </label>

                        {# Render reusable relation field (taxonomy terms selector) #}
                        {{
                            UI.renderRelationField(
                                'terms',
                                taxonomy.selected_terms ?? [],
                                {
                                    api: 'api/taxonomies/' ~ taxonomy.type ~ '/terms'
                                },
                                true,
                                session.lang_id ?? null,
                                'term_id',
                                'name',
                                'tree-dropdown'
                            )
                        }}

                    </div>

                {% endfor %}

                {# Submit filter form #}
                <button class="self-center">
                    {{ 'Search'|trans }}
                </button>

            </form>

        {% endif %}

        {# Display articles grid if results exist #}
        {% if page.viewData.articles is not empty %}

            <div class="grid-container">

                {# Loop through articles collection #}
                {% for article in page.viewData.articles %}

                    <div class="cms-card grid-item-3-col">

                        <div class="card-image">

                            {# Link to article detail page #}
                            <a href="{{ getLink('front.blog.show', { slug: article.slug|e }) }}">

                                {# Render article thumbnail image #}
                                {{ renderImage(article.image.id, { class: 'image-4-3' }) }}

                            </a>

                        </div>

                        <div class="card-content">

                            <div class="card-header">

                                {# Render article taxonomy terms if available #}
                                {% if article.terms is not empty %}

                                    {% for term in article.terms %}

                                        {# Link to filtered taxonomy term page #}
                                        <a
                                            href="{{ getLink('front.taxonomies.terms.show', {
                                                taxonomy_slug: term.taxonomy_slug|e,
                                                term_slug: term.term_slug|e
                                            }) }}"
                                            class="term-link"
                                        >
                                            {{ term.name|e }}
                                        </a>

                                    {% endfor %}

                                {% endif %}

                                {# Article title linking to detail page #}
                                <h3>
                                    <a href="{{ getLink('front.blog.show', { slug: article.slug|e }) }}">
                                        {{ article.title|e }}
                                    </a>
                                </h3>

                            </div>

                            {# Article short description preview #}
                            <div class="description">
                                {{ article.description|truncateText(100)|e }}
                            </div>

                            {# Article publication date #}
                            <time datetime="{{ article.created_at|date('c') }}">
                                {{ article.created_at|date('d/m/Y') }}
                            </time>

                        </div>
                    </div>

                {% endfor %}

            </div>

        {% else %}

            {# Empty state depending on filter context #}
            {% if page.viewData.has_active_filters %}

                <p>{{ 'No result'|trans }}</p>

            {% else %}

                <p>{{ 'No articles found yet'|trans }}.</p>

            {% endif %}

        {% endif %}

        {# Show reset filter button only when filters are active #}
        {% if page.viewData.has_active_filters %}

            <a href="{{ getLink('front.blog.index') }}" class="button-primary mt-s self-start">
                {{ 'Reset search'|trans }}
            </a>

        {% endif %}

    </div>
</section>

Note: In Twig, the |e or |escape filter is highly recommended to secure HTML output.

Template Resolution

To render a template, use render_template("filename.php", $page).

The CMS will use find_template_file() to locate the correct file based on the fallback logic explained earlier. The overarching HTML structure (like <head> and <body>) is typically handled by base.php, which wraps the output of your specific templates.

Taxonomy & Term Hierarchy

Pragma CMS uses a "most-specific-first" logic for taxonomies. This allows you to override designs at the global, taxonomy, or even individual term level.

For Taxonomy Index (List of all terms, e.g., /blog-categories):

  1. taxonomy/{taxonomy_handle}.php
  2. taxonomy/list.php (Generic fallback)

For a Specific Term (List of entries, e.g., /blog-categories/technology):

The system checks for the following files in your theme in order:

  1. taxonomy/{taxonomy_handle}/{term_slug}.php — Example: taxonomy/blog_category/technology.php
  2. taxonomy/{taxonomy_handle}/single.php — Example: taxonomy/blog_category/single.php
  3. taxonomy/single.php — The universal fallback
lightbulb
Pro Tip
This hierarchy means you can have a general layout for all your tags, a specific grid for your blog categories, and a unique "Landing Page" style for one specific high-traffic category.