Building a Custom Theme for PolyCMS — Scaffold Guide
Overview
PolyCMS themes control the entire frontend appearance and layout of your blog. The theme system uses a template hierarchy inspired by WordPress, where specific templates override generic ones.
This guide provides a complete scaffold to build a custom theme from scratch.
Prerequisites: PHP knowledge, HTML/CSS, and familiarity with the PolyCMS hooks system.
Theme Directory Structure
All themes live in modules/polycms/themes/. Each theme has this structure:
modules/polycms/themes/
└── my-theme/
├── style.css # Theme manifest (required)
├── index.php # Fallback template (required)
├── header.php # Site header
├── footer.php # Site footer
├── single.php # Single post template
├── page.php # Static page template
├── category.php # Category archive template
├── tag.php # Tag archive template
├── archive.php # Generic archive template
├── search.php # Search results template
├── 404.php # Not found page
├── functions.php # Theme functions (optional)
├── assets/
│ ├── css/
│ │ └── theme.css
│ ├── js/
│ │ └── theme.js
│ └── images/
└── language/
└── english/
└── my_theme_lang.php
Template Hierarchy
When PolyCMS renders a page, it looks for templates in this order:
Single Post: single.php → index.php
Static Page: page.php → index.php
Category: category.php → archive.php → index.php
Tag: tag.php → archive.php → index.php
Search: search.php → index.php
Homepage: homepage.php → index.php
404: 404.php → index.php
The first matching template is used. index.php is always the final fallback.
MTBuilder Compatibility in Theme Scaffold
If you want to reuse MTBuilder preset layouts, Template Parts, or custom blocks, design your theme scaffold to be MTBuilder-friendly from day one:
- Keep a full-width page template for landing pages (no restrictive container or forced sidebar).
- Always render page/post content through
polycms_apply_filters('polycms_post_content', $content)so visual blocks can be processed correctly. - Include
polycms_do_action('polycms_head')andpolycms_do_action('polycms_footer')to allow MTBuilder/plugin assets to load. - Avoid global CSS overrides that aggressively target all
a,button,h1-h6,.row, and.col-*selectors, because they can break block-level styles.
Example: Full-width Template for MTBuilder Pages
page-landing.php
<?php defined('BASEPATH') or exit('No direct script access allowed'); ?>
<?php include __DIR__ . '/header.php'; ?>
<main class="site-main landing-main">
<?php
// Keep raw visual layout output intact for MTBuilder pages
echo polycms_apply_filters('polycms_post_content', $page->content);
?>
</main>
<?php include __DIR__ . '/footer.php'; ?>
This layout is ideal when you want to combine a custom theme shell with MTBuilder sections, rows, columns, and reusable template parts.
Step 1: Create style.css (Theme Manifest)
The style.css file at the theme root serves as the manifest. PolyCMS reads the comment header to identify the theme.
style.css
/*
Theme Name: My Custom Theme
Theme URI: https://your-website.com/my-theme
Description: A clean, modern blog theme for PolyCMS.
Version: 1.0.0
Author: Your Name
Author URI: https://your-website.com
Requires PolyCMS: 1.0.0
License: Commercial
*/
/* Theme styles can go here or in assets/css/theme.css */
Step 2: Create header.php
The header template is included at the top of every page.
header.php
<?php defined('BASEPATH') or exit('No direct script access allowed'); ?>
<!DOCTYPE html>
<html lang="<?php echo polycms_get_locale(); ?>">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<?php
// SEO meta tags (auto-generated by PolyCMS)
if (isset($seo_data)) {
echo polycms_render_meta_tags($seo_data);
}
if (isset($og_tags)) {
echo $og_tags;
}
if (isset($twitter_tags)) {
echo $twitter_tags;
}
?>
<!-- Theme CSS -->
<link rel="stylesheet" href="<?php echo polycms_theme_url(); ?>/assets/css/theme.css">
<?php
// Allow plugins to inject into <head>
polycms_do_action('polycms_head');
?>
</head>
<body class="polycms-frontend <?php echo isset($body_class) ? $body_class : ''; ?>">
<?php
// Render admin bar for logged-in staff
polycms_render_admin_bar();
?>
<!-- Navigation -->
<nav class="site-nav">
<div class="container">
<a href="<?php echo polycms_blog_url(); ?>" class="site-logo">
<?php echo polycms_get_option('blog_title', 'My Blog'); ?>
</a>
<div class="nav-menu">
<?php echo polycms_render_menu('primary'); ?>
</div>
</div>
</nav>
Step 3: Create footer.php
footer.php
<?php defined('BASEPATH') or exit('No direct script access allowed'); ?>
<!-- Footer -->
<footer class="site-footer">
<div class="container">
<p>© <?php echo date('Y'); ?> <?php echo polycms_get_option('blog_title', 'My Blog'); ?>. All rights reserved.</p>
</div>
</footer>
<?php
// Allow plugins to inject before </body>
polycms_do_action('polycms_footer');
?>
<!-- Theme JS -->
<script src="<?php echo polycms_theme_url(); ?>/assets/js/theme.js"></script>
</body>
</html>
Step 4: Create index.php (Fallback Template)
index.php
<?php defined('BASEPATH') or exit('No direct script access allowed'); ?>
<?php include __DIR__ . '/header.php'; ?>
<main class="site-main">
<div class="container">
<h1><?php echo $title ?? 'Blog'; ?></h1>
<?php if (!empty($posts)): ?>
<div class="post-grid">
<?php foreach ($posts as $post): ?>
<article class="post-card">
<?php if ($post->featured_image): ?>
<img src="<?php echo $post->featured_image; ?>"
alt="<?php echo htmlspecialchars($post->title); ?>"
class="post-thumbnail">
<?php endif; ?>
<div class="post-card-body">
<h2>
<a href="<?php echo polycms_post_url($post); ?>">
<?php echo htmlspecialchars($post->title); ?>
</a>
</h2>
<div class="post-meta">
<time datetime="<?php echo $post->published_at; ?>">
<?php echo date('M d, Y', strtotime($post->published_at)); ?>
</time>
<span class="reading-time"><?php echo $post->reading_time; ?> min read</span>
</div>
<p><?php echo $post->excerpt; ?></p>
</div>
</article>
<?php endforeach; ?>
</div>
<?php if (isset($pagination)): ?>
<div class="pagination">
<?php echo $pagination; ?>
</div>
<?php endif; ?>
<?php else: ?>
<p>No posts found.</p>
<?php endif; ?>
</div>
</main>
<?php include __DIR__ . '/footer.php'; ?>
Step 5: Create single.php (Single Post Template)
single.php
<?php defined('BASEPATH') or exit('No direct script access allowed'); ?>
<?php include __DIR__ . '/header.php'; ?>
<main class="site-main">
<article class="single-post">
<div class="container">
<!-- Post Header -->
<header class="post-header">
<?php if (!empty($post->categories)): ?>
<div class="post-categories">
<?php foreach ($post->categories as $cat): ?>
<a href="<?php echo polycms_category_url($cat); ?>" class="category-badge">
<?php echo htmlspecialchars($cat->name); ?>
</a>
<?php endforeach; ?>
</div>
<?php endif; ?>
<h1><?php echo htmlspecialchars($post->title); ?></h1>
<div class="post-meta">
<time datetime="<?php echo $post->published_at; ?>">
<?php echo date('F d, Y', strtotime($post->published_at)); ?>
</time>
<span><?php echo $post->reading_time; ?> min read</span>
<span><?php echo $post->views; ?> views</span>
</div>
</header>
<!-- Featured Image -->
<?php if ($post->featured_image): ?>
<div class="featured-image">
<img src="<?php echo $post->featured_image; ?>"
alt="<?php echo htmlspecialchars($post->title); ?>">
</div>
<?php endif; ?>
<!-- Post Content -->
<div class="post-content">
<?php
// Apply content filters (plugins can modify this)
echo polycms_apply_filters('polycms_post_content', $post->content);
?>
</div>
<!-- Tags -->
<?php if (!empty($post->tags)): ?>
<div class="post-tags">
<?php foreach ($post->tags as $tag): ?>
<a href="<?php echo polycms_tag_url($tag); ?>" class="tag">
#<?php echo htmlspecialchars($tag->name); ?>
</a>
<?php endforeach; ?>
</div>
<?php endif; ?>
<!-- Comments -->
<?php if ($post->allow_comments): ?>
<section class="comments-section">
<?php echo polycms_render_comments($post->id); ?>
</section>
<?php endif; ?>
<!-- Related Posts -->
<?php if (!empty($related_posts)): ?>
<section class="related-posts">
<h3>Related Posts</h3>
<div class="post-grid">
<?php foreach ($related_posts as $rp): ?>
<a href="<?php echo polycms_post_url($rp); ?>" class="related-card">
<strong><?php echo htmlspecialchars($rp->title); ?></strong>
</a>
<?php endforeach; ?>
</div>
</section>
<?php endif; ?>
</div>
</article>
</main>
<?php include __DIR__ . '/footer.php'; ?>
Step 6: Create category.php
category.php
<?php defined('BASEPATH') or exit('No direct script access allowed'); ?>
<?php include __DIR__ . '/header.php'; ?>
<main class="site-main">
<div class="container">
<header class="archive-header">
<h1>Category: <?php echo htmlspecialchars($category->name); ?></h1>
<?php if ($category->description): ?>
<p class="archive-description"><?php echo $category->description; ?></p>
<?php endif; ?>
</header>
<?php if (!empty($posts)): ?>
<div class="post-grid">
<?php foreach ($posts as $post): ?>
<article class="post-card">
<h2><a href="<?php echo polycms_post_url($post); ?>"><?php echo htmlspecialchars($post->title); ?></a></h2>
<p><?php echo $post->excerpt; ?></p>
</article>
<?php endforeach; ?>
</div>
<?php if (isset($pagination)) echo $pagination; ?>
<?php else: ?>
<p>No posts found in this category.</p>
<?php endif; ?>
</div>
</main>
<?php include __DIR__ . '/footer.php'; ?>
Theme Options (Optional)
Register custom settings that admins can configure without editing code.
functions.php
<?php
defined('BASEPATH') or exit('No direct script access allowed');
// Register theme options
polycms_add_action('polycms_theme_options', function() {
return [
[
'key' => 'primary_color',
'label' => 'Primary Color',
'type' => 'color',
'default' => '#82b440'
],
[
'key' => 'posts_per_page',
'label' => 'Posts Per Page',
'type' => 'number',
'default' => 10
],
[
'key' => 'show_sidebar',
'label' => 'Show Sidebar',
'type' => 'select',
'options' => ['yes' => 'Yes', 'no' => 'No'],
'default' => 'yes'
]
];
});
Mixing Theme Template Parts with MTBuilder Template Parts
In theme development, "template parts" usually means reusable PHP partials such as:
partials/header-hero.phppartials/cta-banner.phppartials/footer-columns.php
In MTBuilder, Template Parts are reusable visual sections saved from the editor. You can combine both approaches:
- Use theme template parts for stable/global sections (site header shell, legal footer, recurring trust badges).
- Use MTBuilder Template Parts for page-specific marketing sections (hero variants, FAQ blocks, pricing layouts, campaign CTAs).
- Keep responsibilities separated: theme controls global structure; MTBuilder controls conversion-focused visual sections.
If you need custom visual components, extend the MTBuilder block registry from a plugin and consume those blocks inside pages that use your theme. See Building Custom Blocks for MTBuilder.
Available Template Helper Functions
| Function | Description |
|---|---|
polycms_blog_url() | Base URL of the blog |
polycms_post_url($post) | URL to a specific post |
polycms_category_url($cat) | URL to a category archive |
polycms_tag_url($tag) | URL to a tag archive |
polycms_theme_url() | URL to the active theme directory |
polycms_render_menu($location) | Render a navigation menu |
polycms_render_admin_bar() | Render the frontend admin bar |
polycms_render_comments($post_id) | Render the comment section |
polycms_render_meta_tags($seo) | Render SEO meta tags |
polycms_render_widgets($area) | Render widgets for a sidebar area |
polycms_get_option($key, $default) | Get a blog setting |
polycms_apply_filters($hook, $value) | Apply content filters |
polycms_do_action($hook) | Trigger an action hook |
For the complete list, see Template Tags & Helper Functions.
Installation
- Create the
my-theme/folder inmodules/polycms/themes/. - Add all template files as described above.
- Go to Blog → Themes in Perfex CRM.
- Find "My Custom Theme" and click Activate.
Best Practices
- Always include
header.phpandfooter.phpin every template for consistent layout. - Use
polycms_do_action('polycms_head')andpolycms_do_action('polycms_footer')so plugins can inject assets. - Escape all user-generated content with
htmlspecialchars()to prevent XSS. - Apply content filters using
polycms_apply_filters('polycms_post_content', $content)so plugins can modify output. - Call
polycms_render_admin_bar()in the header so staff members get frontend editing shortcuts. - Use responsive CSS — test your theme on mobile, tablet, and desktop.
Need Help Building Your Theme?
If you need assistance creating a custom theme, want help with template hierarchy, styling, or have any design-related questions — don't hesitate to contact us. We provide completely free support to all PolyCMS customers.
Simply leave a comment on the PolyCMS page on CodeCanyon describing what you'd like to achieve, and our team will guide you through the process with code examples, layout advice, or troubleshooting assistance. We're here to help you build exactly the theme you need.
Related Documentation
- Hooks & Filters Reference — All available hooks for theme developers.
- Building a Custom Plugin — Create plugins that work with your theme.
- Building Custom Blocks for MTBuilder — Register custom visual blocks for landing-page use cases.
- MTBuilder Visual Page Builder — Use preset templates and template parts in the editor.
- Theme Management — How admins install and manage themes.
- Widgets & Sidebars — Add widget areas to your theme.
Launch Your CMS for Perfex CRM — Lifetime Access for Just $19
Create blogs, pages, themes, plugins, and manage everything from one powerful unified admin panel.
Early Adopter Advantage
Current pricing reflects the module’s present feature set. As additional plugins, themes, multilingual capabilities, builders (MTBuilder), and ecosystem integrations are released, pricing may be adjusted. Early customers secure today’s price while benefiting from future updates.