product Software Technology

KuroCMS DevLog Vol.5: Recipe Type Implementation

We implemented a new Recipe Card feature in KuroCMS based on user feedback. Read about the technical architecture, JSON-LD generation, and editor integration details.


In "KuroCMS" and "KuroEditor," developed as a next-generation headless CMS working at the edge, a new "Recipe Card" has been officially implemented in the latest version. While previous versions of KuroCMS focused on general document expression using markdown and standard HTML, the introduction of this Recipe Card allows strict handling of metadata such as cooking time, ingredient lists, servings, and steps, ensuring consistent output as structured data that search engines like Google can easily understand (Schema.org Recipe). In this article, we detail the technical design of this Recipe Card and show how validation is enforced across the editor, API, and frontend layers.

Design Philosophy and Base Design: Smart Data Model Limited to 1 Document = 1 Recipe

In implementing the Recipe Card, the biggest challenge the development team faced was "data consistency and eliminating redundancy." While general recipe sites often have multiple recipe cards scattered within a single article, this tends to generate a broken structure from the perspective of search engine structured data (JSON-LD). If multiple different dishes or steps are output in parallel for a single URL (article), the mapping between common properties (such as dish name, finished image, description, and publish date) and individual recipe cards becomes ambiguous.

Therefore, KuroCMS enforces a rule-based constraint of "1 Document = 1 Recipe." The RecipeCard is only responsible for the "recipe-specific variable parts"—servings, cooking time, ingredients, and steps—while common information like dish name, description, finished image, and publish date is reused from the document properties (title, summary, cover image, creation date, etc.). This avoids redundant inputs and prevents issues where identical recipe names loop in a broken format within the same structured data.

Honestly, from the design perspective of KuroEditor, we did not want to accept the 1 Document = 1 Recipe constraint as it limits layout flexibility. However, because user recipe pages get higher SEO evaluations and are easier to read and reference, we strictly enforce the 1-recipe-card-per-article rule in the current version.

Consistency Guarantee by Shared Pure Functions

In a headless CMS, executing identical structure validation across the rich editor (KuroEditor), saving API, and the static build process of public pages is not easy. If you write validation logic separately using different languages or frameworks at each layer, implementation gaps will inevitably cause issues, such as "saved in the editor but failed at the API server," or "saved in the database but failed at frontend build, breaking the website."

To solve this fundamentally, KuroCMS adopted a "Shared Pure Function" design pattern. Specifically, we vendor "dist/kuro-recipe.js" (built in the upstream KuroEditor repository) into the KuroCMS core as "src/kuro-recipe.js," alongside the type definition file "kuro-recipe.d.ts." This ensures that the exact same JavaScript code inspects the recipe data in the body HTML during editor editing, saving API calls, and static builds. This Single Source of Truth design minimizes bugs.

*Since both KuroEditor and KuroCMS are "open-source software" developed by Kuroboo, referencing the GitHub history together will make it easier to understand.

Strict Validation in Saving API: recipe-guard.ts Specifications

When an article is saved (PUT) in KuroCMS, the server-side API does not just store the string. It strictly inspects whether the recipe data is correctly embedded within the sent body HTML. This role is handled by the "checkRecipeCards(bodyHtml, isRecipeType)" function in "src/recipe-guard.ts" on the backend.

The most critical aspect of this validation is its timing. KuroCMS considers multi-user collaborative editing, and final saved data is written to the database after a "3-way merge" of multiple versions. If validation is run before the merge, we cannot prevent cases where the merge accidentally duplicates recipe cards or inserts broken data. Therefore, the check is always executed on the final body text after the merge. If validation fails, the API returns a clear error to the client, blocking the save process.

Inspection Item Details and Security Measures
Number of Cards Confirm that exactly 1 card exists in a recipe article, and exactly 0 cards exist in non-recipe articles.
data-recipe Decryption Decrypt the encoded recipe JSON data embedded in the HTML tag and check if it conforms to the schema.
Attribute allowlist Strictly check for unknown HTML attributes. This eliminates vulnerabilities where malicious users inject XSS scripts like "onclick" via HTML body text.
Versioning Check Check version compatibility of the recipe schema to prevent saving old card versions or corrupted formats.

JSON-LD Output: Auto-Generation of Recipe Structure for SEO

This is the core value of adding the recipe card. Normally, KuroCMS outputs "Article" as structured data for the page, but when a document contains a recipe card, it automatically replaces it and outputs "Recipe" as the primary structured data.

It reads the "data-recipe" attribute from the saved HTML and passes the parsed data as "article.recipe" and "page.isRecipe" to the HTML template. To avoid "type check by string comparison" in the template, it uses a simple boolean branch (#if page.isRecipe) to switch templates. Time data is automatically converted internally to the international standard ISO-8601 duration format (e.g., "PT25M" for 25 minutes). Since the HTML display and JSON-LD structured data are generated from the exact same single data source, there is no risk of mismatched information for search engines.

Admin Integration: Dynamic Editor Re-creation Logic

Integration logic has also been built into the editor area of the admin panel. In KuroEditor, showing or hiding the "Pot button" for recipe creation is controlled by the "recipeUi" option passed at initialization. Since KuroEditor's default behavior is to show the button (true), KuroCMS does not need to specify it normally, but if set to false, it looks unchanged to users who do not use KuroEditor for recipe editing, minimizing the impact of the change.

Furthermore, at the KuroEditor level, recipe cards are now locked to 1 recipe per article (the second button is grayed out and cannot be clicked). This preserves the core rule of recipe articles. KuroCMS also performs a double-check on save to ensure recipe-specific checks run correctly for recipe articles.

Based on this, a recipe-related test suite "npm run test:recipe" (src/recipe-guard.test.ts, 12 test cases in total) has been added, validating everything from card counts to character escaping in attributes.

Remaining Issues and Future Roadmap

With this update, the recipe article "core system" and "save validation" are fully operational. However, KuroCMS does not yet offer a full-scale recipe page layout like large portal sites. Our goal was to allow easy recipe posting on personal websites, generating SEO data automatically without the user having to write it. Now anyone can easily launch a cooking site.

Also, since KuroCMS features a template system, users can continue brushing up the design on their own (see Source 3 for details). To make KuroCMS even easier to use, the development team will continue implementing updates based on your feedback, so thank you for your support!



【Sources】

  1. KuroCMS Official Codebase and Implementation Specification
  2. Schema.org Recipe Structured Data (JSON-LD) Official Definition
  3. Public Template Docs 5: Recipe Dedicated Card (RECIPE)