WP Manifestindependent plugin directory
manifest / media / xroad-videos

XRV (Xroad Video)

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.

★ 2stars
10release downloads
v2.11.3latest · 5d ago

Install

Download xroad-videos.zip From the v2.11.3 release on GitHub, Oct 20, 2026

Upload it under Plugins → Add New Plugin → Upload Plugin, then activate.

Install straight from GitHub with WP-CLI:

wp plugin install https://github.com/crossroad-media/xroad-videos/releases/download/v2.11.3/xroad-videos.zip
No built-in updater. New versions won't appear under Dashboard → Updates. Install new releases yourself, or let Git Updater track the repository.

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 the xrv_org_id filter. Single-video pages emit a standalone VideoObject with transcript and key-moment Clips for rich-result and AI-citation eligibility.
  • Shortcode, block, and block sidebar controls under the collision-proof xroad namespace. 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_provider switch 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 with dnt=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.

The gallery is a click-to-load facade: every card is a local first-party poster + a play button. Nothing (no request, cookie, connection, or localStorage) reaches the video host until a visitor deliberately clicks. On click it injects the host's privacy-enhanced player (a youtube-nocookie.com iframe for YouTube, player.vimeo.com with dnt=1 for Vimeo, the equivalent for Wistia / Loom / Dailymotion, or a native <video> for self-hosted files); that click is the consent that loads the embed.

On top of that baseline, the Consent mode (Settings → Videos → Settings, or consent_notice=) sets how consent is obtained:

Mode consent_notice What the visitor gets Pre-click contact with the host
Global (recommended) geo EU/UK/EEA/CH visitors get the opt-in "Load video" prompt; everyone else (including US / CCPA) plays in one click none for prompted visitors; preconnect for others
Strict GDPR strict opt-in prompt for every visitor, worldwide none for anyone
No consent integration off no prompt or notice preconnect on hover (connection only, no data/cookies)

The opt-in prompt is declinable (× / "No thanks"), so refusing is as easy as accepting. Global satisfies GDPR where it applies (a prior-consent gate for EU/UK/EEA/CH) and the US notice-and-opt-out model elsewhere, because the facade shares no data with YouTube until the click. The legacy light value (an on-video caption) is still honored if set via shortcode. (Informational only, not legal advice.)

Key points:

  • The plugin sets no cookies of its own. The only client-side storage it writes is a first-party sessionStorage flag caching the geo decision. That is not a cookie, not an identifier, and it clears on tab close.
  • Dismissible consent. The prompt has an × and a "No thanks" button; declining closes it and loads nothing, so refusing is as easy as accepting.
  • Zero pre-click contact when gated. Whenever a prompt is required (Strict for everyone; Compliance for EU/UK/EEA/CH), the hover preconnect is suppressed too, so the visitor's browser makes no DNS/TLS/HTTP contact with any Google domain until they accept.
  • Geo detection reads an edge country header (CF-IPCountry / WP Engine / CloudFront) via a cache-safe REST call (/wp-json/xrv/v1/region); the page itself stays fully cacheable. With no header present it fails safe to showing the prompt to everyone. Override the logic with the xrv_consent_required filter.
  • Scope. This governs the video player only. Other site trackers (analytics, ad tags, consent managers) are independent; gate those at your CMP / Google Consent Mode. The facade also doesn't forward video-viewing data anywhere (relevant to the US VPPA): the video_play dataLayer event is first-party; don't wire it to a third party with an identifier.
  • Not legal advice. The click-to-load facade with an informed, dismissible prompt is the widely-recognized compliant pattern, but your DPO/counsel makes the final determination for your jurisdiction and content.

Settings for the prompt: consent_text, consent_button (accept label), consent_decline (decline label), privacy_url (defaults to your WordPress privacy page).

Analytics (optional)

On play, the plugin pushes to window.dataLayer:

{ event: 'video_play', video_provider, video_id, video_title, video_series }

Wire it in GTM with a Custom Event trigger on video_play and a GA4 event tag reading those data-layer variables. Because the facade never refreshes the page, it avoids the attribution corruption that page-refresh consent workarounds cause.

