Install
This is the repository archive. It installs, but the folder name will carry the branch suffix and updates will not flow.
Install straight from GitHub with WP-CLI:
wp plugin install https://github.com/johnkakuk/sample-reel-delivery/archive/refs/heads/main.zipMarketing 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.)