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 partialtheme.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.
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.
- The controller renders. e.g.
ThemeRenderer::render('home', ['videos' => $videos]). - Globals are injected. The
$_-prefixed site, appearance, locale and card/watch variables are added. - Plugins adjust the data. The
theme.template_datafilter runs, receiving($data, $templateName). - Your template buffers. The template file is captured into
$content. - The layout loads.
layout.phpreceives$contentplus every variable and outputs the full HTML shell. - Impressions flush.
ImpressionTracker::flush()writes the batched CTR update before</body>. - 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 variables | Use |
|---|---|
$_siteName, $_siteUrl, $_user | Identity and the logged-in user (or null). |
$_videosPerRow | Grid columns (4, 5 or 6) for your listings. |
$_colorOverrides | The <style> block of colour custom properties for <head>. |
$_card*, $_watch* | The card and watch-page toggles set in Appearance. |
$_menuItems, $_menuSearch | Navigation items and the search-bar toggle. |
| Key helpers | Use |
|---|---|
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.
| Hook | Type | Purpose |
|---|---|---|
head.meta | Action | Emit tags inside <head> (meta, JSON-LD, verification). |
footer.scripts | Action | Emit markup just before </body>. |
video.card.html | Filter | Replace 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.