Markdownをレンダリングする際、日本語の「」や全角括弧、読点などの直前・直後に **~~ を配置すると、強調(太字)や取り消し線が正しくレンダリングされず、生の記号がそのまま表示されてしまう問題があります。

現象

以下のようなMarkdownをパースした際、強調が効きません。

個人開発を経験することで**「全体を俯瞰する視野」**が養われます。

原因

CommonMarkの仕様において、強調デリミタ(***)の開始・終了は、記号の前後にある文字が「空白(Whitespace)」か「句読点(Punctuation)」かによって判定されます。

日本語の「」や()はUnicode仕様上「句読点(Punctuation)」に分類されます。しかし、日本語は分かち書きをしないため、文字の間にスペースが入りません。 そのため、る**「全 のように、記号の前に通常文字()があり、後に句読点()がある組み合わせになると、パーサが開始デリミタ(left-flanking)として判定できなくなってしまいます。

解決策

CJK(中国語、日本語、韓国語)環境でのパース不具合を補正する remark プラグインである remark-cjk-friendly(太字・斜体用)および remark-cjk-friendly-gfm-strikethrough(GFMの取り消し線用)を導入します。

1. パッケージのインストール

bun add remark-cjk-friendly remark-cjk-friendly-gfm-strikethrough

2. パーサ(Unified)への追加

unified 処理パイプラインにおいて、remarkParse および remarkGfm の直後にこれらのプラグインを適用します。

import { unified } from 'unified';
import remarkParse from 'remark-parse';
import remarkGfm from 'remark-gfm';
import remarkCjkFriendly from 'remark-cjk-friendly';
import remarkCjkFriendlyGfmStrikethrough from 'remark-cjk-friendly-gfm-strikethrough';

const processor = unified()
  .use(remarkParse)
  .use(remarkGfm)
  .use(remarkCjkFriendly) // 太字・斜体用
  .use(remarkCjkFriendlyGfmStrikethrough) // 取り消し線用
  // ...その後の処理

これで、**「テキスト」** のような日本語特有の記述でも、期待通り <strong>「テキスト」</strong> に変換されるようになります。

ここ数日、AIコーディングアシスタント(Antigravity)と共にこのリポジトリの機能追加や設計変更を進めていく中で、非常に高い効果を発揮した開発・協働ノウハウを整理します。

1. AIエージェントの行動を安定させる「AGENTS.md」の活用

AIとのペアプログラミングにおいて、エージェントの「迷走」を防ぐために最も効果的だったのが、プロジェクトルートに配置した AGENTS.md の整備と継続的なアップデートです。

整備のポイント

  • コンテンツパイプラインの明文化:
    このブログは「ブログ記事」「Scraps」「学習ガイド」「AWSアーキテクチャ・ギャラリー」など複数のMarkdown/YAMLベースのパイプラインを持っています。それぞれのデータ抽出(Zodスキーマ、Repository)、パース(Remark/Rehype)、表示層の役割を明記しておくことで、AIが新しいコンテンツタイプを追加・修正する際にも既存の共通ユーティリティ(renderPostMarkdown など)を適切に再利用できるようになりました。
  • 開発コマンドとスタイリングルールの共有:
    使用するパッケージマネージャー(bun)や、36種類のデザインテーマが存在するスタイリングシステム(data-theme 属性とCSS変数による動的切り替え)の仕様をドキュメント化しておくことで、デザイン崩れを防ぐことができます。
  • コミット前フックとの連動:
    simple-git-hookslint-staged を用いて、コミット時に自動で eslint --fix および type-check が走る仕組みを導入。AIが誤った型定義やコードスタイルを提案しても、コミット前の防御壁として機能し、即座にAIが自己修復アクションを起こせるサイクルを作りました。

2. Next.js 16 (SSG) での Mermaid ダイアグラム動的描画の分離

ブログ記事等で Mermaid 記法を用いたダイアグラムを安全に表示するための、静的生成(SSG)とクライアントサイドの役割分担ノウハウです。

