「構造化されたカスタムテンプレート」をどう作ったか:任意の CSS を開放せずにユーザーがスタイルを変えられるようにする
ユーザーから何度も同じ要望をもらいます:「GitHub テンプレートはいいんだけど、見出しの色を会社のブランドカラーに変えられない?行間ももう少し広く」。
小さな機能に聞こえますが、実装するとなると避けて通れないトレードオフがあります:任意の CSS を開放するかどうか?
この記事では、私たちが最終的に選んだ方法——既存のシステムテンプレートに構造化オーバーライドを重ねる——と、それが md-to.com のような「1 つのコンテンツを複数フォーマットへエクスポートする」ツールに合っている理由を記録します。
よくある 2 つの極端な手法
世の中の Markdown ツールが「カスタムスタイル」を扱う方法は、基本的に 2 通りです:
第 1 案:固定テンプレートのみ。 シンプルで安定していますが、足りません。ユーザーは最も近いテンプレートを選んだあと、結局「見出しだけ色を変えたい」というところで止まります。
第 2 案:任意の CSS を開放。 自由度は最大、プレビューはいくらでもいじれます。問題は、ブラウザで綺麗でもエクスポート後に綺麗とは限らない、という点です。
私たちは Markdown を PDF / Word / HTML / 画像に変換するツールを作っています。エクスポート経路の中には CSS を消費しないものがいくつもあります:
- Word(
.docx)は OOXML スタイルで動いており、「任意の CSS」という概念がそもそもありません。 - PDF 印刷には独自のページ分割・余白ルールがあり、ブラウザ CSS だけで作ったレイアウトは印刷時に崩れがちです。
- 画像エクスポートは canvas スクリーンショット経由で、特定の CSS 機能(一部の
filter、backdrop-filterなど)はサポートが限定的です。
ユーザーに任意の CSS を書かせると、プレビューは正常でもエクスポート後にスタイルが崩れます——この「見えているものと得られるものが違う」状態は最悪のユーザー体験です。
そこで第 3 の道を取りました。
私たちのアプローチ:ベーステンプレート + 構造化オーバーライド
中心となる考え方は一文で:ユーザーはまずシステムテンプレートを選び、その上に構造化されたフィールドを変更する。
完全なテンプレートを保存するのではなく、「ベーステンプレート ID + オーバーライド」を保存します:
{
version: 1,
baseTemplateId: 'classic',
overrides: {
colors: {
heading: '#0F766E'
},
fonts: {
body: 'Noto Sans SC'
},
fontSizes: {
body: 13
},
spacing: {
lineHeight: 1.7
}
}
}
なぜ完全なテンプレートを保存しないのか。理由は 3 つ:
- ベースが改善されたときにユーザーも恩恵を受ける。 例えば後で Classic テンプレートのコードブロック余白を最適化した場合、オーバーライドにそのフィールドがなければカスタムテンプレートも自動的に新しい値を採用します。
- payload が小さく、フロント・バック両方の検証が簡単。 バックエンドはホワイトリスト上の少数のフィールドだけを検証すればよく、テンプレートオブジェクト全体を検証する必要がありません。
- 「どのテンプレートをベースにした変更か」というセマンティクスが明示的に保存される。 UI で「あなたは Classic に対して 3 つの変更を加えています」と伝えられます。
以下が現在の編集 UI で、色・フォント・フォントサイズ・間隔という 4 つの高頻度グループをカバーしています:

