Rehype plugin for oEmbed
2021-12-20
Node.jsI've posted a couple of posts about oEmbed like
Needless to say, the above card is build via oEmbed.
What Rehype Plugin is
First of all, according to the official repository(rehypejs/rehype),
rehype is an HTML processor powered by plugins part of the unified collective.
Rehype is to process HTML and also allows users to inject own processing on intermediate AST in unified realm that is called hast. List of plugins of rehype are available at https://github.com/rehypejs/rehype/blob/main/doc/plugins.md
oEmbed expansion for this blog
This blog serves oEmbed API so that other users can embed my blog posts ss posts can be embedded with oEmbed format via using API.
$ curl 'https://blog.petitviolet.net/api/posts/oembed?url=https://blog.petitviolet.net/post/2020-03-06/oembed-expansion' | jq -S '.'
{
"authorName": "petitviolet",
"authorUrl": "https://blog.petitviolet.net",
"blogTitle": "blog.petitviolet.net",
"blogUrl": "https://blog.petitviolet.net/",
"description": "oEmbed expansion in Gatsby",
"height": "200px",
"html": "...",
"imageUrl": "",
"providerName": "petitviolet blog",
"providerUrl": "https://s.gravatar.com/avatar/93bc8fb48f57c11e417dad9d26a2fb8a?s=512",
"published": "2020-03-06T23:25:44",
"tags": [
"Gatsby"
],
"title": "oEmbed expansion in Gatsby",
"type": "rich",
"url": "https://blog.petitviolet.net/post/2020-03-06/oembed-expansion",
"version": "1.0",
"width": "100%"
}
However, as this blog is just a static site on Next.js, using API is not required and it should be able to expand URLs while building static files. I'm going to describe how to do that with rehype pluging mechanism.
3 steps:
- extract URLs from a HTML
- build a HTML with calling oEmbed API if needed
- embed the HTML into original HTML being processed
Code
The following snipped is to archive the 3 steps.
import rehypeParse from "rehype-parse"
import rehypeStringify from "rehype-stringify/lib"
import { unified } from "unified"
import { Node, Parent, Literal } from "unist"
import { visit } from "unist-util-visit"
const rehypeOEmbed = () => {
return (tree: Node) => visit(tree, "element", visitor)
}
export default rehypeOEmbed
// get Literal element (not nested HTML element)
const getLiteralChild = (node: Node & { children?: any }): Literal<string> | null => {
if (
node.type === "element" &&
node.children &&
node.children[0] &&
node.children[0].type == "text" &&
"value" in node.children[0]
) {
return node.children[0]
} else {
return null
}
}
// regex to find URL from HTML
const URL_PATTERN = /^https:\/\/blog.petitviolet.net\/post\/(\d{4}-\d{2}-\d{2})\/(.+?)(#.+)?$/i
// traverse HTML nodes
const visitor = (
node: Node & { children?: any },
index: number | null,
parent: (Parent & { children: any[] }) | null
) => {
const literalChild = getLiteralChild(node)
if (literalChild == null) {
return
}
if (!URL_PATTERN.test(literalChild.value)) {
return
}
const url = literalChild.value
// process the found URL
// construct HTML from the URL
const html = buildHtml(url)
// parse the HTML into HAST format
const parsed = unified()
.use(rehypeParse, {
fragment: true,
emitParseErrors: true,
duplicateAttribute: false,
})
.use(rehypeStringify)
.parse(html)
// replace the original node
node.children[0] = parsed
}
const buildHtml = (url: string): string => {
const post = // get blog post object somehow using `url`
return `<div>hello!</div>` // build HTML whatever you want to display as a replacement of the original raw URL.
}
I could build HAST directly that represents what is going to be embedded, but I wanted to use HTML instead of HAST since previously I implemented it in HTML format.
How to use the plugin
There is no surprise in terms of how to use the rehype plugin I created above since it can be used as the same with other plugins that are available in public.
As an example, process a markdown with unified, remark, and rehype with expanding URLs by the plugin described above.
import { unified } from "unified"
import remarkParse from "remark-parse"
import remarkRehype from "remark-rehype"
import rehypeRaw from "rehype-raw"
import rehypeStringify from "rehype-stringify"
import { VFileWithOutput } from "unified"
import rehypeOEmbed from "./rehype_oembed"
// markdown -> (remark parse / to rehype) -> HTML -> (rehype raw / oembed / stringify)
const process = async (markdown: string): VFileWithOutput<any> => {
return await unified()
.use(remarkParse)
.use(remarkRehype)
.use(rehypeRaw)
.use(rehypeOEmbed) // expand oEmbed
.use(rehypeStringify)
.process(markdown)
}
As I put comments in the snippet, remark is to process Markdown and rehype is to process HTML.
While processing HTML, rehypeOEmbed
, the created rehype plugin, is executed so that outcome of the pipeline should have embedded oembed components.