Page Component System

The Page Component System enables composing pages from reusable, configurable content blocks. Components combine a template, optional logic function, and admin-configurable settings.


Table of Contents

  1. Overview
  2. ComponentRenderer
  3. Component Types
  4. Component Instances
  5. Creating Templates
  6. Logic Functions
  7. Config Schema Reference
  8. Admin Interface

1. Overview

Key Concepts

  • Component Type - A template definition with config schema (e.g., "Hero Section", "Feature Grid")
  • Component Instance - A configured use of a component type with specific content
  • Config Schema - JSON definition of editable fields for a component type
  • ComponentRenderer - The class that renders component instances

Architecture

Component Type (template + schema)
        ↓
Component Instance (configured content)
        ↓
ComponentRenderer (renders output)
        ↓
HTML Output

Three Ways to Use Components

  1. Page-Attached - Automatically rendered with page content via pag_component_layout on the Page record (most common — no slug needed)
  2. Standalone (Slug-Based) - Explicitly rendered in a template via ComponentRenderer::render('slug')
  3. Programmatic - Rendered from PHP code by type key, no database instance needed

2. ComponentRenderer

The ComponentRenderer class handles all component rendering. It's available globally (loaded by PathHelper).

Render by Slug

// In any template or view file
echo ComponentRenderer::render('homepage-hero');

// Conditional rendering
if (ComponentRenderer::exists('sidebar-promo')) {
    echo ComponentRenderer::render('sidebar-promo');
}

Render by Slug with Config Overrides

// Override specific config values at render time
echo ComponentRenderer::render('homepage-hero', null, ['heading' => 'Custom Title']);

The $overrides array is merged on top of the database-stored config, so overrides win.

Render by Type Key (Programmatic)

Render a component template directly by its type key, without needing a database instance. Useful for reusable UI patterns called from PHP code with runtime data.

// Render image gallery with runtime data — no database component instance needed
echo ComponentRenderer::render(null, 'image_gallery', [
    'photos' => $post->get_photos(),
    'primary_file_id' => $post->get('pst_fil_file_id'),
]);

render() signature: render($slug, $type_key = null, $overrides = [])

ParameterTypeDescription
$slugstring\nullComponent slug (loads from database)
$type_keystring\nullComponent type key (renders directly, no database instance)
$overridesarrayConfig values — merged with database config (slug mode) or used as full config (type_key mode)
Behavior:
  • render('slug') — Loads from database by slug
  • render('slug', null, $overrides) — Loads from database, merges overrides into config
  • render(null, 'type_key', $config) — Renders template by type key with provided config
When rendering by type key:
  • $component_config = the config array passed by the caller
  • $component = null (no PageContent instance)
  • Layout wrapper is skipped (the calling view controls layout)

Render a Pre-Loaded Instance

When you already have a PageContent object (e.g. iterating components loaded for a page), use render_component() instead of render(). Components created through the page editor typically have no slug, so passing pac_location_name to render() would silently return empty output.

// Correct: render a loaded PageContent instance directly
echo ComponentRenderer::render_component($component);

// With config overrides
echo ComponentRenderer::render_component($component, ['heading' => 'Override']);

render_component() signature: render_component($component_instance, $overrides = [])

Render a Page's Components

Pages store an ordered array of pac_page_content_id values in Page::pag_component_layout. Page::get_filled_content() reads that array, loads the referenced PageContent rows in one query, and renders them in order via render_component(). If the layout is empty, the page renders pag_body directly.

// In a view:
echo $page->get_filled_content();   // reads pag_component_layout, renders in order

Components are cross-page reusable — the same PageContent row can appear in multiple pages' layout arrays (edit once, render many places). Use the drag-reorder picker on the admin page edit surface to manage a page's layout.

Render Multiple by Slug

// Render several components in sequence
echo ComponentRenderer::render_multiple(['hero', 'features', 'testimonials']);

Check if Renderable

// Returns true only if: exists, is a component, and is not deleted
if (ComponentRenderer::exists('promo-banner')) {
    echo ComponentRenderer::render('promo-banner');
}

Debug Output

