jekcms Theme Development: From HTML to PHP

Turning a static HTML design into a jekcms theme: the folder layout, the header template, a reusable post card, the core template functions and the checks to run before going live.

jekcms Theme Development: From HTML to PHP, jekcms blog cover

Turning a static HTML design into a jekcms theme: the folder layout, the header template, a reusable post card, the core template functions and the checks to run before going live.

Starting Point: The HTML Design

The easy way to start a theme is to work in plain HTML and CSS first. Building the homepage, post page, archive and 404 page as static files and getting the look right before moving to PHP lets you change the design as much as you like without dealing with database queries.

These HTML pages map directly to theme templates: index.html becomes the homepage template, post.html the post template, category.html the archive template and 404.html the not-found page.

Folder Layout

All bundled themes use the same layout. The shortest route is to copy one of them and start from there:

themes/mytheme/
+-- theme.json         # name, version, colours, fonts
+-- functions.php      # theme-specific helpers
+-- screenshot.png
+-- templates/
|   +-- header.php, footer.php
|   +-- index.php      # homepage
|   +-- single.php     # post
|   +-- page.php       # page
|   +-- archive.php    # category and tag archives
|   +-- search.php, 404.php
+-- partials/
|   +-- post-card.php, pagination.php, sidebar.php, comments.php
+-- sections/          # blocks arranged in the layout editor
+-- assets/css, assets/js

There are no separate files for category and tag pages; both open with archive.php. Repeated pieces live in partials/ and are called from a template with get_template_part('post-card').

The Header Template

The <head> section of the HTML moves into templates/header.php. You do not write SEO tags, Open Graph, structured data or analytics code by hand; core functions print them. A shortened example:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title><?= e($pageTitle ?? get_setting('general', 'site_name')) ?></title>

    <?php output_og_tags($post ?? null); ?>
    <?php output_schema($post ?? null); ?>
    <?php output_robots_meta($post ?? null); ?>

    <link rel="stylesheet" href="<?= theme_url('assets/css/style.css') ?>">
    <?php output_theme_customization_css(); ?>
    <?php output_head_scripts(); ?>
    <?php output_favicon_tags(); ?>
</head>

output_theme_customization_css() adds the colours and fonts chosen in the theme customiser. output_head_scripts() prints the analytics and verification codes entered in the panel.

Template Functions

These functions replace the hard-coded content of the HTML:

  • get_setting('general', 'site_name'): reads site settings
  • get_posts(['limit' => 6, 'category' => 'news']): fetches posts with filters
  • get_categories(): lists categories
  • get_featured_image($post, 'medium'): returns the featured image URL
  • get_featured_picture(): prints a picture tag with AVIF and WebP sources and a srcset
  • theme_url('assets/...'): returns the address of a file in the theme folder
  • format_date($date): formats a date for the site language
  • e($text): prints text safely into HTML

get_posts() returns plain PHP arrays rather than objects; in a template you read $post['title'].

The Post Card

The post card is used on the homepage, in archives, in search and in the sidebar. Writing it as a partial means there is one file to edit when it needs to change:

<?php // partials/post-card.php
$img = get_featured_image($post, 'medium'); ?>
<article class="post-card">
    <?php if ($img): ?>
    <a href="<?= SITE_URL ?>/<?= e($post['slug']) ?>" class="card-image">
        <img src="<?= e($img) ?>" alt="<?= e($post['title']) ?>"
             width="800" height="500" loading="lazy">
    </a>
    <?php endif; ?>
    <div class="card-content">
        <h2><a href="<?= SITE_URL ?>/<?= e($post['slug']) ?>"><?= e($post['title']) ?></a></h2>
        <p><?= e($post['excerpt'] ?? '') ?></p>
        <time datetime="<?= e($post['published_at']) ?>"><?= format_date($post['published_at']) ?></time>
    </div>
</article>

A post's address is the site address plus the post slug (/post-slug), with no prefix such as /blog/. Writing width and height on the image keeps content from jumping while the page loads.

Dark Mode

Define every colour as a CSS custom property. Make light mode the default and define dark mode under a [data-theme="dark"] selector. The script that saves the choice to localStorage should apply it before the page is drawn; otherwise the wrong colours flash for a moment on load.

Before Going Live

  • Run Lighthouse on the homepage, a post and an archive.
  • Try navigating the page with the keyboard only.
  • Check the layout at 375, 768, 1024 and 1440 pixels wide.
  • Make sure dark mode works on every page.
  • Keep DEBUG_MODE on while developing so undefined-variable warnings show up early.

Written by

Celil Uyanıkoğlu

Computer engineer with 25+ years in IT. He builds jekcms and runs his own network of content sites on it - every guide published here is tried on those live installs first.

See all posts →

Choose a Licence

Annual licence with a discounted first year; updates and support included. Setup in 30 minutes.

View Pricing
  • Setup and live in 30 minutes
  • 14 professional themes
  • AVIF/WebP image optimization
  • Automatic SEO - Sitemap, Schema.org
  • ZeroTrack cookieless analytics

Be the first to know

New features, release notes and CMS guides. We send a couple of emails a month.