inu

マークダウンからHTMLにパースする時に、img 要素にアトリビュートを追加する

なぜやるのか

このブログは microCMS でコンテンツを管理しています。作ってしばらくはリッチエディタ機能を使っていましたが、スキーマを平文テキストに変更、マークダウン形式でコンテンツを持つようにしました。

microCMS のリッチエディタで気になる点

  • p 要素で段落分けする設定にした場合、段落内改行をする方法がない
  • 箇条書きリスト内での改行ができない
  • エディタ画面にマークダウンのテキストをコピペしても書式が反映されない

手元のメモでマークダウンで書いてるんだし、最初からマークダウンでコンテンツを持つようにするか、となりました。

そもそもリッチエディタを使っていたのは、img 要素に自動で width/height を付けてくれるのを気に入ったのが理由です。そこだけ自力で実装することにしました。

やりたいこと

  • microCMS で管理しているマークダウン文書を、HTMLにパースする
  • その際に、img 要素に microCMS で管理しているメディアの width/height を指定する
  • 同時に、遅延ロードのアトリビュートを追加する
  • 外部リンクは別タブで開くようにする

作業メモ

マークダウンをHTMLにパース

next のチュートリアルでは remark 使ってたので、それでいきます。

要素の書き換え

microCMS で管理している画像メディアのメタ情報をAPIで取得してセットします。

import { remark } from 'remark'
import html from 'remark-html'
...
export async function getPostData(id) {
  const thisPost = await client.get({
    endpoint: 'posts',
    contentId: id,
  })
  const processedContent = await remark().use(html).process(thisPost.body)
  const contentHtml = await parseHtml(processedContent.toString())
  ...
  return {
    thisPost: { ...thisPost, body: contentHtml },
    ...
  }
import { parse } from 'node-html-parser'

const parseImgEl = async (imgEl) => {
  const src = imgEl.getAttribute('src')
  if (!src.startsWith('https://images.microcms-assets.io/')) {
    return imgEl
  }
  const format = await fetch(`${src}?fm=json`).then((res) => res.json())
  imgEl.setAttribute('height', format.PixelHeight)
  imgEl.setAttribute('width', format.PixelWidth)
  imgEl.setAttribute('loading', 'lazy')
  return imgEl
}

const parseAnchorEl = (anchorEl) => {
  const href = anchorEl.getAttribute('href')
  if (href.startsWith('/')) {
    return anchorEl
  }
  anchorEl.setAttribute('target', '_blank')
  anchorEl.setAttribute('rel', 'noopener')
  return anchorEl
}

export async function parseHtml(content) {
  const root = parse(content, {
    blockTextElements: {
      code: true,
    },
  })
  await Promise.all([
    ...root.querySelectorAll('img').map((imgEl) => parseImgEl(imgEl)),
    ...root.querySelectorAll('a').map((anchorEl) => parseAnchorEl(anchorEl)),
  ])
  return root.toString()
}

後から気づいたのですが、remark-external-links を使えば a 要素の別タブで開く設定とかはできたっぽいです。

参考