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 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.

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>&copy; <?= 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 :root CSS variables from customizer fields. See Theme 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_content filter 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 and the theme.json schema.

Be the first to know

New features, release notes & CMS guides — a couple of emails a month, no spam.