When debug setting is enabled, ComponentRenderer outputs HTML comments explaining why a component didn't render:

<!-- ComponentRenderer (slug: missing-component): Component not found -->
<!-- ComponentRenderer (slug: deleted-hero): Component exists but is deleted -->
<!-- ComponentRenderer (slug: old-widget): Component type 'legacy_widget' is inactive -->


3. Component Types

Component types are the library of available components. Managed by superadmins at /admin/admin_component_types.

Database: com_components

FieldTypeDescription
com_component_idint8Primary key
com_type_keyvarchar(64)Unique identifier (e.g., hero_static)
com_titlevarchar(255)Display name
com_descriptiontextDescription for admins
com_categoryvarchar(64)Grouping category
com_template_filevarchar(255)Template path (relative)
com_config_schemajsonField definitions
com_logic_functionvarchar(255)Optional logic function name
com_is_activeboolWhether type is available
com_requires_pluginvarchar(64)Required plugin name
com_css_frameworkvarchar(32)Required CSS framework (e.g., bootstrap)

Categories

Built-in categories (from Component::get_categories()):

KeyLabel
heroHero Sections
contentContent Blocks
featuresFeatures & Benefits
mediaMedia & Images
testimonialsTestimonials & Social Proof
dynamicDynamic Content
conversionCTAs & Conversion
layoutLayout & Spacing
customCustom & Freeform

Core Component Types

Built-in component types available in /views/components/:

Type KeyCategoryDescriptionFramework
hero_staticheroHero section with heading, subheading, background, CTABootstrap
feature_gridfeaturesGrid of icon + title + description itemsBootstrap
cta_bannerconversionFull-width call-to-action bannerBootstrap
list_signupconversionNewsletter/mailing list signup with logic functionBootstrap
custom_htmlcustomRaw HTML for advanced usersNone
page_titlelayoutPage title with optional breadcrumbsBootstrap
image_gallerymediaImage gallery (programmatic rendering)Bootstrap
text_blockcontentHeading with rich text contentHTML5
text_with_imagecontentText alongside image with flexbox layoutHTML5
accordioncontentCollapsible sections using <details>/<summary>HTML5
tabscontentTabbed content with ARIA markupHTML5
video_embedmediaResponsive YouTube/Vimeo embedHTML5
spacerlayoutVertical spacing between componentsHTML5
dividerlayoutHorizontal divider lineHTML5
Themes can add additional component types prefixed with the theme name (e.g., linka_featured_card). These are discovered automatically during theme sync.

Creating a Component Type

Method 1: JSON Definition Files (Recommended)

Create a JSON file paired with the PHP template:

/views/components/hero_static.json    # Definition file
/views/components/hero_static.php     # Template file

The JSON file defines the component metadata and config schema:

{
  "title": "Hero Static",
  "description": "Single hero section with heading, subheading, background, and CTA",
  "category": "hero",
  "css_framework": "bootstrap",
  "config_schema": {
    "fields": [
      {"name": "heading", "label": "Heading", "type": "textinput"},
      {"name": "subheading", "label": "Subheading", "type": "textarea"}
    ]
  }
}

JSON fields:

FieldRequiredDescription
titleYesDisplay name
descriptionNoDescription for admins
categoryNoGrouping category (see categories table above)
css_frameworkNoRequired framework: bootstrap, tailwind, or omit/null for universal
logic_functionNoName of logic function (see Logic Functions)
layout_defaultsNoDefault layout settings (see Layout Controls)
config_schemaYesField definitions object
Component types are automatically discovered during theme sync operations. The JSON file is the single source of truth - component types cannot be created or edited via the admin interface.

CSS Framework Compatibility

Components can specify framework requirements:

css_framework ValueBehavior
bootstrapOnly active when Bootstrap theme is used
tailwindOnly active when Tailwind theme is used
(omitted/null)Universal - works with any theme
When a theme is activated, components incompatible with its framework are deactivated. Components become active again if a compatible theme is activated.


4. Component Instances

Component instances are configured uses of component types. Stored in pac_page_contents table.

Database Fields (Component-Specific)

