Skip to content

feat(product): display product videos on the PDP#3044

Open
BC-AdamWard wants to merge 2 commits into
bigcommerce:canaryfrom
BC-AdamWard:feat/product-videos-pdp
Open

feat(product): display product videos on the PDP#3044
BC-AdamWard wants to merge 2 commits into
bigcommerce:canaryfrom
BC-AdamWard:feat/product-videos-pdp

Conversation

@BC-AdamWard

@BC-AdamWard BC-AdamWard commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

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 supported
product-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_content
layout — rather than mixing them into the image gallery.

Layout

A "Videos" section sits directly below ProductDetail, before Related Products
and Reviews:

ProductDetail (gallery = images only)
→ Videos          ◄── new section
→ Related Products
→ 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

File Change
core/app/.../product/[slug]/page-data.ts Add videos(first: 25) { edges { node { title url } } } to the PDP product query
core/app/.../product/[slug]/page.tsx Add streamableVideos (independent of the gallery image stream); mount <ProductVideos> below ProductDetail
core/vibes/soul/sections/product-detail/product-videos.tsx New. Section component — heading + collapsible toggle, featured lite-youtube player + title, thumbnail strip that swaps the featured video
core/vibes/soul/sections/product-detail/lite-youtube.tsx New. Client-only <lite-youtube> facade wrapper (poster + play button; injects the iframe only on click)
core/vibes/soul/sections/product-detail/video-embed.ts New. getYouTubeId() (defensive watch-URL parser) + getYouTubePosterUrl()
core/vibes/soul/sections/product-detail/lite-youtube-embed.d.ts New. Ambient module declaration — lite-youtube-embed ships no types and is imported only for its custom-element side effect
core/messages/en.json i18n keys: videosTitle, hideVideos, showVideos, playVideo, viewVideo (English source)
core/next.config.ts Allow i.ytimg.com/vi/** poster thumbnails through next/image
core/package.json Add lite-youtube-embed ^0.3.4
.changeset/product-videos-pdp.md minor bump to @bigcommerce/catalyst-core

Design notes

  • Dedicated section, not the gallery: keeps the gallery purely visual
    (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 a
    poster + play button and injects the youtube-nocookie iframe only on click, so
    no heavy player loads on PDP view. Registered client-side only (it subclasses
    HTMLElement at import, which breaks SSR); inert markup server-side, upgrades on
    hydration.
  • YouTube-only: getYouTubeId() validates protocol + host + id shape and
    drops anything that isn't a YouTube URL, matching BC's supported provider. The
    section renders nothing when there are no usable YouTube URLs.
  • Playback hygiene: the featured <lite-youtube> is keyed by video id, so
    swapping videos remounts the player and stops the previous one.

Accessibility

  • Collapsible toggle uses aria-expanded + aria-controls wired to the panel via
    useId().
  • Thumbnails use aria-pressed for selected state and labelled aria-labels; the
    play control has a visually-hidden label.

Testing

Verified on current canary (base 98d5b87) with a store product carrying 3
videos:

pnpm install --frozen-lockfile   # lockfile valid (only adds lite-youtube-embed)
pnpm -C core generate            # videos query validates against live schema
pnpm -C core lint -- --max-warnings=0
pnpm -C core typecheck           # 0 errors
pnpm -C core build               # next build (tsc + static gen) passes
  • PDP renders the Videos section: featured player, title, 3-thumbnail strip;
    toggle hides/shows; thumbnails swap the featured video.
  • 0 player iframes on initial load (facade); the iframe mounts only on click.

getYouTubeId() verification matrix

core/ has no unit-test runner today (tests are Playwright e2e), so the parser is
covered by this manual matrix (a unit test is a ready follow-up — the function is
pure):

Input Expected
https://www.youtube.com/watch?v=dQw4w9WgXcQ dQw4w9WgXcQ
https://youtu.be/dQw4w9WgXcQ dQw4w9WgXcQ
https://www.youtube.com/embed/dQw4w9WgXcQ dQw4w9WgXcQ
https://www.youtube.com/shorts/dQw4w9WgXcQ dQw4w9WgXcQ
https://m.youtube.com/watch?v=dQw4w9WgXcQ dQw4w9WgXcQ
https://vimeo.com/123456 null (non-YouTube host)
https://youtube.com.evil.com/watch?v=x null (host not whitelisted)
javascript:alert(1) null (non-http(s) protocol)
not a url null (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 iframe
loads). Branch is based on current canary (98d5b87); canary's tax-display
logic is inherited untouched.

@BC-AdamWard BC-AdamWard requested a review from a team as a code owner June 11, 2026 16:07
@vercel

vercel Bot commented Jun 11, 2026

Copy link
Copy Markdown

Someone is attempting to deploy a commit to the BigCommerce Platform Team on Vercel.

A member of the Team first needs to authorize it.

@changeset-bot

changeset-bot Bot commented Jun 11, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: fc32eeb

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@bigcommerce/catalyst-core Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@BC-AdamWard BC-AdamWard force-pushed the feat/product-videos-pdp branch 3 times, most recently from 9d8ed7e to 9d4cf20 Compare June 11, 2026 16:59

// YouTube ids are short alphanumeric tokens; reject anything else so a malformed
// path tail or encoded query material can't leak into a built URL.
const YOUTUBE_ID = /^[\w-]{6,}$/;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a library we could potentially use instead of rolling our own embed logic?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question. Quick context first on why some transform is unavoidable, then the library option.

The Storefront API returns a YouTube watch URL, which can't be iframed directly — YouTube blocks /watch in frames, only /embed/<id> is frameable. So inline playback always needs a watch-URL → embed-id step regardless of approach; the real question is whether we own that logic or delegate it.

First-party option: @next/third-parties/google's YouTubeEmbed (it wraps lite-youtube-embed under the hood). It would replace the embed-URL/poster construction here, the click-to-play facade in product-gallery.tsx, and the i.ytimg.com next/image remote pattern — and removes the hand-built iframe src/thumbnail URLs entirely. We'd keep only a small getYouTubeId() parser, since the component takes a videoid and the API gives us a watch URL.

Honestly, neither route is clearly ideal, so this feels like your call on which risk is more acceptable for core:

  • @next/third-parties — first-party and removes most of the custom/security-sensitive URL building, but it's still marked experimental by Next and adds a runtime dependency to vibes/soul.
  • Roll our own (current) — keeps vibes/soul dependency-free, but we maintain (and you review) the parsing/embed logic.
  • Middle ground — depend on lite-youtube-embed directly (stable, not experimental) and wrap the web component ourselves: stable dep, a bit more glue.

A tiny ID parser stays either way. Which trade-off would you prefer — experimental first-party dep, stable third-party dep, or keeping it in-house? Happy to implement whichever you pick.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd go with either @next/third-parties or lite-youtube-embed instead of rolling our own code. I just don't want to roll my own custom video code in Catalyst that we have to maintain.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Went with lite-youtube-embed directly. The custom embed/facade is gone — the gallery now renders <lite-youtube>, which owns the poster, click-to-play, and the youtube-nocookie iframe. The only thing left is a small getYouTubeId(), since the Storefront API returns watch URLs and the element needs the bare id.

Chose it over @next/third-parties because that wrapper is still flagged experimental, whereas lite-youtube-embed is stable and zero-dependency (and is what @next/third-parties wraps anyway). It's registered client-side only so SSR doesn't trip on the custom element, and each <lite-youtube> remounts when its carousel slide is deselected so playback stops on navigate-away. Scope is YouTube-only, matching BC product-video support.

Pushed.

@BC-AdamWard BC-AdamWard force-pushed the feat/product-videos-pdp branch 2 times, most recently from f35766b to dfe13a9 Compare June 11, 2026 20:00
@jorgemoya

Copy link
Copy Markdown
Contributor

Is there a deployment url I can use to test this?

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: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@BC-AdamWard BC-AdamWard force-pushed the feat/product-videos-pdp branch from dfe13a9 to f8ee729 Compare June 25, 2026 00:54
@BC-AdamWard BC-AdamWard changed the title feat(product): display product videos on the PDP gallery feat(product): display product videos on the PDP Jun 25, 2026
@BC-AdamWard

Copy link
Copy Markdown
Contributor Author

Is there a deployment url I can use to test this?

Preview deployment: https://adam-native-catalyst.catalyst-sandbox.store/zz-plant-wqxt - Updated PR to reflect that videos will load in their own section rather than in the image gallery.

@@ -0,0 +1,5 @@
---

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we include a migration section similar to other changesets from the past?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a Migration section to the changeset in fc32eeb.

@jorgemoya

Copy link
Copy Markdown
Contributor

Looks good, just left a minor comment and we need to make sure the link/typecheck passes.

Address review feedback (changeset should include a migration section like
prior changesets).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@BC-AdamWard

Copy link
Copy Markdown
Contributor Author

Pushed fc32eeb: added the changeset Migration section.

On the failing checks: "Lint, Typecheck, and gql.tada" and both bundle jobs fail at pnpm run -r generate with Error: Missing store hash. generate requires the BigCommerce store credentials, which aren't available to fork-PR CI runs, so the job exits before lint/typecheck run. Lint, typecheck, and build pass locally with credentials set — a maintainer-triggered run with secrets should clear them.

@vercel

vercel Bot commented Jun 26, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
catalyst Ready Ready Preview, Comment Jun 26, 2026 4:16pm

Request Review

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants