XRV (Xroad Video) releases
Privacy-first WordPress video gallery by Crossroad Media. A click-to-load YouTube facade that eliminates the consent-banner overlays, black/blank players, and pre-consent Google requests that Smash Balloon YouTube Feed plus a cookie manager produce. Preoads nothing from YouTube. Local thumbnails + self-generating VideoObject schema.
by Crossroad Media · github.com/crossroad-media/xroad-videos · website
Install
The author publishes release zips, so WP-CLI can install straight from GitHub:
wp plugin install https://github.com/crossroad-media/xroad-videos/releases/download/v2.8.0/xroad-videos-v2.8.0.zipReadme
Crossroad Videos (xroad-videos)
A single-file WordPress plugin by Crossroad Media. A privacy-first, click-to-load video gallery and a drop-in alternative to Smash Balloon YouTube Feed for any site running a cookie/consent manager. Supports YouTube, Vimeo, Wistia, Loom, Dailymotion, and self-hosted MP4 / WebM behind one facade, with geo-aware GDPR consent modes, a built-in bulk importer, a site-wide settings page, and multiple layouts.
Why it exists
Smash Balloon YouTube Feed (and standard YouTube embeds) fire requests to youtube.com / i.ytimg.com / google.com on page load, before consent. When a consent manager (CookieYes, Osano, Cookiebot, Complianz, etc.) is present, it tries to intercept those requests, and that interception is what produces the symptoms operators keep reporting:
- a consent warning overlay sitting on top of the player,
- a black or blank player that never loads,
- cascade JavaScript failures when the CMP and the feed's consent shim collide,
- and corrupted GA4 attribution when a page-refresh "fix" is used to force the player to load.
Every one of those has the same root cause: third-party requests firing before a deliberate user action.
This plugin inverts that. Every card starts as a local poster image + a play button: pure first-party HTML/CSS, zero requests to any Google domain, zero cookies, zero localStorage. Because nothing third-party fires before interaction, the consent manager has nothing to block, so no banner, overlay, or black player can appear. Only on click does it inject a youtube-nocookie.com iframe (the click is the consent) and push a video_play event to the dataLayer. Storing thumbnails locally hardens the guarantee: even the poster makes no call to i.ytimg.com.
This is the "facade" pattern.
What it does
- Curated CPT
xroad_video. Editors paste a video URL from any supported host and drag to reorder; the provider is auto-detected and no API key is required. - Built-in bulk importer (Videos → Import). Paste a list of URLs, point at a channel or playlist (optionally with a free API key for durations and upload dates), or upload a JSON file carrying full metadata and taxonomy terms. A dry-run preview shows new vs. already-in-library before anything is written, then the import runs in batched AJAX with a progress bar.
- Local thumbnail sideload on save, so the grid references
/wp-content/uploads/and never calls the host's image CDN. YouTube uses its predictable poster URLs (maxres with hqdefault fallback, checked by HTTP status); every other host supplies its poster, and for all but YouTube its duration, from its own no-key oEmbed endpoint, which also prefills the title on first save. - Layouts. A featured carousel, a browse grid, or both in one shortcode (
layout="library"), with lightbox or inline playback. Filtering (dropdown selects or chips for Series / Audience / Topic), keyword search, and sort (including shortest or longest by duration) are pure client-side toggles, so they run instantly with no network round-trip. Paged browse adds a Load More button and an optional Subscribe button, and the grid steps 3 to 2 to 1 columns on smaller screens. - Geo-aware consent modes. Global (recommended), Strict GDPR, or No consent integration. Global shows a dismissible opt-in prompt only to EU/UK/EEA/CH visitors (resolved from an edge country header) and stays frictionless for everyone else, including US / CCPA, since the facade shares no data with YouTube until a click. Whenever a prompt is required the plugin makes zero contact with any Google domain until the visitor accepts. See Privacy, consent & GDPR below.
- Site-wide settings page (Videos → Settings). Set the consent mode, privacy URL, filter style, per-page counts, Subscribe URL, and YouTube Data API key once; every gallery inherits them, and any shortcode or block attribute still overrides.
- Self-generating VideoObject JSON-LD inside a
CollectionPage/ItemList, merging with the site's Organization node via thexrv_org_idfilter. Single-video pages emit a standaloneVideoObjectwith transcript and key-momentClips for rich-result and AI-citation eligibility. - Shortcode, block, and block sidebar controls under the collision-proof
xroadnamespace. The Gutenberg block exposes Layout, Browse, Privacy, and pre-filter panels through InspectorControls, with no build step. - Three REST-exposed taxonomies (
xrv_series,xrv_audience,xrv_topic); sites define their own terms. - Multi-source. One
_xrv_providerswitch routes YouTube, Vimeo, Wistia, Loom, Dailymotion, and self-hosted files, auto-detected from the pasted URL. Hosted videos load on click as the host's privacy-enhanced iframe (Vimeo withdnt=1); self-hosted files play in a native<video>element with zero third-party contact ever. Vimeo unlisted and domain-private videos keep their privacy hash, and galleries can mix sources in a single grid.
No page builder, ACF, jQuery, or build step. The inline-asset architecture (CSS, JS, and SVG emitted once per request) survives a performance plugin's unused-CSS pass and moves between themes unchanged.
Usage
[xroad-videos]
[xroad-videos layout="library" per_page="9" subscribe_url="https://youtube.com/@yourchannel"]
[xroad-videos series="webinars" columns="3" filter_ui="chips" consent_notice="geo" privacy_url="/privacy-policy/"]
Block: xroad/videos (a PHP-rendered dynamic block that shares the shortcode's render path). Every attribute below is also a block sidebar control, and anything left blank inherits the site-wide default from Videos → Settings, which a shortcode or block attribute always overrides.
Sources
Editors never pick a provider by hand: paste a URL and the host is detected automatically (the Provider selector defaults to Auto-detect). Supported sources:
| Source | Paste | Metadata without an API key |
|---|---|---|
| YouTube | watch, youtu.be, Shorts, or embed URL |
title, poster |
| Vimeo | vimeo.com/... (including unlisted vimeo.com/{id}/{hash}) |
title, poster, duration, description |
| Wistia | a wistia.com/medias/... URL |
title, poster, duration |
| Loom | loom.com/share/... |
title, poster, duration |
| Dailymotion | dailymotion.com/video/... or dai.ly/... |
title, poster |
| TikTok | a tiktok.com/@user/video/... link (vertical, rendered 9:16) |
title, poster |
| Self-hosted file | a direct .mp4 or .webm URL (pick Self-hosted file) |
none; set the title and upload a poster yourself |
Hosted videos load on click as the host's privacy-enhanced player (Vimeo uses dnt=1 with no title or byline chrome); self-hosted files play in a native <video> element, so they make no third-party contact at any point. Galleries can mix sources freely in one grid. The bulk importer and channel sync are currently YouTube-only; extending bulk import to every provider is a planned roadmap item.
Content and filtering
| Attribute | Default | What it does |
|---|---|---|
series, audience, topic |
all | Pre-filter to one or more taxonomy term slugs (comma-separated). |
limit |
all | Cap how many videos render. |
filter_ui |
select |
Facet filter style: select (dropdowns) or chips (clickable rows). |
controls |
true |
false hides the search / sort / filter bar. |
Layout
| Attribute | Default | What it does |
|---|---|---|
layout |
grid |
grid, carousel, or library (a featured carousel above a browse grid). |
columns |
responsive | Fixed column count; blank gives responsive columns (masonry for grid, 3 for library/carousel). |
featured_limit |
6 |
How many videos feed the featured carousel. |
heading |
none | Optional centered section title above a grid or carousel. |
playback |
lightbox |
lightbox pops the video into a centered overlay; inline plays it in the card. |
per_page |
9 |
Cards shown before a Load More button appears. |
load_more |
3 |
How many more cards each Load More click reveals. |
subscribe_url, subscribe_label |
off | Show a Subscribe button under the grid when subscribe_url is set; subscribe_label sets its text. |
Privacy and consent
| Attribute | Default | What it does |
|---|---|---|
consent_notice |
off |
geo = Global (opt-in prompt for EU/UK/EEA/CH; one-click + facade elsewhere, including US / CCPA), strict = Strict GDPR (opt-in prompt for everyone), off = no consent layer. (light = legacy on-video caption, still honored.) See Privacy, consent & GDPR. |
consent_text |
built-in | Body text of the consent prompt. |
consent_button |
Load video |
Accept-button label. |
consent_decline |
No thanks |
Decline-button label. |
privacy_url |
WP privacy page | Privacy-policy link shown in the prompt. |
Install
Single-file plugin. Either upload xroad-videos.php to wp-content/plugins/xroad-videos/, or zip the folder (xroad-videos/xroad-videos.php) and upload via Plugins → Add New → Upload. Activate; the CPT and taxonomies register on activation.
Configuration filters
| Filter | Purpose |
|---|---|
xrv_org_id |
Pin the publisher @id to your SEO plugin's exact Organization @id so the two schema nodes merge instead of competing. |
xrv_seed_terms |
Pre-seed taxonomy terms on activation: return [ taxonomy => [ slug => name ] ]. Idempotent. |
xrv_synonym_map |
Extend the keyword index: return [ term-slug => 'extra search aliases' ]. |
xrv_video_schema |
Modify a single VideoObject node (add transcript, regionsAllowed, about, etc.). |
xrv_list_name |
Override the CollectionPage / ItemList name. |
xrv_consent_required |
Override the geo consent decision for the current request (force a region, plug in MaxMind, defer to your CMP, and so on). Return true to require the consent prompt, false to allow one-click play. |
Example, merging schema with Yoast/Rank Math's Organization node:
add_filter( 'xrv_org_id', fn() => 'https://example.com/#organization' );
Verify the privacy guarantee
Open a page using the gallery in DevTools (Network tab, cache disabled) and confirm zero requests to youtube.com / youtube-nocookie.com / google.com / i.ytimg.com before any click, and zero YouTube cookies / localStorage. Repeat with your consent manager active and confirm no banner, overlay, or black screen. If any Google-domain request fires pre-click, that's a bug; open an issue.
Read the full README on GitHub →
Releases
| Tag | Published | Asset | Downloads |
|---|---|---|---|
| v2.8.0 | Jul 4, 2026 | xroad-videos-v2.8.0.zip | 0 |
| v2.7.1 | Jul 4, 2026 | xroad-videos-v2.7.1.zip | 0 |
| v2.7.0 | Jul 4, 2026 | xroad-videos-v2.7.0.zip | 0 |
| v2.6.0 | Jul 4, 2026 | xroad-videos-v2.6.0.zip | 0 |
| v2.4.0 | Jul 4, 2026 | xroad-videos-v2.4.0.zip | 0 |
| v2.2.0 | Jul 4, 2026 | xroad-videos-v2.2.0.zip | 0 |
| v2.1.1 | Jul 4, 2026 | xroad-videos-v2.1.1.zip | 0 |
| v2.1.0 | Jul 4, 2026 | xroad-videos-v2.1.0.zip | 0 |
| v2.0.1.beta | Jul 4, 2026 | xroad-videos-v2.0.1.beta.zip | 0 |
| v2.0.0 | Jul 4, 2026 | xroad-videos-v2.0.0.zip | 0 |
| v1.0.9 | Jul 4, 2026 | xroad-videos-v1.0.9.zip | 0 |
| v1.0.8 | Jul 4, 2026 | xroad-videos-v1.0.8.zip | 0 |
| v1.0.7 | Jul 4, 2026 | xroad-videos-v1.0.7.zip | 0 |
| v1.0.6 | Jul 4, 2026 | xroad-videos-v1.0.6.zip | 0 |
| v1.0.5 | Jul 4, 2026 | xroad-videos-v1.0.5.zip | 0 |
| v1.0.4 | Jul 4, 2026 | xroad-videos-v1.0.4.zip | 0 |
| v1.0.3 | Jul 4, 2026 | xroad-videos-v1.0.3.zip | 0 |
| v1.0.2 | Jul 4, 2026 | xroad-videos-v1.0.2.zip | 0 |
| v1.0.1 | Jul 4, 2026 | xroad-videos-v1.0.1.zip | 0 |
| v1.0.0 | Jul 4, 2026 | xroad-videos-v1.0.0.zip | 0 |
| v2.10.0 | Jul 4, 2026 | xroad-videos.zip | 0 |
| v2.5.1 | Jun 19, 2026 | xroad-videos.zip | 0 |
| v2.5.0 | Jun 19, 2026 | xroad-videos.zip | 0 |
| v2.3.0 | Jun 19, 2026 | xroad-videos.zip | 0 |
| v2.2.2 | Jun 19, 2026 | xroad-videos.zip | 1 |