petitviolet blog

    Rehype plugin for oEmbed

    2021-12-20

    Node.js

    I've posted a couple of posts about oEmbed like

    oEmbed expansion in Gatsby oEmbed expansion in Gatsby [oEmbed](https://oembed.com/) > oEmbed is a format for allowing an embedded representation of a URL on third party sites. The simple API allows a website to display embedded content (such as photos or videos) when a user posts a link to that resource, without having to parse the resource directly.

    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.

    rehype-oembed.ts
    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.

    Build oEmbed API for Gatsby blog Build an API for oEmbed expansion of blog posts powered by Gatsby Previously, I wrote a post about that this blog has a capability to expand oEmbed articles using [raae/gatsby-remark-oembed](https://github.com/raae/gatsby-remark-oembed). https://blog.petitviolet.net/post/2020-03-06/oembed-expansion As it is embed here, this blog is also able to expand self post

    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.