【実装】Notion calloutブロックをNext.jsでカラフルなUIコンポーネントとして表示する

この記事でわかること
- Notion の callout が Markdown 変換でただの引用になってしまう問題と、その回避
- HTML を直接埋めずにプレースホルダーを介させる理由
- Notion の全色 + ダークモードに対応する CSS の当て方
- quote ブロックも同じ UI に寄せて見た目を揃える方法
Notion API でブロックを取得して Markdown に変換する実装がすでにあること。markdown-it などの変換器を使っている前提です。
概要: NotionのcalloutブロックをNext.jsブログで色分けされたリッチなUIボックスとして表示する実装方法を解説します。Blocks→Markdown→HTML変換でcalloutが消えてしまう問題を、プレースホルダー方式で解決しました。
はじめに
Notion で記事を書くとき、callout ブロックを多用して重要な情報を目立たせています。ブログ側でも同じように色分けされたボックスで見せたいのですが、Notion Blocks → Markdown → HTML の変換過程で callout の情報が失われてしまう問題がありました。
1. 問題: Blocks→Markdown変換でcalloutが消える
現象
Notion APIから取得したブロックデータをMarkdownに変換すると、calloutブロックが単なるblockquote(> )に変換されてしまう。
Notionでの表示
calloutブロックにはアイコンと背景色の情報がある:
{
"type": "callout",
"callout": {
"icon": { "type": "emoji", "emoji": "💡" },
"color": "green_background",
"rich_text": [{ "plain_text": "これは重要なTipsです" }]
}
}一般的なMarkdown変換後
> これは重要なTipsですアイコンと色の情報が完全に失われ、ただのblockquoteになる。
2. 解決策: 3段階変換
全体の変換フローは以下の3段階:
Notion Blocks → :::calloutマーカー付きMarkdown → プレースホルダー → 最終HTMLなぜ直接HTMLを埋め込まないのか
Markdown→HTML変換(markdown-it)の過程で、Markdown内に直接書いたHTMLタグが除去・エスケープされてしまう場合がある。そのため、変換前にプレースホルダーに置換し、変換後にHTMLに復元する方式が必要。
3. Blocks→Markdown: calloutマーカー形式で出力
blocksToMarkdown関数のcallout処理
function blocksToMarkdown(blocks: Block[]): string {
return blocks.map(block => {
switch (block.type) {
case 'callout': {
const icon = block.callout.icon?.emoji || '💡';
const color = block.callout.color || 'default';
const text = richTextToMarkdown(block.callout.rich_text);
return `:::callout{icon="${icon}" color="${color}"}\n${text}\n:::`;
}
// ... 他のブロックタイプ
}
}).join('\n\n');
}出力されるMarkdown
Tipsこれは重要なTipsです4. プレースホルダー変換
markdownToHtml前にプレースホルダーに置換
type CalloutData = {
icon: string;
color: string;
content: string;
};
function replaceCalloutsWithPlaceholders(markdown: string): {
markdown: string;
callouts: Map<string, CalloutData>;
} {
const callouts = new Map<string, CalloutData>();
let index = 0;
const replaced = markdown.replace(
/:::callout\{icon="([^"]*)"\s+color="([^"]*)"\}\n([\s\S]*?)\n:::/g,
(_, icon, color, content) => {
const placeholder = `CALLOUT_PLACEHOLDER_${index++}`;
callouts.set(placeholder, { icon, color, content });
return placeholder;
}
);
return { markdown: replaced, callouts };
}markdownToHtml後にHTMLに復元
function restoreCallouts(html: string, callouts: Map<string, CalloutData>): string {
let result = html;
callouts.forEach((data, placeholder) => {
// プレースホルダーがpタグで囲まれている場合も考慮
const regex = new RegExp(`(<p>)?${placeholder}(</p>)?`, 'g');
const calloutHtml = `
<div class="callout callout-${data.color}">
<span class="callout-icon">${data.icon}</span>
<div class="callout-content">${data.content}</div>
</div>`;
result = result.replace(regex, calloutHtml);
});
return result;
}全体の処理フロー
// ページコンポーネントでの使用
const markdownBody = blocksToMarkdown(blocks);
// Step 1: calloutをプレースホルダーに置換
const { markdown, callouts } = replaceCalloutsWithPlaceholders(markdownBody);
// Step 2: Markdown → HTML変換
const htmlContent = markdownToHtml(markdown);
// Step 3: プレースホルダーをcallout HTMLに復元
const finalHtml = restoreCallouts(htmlContent, callouts);5. CSS: カテゴリ別カラーとダークモード対応
基本スタイル
.callout {
display: flex;
gap: 12px;
padding: 16px 20px;
border-radius: 8px;
margin: 16px 0;
border-left: 4px solid;
}
.callout-icon {
font-size: 1.4em;
flex-shrink: 0;
}
.callout-content {
flex: 1;
line-height: 1.7;
}Notionカラー全色定義
/* ブルー系 */
.callout-blue_background {
background-color: #e7f3fe;
border-left-color: #2196f3;
}
/* グリーン系 */
.callout-green_background {
background-color: #e8f5e9;
border-left-color: #4caf50;
}
/* イエロー系 */
.callout-yellow_background {
background-color: #fff8e1;
border-left-color: #ff9800;
}
/* レッド系 */
.callout-red_background {
background-color: #ffebee;
border-left-color: #f44336;
}
/* パープル系 */
.callout-purple_background {
background-color: #f3e5f5;
border-left-color: #9c27b0;
}
/* ピンク系 */
.callout-pink_background {
background-color: #fce4ec;
border-left-color: #e91e63;
}
/* オレンジ系 */
.callout-orange_background {
background-color: #fff3e0;
border-left-color: #ff5722;
}
/* グレー系(デフォルト) */
.callout-gray_background,
.callout-default {
background-color: #f5f5f5;
border-left-color: #9e9e9e;
}ダークモード対応
[data-theme='dark'] .callout-blue_background {
background-color: #1a2733;
border-left-color: #64b5f6;
}
[data-theme='dark'] .callout-green_background {
background-color: #1a2e1a;
border-left-color: #81c784;
}
[data-theme='dark'] .callout-yellow_background {
background-color: #2e2a1a;
border-left-color: #ffb74d;
}
[data-theme='dark'] .callout-red_background {
background-color: #2e1a1a;
border-left-color: #e57373;
}
[data-theme='dark'] .callout-purple_background {
background-color: #261a2e;
border-left-color: #ba68c8;
}
[data-theme='dark'] .callout-pink_background {
background-color: #2e1a22;
border-left-color: #f06292;
}
[data-theme='dark'] .callout-orange_background {
background-color: #2e221a;
border-left-color: #ff8a65;
}
[data-theme='dark'] .callout-gray_background,
[data-theme='dark'] .callout-default {
background-color: #2a2a2a;
border-left-color: #757575;
}6. quoteブロックもcallout UIで統一
Notionのquote(引用)ブロックもcalloutと同じUIスタイルで表示することで、見た目を統一。
case 'quote': {
const text = richTextToMarkdown(block.quote.rich_text);
const color = block.quote.color || 'default';
// quoteもcalloutマーカーで出力(アイコンは引用符)
return `:::callout{icon="💬" color="${color}"}\n${text}\n:::`;
}Tips
Tip 1: markdownToHtml内でHTMLタグが消える(エスケープされる)ため、プレースホルダー方式が必須。直接HTMLを埋め込む方法はmarkdown-itの設定次第で動かないことがある
Tip 2: Notionのcalloutカラーは
{色}_background形式(例:green_background)。_backgroundなしのカラー名はテキスト色を指し、背景色とは別物
Tip 3: callout内にインラインコード(
)や太字()が含まれる場合、content`をmarkdownToHtmlで別途変換すると見栄えが良くなる
Tip 4: CSSの色値はNotionの公式カラーに合わせるとユーザーの期待通りの表示になる。Notion Web版のDevToolsで実際のRGB値を確認するのが確実
まとめ
- Notion Blocks → Markdown変換ではcalloutの情報が失われるのが根本的な問題
:::calloutマーカー形式でMarkdownに出力し、プレースホルダー方式でHTML変換を通す- CSSでNotionの全8色 + ダークモードに対応し、左ボーダーラインで視覚的に区別
- quoteブロックもcallout UIに統一することで、記事全体のデザインに一貫性を持たせる
- プレースホルダー方式は他のカスタムブロック(embed, bookmark等)にも応用可能
参考リンク
更新履歴
- 後日談を追加(左 4px バーのデザインが本文を分断して見えたため、全周 1px ボーダーに変更したこと。変換ロジックはそのまま)。壊れていた目次マーカーを自動目次に差し替え。意味が通っていなかった Tip 3 を書き直し。引用形式の Tips を callout に統一。コードブロックの言語指定とファイルパスのキャプションを整理。参考リンクの節を新設。箸条書きだった「はじめに」を文章に整理。

