Original work by BC-AdamWard (#3044); recreated on a fresh branch rebased on
current canary.
Product videos were unsupported on the Catalyst PDP even though the Storefront
GraphQL API exposes them on Product.videos (a { title, url } pair — a YouTube
watch URL). This adds the query and renders videos in a dedicated "Videos"
section below the primary product content (mirroring the Stencil/Cornerstone
product_below_content layout), not inside the image gallery.
- page-data.ts: fetch videos(first: 25) on the PDP product query
- page.tsx: stream videos independently of gallery images; mount <ProductVideos>
below ProductDetail (before related products / reviews)
- ProductVideos: featured lite-youtube player + thumbnail strip that swaps the
featured video, with a Hide/Show toggle (YouTube-only, BC's supported provider)
- lite-youtube: client-only facade (poster + play button; injects the
youtube-nocookie iframe only on click) — no heavy player on PDP view
- video-embed: getYouTubeId() defensive watch-URL parser + poster URL helper
- next.config: allow i.ytimg.com poster thumbnails through next/image
- i18n: videosTitle / hideVideos / showVideos / playVideo / viewVideo (en source)
Co-Authored-By: BC-AdamWard <95659751+BC-AdamWard@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
What / Why
Product videos were unsupported on the Catalyst PDP even though the Storefront
GraphQL API already exposes them on
Product.videos. The API returns a{ title, url }pair (a YouTube watch URL — YouTube is BigCommerce's supportedproduct-video provider), so two pieces were missing: the query never requested
the field, and there was no layer to turn a watch URL into an embeddable player.
This PR adds both and renders videos in a dedicated section below the primary
product content — mirroring the Stencil/Cornerstone
product_below_contentlayout — rather than mixing them into the image gallery.
Layout
A "Videos" section sits directly below
ProductDetail, before Related Productsand Reviews:
It renders a featured player + horizontal thumbnail strip (clicking a
thumbnail swaps the featured video) with a Hide/Show toggle. This matches the
Cornerstone PDP video structure. The featured player is full section width
(
max-w-screen-2xl) as a starting point.Changes
core/app/.../product/[slug]/page-data.tsvideos(first: 25) { edges { node { title url } } }to the PDP product querycore/app/.../product/[slug]/page.tsxstreamableVideos(independent of the gallery image stream); mount<ProductVideos>belowProductDetailcore/vibes/soul/sections/product-detail/product-videos.tsxlite-youtubeplayer + title, thumbnail strip that swaps the featured videocore/vibes/soul/sections/product-detail/lite-youtube.tsx<lite-youtube>facade wrapper (poster + play button; injects the iframe only on click)core/vibes/soul/sections/product-detail/video-embed.tsgetYouTubeId()(defensive watch-URL parser) +getYouTubePosterUrl()core/vibes/soul/sections/product-detail/lite-youtube-embed.d.tslite-youtube-embedships no types and is imported only for its custom-element side effectcore/messages/en.jsonvideosTitle,hideVideos,showVideos,playVideo,viewVideo(English source)core/next.config.tsi.ytimg.com/vi/**poster thumbnails throughnext/imagecore/package.jsonlite-youtube-embed ^0.3.4.changeset/product-videos-pdp.mdminorbump to@bigcommerce/catalyst-coreDesign notes
(image-only), matches the long-standing Stencil/Cornerstone PDP convention, and
lets videos scale without competing with image pagination / load-more.
lite-youtube-embed: a tiny, dependency-free third-party facade — shows aposter + play button and injects the
youtube-nocookieiframe only on click, sono heavy player loads on PDP view. Registered client-side only (it subclasses
HTMLElementat import, which breaks SSR); inert markup server-side, upgrades onhydration.
getYouTubeId()validates protocol + host + id shape anddrops anything that isn't a YouTube URL, matching BC's supported provider. The
section renders nothing when there are no usable YouTube URLs.
<lite-youtube>is keyed by video id, soswapping videos remounts the player and stops the previous one.
Accessibility
aria-expanded+aria-controlswired to the panel viauseId().aria-pressedfor selected state and labelledaria-labels; theplay control has a visually-hidden label.
Testing
Verified on current
canary(basec6b3b0713):toggle hides/shows; thumbnails swap the featured video.
getYouTubeId()verification matrixcore/has no unit-test runner today (tests are Playwright e2e), so the parser iscovered by this manual matrix (a unit test is a ready follow-up — the function is
pure):
https://www.youtube.com/watch?v=dQw4w9WgXcQdQw4w9WgXcQhttps://youtu.be/dQw4w9WgXcQdQw4w9WgXcQhttps://www.youtube.com/embed/dQw4w9WgXcQdQw4w9WgXcQhttps://www.youtube.com/shorts/dQw4w9WgXcQdQw4w9WgXcQhttps://m.youtube.com/watch?v=dQw4w9WgXcQdQw4w9WgXcQhttps://vimeo.com/123456null(non-YouTube host)https://youtube.com.evil.com/watch?v=xnull(host not whitelisted)javascript:alert(1)null(non-http(s) protocol)not a urlnull(parse failure)https://www.youtube.com/watch?v=null(empty id)Migration / risk
Additive — the gallery, image load-more, and the review-form image consumer are
unchanged. CSP isn't affected (Catalyst sets no
frame-src, so the YouTube iframeloads). Branch is based on current
canary(c6b3b0713).🤖 Generated with Claude Code