Creating Themes
Build complete, customizable themes for the ZephyrPHP Marketplace. Themes control the visual presentation of a ZephyrPHP site through layouts, page templates, reusable sections, and bundled assets.
Overview
A ZephyrPHP theme is a self-contained package that defines the look and feel of a website. Themes provide the HTML structure, CSS styling, and JavaScript behavior for every page. They are designed to be fully customizable through the theme customizer panel, which allows site owners to adjust colors, fonts, and other settings without editing code.
This guide covers the complete process of creating a theme, from the directory structure and manifest format through to building layouts, templates, and sections, and finally packaging your theme for distribution.
Theme Structure
A theme follows a consistent directory layout. The framework expects certain directories and files to be present for the theme to function correctly.
my-theme/
+-- theme.json # Theme manifest (required)
+-- layouts/
| +-- base.twig # Base layout (required)
| +-- blog.twig # Blog-specific layout (optional)
+-- templates/
| +-- home.twig # Home page template
| +-- page.twig # Generic page template
| +-- blog-post.twig # Blog post template
| +-- 404.twig # Not found template
+-- sections/
| +-- hero.twig # Hero banner section
| +-- features.twig # Features grid section
| +-- testimonials.twig # Testimonials section
| +-- cta.twig # Call-to-action section
+-- assets/
| +-- css/
| | +-- theme.css # Main theme stylesheet
| +-- js/
| | +-- theme.js # Main theme script
| +-- images/
| +-- logo.png
| +-- placeholder.jpg
+-- snippets/
+-- header.twig # Reusable header partial
+-- footer.twig # Reusable footer partial
+-- pagination.twig # Pagination partial
| Directory / File | Purpose | Required |
|---|---|---|
theme.json |
Manifest with metadata, settings definitions, color and font configuration | Yes |
layouts/ |
Base HTML layouts that define the overall page structure (head, body, scripts) | Yes |
templates/ |
Page-level templates that extend a layout and define page-specific content | Yes |
sections/ |
Reusable content blocks with configurable schemas, placeable on any page | No |
assets/ |
CSS, JavaScript, images, and fonts bundled with the theme | No |
snippets/ |
Small reusable template partials (headers, footers, navigation, etc.) | No |
The theme.json Manifest
Every theme must include a theme.json file at its root. This manifest describes the theme to the framework and marketplace, and defines the configurable settings that appear in the theme customizer.
{
"name": "Starter Theme",
"slug": "starter-theme",
"version": "1.0.0",
"description": "A clean, minimal starter theme for ZephyrPHP.",
"author": "Your Name",
"license": "MIT",
"requires": {
"zephyrphp": ">=0.1"
},
"settings": {
"site_name": {
"type": "text",
"label": "Site Name",
"default": "My Website"
},
"logo": {
"type": "image",
"label": "Site Logo",
"default": null
},
"show_sidebar": {
"type": "checkbox",
"label": "Show Sidebar",
"default": true
},
"posts_per_page": {
"type": "number",
"label": "Posts Per Page",
"default": 10
}
},
"colors": {
"primary": {
"label": "Primary Color",
"default": "#3B82F6"
},
"secondary": {
"label": "Secondary Color",
"default": "#1E40AF"
},
"background": {
"label": "Background Color",
"default": "#FFFFFF"
},
"text": {
"label": "Text Color",
"default": "#1F2937"
},
"accent": {
"label": "Accent Color",
"default": "#F59E0B"
}
},
"fonts": {
"heading": {
"label": "Heading Font",
"default": "Inter",
"options": ["Inter", "Poppins", "Roboto", "Playfair Display", "Merriweather"]
},
"body": {
"label": "Body Font",
"default": "Inter",
"options": ["Inter", "Open Sans", "Roboto", "Lato", "Source Sans Pro"]
}
}
}
Manifest Fields Reference
| Field | Type | Description |
|---|---|---|
name |
string | Human-readable theme name |
slug |
string | URL-safe identifier (lowercase, hyphens only) |
version |
string | Semantic version number |
description |
string | Short description for marketplace listings |
author |
string | Author name or organization |
settings |
object | Custom settings exposed in the theme customizer (text, image, checkbox, number, select, color, textarea) |
colors |
object | Color palette settings with labels and defaults |
fonts |
object | Font family settings with available options |
Available Setting Types
| Type | Description | Example Default |
|---|---|---|
text |
Single-line text input | "My Website" |
textarea |
Multi-line text input | "Welcome to our site." |
number |
Numeric input | 10 |
checkbox |
Boolean toggle | true |
select |
Dropdown with predefined options | "left" |
image |
Image upload or URL | null |
color |
Color picker | "#3B82F6" |
Creating Layouts
Layouts define the outermost HTML structure of a page. Every page template extends a layout. At minimum, your theme must provide a base.twig layout.
{# layouts/base.twig #}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}{{ theme_settings('site_name') }}{% endblock %}</title>
{# Theme colors as CSS custom properties #}
<style>
:root {
--color-primary: {{ theme_settings('colors.primary') }};
--color-secondary: {{ theme_settings('colors.secondary') }};
--color-background: {{ theme_settings('colors.background') }};
--color-text: {{ theme_settings('colors.text') }};
--color-accent: {{ theme_settings('colors.accent') }};
--font-heading: '{{ theme_settings('fonts.heading') }}', sans-serif;
--font-body: '{{ theme_settings('fonts.body') }}', sans-serif;
}
</style>
{# Theme CSS #}
<link rel="stylesheet" href="{{ theme_asset('css/theme.css') }}">
{% block head %}{% endblock %}
</head>
<body>
{# Header #}
{% include 'snippets/header.twig' %}
{# Main content area #}
<main class="main-content">
{% block content %}{% endblock %}
</main>
{# Footer #}
{% include 'snippets/footer.twig' %}
{# Theme JavaScript #}
<script src="{{ theme_asset('js/theme.js') }}"></script>
{% block scripts %}{% endblock %}
</body>
</html>
Standard Blocks
Define these blocks in your base layout so that page templates and sections can inject content at the right points:
| Block | Purpose |
|---|---|
{% block title %}{% endblock %} |
Page title in the <title> tag |
{% block head %}{% endblock %} |
Additional <head> content (meta tags, extra CSS) |
{% block content %}{% endblock %} |
Main page content area |
{% block scripts %}{% endblock %} |
Additional JavaScript before </body> |
Multiple Layouts
Themes can include multiple layouts for different sections of a site. For example, a blog layout with a sidebar:
{# layouts/blog.twig #}
{% extends 'layouts/base.twig' %}
{% block content %}
<div class="blog-layout">
<div class="blog-content">
{% block blog_content %}{% endblock %}
</div>
{% if theme_settings('show_sidebar') %}
<aside class="blog-sidebar">
{% block sidebar %}
{% include 'snippets/sidebar.twig' %}
{% endblock %}
</aside>
{% endif %}
</div>
{% endblock %}
Creating Page Templates
Page templates extend a layout and define the content structure for a specific type of page. Each template is selectable in the CMS page editor, allowing users to choose which template to apply to any given page.
{# templates/home.twig #}
{% extends 'layouts/base.twig' %}
{% block title %}Home - {{ theme_settings('site_name') }}{% endblock %}
{% block content %}
{# Render page sections dynamically #}
{% for section in page.sections %}
{% include 'sections/' ~ section.type ~ '.twig' with section.data %}
{% endfor %}
{% endblock %}
{# templates/page.twig #}
{% extends 'layouts/base.twig' %}
{% block title %}{{ page.title }} - {{ theme_settings('site_name') }}{% endblock %}
{% block content %}
<article class="page-content">
<h1>{{ page.title }}</h1>
<div class="prose">
{{ page.content|raw }}
</div>
</article>
{% endblock %}
{# templates/blog-post.twig #}
{% extends 'layouts/blog.twig' %}
{% block title %}{{ post.title }} - {{ theme_settings('site_name') }}{% endblock %}
{% block blog_content %}
<article class="blog-post">
<header>
<h1>{{ post.title }}</h1>
<time datetime="{{ post.published_at|date('Y-m-d') }}">
{{ post.published_at|date('F j, Y') }}
</time>
</header>
<div class="prose">
{{ post.content|raw }}
</div>
</article>
{% endblock %}
Creating Sections
Sections are the building blocks of dynamic pages. Each section is a Twig template paired with a schema that defines its configurable fields. Users can add, remove, reorder, and customize sections through the CMS page editor.
Section with Schema
A section file contains both the HTML template and a {% schema %} block that defines its editable fields:
{# sections/hero.twig #}
<section class="hero" style="background-image: url('{{ section.background_image }}');">
<div class="hero-content">
<h1>{{ section.heading }}</h1>
<p>{{ section.subheading }}</p>
{% if section.button_text %}
<a href="{{ section.button_url }}" class="btn btn-primary">
{{ section.button_text }}
</a>
{% endif %}
</div>
</section>
{% schema %}
{
"name": "Hero Banner",
"description": "A full-width hero section with heading, subheading, and call-to-action button.",
"settings": [
{
"id": "heading",
"type": "text",
"label": "Heading",
"default": "Welcome to Our Site"
},
{
"id": "subheading",
"type": "textarea",
"label": "Subheading",
"default": "Discover what we have to offer."
},
{
"id": "background_image",
"type": "image",
"label": "Background Image",
"default": null
},
{
"id": "button_text",
"type": "text",
"label": "Button Text",
"default": "Get Started"
},
{
"id": "button_url",
"type": "text",
"label": "Button URL",
"default": "/get-started"
}
]
}
{% endschema %}
Example: Features Grid Section
{# sections/features.twig #}
<section class="features">
<div class="container">
<h2>{{ section.heading }}</h2>
{% if section.subheading %}
<p class="features-subtitle">{{ section.subheading }}</p>
{% endif %}
<div class="features-grid">
{% for feature in section.features %}
<div class="feature-card">
{% if feature.icon %}
<div class="feature-icon">{{ feature.icon }}</div>
{% endif %}
<h3>{{ feature.title }}</h3>
<p>{{ feature.description }}</p>
</div>
{% endfor %}
</div>
</div>
</section>
{% schema %}
{
"name": "Features Grid",
"description": "A grid of feature cards with icons, titles, and descriptions.",
"max_blocks": 6,
"settings": [
{
"id": "heading",
"type": "text",
"label": "Section Heading",
"default": "Features"
},
{
"id": "subheading",
"type": "textarea",
"label": "Section Subheading",
"default": ""
}
],
"blocks": [
{
"type": "feature",
"name": "Feature",
"settings": [
{
"id": "icon",
"type": "text",
"label": "Icon Name"
},
{
"id": "title",
"type": "text",
"label": "Title",
"default": "Feature Title"
},
{
"id": "description",
"type": "textarea",
"label": "Description",
"default": "Describe this feature."
}
]
}
]
}
{% endschema %}
The settings array defines top-level fields for the section. The blocks array defines repeatable items within the section (e.g., individual feature cards). Each block can have its own settings. Use max_blocks to limit how many blocks a user can add.
Using theme_settings() in Templates
The theme_settings() function retrieves the current value of any setting defined in your theme.json. If the user has not customized a setting, the default value from the manifest is returned.
{# Access a top-level setting #}
{{ theme_settings('site_name') }}
{# Access a color setting #}
{{ theme_settings('colors.primary') }}
{# Access a font setting #}
{{ theme_settings('fonts.heading') }}
{# Use in conditional logic #}
{% if theme_settings('show_sidebar') %}
{% include 'snippets/sidebar.twig' %}
{% endif %}
{# Use in inline styles #}
<div style="color: {{ theme_settings('colors.text') }};">
Content here
</div>
The recommended pattern is to map theme colors and fonts to CSS custom properties in your base layout (as shown in the layout example above), and then reference those properties throughout your CSS. This keeps your templates clean and makes it easy to apply consistent theming.
/* assets/css/theme.css */
body {
font-family: var(--font-body);
color: var(--color-text);
background-color: var(--color-background);
}
h1, h2, h3, h4, h5, h6 {
font-family: var(--font-heading);
color: var(--color-primary);
}
.btn-primary {
background-color: var(--color-primary);
color: #fff;
}
.btn-primary:hover {
background-color: var(--color-secondary);
}
a {
color: var(--color-accent);
}
Theme Customizer Integration
The ZephyrPHP CMS includes a theme customizer panel that reads your theme.json manifest and automatically generates a user interface for editing theme settings. Site owners can adjust colors, fonts, text, images, and toggles through this panel without writing any code.
Customizer Features
- Live preview: Changes are reflected in real time as the user adjusts settings.
- Color picker: Visual color selection for all entries defined in the
colorsobject. - Font selector: Dropdown menus populated from the
optionsarrays in thefontsobject. - Image upload: Drag-and-drop or file browser for
imagetype settings. - Reset to defaults: Users can restore any setting to its default value defined in the manifest.
- Save and publish: Changes are saved to the database and take effect site-wide when published.
Grouping Settings in the Customizer
You can organize settings into groups by adding a group field to each setting definition. Grouped settings appear under collapsible headings in the customizer panel.
{
"settings": {
"header_logo": {
"type": "image",
"label": "Header Logo",
"group": "Header",
"default": null
},
"header_sticky": {
"type": "checkbox",
"label": "Sticky Header",
"group": "Header",
"default": false
},
"footer_text": {
"type": "text",
"label": "Footer Copyright Text",
"group": "Footer",
"default": "All rights reserved."
},
"footer_show_social": {
"type": "checkbox",
"label": "Show Social Links",
"group": "Footer",
"default": true
}
}
}
Working with Theme Assets
Use the theme_asset() helper to generate correct URLs to files in your theme's assets/ directory:
<link rel="stylesheet" href="{{ theme_asset('css/theme.css') }}">
<script src="{{ theme_asset('js/theme.js') }}"></script>
<img src="{{ theme_asset('images/logo.png') }}" alt="Logo">
This helper resolves the full path to the asset based on the active theme, ensuring that asset URLs remain correct even when the theme is installed in different environments or served from a CDN.
Packaging and Distributing Themes
When your theme is ready for distribution, package it as a ZIP archive for upload to the ZephyrPHP Marketplace.
Creating the Package
# From your theme's root directory
zip -r starter-theme-1.0.0.zip . -x ".git/*" "node_modules/*" ".env"
The root of the ZIP must contain the theme.json file directly:
starter-theme-1.0.0.zip
+-- theme.json
+-- layouts/
+-- templates/
+-- sections/
+-- assets/
+-- snippets/
Marketplace Listing Requirements
When submitting your theme to the marketplace, include the following alongside your ZIP:
- Screenshots: At least one full-page screenshot showing the theme in use. Multiple screenshots showing different pages and customization options are recommended.
- Description: A clear description of the theme's purpose, target audience, and included features.
- Changelog: Version history documenting what changed in each release.
- Compatibility: The minimum ZephyrPHP version required.
Installing a Theme
Users install your theme using the Craftsman CLI:
php craftsman theme:install starter-theme
This downloads and extracts the theme into the project's themes/ directory. The user can then activate it from the CMS panel or by updating their theme configuration.
Best Practices
- Use CSS custom properties for theming. Map your
theme.jsoncolors and fonts to CSS variables in the base layout, then reference them in your stylesheets. This keeps styling consistent and customizer-friendly. - Keep layouts minimal. The base layout should define the HTML skeleton and include shared components (header, footer). Avoid putting page-specific logic in layouts.
- Make sections self-contained. Each section should render correctly on its own without depending on other sections being present on the page.
- Provide sensible defaults. Every setting in your
theme.jsonshould have a default value so the theme looks good out of the box without any customization. - Use semantic HTML. Structure your templates with proper heading hierarchy, landmark elements, and accessible markup.
- Optimize assets. Minify CSS and JavaScript for production. Compress images. Keep the total theme size reasonable for fast installations.
- Test across pages. Verify your theme works well with all template types, various section combinations, and different amounts of content.
- Document your sections. If your theme includes custom sections, provide brief descriptions in the schema so users understand what each section does and how to configure it.
Next Steps
- Learn about the Marketplace and how distribution works
- Explore Creating Apps to build functional extensions
- Review Views for more on Twig templating
- Read about Assets for asset management in ZephyrPHP