
【実装】Notion calloutブロックをNext.jsでカラフルなUIコンポーネントとして表示する
[toc]
はじめに
-
Notionで記事を書く際、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処理
// libs/notion.ts
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
CALLOUT_PLACEHOLDER_1
4. プレースホルダー変換
markdownToHtml前にプレースホルダーに置換
// src/app/blogs/[blogId]/page.tsx
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
{色}_background 形式(例: green_background)。_backgroundなしのカラー名はテキスト色を指し、背景色とは別物 )や太字()が含まれる場合、content`をmarkdownToHtmlで別途変換すると見栄えが良くなるまとめ
-
Notion Blocks → Markdown変換ではcalloutの情報が失われるのが根本的な問題
-
:::calloutマーカー形式でMarkdownに出力し、プレースホルダー方式でHTML変換を通す -
CSSでNotionの全8色 + ダークモードに対応し、左ボーダーラインで視覚的に区別
-
quoteブロックもcallout UIに統一することで、記事全体のデザインに一貫性を持たせる
-
プレースホルダー方式は他のカスタムブロック(embed, bookmark等)にも応用可能
最新記事
- 【設定・環境構築】OpenNext でNext.js SSGサイトをCloudflare Workersにデプロイする完全ガイド
2026/3/19
- 【実装】Notion calloutブロックをNext.jsでカラフルなUIコンポーネントとして表示する
2026/3/19
- 【トラブルシューティング】Cloudflare Pages → Workers 移行で遭遇したEdge Runtime問題集
2026/3/19
- 【実践】Next.js 13→16メジャーアップグレードの全記録 — 破壊的変更と対応策
2026/3/19
- 【自動化】Gemini Imagen APIでブログのeyecatch画像を自動生成してR2にアップロードする
2026/3/19
- 【実装】Notion APIでブログシステムを構築する(Next.js 13 App Router × SDK v5)
2026/3/19
- 【移行ガイド】microCMSからNotion APIへブログCMSを完全移行する
2026/3/19
- 【トラブルシューティング】本番デプロイで遭遇した問題と解決策まとめ
2026/3/15
- 【環境構築】Next.js × Cloudflare Workers の本番環境を一から構築する
2026/3/15
- 【設定・環境構築】Neon → Prisma Postgres 移行とローカル開発環境の構築
2026/2/26