FieldTypeDescription
pac_page_content_idint8Primary key
pac_com_component_idint4FK to component type
pac_location_namevarchar(255)Slug for explicit rendering
pac_titlevarchar(255)Admin label
pac_configjsonConfigured values
pac_max_widthvarchar(50)Layout: max width CSS value (e.g., 720px)
pac_max_heightvarchar(50)Layout: max height CSS value
pac_vertical_marginvarchar(20)Layout: vertical margin keyword (none, sm, md, lg, xl)
> Note: Components are cross-page reusable. Page membership and display order are managed by pag_component_layout — a JSON array of pac_page_content_id values on the Page record, not by fields on the component itself.

Loading Components

require_once(PathHelper::getIncludePath('data/page_contents_class.php'));

// By slug
$component = PageContent::get_by_slug('homepage-hero');

// By ID
$component = new PageContent($id, TRUE);

// Check if it's a component (vs legacy content)
if ($component->is_component()) {
    $type = $component->get_component_type();
    $config = $component->get_config();
}

Querying Components

require_once(PathHelper::getIncludePath('data/page_contents_class.php'));

// All components (not legacy content)
$components = new MultiPageContent(
    ['components_only' => true, 'deleted' => false]
);
$components->load();

// Components attached to a specific page — load via the Page record
require_once(PathHelper::getIncludePath('data/pages_class.php'));
$page = new Page($page_id, TRUE);
$rendered = $page->get_filled_content();   // renders layout components in order
$layout_ids = $page->get_component_layout(); // returns array of pac_page_content_id values


5. Creating Templates

Templates are PHP files in views/components/ that render component output.

File Location

/views/components/hero_static.php       # Base template
/theme/{theme}/views/components/hero_static.php  # Theme override

Templates follow the standard theme override chain via PathHelper::getThemeFilePath().

Available Variables

Inside a component template, these variables are available:

VariableTypeDescription
$component_configarrayThe configured values from admin
$component_dataarrayData from logic function (if any)
$componentPageContentThe component instance object
$component_type_recordComponentThe component type definition
$component_slugstringThe component's slug
$container_classstringCSS class for layout container (e.g., "container")
$container_stylestringInline style for container width (e.g., "", "max-width:720px")
$max_height_stylestringInline style for max height (e.g., "", "max-height:400px;overflow:hidden")
> Layout Controls: Container width and max height are controlled automatically by the renderer via a wrapper div with CSS custom properties. Most templates don't need to use the layout variables above -- they exist for component types that opt out of auto-wrapping via skip_wrapper: true in their layout_defaults.

Basic Template Example

<?php
/**
 * Hero Static Component
 *
 * A simple hero section with heading, subheading, and CTA button.
 */

$heading = $component_config['heading'] ?? 'Welcome';
$subheading = $component_config['subheading'] ?? '';
$button_text = $component_config['button_text'] ?? '';
$button_url = $component_config['button_url'] ?? '#';
$background_image = $component_config['background_image'] ?? '';
?>

<section class="hero-section" <?php if ($background_image): ?>style="background-image: url('<?= htmlspecialchars($background_image) ?>')"<?php endif; ?>>
    <div class="container">
        <h1><?= htmlspecialchars($heading) ?></h1>

        <?php if ($subheading): ?>
            <p class="lead"><?= htmlspecialchars($subheading) ?></p>
        <?php endif; ?>

        <?php if ($button_text): ?>
            <a href="<?= htmlspecialchars($button_url) ?>" class="btn btn-primary btn-lg">
                <?= htmlspecialchars($button_text) ?>
            </a>
        <?php endif; ?>
    </div>
</section>

Template with Repeater Data

<?php
/**
 * Feature Grid Component
 *
 * Displays a grid of features with icons.
 */

$title = $component_config['title'] ?? 'Features';
$features = $component_config['features'] ?? [];
$columns = $component_config['columns'] ?? 3;
?>

