概要
ひとつ前の記事でCodepenを埋め込みたかったので、oEmbedで埋め込んでみました。Astroでの実装をメモ書きします。
oEmbedとは
oEmbedとは何かというと、SNSやブログなどの埋め込みコードなどを返却する規格です。具体的には、各サイトのAPIに「このページのコンテンツを埋め込みたいよ」とリクエストを投げると、それに対して埋め込みコードを返します。詳しくはoEmbed公式サイトを見てみてください。
この記事を書いている時点で331のプロバイダが存在します。有名どころだとYouTube、Instagram、Twitter(X)など。後ほど言及しますが、プロバイダ一覧がJSON形式でまとまっています。
実装
Astroはremarkを使ってMakrdownを処理しているため、remarkプラグインで機能を拡張できます。今回はURLを検知するremark-embedderプラグインを使います。
このプラグインはURLを検知するコア部分のみを提供し、Transformerと呼ばれる埋め込みコードを取得・加工するプラグインが別途必要となります。公式Transformerは存在するのですが、なぜか私の環境ではうまく動かず、解決するのも難しかったので自作しました。
Transformerを作る
自作といっても公式Transformerとおおかたの処理は同じで、適宜cheerioでユーティリティクラスをつけるなど変更を加えています。なぜiframe要素をfigure要素で囲んでいるかというと、tailwindcss-typographyで上下の余白などをよしなにしてもらうためです。
import { URL } from 'node:url'
import { type Transformer } from '@remark-embedder/core'
import * as cheerio from 'cheerio'
type Provider = {
provider_name: string
provider_url: string
endpoints: Array<{
schemes?: string[]
discovery?: boolean
url: string
}>
}
let providers: Provider[] = []
// oEmbed公式サイトからプロバイダ一覧を取得
const getProviders = async (): Promise<Provider[]> => {
if (providers.length === 0) {
try {
const response = await fetch('https://oembed.com/providers.json')
if (!response.ok) {
throw new Error(
`[remark-embedder] oEmbedプロバイダ一覧の取得に失敗しました: ${response.status}`
)
}
providers = await response.json()
} catch (error) {
console.warn(error.message)
}
}
return providers
}
// プロバイダ一覧からエンドポイントを取得
const getEndpointURL = async (url: string): string | null => {
const providers = await getProviders()
for (const provider of providers) {
for (const endpoint of provider.endpoints) {
if (
endpoint.schemes?.some((scheme) =>
new RegExp(scheme.replace(/\*/g, '(.*)')).test(url)
)
) {
return endpoint.url
}
}
}
return null
}
export const transformer: Transformer = {
name: 'default',
// 埋め込みコードに置換するかどうか
async shouldTransform(urlString: string) {
const response = await getEndpointURL(urlString)
return Boolean(response)
},
// 埋め込みコードを取得
async getHTML(urlString: string) {
const endpoint = await getEndpointURL(urlString)
if (!endpoint) return null
const url = new URL(endpoint)
url.searchParams.set('url', urlString)
url.searchParams.set('format', 'json')
try {
const response = await fetch(url.toString())
if (!response.ok) {
throw new Error(
`[remark-embedder] oEmbed情報の取得に失敗しました: ${urlString} ${response.status}`
)
}
const data = await response.json()
if (data.html) {
const { host } = new URL(urlString)
const $ = cheerio.load(data.html, null, false)
switch (host) {
case 'www.youtube.com':
case 'youtu.be':
$('iframe').addClass('w-full h-auto aspect-video')
break
case 'codepen.io':
$('iframe').addClass('w-full h-[max(300px,50vh)]')
break
case 'speakerdeck.com':
$('iframe').addClass('max-w-full h-auto')
break
default:
break
}
$('iframe').wrap('<figure></figure>')
return $.html()
} else if (data.url) {
const { host } = new URL(urlString)
switch (host) {
case 'gyazo.com':
return `<img src="${data.url}" alt="${data.title}" width="${data.width}" height="${data.height}" />`
}
}
} catch (error) {
console.warn(error.message)
}
return urlString
},
}
そして、Astro設定ファイルで次のように指定します。
import remarkEmbedder from '@remark-embedder/core'
import { transformer } from './src/libs/oembed'
export default defineConfig({
markdown: {
remarkPlugins: [
[remarkEmbedder.default, { transformers: [transformer] }],
],
},
})
こうすることで、Markdownファイル内のURLがoEmbedに対応していれば埋め込みコードに置換されます。
サンプル
https://www.youtube.com/watch?v=1jFEhVWIQIM
https://twitter.com/Interior/status/463440424141459456
https://www.flickr.com/photos/nasacommons/30648886821/
https://gyazo.com/8980c52421e452ac3355ca3e5cfe7a0c
https://speakerdeck.com/tsumugu1214/dokiyumento-sonoqian-ni
https://codepen.io/87oui/pen/wBvMdNG
https://open.spotify.com/intl-ja/album/11LqngEbNjOK5zKCqw3TiC
YouTube
余談ですが、アゲハ蝶のどの歌詞が好きかって結構分かれますよね〜。
Twitter(現X)
Sunsets don't get much better than this one over @GrandTetonNPS. #nature #sunset pic.twitter.com/YuKy2rcjyU
— US Department of the Interior (@Interior) May 5, 2014
blockquote要素のみ返るためwidgets.jsをどこかに追記する必要があります。私はあまりTwitterを埋め込むことはなさそうなので、必要になったらコンポーネントを作って都度MDXファイルにインポートすればいいかな〜と考えています。
Flickr

Gyazo

Speakerdeck
余談ですが、ひろクリギルドメンバーつむさんのこちらの発表資料が素晴らしかったです。私もこのようなチームメンバーでありたい…まだまだ道半ばです。
Codepen
Spotify
https://open.spotify.com/intl-ja/album/11LqngEbNjOK5zKCqw3TiC
くるりの中ではこのアルバムの「虹色の天使」が一番好きです。
その他
Instagramはトークンを渡してやらないとエラーになります。Instagram APIを叩く時と同じですね。
