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",
"supports": ["subscriptions", "member_uploads", "pwa"],
"colors": {
"primary": "#2563eb",
"secondary": "#7c3aed",
"background": "#ffffff",
"text": "#1e293b"
},
"settings": {
"videos_per_row": 4
}
}Optional features: supports
Since TubePress 1.2, a theme opts in to the community features with the supports list. A feature switched on in Settings stays invisible on a theme that does not declare it, so add a flag only once your templates handle it:
| Flag | What your theme provides |
|---|---|
subscriptions | Subscribe buttons in channel.php and video.php (read $subscriptionsOn, $isSubscribed and $subscriberCount) and the Subscriptions tab of auth/profile.php. |
member_uploads | The upload.php template — without the flag, /upload answers 404 — and the My uploads tab of auth/profile.php. |
pwa | Nothing to build: the core adds the manifest, service worker and install banner through the head.meta and footer.scripts hooks. Declare it when your layout shows the Install the app entries (any element with data-pwa-install). |
Simply and Hentai Noir declare all three, so their templates are the best starting point. Every variable involved is listed in the theme API reference.
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.