<section class="feature-grid">
    <div class="container">
        <h2 class="text-center mb-4"><?= htmlspecialchars($title) ?></h2>

        <div class="row">
            <?php foreach ($features as $feature): ?>
                <div class="col-md-<?= 12 / intval($columns) ?>">
                    <div class="feature-item text-center">
                        <?php if (!empty($feature['icon'])): ?>
                            <i class="<?= htmlspecialchars($feature['icon']) ?> fa-3x mb-3"></i>
                        <?php endif; ?>

                        <h4><?= htmlspecialchars($feature['title'] ?? '') ?></h4>
                        <p><?= htmlspecialchars($feature['description'] ?? '') ?></p>
                    </div>
                </div>
            <?php endforeach; ?>
        </div>
    </div>
</section>

Template with Dynamic Data

<?php
/**
 * Recent Posts Component
 *
 * Displays recent blog posts. Uses logic function for data.
 */

$title = $component_config['title'] ?? 'Recent Posts';
$posts = $component_data['posts'] ?? [];  // From logic function
?>

<section class="recent-posts">
    <div class="container">
        <h2><?= htmlspecialchars($title) ?></h2>

        <?php if (empty($posts)): ?>
            <p class="text-muted">No posts available.</p>
        <?php else: ?>
            <div class="row">
                <?php foreach ($posts as $post): ?>
                    <div class="col-md-4">
                        <article class="post-card">
                            <h3><a href="<?= htmlspecialchars($post['url']) ?>">
                                <?= htmlspecialchars($post['title']) ?>
                            </a></h3>
                            <p><?= htmlspecialchars($post['excerpt']) ?></p>
                        </article>
                    </div>
                <?php endforeach; ?>
            </div>
        <?php endif; ?>
    </div>
</section>


6. Logic Functions

Logic functions provide dynamic data to component templates. They're optional - static components don't need them.

File Location

/logic/components/{function_name}.php

The file must define a function with the same name as the file.

Function Signature

<?php
/**
 * Recent Posts Logic Function
 *
 * @param array $config The component's configuration values
 * @return array Data to pass to template as $component_data
 */
function recent_posts_logic($config) {
    require_once(PathHelper::getIncludePath('data/posts_class.php'));

    $limit = intval($config['post_count'] ?? 3);

    $posts = new MultiPost(
        ['published' => true, 'deleted' => false],
        ['post_date' => 'DESC'],
        $limit
    );
    $posts->load();

    $result = [];
    foreach ($posts as $post) {
        $result[] = [
            'title' => $post->get('post_title'),
            'excerpt' => $post->get('post_excerpt'),
            'url' => $post->get_url(),
            'date' => $post->get('post_date')
        ];
    }

    return ['posts' => $result];
}

Configuring Logic Function

In the component type's JSON definition, set the logic_function field:

{
  "title": "Newsletter Signup",
  "logic_function": "newsletter_signup_logic",
  "config_schema": { ... }
}

ThemeManager syncs this value to com_logic_function during theme sync. The corresponding file logic/components/newsletter_signup_logic.php must exist.

Real-World Example: Newsletter Signup

The newsletter_signup component demonstrates a logic function that loads data from existing models, checks user state, and returns everything the template needs:

function newsletter_signup_logic($config) {
    // Check if feature is enabled
    $settings = Globalvars::get_instance();
    if (!$settings->get_setting('mailing_lists_active')) {
        return ['is_active' => false];
    }

    // Load mailing list(s) based on config
    require_once(PathHelper::getIncludePath('data/mailing_lists_class.php'));
    $list_mode = $config['list_mode'] ?? 'default';

    // ... resolve lists, check subscriptions, build form action ...

    return [
        'is_active' => true,
        'mailing_lists' => $mailing_lists,
        'form_action' => $form_action,
        'session' => $session,
        'user_subscribed_list' => $user_subscribed_list,
        // ...
    ];
}

Key pattern: the logic function returns data — it does not handle form submission. The newsletter form posts to the existing /list/{slug} or /lists endpoints, reusing all existing subscription logic.

Error Handling

If a logic function throws an exception, ComponentRenderer catches it and returns debug output (when debug mode is enabled). The component will not render.


7. Config Schema Reference

The config schema is a JSON object defining the admin form fields for a component type.

Basic Structure

