Skip to content

The Library

Damilola Ogunsakin edited this page Oct 18, 2026 · 9 revisions

The Library

Desktop Mobile
The library on desktop The library on mobile

The tour shows videos inside your app, help on each page, searching, each screen size, a video in French, and asking for a video that is missing. Click a preview for the full MP4.

Your users watch videos in the library:

Library Address Shows
Public /help/videos Videos marked public
Private /admin/videos All published videos, plus the videos users asked for. Behind auth.

Both prefixes, their middleware and their domains are in the config (library.public, library.private). The private library uses Laravel's auth middleware, which sends signed-out visitors to your login route; if your app signs people in another way, change library.private.middleware.

The library reads the published manifest, so production needs only the storage disk, not the studio or its database.

Each video's page

Under the video is a written Step by step guide, made from its plan at no AI cost: the chapters as headings, the steps as instructions ("Click Post a job", "Fill in Job title and Location"), and the captions as notes. It's for people who would rather read, for screen readers, and for search engines, which index text and not video. What was typed into fields isn't shown, as it is sample data. Videos published since v0.17.11 have one without being published again.

The words around the labels follow the video's language: English, French, Spanish, German, Portuguese and Italian come with the package (a region, such as pt-BR, uses its language's), and other languages fall back to English. To add a language or reword one, publish them and edit lang/vendor/video-demo/{locale}/guide.php:

php artisan vendor:publish --tag=video-demo-lang

Public video pages also have what search engines and link previews read: a description, a canonical address, preview tags with the video's poster and file (so a link shared in Slack or WhatsApp shows them), and VideoObject structured data. /help/videos/sitemap.xml lists every public video with its poster, file and length: add it to your robots.txt (Sitemap: https://your-app.com/help/videos/sitemap.xml) or Google Search Console. Private library pages ask search engines not to index them.

In any frontend (Vue, React…)

For a frontend that isn't Blade, such as a separate frontend, load the library's web component and use it like any element:

<script src="https://your-app.com/help/videos/embed.js" defer></script>

<video-demo-player slug="post-a-job"></video-demo-player>
<video-demo-player slug="post-a-job" device="mobile"></video-demo-player>
<video-demo-player slug="post-a-job" language="fr"></video-demo-player>

<video-demo-help></video-demo-help> lists the public videos that show the page it is on (path="/jobs/new" for another page), behind a "Videos for this page" button, and plays the one chosen. It follows a single-page app's navigation, and renders nothing on a page no video shows.

<video-demo-player> plays a public video in the version for the screen it is on, in the page's language (<html lang>) when there is a version in it, with subtitles when it has a voice-over, and renders nothing for a video that isn't published. In Vue, tell the compiler it's a custom element (compilerOptions.isCustomElement: tag => tag.startsWith('video-demo-')). It fires video-demo:ready with the video's details.

Plays and how far viewers get count towards the library's watch counts when they are on.

To build your own player instead, GET /help/videos/{slug}.json (?lang=fr for another language) gives the title, chapters and each version's video, poster and subtitles, with full addresses. Both can be fetched from any address. Private videos aren't available this way. Subtitles from another address need your storage to allow it (CORS).

In your own pages