Changelog

2.10.0

Front-end + admin. Two lightbox upgrades: the pop-out modal becomes a per-device choice, and it can now carry a details panel (title, date, collapsible description) under the player. The zero-third-party-request-before-click guarantee is untouched.

  • Device-scoped playback. XRV → Settings → Browse defaults → Play videos in now offers: lightbox on all devices, lightbox on desktop / inline on mobile, lightbox on mobile / inline on desktop, or inline everywhere. The XRV Video block and [xroad-videos playback="…"] accept the same four values (plus Site default), so any gallery can override the site setting.
  • Decided in the browser, not on the server. The desktop / mobile split is resolved client-side by viewport width (under 768 px is mobile) at the moment of the click, so the rendered HTML is identical for every visitor and the page stays fully cacheable — no UA sniffing, no cache fragmentation. A rotate or resize lands the next play in the right mode with no reload.
  • Lightbox details panel. A watch-page-style caption can now sit under the player in the lightbox: the title, a relative upload date (“2 months ago”), and the description collapsed to four lines behind a Description toggle. On by default; turn it off at Settings → Browse defaults → Lightbox details, or per gallery with [xroad-videos lightbox_details="false"] / the block toggle. The panel is text rendered from the video’s own stored metadata — no view count, and no extra network calls. Shorts stay player-only.

2.9.0

Adds Collections — build more than one curated gallery and drop each anywhere on the site, including the hero. Removes the 2.8.0 privacy self-test. Existing single galleries are unchanged; the zero-third-party-request-before-click guarantee is untouched.

  • Collections (new Collections screen under XRV Video). A collection is a saved, placeable gallery: hand-pick and order specific videos, choose a layout (grid / carousel / library), and drop it with [xroad-videos collection="slug"] or the XRV Video block's Collection slug field — in a page, a widget, an FSE template part, or the theme hero. Presentation (columns, heading, etc.) is set on the shortcode/block at each placement. Dependency-free editor (no wp.media, no build step).
  • ids attribute. [xroad-videos ids="12,7,30"] renders an exact, ordered set via post__in — what collections ride on, usable on its own too.
  • Multiple galleries per page. The facade is now fully instance-scoped (wrapper and every control are class- or per-instance-id based), so two carousels plus a filtered grid coexist on one page with independent search / sort / filter.
  • Removed: the privacy self-test shipped in 2.8.0 — low practical value in an isolated render; may return later as a real-page check.

2.8.0

Adds a Privacy self-test that turns the zero-third-party guarantee from a claim into an in-product proof. Front-end harness + admin diagnostic; no new dependencies, no front-end weight on real pages.

  • Resource-Timing privacy self-test (Settings → Privacy self-test). One click renders your real gallery in an isolated, admin-only frame, reads the browser's performance.getEntriesByType('resource') log without ever dispatching a play, and reports a clear PASS/FAIL — naming by URL any pre-click request to a video-provider host (YouTube, Vimeo, Wistia, Loom, Dailymotion, TikTok and their CDNs). It is both the strongest privacy demo for a regulated-industry buyer and a standing regression guard: a future change that leaked a pre-click request turns this red. Requests to other origins you control (e.g. an image CDN) are reported but do not fail the test.
  • Verify the detector. A negative-control mode loads a deliberately leaky page (one planted provider request) so you can watch the detector catch a real leak — the only place XRV ever contacts a provider without a click, and only when an admin explicitly runs it.
  • Doc-hygiene: corrected the stale note that multi-source bulk import was "planned for 2.5."

2.7.1

Admin-only. Enhanced Watch Page Controls: finer control over each video's standalone page, plus a clearer home for the bulk switch.

  • Editable watch page slug in the editor. Ticking "Give this video its own watch page" now reveals an editable URL-slug field, so the standalone page's address is set inline (kept unique by WordPress). It hides when the video is gallery-only.
  • Watch Page Controls, relocated. The bulk on/off card (renamed from "Bulk watch pages") now sits beside the Video URLs card in Settings, with its own nav chip, grouped where it belongs since watch pages are the per-video URLs.