{
    "fields": [
        {
            "name": "field_name",
            "label": "Field Label",
            "type": "textinput",
            "help": "Optional help text",
            "default": "Default value"
        }
    ]
}

Field Properties

PropertyRequiredDescription
nameYesUnique field identifier (lowercase_snake_case)
labelYesDisplay label in admin form
typeYesField type (see table below)
helpNoHelp text shown below field
defaultNoDefault value for new component instances
optionsFor dropinput/radioinputKey-value pairs for selection fields
fieldsFor repeaterNested field definitions
advancedNoIf true, field is hidden behind "Show advanced fields" toggle
placeholderNoExample text shown in empty text/number fields
requiredNoIf true, field must have a value to save. Adds * to label.
minFor numberinput/repeaterMinimum value (numberinput) or minimum rows (repeater)
maxFor numberinput/repeaterMaximum value (numberinput) or maximum rows (repeater)
stepFor numberinputStep increment for number input
item_labelFor repeaterLabel for each row (e.g., "Feature" → "Feature 1", "Feature 2")

Default Values

The default property pre-populates fields when creating new component instances in the admin interface. This improves the admin experience by showing sensible starting values.

{
  "fields": [
    {"name": "columns", "label": "Columns", "type": "dropinput", "default": "3",
     "options": {"2": "2 Columns", "3": "3 Columns", "4": "4 Columns"}},
    {"name": "alignment", "label": "Alignment", "type": "dropinput", "default": "center",
     "options": {"left": "Left", "center": "Center", "right": "Right"}},
    {"name": "show_button", "label": "Show Button", "type": "checkboxinput", "default": true}
  ]
}

Important: Default values are applied in the admin form when creating new components. They are NOT applied at render time - templates should still use ?? fallbacks for robustness.

Advanced Fields

Fields that users rarely need to change can be marked as "advanced": true. These fields are hidden behind a collapsible "Show advanced fields" link in the admin form, keeping the interface cleaner.

{
  "fields": [
    {"name": "heading", "label": "Heading", "type": "textinput"},
    {"name": "subheading", "label": "Subheading", "type": "textarea"},
    {"name": "background_color", "label": "Background", "type": "colorpicker", "advanced": true},
    {"name": "text_alignment", "label": "Alignment", "type": "dropinput", "advanced": true,
     "options": {"left": "Left", "center": "Center", "right": "Right"}}
  ]
}

Repeater sub-fields can also be marked as advanced. They appear in a nested collapsible section within each repeater row:

{
  "name": "features",
  "type": "repeater",
  "fields": [
    {"name": "title", "label": "Title", "type": "textinput"},
    {"name": "description", "label": "Description", "type": "textarea"},
    {"name": "link_url", "label": "Link URL", "type": "textinput", "advanced": true}
  ]
}

Guidelines for advanced fields:

  • Mark styling options (colors, alignment, spacing) as advanced
  • Keep content fields (text, images, main links) as regular
  • If a field has a sensible default that works 80%+ of the time, consider marking it advanced

Field Validation

The required property prevents saving components with empty essential fields:

{
  "fields": [
    {"name": "heading", "label": "Heading", "type": "textinput", "required": true},
    {"name": "columns", "label": "Columns", "type": "numberinput", "min": 1, "max": 6, "step": 1, "default": 3}
  ]
}

How validation works:

  • required adds an HTML required attribute for client-side browser validation plus server-side validation before save
  • Required fields display * after the label
  • numberinput min/max/step provide browser-native range validation via HTML5
  • Validation errors prevent save and re-display the form with entered values preserved
  • required on a repeater means at least one item must exist
  • required on a checkbox is ignored (false is a valid state)

Repeater Options

Repeaters support item_label, min, and max properties for better admin UX:

{
  "name": "features",
  "label": "Features",
  "type": "repeater",
  "item_label": "Feature",
  "min": 1,
  "max": 12,
  "fields": [
    {"name": "title", "label": "Title", "type": "textinput", "required": true, "placeholder": "Feature name"},
    {"name": "description", "label": "Description", "type": "textarea"},
    {"name": "count", "label": "Count", "type": "numberinput", "min": 0, "max": 100}
  ]
}

