🧩🚀🛠️ヽ(`Д´)ノ
Remark プラグイン「remark-card」を自作して Astro 製ウェブサイトで使ってみた
導入
幼い頃から、魔法使いになることを夢見ていた──
この度、ようやくその念願が叶った。
しかしその能力は限定的で、リンゴをアップルパイに変えたり、嫌いな人をマリモッコリに変えたりはできない。代わりに、マークダウンを望み通りの HTML に変えることならできる。
ところで、Remark や Rehype を擁する文書変換エコシステム「Unified」の下では、世界各地に点在する有志たちによって、数多のプラグインが日夜開発・メンテナンスされている。
しかし調べた限りでは、「フィールド上にカードコンポーネントをマークダウン記法で召喚できる」と謳うものは存在しなかった。1
そういう訳で、無いなら自分で作ろうということに相成った。今なら、空だって飛べる気がする。
想定読者
- Remark プラグインを自分で作ってみたい
remark-card
に興味がある- マークダウン/MDX のブログに彩りを添えようと躍起になっている
現時点での私の習熟度
記事執筆時点での私の習熟度は次の通り。
- ソフトウェアエンジニアとして計 3 年の経験あり(勤続ではない)
- JavaScript、TypeScript とはかれこれ 4 年程の付き合いになる
- NPM パッケージは公開したことがない
本題
Github リポジトリはこちら。インストール方法については README に認めてあるので、ここでは触れない。
使い方
remark-card
はcard-grid
、card
の 2 つのマークダウン方言を提供する。
前者はカードを複数表示する場合にラッパーの役割を果たし、後者はカード単体である。
class
等)を削ぎ落としている。 card
まずカード単体の記法はこちら。
:::cardyounagi.dev:::
これがイイ感じの HTML に変換され・・。
<div> <div class="image-container"> <figure> <picture> <source srcset="..."> <source srcset="..."> <img src="..." alt="younagi.dev"> </picture> <figcaption>younagi.dev</figcaption> </figure> </div> <div class="content-container"> younagi.dev </div></div>
最終的な出力はコレだ。

younagi.dev
card-grid
次にカードグリッドの中に複数のカードを並べてみよう。
::::card-grid:::cardyounagi.dev 其の一::::::cardyounagi.dev 其の二::::::cardyounagi.dev 其の三:::::::
これが HTML に変換されると次のようになる。
<div> <div> <div class="image-container"> <figure> <picture> <source srcset="..."> <source srcset="..."> <img src="..." alt="younagi.dev 其の一"> </picture> <figcaption>younagi.dev 其の一</figcaption> </figure> </div> <div class="content-container"> younagi.dev 其の一 </div> </div> <div> <div class="image-container"> <figure> <picture> <source srcset="..."> <source srcset="..."> <img src="..." alt="younagi.dev 其の二"> </picture> <figcaption>younagi.dev 其の二</figcaption> </figure> </div> <div class="content-container"> younagi.dev 其の二 </div> </div> <div> <div class="image-container"> <figure> <picture> <source srcset="..."> <source srcset="..."> <img src="..." alt="younagi.dev 其の三"> </picture> <figcaption>younagi.dev 其の三</figcaption> </figure> </div> <div class="content-container"> younagi.dev 其の三 </div> </div></div>
そして最終的な出力はこちら。

younagi.dev 其の一

younagi.dev 其の二

younagi.dev 其の三
Astro で使う
実は、当サイトでも既にカードコンポーネントが随所に鏤められている。
Astro で Remark や Rehype のプラグインを利用するには、ルート直下にある専用のコンフィグファイルで下記の手続きが必要となる。
import { defineConfig /* ... */ } from "astro/config";import remarkCard, { type Config as RemarkCardConfig } from 'remark-card';// ...
export default defineConfig({ // ... markdown: { remarkPlugins: [ // ... [ remarkCard, { customHTMLTags: { enabled: true, }, cardGridClass: 'card-grid', cardClass: 'card', } satisfies RemarkCardConfig, ], // ... ], // ... } // ...});
一応これだけでプラグインは動くようになるのだが、Astro コンポーネントを使って HTML タグやスタイルを細かく調整したい場合は、もうひと手間必要となる。
私の場合、remark-card
によってcard
、card-grid
タグに変換された箇所を、更にそれぞれCard.astro
、CardGrid.astro
へと変換している。詳細は下記ソースコードを確認してほしい。
そして MDX の作法に倣い、カスタムコンポーネントとして<Content />
タグにこれらを引き渡せば取引完了だ。下記がその一例である。
import { Card, CardGrid } from '@/components/elements/Card';// ...
export const mdxComponents = { // ... card: Card, 'card-grid': CardGrid, // ...};
---import BlogLayout from '@/layouts/BlogLayout.astro';import { mdxComponents } from '@/lib/mdx-components';import type { GetStaticPaths } from 'astro';// ...
export const getStaticPaths = (async () => { // ...}) satisfies GetStaticPaths;
const { entry } = Astro.props;const { Content, headings } = await entry.render();---
<BlogLayout entry={{ ...entry }} {headings}> <Content components={mdxComponents} /></BlogLayout>
開発後記
このままでは README と書いてあることがほとんど変わらないので、技術選定について少し真面目に振り返ろうと思う。
- 言語:
JavaScriptTypeScript- 型がないと生まれたての小鹿のように足元が覚束なくなるため TypeScript で書くことにした
CommonJSESM の作法に倣って書いている
- マークダウン記法: Generic directives/plugins syntax
- はじめは知る人が見たらひっくり返るようなド方言を編み出し、開発を進めていたが、より一般に浸透していそうなこちらの記法に方向転換
:::
や::::
など
- テストランナー & パッケージマネージャ: Bun
- JavaScript ランタイム、テストランナー、パッケージマネージャ、と複数の顔を併せ持つ肉まん君
- テストコードが Jest 互換で馴染みがあり、移行しやすい
- 目新しさとパフォーマンスの高さに惹かれ、採用
開発にあたり、先人たちの記したソースコードを浴びるように摂取したのだが、大半が JavaScript に JSDoc の組み合わせで書かれていた。
製品コードを書く場合は総じてこの組み合わせの方が良いと思う 2 が、今回は零細なプロジェクトなので慣れ親しんだ TypeScript で書くことにした。
結び
実はremark-card
の後、立て続けにremark-video
という Remark プラグインも開発・リリースした。
当初はそちらも記事にしようと画策していたが、remark-card
程の手間がかかっておらず、あまり書くこともないため断念した。3
感謝👏