Skip to content

fix(import): route YouTube iframes and <video> elements to the base.video module#151

Draft
DavidBabinec wants to merge 2 commits into
mainfrom
fix/import-video-embeds-to-base-video
Draft

fix(import): route YouTube iframes and <video> elements to the base.video module#151
DavidBabinec wants to merge 2 commits into
mainfrom
fix/import-video-embeds-to-base-video

Conversation

@DavidBabinec

Copy link
Copy Markdown
Contributor

Summary

The gap: HTML_TO_MODULE_RULES had no rule for <iframe> or <video>, so they fell through to the catch-all * rule and became base.container nodes with tag:'custom', customTag:'iframe' (or 'video'). base.container declares no CSP sources, so YouTube iframes stayed blocked by the published page's frame-src 'none' even after PR #141's relaxation — that only fires when base.video renders and calls renderYoutube() which returns YOUTUBE_CSP_SOURCES. The bug affects all three consumers of the importer: the paste-HTML modal, the full-site Super Import, and the AI agent's site_insert_html/site_replace_node_html tools.

The fix:

  • src/core/htmlImport/rules.ts — two new rules added before the catch-all *:

    • iframe rule: checks if the src hostname is a YouTube domain (youtube.com, m.youtube.com, youtube-nocookie.com, youtu.be) via an inline host check (layering forbids importing parseYoutubeId from src/modules/). YouTube iframes map to base.video with videoUrl, title, noRelatedVideos, and playsinline all read from the element. Any other iframe (Vimeo, Maps, forms, arbitrary) falls back to base.container with tag:'custom', customTag:'iframe' — same as before, so non-video iframes continue to work and carry their attributes via htmlAttributes. recurse: false.
    • video rule: maps to base.video with videoUrl from <video src> or the first <source src> child, and boolean attrs controls, autoplay, loop, muted, playsinline. recurse: false so <source> children are consumed, not emitted as extra nodes.
  • src/modules/base/video/props.ts — two new props added to VideoPropsSchema:

    • title (default 'YouTube video') — replaces the hardcoded string in the iframe title attribute, improving accessibility.
    • noRelatedVideos (default false) — appends rel=0 to the YouTube embed URL to suppress recommended videos.
  • src/modules/base/video/youtube.tsyoutubeEmbedUrl() updated to accept noRelatedVideos and build the query string from both autoplay and noRelatedVideos together.

  • src/modules/base/video/index.tsrenderYoutube() receives title and noRelatedVideos from props and uses them. Both fields added to the module schema so they appear in the editor panel.

  • src/modules/base/video/VideoEditor.tsx — canvas preview iframe wired up with props.title and props.noRelatedVideos.

What still falls back to base.container:
Vimeo, Google Maps, and arbitrary third-party iframes still map to base.container with customTag:'iframe'. Full iframe embedding for non-YouTube providers requires a future site-level trusted-frame-hosts allowlist so frame-src can be relaxed safely.

AI agent fix included: site_insert_html uses the same importer, so AI-pasted YouTube embeds now also land as base.video and render correctly in published pages.

Verification

bun run build   # tsc -b && vite build — clean
bun test        # 5933 pass, 0 fail
bun run lint    # 0 errors, 0 warnings

Targeted tests:

  • src/__tests__/htmlImport/mapping.test.ts — 18 new tests in base.video — <iframe> import mapping and base.video — <video> import mapping (YouTube URL variants, non-YouTube fallback, title/noRelatedVideos/playsinline mapping, source-child fallback, no children on leaf nodes)
  • src/__tests__/base-modules.test.ts — 5 new tests for title and noRelatedVideos props; schema shape test updated to include both new fields
DavidBabinec and others added 2 commits July 1, 2026 22:04
…ideo module

YouTube <iframe> embeds and <video> elements imported via paste-HTML, Super
Import, or the AI agent's site_insert_html tool previously fell through to the
catch-all rule as base.container nodes. base.container declares no CSP sources,
so YouTube iframes stayed blocked by the published page's frame-src 'none' even
after PR #141 (which only fires when base.video renders).

New rules added to HTML_TO_MODULE_RULES in src/core/htmlImport/rules.ts:
- iframe: YouTube host check (youtube.com/m.youtube.com/youtube-nocookie.com/
  youtu.be) maps to base.video; any other iframe falls back to base.container
  with tag:'custom',customTag:'iframe' so Vimeo, Maps, and arbitrary embeds
  keep working. rel=0 in the embed URL sets noRelatedVideos:true; playsinline=1
  sets playsinline:true; the title attribute maps to the title prop.
- video: maps to base.video with videoUrl from <video src> or the first
  <source src> child, and all boolean attributes (controls, autoplay, loop,
  muted, playsinline). recurse:false so <source> children are consumed, not
  emitted as extra nodes.

Inline YouTube host detection in rules.ts — src/core/ must not import from
src/modules/, so parseYoutubeId from src/modules/base/video/youtube.ts is not
imported. The importer only needs host-level detection to decide the module;
base.video's render() re-parses the stored videoUrl at publish time.

base.video (src/modules/base/video/) extended with two new props:
- title (default: 'YouTube video') — used as the iframe title attribute,
  replacing the previous hardcoded string. Improves accessibility.
- noRelatedVideos (default: false) — appends rel=0 to the YouTube embed URL
  to suppress recommended videos. youtubeEmbedUrl() updated to build the query
  string from autoplay and noRelatedVideos together. VideoEditor.tsx wired up.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The chore/gitignore-superpowers branch had these files staged for deletion.
When switching to fix/import-video-embeds-to-base-video the staged index was
inherited and the previous commit swept them in unintentionally. Restore them
from origin/main to keep this PR scope clean.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

1 participant