Creating a Theme
A jekcms theme is a plain PHP + CSS folder. No build step, no framework, no boilerplate generator — just files in the right places. This page walks through the minimum setup for a working theme.
Minimum directory layout
themes/my-theme/
theme.json
templates/
header.php
footer.php
index.php
single.php
assets/
css/style.css
Drop this into /themes/ and jekcms picks it up on the next admin page load. See [Installing a theme](/docs/themes/installing-a-theme) for how discovery works.
Step 1 — theme.json
Start with the smallest possible manifest:
{
"name": "My Theme",
"slug": "my-theme",
"version": "0.1.0",
"author": "Your Name",
"license": "free"
}
slug must match the folder name. For the full schema (including customizer.tabs), see the [theme.json reference](/docs/themes/theme-json-schema).
Step 2 — header.php and footer.php
These wrap every page. Keep them minimal at first.
templates/header.php:
<!doctype html>
<html lang="<?= htmlspecialchars(site_option('language', 'en')) ?>">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title><?= htmlspecialchars(render_page_title()) ?></title>
<?php output_robots_meta(); ?>
<?php output_theme_customization_css(); ?>
<link rel="stylesheet" href="<?= theme_asset('css/style.css') ?>">
<?php do_action('wp_head'); ?>
</head>
<body>
<header class="site-header">
<a href="/"><?= htmlspecialchars(site_option('site_name')) ?></a>
</header>
<main class="site-main">
templates/footer.php:
</main>
<footer class="site-footer">
<p>© <?= date('Y') ?> <?= htmlspecialchars(site_option('site_name')) ?></p>
</footer>
<?php do_action('wp_footer'); ?>
</body>
</html>
Hook points you should always include
do_action('wp_head')— plugins, SEO modules, analytics snippets inject here. Required for integration compatibility.do_action('wp_footer')— late-load scripts and cookie banners hook here.output_robots_meta()— emits the<meta name="robots">tag based on site settings and post-level overrides.output_theme_customization_css()— emits the:rootCSS variables from customizer fields. See [Theme customization](/docs/themes/customization).
Step 3 — index.php (list/archive)
Used for the homepage, category pages, tag pages, and search results.
<?php include __DIR__ . '/header.php'; ?>
<h1><?= htmlspecialchars(render_archive_title()) ?></h1>
<div class="post-list">
<?php foreach ($GLOBALS['posts'] as $post): ?>
<?php include __DIR__ . '/partials/post-card.php'; ?>
<?php endforeach; ?>
</div>
<?php render_pagination(); ?>
<?php include __DIR__ . '/footer.php'; ?>
$GLOBALS['posts'] is the array of posts for the current query — the router populates it before including your template.
Step 4 — single.php (post detail)
<?php include __DIR__ . '/header.php'; ?>
<article class="post">
<h1><?= htmlspecialchars($GLOBALS['post']['title']) ?></h1>
<p class="post-meta">
By <?= htmlspecialchars($GLOBALS['post']['author_name']) ?>
on <?= date('M j, Y', strtotime($GLOBALS['post']['published_at'])) ?>
</p>
<div class="post-content">
<?php render_content(); ?>
</div>
</article>
<?php include __DIR__ . '/footer.php'; ?>
render_content() vs raw $post['content']
Always use render_content() inside single.php. It:
- Resolves shortcodes
- Runs the
the_contentfilter chain (plugins hook here) - Handles lazy-loaded media
- Applies reading-time and related-post injections
Printing $post['content'] directly skips all of that.
Step 5 — partials pattern
For any chunk you reuse — post cards, navigation, comment blocks — put it in partials/:
templates/
partials/
post-card.php
nav-primary.php
comments.php
Include them with <?php include __DIR__ . '/partials/post-card.php'; ?>. The partial inherits the caller's scope, so $post inside a foreach loop is visible to post-card.php without explicit passing.
Template variables at a glance
| Variable | Available in | Contains | |---|---|---| | $GLOBALS['post'] | single.php, partials | Current post array: id, title, slug, content, excerpt, author_id, author_name, published_at, featured_image | | $GLOBALS['posts'] | index.php, archive templates | Array of post arrays for the current page | | $GLOBALS['pagination'] | archive templates | current_page, total_pages, per_page | | $GLOBALS['query_type'] | all templates | single / archive / category / tag / search / 404 |
Next steps
Once your theme renders, expose customizer tabs so site admins can tweak colors and fonts without editing files. See [Theme customization](/docs/themes/customization) and the [theme.json schema](/docs/themes/theme-json-schema).