在 MDX 博客里加图片、音乐和视频
一份按「现在最简单 → 长期才需要」排列的多媒体方案——什么时候直接进仓库、什么时候必须走外部存储、以及对应的 MDX 组件长什么样。这个博客是纯文件 + MDX 的——所有内容都是 git 里的 .mdx 文件,没有数据库、没有后台。
那要怎么往文章里塞图片、音乐、视频?下面是我打算长期遵循的一套方案。
一个核心权衡
只有一件事需要先讲清楚:资源进 Git,还是走外部存储?
- 进 Git:一切 portable,文章和资源永远绑定,备份 =
git clone - 走外部:仓库轻,CI 快,但多一个 dashboard 要管,链接可能失效
我的默认策略是能进 public/ 就进 public/,触发以下任一条件再外迁:
- 单文件 > 5 MB
- 仓库总大小 > 200 MB
- 某类资源(比如完整视频)从一开始就明显属于"外部生态"
图片
默认做法:public/ + Markdown 语法
最简单:

文件放在 public/images/posts/2026/cat.jpg 即可。能用、能被搜索引擎抓、能右键保存。
小博客这一招够撑很久。
想要懒加载 + 响应式:包一层 <Image>
直接 <img> 会让首屏带宽暴涨。Next.js 自带 <Image> 能解决,
但它不能直接在 MDX 里替代 markdown 的 ![]()——需要在 mdx-components.tsx 里把 img 映射过去:
// components/mdx/mdx-components.tsx 节选
import NextImage from 'next/image'
export const mdxComponents = {
// ... 其它
img: ({ src, alt }) => (
<NextImage
src={src as string}
alt={alt ?? ''}
width={1200}
height={800}
sizes="(min-width: 768px) 720px, 100vw"
style={{ width: '100%', height: 'auto', borderRadius: 'var(--radius-3)' }}
/>
),
}
这样所有文章里的  自动升级为优化过的图片,原始 MDX 写法不用改。
量大了:外部存储
仓库突破 200 MB 的时候,把图迁去 Cloudflare R2 / Vercel Blob / 七牛—— MDX 里就写完整 URL,其它都不用动。
音乐
短片段(几 MB):public/ + <audio>
写一个简单的 MDX 组件:
// components/mdx/audio-player.tsx
export function AudioPlayer({ src, title }: { src: string; title?: string }) {
return (
<figure style={{ margin: '1.5rem 0' }}>
{title && <figcaption style={{ marginBottom: 8, opacity: 0.7 }}>{title}</figcaption>}
<audio controls src={src} style={{ width: '100%' }} />
</figure>
)
}
注册到 mdx-components.tsx,文章里直接:
<AudioPlayer src="/audio/demo.mp3" title="一段背景音" />
完整曲目 / 专辑:嵌入外部平台
整张专辑、几十分钟的播客——不要 commit 进 Git。要么用 SoundCloud / 网易云 / Spotify 的 iframe, 要么自己包一个语义化组件:
// components/mdx/netease-music.tsx
export function NeteaseMusic({ id, height = 86 }: { id: string; height?: number }) {
return (
<iframe
width="100%"
height={height}
src={`//music.163.com/outchain/player?type=2&id=${id}&auto=0&height=66`}
frameBorder="0"
allow="autoplay"
/>
)
}
文章里 <NeteaseMusic id="123456" /> 就能用,未来换平台只改一个组件。
视频
几乎永远不要自托管视频。 带宽、转码、字幕、移动端兼容——每一项单拎出来都够写一篇文章。
一律嵌入
YouTube:
// components/mdx/youtube.tsx
export function YouTube({ id }: { id: string }) {
return (
<div style={{ position: 'relative', paddingTop: '56.25%', margin: '1.5rem 0' }}>
<iframe
src={`https://www.youtube.com/embed/${id}`}
allow="accelerometer; autoplay; clipboard-write; encrypted-media; picture-in-picture"
allowFullScreen
style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', border: 0 }}
/>
</div>
)
}
Bilibili 同理,把 src 换成:
`https://player.bilibili.com/player.html?bvid=${bvid}&page=${page}&high_quality=1`
注意国内访问稳定性,海外读者会看不到——双语博客最好两个平台都嵌一份。
进阶可以用 lite-youtube-embed: 首屏只加载缩略图,点击才真正拉 iframe。首页带视频列表时差距巨大。
例外:极短演示(< 10 秒)
录屏演示一个交互、一段动画——这种 webm/mp4 放 public/ 是合理的:
<video autoPlay muted loop playsInline style={{ width: '100%', borderRadius: 8 }}>
<source src="/videos/demo.webm" type="video/webm" />
</video>
永远不要用 GIF。同样画质下 webm 通常是 GIF 的 1/10 体积,浏览器支持率早已 99%+。
现在就可以做的事
如果接受这套方案,落地到当前项目只需要加 4 个组件 + 3 个目录:
components/mdx/image.tsx——包next/image,覆盖默认imgcomponents/mdx/audio-player.tsx——<AudioPlayer />components/mdx/youtube.tsx——<YouTube />components/mdx/bilibili.tsx——<Bilibili />public/images/posts/、public/audio/、public/videos/——三个目录可以先留空
每个组件都不超过 30 行。加完之后,任意一篇 MDX 都能直接:

<AudioPlayer src="/audio/intro.mp3" title="开场白" />
<YouTube id="dQw4w9WgXcQ" />
等真正写到第一篇带视频或音乐的文章再说也不晚——不要为了未来才需要的东西现在就动手。