2.7.0

Admin-only. Bulk controls for the two things a privacy-first curated library has that a feed plugin does not: per-video SEO watch pages and local-poster integrity.

  • Bulk Actions on All Videos. The native Bulk Actions dropdown gains Watch page: turn off / turn on (set a selected set's standalone-page state in one go) and Rebuild local posters (re-pull each selected video's poster from its provider, swap in the fresh copy, and drop the stale one — the repair tool for broken or missing thumbnails). A new Watch page column shows each video's On/Off state at a glance.
  • One-click whole-library toggle. Settings gains Turn off all watch pages / Turn all back on buttons that apply to every video at once, for when you want the library gallery-only without selecting row by row.

2.6.0

Admin-only. Extends the bulk importer so the watch page state is part of the import record, making it controllable per video from a file.

  • watch_page import field. A .json import record now accepts "watch_page": "0" (no standalone watch page) or "1" (force on); omit it to leave the video's current setting untouched. Accepts string, number, or boolean (false/"no"/"off" all map to off), so a hand-authored file can switch watch pages off without knowing the stored value. Paired with Overwrite, a re-import flips videos already in the library; on a fresh import it sets the state at creation. The URL/channel/playlist paste paths are unaffected (they carry no such field, so the default stays on).

2.5.1

Reworks the per-video "dedicated page" control into a clearer watch page toggle.

  • Watch page toggle (on by default). Each video can have its own standalone watch page — its own URL, with the embedded player and VideoObject schema — and the gallery card title links to it. The card still plays in place in the modal; the title link is a separate affordance, not a bypass. Untick to keep a video gallery-only: its title stops linking and its standalone URL redirects home (filterable via xrv_watch_page_off_url / xrv_watch_page_off_status).
  • The old free-text "dedicated page URL" field is retired from the editor. Any URL already saved on a video is still honored as the title's link target, so nothing breaks.

2.5.0

Two new front-end capabilities plus a per-video editor pass.

  • Embed any video anywhere. [xroad-video url="..."] now renders the privacy-first click-to-load facade for any supported URL with no library entry needed, so an editor can drop a video into any post or page by pasting its link. Optional poster (a local attachment id or URL) and title attributes; the poster is sideloaded locally on first render and cached, so the embed still makes zero third-party requests before the click. Curated library videos ([xroad-video id="123"]) keep their richer VideoObject schema.
  • TikTok support. TikTok joins YouTube, Vimeo, Wistia, Loom, and Dailymotion as an auto-detected source. Paste a tiktok.com/@user/video/... link; it renders vertical (9:16) and loads on click as TikTok's native iframe player (no embed.js), poster fetched keyless via oEmbed. Works in galleries, single embeds, and the URL embed above.

A per-video editor pass focused on a calmer, more intuitive add/edit flow. Admin-facing; the front-end gains optional mobile posters and auto-generated image renditions.

  • Single-field editor. The xroad_video post type no longer loads the full block editor body (videos are defined by their meta, not post content). The Add/Edit screen is now just a title field plus the Video Details box.
  • Opt-in dedicated-page link. The "Dedicated page URL" field is hidden behind a checkbox, so it only appears when you actually want a card to link out — a clearer default.
  • Friendly duration entry. Type the running time as minutes:seconds and the schema-accurate ISO 8601 value (PT12M30S) fills itself. A help accordion explains the format and links to a converter.
  • Native upload-date picker. Upload date is now a real date picker instead of a hand-typed YYYY-MM-DD string.
  • Poster images, expanded. The poster control is split into a clear Primary poster (16:9, grids/desktop) and an optional Mobile poster (a portrait/square crop shown ≤600px, served via <picture> and still 100% local — no third-party calls). Recommended specs live in an inline help accordion.
  • Auto-generated renditions. Registered xrv-poster (1280×720) and xrv-poster-mobile (720×960) image sizes, so WordPress generates desktop and mobile crops of every poster on upload.

2.4.0