PropertyDescription
item_labelLabel for each row (e.g., "Feature 1", "Feature 2"). Omit for no label.
minMinimum number of rows. Pre-populates empty rows on new instances. Disables remove at limit.
maxMaximum number of rows. Disables add button at limit.
Sub-fields within repeaters support the same schema properties as top-level fields: helptext, default, placeholder, required, advanced, and type-specific options like min/max/step for numberinput.

Accessing Defaults Programmatically

For testing and utility scripts, the Component class provides a method to extract defaults:

$component_type = new Component($type_id, TRUE);

// Get only fields that have defaults defined
$defaults = $component_type->get_default_config();
// Returns: ['columns' => '3', 'alignment' => 'center', 'show_button' => true]

// Get all fields (empty values for fields without defaults)
$all_fields = $component_type->get_default_config(true);
// Returns: ['columns' => '3', 'alignment' => 'center', 'show_button' => true, 'heading' => '', 'features' => []]

Available Field Types

TypeDescriptionExtra Options
textinputSingle-line text-
textareaMulti-line text-
textboxAlias for textarea-
richtextWYSIWYG editor (Trumbowyg)-
checkboxinputBoolean checkbox-
dropinputDropdown selectoptions
radioinputRadio buttonsoptions
dateinputDate picker-
timeinputTime picker-
datetimeinputDate and time-
fileinputFile upload-
imageinputImage upload-
imageselectorImage picker with gallerybutton_text, grid_columns, etc.
numberinputNumeric input with constraintsmin, max, step
colorpickerColor picker with theme swatchesmax_swatches, sort, etc.
hiddeninputHidden field-
repeaterRepeatable field groupfields, item_label, min, max

Field Options

{
    "name": "button_style",
    "label": "Button Style",
    "type": "dropinput",
    "help": "Choose the button appearance",
    "options": {
        "primary": "Primary (Blue)",
        "secondary": "Secondary (Gray)",
        "success": "Success (Green)",
        "danger": "Danger (Red)"
    }
}

Repeater Fields

{
    "name": "features",
    "label": "Features",
    "type": "repeater",
    "help": "Add feature items",
    "fields": [
        {"name": "icon", "label": "Icon Class", "type": "textinput"},
        {"name": "title", "label": "Title", "type": "textinput"},
        {"name": "description", "label": "Description", "type": "textarea"}
    ]
}

Complete Schema Example

{
    "fields": [
        {
            "name": "heading",
            "label": "Heading",
            "type": "textinput",
            "help": "Main heading text"
        },
        {
            "name": "subheading",
            "label": "Subheading",
            "type": "textarea",
            "help": "Supporting text below heading"
        },
        {
            "name": "show_button",
            "label": "Show CTA Button",
            "type": "checkboxinput"
        },
        {
            "name": "button_text",
            "label": "Button Text",
            "type": "textinput"
        },
        {
            "name": "button_url",
            "label": "Button URL",
            "type": "textinput"
        },
        {
            "name": "button_style",
            "label": "Button Style",
            "type": "dropinput",
            "options": {
                "primary": "Primary",
                "secondary": "Secondary",
                "outline-primary": "Outline"
            }
        },
        {
            "name": "features",
            "label": "Feature List",
            "type": "repeater",
            "fields": [
                {"name": "title", "label": "Title", "type": "textinput"},
                {"name": "description", "label": "Description", "type": "textarea"}
            ]
        }
    ]
}


8. Admin Interface

Component Types (Superadmin)

URL: /admin/admin_component_types

  • List all active component types (read-only)
  • View component type details
  • Component types are defined by JSON files and synced automatically
Permission Level: 10 (Superadmin)

Note: Component types cannot be created, edited, or deleted via the admin interface. The JSON definition files are the single source of truth. To add or modify component types, edit the JSON files in /views/components/ or /theme/{theme}/views/components/.

Component Instances (Admin)

URL: /admin/admin_components

  • List all component instances
  • Add, edit, delete instances
Permission Level: 5 (Admin)

Components on Page Edit

URL: /admin/admin_page?pag_page_id=X

