KuroCMS開発記 Vol.5:レシピ専用機能の実装
product Software Technology

KuroCMS開発記 Vol.5:レシピ専用機能の実装

黒兎のリアルな知り合いから「料理のレシピ」に対応したカードを追加してくれ!と、言われたので早速、レシピフォーマット?なるものを調査して、KuroCMSにその機能を追加しました。今回追加されたレシピカードとは何なのか、何が便利になるのかも詳しく紹介していきます。


エッジで動作する次世代ヘッドレスCMSとして開発された「KuroCMS」及び「KuroEditor」において、最新バージョンとして新たな「レシピカード」が正式に追加実装されました。これまでのKuroCMSは、標準的なHTMLを用いた汎用的なドキュメント表現を中心としていましたが、今回のレシピカードの導入により、料理の調理時間、材料リスト、人数、手順といったメタデータを厳密に扱い、Googleなどの検索エンジンが理解しやすい構造化データ(Schema.org Recipe)として一貫した出力をすることが可能になりました。本記事では、このレシピカードがどのような技術設計のもとに作られ、いかにしてエディタ・API・フロントエンド間でバリデーションを徹底しているのか、その詳細なアーキテクチャと実装の裏側を詳しく紹介します。

設計思想と基本設計:1記事=1レシピに制限したスマートなデータモデル

レシピカードを実装するにあたり、開発チームが直面した最大の課題は「データの一貫性と冗長性の排除」でした。一般的なレシピ投稿サイトでは、一つの記事の中に複数のレシピカードが乱立することがありますが、これは検索エンジンの構造化データ(JSON-LD)の観点から見ると、非常に望ましくない壊れた構造を生成しがちです。なぜなら、1つのURL(記事)に対して複数の異なる料理や手順が並列で出力されると、料理名や完成画像、説明文、公開日などの記事共通プロパティと、個々のレシピカード内のデータとの紐付けが曖昧になるためです。

そこでKuroCMSでは、設計思想として「1記事=1レシピ(1 Document = 1 Recipe)」という制約をルール化しました。レシピカード(RecipeCard)は、人数、調理時間、材料、調理手順という「レシピ固有の可変部分」のみを担当し、料理の名称、説明文、完成画像、および公開日などの共通情報は、ドキュメント自体のプロパティ(タイトル、サマリー、カバー画像、作成日など)を再利用する設計としました。これにより、無駄な二重入力を防ぎ、同名のレシピが同じ構造化データ内で壊れた形でループする問題を防いでいます。

正直、KuroEditor 側の設計思想としては、レイアウトの制約になる1記事=1レシピに対してあまり受け入れたく無かったのですが、作成されたユーザーのレシピページがSEO的に評価が高くなり、見やすく参照もされやすくなるということで、現行バージョンでは1記事1レシピカードのルールを厳守しています。

共有純関数による一貫性保証

ヘッドレスCMSにおいて、リッチエディタ(KuroEditor)、保存用API、そして公開ページの静的ビルドプロセスの3箇所で、同じデータの構造検証(バリデーション)を行うことは容易ではありません。それぞれのレイヤーで異なる言語やフレームワークを使って個別にバリデーションを記述すると、実装のズレによって「エディタでは保存できたのに、APIサーバーでエラーになって公開できない」、あるいは「データベースには保存できたが、フロントエンドのビルドでエラーが発生してWebサイトが壊れる」といった不整合が必ず発生します。

この問題を抜本的に解決するため、KuroCMSでは「共有純関数(Shared Pure Function)」の設計手法を採用しました。具体的には、上流のKuroEditorリポジトリでビルドされた共通のレシピ処理モジュールである「dist/kuro-recipe.js」を、KuroCMS本体の「src/kuro-recipe.js」としてベンダーイン(内包)し、型定義ファイルである「kuro-recipe.d.ts」とともに配置しています。これにより、エディタでの編集時、保存API、そして公開静的ビルド時に、全く同じJavaScriptコードが同じ検証基準で本文HTML内のレシピデータを検査する体制が整いました。別々の実装を持たせないこの単一光源(Single Source of Truth)の設計により、バグの発生を最小限に抑えています。

※KuroEditor も KuroCMS も黒兎が開発している「オープン・ソース・ソフトウェア」なので、Github の履歴なども一緒に参照してもらえると分かりやすいと思います。

保存APIにおける厳格な検証:recipe-guard.tsのバリデーション仕様

KuroCMSに記事が保存(PUT)される際、サーバーサイドのAPIはただ文字列を保存するだけではありません。送信された本文HTMLの中にレシピデータが正しく埋め込まれているかを厳しく検査します。この役割を担うのが、バックエンドに実装された「src/recipe-guard.ts」内の「checkRecipeCards(bodyHtml, isRecipeType)」関数です。

