OOKNET                             [ /  search the index  ]  
──────────────────────────────────────────────────────────────────────────────────────
══════════════════════════════════════════════════════════════════════════════════════
OOKNET   [ /  search  ]  
────────────────────────────────────────────────
════════════════════════════════════════════════
 
HASH      012d217add3d
DATE      2026-07-17
SUBJECT   web: meta and og tags, rss feed, sitemap, robots.txt
FILES     18 CHANGED
HASH      012d217add3d
DATE      2026-07-17
SUBJECT   web: meta and og tags, rss feed,
          sitemap, robots.txt
FILES     18 CHANGED
 

diff --git a/ooknet-design/astro.config.ts b/ooknet-design/astro.config.ts
index 1d569a4..b651c66 100644
--- a/ooknet-design/astro.config.ts
+++ b/ooknet-design/astro.config.ts
@@ -3,6 +3,7 @@ import { rehypeAscii } from "./src/lib/rehype-ascii";
 import { remarkWikilinks } from "./src/lib/remark-wikilinks";
 
 export default defineConfig({
+  site: "https://ooknet.org",
   markdown: {
     syntaxHighlight: false,
     remarkPlugins: [remarkWikilinks],

diff --git a/ooknet-design/astro.config.ts b
/ooknet-design/astro.config.ts
index 1d569a4..b651c66 100644
--- a/ooknet-design/astro.config.ts
+++ b/ooknet-design/astro.config.ts
@@ -3,6 +3,7 @@ import { rehypeAscii } from 
"./src/lib/rehype-ascii";
 import { remarkWikilinks } from "./src/lib/
remark-wikilinks";
 
 export default defineConfig({
+  site: "https://ooknet.org",
   markdown: {
     syntaxHighlight: false,
     remarkPlugins: [remarkWikilinks],
 

diff --git a/ooknet-design/public/robots.txt b/ooknet-design/public/robots.txt
new file mode 100644
index 0000000..e2a357d
--- /dev/null
+++ b/ooknet-design/public/robots.txt
@@ -0,0 +1,4 @@
+User-agent: *
+Allow: /
+
+Sitemap: https://ooknet.org/sitemap.xml

diff --git a/ooknet-design/public/robots.txt
 b/ooknet-design/public/robots.txt
new file mode 100644
index 0000000..e2a357d
--- /dev/null
+++ b/ooknet-design/public/robots.txt
@@ -0,0 +1,4 @@
+User-agent: *
+Allow: /
+
+Sitemap: https://ooknet.org/sitemap.xml
 

diff --git a/ooknet-design/src/layouts/ArticleLayout/ArticleLayout.astro b/ooknet-
design/src/layouts/ArticleLayout/ArticleLayout.astro
index 2eefd2b..bc0c0c7 100644
--- a/ooknet-design/src/layouts/ArticleLayout/ArticleLayout.astro
+++ b/ooknet-design/src/layouts/ArticleLayout/ArticleLayout.astro
@@ -25,7 +25,12 @@ import plex600 from "@fontsource/ibm-plex-mono/files/ibm-plex-m
ono-latin-600-nor
 // symbols2 carries the box-drawing glyphs - every frame needs it at
 // first paint just as much as latin.
 const preloadFonts = [cascadia400, cascadia700, cascadiaSym, plex400, plex600];
-const { title = "OOKNET KB" } = Astro.props;
+const {
+  title = "OOKNET KB",
+  description = "OOKNET knowledge base - a text-mode wiki of filed notes, docs, a
nd references, rendered on a strict ASCII grid.",
+  type = "website",
+} = Astro.props;
+const canonical = new URL(Astro.url.pathname, Astro.site);
 ---
 <!doctype html>
 <html lang="en">
@@ -37,6 +42,15 @@ const { title = "OOKNET KB" } = Astro.props;
       <link rel="preload" as="font" type="font/woff2" crossorigin href={href} />
     ))}
     <title>{title}</title>
+    <meta name="description" content={description} />
+    <link rel="canonical" href={canonical} />
+    <meta property="og:site_name" content="OOKNET KB" />
+    <meta property="og:type" content={type} />
+    <meta property="og:title" content={title} />
+    <meta property="og:description" content={description} />
+    <meta property="og:url" content={canonical} />
+    <meta name="twitter:card" content="summary" />
+    <link rel="alternate" type="application/rss+xml" title="OOKNET KB" href="/rss
.xml" />
     <script is:inline>
       (() => {
         try {

diff --git a/ooknet-design/src/layouts/Artic
leLayout/ArticleLayout.astro b/ooknet-design
/src/layouts/ArticleLayout/ArticleLayout.ast
ro
index 2eefd2b..bc0c0c7 100644
--- a/ooknet-design/src/layouts/ArticleLayou
t/ArticleLayout.astro
+++ b/ooknet-design/src/layouts/ArticleLayou
t/ArticleLayout.astro
@@ -25,7 +25,12 @@ import plex600 from "@fon
tsource/ibm-plex-mono/files/ibm-plex-mono-la
tin-600-nor
 // symbols2 carries the box-drawing glyphs 
- every frame needs it at
 // first paint just as much as latin.
 const preloadFonts = [cascadia400, cascadia
700, cascadiaSym, plex400, plex600];
-const { title = "OOKNET KB" } = Astro.props
;
+const {
+  title = "OOKNET KB",
+  description = "OOKNET knowledge base - a 
text-mode wiki of filed notes, docs, and ref
erences, rendered on a strict ASCII grid.",
+  type = "website",
+} = Astro.props;
+const canonical = new URL(Astro.url.pathnam
e, Astro.site);
 ---
 <!doctype html>
 <html lang="en">
@@ -37,6 +42,15 @@ const { title = "OOKNET K
B" } = Astro.props;
       <link rel="preload" as="font" type="f
ont/woff2" crossorigin href={href} />
     ))}
     <title>{title}</title>
+    <meta name="description" content={descr
iption} />
+    <link rel="canonical" href={canonical} 
/>
+    <meta property="og:site_name" content="
OOKNET KB" />
+    <meta property="og:type" content={type}
 />
+    <meta property="og:title" content={titl
e} />
+    <meta property="og:description" content
={description} />
+    <meta property="og:url" content={canoni
cal} />
+    <meta name="twitter:card" content="summ
ary" />
+    <link rel="alternate" type="application
/rss+xml" title="OOKNET KB" href="/rss.xml" 
/>
     <script is:inline>
       (() => {
         try {
 

diff --git a/ooknet-design/src/lib/feeds.test.ts b/ooknet-design/src/lib/feeds.tes
t.ts
new file mode 100644
index 0000000..da68f58
--- /dev/null
+++ b/ooknet-design/src/lib/feeds.test.ts
@@ -0,0 +1,45 @@
+import { describe, expect, it } from "vitest";
+import { buildRss, buildSitemap } from "./feeds";
+
+const item = {
+  title: "ENTROPY <BOUNDS> & GRAPHS",
+  url: "https://ooknet.org/notes/n217-entropy-bounds/",
+  published: "2026-04-01",
+  description: 'Bounds on "sparse" graphs.',
+};
+
+describe("buildRss", () => {
+  const xml = buildRss({
+    title: "OOKNET KB",
+    site: "https://ooknet.org/",
+    description: "notes feed",
+    items: [item],
+  });
+
+  it("escapes xml in titles and descriptions", () => {
+    expect(xml).toContain("ENTROPY &lt;BOUNDS&gt; &amp; GRAPHS");
+    expect(xml).toContain("&quot;sparse&quot;");
+    expect(xml).not.toContain("<BOUNDS>");
+  });
+
+  it("formats pubDate as RFC 822", () => {
+    expect(xml).toContain("<pubDate>Wed, 01 Apr 2026");
+  });
+
+  it("links item and self", () => {
+    expect(xml).toContain(`<link>${item.url}</link>`);
+    expect(xml).toContain('href="https://ooknet.org/rss.xml" rel="self"');
+  });
+});
+
+describe("buildSitemap", () => {
+  it("emits one url entry per page, lastmod only when known", () => {
+    const xml = buildSitemap([
+      { loc: "https://ooknet.org/" },
+      { loc: "https://ooknet.org/notes/n217-entropy-bounds/", lastmod: "2026-04-0
1" },
+    ]);
+    expect(xml.match(/<url>/g)).toHaveLength(2);
+    expect(xml.match(/<lastmod>/g)).toHaveLength(1);
+    expect(xml).toContain("<lastmod>2026-04-01</lastmod>");
+  });
+});

diff --git a/ooknet-design/src/lib/feeds.tes
t.ts b/ooknet-design/src/lib/feeds.test.ts
new file mode 100644
index 0000000..da68f58
--- /dev/null
+++ b/ooknet-design/src/lib/feeds.test.ts
@@ -0,0 +1,45 @@
+import { describe, expect, it } from "vites
t";
+import { buildRss, buildSitemap } from "./f
eeds";
+
+const item = {
+  title: "ENTROPY <BOUNDS> & GRAPHS",
+  url: "https://ooknet.org/notes/n217-entro
py-bounds/",
+  published: "2026-04-01",
+  description: 'Bounds on "sparse" graphs.'
,
+};
+
+describe("buildRss", () => {
+  const xml = buildRss({
+    title: "OOKNET KB",
+    site: "https://ooknet.org/",
+    description: "notes feed",
+    items: [item],
+  });
+
+  it("escapes xml in titles and description
s", () => {
+    expect(xml).toContain("ENTROPY &lt;BOUN
DS&gt; &amp; GRAPHS");
+    expect(xml).toContain("&quot;sparse&quo
t;");
+    expect(xml).not.toContain("<BOUNDS>");
+  });
+
+  it("formats pubDate as RFC 822", () => {
+    expect(xml).toContain("<pubDate>Wed, 01
 Apr 2026");
+  });
+
+  it("links item and self", () => {
+    expect(xml).toContain(`<link>${item.url
}</link>`);
+    expect(xml).toContain('href="https://oo
knet.org/rss.xml" rel="self"');
+  });
+});
+
+describe("buildSitemap", () => {
+  it("emits one url entry per page, lastmod
 only when known", () => {
+    const xml = buildSitemap([
+      { loc: "https://ooknet.org/" },
+      { loc: "https://ooknet.org/notes/n217
-entropy-bounds/", lastmod: "2026-04-01" },
+    ]);
+    expect(xml.match(/<url>/g)).toHaveLengt
h(2);
+    expect(xml.match(/<lastmod>/g)).toHaveL
ength(1);
+    expect(xml).toContain("<lastmod>2026-04
-01</lastmod>");
+  });
+});
 

diff --git a/ooknet-design/src/lib/feeds.ts b/ooknet-design/src/lib/feeds.ts
new file mode 100644
index 0000000..aae243f
--- /dev/null
+++ b/ooknet-design/src/lib/feeds.ts
@@ -0,0 +1,78 @@
+// Hand-rolled RSS and sitemap XML - no integration dependency, and the
+// builders stay pure and testable.
+
+export interface FeedItem {
+  title: string;
+  url: string;       // absolute
+  published: string; // ISO date
+  description: string;
+}
+
+export interface SitemapUrl {
+  loc: string;       // absolute
+  lastmod?: string;  // ISO date
+}
+
+const esc = (s: string) =>
+  s
+    .replace(/&/g, "&amp;")
+    .replace(/</g, "&lt;")
+    .replace(/>/g, "&gt;")
+    .replace(/"/g, "&quot;");
+
+export interface RssOpts {
+  title: string;
+  site: string;      // absolute site url
+  description: string;
+  items: FeedItem[];
+}
+
+export function buildRss({ title, site, description, items }: RssOpts): string {
+  const channel = items
+    .map((it) =>
+      [
+        "    <item>",
+        `      <title>${esc(it.title)}</title>`,
+        `      <link>${esc(it.url)}</link>`,
+        `      <guid>${esc(it.url)}</guid>`,
+        `      <pubDate>${new Date(it.published).toUTCString()}</pubDate>`,
+        `      <description>${esc(it.description)}</description>`,
+        "    </item>",
+      ].join("\n"),
+    )
+    .join("\n");
+  return [
+    `<?xml version="1.0" encoding="UTF-8"?>`,
+    `<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">`,
+    "  <channel>",
+    `    <title>${esc(title)}</title>`,
+    `    <link>${esc(site)}</link>`,
+    `    <atom:link href="${esc(site)}rss.xml" rel="self" type="application/rss+x
ml"/>`,
+    `    <description>${esc(description)}</description>`,
+    "    <language>en</language>",
+    channel,
+    "  </channel>",
+    "</rss>",
+    "",
+  ].join("\n");
+}
+
+export function buildSitemap(urls: SitemapUrl[]): string {
+  const body = urls
+    .map((u) =>
+      [
+        "  <url>",
+        `    <loc>${esc(u.loc)}</loc>`,
+        ...(u.lastmod ? [`    <lastmod>${esc(u.lastmod)}</lastmod>`] : []),
+        "  </url>",
+      ].join("\n"),
+    )
+    .join("\n");
+  return [
+    `<?xml version="1.0" encoding="UTF-8"?>`,
+    `<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">`,
+    body,
+    "</urlset>",
+    "",
+  ].join("\n");
+}

diff --git a/ooknet-design/src/lib/feeds.ts 
b/ooknet-design/src/lib/feeds.ts
new file mode 100644
index 0000000..aae243f
--- /dev/null
+++ b/ooknet-design/src/lib/feeds.ts
@@ -0,0 +1,78 @@
+// Hand-rolled RSS and sitemap XML - no int
egration dependency, and the
+// builders stay pure and testable.
+
+export interface FeedItem {
+  title: string;
+  url: string;       // absolute
+  published: string; // ISO date
+  description: string;
+}
+
+export interface SitemapUrl {
+  loc: string;       // absolute
+  lastmod?: string;  // ISO date
+}
+
+const esc = (s: string) =>
+  s
+    .replace(/&/g, "&amp;")
+    .replace(/</g, "&lt;")
+    .replace(/>/g, "&gt;")
+    .replace(/"/g, "&quot;");
+
+export interface RssOpts {
+  title: string;
+  site: string;      // absolute site url
+  description: string;
+  items: FeedItem[];
+}
+
+export function buildRss({ title, site, des
cription, items }: RssOpts): string {
+  const channel = items
+    .map((it) =>
+      [
+        "    <item>",
+        `      <title>${esc(it.title)}</tit
le>`,
+        `      <link>${esc(it.url)}</link>`
,
+        `      <guid>${esc(it.url)}</guid>`
,
+        `      <pubDate>${new Date(it.publi
shed).toUTCString()}</pubDate>`,
+        `      <description>${esc(it.descri
ption)}</description>`,
+        "    </item>",
+      ].join("\n"),
+    )
+    .join("\n");
+  return [
+    `<?xml version="1.0" encoding="UTF-8"?>
`,
+    `<rss version="2.0" xmlns:atom="http://
www.w3.org/2005/Atom">`,
+    "  <channel>",
+    `    <title>${esc(title)}</title>`,
+    `    <link>${esc(site)}</link>`,
+    `    <atom:link href="${esc(site)}rss.x
ml" rel="self" type="application/rss+xml"/>`
,
+    `    <description>${esc(description)}</
description>`,
+    "    <language>en</language>",
+    channel,
+    "  </channel>",
+    "</rss>",
+    "",
+  ].join("\n");
+}
+
+export function buildSitemap(urls: SitemapU
rl[]): string {
+  const body = urls
+    .map((u) =>
+      [
+        "  <url>",
+        `    <loc>${esc(u.loc)}</loc>`,
+        ...(u.lastmod ? [`    <lastmod>${es
c(u.lastmod)}</lastmod>`] : []),
+        "  </url>",
+      ].join("\n"),
+    )
+    .join("\n");
+  return [
+    `<?xml version="1.0" encoding="UTF-8"?>
`,
+    `<urlset xmlns="http://www.sitemaps.org
/schemas/sitemap/0.9">`,
+    body,
+    "</urlset>",
+    "",
+  ].join("\n");
+}
 

diff --git a/ooknet-design/src/lib/meta.test.ts b/ooknet-design/src/lib/meta.test.
ts
new file mode 100644
index 0000000..65e1609
--- /dev/null
+++ b/ooknet-design/src/lib/meta.test.ts
@@ -0,0 +1,35 @@
+import { describe, expect, it } from "vitest";
+import { describeBody } from "./meta";
+
+describe("describeBody", () => {
+  it("takes the first prose paragraph, skipping headings and fences", () => {
+    const md = "# Title\n\n```\ncode\n```\n\nThe actual lede sentence.\n\nSecond 
para.";
+    expect(describeBody(md)).toBe("The actual lede sentence.");
+  });
+
+  it("skips tables, quotes, and lists", () => {
+    const md = "| A | B |\n| - | - |\n\n> quote\n\n- item\n\nProse at last.";
+    expect(describeBody(md)).toBe("Prose at last.");
+  });
+
+  it("strips wikilinks, links, and emphasis", () => {
+    const md = "See [[N217|the bound]] and [docs](/docs/) for *context* on `check
Grid`.";
+    expect(describeBody(md)).toBe("See the bound and docs for context on checkGri
d.");
+  });
+
+  it("collapses hard-wrapped lines into one sentence", () => {
+    expect(describeBody("wrapped\nacross\nlines")).toBe("wrapped across lines");
+  });
+
+  it("truncates at a word boundary with an ellipsis", () => {
+    const long = "word ".repeat(60).trim();
+    const out = describeBody(long, 40);
+    expect([...out].length).toBeLessThanOrEqual(40);
+    expect(out.endsWith("...")).toBe(true);
+    expect(out).not.toContain("wor...");
+  });
+
+  it("returns empty for bodies with no prose", () => {
+    expect(describeBody("# Only\n\n## Headings")).toBe("");
+  });
+});

diff --git a/ooknet-design/src/lib/meta.test
.ts b/ooknet-design/src/lib/meta.test.ts
new file mode 100644
index 0000000..65e1609
--- /dev/null
+++ b/ooknet-design/src/lib/meta.test.ts
@@ -0,0 +1,35 @@
+import { describe, expect, it } from "vites
t";
+import { describeBody } from "./meta";
+
+describe("describeBody", () => {
+  it("takes the first prose paragraph, skip
ping headings and fences", () => {
+    const md = "# Title\n\n```\ncode\n```\n
\nThe actual lede sentence.\n\nSecond para."
;
+    expect(describeBody(md)).toBe("The actu
al lede sentence.");
+  });
+
+  it("skips tables, quotes, and lists", () 
=> {
+    const md = "| A | B |\n| - | - |\n\n> q
uote\n\n- item\n\nProse at last.";
+    expect(describeBody(md)).toBe("Prose at
 last.");
+  });
+
+  it("strips wikilinks, links, and emphasis
", () => {
+    const md = "See [[N217|the bound]] and 
[docs](/docs/) for *context* on `checkGrid`.
";
+    expect(describeBody(md)).toBe("See the 
bound and docs for context on checkGrid.");
+  });
+
+  it("collapses hard-wrapped lines into one
 sentence", () => {
+    expect(describeBody("wrapped\nacross\nl
ines")).toBe("wrapped across lines");
+  });
+
+  it("truncates at a word boundary with an 
ellipsis", () => {
+    const long = "word ".repeat(60).trim();
+    const out = describeBody(long, 40);
+    expect([...out].length).toBeLessThanOrE
qual(40);
+    expect(out.endsWith("...")).toBe(true);
+    expect(out).not.toContain("wor...");
+  });
+
+  it("returns empty for bodies with no pros
e", () => {
+    expect(describeBody("# Only\n\n## Headi
ngs")).toBe("");
+  });
+});
 

diff --git a/ooknet-design/src/lib/meta.ts b/ooknet-design/src/lib/meta.ts
new file mode 100644
index 0000000..1efaed9
--- /dev/null
+++ b/ooknet-design/src/lib/meta.ts
@@ -0,0 +1,28 @@
+// Page descriptions for <meta>, OG, and feeds - derived from the first
+// prose paragraph of a markdown body so notes don't need a description
+// field in frontmatter.
+
+/** First prose paragraph, markdown stripped, truncated at a word
+ *  boundary. Empty string when the body has no prose (tables, code,
+ *  and headings don't count). */
+export function describeBody(md: string, max = 160): string {
+  let text = "";
+  for (const raw of md.split(/\n\s*\n/)) {
+    const b = raw.trim();
+    if (!b) continue;
+    if (/^(#|```|~~~|\||>|[-*+] |\d+\. |!\[|<)/.test(b)) continue;
+    text = b;
+    break;
+  }
+  if (!text) return "";
+  const clean = text
+    .replace(/\[\[([^\]|]+)\|([^\]]+)\]\]/g, "$2")
+    .replace(/\[\[([^\]|]+)\]\]/g, "$1")
+    .replace(/\[([^\]]+)\]\([^)]*\)/g, "$1")
+    .replace(/[`*_]/g, "")
+    .replace(/\s+/g, " ")
+    .trim();
+  if ([...clean].length <= max) return clean;
+  const cut = [...clean].slice(0, max - 1).join("");
+  return cut.replace(/\s+\S*$/, "") + "...";
+}

diff --git a/ooknet-design/src/lib/meta.ts b
/ooknet-design/src/lib/meta.ts
new file mode 100644
index 0000000..1efaed9
--- /dev/null
+++ b/ooknet-design/src/lib/meta.ts
@@ -0,0 +1,28 @@
+// Page descriptions for <meta>, OG, and fe
eds - derived from the first
+// prose paragraph of a markdown body so no
tes don't need a description
+// field in frontmatter.
+
+/** First prose paragraph, markdown strippe
d, truncated at a word
+ *  boundary. Empty string when the body ha
s no prose (tables, code,
+ *  and headings don't count). */
+export function describeBody(md: string, ma
x = 160): string {
+  let text = "";
+  for (const raw of md.split(/\n\s*\n/)) {
+    const b = raw.trim();
+    if (!b) continue;
+    if (/^(#|```|~~~|\||>|[-*+] |\d+\. |!\[
|<)/.test(b)) continue;
+    text = b;
+    break;
+  }
+  if (!text) return "";
+  const clean = text
+    .replace(/\[\[([^\]|]+)\|([^\]]+)\]\]/g
, "$2")
+    .replace(/\[\[([^\]|]+)\]\]/g, "$1")
+    .replace(/\[([^\]]+)\]\([^)]*\)/g, "$1"
)
+    .replace(/[`*_]/g, "")
+    .replace(/\s+/g, " ")
+    .trim();
+  if ([...clean].length <= max) return clea
n;
+  const cut = [...clean].slice(0, max - 1).
join("");
+  return cut.replace(/\s+\S*$/, "") + "..."
;
+}
 

diff --git a/ooknet-design/src/pages/about.astro b/ooknet-design/src/pages/about.a
stro
index 95c4cda..e726ec8 100644
--- a/ooknet-design/src/pages/about.astro
+++ b/ooknet-design/src/pages/about.astro
@@ -23,7 +23,8 @@ const buildColophon = (w: number) =>
     ruleD(w),
   ].join("\n");
 ---
-<ArticleLayout title="OOKNET KB - About">
+<ArticleLayout title="OOKNET KB - About"
+  description="What OOKNET is: a personal knowledge base rendered as pure text on
 a monospace grid - every border, chart, and frame is a real character.">
   <TopHeader />
   <Frame build={buildHeader} />
   <Prose>

diff --git a/ooknet-design/src/pages/about.a
stro b/ooknet-design/src/pages/about.astro
index 95c4cda..e726ec8 100644
--- a/ooknet-design/src/pages/about.astro
+++ b/ooknet-design/src/pages/about.astro
@@ -23,7 +23,8 @@ const buildColophon = (w: 
number) =>
     ruleD(w),
   ].join("\n");
 ---
-<ArticleLayout title="OOKNET KB - About">
+<ArticleLayout title="OOKNET KB - About"
+  description="What OOKNET is: a personal k
nowledge base rendered as pure text on a mon
ospace grid - every border, chart, and frame
 is a real character.">
   <TopHeader />
   <Frame build={buildHeader} />
   <Prose>
 

diff --git a/ooknet-design/src/pages/components.astro b/ooknet-design/src/pages/co
mponents.astro
index 9f7d9f0..0f28dcb 100644
--- a/ooknet-design/src/pages/components.astro
+++ b/ooknet-design/src/pages/components.astro
@@ -95,7 +95,8 @@ const FILED_TREE: TreeNode[] = [
   },
 ];
 ---
-<ArticleLayout title="OOKNET KB - Components">
+<ArticleLayout title="OOKNET KB - Components"
+  description="The OOKNET ASCII component library - every frame, chart, and contr
ol the design system ships, demonstrated live.">
   <TopHeader />
   <Frame build={(w) => [row2("COMPONENT INDEX", "DESIGN SYSTEM v0.1", w), ruleD(w
)].join("\n")} />
   <Pre>{" "}</Pre>

diff --git a/ooknet-design/src/pages/compone
nts.astro b/ooknet-design/src/pages/componen
ts.astro
index 9f7d9f0..0f28dcb 100644
--- a/ooknet-design/src/pages/components.ast
ro
+++ b/ooknet-design/src/pages/components.ast
ro
@@ -95,7 +95,8 @@ const FILED_TREE: TreeNode
[] = [
   },
 ];
 ---
-<ArticleLayout title="OOKNET KB - Component
s">
+<ArticleLayout title="OOKNET KB - Component
s"
+  description="The OOKNET ASCII component l
ibrary - every frame, chart, and control the
 design system ships, demonstrated live.">
   <TopHeader />
   <Frame build={(w) => [row2("COMPONENT IND
EX", "DESIGN SYSTEM v0.1", w), ruleD(w)].joi
n("\n")} />
   <Pre>{" "}</Pre>
 

diff --git a/ooknet-design/src/pages/docs/[...slug].astro b/ooknet-design/src/page
s/docs/[...slug].astro
index 97c33ee..2210104 100644
--- a/ooknet-design/src/pages/docs/[...slug].astro
+++ b/ooknet-design/src/pages/docs/[...slug].astro
@@ -15,6 +15,7 @@ import { box, center, len, rule, ruleD, row2, wrap } from "../..
/lib/ascii";
 import { published } from "../../lib/notes";
 import { docsOrder, type DocEntry } from "../../lib/docs";
 import { buildBacklinks, type Backlink } from "../../lib/wikilinks";
+import { describeBody } from "../../lib/meta";
 
 export async function getStaticPaths() {
   const docs = await getCollection("docs", published);
@@ -54,6 +55,8 @@ interface Props {
 }
 const { doc, prev, next, all, refs } = Astro.props as Props;
 const { Content, headings } = await render(doc);
+const description =
+  doc.data.description ?? (describeBody(doc.body ?? "") || `OOKNET documentation 
- ${doc.data.title}.`);
 
 const buildTitleCard = (w: number) => {
   const cardW = Math.min(46, w - 8);
@@ -65,7 +68,7 @@ const buildTitleCard = (w: number) => {
   return card.split("\n").map((line) => center(line, w)).join("\n");
 };
 ---
-<ArticleLayout title={`DOCS - ${doc.data.title}`}>
+<ArticleLayout title={`DOCS - ${doc.data.title}`} description={description} type=
"article">
   <TopHeader />
   <Frame build={(w) => [row2("OOKNET DOCUMENTATION", doc.id.toUpperCase(), w), ru
le(w)].join("\n")} />
   <Pre>{" "}</Pre>

diff --git a/ooknet-design/src/pages/docs/[.
..slug].astro b/ooknet-design/src/pages/docs
/[...slug].astro
index 97c33ee..2210104 100644
--- a/ooknet-design/src/pages/docs/[...slug]
.astro
+++ b/ooknet-design/src/pages/docs/[...slug]
.astro
@@ -15,6 +15,7 @@ import { box, center, len,
 rule, ruleD, row2, wrap } from "../../lib/a
scii";
 import { published } from "../../lib/notes"
;
 import { docsOrder, type DocEntry } from ".
./../lib/docs";
 import { buildBacklinks, type Backlink } fr
om "../../lib/wikilinks";
+import { describeBody } from "../../lib/met
a";
 
 export async function getStaticPaths() {
   const docs = await getCollection("docs", 
published);
@@ -54,6 +55,8 @@ interface Props {
 }
 const { doc, prev, next, all, refs } = Astr
o.props as Props;
 const { Content, headings } = await render(
doc);
+const description =
+  doc.data.description ?? (describeBody(doc
.body ?? "") || `OOKNET documentation - ${do
c.data.title}.`);
 
 const buildTitleCard = (w: number) => {
   const cardW = Math.min(46, w - 8);
@@ -65,7 +68,7 @@ const buildTitleCard = (w:
 number) => {
   return card.split("\n").map((line) => cen
ter(line, w)).join("\n");
 };
 ---
-<ArticleLayout title={`DOCS - ${doc.data.ti
tle}`}>
+<ArticleLayout title={`DOCS - ${doc.data.ti
tle}`} description={description} type="artic
le">
   <TopHeader />
   <Frame build={(w) => [row2("OOKNET DOCUME
NTATION", doc.id.toUpperCase(), w), rule(w)]
.join("\n")} />
   <Pre>{" "}</Pre>
 

diff --git a/ooknet-design/src/pages/docs/index.astro b/ooknet-design/src/pages/do
cs/index.astro
index fae3fd1..e22ce1d 100644
--- a/ooknet-design/src/pages/docs/index.astro
+++ b/ooknet-design/src/pages/docs/index.astro
@@ -16,7 +16,8 @@ const docs = (await getCollection("docs", published)).map((d) =>
 ({
 }));
 const tree = docsTree(docs);
 ---
-<ArticleLayout title="OOKNET KB - Docs">
+<ArticleLayout title="OOKNET KB - Docs"
+  description="OOKNET documentation - how the ASCII framework, the markdown pipel
ine, and the filing system work.">
   <TopHeader />
   <Frame build={(w) => [row2("DOCUMENTATION", `${docs.length} pages`, w), ruleD(w
)].join("\n")} />
   <Pre>{" "}</Pre>

diff --git a/ooknet-design/src/pages/docs/in
dex.astro b/ooknet-design/src/pages/docs/ind
ex.astro
index fae3fd1..e22ce1d 100644
--- a/ooknet-design/src/pages/docs/index.ast
ro
+++ b/ooknet-design/src/pages/docs/index.ast
ro
@@ -16,7 +16,8 @@ const docs = (await getCol
lection("docs", published)).map((d) => ({
 }));
 const tree = docsTree(docs);
 ---
-<ArticleLayout title="OOKNET KB - Docs">
+<ArticleLayout title="OOKNET KB - Docs"
+  description="OOKNET documentation - how t
he ASCII framework, the markdown pipeline, a
nd the filing system work.">
   <TopHeader />
   <Frame build={(w) => [row2("DOCUMENTATION
", `${docs.length} pages`, w), ruleD(w)].joi
n("\n")} />
   <Pre>{" "}</Pre>
 

diff --git a/ooknet-design/src/pages/index.astro b/ooknet-design/src/pages/index.a
stro
index be49b27..016fafc 100644
--- a/ooknet-design/src/pages/index.astro
+++ b/ooknet-design/src/pages/index.astro
@@ -20,7 +20,8 @@ const buildIndexBody = (w: number) =>
 
 const buildIndexRule = (w: number) => rule(w);
 ---
-<ArticleLayout title="OOKNET KB - Index">
+<ArticleLayout title="OOKNET KB - Index"
+  description="OOKNET knowledge base - the index of filed notes, browsable by tax
onomy and searchable by tag, title, or note number.">
   <TopHeader />
   <Frame build={buildIndexHeader} />
   <Frame build={buildIndexBody} />

diff --git a/ooknet-design/src/pages/index.a
stro b/ooknet-design/src/pages/index.astro
index be49b27..016fafc 100644
--- a/ooknet-design/src/pages/index.astro
+++ b/ooknet-design/src/pages/index.astro
@@ -20,7 +20,8 @@ const buildIndexBody = (w:
 number) =>
 
 const buildIndexRule = (w: number) => rule(
w);
 ---
-<ArticleLayout title="OOKNET KB - Index">
+<ArticleLayout title="OOKNET KB - Index"
+  description="OOKNET knowledge base - the 
index of filed notes, browsable by taxonomy 
and searchable by tag, title, or note number
.">
   <TopHeader />
   <Frame build={buildIndexHeader} />
   <Frame build={buildIndexBody} />
 

diff --git a/ooknet-design/src/pages/notes/[slug].astro b/ooknet-design/src/pages/
notes/[slug].astro
index fe783ff..97120ed 100644
--- a/ooknet-design/src/pages/notes/[slug].astro
+++ b/ooknet-design/src/pages/notes/[slug].astro
@@ -17,6 +17,7 @@ import ArticleBacklinks from "../../components/ArticleBacklinks/
ArticleBacklinks
 import type { PageLink } from "../../components/ArticlePagination/ArticlePaginati
on";
 import { published, byNoteNum } from "../../lib/notes";
 import { buildBacklinks, type Backlink } from "../../lib/wikilinks";
+import { describeBody } from "../../lib/meta";
 
 export async function getStaticPaths() {
   const notes = (await getCollection("notes", published)).sort(byNoteNum);
@@ -42,8 +43,10 @@ interface Props {
 }
 const { note, prev, next, refs } = Astro.props as Props;
 const { Content, headings } = await render(note);
+const description =
+  describeBody(note.body ?? "") || `${note.data.note} - filed ${note.data.filed}.
`;
 ---
-<ArticleLayout title={note.data.title}>
+<ArticleLayout title={note.data.title} description={description} type="article">
   <TopHeader />
   <ArticleHeader fm={note.data} />
   <DocumentTitle fm={note.data} />

diff --git a/ooknet-design/src/pages/notes/[
slug].astro b/ooknet-design/src/pages/notes/
[slug].astro
index fe783ff..97120ed 100644
--- a/ooknet-design/src/pages/notes/[slug].a
stro
+++ b/ooknet-design/src/pages/notes/[slug].a
stro
@@ -17,6 +17,7 @@ import ArticleBacklinks fr
om "../../components/ArticleBacklinks/Articl
eBacklinks
 import type { PageLink } from "../../compon
ents/ArticlePagination/ArticlePagination";
 import { published, byNoteNum } from "../..
/lib/notes";
 import { buildBacklinks, type Backlink } fr
om "../../lib/wikilinks";
+import { describeBody } from "../../lib/met
a";
 
 export async function getStaticPaths() {
   const notes = (await getCollection("notes
", published)).sort(byNoteNum);
@@ -42,8 +43,10 @@ interface Props {
 }
 const { note, prev, next, refs } = Astro.pr
ops as Props;
 const { Content, headings } = await render(
note);
+const description =
+  describeBody(note.body ?? "") || `${note.
data.note} - filed ${note.data.filed}.`;
 ---
-<ArticleLayout title={note.data.title}>
+<ArticleLayout title={note.data.title} desc
ription={description} type="article">
   <TopHeader />
   <ArticleHeader fm={note.data} />
   <DocumentTitle fm={note.data} />
 

diff --git a/ooknet-design/src/pages/projects.astro b/ooknet-design/src/pages/proj
ects.astro
index 78b36b1..c6255da 100644
--- a/ooknet-design/src/pages/projects.astro
+++ b/ooknet-design/src/pages/projects.astro
@@ -17,7 +17,8 @@ const PROJECTS = [
   ["NIX-FIELD-GUIDE", "climbing the NixOS mountain without a sherpa", "DRAFT"],
 ];
 ---
-<ArticleLayout title="OOKNET KB - Projects">
+<ArticleLayout title="OOKNET KB - Projects"
+  description="Active and archived OOKNET projects, with status and stack for eac
h.">
   <TopHeader />
   <Frame build={buildHeader} />
   <Pre>{" "}</Pre>

diff --git a/ooknet-design/src/pages/project
s.astro b/ooknet-design/src/pages/projects.a
stro
index 78b36b1..c6255da 100644
--- a/ooknet-design/src/pages/projects.astro
+++ b/ooknet-design/src/pages/projects.astro
@@ -17,7 +17,8 @@ const PROJECTS = [
   ["NIX-FIELD-GUIDE", "climbing the NixOS m
ountain without a sherpa", "DRAFT"],
 ];
 ---
-<ArticleLayout title="OOKNET KB - Projects"
>
+<ArticleLayout title="OOKNET KB - Projects"
+  description="Active and archived OOKNET p
rojects, with status and stack for each.">
   <TopHeader />
   <Frame build={buildHeader} />
   <Pre>{" "}</Pre>
 

diff --git a/ooknet-design/src/pages/resources.astro b/ooknet-design/src/pages/res
ources.astro
index da0d82e..a9d3658 100644
--- a/ooknet-design/src/pages/resources.astro
+++ b/ooknet-design/src/pages/resources.astro
@@ -26,7 +26,8 @@ const TOOLING: [string, string, string][] = [
   ["SEARCH", "FUSE.JS", "https://www.fusejs.io"],
 ];
 ---
-<ArticleLayout title="OOKNET KB - Resources">
+<ArticleLayout title="OOKNET KB - Resources"
+  description="External references filed by the OOKNET knowledge base - tools, pa
pers, and reading.">
   <TopHeader />
   <Frame build={buildSection("FILING TAXONOMY", `${notes.length} SHEETS`)} />
   <Pre>{" "}</Pre>

diff --git a/ooknet-design/src/pages/resourc
es.astro b/ooknet-design/src/pages/resources
.astro
index da0d82e..a9d3658 100644
--- a/ooknet-design/src/pages/resources.astr
o
+++ b/ooknet-design/src/pages/resources.astr
o
@@ -26,7 +26,8 @@ const TOOLING: [string, st
ring, string][] = [
   ["SEARCH", "FUSE.JS", "https://www.fusejs
.io"],
 ];
 ---
-<ArticleLayout title="OOKNET KB - Resources
">
+<ArticleLayout title="OOKNET KB - Resources
"
+  description="External references filed by
 the OOKNET knowledge base - tools, papers, 
and reading.">
   <TopHeader />
   <Frame build={buildSection("FILING TAXONO
MY", `${notes.length} SHEETS`)} />
   <Pre>{" "}</Pre>
 

diff --git a/ooknet-design/src/pages/rss.xml.ts b/ooknet-design/src/pages/rss.xml.
ts
new file mode 100644
index 0000000..8c8b6c8
--- /dev/null
+++ b/ooknet-design/src/pages/rss.xml.ts
@@ -0,0 +1,26 @@
+// Notes feed, newest first.
+import type { APIRoute } from "astro";
+import { getCollection } from "astro:content";
+import { published } from "../lib/notes";
+import { describeBody } from "../lib/meta";
+import { buildRss } from "../lib/feeds";
+
+export const GET: APIRoute = async ({ site }) => {
+  const notes = (await getCollection("notes", published)).sort((a, b) =>
+    b.data.published.localeCompare(a.data.published),
+  );
+  const xml = buildRss({
+    title: "OOKNET KB",
+    site: site!.href,
+    description: "OOKNET knowledge base - filed notes, newest first.",
+    items: notes.map((n) => ({
+      title: `${n.data.note} - ${n.data.title}`,
+      url: new URL(`/notes/${n.id}/`, site).href,
+      published: n.data.published,
+      description: describeBody(n.body ?? "") || `Filed ${n.data.filed}.`,
+    })),
+  });
+  return new Response(xml, {
+    headers: { "Content-Type": "application/rss+xml; charset=utf-8" },
+  });
+};

diff --git a/ooknet-design/src/pages/rss.xml
.ts b/ooknet-design/src/pages/rss.xml.ts
new file mode 100644
index 0000000..8c8b6c8
--- /dev/null
+++ b/ooknet-design/src/pages/rss.xml.ts
@@ -0,0 +1,26 @@
+// Notes feed, newest first.
+import type { APIRoute } from "astro";
+import { getCollection } from "astro:conten
t";
+import { published } from "../lib/notes";
+import { describeBody } from "../lib/meta";
+import { buildRss } from "../lib/feeds";
+
+export const GET: APIRoute = async ({ site 
}) => {
+  const notes = (await getCollection("notes
", published)).sort((a, b) =>
+    b.data.published.localeCompare(a.data.p
ublished),
+  );
+  const xml = buildRss({
+    title: "OOKNET KB",
+    site: site!.href,
+    description: "OOKNET knowledge base - f
iled notes, newest first.",
+    items: notes.map((n) => ({
+      title: `${n.data.note} - ${n.data.tit
le}`,
+      url: new URL(`/notes/${n.id}/`, site)
.href,
+      published: n.data.published,
+      description: describeBody(n.body ?? "
") || `Filed ${n.data.filed}.`,
+    })),
+  });
+  return new Response(xml, {
+    headers: { "Content-Type": "application
/rss+xml; charset=utf-8" },
+  });
+};
 

diff --git a/ooknet-design/src/pages/sitemap.xml.ts b/ooknet-design/src/pages/site
map.xml.ts
new file mode 100644
index 0000000..9085558
--- /dev/null
+++ b/ooknet-design/src/pages/sitemap.xml.ts
@@ -0,0 +1,20 @@
+import type { APIRoute } from "astro";
+import { getCollection } from "astro:content";
+import { published } from "../lib/notes";
+import { buildSitemap } from "../lib/feeds";
+
+const STATIC_PAGES = ["/", "/about/", "/projects/", "/resources/", "/components/"
, "/docs/", "/status/"];
+
+export const GET: APIRoute = async ({ site }) => {
+  const notes = await getCollection("notes", published);
+  const docs = await getCollection("docs", published);
+  const abs = (path: string) => new URL(path, site).href;
+  const xml = buildSitemap([
+    ...STATIC_PAGES.map((p) => ({ loc: abs(p) })),
+    ...notes.map((n) => ({ loc: abs(`/notes/${n.id}/`), lastmod: n.data.published
 })),
+    ...docs.map((d) => ({ loc: abs(`/docs/${d.id}/`) })),
+  ]);
+  return new Response(xml, {
+    headers: { "Content-Type": "application/xml; charset=utf-8" },
+  });
+};

diff --git a/ooknet-design/src/pages/sitemap
.xml.ts b/ooknet-design/src/pages/sitemap.xm
l.ts
new file mode 100644
index 0000000..9085558
--- /dev/null
+++ b/ooknet-design/src/pages/sitemap.xml.ts
@@ -0,0 +1,20 @@
+import type { APIRoute } from "astro";
+import { getCollection } from "astro:conten
t";
+import { published } from "../lib/notes";
+import { buildSitemap } from "../lib/feeds"
;
+
+const STATIC_PAGES = ["/", "/about/", "/pro
jects/", "/resources/", "/components/", "/do
cs/", "/status/"];
+
+export const GET: APIRoute = async ({ site 
}) => {
+  const notes = await getCollection("notes"
, published);
+  const docs = await getCollection("docs", 
published);
+  const abs = (path: string) => new URL(pat
h, site).href;
+  const xml = buildSitemap([
+    ...STATIC_PAGES.map((p) => ({ loc: abs(
p) })),
+    ...notes.map((n) => ({ loc: abs(`/notes
/${n.id}/`), lastmod: n.data.published })),
+    ...docs.map((d) => ({ loc: abs(`/docs/$
{d.id}/`) })),
+  ]);
+  return new Response(xml, {
+    headers: { "Content-Type": "application
/xml; charset=utf-8" },
+  });
+};
 

diff --git a/ooknet-design/src/pages/status.astro b/ooknet-design/src/pages/status
.astro
index ccf2afd..231c1fe 100644
--- a/ooknet-design/src/pages/status.astro
+++ b/ooknet-design/src/pages/status.astro
@@ -44,7 +44,8 @@ const log: TimelineEntry[] = byDate.map((n) => ({
   href: `/notes/${n.id}/`,
 }));
 ---
-<ArticleLayout title="OOKNET KB - Status">
+<ArticleLayout title="OOKNET KB - Status"
+  description="OOKNET system telemetry - filing capacity, note volume, and activi
ty, charted in text.">
   <TopHeader />
   <Frame build={(w) => [row2("SYSTEM TELEMETRY", `AS OF ${latestDate}`, w), ruleD
(w)].join("\n")} />
   <Pre>{" "}</Pre>

diff --git a/ooknet-design/src/pages/status.
astro b/ooknet-design/src/pages/status.astro
index ccf2afd..231c1fe 100644
--- a/ooknet-design/src/pages/status.astro
+++ b/ooknet-design/src/pages/status.astro
@@ -44,7 +44,8 @@ const log: TimelineEntry[]
 = byDate.map((n) => ({
   href: `/notes/${n.id}/`,
 }));
 ---
-<ArticleLayout title="OOKNET KB - Status">
+<ArticleLayout title="OOKNET KB - Status"
+  description="OOKNET system telemetry - fi
ling capacity, note volume, and activity, ch
arted in text.">
   <TopHeader />
   <Frame build={(w) => [row2("SYSTEM TELEME
TRY", `AS OF ${latestDate}`, w), ruleD(w)].j
oin("\n")} />
   <Pre>{" "}</Pre>
 
[ BACK TO LOG ]
──────────────────────────────────────────────────────────────────────────────────────
OOKNET
────────────────────────────────────────────────
OOKNET