How to Embed YouTube Videos in Gatsby (MDX + Markdown)
Learn how to embed YouTube videos in Gatsby with markdown plugins, MDX components, and fast lite-youtube facades. Includes Contentful setup and GDPR tips.
48 min read

To embed YouTube videos in Gatsby, pasting an iframe into markdown is not enough. React strips raw HTML out of the rendered output, so the embed silently disappears and you are left with an empty gap in the post. You need either a build-time plugin that rewrites video URLs into iframes, or an MDX component that renders the iframe itself.
Gatsby is in maintenance mode
Gatsby has been in maintenance mode since the Netlify acquisition in February 2023. Releases since then are bug fixes and runtime support only. The version history matters if you are upgrading: gatsby@5.16.0 (2026-01-26) added React 19 and Node.js 24 support. gatsby@5.16.1 (2026-02-10) is a single bug fix for Gatsby Head API regressions (<title> not updating document.title). React 19 is opt-in. Staying on React 18 is fine and supported. If you bump to React 19 on npm 7+, use npm install --legacy-peer-deps, and note that Partial Hydration (experimental) is incompatible with React 19. The engines field is >=18.0.0 <26, so Node 24 works, Node 26 does not yet.
The instructions below still work for existing Gatsby sites (the plugin ecosystem gets roughly 3.4k installs a month against Gatsby core’s ~1.08M). If you are starting fresh, pick something else: see how to add YouTube videos to your Astro blog, build a free blog with Astro and Cloudflare, or if you are leaving WordPress, migrating from WordPress to Astro and MDX. For the framework choice itself, Next.js and Astro are both reasonable.
Every embed plugin in the Gatsby niche is stale. gatsby-remark-embed-video is the workhorse and it has not shipped since 2022. Choose based on syntax needs and performance budget, not on which repo looks alive. If you control the content yourself, prefer the component or facade path below.
This guide covers four approaches: a remark plugin for marker syntax, a bare-URL embedder, a custom MDX component, and a lazy-loading facade. The facade is the one I would use on a new site.
Before you start: versions and what you need
- An existing Gatsby 5 site, or a fresh one with
gatsby new. - Node.js 18 to 24. Node 24 is supported since gatsby@5.16.0; the
enginesfield caps at<26. - npm 7 or newer (you may need
--legacy-peer-depswhen bumping to React 19). - A content source: local
.md/.mdxfiles, or a headless CMS with a Markdown field type. - Comfort with
gatsby clean. The cache will bite you after plugin changes.
Verify before installing anything:
node -v # expect v18.x - v24.x
npm ls gatsby # expect gatsby@5.xGatsby-team remark plugins are versioned in lockstep with core and got the React 19 peer-dep bump on 2026-01-26: gatsby-transformer-remark 6.16.0, gatsby-remark-responsive-iframe 6.16.0, gatsby-plugin-mdx 5.16.0. The install snippets below pull these automatically.
One footnote that saves confusion: Gatsby’s MDX stack is MDX v2 (@mdx-js/mdx ^2.3.0), not v3. Do not copy MDX v3 syntax from other blogs into a Gatsby project.
Option 1: gatsby-remark-embed-video (Markdown & MDX)
gatsby-remark-embed-video is the workhorse for `youtube:` marker syntax. It supports YouTube, Vimeo, VideoPress, and Twitch, and it works with both gatsby-transformer-remark (plain markdown) and gatsby-plugin-mdx.
Be clear about what you are adopting: v3.2.1 was published 2022-06-30, the repo was last pushed 2023-03-01, and it still pulls about 3.4k downloads a month. It works with Gatsby 5 (the README’s current config examples target Gatsby 5), but nobody is shipping features. Pick it for marker syntax and CMS workflows, not for maintenance velocity.
Install
npm i gatsby-remark-embed-video gatsby-transformer-remarkIf you want responsive iframes (recommended), also install:
npm i gatsby-remark-responsive-iframeOn Gatsby 5.16 with React 19, if npm reports peer-dep conflicts:
npm install --legacy-peer-deps gatsby-remark-embed-video gatsby-transformer-remark gatsby-remark-responsive-iframeConfigure for markdown (gatsby-transformer-remark)
Add this to your gatsby-config.js:
{
resolve: "gatsby-transformer-remark",
options: {
plugins: [
{
resolve: "gatsby-remark-embed-video",
options: {
width: 800,
ratio: 1.77, // 16/9 aspect ratio
height: 400, // overrides ratio if set
related: false, // rel=0: end-screen related videos limited to the same channel
noIframeBorder: true,
loadingStrategy: "lazy",
containerClass: "embedVideo-container",
urlOverrides: [
{
id: "youtube",
embedURL: (videoId) =>
`https://www.youtube-nocookie.com/embed/${videoId}`,
},
],
},
},
"gatsby-remark-responsive-iframe", // must come after embed-video
],
},
},The youtube-nocookie.com URL override uses YouTube’s privacy-enhanced mode. No cookies until the user clicks play. Good default for GDPR-sensitive audiences.
Verify: gatsby develop, open a post with a video tag, and check DevTools Elements for <iframe src="https://www.youtube-nocookie.com/embed/...">. If the iframe is missing, jump to Troubleshooting.
Configure for MDX (gatsby-plugin-mdx)
If you are using MDX instead of plain markdown:
{
resolve: "gatsby-plugin-mdx",
options: {
gatsbyRemarkPlugins: [
{
resolve: "gatsby-remark-embed-video",
options: {
width: 800,
ratio: 1.77,
related: false,
noIframeBorder: true,
loadingStrategy: "lazy",
},
},
"gatsby-remark-responsive-iframe",
],
},
},gatsby-plugin-mdx 5.16.0 still pins MDX v2. See the prerequisites note above if a snippet you found online uses MDX v3 syntax.
Usage in content
Add these tags on their own line in your markdown or MDX files:
`video: https://www.youtube.com/embed/2Xc9gXyf2G4`
`youtube: https://www.youtube.com/watch?v=2Xc9gXyf2G4`
`youtube: 2Xc9gXyf2G4`
`vimeo: https://vimeo.com/5299404`
`vimeo: 5299404`
`videoPress: https://videopress.com/v/kUJmAcSf`
`videoPress: kUJmAcSf`
`twitch: https://player.twitch.tv/?channel=dakotaz`
`twitch: https://player.twitch.tv/?autoplay=false&video=v273436948`
`twitch: 273436948`
`twitchLive: dakotaz`You can also add accessibility titles:
`youtube: [My Video Title](https://www.youtube.com/watch?v=2Xc9gXyf2G4)`One caution: the plugin parses URLs through get-video-id@^3.6.4, and YouTube Shorts /shorts/ URLs are unverified with that parser. Use standard watch?v= or youtu.be forms and do not assume Shorts links work without testing one yourself.
Verify: save a post with a marker, run gatsby develop, and confirm the iframe renders and plays.
Plugin options worth knowing (and one to avoid)
The README documents a few options beyond the basics:
| Option | What it does |
|---|---|
width / height |
Fixed iframe size in px. height overrides ratio when set. |
ratio |
Aspect ratio as width/height. 1.77 is 16:9. |
related |
false sets rel=0. See the next section for what that does now. |
noIframeBorder |
Strips the iframe border attribute. |
loadingStrategy |
"lazy" adds loading="lazy" to the iframe. |
containerClass |
Class name on the wrapper div, useful for your own responsive CSS. |
urlOverrides |
Rewrite the embed URL per provider. This is how you get youtube-nocookie.com. |
iframeId |
true sets the iframe id to the video ID. Required if you want the YouTube IFrame Player API to talk to your embeds (play/pause control from your own buttons, progress events, etc.). |
sandbox |
Broken. See below. Do not use. |
Do not use the sandbox option (plugin bug #200)
The sandbox option stringifies the value wrong: only the first token is rendered. Setting sandbox: 'allow-same-origin allow-scripts allow-presentation' produces sandbox="allow-same-origin", which blocks scripts and silently breaks playback. This is issue #200, open since 2023-12-16 with no fix shipped. Omit the option entirely. If you genuinely need a sandboxed iframe, use Option 2 or 3 below where you control the attribute yourself.
YouTube player parameters that still work
The related: false comment in older tutorials is misleading. Since YouTube’s September 25, 2018 change, rel=0 does not hide related videos. It limits end-screen suggestions to videos from the same channel. You cannot fully suppress them anymore.
A few parameters you will still find in blog posts are dead ends. Per the official YouTube IFrame Player API docs (fetched 2026-09-25):
| Parameter | Status |
|---|---|
rel=0 |
Works. Same-channel related videos at the end, not “none”. |
playsinline=1 |
Works. Inline playback on iOS instead of a fullscreen takeover. Worth adding. |
autoplay, mute, start, end, controls, loop, playlist |
Still functional. |
modestbranding, showinfo, autohide, theme |
Deprecated and non-functional. Do not copy them into urlOverrides or component URLs. |
If you want playsinline=1 with Option 1, add it inside the urlOverrides template string:
urlOverrides: [
{
id: "youtube",
embedURL: (videoId) =>
`https://www.youtube-nocookie.com/embed/${videoId}?playsinline=1`,
},
],Using with Contentful CMS
This approach works with Contentful or any headless CMS that outputs markdown. The one requirement: your content field must be Markdown type, not rich text.
In Contentful, create a text field with “Markdown” type. Then add the video tag on its own line in the editor:
`youtube: https://youtu.be/2Wmats7Q6ck`When Gatsby builds, gatsby-transformer-remark processes the markdown and the embed-video plugin converts the tag into an iframe.
If editors put videos in a Contentful rich text field, none of this fires. Rich text never enters the remark pipeline. Those entries need Contentful’s rich-text renderer with an embedded-entry resolver instead, which is a different setup entirely. If you are still choosing a CMS, I have a rundown of best headless CMS options that covers which ones hand you markdown and which do not.
Verify: add a video in Contentful, run gatsby build, and grep the output for the embed host:
grep -r "youtube-nocookie.com/embed" public/ | headIf grep finds nothing, the field is almost certainly rich text.
Important: plugin order matters
If you use gatsby-remark-responsive-iframe, gatsby-remark-images, or gatsby-remark-prismjs, the embed-video plugin must come first:
plugins: [
"gatsby-remark-embed-video",
"gatsby-remark-responsive-iframe",
"gatsby-remark-prismjs",
"gatsby-remark-images",
]Wrong order breaks embedding. If responsive-iframe runs first, the video iframe is already in the AST and gets mangled or skipped. This is the second most common reason “the video tag does nothing”.
Option 1b: Paste-a-URL embeds with gatsby-remark-embedder
gatsby-remark-embedder is the alternative if marker syntax is too fussy for your editors. You paste a bare URL on its own line, surrounded by blank lines, and the plugin turns it into an embed at build time. No youtube: prefix.
Some intro text.
https://www.youtube.com/watch?v=2Xc9gXyf2G4
More text.It accepts watch?v= and youtu.be forms. Two details make it attractive:
- Its YouTube transformer already emits
youtube-nocookie.comwithrel=0by default. The privacy setup Option 1 needsurlOverridesfor comes free here. - It ships transformers for many more services: CodePen, CodeSandbox, GIPHY, Instagram, Lichess, Pinterest, Slides, SoundCloud, Spotify, Streamable, Twitch, Twitter/X, and more.
npm i gatsby-remark-embedder// gatsby-config.js
{
resolve: "gatsby-transformer-remark", // or gatsby-plugin-mdx via gatsbyRemarkPlugins
options: {
plugins: ["gatsby-remark-embedder"],
},
},Version note: v7.0.0 (2023-05-30), peer dep gatsby ^5, dormant like everything else here.
One operational caveat: some transformers fetch remote HTML at build time (node-fetch / fetch-retry). YouTube’s is template-based, so it is offline-safe. Twitter/X’s is not. If you build on flaky or air-gapped CI, test a full build before committing to this plugin.
Option 2: Custom React YouTube component for MDX
If you write content directly in MDX and want full control, skip the plugin and create a YouTube component:
// src/components/YouTube.js
const YouTube = ({ id, title = "YouTube video" }) => (
<div style={{ position: "relative", paddingBottom: "56.25%", height: 0, overflow: "hidden" }}>
<iframe
src={`https://www.youtube-nocookie.com/embed/${id}?playsinline=1`}
title={title}
style={{ position: "absolute", top: 0, left: 0, width: "100%", height: "100%" }}
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
loading="lazy"
/>
</div>
);
export default YouTube;Then import and use it in your MDX files:
import YouTube from "../components/YouTube";
# My Post
Some content here.
<YouTube id="2Xc9gXyf2G4" title="Demo video" />The 16:9 paddingBottom: 56.25% wrapper is the standard responsive-embed trick. The box keeps its aspect ratio at any container width. I use the same technique in responsive YouTube embeds in Astro MDX if you want the cross-framework version.
What you get: full control over iframe attributes (including a real sandbox if you need one), no plugin dependency, and loading="lazy" on the iframe. The trade is scope: this only works where you author MDX yourself. A CMS markdown field will not run your imports.
Verify: render the MDX page, confirm the iframe and loading="lazy" in DevTools, then resize the window and check the box holds 16:9.
Option 3: Lazy load YouTube videos with a lite-youtube facade
The iframe-at-build-time approach still loads YouTube’s player JavaScript on every page view, even when the reader never presses play. That hurts Core Web Vitals, and it is the wrong default if videos sit below the fold.
The facade pattern fixes it: render a poster image and a play button first, and load the real iframe only on click. On page load you pull a single JPEG and nothing from youtube.com. The maintained package in this space, paulirish/lite-youtube-embed (6,349 stars), documents the effect as “approximately 224x faster” than a real embed. I have not measured an MB figure and I will not invent one. Open DevTools on any YouTube embed and look at the network tab yourself.
Three ways to do it in Gatsby:
lite-youtube-embed
npm i lite-youtube-embed// src/components/YouTubeLite.js
import "lite-youtube-embed/src/lite-yt-embed.css";
import "lite-youtube-embed";
const YouTubeLite = ({ id, title }) => (
<lite-youtube videoid={id} playlabel={title}></lite-youtube>
);
export default YouTubeLite;Uses youtube-nocookie.com by default and supports custom poster images. Choose this if you want the best-known facade with the least thought.
justinribeiro/lite-youtube
This is a zero-dependency Shadow DOM web component (repo, pushed 2026-04-21). Load the single component file once in your layout (copy it into src/components/ or pull it from a CDN), then use it in MDX like any element:
<lite-youtube
videoid="2Xc9gXyf2G4"
videotitle="Demo video"
nocookie
autoload
></lite-youtube>Useful attributes: nocookie, autoload (IntersectionObserver-based lazy load on scroll), videotitle for ARIA, playlistid, and a short attribute for YouTube Shorts that is experimental. Do not rely on it in production yet. Poster is WebP with a JPEG fallback, and recent versions support a CSP nonce. Choose this if you want scroll-triggered loading or have a strict CSP.
Hand-rolled React facade
Zero dependencies, and you control every attribute. Keep the 16:9 wrapper from Option 2 around this component:
// src/components/YouTubeFacade.js: poster first, iframe on click
import { useEffect, useRef, useState } from "react";
const YouTubeFacade = ({ id, title = "YouTube video" }) => {
const [play, setPlay] = useState(false);
const ref = useRef(null);
useEffect(() => {
if (play && ref.current) ref.current.focus();
}, [play]);
return play ? (
<iframe
src={`https://www.youtube-nocookie.com/embed/${id}?autoplay=1&playsinline=1`}
title={title}
ref={ref}
style={{ position: "absolute", inset: 0, width: "100%", height: "100%" }}
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
allowFullScreen
/>
) : (
<button
type="button"
onClick={() => setPlay(true)}
aria-label={`Play video: ${title}`}
style={{ position: "absolute", inset: 0, width: "100%", height: "100%", padding: 0, border: 0, cursor: "pointer",
background: `center / cover no-repeat url(https://i.ytimg.com/vi/${id}/hqdefault.jpg)` }}
/>
);
};
export default YouTubeFacade;Choose this if you want zero dependency and need to wire in your own analytics or consent logic on the click event.
How the facade behaves at runtime: on page load only the poster JPEG is fetched. No requests to youtube.com or googlevideo.com. After the click, the iframe appears with autoplay=1 and the player JavaScript loads then.
Two failure modes worth knowing before you ship this:
- Poster 404s. Use
https://i.ytimg.com/vi/<ID>/hqdefault.jpg. That always resolves.maxresdefault.jpg404s for some videos and you get a black box. - Shorts URLs. Support in justinribeiro’s
lite-youtubeis experimental. Test before promising it to editors.
If you would rather not embed anyone’s player at all and want to own delivery (or run video off-YouTube for privacy or monetization), see how to host and deliver videos without YouTube or Vimeo. For the delivery side, Bunny.net CDN is what I would price out first.
Verify: DevTools Network tab on page load. Expect zero requests to youtube.com or googlevideo.com before the click. After the click, player JS shows up and the video plays. Optional: run Lighthouse before and after and compare LCP.
Which approach should you choose?
All the plugins in this niche are stale, so pick by syntax needs and performance budget, not maintenance cadence.
- Content is in a CMS or plain markdown with existing
youtube:tags: Option 1. - Editors paste URLs and hate markers: Option 1b.
- You author MDX and care about iframe attributes: Option 2.
- LCP is red in PageSpeed Insights and videos sit below the fold: Option 3.
| Situation | Best option | Why |
|---|---|---|
| Headless CMS (Contentful, Sanity) with markdown fields; non-technical editors | Option 1 or 1b | Transform happens at build; editors never touch JSX |
| Local MDX content, you want full control over the iframe | Option 2 | Zero plugin dependency, you own the markup |
| Performance budget matters, videos below the fold | Option 3 | Player JS loads only on click |
| You are starting a new site in 2026 | Not Gatsby | A framework-free facade survives any migration. See the Astro vs Next.js vs TanStack Start comparison |
For a legacy Gatsby site with a CMS behind it, Option 1 is the boring, correct default. For anything new, I would build the facade component and host it in Astro or Next.js.
Troubleshooting
Videos not showing up? Check that your content field is markdown type, not rich text. Rich text in Contentful uses a different rendering pipeline that skips remark plugins entirely.
Build errors after install? Clear the Gatsby cache:
gatsby clean && gatsby developPlugin conflicts? Make sure gatsby-remark-embed-video is listed before any other remark plugins in your config. If gatsby-remark-responsive-iframe runs first, the video iframe is already in the AST and gets mangled or skipped.
I set sandbox and playback broke? That is plugin bug #200. Only the first sandbox token is rendered, so allow-scripts is dropped and the player is dead. Remove the option. If you need sandboxing, use the Option 2 or 3 component where you control the attribute.
Related videos still show at the end? Expected since 2018. rel=0 only limits end-screen suggestions to the same channel. There is no parameter that removes them.
npm peer-dep errors on Gatsby 5.16 / React 19? React 19 is opt-in. Either stay on React 18, or install with npm install --legacy-peer-deps. Partial Hydration (experimental) does not work with React 19. Turn one or the other off.
Facade poster not loading? You are probably using maxresdefault.jpg. Switch to hqdefault.jpg. It always resolves.
Embeds break on CI with gatsby-remark-embedder? Some of its transformers fetch remote HTML at build time (Twitter/X is the usual culprit). YouTube’s transformer is template-based and offline-safe. Pin which transformer needs the network and drop the others if your CI is locked down.
FAQ
Does this still work with Gatsby 5 and Gatsby 5.16?
Yes. gatsby-remark-embed-video is stale (v3.2.1, 2022) but functional on Gatsby 5. The README’s current examples target Gatsby 5, and the plugin still gets roughly 3.4k installs a month. That said, the last release predates the Gatsby 5.16 / React 19 changes, so smoke-test one fresh install on your Node version before trusting it across a whole content set. Clear the cache after installing.
Do I need a plugin at all if I use MDX?
No. If you author the content as MDX files yourself, Options 2 and 3 skip the plugin layer completely and give you full control over the iframe (including a working sandbox attribute). The plugin exists for markdown fields and CMS workflows, where editors paste URLs or markers and cannot write JSX.
Is youtube-nocookie enough for GDPR?
It is a good default, not a complete answer. youtube-nocookie.com defers cookies until the user clicks play. That is often enough for a personal blog and far better than a bare youtube.com embed. For a strict consent-mode setup, you need a click-to-load facade (Option 3) so no third-party request happens at all before consent, and even then talk to whoever owns your compliance posture.
Can I lazy load without a facade package?
Partially. loading="lazy" on the iframe (Option 1’s loadingStrategy or Option 2’s attribute) tells the browser to defer the iframe until it nears the viewport, but once loaded, YouTube’s player JavaScript still arrives in full. A facade avoids the player JS entirely until the click. If LCP is the problem, loading="lazy" alone will not fix it for below-the-fold video embeds.
If you want to go deeper on the framework itself, here are the best Gatsby.js online courses I have looked at.
DigitalOcean $100 Free Vultr $100 Free Hetzner €20 Free Hostinger VPSVPS prices jumped across the board in 2026. If you’re rethinking a rented box, see what changed and when a mini PC wins.


