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

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](/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_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](/docs/themes/customization) and the [theme.json schema](/docs/themes/theme-json-schema).

Be the first to know

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