A full admin polish pass. One shared design system now brands every screen, and a bold-carat Q&A accordion puts answers one click away. Admin-facing only; no front-end or behavior change.

  • One design system across every screen. A single, scoped admin stylesheet (Crossroad palette, card system, buttons, badges, line-icons) is now shared by Settings, Import, and the per-video editor, so the whole plugin reads as one finished product. The Import screen, previously stock WordPress chrome, now matches Settings — branded header lockup, cards, and a 1-2-3 step indicator for the import workflow.
  • Bold-carat Q&A accordions. Each screen gains a Questions & answers card: common admin questions (consent mode, API key, URL/SEO, thumbnails, Shorts, duplicates) expand inline with a bold rotating carat. The per-video editor's Rich video schema disclosure and contextual help use the same pattern.
  • Premium page headers. Every screen opens with a logo-locked hero (eyebrow, title, one-line summary, version chip) instead of a bare <h1>.
  • Sharper navigation. The Settings pill nav gains section icons and a scroll-spy active state that tracks the section in view.
  • Tidied structure. The on-demand Run a sync now control is now its own card (it previously floated outside the card grid), and section headers carry consistent icon chips.
  • Single source of version truth. A new XRV_VERSION constant feeds the script handles and update check.

2.3.0

Faster first paint and a live edge-region check. The front-end gains an LCP optimization; the new diagnostic is admin-only.

  • Faster first paint. The first card in a grid now loads its poster with loading="eager" and fetchpriority="high" (the LCP image); every other card stays lazy. No markup or API change for callers.
  • Edge check (Settings, Privacy & consent). A new admin-only diagnostic runs a live region check against the REST endpoint, times the round trip with a count-up, names the detected country, and contrasts XRV (zero extra calls, zero cookies, cache intact) with a typical geo-IP plugin. It shows a one-toggle Cloudflare hint when no region header is present. The front-end gallery is unchanged.

2.2.2