この検証において最も重要なのは、検証を実行するタイミングです。KuroCMSは一応、複数人による共同編集を考慮しており、最終的な保存データは複数バージョンの「3-wayマージ」を経てデータベースに書き込まれます。もしマージ前のデータに対して検証を行ってしまうと、マージの結果として偶然レシピカードが2枚に増えてしまったり、壊れたデータが入り込んだりするケースを防げません。そのため、チェックは必ずマージ完了後の最終本文に対して実行されます。バリデーションに失敗した場合、APIはクライアントに明確なエラーを返し、保存処理をブロックします。

検査項目 具体的な検査内容とセキュリティ対策
レシピカードの個数 記事では必ず1個のカードが存在すること。レシピ記事でない場合では、必ず0個であることを確認します。
data-recipe属性の復号 HTMLタグに埋め込まれたエンコード済みのレシピJSONデータを復号し、スキーマ定義に合致しているかを判定します。
属性のホワイトリスト(allowlist) 未知のHTML属性が混入していないか厳しくチェックします。これにより、悪意あるユーザーが本文のHTMLを経由して「onclick」などのXSS攻撃用スクリプトを持ち込む脆弱性を根本的に排除します。
バージョニング検査 レシピスキーマのバージョン互換性をチェックし、古いバージョンのカードや破損したデータフォーマットの保存を未然に防ぎます。

構造化データ(JSON-LD)の出力仕様:SEO効果を最大化するRecipe構造の自動生成

今回のレシピカードの追加の一番のキモがここになります。通常、KuroCMSはページの構造化データとして「Article」を出力しますが、ドキュメントにレシピカードがある場合には、Articleを併記するのではなく、「Recipe」を主たる構造化データとして自動的に置換出力します。

保存された正本HTMLから「data-recipe」属性を読み込み、パースしたデータを「article.recipe」および「page.isRecipe」という変数としてHTMLテンプレートへと渡します。この際、テンプレート側で「文字列比較による型判定」をさせないよう、論理値によるシンプルな条件分岐(#if page.isRecipe)でテンプレートを切り替える実装になっています。また、調理時間などの時間データは、国際規格であるISO-8601 durationフォーマット(例えば、25分なら「PT25M」)へと内部的に自動変換されて出力されます。表示用のHTMLとJSON-LD構造化データが全く同じ単一のデータソースから生成されるため、検索エンジンに対して情報が食い違うリスクもありません。

管理画面との親和性:エディタの動的再生成ロジック

ユーザーが直接操作する管理画面のエディタ領域にも、しっかり連携ロジックが組み込まれました。KuroEditor側の仕様として、レシピ作成用の「鍋ボタン」の有効・無効は、エディタ生成時に渡される「recipeUi」オプションによって制御されます。KuroEditorのデフォルトの挙動は「鍋ボタンを表示(true)」となっているため、KuroCMSでは特に指定の必要がありませんが、false にすれば、KuroEditor をレシピ編集に使わないユーザーから見れば、何も変更されていないように見えるので、影響範囲を小さくすることが可能です。(設定で1行増えてしまいますが・・)

さらに重要な仕様として、KuroEditor の時点でレシピカードは1記事1レシピを固定化(2枚目はボタンがグレーになって押せない)するようになりました。これによってレシピ記事の大原則を維持できるようになっています。さらには KuroCMS 側でも記事の保存時にレシピカードの有無をチェックし2重チェックを行って、その後に、レシピ記事の場合にはレシピ専用のチェック機能が働くようになっています。

この仕様に基づき、レシピ関連のテストスイートとして「npm run test:recipe」(src/recipe-guard.test.ts、計12テストケース)が追加されており、個数チェックから属性値内の特殊文字エスケープ判定に至るまで堅牢にテストされています。

残された課題と今後の開発ロードマップ

今回のアップデートによって、レシピ記事の「コアシステム」および「保存バリデーション」は完全に稼働しました。しかしながら、KuroCMSは大手のレシピサイトが提供するような本格的なページを提供するには至っていません。あくまでも個人サイトにおいて、簡単にレシピを投稿して、それらがSEO的に必要なデータをユーザーが作成することなく自動生成してくれることを目指しました。これで誰でも簡単にお料理サイトを立ち上げられるようになりましたw

またKuroCMSはテンプレートシステムが搭載されているので、ユーザー自身によるさらなるデザイン上のブラッシュアップを進めていけるようになっています。(詳しくは出典3を参照)もちろん、よりKuroCMSが使いやすいものになるように、開発チームは今後もユーザーの皆様からのフィードバックをもとに、さらに使いやすいCMSの開発を進めていきますので、何卒応援を宜しくお願いします!



【出典】

  1. KuroCMS 公式コードベースおよび実装仕様書
  2. Schema.org Recipe 構造化データ(JSON-LD)公式仕様定義
  3. 公開テンプレートドキュメント 5 レシピ専用カード(RECIPE)