A self-hosted YouTube video feed, built to work inside an AEM instance that only allows iframe embeds.
This project is a small static site (hosted on Cloudflare Pages) that renders a YouTube video feed and is designed to be dropped into that one iframe, configured entirely through the iframe's own attributes. SFU's AEM External Video component is lacking. So, I decided to create this solution. The AEM setup only needs embedding an <iframe> and hosting one JS file.
One feed layout, with potential to create more in the future:
latest-videos-styled.html — SFU-branded thumbnail cards; clicking a card swaps its thumbnail for a live embed in place.If no playlist or videos are configured, the iframe renders completely blank — no placeholder text.
Both pages read their content from URL query parameters:
| Param | What it does |
|---|---|
playlist | A YouTube playlist ID (e.g. the channel's uploads playlist). Shows the latest videos from it. |
max | How many videos to show from the playlist. Defaults to 2. |
videos | A comma-separated list of specific YouTube URLs (or bare video IDs) to show instead of a playlist. Always overrides playlist/max when present. |
titleStyle | Set to regular to render titles as a bolded paragraph matching the body font, instead of the default h3 heading. |
latest-videos.html?playlist=UUxxxxxxxxxxxxxxxxxxxxxx&max=6
latest-videos-styled.html?videos=https://youtu.be/dQw4w9WgXcQ,https://youtu.be/xvFZjo5PgG0
latest-videos-styled.html?playlist=UUxxxx&titleStyle=bold
A YouTube channel's "uploads" playlist ID is its channel ID with the UC prefix swapped for UU.
Rather than hand-building that query string inside AEM, embeds use plain data-* attributes — a shared loader script reads them and builds the URL (with correct escaping) automatically:
<script src="https://www.sfu.ca/content/dam/sfu/politics/CERG/code/js/iframe.js"></script>
<iframe class="sfu-video-embed"
src="https://cerg-youtube-feed.pages.dev/latest-videos-styled"
data-videos=""
data-playlist=""
data-max-videos=""
data-title-style=""
title="CERG Youtube Feed" width="100%" height="800px" frameborder="no"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
style="border-style: none; overflow: hidden; transition: height 0.2s ease;">
</iframe>
Fill in whichever data-* attribute matches what you want to show and leave the rest empty — leaving all of them empty shows nothing. The iframe also auto-resizes to fit its content height via postMessage, handled by that same script.
_worker.js) proxies requests to the YouTube Data API v3, so the API key never reaches the browser. Two routes: /api/videos (playlist-based) and /api/videos-by-id (specific videos, used by ?videos=).YOUTUBE_API_KEY secret, set in Cloudflare Pages → Settings → Variables and Secrets.iframe.js — the AEM-side loader that builds each embed's URL from its data-* attributes and handles the resize listener — is hosted separately on SFU's own CDN, not in this repo.
YOUTUBE_API_KEY with that key.iframe.js somewhere AEM can load it from, and embed the iframe snippet above wherever the feed is needed.