Introduction

VoidForge themes control your site's appearance and layout. Themes are built with PHP, HTML, CSS, and JavaScript, using a simple template system.

📄 Template Hierarchy

Flexible template system for any content type

🎨 Theme Settings

Built-in customization options

📍 Navigation Menus

Multiple menu locations support

🪝 Hook Integration

Extend functionality with hooks

Theme Structure

Each theme lives in its own folder within /content/themes/.

content/themes/
my-theme/
functions.php
header.php, footer.php
index.php, single.php, page.php
style.css
screenshot.png

Required Files

  • functions.php — Theme setup and configuration
  • index.php — Fallback template for all content
  • style.css — Theme stylesheet with header comment

Your First Theme

Step 1: Create style.css

/*
Theme Name: My Theme
Description: A custom theme
Version: 1.0.0
Author: Your Name
*/

body {
    font-family: system-ui, sans-serif;
    line-height: 1.6;
    max-width: 800px;
    margin: 0 auto;
    padding: 2rem;
}

Step 2: Create functions.php

<?php
// Register menu location
Theme::registerMenuLocation('primary', 'Primary Navigation');

// Register theme settings
Theme::registerSettings([
    'footer_text' => [
        'type' => 'text',
        'label' => 'Footer Text',
        'default' => '© 2025'
    ]
]);

Step 3: Create index.php

<?php get_header(); ?>

<main>
    <?php if ($posts): ?>
        <?php foreach ($posts as $post): ?>
            <article>
                <h2><?= esc($post['title']) ?></h2>
                <div><?= the_excerpt($post) ?></div>
                <a href="<?= Post::permalink($post) ?>">Read more</a>
            </article>
        <?php endforeach; ?>
    <?php endif; ?>
</main>

<?php get_footer(); ?>
Done! Activate your theme in Admin → Appearance → Themes.

Template Hierarchy

VoidForge looks for templates in a specific order, from most specific to least:

Content TypeTemplates (in order)
Single Postsingle-{type}.phpsingle.phpindex.php
Pagepage-{slug}.phppage.phpindex.php
Archivearchive-{type}.phparchive.phpindex.php
Categorycategory-{slug}.phpcategory.phparchive.php
Homepagehome.phpindex.php
Searchsearch.phpindex.php
404404.phpindex.php

Template Tags

Site Information

Settings::get('site_name')     // Site name
Settings::get('site_tagline')  // Site description
SITE_URL                       // Site URL
Theme::url()                   // Active theme URL

Post Data

$post['title']                 // Post title
$post['content']               // Raw content
the_content($post)             // Filtered content
the_excerpt($post)             // Post excerpt
Post::permalink($post)         // Post URL
Post::getMeta($id, 'key')      // Custom field value
Post::featuredImage($post)     // Featured image URL

Querying Posts

$posts = Post::query([
    'post_type' => 'post',
    'status' => 'published',
    'limit' => 10,
    'orderby' => 'created_at',
    'order' => 'DESC'
]);

Escaping Output

esc($text)       // HTML escape
esc_attr($attr)  // Attribute escape
esc_url($url)    // URL escape

Theme Settings

// Register in functions.php
Theme::registerSettings([
    'primary_color' => [
        'type' => 'color',
        'label' => 'Primary Color',
        'default' => '#8b5cf6'
    ],
    'show_sidebar' => [
        'type' => 'checkbox',
        'label' => 'Show Sidebar',
        'default' => true
    ],
    'posts_per_page' => [
        'type' => 'number',
        'label' => 'Posts Per Page',
        'default' => 10
    ]
]);

// Use in templates
$color = Theme::setting('primary_color');
if (Theme::setting('show_sidebar')) { ... }

Custom Fields

Access post custom fields defined in the admin.

// Get single value
$value = Post::getMeta($post['id'], 'field_name');

// Get with default
$value = Post::getMeta($post['id'], 'field_name', 'default');