Improved UI aesthetics. Admin-facing only; no front-end or behavior change.

  • Settings cards de-cluttered. Removed the decorative { brace accent that ran down the left edge of every Settings card and tightened the reclaimed left gutter so card content now aligns symmetrically with the right edge.

2.2.0

Admin UX cleanup from a full review. No front-end behavior change; admin code got smaller, not bigger.

  • Settings consolidated. The separate Default poster, Play button, and Shorts cards are now one Appearance card (one nav pill instead of three).
  • Consent copy de-scared. "No Consent Integration" is now "Facade only (no prompt)," and the card leads with "all three modes are cookie-free until the click" so the safe option no longer reads as unsafe. The geo-source diagnostic is hidden unless the Global mode is selected (it is irrelevant otherwise).
  • Block trimmed. The block Inspector no longer duplicates site policy/look — filter style, card text, consent mode, and privacy URL now simply inherit the site defaults; the block keeps the genuinely per-instance controls (layout, columns, playback, heading, shorts, per-page, load-more, subscribe, pre-filter terms).
  • Shortcode footgun fixed. Using the singular [xroad-video] without a valid id now shows editors a visible hint pointing to the correct usage (and to the plural [xroad-videos] for galleries), instead of failing silently.
  • Dead code removed. Deleted the unreachable light consent mode and the vestigial write-once locked flag.
  • URL safety upgraded. Settings → Video URLs now shows how many published videos use the current base, warns prominently when any are live, and rewrite rules auto-flush on version change so a changed base resolves without a manual re-save. Menu label is now "XRV Video" for clarity.

2.1.2

Rebrands the product to XRV (Xroad Video) and makes the URL base editable. Admin-facing only; no breaking changes.

  • Rebrand to XRV. The admin menu, Settings title, plugin name, and block are now XRV (the maker, Crossroad Media, stays as author/logo). Internal identifiers are deliberately unchanged — the post type (xroad_video), shortcodes ([xroad-videos], [xroad-video]), block name (xroad/videos), option keys, and text domain stay the same, so existing content, embeds, and URLs keep working.
  • Default URL base is now /video/ (was /xroad-video/) for a cleaner single-video path. Fresh installs pick this up automatically.
  • The video URL base is now editable in Settings → Video URLs, rather than write-once. Each change is gated behind a confirmation box, an inline warning, and a two-part test: before changing a base that is already live, check (1) organic/search traffic to that path and (2) inbound or internal links to it. If either exists, it is a showstopper — don't change the base without a full 301-redirect migration first. (Also avoid a base that collides with an existing page slug.)

2.1.1

Finishes the Shorts story end-to-end, refines the admin UI, and bounds bulk imports. No new dependencies; the only front-end weight added is the swipe-shelf CSS.

  • Import is capped to 50 videos per run. A pasted playlist or channel of thousands no longer fires thousands of metadata + Shorts-probe + thumbnail requests at once — the preview reports the overflow ("N more found — capped to 50 per import") and the run step enforces it server-side. Run the import again to add the rest. Self-hosted MP4/WebM stays one file at a time (one video per post).

  • Admin brand accent fixed. The card { accent is now uniformly scaled and vertically centered (background-size:contain) instead of stretched to full card height — stretching a stroked brace distorted it into a thin wobbly line.

  • Synced/imported Shorts are auto-flagged. The channel sync and the bulk importer now detect a Short for each new video (a one-time HEAD probe of the canonical /shorts/ URL — the Data API exposes no aspect ratio) and store the flag, so Shorts pulled in automatically render vertical with a portrait poster instead of needing a manual /shorts/ paste or re-save.

  • Swipe shelf for shorts="only". A verticals-only gallery now lays out as a horizontal, thumb-swipeable scroll-snap strip (the familiar Shorts shelf) rather than a grid of tall cards. Pure CSS, scoped to that one layout.

  • Block-editor Shorts control. The Crossroad Videos block gains a YouTube Shorts selector (Site default / Show alongside / Only / Hide) in its Browse panel, mirroring the shortcode shorts attribute. Blank inherits the Settings default (the render now falls back to the site default rather than forcing all).

  • DM Sans on the admin chrome. The Settings UI now prefers DM Sans where it's present on the system, falling back to the native UI sans stack. Deliberately not bundled: Google Sans (the brand display face) is proprietary and not licensed for self-hosting, and shipping font binaries would contradict the plugin's zero-dependency, no-third-party-request posture. The palette and logo already carry the brand; this adds the typeface for anyone who has it at zero cost.

2.1.0

Closes the gaps raised in a video-infrastructure review. All four additions reuse existing rendering; no new dependencies, front-end payload effectively unchanged.

  • YouTube Shorts. Shorts are auto-detected from their /shorts/ URL and treated as the vertical (9:16) videos they are: a portrait poster (sideloaded from YouTube's original-aspect oardefault image, not a letterboxed 16:9 frame), a 9:16 facade, and a portrait lightbox that pops on click and loops (the Shorts feel). In inline playback they play in their vertical card instead. A shorts attribute — all (default), only (a verticals-only shelf), or hide — plus a default in the new Settings → Shorts tab controls how each gallery treats them. This directly resolves the common WordPress Shorts headaches (black-bar aspect ratio, theme/oEmbed wrapper conflicts, letterboxed thumbnails, manual /shorts/→/embed/ conversion, pre-consent cookies, heavy on-load iframes) — the facade architecture pre-empts most by design. (Channel-imported shorts normalize to a watch URL, so flag those by adding via the /shorts/ URL or a re-save.)
  • Selectable video URL structure (write-once). Settings → Video URLs lets you choose the single-post base (e.g. /video/{slug}) and an optional collection/archive base (e.g. /videos/) — any combination. It's a deliberate, one-time choice: once locked it can't be changed (live URLs and SEO depend on it), and locking flushes rewrite rules once. Defaults preserve the historical /xroad-video/ base, so existing installs are unchanged until an admin sets a structure.
  • Single-video embed. New [xroad-video id="123"] shortcode drops one video inline anywhere — same privacy-first click-to-load facade and VideoObject schema as its dedicated page, for editorial placements like an About page that sit a specific video in the page flow (replacing raw youtube-nocookie iframes that otherwise contact Google on page load). Reuses xrv_render_single(), so it works for every 2.0 source. Embed by post ID; URL-paste resolution is a documented upgrade path. Add playback="lightbox" to pop the video in a centered modal (matching a Divi/Magnific video-popup) instead of playing in place.
  • Full description on dedicated pages. A video's own page now shows its complete description (the card line-clamp is lifted only in single-video context), so there's a place to read the full text the grid preview truncates.
  • Incomplete videos stay out of galleries. A video with no resolved platform ID can't play, so it is no longer rendered as a dead, unclickable card — it is skipped until a working URL is added. Fixes the "some videos show with no thumbnail and aren't clickable" report at the source.
  • Settings UI redesign. The flat run of option rows is now a set of clearly-headed cards with a sticky section nav (Privacy, Browse, API key, Default poster, Play button, Auto-sync), so groups are scannable and harder to miss. The long consent-mode explainer collapses into a disclosure so the default view stays compact. The video editor's Video Details box now tucks the optional transcript/chapters ("Rich video schema") behind a disclosure too, so the core fields lead. Pure CSS / native <details> — no admin JS added. (The Import screen was left as-is: it's a focused task flow, not a list.) The admin chrome is themed to the Crossroad Media brand palette (deep purple lead, brand-blue support, warm-gray neutrals, brand radii + signature shadow; orange left reserved) — applied to the plugin's own UI only; the front-end gallery still inherits each tenant's brand. No web fonts loaded (keeps the zero-dependency, no-third-party-request posture). Also de-duplicated the YouTube provider-meta write shared by the importer and channel sync into one helper.
  • Play-button color control. Settings → Play button color adds a brand color combo (idle + hover) for the click-to-load play icon, via two native color pickers. Tick to override; left off, the button inherits the theme's --xrv-primary / --xrv-action brand tokens. Emits two scoped CSS variables (--xrv-play, --xrv-play-hover) only when set, so it never affects the other elements that share the brand tokens.
  • SECURITY.md added: disclosure contact, security posture (no secrets in the repo, scheme-guarded fetches, escaped output, gated writes), and the recommended YouTube API-key restriction for site operators.

2.0.1.beta

Beta release for testing the playlist picker before 2.0.1 stable. Adds a YouTube playlist picker to the importer. On the Import screen, enter a channel (URL, @handle, or name) and click List playlists to see that channel's public playlists with their video counts; pick one and it loads into the Videos box, ready to preview and import. Saves you finding and pasting an exact ?list= playlist URL. Needs the same free YouTube Data API key the importer already uses; admin-only, no front-end change.

2.0.0

Multi-source. The gallery is no longer YouTube-only: it now plays Vimeo, Wistia, Loom, Dailymotion, and self-hosted MP4 / WebM files through the same click-to-load facade, with the provider auto-detected from the pasted URL. Still a single-file, zero-dependency plugin, and the zero-third-party-request-until-click guarantee holds for every source (self-hosted files make no third-party contact at all). No new API keys: each host's title, poster, and (for all but YouTube) duration come from its no-key oEmbed endpoint.

  • Five new providers via the existing _xrv_provider seam. Vimeo, Wistia, Loom, Dailymotion, and self-hosted files join YouTube. Each routes through the same switch for ID parsing, embed URL, oEmbed endpoint, and poster source, so the renderer, schema, lightbox, consent modes, layouts, and search/sort are unchanged and behave identically across sources. Galleries can mix providers in one grid.
  • Auto-detect provider. The editor's Provider selector defaults to Auto-detect: paste a URL and the host is recognized from it, so editors never choose a provider by hand. The selector is still there to force a choice and to pick Self-hosted file.
  • Self-hosted files (provider:file). Paste a direct MP4 / WebM URL and the card plays it in a native <video> element on click. No iframe, no oEmbed, no external request at any point: the most private option, and a natural fit with the password-gate stack for client review reels. Upload a poster image in the editor, since self-hosted files have no remote thumbnail to fetch.
  • Vimeo privacy hash. Unlisted and domain-private Vimeo videos (vimeo.com/{id}/{hash}) keep their hash, so the on-click embed is reconstructed correctly. Vimeo plays with dnt=1 and no title / byline / portrait chrome.
  • No third-party thumbnail relay. Posters for the new hosts are sideloaded from each host's own oEmbed thumbnail_url and stored locally like the YouTube poster, so there is no dependency on any external thumbnail service.
  • Duration normalization. oEmbed durations (returned in seconds) are normalized to ISO 8601 on save, so the existing clock display and shortest/longest sort work across every source with no other change.
  • Scope. The bulk importer and automatic channel sync stay YouTube-only in this release; multi-source bulk import is planned for 2.5. The enterprise tier (Brightcove, Kaltura) is intentionally out of scope, since composite account/player identity needs a providers settings panel.
  • Payload. Front-end inline assets are about 30.3 KB raw / 8.7 KB gzipped per page (the provider routing adds roughly 1.2 KB of JS over 1.0.9); admin and importer code is unchanged, and everything is still emitted once per request.

1.0.9

Adds automatic channel sync: the plugin can poll a YouTube channel or playlist for new uploads and add them to the library, either on a schedule you set or on demand. Still a single-file, zero-dependency plugin; the sync engine and its controls are admin-only and add nothing to the front-end payload (unchanged at about 29 KB raw / 8.3 KB gzipped per page).

  • Automatic channel sync (Videos → Settings → Automatic channel sync). Point it at a channel URL (/@handle, /channel/UC…, /user/…) or a ?list=… playlist and it scans the most recent uploads, adding any video not already in the library. Dedup is by video ID (_xrv_video_id), so a run is always idempotent and safe to repeat. Each new video gets its title, duration, upload date, description, and a sideloaded thumbnail, exactly like the importer. Requires the (free) YouTube Data API key already used by Import.
  • **Schedu

This README is longer than the copy stored here. Read the rest on GitHub →

Releases

Newest 25 of 29 recorded releases. Each count is every asset in that release; expand a row for the breakdown.

Tag
Published
Assets
Downloads
v2.11.3 latest
Oct 20, 2026 5d ago
xroad-videos.zip
1
Oct 2, 2026 23d ago
xroad-videos.zip cohort
2
Oct 1, 2026 24d ago
xroad-videos.zip
1
Oct 1, 2026 24d ago
xroad-videos.zip
0
Jul 4, 2026 3mo ago
xroad-videos-v2.8.0.zip
2
Jul 4, 2026 3mo ago
xroad-videos-v2.7.1.zip
0
Jul 4, 2026 3mo ago
xroad-videos-v2.7.0.zip
0
Jul 4, 2026 3mo ago
xroad-videos-v2.6.0.zip
0
Jul 4, 2026 3mo ago
xroad-videos-v2.4.0.zip
0
Jul 4, 2026 3mo ago
xroad-videos-v2.2.0.zip
0
Jul 4, 2026 3mo ago
xroad-videos-v2.1.1.zip
0
Jul 4, 2026 3mo ago
xroad-videos-v2.1.0.zip
0
Jul 4, 2026 3mo ago
xroad-videos-v2.0.1.beta.zip
0
Jul 4, 2026 3mo ago
xroad-videos-v2.0.0.zip
0
Jul 4, 2026 3mo ago
xroad-videos-v1.0.9.zip
0
Jul 4, 2026 3mo ago
xroad-videos-v1.0.8.zip
0
Jul 4, 2026 3mo ago
xroad-videos-v1.0.7.zip
0
Jul 4, 2026 3mo ago
xroad-videos-v1.0.6.zip
0
Jul 4, 2026 3mo ago
xroad-videos-v1.0.5.zip
0
Jul 4, 2026 3mo ago
xroad-videos-v1.0.4.zip
0
Jul 4, 2026 3mo ago
xroad-videos-v1.0.3.zip
0
Jul 4, 2026 3mo ago
xroad-videos-v1.0.2.zip
0
Jul 4, 2026 3mo ago
xroad-videos-v1.0.1.zip
0
Jul 4, 2026 3mo ago
xroad-videos-v1.0.0.zip
0
Jul 4, 2026 3mo ago
xroad-videos.zip
1
All releases on GitHub →