mirror of
https://github.com/profullstack/logicsrc.git
synced 2026-10-03 05:07:10 +00:00
docs: OpenCoupon and OpenRecipe.md, the first two niche specs
Anthony: every top-level nichedb.dev niche may need its own open<niche> spec so the serve-your-own-file pattern scales across industries. These are the two he named first. OpenCoupon: one JSON file a merchant serves at /.well-known/opencoupon.json about what is on offer right now: every code, sale and shipping threshold with kind (percent, amount, shipping, bogo, gift, other), value, scope, min_order, dates, status, per-customer and region limits. Expired coupons stay in the file so a directory learns a code died from the one party that knows. No affiliate links, no redemption, no votes. First reader: nichedb.dev/c/deals. OpenRecipe.md: one Markdown file that is a recipe, in the OpenProfile.md and OpenResume.md style: a summary block (Serves, Prep, Cook, Cuisine, Course, Diet, Author, Source, Image), a description line, Ingredients and Steps as written, Notes, Nutrition per serving. Served next to the page, linked with rel="openrecipe", or indexed at /.well-known/openrecipe.md. A one-way mapping to schema.org/Recipe: the JSON-LD is generated from the Markdown, never the reverse. Both registered in DOC_SLUGS, NAV, STATIC_ROUTES and llms.txt. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014cmNRtR2vL1p89dbVQ7FZJ
This commit is contained in:
parent
41c362ddd8
commit
c3f051736b
8 changed files with 742 additions and 0 deletions
211
apps/logicsrc-web/src/app/openrecipe/page.tsx
Normal file
211
apps/logicsrc-web/src/app/openrecipe/page.tsx
Normal file
|
|
@ -0,0 +1,211 @@
|
|||
import Link from "next/link";
|
||||
import type { ReactNode } from "react";
|
||||
import type { Metadata } from "next";
|
||||
import { SiteShell } from "@/components/site-shell";
|
||||
import { mono, pre, table, td, th } from "../openontology/ui";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "OpenRecipe.md · LogicSRC",
|
||||
description:
|
||||
"OpenRecipe.md is one Markdown file that is a recipe: a summary block (Serves, Prep, Cook, Cuisine, Diet, Author, Source), ingredients and steps as written, notes and nutrition, served next to the recipe page or linked with rel=\"openrecipe\". schema.org/Recipe JSON-LD is derived from it, never the reverse.",
|
||||
alternates: { canonical: "/openrecipe" }
|
||||
};
|
||||
|
||||
const EXAMPLE = `# Shakshuka
|
||||
|
||||
- **Serves**: 4
|
||||
- **Prep**: 10 min
|
||||
- **Cook**: 25 min
|
||||
- **Cuisine**: North African
|
||||
- **Diet**: vegetarian, gluten-free
|
||||
- **Author**: Ada Lovelace
|
||||
- **Source**: https://ada.example/recipes/shakshuka
|
||||
|
||||
Eggs poached in a spiced tomato and pepper sauce. One pan, bread on the side.
|
||||
|
||||
## Ingredients
|
||||
|
||||
- 2 tbsp olive oil
|
||||
- 1 onion, diced
|
||||
- 800 g canned crushed tomatoes
|
||||
- 6 eggs
|
||||
- parsley, chopped (to serve)
|
||||
|
||||
## Steps
|
||||
|
||||
1. Heat the oil in a wide pan. Cook the onion until soft, about 8 minutes.
|
||||
2. Pour in the tomatoes, season, and simmer 10 minutes until thickened.
|
||||
3. Make six wells, crack an egg into each, cover, cook 5 to 8 minutes.
|
||||
|
||||
## Nutrition
|
||||
|
||||
- **Calories**: 260
|
||||
- **Protein**: 14 g`;
|
||||
|
||||
const RULES: Array<[string, string]> = [
|
||||
["One # heading", "The name of the dish. More than one and the first wins."],
|
||||
["The summary block", "Serves or Yield, Prep, Cook, Total, Cuisine, Course, Diet, Author, Source, Image are understood; unknown keys are kept."],
|
||||
["The description", "One prose line between the block and the first ##."],
|
||||
["## opens a section", "ingredients, steps (method, directions, instructions), notes, nutrition, equipment, variations. Unknown sections are kept."],
|
||||
["Ingredients as written", "One bullet per ingredient: 1 onion, diced. A reader parses a leading quantity when it can and keeps the line whole when it cannot. ### groups them."],
|
||||
["Steps in order", "One numbered item per step. A time in the text may become a timer but stays in the text."],
|
||||
["Nutrition per serving", "Key: value with the unit written; Per changes the basis. Never computed silently."],
|
||||
["Source and Author", "An adapted recipe names where it came from and says what changed in Notes."]
|
||||
];
|
||||
|
||||
const MAPPING: Array<[string, string]> = [
|
||||
["# heading", "name"],
|
||||
["Serves / Yield", "recipeYield"],
|
||||
["Prep, Cook, Total", "prepTime, cookTime, totalTime as ISO 8601 durations"],
|
||||
["Cuisine, Course, Diet", "recipeCuisine, recipeCategory, suitableForDiet"],
|
||||
["Ingredients bullets", "recipeIngredient, one string each, as written"],
|
||||
["Steps items", "recipeInstructions as HowToStep, ### groups as HowToSection"],
|
||||
["Nutrition", "nutrition as NutritionInformation"]
|
||||
];
|
||||
|
||||
const ABSENT: Array<[string, string]> = [
|
||||
["No required fields beyond the name", "A name and a list of ingredients is a valid recipe. So is a name and a list of steps."],
|
||||
["No unit system", "Grams and cups are both kept as written. A reader converts on display and says it did."],
|
||||
["No ingredient grammar", "1 onion, diced is one line, and every cooking app already parses lines like it."],
|
||||
["No ratings, no story", "The page has those. The file is the recipe."],
|
||||
["No JSON", "The structured view is derived and regenerated from the Markdown on every read."]
|
||||
];
|
||||
|
||||
export default function OpenRecipePage(): ReactNode {
|
||||
return (
|
||||
<SiteShell active="OpenRecipe.md">
|
||||
<div className="band">
|
||||
<div className="section-head">
|
||||
<p className="eyebrow">LogicSRC standards surface</p>
|
||||
<h2>OpenRecipe.md</h2>
|
||||
<p>
|
||||
One Markdown file that is a recipe, in the form a person writes one and a printer prints
|
||||
one. The page has the story. The file has the recipe.
|
||||
</p>
|
||||
</div>
|
||||
<p style={{ color: "#41505d" }}>
|
||||
A recipe on the web is four thousand words of memoir with the recipe at the bottom, and a
|
||||
block of JSON-LD in the head that search engines read and people never see. The two
|
||||
drift: the page says three eggs and the schema says two. The form people actually write
|
||||
recipes in has not changed in a century and it is Markdown already. OpenRecipe.md fixes
|
||||
where the file lives and which lines mean what, so a cooking app, a grocery list and an
|
||||
agent read the file the author wrote.
|
||||
</p>
|
||||
<p style={{ color: "#5b6b7a" }}>
|
||||
Status: 0.1. Same rules as <Link href="/openprofile">OpenProfile.md</Link> and{" "}
|
||||
<Link href="/docs/openresume">OpenResume.md</Link>: Markdown is canonical, every rule
|
||||
degrades, the structured view is derived.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="band">
|
||||
<div className="section-head">
|
||||
<h2>The shape</h2>
|
||||
</div>
|
||||
<pre style={pre}>{EXAMPLE}</pre>
|
||||
</div>
|
||||
|
||||
<div className="band">
|
||||
<div className="section-head">
|
||||
<h2>The eight rules</h2>
|
||||
<p>Every one of them degrades rather than fails.</p>
|
||||
</div>
|
||||
<table style={table}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style={th}>Rule</th>
|
||||
<th style={th}>What it means</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{RULES.map(([rule, meaning], index) => (
|
||||
<tr key={rule}>
|
||||
<td style={td}>
|
||||
<strong>
|
||||
{index + 1}. {rule}
|
||||
</strong>
|
||||
</td>
|
||||
<td style={td}>{meaning}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div className="band">
|
||||
<div className="section-head">
|
||||
<h2>Discovery</h2>
|
||||
<p>
|
||||
Next to the page (<code style={mono}>/recipes/shakshuka.md</code>), a{" "}
|
||||
<code style={mono}>{'<link rel="openrecipe">'}</code> on the page, or a site index at{" "}
|
||||
<code style={mono}>/.well-known/openrecipe.md</code>, one recipe per bullet. Served as{" "}
|
||||
<code style={mono}>text/markdown</code>.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="band">
|
||||
<div className="section-head">
|
||||
<h2>schema.org, one way</h2>
|
||||
<p>
|
||||
Keep serving schema.org/Recipe for search engines. Generate it from the Markdown on
|
||||
every publish. The Markdown is the canonical copy.
|
||||
</p>
|
||||
</div>
|
||||
<table style={table}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style={th}>OpenRecipe.md</th>
|
||||
<th style={th}>schema.org/Recipe</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{MAPPING.map(([md, schema]) => (
|
||||
<tr key={md}>
|
||||
<td style={td}>{md}</td>
|
||||
<td style={td}>
|
||||
<code style={mono}>{schema}</code>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div className="band">
|
||||
<div className="section-head">
|
||||
<h2>What is deliberately absent</h2>
|
||||
</div>
|
||||
<table style={table}>
|
||||
<tbody>
|
||||
{ABSENT.map(([what, why]) => (
|
||||
<tr key={what}>
|
||||
<td style={td}>
|
||||
<strong>{what}</strong>
|
||||
</td>
|
||||
<td style={td}>{why}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div className="band">
|
||||
<div className="section-head">
|
||||
<h2>Where everything lives</h2>
|
||||
</div>
|
||||
<ul style={{ color: "#41505d", lineHeight: 1.9, paddingLeft: "1.1rem" }}>
|
||||
<li>
|
||||
<Link href="/docs/openrecipe">Specification</Link>: the shape, eight rules, discovery,
|
||||
the schema.org mapping
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/openprofile">OpenProfile.md</Link>, the Author behind a recipe;{" "}
|
||||
<Link href="/opencoupon">OpenCoupon</Link>, the same serve-your-own-file idea for a
|
||||
merchant's promotions
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</SiteShell>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue