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:

  1. Child theme: wp-content/themes/child-theme/wpmediaverse/template-name.php
  2. Parent theme: wp-content/themes/parent-theme/wpmediaverse/template-name.php
  3. 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 to MediaUrl::thumb(), so existing templates that already use it keep working unchanged — MediaUrl is 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 automaticallyLayoutManager::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_collection posts
  • mvs_media, mvs_album and mvs_collection archives
  • mvs_tag and mvs_category taxonomy 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:

  1. 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.
  2. 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.