// Get all meta
$meta = Post::getAllMeta($post['id']);

Available Field Types

text, textarea, number, email, url, date, datetime, color, select, checkbox, radio, image, gallery, file, wysiwyg, repeater

Assets

// Theme URL
Theme::url()  // /content/themes/my-theme

// In templates
<link rel="stylesheet" href="<?= Theme::url() ?>/style.css">
<script src="<?= Theme::url() ?>/js/main.js"></script>
<img src="<?= Theme::url() ?>/images/logo.png">

// Enqueue with hooks
Plugin::addAction('vf_head', function() {
    Plugin::enqueueStyle('theme', Theme::url() . '/style.css');
});

Plugin::addAction('vf_footer', function() {
    Plugin::enqueueScript('theme', Theme::url() . '/js/main.js');
});

Theme Hooks

Actions

HookDescription
vf_headInside <head> tag
vf_footerBefore </body> tag
get_headerWhen header.php loads
get_footerWhen footer.php loads
template_redirectBefore template loads

Filters

HookDescription
the_contentFilter post content
the_titleFilter post title
the_excerptFilter post excerpt
body_classFilter body CSS classes
template_includeOverride template selection

SEO Integration

VoidForge includes a built-in SEO system that automatically outputs meta tags when themes use the vf_head hook.

Required Theme Setup

Your header.php must include the vf_head hook inside the <head> tag:

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title><?= get_page_title() ?></title>
    <?php Plugin::doAction('vf_head'); ?>
</head>

What Gets Output

The SEO system automatically outputs:

  • Meta description — From post SEO settings or auto-generated from excerpt
  • Canonical URL — Prevents duplicate content issues
  • Robots meta — Index/noindex, follow/nofollow directives
  • Open Graph tags — For Facebook, LinkedIn, etc.
  • Twitter Cards — Optimized Twitter/X sharing
  • JSON-LD Schema — Structured data for rich search results

Page Title Function

Use get_page_title() for SEO-optimized titles:

// Returns formatted title based on SEO settings
// Example: "Post Title | Site Name"
<title><?= get_page_title() ?></title>

Debug Mode

Add ?seo_debug=1 to any frontend URL to see SEO output (admin only):

// View SEO data for any page
https://yoursite.com/your-post?seo_debug=1

Custom Post Type Templates

Create specific templates for custom post types:

Single Template

Create single-{post_type}.php:

<!-- single-portfolio.php -->
<?php get_header(); ?>

<article class="portfolio-item">
    <h1><?= esc($post['title']) ?></h1>
    
    <?php if ($gallery = Post::getMeta($post['id'], 'gallery')): ?>
        <div class="gallery">
            <?php foreach ($gallery as $img): ?>
                <img src="<?= esc($img['url']) ?>">
            <?php endforeach; ?>
        </div>
    <?php endif; ?>
    
    <div><?= the_content($post) ?></div>
</article>

<?php get_footer(); ?>

Archive Template

Create archive-{post_type}.php:

<!-- archive-portfolio.php -->
<?php get_header(); ?>

<h1>Portfolio</h1>

<div class="portfolio-grid">
    <?php foreach ($posts as $post): ?>
        <a href="<?= Post::permalink($post) ?>" class="item">
            <?php if ($img = Post::featuredImage($post)): ?>
                <img src="<?= esc($img) ?>">
            <?php endif; ?>
            <h3><?= esc($post['title']) ?></h3>
        </a>
    <?php endforeach; ?>
</div>

<?php get_footer(); ?>

Best Practices

Security

  • Always escape output with esc(), esc_attr(), esc_url()
  • Never trust user input
  • Use the_content() instead of raw content

Performance

  • Minimize database queries in templates
  • Use appropriate image sizes
  • Lazy load images when possible

Code Quality

  • Use consistent indentation and formatting
  • Comment complex template logic
  • Keep functions.php organized
Need more? See the Plugin Development Guide for extending functionality.