I used to write blog posts on Medium. When I moved on to my own site, I started writing my blog posts in markdown and I missed the ability to just copy a URL (like for a tweet), paste it in the blog post, and have Medium auto-embed it for me.
This allows you to transform a link in your markdown into the embedded version of that link. It's a remark plugin (the de-facto standard markdown parser). You provide a "transformer" the the plugin does the rest.
- Installation
- Usage
- Making a transformer module
- Inspiration
- Other Solutions
- Issues
- Contributors β¨
- LICENSE
This module is distributed via npm which is bundled with node and
should be installed as one of your project's dependencies
:
npm install --save @remark-embedder/core
Here's the most complete, simplest, practical example I can offer:
import remark from 'remark'
import html from 'remark-html'
import remarkEmbedder from '@remark-embedder/core'
// or, if you're using CJS:
// const {default: remarkEmbedder} = require('@remark-embedder/core')
const codesandboxTransformer = {
name: 'Codesandbox',
// shouldTransform can also be async
shouldTransform(url) {
const {host, pathname} = new URL(url)
return (
['codesandbox.io', 'www.codesandbox.io'].includes(host) &&
pathname.includes('/s/')
)
},
// getHTML can also be async
getHTML(url) {
const iframeUrl = url.replace('/s/', '/embed/')
return `<iframe src="${iframeUrl}" style="width:100%; height:500px; border:0; border-radius: 4px; overflow:hidden;" allow="accelerometer; ambient-light-sensor; camera; encrypted-media; geolocation; gyroscope; hid; microphone; midi; payment; usb; vr; xr-spatial-tracking" sandbox="allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts"></iframe>`
},
}
const exampleMarkdown = `
This is a codesandbox:
https://codesandbox.io/s/css-variables-vs-themeprovider-df90h
`
async function go() {
const result = await remark()
.use(remarkEmbedder, {
transformers: [codesandboxTransformer],
})
.use(html)
.process(exampleMarkdown)
console.log(result.toString())
// logs:
// <p>This is a codesandbox:</p>
// <iframe src="https://codesandbox.io/embed/css-variables-vs-themeprovider-df90h" style="width:100%; height:500px; border:0; border-radius: 4px; overflow:hidden;" allow="accelerometer; ambient-light-sensor; camera; encrypted-media; geolocation; gyroscope; hid; microphone; midi; payment; usb; vr; xr-spatial-tracking" sandbox="allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts"></iframe>
}
The transformers
option is required (otherwise the plugin won't do anything),
but there are a few optional options as well.
The transformer objects are where you convert a link to it's HTML embed representation.
This is the name of your transformer. It's required because it's used in error messages. I suggest you use your module name if you're publishing this transformer as a package so people know where to open issues if there's a problem.
Only URLs on their own line will be transformed, but for your transformer to be
called, you must first determine whether a given URL should be transformed by
your transformer. The shouldTransform
function accepts the URL string and
returns a boolean. true
if you want to transform, and false
if not.
Typically this will involve checking whether the URL has all the requisite information for the transformation (it's the right host and has the right query params etc.).
You might also consider doing some basic checking, for example, if it looks a
lot like the kind of URL that you would handle, but is missing important
information and you're confident that's a mistake, you could log helpful
information using console.log
.
The getHTML
function accepts the url
string and a config option (learn more
from the services
option). It returns a string of HTML or a promise that
resolves to that HTML. This HTML will be used to replace the link.
It's important that the HTML you return only has a single root element.
// This is ok β
return `<iframe src="..."></iframe>`
// This is not ok β
return `<blockquote>...</blockquote><a href="...">...</a>`
// But this would be ok β
return `<div><blockquote>...</blockquote><a href="...">...</a></div>`
Some services have endpoints that you can use to get the embed HTML (like twitter for example).
Because some of your transforms may make network requests to retrieve the HTML,
we support providing a cache
. You could pass new Map()
, but that would only
be useful during the life of your process (which means it probably wouldn't be
all that helpful). If you want to persist this to the file system (so it works
across compilations), you could use something like
lowdb.
The cache key is set to remark-embedder:${transformerName}:${urlString}
and
the value is the resulting HTML.
Also, while technically we treat the cache as a Map
, all we really care about
is that the cache has a get
and a set
and we await
both of those calls to
support async caches (like gatsby's).
You can provide configuration for your transformer by specifying the transformer
as an array. This may not seem very relevant if you're creating your own custom
transformer where you can simply edit the code directly, but if the transformer
is published to npm
then allowing users to configure your transformer
externally can be quite useful (especially if your transformer requires an API
token to request the embed information like with
instagram).
Here's a simple example:
const codesandboxTransformer = {
name: 'Codesandbox',
shouldTransform(url) {
// ...
},
getHTML(url, config) {
// ... config(url).height === '600px'
},
}
const getCodesandboxConfig = url => ({height: '600px'})
const result = await remark()
.use(remarkEmbedder, {
transformers: [
someUnconfiguredTransformer, // remember, not all transforms need/accept configuration
[codesandboxTransformer, getCodesandboxConfig],
],
})
.use(html)
.process(exampleMarkdown)
The config
is typed as unknown
so transformer authors have the liberty to
set it as anything they like. The example above uses a function, but you could
easily only offer an object. Personally, I think using the function gives the
most flexibility for folks to configure the transform. In fact, I think a good
pattern could be something like the following:
const codesandboxTransformer = {
name: 'Codesandbox',
shouldTransform(url) {
// ...
},
// default config function returns what it's given
getHTML(url, config = html => html) {
const html = '... embed html here ...'
return config({url, html})
},
}
const getCodesandboxConfig = ({url, html}) => {
if (hasSomeSpecialQueryParam(url)) {
return modifyHTMLBasedOnQueryParam(html)
}
return html
}
const result = await remark()
.use(remarkEmbedder, {
transformers: [
someUnconfiguredTransformer, // remember, not all transforms need/accept configuration
[codesandboxTransformer, getCodesandboxConfig],
],
})
.use(html)
.process(exampleMarkdown)
This pattern inverts control for folks who like what your transform does, but
want to modify it slightly. If written like above (return config(...)
) it
could even allow the config function to be async
.
Here's what our simple example would look like as a transformer module:
import type {Transformer} from '@remark-embedder/core'
type Config = (url: string) => {height: string}
const getDefaultConfig = () => ({some: 'defaultConfig'})
const transformer: Transformer<Config> = {
// this should be the name of your module:
name: '@remark-embedder/transformer-codesandbox',
shouldTransform(url) {
// do your thing and return true/false
return false
},
getHTML(url, getConfig = getDefaultConfig) {
// get the config...
const config = getConfig(url)
// do your thing and return the HTML
return '<iframe>...</iframe>'
},
}
export default transformer
export type {Config}
If you're not using TypeScript, simply remove the type
import and the
: Transformer
bit.
If you're using CommonJS, then you'd also swap export default transformer
for
module.exports = transformer
NOTE: If you're using export default
then CommonJS consumers will need to add
a .default
to get your transformer with require
.
To take advantage of the config type you export, the user of your transform would need to cast their config when running it through remark. For example:
// ...
import type {Config as CodesandboxConfig} from '@remark-embedder/transformer-codesandbox'
import transformer from '@remark-embedder/transformer-codesandbox'
// ...
remark().use(remarkEmbedder, {
transformers: [codesandboxTransformer, config as CodesandboxConfig],
})
// ...
This whole plugin was extracted out of
Kent C. Dodds' Gatsby website
into gatsby-remark-embedder
by
MichaΓ«l De Boey and then Kent extracted the
remark plugin into this core package.
- MDX Embed: Allows you to use components in MDX files for common services. A pretty different approach to solving a similar problem.
Looking to contribute? Look for the Good First Issue label.
Please file an issue for bugs, missing documentation, or unexpected behavior.
Please file an issue to suggest new features. Vote on feature requests by adding a π. This helps maintainers prioritize what to work on.
Thanks goes to these people (emoji key):
Kent C. Dodds π» π π |
MichaΓ«l De Boey π π» π |
This project follows the all-contributors specification. Contributions of any kind welcome!
MIT