Skip to content

Two failure modes that ship a silently inert page (CSP media-src, prefers-reduced-motion) #9

Description

@yepgent

Thanks for the skill — I ran it end to end and shipped a page with it (yepgent.com/world/, 6 scenes, architecture A). Two issues made the page render as static posters in production while every local check passed. Both are environmental, so local QA cannot see them.

Happy to open a PR for any of this if you'd like it.


1. blob: media needs an explicit CSP media-src

The engine's blob-loading strategy is the right call — it's what keeps clips seekable on hosts without byte-range support. But on any site with a CSP, blob: is not covered by default-src 'self'. Result:

MEDIA_ELEMENT_ERROR: Media load rejected by URL safety check

readyState 0, videoWidth 0, every clip dead, page renders as six posters. Reproduced by serving the same build with the production CSP string, then fixed by adding one directive:

prod CSP + media-src 'self' blob:
readyState 0 4
videoWidth 0 1280
currentTime on scroll 0 → 0 3.66 → 5.01

Why it evades QA: python -m http.server sends no CSP header at all, so the dev surface is strictly more permissive than production.

Suggested: a Step 0 preflight that curls the deploy target and asserts media-src names blob: (and greps netlify.toml / _headers / vercel.json), plus a line in SKILL.md Gotchas. This is cheap to check before generating and expensive to discover after.


2. prefers-reduced-motion leaves no way in

Under reduced motion the engine creates zero <video> elements and leaves the posters up. On a page whose only content is the camera flight, that's a dead end rather than an accommodation — the visitor gets a page that does nothing, with no affordance suggesting otherwise.

Why it evades QA: Playwright's bundled Chromium reports prefers-reduced-motion: false. I only found it by instrumenting the actual user's Chrome over CDP: {reduced: true, videos: 0}.

Suggested: keep the OS setting as the default, but let the host page offer an explicit opt-in. A one-line change makes this possible:

const reduce = config.forceMotion ? false
  : window.matchMedia('(prefers-reduced-motion: reduce)').matches;

The page then renders a small bar ("Your system asks for reduced motion… [Play the flight]") that re-mounts with forceMotion: true. Worth noting in the docs that anything injected after mount must re-run on that second mount.


3. Smaller things

  • Poster = frame 0 is wrong when a leg round-trips. With seedance_2_0 I found legs that end where they began (see below), which makes frame 0 the previous scene's arrival frame — so the reduced-motion fallback illustrated each section with the wrong picture. Using each section's resting frame fixed both that and the paint-in pop.
  • cta.href: '#finale' in index-template.html matches nothing — the engine never assigns section ids to any element, so the header CTA is inert.
  • The handoff clause [the doorway / opening / direction of the next scene] — the third option is a trap. Describing the next scene vividly made the model spend the whole leg arriving there and skip the scene it was meant to traverse. Worth narrowing the template to a direction only.
  • Model tiers fail in opposite directions. seedance_2_0_mini under-weights the start image (chases the described destination); seedance_2_0 over-weights it (won't leave the start frame's world, and ambiguous directional referents that match something in the start frame resolve backward). A fix tuned on one tier can break the other — might be worth flagging in the Step 4 table.

Great skill overall — the frame-locked chain genuinely works, and seams were invisible once the prompts were right.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions