Building a Custom Theme for PolyCMS — Scaffold Guide

Last updated on May 16, 2026 2:38 AM

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.

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>&copy; <?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'
        ]
    ];
});

Available Template Helper Functions

FunctionDescription
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

  1. Create the my-theme/ folder in modules/polycms/themes/.
  2. Add all template files as described above.
  3. Go to Blog → Themes in Perfex CRM.
  4. Find "My Custom Theme" and click Activate.

Best Practices

  1. Always include header.php and footer.php in every template for consistent layout.
  2. Use polycms_do_action('polycms_head') and polycms_do_action('polycms_footer') so plugins can inject assets.
  3. Escape all user-generated content with htmlspecialchars() to prevent XSS.
  4. Apply content filters using polycms_apply_filters('polycms_post_content', $content) so plugins can modify output.
  5. Call polycms_render_admin_bar() in the header so staff members get frontend editing shortcuts.
  6. 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