kt-tech.blog

画像

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

Share
📌
概要: NotionのcalloutブロックをNext.jsブログで色分けされたリッチなUIボックスとして表示する実装方法を解説します。Blocks→Markdown→HTML変換でcalloutが消えてしまう問題を、プレースホルダー方式で解決しました。

[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

📌
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等)にも応用可能