実装の工夫

  • Shikiによるシンタックスハイライトのバイパス:
    通常、Remark/Rehypeのビルドプロセスを通すと、```mermaid ブロックも Shiki によってHTMLエンティティ化されたソースコードブロックとしてハイライトされてしまいます。これを防ぐため、レンダラー(src/lib/post-renderer.ts)で Mermaid コードブロックを抽出し、生のテキストを含む特定のHTMLラッパー(例: <div class="mermaid">...</div>)として出力するようにカスタマイズしました。
  • 非同期ロードとSVG描画:
    ビルドされた静的HTMLがクライアントサイドでマウントされた後、Reactの useEffect または専用のフックを使い、クライアントサイドで Mermaid.js を動的にインポートしてSVGにレンダリングします。これにより、Next.js 16 の output: 'export' 環境でもビルドエラーを引き起こさず、軽量かつ安全に図面を動的描画する仕組みを確立しました。

3. 「多角的レビュー(Multi-Perspective Audit)」による品質・コンテンツの監査

AIエージェントの得意分野である「高速な全文コンテキスト把握」を活かし、コードだけでなく、プロジェクト全体やコンテンツの多角的なレビューを依頼する手法です。

  • 多角的プロジェクトレビュー:
    Next.js 16の設計パターン、アクセシビリティ(A11y)、SEO、パフォーマンスといった異なる視点でのチェックリストをAIに与えて監査させました。PCレイアウト時のサイドバー目次(TOC)のスクロール追従(sticky)や、テーマ切り替え時のコントラスト確保など、UI/UXの細かな改善点が自動的にリストアップされ、順次修正することができました。
  • コンテンツ・ドキュメントの監査:
    learning/ ディレクトリ配下にある17つの学習コースの内容や、aws-patterns/ のCloudFormationテンプレート・Draw.io構成図をAIに一括レビューさせ、記述の古さやセキュリティ上の懸念点、ペダゴジカル(教育的)な説明の不足箇所を特定させました。これにより、単なるソースコードのバグ修正にとどまらず、プロダクトの提供価値全体の品質を維持するパートナーとしてAIを機能させることが可能になりました。

まとめ

AIアシスタントを真のペアプログラミングパートナーにするためには、「コンテキストの明文化(AGENTS.md)」「静的(サーバー)と動的(クライアント)の責務の明確な分離」、そして「多角的なレビューの組み込み」が極めて重要です。このサイクルを回すことで、開発速度とコード品質を同時に飛躍させることができます。

ここ数日、このブログに対して加えた大きな設計変更とリファクタリングのノウハウをまとめます。

1. CSS変数と data-* 属性による動的マルチテーマ&スキン設計

レイアウトや枠線、影といった「テーマの質感」と「アクセントカラー(スキン)」を直交させて制御する仕組みを構築しました。

  • data-theme 属性: Neo-Brutalism, Glassmorphism, Minimal, Neumorphism, Cyberpunk, Retro などのデザインテーマを制御
  • data-skin 属性: Emerald, Ocean, Sunset, Purple, Rose などのアクセントカラーを制御

設計のポイント

  1. 抽象スタイルの定義: コンポーネントには個別のスタイルを直接書き込まず、.theme-card.theme-btn といった抽象クラスを付与しておきます。
  2. テーマ別の差分定義: app/globals.css にて、テーマ別のセレクタで上書きします。
    /* Neo-Brutalism */
    [data-theme='neo-brutalism'] .theme-card {
      border: 3px solid var(--color-border);
      box-shadow: 5px 5px 0px 0px var(--color-border);
    }
    /* Glassmorphism */
    [data-theme='glassmorphism'] .theme-card {
      border: 1px solid rgba(255, 255, 255, 0.5);
      backdrop-filter: blur(12px);
    }
  3. スキンの分離: data-skin--color-accent のみを選択的に上書きすることで、すべてのテーマとシームレスに調和します。

2. html要素での一元管理とOS同期

ダークモード制御とテーマを安定して連動させるため、ダークモードクラス(.dark)を html 要素に付与するように変更しました。

また、matchMedia('(prefers-color-scheme: dark)') を用いた「OSテーマ同期」オプションを実装しました。これにより、ユーザー設定を維持しつつ、システム preference にも瞬時に反応するアクセシビリティの向上を実現しています。

3. ファイルアクセスの抽象化(MarkdownRepository)

ブログ記事(md/)とScraps(scraps/)で共通していた Markdown ファイルの読み込みロジックを、ジェネリックなリポジトリパターンに抽象化しました。

export interface MarkdownRepository {
  readAllSources(): Promise<MarkdownSource[]>;
  readSourceBySlug(slug: string): Promise<MarkdownSource | null>;
}

各コンテンツタイプのリポジトリは、このファクトリ関数 createMarkdownRepository(dirPath) を呼び出すだけでファイル操作ができるようになり、コードの重複を完全に排除(DRY化)できました。


この設計により、新テーマや新しいコンテンツ形式の追加、あるいはコンポーネントスタイルの刷新を非常に疎結合に行えるようになりました。

ブログに Scrap(技術メモ)機能を追加しました。

Zenn のスクラップのように、日々の気づきやエラー解決ログを短く残すための場所です。

使い方

scraps/ ディレクトリに Markdown ファイルを置くだけです。

---
title: メモのタイトル
date: YYYY-MM-DD
tags: [tag1, tag2]
---

本文は Markdown で自由に書けます。コードブロックにも対応しています。

App Router の fetch はデフォルトで積極的にキャッシュされる。開発中に API レスポンスが古いまま返ってきて原因究明に時間がかかった。

解決策

キャッシュを無効にしたい場合は cache: 'no-store' を渡す。

const res = await fetch('/api/data', { cache: 'no-store' });

再検証間隔を指定したい場合は next.revalidate を使う。

const res = await fetch('/api/data', { next: { revalidate: 60 } }); // 60秒ごと

落とし穴

generateStaticParams 内の fetch は SSG 時に 1 度だけ実行される。動的データを使いたい場合は Route Handler 経由にする必要がある。

React の cache() と Next.js の fetch キャッシュは別物なので混同しないように注意。

satisfies 演算子を使うと、型チェックをしつつ型を広げずに推論させられる。

const palette = {
  red: [255, 0, 0],
  green: '#00ff00',
} satisfies Record<string, string | number[]>;

// NG: as や明示的な型注釈だと string | number[] になる
palette.green.toUpperCase(); // OK — string として推論される
palette.red.at(0);           // OK — number[] として推論される

as const との組み合わせも強力。オブジェクトのキーが特定の型に合致することを保証しながら、値の型は可能な限り絞り込める。

ライブラリの設定オブジェクト定義や、定数マップを書くときに重宝している。