mirror of
https://github.com/profullstack/logicsrc.git
synced 2026-10-02 20:57:03 +00:00
docs: OpenCoupon and OpenRecipe.md, the first two niche specs (#165)
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. Claude-Session: https://claude.ai/code/session_014cmNRtR2vL1p89dbVQ7FZJ Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
26295dd740
commit
8269c12b75
8 changed files with 742 additions and 0 deletions
|
|
@ -30,6 +30,8 @@ export function GET(): Response {
|
|||
- [OpenBandwidth](${SITE_URL}/openbandwidth): The network block of an OpenServer offer: port speed, transfer, unmetered, 95th percentile or flat metering, overage, IPv4 and IPv6 addresses as a priced resource, DDoS scrubbing, and a range.
|
||||
- [OpenFile](${SITE_URL}/openfile): One file a publisher serves about the files it has published: content hash, swarm and HTTP fetch routes, verification, consent basis, price, and who holds it now, discovered at /.well-known/openfile.json. The web door onto an OpenSwarm ipfile swarm.
|
||||
- [OpenDisk](${SITE_URL}/opendisk): One file a machine serves about the disk it will rent: free GiB, price per GiB-month, location, policy, proof cadence and hub standing, discovered at /.well-known/opendisk.json. What a peer-to-peer storage market is made of; reference marketplace d1sks.com.
|
||||
- [OpenCoupon](${SITE_URL}/opencoupon): One file a merchant serves about what is on offer right now, at /.well-known/opencoupon.json: every code, sale and shipping threshold with kind, value, scope, dates, status and regions, expired codes kept so directories learn they died. A coupon site reads the merchant instead of a forum thread.
|
||||
- [OpenRecipe.md](${SITE_URL}/openrecipe): One Markdown file that is a recipe: summary block (Serves, Prep, Cook, Cuisine, Diet, Author, Source), ingredients and steps as written, notes, nutrition; served next to the page or linked with rel="openrecipe"; schema.org/Recipe JSON-LD is derived from it, never the reverse.
|
||||
- [AgentSwarm](${SITE_URL}/agent-swarm): Provider-neutral agent orchestration, model routing, and cost controls.
|
||||
- [AgentByte](${SITE_URL}/agentbyte): Agent screening sessions, policy events, and APIs.
|
||||
- [Credential Sharing](${SITE_URL}/credential-sharing): End-to-end-encrypted team vaults, plus source/target credential diffs, approval, sync, rollback, and audit.
|
||||
|
|
|
|||
185
apps/logicsrc-web/src/app/opencoupon/page.tsx
Normal file
185
apps/logicsrc-web/src/app/opencoupon/page.tsx
Normal file
|
|
@ -0,0 +1,185 @@
|
|||
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: "OpenCoupon · LogicSRC",
|
||||
description:
|
||||
"OpenCoupon is one file a merchant serves about what is on offer right now, at /.well-known/opencoupon.json: every code, sale and shipping threshold with its kind, value, scope, dates, status and regions, expired codes kept so directories learn they died. A coupon site reads the merchant instead of a forum thread.",
|
||||
alternates: { canonical: "/opencoupon" }
|
||||
};
|
||||
|
||||
const DESCRIPTOR = `{
|
||||
"merchant": { "name": "Northwind Outfitters", "web": "https://northwind.example",
|
||||
"currency": "USD", "terms": "https://northwind.example/promotions/terms" },
|
||||
"updated": "2026-09-13T06:00:00Z",
|
||||
"coupons": [
|
||||
{ "id": "fall15", "code": "FALL15", "title": "15% off everything for fall",
|
||||
"kind": "percent", "value": 15, "min_order": 50,
|
||||
"scope": { "excludes": ["gift-cards", "sale"] },
|
||||
"ends": "2026-09-30T23:59:59Z", "new_customers": false, "stackable": false,
|
||||
"regions": ["US", "CA"], "status": "active" },
|
||||
{ "id": "ship-75", "title": "Free shipping over 75", "kind": "shipping", "min_order": 75 },
|
||||
{ "id": "boots-sale", "title": "Trail boots, 40 off", "kind": "amount", "value": 40,
|
||||
"url": "https://northwind.example/boots/trail", "price": { "was": 160, "now": 120 } },
|
||||
{ "id": "summer10", "code": "SUMMER10", "title": "10% off summer", "kind": "percent",
|
||||
"value": 10, "ends": "2026-08-31T23:59:59Z", "status": "expired" }
|
||||
]
|
||||
}`;
|
||||
|
||||
const KINDS: Array<[string, string, string]> = [
|
||||
["percent", "value is a percentage", "15% off, capped by max_discount when stated"],
|
||||
["amount", "value is an amount in currency", "40 off; price {was, now} shows the cut on a sale"],
|
||||
["shipping", "no value", "free shipping at min_order"],
|
||||
["bogo", "value bought, gets given", "buy 2 get 1"],
|
||||
["gift", "gift names the item", "a gift with purchase"],
|
||||
["other", "the merchant's own words", "kept, listed, not filtered on"]
|
||||
];
|
||||
|
||||
const DIRECTORY: Array<[string, string]> = [
|
||||
["Hourly, at least", "Coupons start and end on the hour. A file read once is a snapshot."],
|
||||
["Dedupe on origin + id", "A re-read updates the row. An id that leaves the file is marked gone, not deleted."],
|
||||
["Expiry with the read time", "A code shown as active is active as of a stated moment."],
|
||||
["The merchant's url, unchanged", "A directory that routes through its own tracking says so beside the link."],
|
||||
["Verified above claimed", "A code from the merchant's origin outranks the same code from a forum, and the page shows which is which."]
|
||||
];
|
||||
|
||||
const ABSENT: Array<[string, string]> = [
|
||||
["No affiliate links", "url is the merchant's. A commission link is the directory's own, marked as such, beside it."],
|
||||
["No redemption", "The file says a code exists and what it does. Whether checkout accepts it for this cart is checkout's business."],
|
||||
["No votes, no badges", "The origin is the verification. A score a directory adds is labelled as its own."],
|
||||
["No catalog prices", "A coupon names what it applies to; the store says what that costs. price on a sale is the one exception."]
|
||||
];
|
||||
|
||||
export default function OpenCouponPage(): ReactNode {
|
||||
return (
|
||||
<SiteShell active="OpenCoupon">
|
||||
<div className="band">
|
||||
<div className="section-head">
|
||||
<p className="eyebrow">LogicSRC standards surface</p>
|
||||
<h2>OpenCoupon</h2>
|
||||
<p>
|
||||
One file a merchant serves about what is on offer right now. A coupon site reads the
|
||||
merchant instead of a forum thread, and a dead code dies everywhere at once.
|
||||
</p>
|
||||
</div>
|
||||
<p style={{ color: "#41505d" }}>
|
||||
Every coupon site is a graveyard. A code is posted once, copied everywhere, and lives on
|
||||
for years after the merchant retired it, because no coupon site knows when a code died
|
||||
and the merchant has no way to tell them. The merchant already knows exactly which codes
|
||||
work: its checkout is the source of truth and the promotion lives in a table with a start,
|
||||
an end and a rule. OpenCoupon is that table, exported, at{" "}
|
||||
<code style={mono}>/.well-known/opencoupon.json</code>.
|
||||
</p>
|
||||
<p style={{ color: "#5b6b7a" }}>
|
||||
Status: 0.1. The first directory reading it is the deals collection at{" "}
|
||||
<a href="https://nichedb.dev/c/deals">nichedb.dev</a>. The smallest valid file is a
|
||||
merchant with a name and a coupon with a title.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="band">
|
||||
<div className="section-head">
|
||||
<h2>The descriptor</h2>
|
||||
<p>
|
||||
A code fetched from the merchant's own origin is a code the merchant says works.
|
||||
That is the whole verification, and the whole point.
|
||||
</p>
|
||||
</div>
|
||||
<pre style={pre}>{DESCRIPTOR}</pre>
|
||||
<p style={{ color: "#41505d" }}>
|
||||
<code style={mono}>code</code> absent means nothing to type: the promotion applies on its
|
||||
own at <code style={mono}>url</code>. <code style={mono}>scope</code> narrows to
|
||||
categories or products, or everything but <code style={mono}>excludes</code>.{" "}
|
||||
<code style={mono}>status</code> is derived from the dates unless stated, and an expired
|
||||
coupon stays in the file for as long as copies of it circulate, so a directory learns it
|
||||
died from the one party that knows. Unknown keys are kept.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="band">
|
||||
<div className="section-head">
|
||||
<h2>Six kinds</h2>
|
||||
</div>
|
||||
<table style={table}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style={th}>kind</th>
|
||||
<th style={th}>value</th>
|
||||
<th style={th}>example</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{KINDS.map(([kind, value, example]) => (
|
||||
<tr key={kind}>
|
||||
<td style={td}>
|
||||
<code style={mono}>{kind}</code>
|
||||
</td>
|
||||
<td style={td}>{value}</td>
|
||||
<td style={td}>{example}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div className="band">
|
||||
<div className="section-head">
|
||||
<h2>What a directory owes a merchant</h2>
|
||||
</div>
|
||||
<table style={table}>
|
||||
<tbody>
|
||||
{DIRECTORY.map(([what, how]) => (
|
||||
<tr key={what}>
|
||||
<td style={td}>
|
||||
<strong>{what}</strong>
|
||||
</td>
|
||||
<td style={td}>{how}</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/opencoupon">Specification</Link>: the descriptor, ten rules, six
|
||||
kinds, expired coupons, discovery, what a directory owes a merchant
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://nichedb.dev/c/deals">nichedb.dev/c/deals</a>: the first directory
|
||||
reading it, beside the deal communities it reads today
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/openserver">OpenServer</Link>, the same idea for a hosting provider's
|
||||
catalog; <Link href="/openprofile">OpenProfile.md</Link>, the operator behind a merchant
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</SiteShell>
|
||||
);
|
||||
}
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
|
@ -32,6 +32,8 @@ const STATIC_ROUTES: Array<{
|
|||
{ path: "/openbandwidth", changeFrequency: "weekly", priority: 0.9 },
|
||||
{ path: "/openfile", changeFrequency: "weekly", priority: 0.9 },
|
||||
{ path: "/opendisk", changeFrequency: "weekly", priority: 0.9 },
|
||||
{ path: "/opencoupon", changeFrequency: "weekly", priority: 0.9 },
|
||||
{ path: "/openrecipe", changeFrequency: "weekly", priority: 0.9 },
|
||||
{ path: "/openontology/explore", changeFrequency: "daily", priority: 0.7 },
|
||||
{ path: "/openspec", changeFrequency: "weekly", priority: 0.8 },
|
||||
{ path: "/agent-swarm", changeFrequency: "weekly", priority: 0.8 },
|
||||
|
|
|
|||
|
|
@ -24,6 +24,8 @@ const NAV: Array<{ href: string; label: string; external?: boolean }> = [
|
|||
{ href: "/openbandwidth", label: "OpenBandwidth" },
|
||||
{ href: "/openfile", label: "OpenFile" },
|
||||
{ href: "/opendisk", label: "OpenDisk" },
|
||||
{ href: "/opencoupon", label: "OpenCoupon" },
|
||||
{ href: "/openrecipe", label: "OpenRecipe.md" },
|
||||
{ href: "/#cli", label: "CLI" },
|
||||
{ href: "/docs", label: "Docs" },
|
||||
{ href: "/blog", label: "Blog" },
|
||||
|
|
|
|||
|
|
@ -23,6 +23,8 @@ export const DOC_SLUGS = [
|
|||
"openserver",
|
||||
"openfile",
|
||||
"opendisk",
|
||||
"opencoupon",
|
||||
"openrecipe",
|
||||
"openstream",
|
||||
"opencpu",
|
||||
"openmemory",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue