Skip to content
TubePress — free, self-hosted & actively maintained
Appearance & themes

Theme development

Build your own theme: structure, theme.json, functions.php, templates and helpers.

A TubePress theme is a folder of PHP templates plus optional assets and helper functions. Reach for a custom theme when Appearance and the theme editor are not enough and you want full control of the markup. This page is the build guide; keep the theme API reference open beside it for the exhaustive list of variables and helpers.

The golden rule throughout: read the settings TubePress injects rather than hard-coding. If you honour the $_-prefixed variables, your theme stays fully configurable from the admin, exactly like the bundled Simply theme.

Theme structure

Themes live under themes/{slug}/. Only theme.json and templates/layout.php are strictly required — everything else is optional and falls back to Simply.

themes/aurora/
├── theme.json            Required — metadata
├── functions.php         Enqueue assets, helper functions
├── assets/
│   ├── css/style.css
│   └── js/app.js
└── templates/
    ├── layout.php        Required — the HTML shell
    ├── home.php          Homepage
    ├── video.php         Watch page
    ├── category.php      …tag/performer/channel (+ plurals)
    ├── search.php        Search results
    ├── page.php          Static page
    ├── favorites.php     User favourites
    ├── history.php       Watch history
    ├── 404.php           Not found
    ├── auth/             login, register, profile
    └── partials/
        └── video-card.php  Reusable card partial

theme.json

This manifest is required. It carries the theme's metadata, an optional default colour set, and arbitrary settings. The name, version and author surface in /admin/themes.

{
    "name": "Aurora",
    "description": "A custom theme for TubePress",
    "version": "1.0.0",
    "author": "Your Name",
    "screenshot": "screenshot.png",
    "colors": {
        "primary": "#2563eb",
        "secondary": "#7c3aed",
        "background": "#ffffff",
        "text": "#1e293b"
    },
    "settings": {
        "videos_per_row": 4
    }
}

functions.php

This file is loaded automatically at boot. Use it to enqueue your stylesheet and script, and to define the helper functions your templates call. Enqueue assets with a filemtime() query string so browsers always pick up your latest edit; the second argument is the load priority (lower loads earlier).

<?php
declare(strict_types=1);

// filemtime() busts the cache on every edit.
$cssVer = @filemtime(ThemeManager::themePath()
    . '/assets/css/style.css') ?: time();
$jsVer  = @filemtime(ThemeManager::themePath()
    . '/assets/js/app.js') ?: time();

ThemeRenderer::enqueueCSS(
    ThemeManager::assetUrl('css/style.css') . '?v=' . $cssVer, 1);
ThemeRenderer::enqueueJS(
    ThemeManager::assetUrl('js/app.js') . '?v=' . $jsVer, 1);

// A reusable video-card helper, called from listing templates.
function auroraVideoCard(array $video): string
{
    // Record an impression so the CTR system can rank this card.
    ImpressionTracker::collect((int) $video['id']);

    // Let a plugin replace the card entirely if it wants to.
    $card = HookSystem::applyFilter('video.card.html', '', $video);
    if ($card !== '') {
        return $card;
    }

    ob_start();
    require ThemeManager::templatePath('partials/video-card');
    return ob_get_clean();
}

Simply defines exactly this kind of helper — simplyVideoCard() — alongside simplyPagination() and simplySortTabs(). The ImpressionTracker::collect() call is what feeds CTR ranking; the batched write is flushed later in layout.php.

Prefer a partial for the card. Render partials/video-card.php via ThemeManager::templatePath() and output buffering, as above. It keeps the markup in one place and lets plugins override it through the video.card.html filter.

Templates & the rendering flow

A controller hands data to ThemeRenderer::render($template, $data), and the renderer takes it from there. Understanding the order helps you know what is available where.

  1. The controller renders. e.g. ThemeRenderer::render('home', ['videos' => $videos]).
  2. Globals are injected. The $_-prefixed site, appearance, locale and card/watch variables are added.
  3. Plugins adjust the data. The theme.template_data filter runs, receiving ($data, $templateName).
  4. Your template buffers. The template file is captured into $content.
  5. The layout loads. layout.php receives $content plus every variable and outputs the full HTML shell.
  6. Impressions flush. ImpressionTracker::flush() writes the batched CTR update before </body>.
  7. Cron runs. CronManager::run() executes any due scheduled tasks.

Your layout.php is responsible for the shell: include ThemeRenderer::renderCSS() and $_colorOverrides in <head>, echo $content, then call ThemeRenderer::renderJS() and ImpressionTracker::flush() before </body>. The theme API lists the full eight-point checklist.

Global variables & helpers

Every template receives the global $_ variables plus its own data. The theme API reference documents all of them; these are the ones you will reach for most.

Most-used variablesUse
$_siteName, $_siteUrl, $_userIdentity and the logged-in user (or null).
$_videosPerRowGrid columns (4, 5 or 6) for your listings.
$_colorOverridesThe <style> block of colour custom properties for <head>.
$_card*, $_watch*The card and watch-page toggles set in Appearance.
$_menuItems, $_menuSearchNavigation items and the search-bar toggle.
Key helpersUse
ThemeRenderer::enqueueCSS/::enqueueJS, ::partial, ::renderCSS/::renderJS.
ThemeManager::assetUrl, ::themePath, ::templatePath, ::active.
Format::number, ::duration, ::timeAgo for display.
Router / url()URLs, plus ::csrfField() for forms.
Setting, __()Read a setting; translate a key.

Template fallback

If the active theme is missing a template, TubePress renders Simply's copy instead. That means you override only what you want to change: start by copying a single template — home.php, say — into your theme, and let everything else inherit from Simply until you are ready to take it over. A theme can be one file or fifty.

Theme hooks

Beyond templates, a small set of hook points lets you (and plugins) extend the page without forking it. Actions are fire-and-forget; filters transform a value you return.

HookTypePurpose
head.metaActionEmit tags inside <head> (meta, JSON-LD, verification).
footer.scriptsActionEmit markup just before </body>.
video.card.htmlFilterReplace a card's HTML. Args ($html, $video); return a non-empty string to override.
// An action — fire and forget, inside <head>.
HookSystem::doAction('head.meta');

// A filter — return '' to keep the default card.
HookSystem::addFilter('video.card.html',
    function (string $html, array $video): string {
        return $html;
    });

Two more filters are worth knowing: theme.template_data to adjust the data array before render, and head.css to append a <style> string — Simply uses the latter to publish its grid gap and radius as CSS variables. For the wider action/filter catalogue, see hooks & plugins.

Next steps

Still stuck?

Open a ticket from your dashboard and our team will help you out.

Try the live demo → Download TubePress →