diff --git a/apps/logicsrc-web/src/app/opensite/page.tsx b/apps/logicsrc-web/src/app/opensite/page.tsx
new file mode 100644
index 0000000..5c0b4b7
--- /dev/null
+++ b/apps/logicsrc-web/src/app/opensite/page.tsx
@@ -0,0 +1,240 @@
+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: "OpenSite · LogicSRC",
+ description:
+ "OpenSite is one record about a page or a site: the card a careful reader would draw from one URL, declared by the site at /.well-known/opensite.json or read from the page by written-down rules, kept by an index and handed to anyone. Title, description, picture, kind, author, feeds, and the card tags verbatim.",
+ alternates: { canonical: "/opensite" }
+};
+
+const RECORD = `{
+ "opensite": "0.1",
+ "url": "https://nixamp.com/?url=https%3A%2F%2Fserver1.chovy.nixamp.com%3A4321%2Fview%2FJV5m&play=channel%3Aurl-6f4152c1590e",
+ "canonical": "https://nixamp.com/?url=https%3A%2F%2Fserver1.chovy.nixamp.com%3A4321%2Fview%2FJV5m&play=channel%3Aurl-6f4152c1590e",
+ "site": { "name": "nixamp", "web": "https://nixamp.com" },
+ "kind": "stream",
+ "title": "Inspiring Founders Podcast",
+ "description": "Inspiring Founders Podcast is live on server1. Tune in free on nixamp, no account needed.",
+ "image": { "url": "https://d3t3ozftmdmh3i.cloudfront.net/.../44567180.jpg", "width": 3000, "height": 3000 },
+ "language": "en",
+ "fetched_at": "2026-09-13T04:52:10Z",
+ "status": "live",
+ "source": "read",
+ "cards": {
+ "og": { "title": "Inspiring Founders Podcast", "type": "website", "image": "https://d3t3ozftmdmh3i.cloudfront.net/.../44567180.jpg" },
+ "twitter": { "card": "summary" }
+ }
+}`;
+
+const DESCRIPTOR = `{
+ "opensite": "0.1",
+ "site": { "name": "nixamp", "web": "https://nixamp.com",
+ "description": "Broadcast live radio, TV and film from your own machine.",
+ "image": "https://nixamp.com/hero.png", "language": "en",
+ "operator": "https://nixamp.com/~chovy/OpenProfile.md",
+ "kinds": ["stream", "page"], "sitemaps": ["https://nixamp.com/sitemap.xml"] },
+ "index": { "allow": true, "refresh": 3600,
+ "records": "https://nixamp.com/opensite/records.jsonl" }
+}`;
+
+const READING: Array<[string, string]> = [
+ ["title", "JSON-LD headline or name, else og:title, else twitter:title, else
"],
+ ["description", "JSON-LD description, else og:description, else twitter:description, else "],
+ ["image", "og:image with its width, height and alt, else twitter:image, else JSON-LD image, else the largest icon; http(s) only"],
+ ["kind", "JSON-LD @type, else og:type, else page"],
+ ["canonical", "rel=canonical, else og:url, else the final URL after redirects"],
+ ["author", "JSON-LD author.name or article:author, else ; a rel=me link to an OpenProfile.md is the profile"],
+ ["cards", "every og:, twitter: and other prefixed tag, kept verbatim so a person can see what a scraper saw"]
+];
+
+const CONSUMERS: Array<[string, string, string]> = [
+ ["X", "twitter:* then og:*", "caches per URL about a week; no way to flush it"],
+ ["Slack", "og:*, then twitter:*, then ", "caches per URL about a month; a changed URL is a new read"],
+ ["iMessage", "og:*, then , then apple-touch-icon", "caches on the device"],
+ ["Discord", "og:*, twitter:*, theme-color", "caches for hours"],
+ ["LinkedIn", "og:*", "wants at least 1200 by 627 for a wide card; the Post Inspector re-reads on request"],
+ ["WhatsApp", "og:*", "reads only the first part of the page, so tags belong at the top of "],
+ ["Facebook", "og:*", "wants og:image:width and height or the first share has no picture"]
+];
+
+const ABSENT: Array<[string, string]> = [
+ ["No ranking", "A record says what a page is. Which page is better is an index's business, and it says so under its own name."],
+ ["No rendering rules", "The table of consumers is a reader's guide to what others do, not a rule for how a card must look."],
+ ["No content", "A record is the card, not the page. The page stays where it is; the record points at it."],
+ ["No push", "A site serves a descriptor and, if it likes, a records file. An index reads them on its own schedule."]
+];
+
+export default function OpenSitePage(): ReactNode {
+ return (
+
+
+
+
LogicSRC standards surface
+
OpenSite
+
+ One record about a page or a site: the card a careful reader would draw, declared by the
+ site or read from the page by rules that are written down, kept by an index and handed to
+ anyone.
+
+
+
+ Every link pasted into a chat becomes a card, drawn from tags the page carries for the
+ purpose. A dozen consumers read those tags a dozen ways, each caches its first reading for
+ days, and none of them publishes what it got. A site that fixes its card cannot tell them.
+ An index that read a page well cannot share the reading except as another scrape. OpenSite
+ is the reading, written down: a record per URL, a descriptor per site at{" "}
+ /.well-known/opensite.json, and the order in which a reader takes
+ each field from a page.
+
+
+ Status: 0.1. The first index is{" "}
+ nichedb.dev/c/sites, which reads any URL you paste
+ and shows what each consumer would draw; the first publisher is{" "}
+ nixamp, whose share links carry a card for every live
+ channel.
+
+
+
+
+
+
The record
+
+ One JSON object per URL, keyed by canonical. Required:{" "}
+ url, canonical,{" "}
+ site, kind,{" "}
+ title, fetched_at,{" "}
+ status and source.
+
+
+
{RECORD}
+
+ source is declared when the site
+ published the record itself and read when a reader derived it
+ from the page. cards keeps the page's own tags verbatim and
+ unmerged, because the consumers do not agree and a person checking a card wants to see what
+ each will do. status is live, gone, moved or blocked.
+
+
+
+
+
+
The descriptor
+
+ What a site says about itself as a whole, and whether it wants to be read at all.{" "}
+ index.records, when present, is a JSON Lines file of the
+ site's own records that an index may take instead of reading every page.
+
+
+
{DESCRIPTOR}
+
+
+
+
+
Reading a page
+
+ The part every scraper already does and nobody wrote down. First answer wins for each
+ key; robots.txt and index.allow are honoured; at most five
+ redirects, 2 MB and 15 seconds.
+
+
+
+
+
+
key
+
taken from, in order
+
+
+
+ {READING.map(([key, from]) => (
+
+
+ {key}
+
+
{from}
+
+ ))}
+
+
+
+
+
+
+
How consumers draw a card
+
+ Observed on 2026-09-13. Two rules follow: put the card tags in the first few kilobytes of
+ the page, and when a consumer has cached a wrong card, the only certain fix is a URL it
+ has not seen.
+
+ nixamp.com: the first publisher, a card for every live
+ channel on a share link
+
+
+ OpenProfile.md, who a page's author and a site's
+ operator are; OpenBroadcast, what a stream record
+ points at; OpenAccess, who may edit a declared record
+
+
+
+
+ );
+}
diff --git a/apps/logicsrc-web/src/lib/specs.ts b/apps/logicsrc-web/src/lib/specs.ts
index cdfc314..1bebc0f 100644
--- a/apps/logicsrc-web/src/lib/specs.ts
+++ b/apps/logicsrc-web/src/lib/specs.ts
@@ -90,7 +90,8 @@ export const FAMILIES: Family[] = [
s("opensaas", "OpenSaaS", "One file a subscription service serves about the way in and the way out of every plan, for a person and for an agent"),
s("openaffiliate", "OpenAffiliate", "One file a merchant serves about the commission it pays"),
s("openrecipe", "OpenRecipe.md", "One Markdown file that is a recipe, with schema.org derived from it and never the reverse"),
- s("openthreat", "OpenThreat", "One file a security tool serves about what it found in the open: public subjects only, secrets never located")
+ s("openthreat", "OpenThreat", "One file a security tool serves about what it found in the open: public subjects only, secrets never located"),
+ s("opensite", "OpenSite", "One record about a page or a site: the card a reader would draw, declared by the site or read from it, kept by an index")
]
},
{
diff --git a/docs/opensite.md b/docs/opensite.md
new file mode 100644
index 0000000..2ba9b20
--- /dev/null
+++ b/docs/opensite.md
@@ -0,0 +1,175 @@
+# OpenSite
+
+OpenSite is one record about a page or a site: what a careful reader concluded about one URL, or what the site says about itself, in a shape every index agrees on. A chat app, a social network and a search engine each read a page's metadata to draw a card, each a little differently, each caching what it read, none publishing what it got. OpenSite writes the reading down, so a site can say what it is and an index can hand a reading to anyone instead of scraping again. It is maintained by Profullstack, Inc. as part of the LogicSRC open-standards surface.
+
+Status: **0.1**. A description of the records [nichedb.dev](https://nichedb.dev/c/sites) keeps and the cards [nixamp](https://nixamp.com) serves on its share links, published so any site can declare its own records and any index can read them.
+
+Slug: `opensite`
+
+## The problem
+
+Every link pasted into a chat becomes a card, and the card is drawn from tags the page carries for that purpose: a title, a line, a picture. The tags are read by a dozen consumers with a dozen readings, and every consumer caches its first reading for days. A site that fixes its card cannot tell them. An index that scraped a page well cannot share what it found except as another scrape. And a site that knows exactly what a page is, because it made the page, has no way to say so beyond hoping the scraper guesses right. The result is a web full of readings that nobody publishes and everybody repeats.
+
+## Terms
+
+- A **page** is one URL that answers. A **site** is an origin and what it says about itself.
+- A **record** is what is known about one page: the OpenSite reading of it.
+- A **descriptor** is the file a site serves about itself at `/.well-known/opensite.json`.
+- A **reader** is anything that takes a record from a page by the reading rules below.
+- An **index** is a reader that keeps records and offers them back.
+- A **consumer** is anything that draws a card from a page: a chat app, a social network, a search engine.
+- A record is **declared** when the site published it, and **read** when a reader derived it.
+
+## Two documents
+
+### The record
+
+One JSON object per URL. This is the unit an index stores and a card is drawn from.
+
+```json
+{
+ "opensite": "0.1",
+ "url": "https://nixamp.com/?url=https%3A%2F%2Fserver1.chovy.nixamp.com%3A4321%2Fview%2FJV5m&play=channel%3Aurl-6f4152c1590e",
+ "canonical": "https://nixamp.com/?url=https%3A%2F%2Fserver1.chovy.nixamp.com%3A4321%2Fview%2FJV5m&play=channel%3Aurl-6f4152c1590e",
+ "site": { "name": "nixamp", "web": "https://nixamp.com" },
+ "kind": "stream",
+ "title": "Inspiring Founders Podcast",
+ "description": "Inspiring Founders Podcast is live on server1. Tune in free on nixamp, no account needed.",
+ "image": { "url": "https://d3t3ozftmdmh3i.cloudfront.net/.../44567180.jpg", "width": 3000, "height": 3000, "alt": "Inspiring Founders Podcast" },
+ "language": "en",
+ "author": { "name": "chovy", "profile": "https://nixamp.com/~chovy/OpenProfile.md" },
+ "tags": ["podcast", "live"],
+ "feeds": [],
+ "published_at": null,
+ "modified_at": null,
+ "fetched_at": "2026-09-13T04:52:10Z",
+ "status": "live",
+ "source": "read",
+ "cards": {
+ "og": { "title": "Inspiring Founders Podcast", "type": "website", "image": "https://d3t3ozftmdmh3i.cloudfront.net/.../44567180.jpg" },
+ "twitter": { "card": "summary" }
+ },
+ "jsonld": []
+}
+```
+
+| key | required | meaning |
+| --- | --- | --- |
+| `opensite` | yes | The spec version this record follows. |
+| `url` | yes | The address that was asked about, exactly as given. |
+| `canonical` | yes | The address the page names as its own (`rel=canonical`, else `og:url`, else `url` after redirects). Records are keyed by this. |
+| `site` | yes | `name` and `web` (the origin). From the site's descriptor when it has one, else `og:site_name` and the origin. |
+| `kind` | yes | One of `site`, `page`, `article`, `profile`, `product`, `event`, `video`, `audio`, `podcast`, `episode`, `stream`, `feed`, `other`. |
+| `title` | yes | What to call it. May be empty when nothing on the page says. |
+| `description` | no | One or two lines about it. |
+| `image` | no | A picture a card can show: `url` (http or https only), and `width`, `height`, `alt` when known. |
+| `language` | no | BCP 47, from `` or `og:locale`. |
+| `author` | no | `name` and, when the page names one, an [OpenProfile](/docs/openprofile) URL under `profile`. |
+| `tags` | no | Short words, lower case. |
+| `feeds` | no | RSS, Atom or JSON Feed addresses the page links to. |
+| `published_at`, `modified_at` | no | ISO 8601, from JSON-LD, `article:published_time` or the like. `null` when unknown. |
+| `fetched_at` | yes | When this reading was taken. |
+| `status` | yes | `live`, `gone` (404 or 410), `moved` (a redirect to another origin or path; `canonical` says where) or `blocked` (the site refused the read). |
+| `source` | yes | `declared` when the site published this record itself; `read` when an indexer derived it from the page. |
+| `cards` | no | What the page's own card tags said, verbatim and unmerged: `og`, `twitter`, and any other prefix, each a flat object. Kept so a consumer can see what a scraper saw. |
+| `jsonld` | no | The page's JSON-LD blocks, as parsed, unmodified. |
+
+Anything a reader does not understand is ignored. A record is never bigger than 256 KB; `jsonld` is the first thing cut to fit.
+
+### The descriptor
+
+What a site says about itself as a whole, at `/.well-known/opensite.json`.
+
+```json
+{
+ "opensite": "0.1",
+ "site": {
+ "name": "nixamp",
+ "web": "https://nixamp.com",
+ "description": "Broadcast live radio, TV and film from your own machine.",
+ "image": "https://nixamp.com/hero.png",
+ "icon": "https://nixamp.com/icons/icon-512.png",
+ "language": "en",
+ "operator": "https://nixamp.com/~chovy/OpenProfile.md",
+ "kinds": ["stream", "page"],
+ "feeds": ["https://nixamp.com/feed.xml"],
+ "sitemaps": ["https://nixamp.com/sitemap.xml"]
+ },
+ "index": {
+ "allow": true,
+ "refresh": 3600,
+ "records": "https://nixamp.com/opensite/records.jsonl",
+ "contact": "mailto:hi@nixamp.com"
+ }
+}
+```
+
+`site` is what a card falls back to when a page says nothing: its name, a line, a picture. `operator` is who runs it, as an OpenProfile. `kinds` says what kinds of record the site publishes, so an index knows what to expect.
+
+`index.allow` is whether the site wants to be read at all; `false` is honoured like a robots rule. `refresh` is how often, in seconds, a record is worth reading again. `records`, when present, is a JSON Lines file of the site's own records, one per line, which an index may take instead of reading every page. `contact` is where to write about the index.
+
+A single page may also point at its own record with `` or `Link: <…>; rel="opensite"`. A declared record wins over a read one when both exist and the declared one is fresher.
+
+## Reading a page
+
+This is the part every scraper already does and nobody wrote down. An OpenSite reader takes a record from a page like this, in this order, first answer wins for each key.
+
+1. **Fetch.** `GET` the URL with `Accept: text/html`, a user agent that names the reader and a URL where its policy is published (`OpenSite/0.1 (+https://…)`), following at most five redirects, reading at most 2 MB, within 15 seconds. Send `If-None-Match` and `If-Modified-Since` when a previous reading has them. A response that is not HTML is a record of kind `video`, `audio`, `feed` or `other` by its content type, with no card tags.
+2. **Refuse politely.** `robots.txt` applies. A descriptor with `index.allow: false` applies. Either makes a record with `status: blocked` and nothing else.
+3. **Canonical.** ``, else `og:url`, else the final URL after redirects. A canonical on another origin is `status: moved`.
+4. **Title.** JSON-LD `headline` or `name`, else `og:title`, else `twitter:title`, else ``.
+5. **Description.** JSON-LD `description`, else `og:description`, else `twitter:description`, else ``.
+6. **Image.** `og:image` (with `og:image:width`, `og:image:height`, `og:image:alt` when given), else `twitter:image`, else JSON-LD `image`, else the largest `rel="icon"` or `apple-touch-icon`. Only http or https. A relative address is resolved against the page. A reader that fetches the image records its real size.
+7. **Kind.** From JSON-LD `@type` (Article, NewsArticle, BlogPosting are `article`; Person is `profile`; Product; Event; VideoObject `video`; AudioObject `audio`; PodcastSeries `podcast`; PodcastEpisode `episode`; BroadcastEvent `stream`; WebSite `site`), else `og:type` (`article`, `profile`, `product`, `video.*` to `video`, `music.*` to `audio`, `website` to `page`), else `page`.
+8. **Author.** JSON-LD `author.name` or `article:author`, else ``. A `rel="me"` or `rel="author"` link to an `OpenProfile.md` is the `profile`.
+9. **Dates.** JSON-LD `datePublished` / `dateModified`, else `article:published_time` / `article:modified_time`.
+10. **Feeds.** Every `` whose type is RSS, Atom or JSON Feed.
+11. **Language.** ``, else `og:locale`.
+12. **Cards.** Every ``, `` and any other `prefix:name` pair, kept verbatim under `cards.`.
+13. **JSON-LD.** Every `