Template Overrides
Updated
Pro feature. Available in MediaVerse Pro.
Endpoints and hooks marked (Pro) require WPMediaVerse Pro.
WPMediaVerse uses a template loading system that checks your active theme before falling back to plugin templates. This lets you fully customize media page layouts without modifying the plugin.
How It Works
The TemplateLoader class calls locate_template() to check these locations in order:
- Child theme:
wp-content/themes/child-theme/wpmediaverse/template-name.php - Parent theme:
wp-content/themes/parent-theme/wpmediaverse/template-name.php - Plugin default:
wp-content/plugins/wpmediaverse/templates/template-name.php
Creating a Theme Override
Create a wpmediaverse/ directory inside your theme and copy the template file you want to modify:
wp-content/themes/your-theme/
└── wpmediaverse/
├── media-single.php # Single media item page
├── album.php # Single album page
├── collection.php # Single collection page
├── explore.php # Explore feed / taxonomy archives
└── profile-edit.php # Profile edit page
Available Templates
| File | Used For |
|---|---|
media-single.php |
Single mvs_media post page |
album.php |
Single mvs_album post page |
collection.php |
Single mvs_collection post page |
cpt-archive.php |
mvs_album and mvs_collection post-type archives |
explore.php |
The /media/ explore feed, mvs_tag / mvs_category taxonomy archives, and /media/@username/ profile pages when no user-profile.php resolves |
user-profile.php |
/media/@username/ profile pages. No plugin default ships - the lookup falls through to explore.php unless a theme (or a Pro layout) supplies this file |
profile-edit.php |
/media/edit-profile/ endpoint |
documents.php |
The [mvs_documents] document-drive shortcode |
messages.php |
The direct-messages page |
app-page.php |
Plugin-created pages that host a WPMediaVerse shortcode/block surface |
404.php |
The plugin's branded not-found page (unknown profile, missing media) |
Available Partials
Template partials live in templates/partials/ and are loaded with TemplateLoader::get_template() or TemplateLoader::locate(), passing partials as the second (subdirectory) argument:
WPMediaVerse\Core\TemplateLoader::get_template( 'partials/usage-history.php', array(
'user_id' => get_current_user_id(),
) );
// Or, with the subdirectory as its own argument:
$path = WPMediaVerse\Core\TemplateLoader::locate( 'usage-history.php', 'partials' );
Override a partial at wp-content/themes/your-theme/wpmediaverse/partials/<file>.php. The partials shipped today include dashboard-content.php, usage-history.php, shared-ui-frame.php, profile-actions.php, profile-edit-panel.php, follows-modal.php, blocked-members.php, and the chat-*.php messaging set.
Using TemplateLoader in Custom Code
use WPMediaVerse\Core\TemplateLoader;
// Load a template with data.
TemplateLoader::get_template( 'media-single.php', array(
'media_id' => 123,
'show_reactions' => true,
) );
// Just locate the path (without loading).
$path = TemplateLoader::locate( 'explore.php' );
Getting Media URLs in a Template (MediaUrl)
When you write a custom template you almost always need a media URL — a thumbnail for a grid card, or the full file for a lightbox. Do not hand-build these URLs from wp_upload_dir() and do not call SignedUrlService directly. A raw upload path breaks the moment a site enables cloud storage or marks media private, and calling the signing service directly means re-implementing the privacy gate.
Since 1.5.0 the read-side facade WPMediaVerse\Core\MediaUrl is the single entry point. It resolves the active storage driver, runs the privacy check, and returns either a signed /serve URL or a direct CDN URL — whichever is correct for that media's privacy and the current driver.
use WPMediaVerse\Core\MediaUrl;
// Thumbnail URL for the current viewer (size: large | medium | thumb).
$thumb = MediaUrl::thumb( $media_id, 'large' );
// Full original file URL for the current viewer.
$full = MediaUrl::file( $media_id );
if ( $thumb ) {
printf(
'<img src="%s" alt="%s" loading="lazy" />',
esc_url( $thumb ),
esc_attr( get_the_title( $media_id ) )
);
}
Both methods return an empty string when the service isn't ready (very early bootstrap) or when the viewer's identity is rejected by the privacy gate — always guard the return value before printing, as shown above.
Public static methods
namespace WPMediaVerse\Core;
final class MediaUrl {
// Signed /serve URL for a thumb variant. $size: large|medium|thumb.
// $ttl 0 = service default. $user_id null = current user; 0 = broadcast surface.
// $skip_privacy is forced to false when the resolved user id is 0.
public static function thumb(
int $media_id,
string $size = 'large',
int $ttl = 0,
?int $user_id = null,
bool $skip_privacy = true
): string;
// Signed /serve URL for the full original file.
public static function file( int $media_id, ?int $user_id = null ): string;
// The meta key holding a variant's stored URL.
// e.g. ('large') -> 'thumb_large'; ('large','webp') -> 'thumb_large_webp'.
public static function variant_meta_key(
string $size,
string $format = VariantSpec::FORMAT_PRIMARY
): string;
// The meta key holding a variant's driver-agnostic relative path
// (the URL key with a `_path` suffix). e.g. ('large') -> 'thumb_large_path'.
public static function variant_path_meta_key(
string $size,
string $format = VariantSpec::FORMAT_PRIMARY
): string;
}
The two *_meta_key() helpers are the single source of truth for the size+format → meta-key mapping. Use them instead of hardcoding 'thumb_large_webp'-style strings when you need to read variant meta yourself:
use WPMediaVerse\Core\MediaUrl;
use WPMediaVerse\Services\VariantSpec;
$webp_key = MediaUrl::variant_meta_key( 'large', VariantSpec::FORMAT_WEBP ); // 'thumb_large_webp'
$webp_url = get_post_meta( $media_id, $webp_key, true );
Note:
TemplateHelpers::get_thumb_url()is now a one-line delegate toMediaUrl::thumb(), so existing templates that already use it keep working unchanged —MediaUrlis simply the canonical name to reach for in new code.
Pro Layout Templates (Pro)
When a Pro feed layout (Instagram, Pinterest, Flickr, or Dribbble) is active (mvs_pro_feed_layout setting), Frontend\Layouts\LayoutManager hooks the same mvs_locate_template filter chain described above to swap in the layout's own explore.php / user-profile.php replacements. Because it hooks the same filter, a child-theme override you already placed under wpmediaverse/ wins automatically — LayoutManager::override_template() checks whether the resolved path already lives inside the active theme before forcing the layout file, so wp-content/themes/your-theme/wpmediaverse/explore.php (or user-profile.php) is honored over any layout's version, exactly like Free templates.
| Free template name | Overridden by (per layout) | Layout template directory |
|---|---|---|
explore.php |
feed.php |
templates/layouts/{instagram,pinterest,flickr,dribbble}/ |
user-profile.php |
profile.php |
templates/layouts/{instagram,pinterest,flickr,dribbble}/ |
Each layout also loads a body partial (feed-body.php) and further partials (e.g. Instagram's partials/stories-bar.php, partials/feed-card.php) that are outside the mvs_locate_template filter's $template_name map. These use a separate, path-based lookup (LayoutManager::theme_or_plugin()) that checks the SAME wpmediaverse/ theme directory, just keyed by the file's path relative to templates/ instead of by template name:
wp-content/themes/your-theme/
└── wpmediaverse/
└── layouts/
└── instagram/
├── feed-body.php
└── partials/
├── stories-bar.php
└── feed-card.php
Ship an override at that path (mirroring wp-content/plugins/wpmediaverse-pro/templates/layouts/{slug}/...) and it is read in preference to the plugin's copy — no filter needed.
Third-party hooks for customizing a layout without a template override: mvs_layout_modes (register additional layout modes), mvs_active_layout (force the active layout slug), mvs_layout_template_map (remap which layout file backs each free template name), mvs_layout_config (filter a layout's config array), mvs_before_layout_render (fires before a layout template loads). See Hooks & Filters Reference.
Compete-Page Templates (Pro)
The gamification frontend pages (GamificationTemplateLoader) are theme-overridable through the SAME WPMediaVerse\Core\TemplateLoader::locate() call the free plugin uses — so the wpmediaverse/ child-theme directory convention applies here too, with no separate lookup path:
| Route | Query var | Template file |
|---|---|---|
/media/battles/ |
mvs_battles_page |
battles.php |
/media/challenges/ |
mvs_challenges_page |
challenges.php |
/media/tournaments/ |
mvs_tournaments_page |
tournaments.php |
/compete/ |
mvs_compete_page |
compete-hub.php |
wp-content/themes/your-theme/
└── wpmediaverse/
├── battles.php
├── challenges.php
├── tournaments.php
└── compete-hub.php
Each page 404s if its backing feature toggle (mvs_battles_enabled, mvs_challenges_enabled, mvs_tournaments_enabled) is off — Compete Hub (/compete/) 404s only when none of the three are enabled. Added in 1.9.0.
Filtering the Template Path
You can override any template path using the mvs_locate_template filter:
add_filter( 'mvs_locate_template', function( string $template, string $name, string $path ) {
// Use a completely different directory for all WPMediaVerse templates.
$override = WP_CONTENT_DIR . '/my-media-templates/' . $name;
return file_exists( $override ) ? $override : $template;
}, 10, 3 );
BuddyX Theme Integration
WPMediaVerse adds the mvs-page and no-sidebar CSS body classes to all WPMediaVerse pages. The BuddyX theme (and any theme that handles these classes) renders these pages full-width without a sidebar.
Pages that receive these classes:
- Single
mvs_media,mvs_album,mvs_collectionposts mvs_media,mvs_albumandmvs_collectionarchivesmvs_tagandmvs_categorytaxonomy pages/media/edit-profile/endpoint/media/@username/profile pages- Any page whose ID matches an
mvs_page_*option (e.g., the page containing[mvs_dashboard])
The class list itself is filterable via mvs_body_classes (receives array( 'mvs-page', 'no-sidebar' )).
Shortcode Context in Block Templates
When a shortcode renders a block template (via Shortcodes::render_block_template()), the variable $mvs_shortcode_context is set to true. Block render.php files should check this variable before calling get_block_wrapper_attributes(), which causes a PHP warning outside a block context:
if ( empty( $mvs_shortcode_context ) ) {
$wrapper_attrs = get_block_wrapper_attributes();
}
Styling Tokens: the Border Contract
The plugin's colours come from CSS custom properties defined once on :root in
assets/css/frontend.css (and again in assets/css/admin.css, because wp-admin
does not load the frontend stylesheet). Override a token in your theme and every
surface follows; there is no need to target individual components.
Borders in particular use three tiers, one job each. This matters when you override them, because before 2.4.0 a single token did all three jobs and changing it moved every outline in the plugin at once:
| Token | Job | Use it for |
|---|---|---|
--mvs-border-light |
Structure | Cards, panels, sections, rows, dividers. Quiet: it separates, it does not ask to be looked at. |
--mvs-border |
Controls | input, select, textarea, buttons, chips, dropzones. A visible edge here means "you can interact with me". |
--mvs-border-strong |
Emphasis | Active or selected state, table head rules. |
--mvs-border inherits from the active theme (Reign, BuddyX, BuddyNext, then a
neutral fallback), so a themed border colour reaches the plugin's controls
automatically. --mvs-border-strong is derived with color-mix() from
--mvs-border and --mvs-text, so it strengthens correctly in both light and
dark mode from one declaration.
Two rules the plugin follows internally, worth keeping if you extend it:
- A bordered box does not contain another bordered box. Where nesting is real, the outer element drops its border and expresses depth with background and spacing instead. Repeated 1px lines read as boxes inside boxes.
- Semantic borders are not part of this ladder. Success, warning, danger and brand colours carry meaning; never substitute a neutral tier for one.
bin/ux-audit.sh reports an advisory row when a container selector draws a
border with the control token, which is the drift these rules exist to prevent.