{{-- Plays the version that suits the viewer's screen --}}
<x-video-demo::player slug="post-a-job" />

{{-- Always the mobile version --}}
<x-video-demo::player slug="post-a-job" device="mobile" />

{{-- A slim "How-to video" bar that opens into the video and minimises again --}}
<x-video-demo::player slug="post-a-job" collapsible />

{{-- A search box that opens the library --}}
<x-video-demo::search placeholder="How do I…" />

collapsible suits a video placed inside a page, such as above a form: it starts minimised (add open to start it open), takes its colours from your page so it fits light and dark designs, and remembers whether each viewer keeps it open. It needs no JavaScript to open and close.

Each published video's page in the studio has this code ready to copy, plus a plain HTML <video> tag for pages outside your app.

A video made in other languages plays in the page's language when there is a version in it. language="fr" picks one, and :language="false" always plays the video named.

Videos for this page

Put this in your layout once, and every page shows the videos made of it:

<x-video-demo::help />

It shows a Videos for this page button that opens into the list, each ready to play. Pages no video shows render nothing. Pages are matched by route, so a video that opens /jobs/7 is listed on /jobs/42 too, and videos that start on the page come first.

Attribute What it does
route="jobs.create" or route="/jobs/create" List another page's videos
area="private" Include private videos
label="Help with this page" The button's words
:open="true" Start it open

It takes its font from your page, and its colours follow light and dark mode. VideoDemo::forPage('jobs.create') gives the same list for a menu of your own. Videos published before v0.17.0 don't know their pages yet: publish them again (or run video-demo:refresh) to list them.

To restyle the library pages, publish the views:

php artisan vendor:publish --tag=video-demo-views

Videos users ask for

When a search finds nothing, users can ask for a video. Requests are stored in production. To see them in your local studio, set the same VIDEO_DEMO_REQUESTS_TOKEN in both environments and VIDEO_DEMO_PRODUCTION_URL locally.

How videos are watched

Set VIDEO_DEMO_STATS=true in production to count:

  • Plays, and how many viewers reach a quarter, half, three quarters and the end of each video. The player sends these as the video plays, once per page view.
  • Searches that found nothing, by their words.

Only daily totals are kept, in the video_demo_stats table: no cookies, IP addresses or user ids. With the requests token set (above), the studio reads them from production:

  • A published video's page shows its plays, how many watched to the end, and where most viewers stop, such as "between 25% and 50%, around 0:23, in 'Fill in the details'".
  • The videos page lists searches that found nothing, each a link to start a video.
  • Ideas puts what users asked for and searched for first.

Publishing to production

There is no separate step to send a video to production. Publish in the studio writes the video's files and the list of published videos (manifest.json) to the storage disk, and production's library reads that same disk. Set up a disk both machines reach once, and every video you publish after that appears in production without a deploy.

Once

  1. Install the package in your app as usual, so it ships with your deploys, and run php artisan migrate in production. Production doesn't need Node, ffmpeg or an AI key. The studio stays off there (it is on only when APP_ENV is local), and the library is on at /help/videos and /admin/videos. Production's tables hold only requests for videos and, if you turn them on, watch counts.

  2. Use a shared disk, with the same bucket and keys in both .env files, on your machine and in production:

    VIDEO_DEMO_DISK=s3
    AWS_ACCESS_KEY_ID=...
    AWS_SECRET_ACCESS_KEY=...
    AWS_DEFAULT_REGION=...
    AWS_BUCKET=...
    AWS_URL=https://videos.example.com   # the bucket's public address, or your CDN's (needed for R2)
    AWS_ENDPOINT=...                     # for R2 and other S3-compatible storage

    The default disk, public, is a folder on whichever machine published the video. With it, videos you publish on your laptop stay on your laptop and the production library stays empty. To skip shared storage, keep them in your codebase instead.

  3. Let viewers' browsers read public videos. Public videos are linked by the disk's address (AWS_URL), so the bucket must serve them: a public bucket, R2's public domain, or a CDN in front. Private videos get links that last two hours on disks that support them, such as S3 and R2, so a private bucket works for them. On a disk without such links, anyone with a private video's address can open it.

  4. If production caches its config, run php artisan config:clear (or config:cache again) after changing .env.

Each video

  1. Make it in the studio on your machine, choose Public or Private (staff) under Library (in More options on New video, or in the video's Details), and press Publish.
  2. Production shows it within 5 minutes, how long it caches the manifest (library.cache_seconds).
  3. Its addresses: https://your-app.com/help/videos/{slug} for a public video, /admin/videos/{slug} for a private one, and the lists at /help/videos and /admin/videos.

Each version is published under a new file name, so viewers and CDNs never get an old cut. Unpublish takes the video out of the manifest. Next to its files is the video's studio record ({slug}-v{version}.json: its prompt, plan, style and voice), so the studio can bring it back if its database loses it. The record sits on the same disk with the same reach as the video, so on a public disk it can be read by anyone who has its address.

<x-video-demo::player slug="…" /> and <x-video-demo::help /> in your pages are code, so they reach production with your next deploy. After that, publishing a new version updates what they play without one.

Or keep them in your codebase

Without shared storage, keep published videos in the app's public folder and deploy them with your code. In the .env where you make videos, and in production:

VIDEO_DEMO_DISK=video-demo-repo
  1. Publish in the studio. The videos, posters and manifest.json are written to public/video-demo.
  2. Commit that folder and deploy. The studio reminds you after each publish.
  3. Production shows the change as soon as the deploy is done.

Republishing replaces a video's old files and Unpublish removes them, so the folder holds only what is live. Commit the removals too.

Things to weigh:

  • Repository size. Each published video adds a few MB per screen size, and every version stays in git history. For more than a few dozen videos, a shared disk such as S3 or R2 is lighter, or track public/video-demo/*.mp4 with Git LFS.
  • Private videos are plain files in a public folder: anyone with a video's address can open it, and anyone who can read the repository can see them. Use a shared disk with temporary links for videos that must stay private.
  • Several servers each get the folder with the deploy, so it works behind a load balancer too.

Where files are linked from

  • On a disk on the app's own server (such as public), videos are linked by path, /storage/video-demo/…, so they play at whatever address the app is opened at.
  • On a disk somewhere else, such as S3 or a CDN, they keep the disk's full address.

Seeing production from your studio

Set the same VIDEO_DEMO_REQUESTS_TOKEN in both .env files and VIDEO_DEMO_PRODUCTION_URL=https://your-app.com on your machine. The studio then shows the videos users asked for, the searches that found nothing and, with VIDEO_DEMO_STATS=true in production, how each video is watched. To keep published videos in step with your code, see Keeping Videos Up To Date.

After a redesign

Plans are stored with the videos, so after your app's look changes, record them again at no AI cost:

php artisan video-demo:refresh     # every published video

Or use Record the same plan again on one video.

Clone this wiki locally