This guide explains how to convert existing theme HTML sections into reusable components.
Many themes include pre-built sections (hero areas, feature grids, testimonials, etc.) with hardcoded content. The component system lets you:
Components must be presentation-only. Do NOT create components that require backend functionality to work properly.
Do not create components for:
Newsletter/subscription sections are common in themes. Do not create custom newsletter components — use the built-in newsletter_signup component instead. It renders a functional signup form that posts to existing /list/{slug} or /lists endpoints, handling all subscription logic including user registration, email validation, and anti-spam protection.
When extracting a newsletter section from a theme, replace it with a newsletter_signup component render call:
// Replace theme newsletter HTML with the built-in component
echo ComponentRenderer::render(null, 'newsletter_signup', [
'heading' => 'Subscribe to Newsletter',
'list_mode' => 'default',
'compact_mode' => true,
'button_text' => 'Subscribe',
]);Or create a database instance via the admin interface and render by slug:
echo ComponentRenderer::render('homepage-newsletter');The newsletter_signup component supports:
default (site default list), specific (choose a list), all (show all lists with checkboxes)These are generally safe to create as presentation-only:
newsletter_signup component (see above)Raw HTML theme files are available at /theme-sources/ for reference:
https://[yoursite]/theme-sources/Test components instantly without database setup:
/utils/component_preview - All components for current theme
/utils/component_preview?type=hero - Single component
/utils/component_preview?theme=falcon - Test in specific theme
/utils/component_preview?theme=all - View ALL components across ALL themes
/utils/component_preview?config - Show generated config data
/utils/component_preview?paths - Show template file pathsNote: Requires admin login (permission level 5+).
This utility auto-generates placeholder data based on your config schema, letting you iterate quickly on templates. Each component card shows:
For utility scripts and automated testing, you can access schema defaults programmatically:
require_once(PathHelper::getIncludePath('data/components_class.php'));
$component_type = new Component($type_id, TRUE);
// Get only fields with defaults defined
$defaults = $component_type->get_default_config();
// Get all fields (empty values for those without defaults)
$all_fields = $component_type->get_default_config(true);This is useful for:
Find a section in your theme that should be configurable. For example, a hero section:
<!-- From theme's index.php or landing page -->
<section class="hero-section bg-primary text-white py-5">
<div class="container text-center">
<h1>Welcome to Our Site</h1>
<p class="lead">We help businesses grow with innovative solutions.</p>
<a href="/contact" class="btn btn-light btn-lg">Get Started</a>
</div>
</section>List what admins should be able to change:
| Element | Field Name | Field Type |
|---|---|---|
| "Welcome to Our Site" | heading | textinput |
| Subtitle text | subheading | textarea |
| "Get Started" | button_text | textinput |
| Button URL | button_url | textinput |
| Background color | background_color | textinput (hex) |
| Text alignment | alignment | dropinput |
Build the JSON schema for the component type. CRITICAL: Add default values that exactly match the original theme HTML. This ensures:
<!-- Original theme HTML -->
<section class="hero-section bg-primary text-white py-5">
<div class="container text-center">
<h1>Welcome to Our Site</h1>
<p class="lead">We help businesses grow with innovative solutions.</p>
<a href="/contact" class="btn btn-light btn-lg">Get Started</a>
</div>
</section>JSON schema with defaults matching the reference:
{
"fields": [
{
"name": "heading",
"label": "Heading",
"type": "textinput",
"help": "Main headline text",
"default": "Welcome to Our Site"
},
{
"name": "subheading",
"label": "Subheading",
"type": "textarea",
"help": "Supporting text below headline",
"default": "We help businesses grow with innovative solutions."
},
{
"name": "button_text",
"label": "Button Text",
"type": "textinput",
"default": "Get Started"
},
{
"name": "button_url",
"label": "Button URL",
"type": "textinput",
"default": "/contact"
},
{
"name": "background_color",
"label": "Background Color",
"type": "textinput",
"help": "Hex color code, e.g., #007bff",
"default": "#007bff"
},
{
"name": "alignment",
"label": "Text Alignment",
"type": "dropinput",
"default": "center",
"options": {
"left": "Left",
"center": "Center",
"right": "Right"
}
}
]
}Rule: Every field should have a default that makes the component render identically to the reference HTML. This includes:
/theme/themename/assets/img/...<img src="assets/img/home-one/main-blog-img/1.jpg" alt="...">Becomes this default in JSON (with full theme path):
{"name": "image", "type": "textinput", "default": "/theme/linka-reference/assets/images/home-one/main-blog-img/1.jpg"}Create /views/components/hero_simple.php:
<?php
/**
* Simple Hero Component
*
* Extracted from theme landing page.
*/
// Get config values with defaults
$heading = $component_config['heading'] ?? 'Welcome';
$subheading = $component_config['subheading'] ?? '';
$button_text = $component_config['button_text'] ?? '';
$button_url = $component_config['button_url'] ?? '#';
$background_color = $component_config['background_color'] ?? '#007bff';
$alignment = $component_config['alignment'] ?? 'center';
// Build alignment class
$align_class = 'text-' . $alignment;
?>
<section class="hero-section py-5 <?= htmlspecialchars($align_class) ?>" style="background-color: <?= htmlspecialchars($background_color) ?>; color: #fff;">
<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-light btn-lg">
<?= htmlspecialchars($button_text) ?>
</a>
<?php endif; ?>
</div>
</section>Create a JSON file alongside your template:
File: /views/components/hero_simple.json
{
"title": "Simple Hero",
"description": "Extracted from theme landing page.",
"category": "hero",
"css_framework": "bootstrap",
"config_schema": {
"fields": [
{"name": "heading", "label": "Heading", "type": "textinput", "help": "Main headline text", "default": "Welcome to Our Site"},
{"name": "subheading", "label": "Subheading", "type": "textarea", "default": "We help businesses grow with innovative solutions."},
{"name": "button_text", "label": "Button Text", "type": "textinput", "default": "Get Started"},
{"name": "button_url", "label": "Button URL", "type": "textinput", "default": "/contact"},
{"name": "background_color", "label": "Background Color", "type": "textinput", "help": "Hex color code, e.g., #007bff", "default": "#007bff"},
{
"name": "alignment",
"label": "Text Alignment",
"type": "dropinput",
"default": "center",
"options": {"left": "Left", "center": "Center", "right": "Right"}
}
]
}
}Note: Every field has a default that matches the original theme HTML exactly. The component preview will now render identically to the reference.
The component type is automatically discovered during theme sync. JSON files are the single source of truth - component types cannot be created via the admin interface.
IMPORTANT: On development servers, newly created files may have restrictive permissions that prevent the web server from reading them. After creating component files, ensure proper permissions:
chmod 666 /path/to/theme/views/components/your_component.php
chmod 666 /path/to/theme/views/components/your_component.jsonIf the component preview shows "Template file not found" but the file exists, this is almost always a permissions issue.
/admin/admin_components or a specific page's edit viewBy slug (standalone):
echo ComponentRenderer::render('homepage-hero');By slug with overrides:
echo ComponentRenderer::render('homepage-hero', null, ['heading' => 'Custom Title']);By type key (programmatic, no database instance):
// Useful for components rendered from code with runtime data
echo ComponentRenderer::render(null, 'image_gallery', [
'photos' => $entity->get_photos(),
'primary_file_id' => $entity->get('evt_fil_file_id'),
]);Automatic (page-attached):
Components attached to a page render automatically via Page::get_filled_content(). Page-attached components typically have no slug — they are rendered using ComponentRenderer::render_component($instance), not render('slug').
| Item | Convention | Example |
|---|---|---|
| Type Key | lowercase_snake_case | feature_grid |
| Template File | Same as type key + .php | feature_grid.php |
| Field Names | lowercase_snake_case | button_text |
<?= htmlspecialchars($heading) ?> $columns = $component_config['columns'] ?? '3'; <?php if ($subtitle): ?>
<p><?= htmlspecialchars($subtitle) ?></p>
<?php endif; ?><section> for page sections
- <article> for self-contained content
- Proper heading hierarchy<div class="container"> as normal. Components own their internal padding (e.g., py-4); the layout system owns external margin between components via the Vertical Margin control. Admins can override width, height, and margin per instance without template changes. If a component type needs to manage its own layout entirely, set "skip_wrapper": true in the type's layout_defaults JSON — the renderer will skip auto-wrapping and the template can use $container_class, $container_style, and $max_height_style variables directly. {"label": "Call-to-Action Button Text"}
Not:
{"label": "CTA"} {"help": "Enter a hex color code like #ff5733"} {
"type": "dropinput",
"options": {"small": "Small", "medium": "Medium", "large": "Large"}
} {"name": "heading", "type": "textinput", "default": "The City of London Wants to Have It"},
{"name": "columns", "type": "dropinput", "default": "3", "options": {...}},
{"name": "background_color", "type": "textinput", "default": "#1a1a2e"}
- REQUIRED: Every field must have a default that matches the original theme
- Extract exact text, colors, and values from the reference HTML
- The component preview must render identically to the theme source
- For images, use actual theme asset paths: /theme/themename/assets/img/... {"name": "icon_color", "type": "colorpicker", "default": "#007bff", "advanced": true}Fields that users rarely need to change can be marked as "advanced": true. These fields are hidden behind a collapsible "Show advanced fields" link, keeping the form cleaner.
Add "advanced": true to any field definition:
{
"fields": [
{"name": "heading", "label": "Heading", "type": "textinput"},
{"name": "subheading", "label": "Subheading", "type": "textarea"},
{"name": "background_color", "label": "Background Color", "type": "colorpicker", "advanced": true},
{"name": "text_alignment", "label": "Alignment", "type": "dropinput", "advanced": true, "options": {...}}
]
}Repeater sub-fields can also be marked as advanced. They appear in a nested collapsible section within each repeater row:
{
"name": "features",
"label": "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},
{"name": "link_text", "label": "Link Text", "type": "textinput", "advanced": true}
]
}| Typically Regular (shown by default) | Typically Advanced (hidden by default) |
|---|---|
| Headings and titles | Colors (background, text, accent) |
| Body text and descriptions | Icon classes and icon styles |
| Primary images | CSS classes and custom styles |
| Button text | Alignment and layout options |
| Main URLs/links | Animation settings |
| Repeater content items | Spacing and padding options |
| Enable/disable toggles | SEO fields (aria labels, etc.) |
| Slugs and internal identifiers |
For sections with multiple items (features, testimonials, etc.):
Theme HTML:
<div class="row">
<div class="col-md-4">
<i class="bx bx-check"></i>
<h4>Feature One</h4>
<p>Description of feature one.</p>
</div>
<div class="col-md-4">
<i class="bx bx-star"></i>
<h4>Feature Two</h4>
<p>Description of feature two.</p>
</div>
<!-- More items... -->
</div>Schema:
{
"fields": [
{
"name": "features",
"label": "Features",
"type": "repeater",
"fields": [
{"name": "icon", "label": "Icon Class", "type": "textinput"},
{"name": "title", "label": "Title", "type": "textinput"},
{"name": "description", "label": "Description", "type": "textarea"}
]
},
{
"name": "columns",
"label": "Columns",
"type": "dropinput",
"default": "3",
"options": {"2": "2", "3": "3", "4": "4"}
}
]
}Note: Repeater fields don't support defaults for their nested content, but the parent repeater will start empty. Defaults work well for simple fields like the columns dropdown above.
Template:
<?php
$features = $component_config['features'] ?? [];
$columns = intval($component_config['columns'] ?? 3);
$col_class = 'col-md-' . (12 / $columns);
?>
<section class="features-section py-5">
<div class="container">
<div class="row">
<?php foreach ($features as $feature): ?>
<div class="<?= $col_class ?>">
<?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>
<?php endforeach; ?>
</div>
</div>
</section>For components that need database data (recent posts, events, etc.):
Create /logic/components/recent_posts_logic.php:
<?php
/**
* Recent Posts Logic Function
*/
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'),
'image' => $post->get('post_image')
];
}
return ['posts' => $result];
}Add logic_function to the component's JSON file:
{
"title": "Recent Posts",
"logic_function": "recent_posts_logic",
"config_schema": { ... }
}ThemeManager syncs this to com_logic_function during theme sync.
<?php
$posts = $component_data['posts'] ?? [];
foreach ($posts as $post): ?>
<article>
<h3><a href="<?= htmlspecialchars($post['url']) ?>">
<?= htmlspecialchars($post['title']) ?>
</a></h3>
<p><?= htmlspecialchars($post['excerpt']) ?></p>
</article>
<?php endforeach; ?>Component templates follow the standard theme override:
/theme/{active_theme}/views/components/{template}.php (checked first)/views/components/{template}.php (fallback)Themes can include their own exclusive components that only work with that theme.
/theme/{theme_name}/views/components/theme_hero.json
/theme/{theme_name}/views/components/theme_hero.php{
"title": "Theme Hero",
"description": "Hero section specific to this theme",
"category": "hero",
"css_framework": "bootstrap",
"config_schema": {
"fields": [
{"name": "heading", "label": "Heading", "type": "textinput"}
]
}
}The css_framework field controls when a component is available:
| Value | Behavior |
|---|---|
"bootstrap" | Only active when a Bootstrap theme is used |
"tailwind" | Only active when a Tailwind theme is used |
| (omitted) | Universal - works with any theme |
"css_framework": "bootstrap" in /views/components/css_frameworkcss_framework (e.g., Custom HTML)Component types are synchronized automatically when:
/theme-sources/ for reference)/views/components/ or /theme/{theme}/views/components/default values for EVERY field matching the reference HTML exactlychmod 666) if on development server/utils/component_preview?type=your_component?config to see generated data)?theme= parameter