実行経路:1 回のクリックから 1 回のエクスポートまで
システム全体は 3 本の経路でつながっています。
経路 1:リアルタイムプレビュー
ユーザーがエディタで色を変えたら、プレビューは即座に更新されなければなりません:
- エディタが新しいオーバーライド値を
localStorage.customDocumentTemplateに書き込む。 - エディタが
template-changeイベントを発行する。 MarkdownPreviewがイベントを受け取りgetActiveTemplate()を呼ぶ。getActiveTemplate()がdocumentTemplate(現在のベース)とcustomDocumentTemplate(オーバーライド)を読み出し、両者が一致すればcreateCustomTemplate(baseTemplate, overrides)を呼んで完全なテンプレートにマージする。generatePreviewCSS(template)がプレビュースタイルを再生成する。
マージ自体は単純で、フィールドグループごとの浅いマージです:
return {
...baseTemplate,
id: `custom:${baseTemplate.id}`,
colors: {
...baseTemplate.colors,
...(overrides.colors ?? {}),
},
fonts: {
...baseTemplate.fonts,
...(overrides.fonts ?? {}),
},
fontSizes: {
...baseTemplate.fontSizes,
...(overrides.fontSizes ?? {}),
},
spacing: {
...baseTemplate.spacing,
...(overrides.spacing ?? {}),
},
};
経路 2:プライベートテンプレートとして保存
ユーザーが「テンプレートを保存」を押すと、ツール adapter が現在の状態をパッケージ化します:
{
version: 1,
toolSlug,
documentTemplateId,
customDocumentTemplate
}
この payload は saved_templates.settings_json に書き込まれます。documentTemplateId と customDocumentTemplate を分けて保存している点に注目してください——プライベートテンプレートのセマンティクスは「テンプレート X の上に Y 個の変更」であって、出自を切り離したスタイルスナップショットではありません。
経路 3:プライベートテンプレートを適用
ユーザーが PrivateTemplateSelector から再選択するか、?tpl=<id> パラメータでロードしたとき:
- adapter が
applySettings(settingsJson)を呼ぶ。 - payload に
customDocumentTemplateがあれば localStorage に書き戻し、なければクリアする。 documentTemplateIdを適用する。template-changeを発行し、プレビューとエクスポート経路はマージ後の完全なテンプレートを受け取る。
見落としがちだが重要な点:システムテンプレートを選んだらカスタムオーバーライドをクリアする
setActiveTemplate(templateId) の中に 1 行追加しています:
localStorage.removeItem('customDocumentTemplate');
なぜか?クリアしないと、こんな奇妙な状況が起きます:
- ユーザーが Classic テンプレートで見出し色を緑に変え、localStorage に保存される。
- ユーザーが Dracula テンプレートに切り替える。
- Classic に戻すと、見出しがまた緑——でもユーザーは変更したことを覚えていない。
私たちは「システムテンプレートを選ぶ」ことを明示的なリセット動作として定義しました:きれいなテンプレートを選んだなら、それはきれいです。その上で変えたければ「カスタマイズ」をもう一度押す。これは「ユーザーの最後の編集をできる限り維持する」よりはるかに分かりやすいセマンティクスです。
TemplatePickerModal で「適用」を押したときも同じクリーンアップが走ります。
バックエンド検証:ホワイトリストが安全網
バックエンドの validateTemplateInput() は settingsJson.customDocumentTemplate を厳しく検証します:
customDocumentTemplateはオプショナル;存在する場合はオブジェクトでなければならない。versionは1でなければならない(後の移行余地を残すため)。baseTemplateIdは空でない文字列でなければならない。overridesはオブジェクトでなければならない。- ホワイトリストにないフィールドは拒否される。
ホワイトリストは現在の UI で変更可能なすべてのフィールドをカバーし、UI より少し広く取って将来のフィールド追加に余裕を持たせています:
| グループ | 許可フィールド |
|---|---|
colors | heading, text, link, codeBackground, codeBorder, codeText, quoteText, quoteBorder, quoteBackground, tableHeaderBg, tableHeaderText, tableBorder, tableRowOdd, tableRowEven |
fonts | heading, body, code |
fontSizes | h1, h2, h3, h4, h5, h6, body, code |
spacing | headingBefore, headingAfter, paragraphBefore, paragraphAfter, lineHeight |
ホワイトリストはこの設計全体の安全網です:任意のキーを受け付けないので、後でフィールドにレンダリングロジックを追加したり名前を変えたりしても、ユーザーが保存した古いキーがランタイムテンプレートに紛れ込んで出力を汚染することはありません。
効果:エクスポート経路は「カスタムテンプレート」を意識する必要がない
マージ後は完全な DocumentTemplate なので、下流のエクスポート関数はどれも変更不要です:
- プレビュー:
generatePreviewCSS(template) - PDF / 印刷:
generatePdfStyles()/generatePrintStyles() - HTML:
generateExportHTML() - リッチテキストコピー:
generateRichTextHTML() - Word:
templateToDocxStyles(template)
これがまさに、最初に「任意の CSS」を拒否したことの最大の見返りです:全出力が DocumentTemplate という統一された中間表現を消費し続ける限り、フォーマット間の一貫性はほぼ無料で得られる。ユーザーが選んだ見出しの色は、ブラウザプレビュー、PDF、Word、画像のどれでも同じ色で見えます——「だいたい同じ」ではなく、文字どおり同じ hex 値です。すべてが同じマージ済みテンプレートから読み出されているからです。
現時点の制約
第 1 版は意図的に範囲を狭くしています:
- 高頻度フィールドのみ開放。すべての色、フォントサイズ、間隔値を編集できるわけではない。
- カスタムテンプレートの命名入口はなし;命名は「テンプレートを保存」ダイアログ経由のまま。
- テンプレートの JSON インポート / エクスポートは未対応。
- 任意の CSS は未開放。
- エディタ UI は軽量な初版——グループ化されたパネル、プリセットカラー、モバイルレイアウトはまだ改善余地あり。
今後の方向
この構造から先に進むためのいくつかの目印:
- 編集可能フィールドの追加。 表の枠線色、交互行の背景、引用の背景、コードの文字色——ホワイトリストには既に枠が用意してあり、足りないのは UI だけ。
- 統一されたフィールド schema。 フロントのフォームとバックの検証が同じ定義を共有し、二重管理によるズレを防ぐ。
- workspace のテンプレート一覧に「編集」入口を追加。 今は新しいコピーとしてしか保存できず、既存のプライベートテンプレートを継続編集できない。
- 「システムテンプレートをプライベートテンプレートとして複製」する明示的なフロー。 「Classic をベースに自分のテンプレートを作りたい」を直接的な操作にする。
- カスタムテンプレートの JSON インポート / エクスポート。 チーム内でスタイルを共有するときに役立つ。
まとめ
振り返ると、この設計の鍵は「何を作ったか」ではなく「何を作らなかったか」です:
- 任意の CSS を開放しなかった——フォーマット間の崩壊を回避。
- 完全なテンプレートスナップショットを保存しなかった——ベーステンプレートのアップグレード恩恵を保持。
- システムテンプレート切替時にオーバーライドを密かに残さなかった——「変えてないのにスタイルが違う」混乱を回避。
- バックエンドはホワイトリスト外のフィールドを受け付けなかった——将来の進化に余地を残した。
制約はむしろ予測可能な体験をもたらします。複数のエクスポートフォーマットをまたぐツールでは、その予測可能性は自由度より価値があります。
「1 つのコンテンツ、複数のエクスポート」を作るプロダクトに関わっているなら、md-to.com でこのカスタムテンプレートを試してみてください——あなたが探していた「ちょうどいい」自由度かどうかを確かめてみてください。
関連ツール
無料 Markdown から Word 変換 - オンラインで MD を DOCX に変換
無料オンライン Markdown から Word コンバーター。リアルタイムプレビュー、ワンクリックで DOCX ダウンロード。フォーマット完全保持、登録不要、ローカル処理でプライバシー保護。今すぐお試しください!
無料 Word から Markdown 変換 - オンラインで DOCX を MD に変換
無料オンラインで Word 文書を Markdown に変換。DOCX から MD コンバーターで見出し、表、リスト、書式を完全保持。登録不要、今すぐお試しください!
無料 Markdown から PDF 変換 - オンラインで MD を PDF に変換
無料オンライン Markdown から PDF コンバーター。20 + 種類のプロフェッショナルテンプレート、数式・コードハイライト・テーブル対応。登録不要、ローカル処理でプライバシー保護。今すぐお試しください!
無料 Markdown から LaTeX 変換 - オンラインで MD を TeX に変換
オンラインで無料で Markdown を LaTeX に変換。MD ドキュメントを学術論文や技術文書用の TeX 形式に変換。登録不要!