Marketing Video Reels
A WordPress plugin I'm building to deliver sample reels to prospective clients
Install
No release zip yet. The repository archive installs, but the folder name will carry the branch suffix and updates will not flow:
wp plugin install https://github.com/johnkakuk/sample-reel-delivery/archive/refs/heads/main.zipReadme
Marketing Video Plugin
A WordPress plugin for delivering a free personalized reel to a prospect through a phone-friendly, swipe-through landing page — instead of just texting them a video file.
The visitor lands on a reels custom post (slug configurable) and swipes
through four full-screen, portrait (9:16) slides:
- Personalized video — a short "Hey NAME, I shot this for you" intro. Requires a tap to start (browsers block autoplay-with-sound on the very first interaction).
- Deliverable video — the actual free reel. Autoplays, has a download button, and once it finishes shows a thumbs up/down reaction overlay (up advances to the VSL, down shows a "thanks — want changes? email me" note) with a "Watch again" replay option.
- VSL — a reusable pitch video, autoplays, picked from a dropdown of past VSLs rather than re-uploaded per post since one may get reused for months.
- Booking — a Calendly embed, loaded immediately in the background so it's ready by the time the visitor scrolls there.
Every video's progress bar is scrubbable (click/drag to seek), shows a buffering spinner when it genuinely has to wait on data, and videos load one at a time in priority order (the current one first, the next one once the current one starts playing) rather than all three competing for bandwidth at once — self-hosted video has no adaptive streaming or CDN to fall back on, so this matters more here than it would embedding a YouTube player.
Status: Alpha
Feature-complete and tested end-to-end — built and iterated against a local Docker WordPress sandbox throughout, and validated on the author's real production site (WordPress + Elementor) across desktop and iOS (Safari and Chrome, both WebKit under the hood).
Packaged as marketing-video-plugin-0.1.0-alpha.zip in the project root —
upload via Plugins → Add New → Upload Plugin, or extract into
wp-content/plugins/marketing-video-plugin/. Re-run the packaging steps
below after any further change; the zip is gitignored (a generated
deliverable, not source) and needs re-exporting by hand.
What's in the box
marketing-video-plugin.php+includes/— the plugin itselftemplates/single-mvp_reel.php— the full-bleed reel template (bypasses the theme's header/footer entirely, since the swipe experience needs the whole viewport)assets/dist/— the built front-end bundle the plugin enqueues (built from/frontend, committed since WordPress doesn't run a build step)assets/admin/— the meta box JS/CSS (media picker)assets/images/— static plugin assets (the thumbs-up/down icon)/frontend— the front-end source; required for making any future change to the reel experience —assets/dist/reel.jsis generated, minified output, not something anyone would hand-editdocker-compose.yml— a local WordPress + MariaDB sandbox for testing (see below)
Known gaps / things worth knowing before relying on this further
- No uninstall cleanup — deactivating/deleting the plugin leaves reel data in the database.
- No admin UI for poster/thumbnail images on videos.
- Video compression is a manual step in whatever export tool is used (guidance on bitrate/ffmpeg settings came up during development, not automated in the plugin — see conversation history if picking this back up, or just ask again).
- The WordPress admin bar isn't suppressed on the reel template for logged-in viewers — it'll show at the top of the full-bleed layout.
- Tested against one theme (a WordPress default block theme) and one real-world stack (Elementor); other page builders/themes are unverified.
Plugin architecture
Two custom post types:
mvp_reel(menu: "Demo Reels", public, rewrite slug configurable via Settings, defaultreels) — one per prospect. Meta boxes:- Personalization — Recipient Name, Company Name. Not rendered as on-page text (the personalized video carries the message) — instead used to build the page's SEO title / link-preview text, e.g. "Video for Mike, Anderson Roofing" (what shows up when the link is texted).
- Videos — personalized + deliverable video pickers (media library), download filename + toggle, VSL dropdown.
- Booking — Calendly link; falls back to the site-wide default in Settings if left blank.
- Notes — a private WYSIWYG field, admin-only, never sent to the front end.
- Analytics (sidebar) — read-only counters: Loads, Intro Views, Reel Views, Likes, Dislikes, Downloads, VSL Views, Calendar Views. No booking count — Calendly already tracks that. See Analytics below.
mvp_vsl("VSL Library", admin-only, nested under the Reels menu) — named video entries (title + one media-library video) that feed the VSL dropdown on a reel. Since a VSL often gets reused for months, this avoids re-picking a media library file on every single reel while still keeping every video backed by the media library.
Settings (Reels → Settings): reel URL slug, and a default Calendly link used whenever a reel doesn't set its own.
MVP_Frontend reads a reel's meta into the exact shape of the front
end's ReelData TypeScript interface, injects it as window.REEL_DATA
before the bundle runs, swaps in the plugin's own template via
template_include (a theme can override this by providing its own
single-mvp_reel.php), builds the SEO title/Open Graph tags, and —
critically for page-builder compatibility — strips every other enqueued
style/script from the reel page (strip_other_assets(), running dead last
on wp_enqueue_scripts), since page builders like Elementor otherwise load
their global CSS/JS on every page by default and visibly clash with this
one's full-bleed design.
MVP_Analytics owns the whole analytics pipeline:
- A public REST route (
POST /wp-json/mvp/v1/track) the front end beacons events to for anything play/interaction-driven (video plays, likes, downloads, slide views). - Loads is tracked separately, server-side, on
template_redirect— not a beaconed event — so it counts every qualifying page render even if the JS bundle never executes. Intro Views is deliberately not the same number: it's driven by the front end'svideo_playevent, so it only counts when the first video is actually played, not just scrolled to (which happens automatically on page load, before any interaction). - Every counting path excludes logged-in visitors — the reel owner's own
testing/preview visits never inflate the numbers. Worth noting: this
required
wp_validate_auth_cookie( '', 'logged_in' )rather than the more obviousis_user_logged_in()inside the REST callback specifically — WordPress's default current-user resolution checks the admin-scoped auth cookie, which browsers never send to a non-/wp-adminURL, sois_user_logged_in()reads as logged-out there even for an authenticated admin. (template_redirect, used for Loads, doesn't have this problem — regular page requests resolve the current user normally.) - The first time a reel gets a non-logged-in load,
wp_mail()notifies the site admin email.
Frontend
Framework-free Vite + TypeScript. Slide paging uses native CSS scroll-snap
(no swipe library). npm run build outputs straight into assets/dist/
with fixed filenames (reel.js / reel.css) so the PHP side can enqueue
them without a manifest.
cd frontend
npm install
npm run dev # standalone dev server, mock data
npm run build # builds into ../assets/dist for the plugin to enqueue
Key files:
src/types.ts— theReelDatashape WordPress injects aswindow.REEL_DATAsrc/mock-data.ts— stand-in data for standalone dev (swap for real footage/links as needed)src/modules/video-controller.ts— per-slide play/pause/scrub, progress bar, staggered-loading hooks, near-end swipe-hint triggersrc/modules/analytics.ts— fires the interaction-driven events; posts toReelData.analyticsEndpoint(the plugin's REST route) when setsrc/modules/calendly.ts— lazy-loads the Calendly inline widgetsrc/modules/device.ts— touch vs. pointer detection (swipe vs. scroll hint wording)
A couple of non-obvious things worth knowing if picking this back up:
- The deliverable video's
<video>src gets#t=0.001appended (main.ts) — a documented WebKit workaround for a real bug where iOS Safari shows a plain black box instead of a video's first frame unless it's autoplaying. Don't remove it without re-testing on an actual iPhone. - Interactive elements'
:hoverCSS rules are wrapped in@media (hover: hover)(style.css) — iOS otherwise needs two taps on anything with a bare:hoverrule (first tap simulates hover, second actually clicks).
Note: frontend/public/demo/ holds large sample video files (~100MB total)
used only for local testing — gitignored, not shipped.
Local WordPress sandbox (Docker)
docker compose up -d
docker compose run --rm wpcli wp <command> # one-off WP-CLI commands
WordPress at http://localhost:8080, admin login admin / admin. The
whole project directory is bind-mounted into
wp-content/plugins/marketing-video-plugin, so PHP edits are live
immediately; frontend changes need npm run build first. Note the
wordpress:cli image defaults to a different www-data UID (Alpine, 82)
than the wordpress image (Debian, 33) — the wpcli service in
docker-compose.yml is pinned to user: "33:33" to avoid file-ownership
mismatches on uploads.
Packaging a release zip
STAGE=/tmp/marketing-video-plugin # or any scratch dir
rm -rf "$STAGE" && mkdir -p "$STAGE"
cp marketing-video-plugin.php "$STAGE/"
cp -R includes templates "$STAGE/"
mkdir -p "$STAGE/assets/admin" "$STAGE/assets/dist" "$STAGE/assets/images"
cp assets/admin/* "$STAGE/assets/admin/"
cp assets/dist/reel.js assets/dist/reel.css "$STAGE/assets/dist/"
cp assets/images/thumb.png "$STAGE/assets/images/"
cd "$(dirname "$STAGE")" && zip -r -X marketing-video-plugin.zip "$(basename "$STAGE")"
(Rebuild the frontend first with npm run build if anything in frontend/
changed.)