Building a Custom Theme for Synaptik CMS: A Complete Guide
Most CMS platforms make theming feel like an archaeological dig — you spend more time understanding the framework than actually building. Synaptik CMS takes a different approach: if you know PHP, HTML, and CSS, you already know how to build a theme for it.
This guide walks you through everything from an empty folder to a fully functional, production-ready theme.
What a Theme Actually Is
A Synaptik CMS theme is a folder inside theme/ containing PHP templates, a stylesheet, and a theme.json manifest. That's it. No build tools, no bundlers, no compiled templates. You edit a file, reload the browser, and see the result.
To actually show up as selectable in Admin → Appearance → Theme Manager, a theme folder needs:
theme/my-theme/
├── theme.json
├── header.php
├── footer.php
├── home.php
└── css/style.css
Skip theme.json and the theme still works, just anonymously — no name, no description, no screenshot in the Theme Manager. Skip any of the other four files and the theme won't be listed at all; those four are the real floor.
You don't have to write that floor from scratch. Every install ships with theme/starter/ — a minimal, fully working theme containing exactly this set (plus content-articles.php, content-pages.php, content-projects.php, content-list.php, and 404.php), meant as a clean starting point. Copy the folder, rename it, and you already have a working theme before you've written a line of your own code.
Any template you don't provide beyond that automatically falls back to theme/default/, so you never see a broken page mid-development — building out the rest is incremental, not all-or-nothing.
The Manifest
theme.json identifies your folder as a valid theme and provides metadata for the theme manager:
{
"synaptik_theme": true,
"name": "My Theme",
"folder": "my-theme",
"version": "1.0.0",
"description": "A clean, minimal theme.",
"author": "Your Name",
"author_url": "https://example.com"
}
"synaptik_theme": true is required — it's also what the ZIP installer checks before accepting an upload. folder matters more than it looks: when someone installs your theme from a ZIP, this is the destination folder name it's extracted to, regardless of how the ZIP itself is structured. Set it explicitly, and never set it to default — that name belongs to the bundled fallback theme and isn't protected from being overwritten by a core update.
The Template Cascade
Synaptik CMS resolves each template through a three-level cascade:
theme/child_theme/{active}/{template}.php— a child theme override, if one exists (see below)theme/{active}/{template}.php— your themetheme/default/{template}.php— built-in fallback
The same cascade applies to partials and page templates, not just top-level templates. This means you can start with just a stylesheet and override templates one at a time. Building a theme is incremental, not all-or-nothing.
The templates you'll want to create:
| Template | Renders |
|---|---|
header.php | <!DOCTYPE html> through <main> |
footer.php | Closing tags, search overlay |
home.php | Homepage |
content-articles.php | Single article |
content-pages.php | Static pages |
content-projects.php | Portfolio items |
content-list.php | Article/project list, category and tag pages |
404.php | Not found page |
Child Themes
If you just want to tweak somebody else's theme — a catalogue theme, one you downloaded, one a client picked — editing it directly means every update to that theme overwrites your changes. A child theme avoids this: it's a small folder that sits on top of a parent theme and is never touched by a theme update, because updates only ever replace the parent theme's own folder.
Create one at:
theme/child_theme/{parent-slug}/
{parent-slug} must match the parent theme's folder name exactly — theme/child_theme/myelin/ for the Myelin theme, for instance. Inside, add only what you actually want to change:
- Any template file (
home.php,content-articles.php, a partial, a page template…) — overrides the parent's version outright, same filename, same path. css/style.css— loaded in addition to the parent's stylesheet, right after it, so your rules win without editing the original file.js/script.js— same idea, loaded alongside the parent's script rather than replacing it.functions.php— also loaded in addition to the parent'sfunctions.php(both run), so you can register extra hooks without touching the parent's file.
Everything else — anything you don't add — keeps coming from the parent theme. This is the recommended way to customize a theme you didn't build yourself: your changes survive every update, and you only ever need to understand the one file you're overriding.
A Minimal header.php
The header template has access to $settings, $data, $metaTitle, $metaDescription, $headerScripts, and a few other injected variables. The key call is render_header_scripts() — it handles CSS injection (including any child theme stylesheet), cache-busting, the JS i18n bridge, and your theme's script.js, all automatically.
<!DOCTYPE html>
<html lang="<?php echo htmlspecialchars($settings['site_language'] ?? 'en'); ?>">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?php echo $metaTitle; ?></title>
<?php echo render_meta_tags($settings, $metaTitle, $metaDescription); ?>
<?php echo render_header_scripts($headerScripts); ?>
</head>
<body>
<?php render_adminbar(); ?>
<header class="site-header">
<a href="<?php echo getBaseUrl(); ?>" class="logo">
<?php echo render_site_logo($settings, 'site-logo'); ?>
</a>
<nav><?php echo renderHierarchicalMenu($settings, $data); ?></nav>
</header>
<main>
render_adminbar() is a no-op for visitors — it only outputs the 36px admin toolbar when an admin session is active. renderHierarchicalMenu() emits a bare <ul><li><a> structure with no classes, so it's ready to style however your theme needs.
A Minimal footer.php
</main>
<footer class="site-footer">
<?php echo render_footer_content(); ?>
</footer>
<?php echo render_search_ui(); ?>
</body>
</html>
render_search_ui() must be called before </body>. It generates the search overlay that powers Ctrl+K, whether or not the search icon is visible in the navigation.
Rendering Content
Single-item templates (content-articles.php, etc.) receive a single variable: $item. The full item data — title, content, SEO fields, custom fields, galleries — is all in there.
<article class="single-article">
<?php echo render_featured_image($item); ?>
<?php echo render_content_title($item); ?>
<?php echo render_content_date($item); ?>
<?php echo render_content_category($item); ?>
<div class="prose">
<?php echo render_content_html($item['content'] ?? '', $item); ?>
</div>
<?php echo render_content_tags($item); ?>
<?php echo render_related_items($item); ?>
</article>
One rule worth repeating: never echo $item['content'] directly. Always go through render_content_html() — it handles shortcodes, gallery injection, and Markdown conversion.
Customising Card Layouts with Partials
Article and project cards appear on the homepage, list pages, and in shortcodes. By default the CMS renders a built-in card. To override it, create partials/article-card.php in your theme (or in a child theme, following the same cascade):
<article class="article-card">
<?php if (!empty($article['image'])): ?>
<a href="<?php echo $article_link; ?>" class="card-thumb">
<img src="<?php echo getBaseUrl() . htmlspecialchars($article['image']); ?>"
alt="<?php echo htmlspecialchars($article['title']); ?>">
</a>
<?php endif; ?>
<div class="card-body">
<time><?php echo format_date($article['date'] ?? ''); ?></time>
<h3><a href="<?php echo $article_link; ?>"><?php echo htmlspecialchars($article['title']); ?></a></h3>
<p><?php echo get_article_summary($article); ?></p>
</div>
</article>
The partial has access to $article (index-level data) and $article_link (pre-built URL). No configuration required — the CMS detects and uses the partial automatically.
Hooks and Filters
functions.php at the root of your theme folder is loaded after the CMS core (and again from a child theme's own functions.php, if you have one — see Child Themes above). Use it to register hooks, enqueue additional assets, and set up theme-specific data:
// Enqueue an additional stylesheet
add_theme_stylesheet('css/animations.css');
// Add content before the footer
add_theme_action('before_footer', function() {
echo '<div class="cta-banner">Get in touch →</div>';
});
// Filter footer text
add_theme_filter('footer_text', function($text) {
return $text . ' · Built with Synaptik CMS';
});
This is for building extension points into a theme you're authoring yourself — if you're only customizing somebody else's theme, a child theme is almost always simpler and doesn't require touching PHP at all.
Page Templates
For pages that need a unique layout — a contact page, a landing page, a full-width hero — create a file in page-templates/:
<?php /* Template Name: Landing Page */ ?>
<section class="hero">
<h1><?php echo htmlspecialchars($item['title']); ?></h1>
</section>
<div class="landing-content">
<?php echo render_content_html($item['content'] ?? '', $item); ?>
</div>
The comment on line 1 registers it as a selectable template in the admin editor. Editors can assign it to any page without touching code.
Shipping Your Theme
When the theme is ready, zip it up — the installer locates theme.json inside the archive automatically, so it works whether the ZIP contains a wrapping folder or the files loose at its root. What actually determines the installed folder name is the folder key in theme.json, not the ZIP's internal structure (see The Manifest above). Upload via Admin → Appearance → Import Theme, or drop the folder directly into theme/ on the server.
For a thumbnail in the theme manager, add a preview.jpg, preview.jpeg, preview.png, or preview.webp file at the theme's root — any one of the four is picked up automatically. It's displayed at a 16:9 ratio and cropped to fit, so a widescreen screenshot (e.g. 1200×675px) works best.
What You Don't Have to Think About
- Cache busting: handled automatically via
?v=mtimeon CSS and JS - RSS auto-discovery: injected by
render_header_scripts() - Search overlay: covered by
render_search_ui() - Admin bar offset: use
var(--snk-adminbar-height, 0px)in CSS for fixed headers - SEO tags, Open Graph, JSON-LD schema: all from
render_meta_tags()
Synaptik CMS handles the plumbing. The theme is just the design.