The page edit view includes a Components card showing:

  • Components attached to this page (sourced from pag_component_layout)
  • Quick add/edit/delete actions
  • Ordered by position in pag_component_layout — drag-reorder picker to rearrange

Component Instance Edit

URL: /admin/admin_component_edit?pac_page_content_id=X

Dynamic form based on component type:

  1. Select component type (triggers page reload to show fields)
  2. Configure component-specific fields from schema
  3. Save returns to appropriate list (page or components)
Page membership and display order are managed from the page edit surface (/admin/admin_page), not from the component edit form.


Layout Controls

Component instances have per-instance layout controls: Width, Height, and Vertical Margin. These appear in the advanced fields section of the admin component edit form.

Spacing Principle

Components and the layout system have distinct responsibilities:

  • Components own padding — Internal spacing within the component (e.g., py-4, p-3) is the template's responsibility
  • The layout system owns margin — External spacing between components is controlled by the layout wrapper via the Vertical Margin field
This separation keeps templates focused on their content while giving admins consistent control over how components are spaced on a page.

How It Works

Layout values are stored on the component instance (pac_max_width, pac_max_height, pac_vertical_margin). When any layout value is set, ComponentRenderer wraps the template output in a lightweight <div class="component-layout"> with CSS custom properties and data attributes:

<div class="component-layout" data-maxw data-vmargin="md" style="--cl-max-width: 720px">
    <!-- Template output unchanged -->
    <section class="hero-static">
        <div class="container">...</div>
    </section>
</div>

When all values are NULL/default, no wrapper is added — zero impact on existing pages.

Width and Height

Width and Height are plain text inputs in the advanced fields section. Empty = no restriction (NULL). Any CSS value (e.g., 720px, 80%) is stored directly.

Vertical Margin

Vertical Margin is a dropdown that controls the space above and below the component. It applies the same margin to both top and bottom. There are no side margin controls — side margins are handled by the component template's container.

KeywordValueTypical Use
Default(none)Uses the component type's preferred spacing from layout_defaults
None0No vertical margin
Small1rem (16px)Tight spacing between related components
Medium2rem (32px)Standard spacing between sections
Large3rem (48px)Generous spacing (matches Bootstrap py-5)
Extra Large5rem (80px)Maximum spacing for visual separation
These values use rem units, which are standard CSS and work with any framework or plain HTML5.

Layout Defaults

Component types can specify default layout values in their JSON definition. These pre-fill the admin fields when creating a new component instance:

{
  "title": "Newsletter Signup",
  "layout_defaults": {
    "container_width": "600px",
    "vertical_margin": "md"
  },
  "config_schema": { ... }
}

layout_defaults KeyDescription
container_widthPre-fills Width field
container_heightPre-fills Height field
vertical_marginPre-fills Vertical Margin dropdown (keyword)
skip_wrapperOpts out of the layout wrapper entirely (see below)

Developer Opt-Out (skip_wrapper)

Component types that need full control over their own layout can set skip_wrapper: true in their layout_defaults. This skips the auto-wrapper and hides the layout fields in the admin form:

{
  "title": "Custom Widget",
  "layout_defaults": {
    "skip_wrapper": true
  },
  "config_schema": { ... }
}


Quick Reference

Render a Component

// By slug (from database — slug must be set)
echo ComponentRenderer::render('my-component-slug');

// By slug with config overrides
echo ComponentRenderer::render('my-component-slug', null, ['heading' => 'Override']);

// By type key (programmatic, no database instance)
echo ComponentRenderer::render(null, 'image_gallery', ['photos' => $photos]);

// From a loaded PageContent instance (use this when iterating page components)
echo ComponentRenderer::render_component($component_instance);

Check Before Rendering

if (ComponentRenderer::exists('optional-component')) {
    echo ComponentRenderer::render('optional-component');
}

Get Config in Template

$title = $component_config['title'] ?? 'Default Title';
$items = $component_config['items'] ?? [];

Access Dynamic Data

$posts = $component_data['posts'] ?? [];

Create Logic Function

File: /logic/components/my_logic.php

function my_logic($config) {
    // Fetch/compute data
    return ['key' => $value];
}


See Also