mirror of
https://github.com/profullstack/logicsrc.git
synced 2026-10-01 20:33:50 +00:00
seo: give every page its own social title, not the homepage's
Reddit, Slack, X and Facebook read og:title, and every page of
logicsrc.com was serving the homepage's. The <title> tag was correct per
page, which is why it went unnoticed: Next.js shallow-merges metadata, so
a page exporting only `{ title, description, alternates }` inherits the
root layout's `openGraph` object whole. og:title, og:description and
og:url were the homepage's on all 45 spec landing pages, every doc, every
report and every SPA section.
Blog posts were the one route that declared openGraph, and they proved
the mechanism: their og:title is right. They also showed the other half
of it -- declaring openGraph drops the file-convention opengraph-image,
so live posts shipped with no og:image at all, and their twitter:title
was still the homepage's.
- src/lib/page-meta.ts: one builder, three entry points. specMetadata
titles a spec landing page from its lib/specs.ts entry, pageMetadata
titles the site's own pages from PAGE_META, contentMetadata titles a
doc, post, skill or ontology record from its content. All three always
emit openGraph and twitter title, description, url and image.
- Spec titles come from the registry, so they cannot drift from the way
the sidebar, /specs and the home page describe the same spec, and a new
spec page is titled the moment its registry entry exists. composeTitle
trims a long subject line at a clause boundary to fit a link preview.
- Doc pages carry that subject line too, because every spec's H1 is just
its name: "OpenStream" alone was the whole title.
- docExcerpt joins the wrapped lines of the first real paragraph and
skips Status:/Slug: headers and list lead-ins, so descriptions stopped
being "Status: 0.1 draft" and "Core tables:".
- contract/page-metadata.contract.test.ts fails if a sitemap route has no
title, if a spec page has no registry entry, if two pages share a
title, if a page declares metadata without the builder, or if a page
loses its social image.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
baa3adf716
commit
878ac53718
57 changed files with 909 additions and 361 deletions
297
apps/logicsrc-web/contract/page-metadata.contract.test.ts
Normal file
297
apps/logicsrc-web/contract/page-metadata.contract.test.ts
Normal file
|
|
@ -0,0 +1,297 @@
|
|||
import { readdirSync, readFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { DOC_SLUGS, docExcerpt, docSubtitle, docTitle, readDoc } from "../src/lib/docs";
|
||||
import {
|
||||
buildMetadata,
|
||||
composeTitle,
|
||||
PAGE_META,
|
||||
pageMetadata,
|
||||
SITE_NAME,
|
||||
SITE_OG_IMAGE,
|
||||
siteTitle,
|
||||
specMetadata,
|
||||
TITLE_MAX,
|
||||
} from "../src/lib/page-meta";
|
||||
import { allSpecs } from "../src/lib/specs";
|
||||
|
||||
/**
|
||||
* Guards every page's own title.
|
||||
*
|
||||
* Pages exported `{ title, description, alternates }` and nothing else.
|
||||
* Next.js shallow-merges metadata, so a page without an `openGraph` block
|
||||
* inherited the root layout's -- meaning og:title, og:description and og:url
|
||||
* were the HOMEPAGE's on every page of the site. Reddit, Slack, X and Facebook
|
||||
* read og:title, so every spec we posted was announced as "LogicSRC — Open
|
||||
* Coordination Standards for Humans & AI Agents" regardless of which spec the
|
||||
* link pointed at. The <title> tag was correct throughout, which is why
|
||||
* looking at the page never caught it.
|
||||
*
|
||||
* These tests fail if a page stops carrying its own social title, if a new
|
||||
* route reaches the sitemap without one, or if two pages share a title.
|
||||
*/
|
||||
|
||||
const APP_DIR = resolve(process.cwd(), "src/app");
|
||||
|
||||
type Meta = ReturnType<typeof pageMetadata>;
|
||||
|
||||
function titleOf(metadata: Meta): string {
|
||||
return String(metadata.title);
|
||||
}
|
||||
|
||||
function ogOf(metadata: Meta): { title?: unknown; description?: unknown; url?: unknown; images?: unknown } {
|
||||
return (metadata.openGraph ?? {}) as {
|
||||
title?: unknown;
|
||||
description?: unknown;
|
||||
url?: unknown;
|
||||
images?: unknown;
|
||||
};
|
||||
}
|
||||
|
||||
function twitterOf(metadata: Meta): { title?: unknown; images?: unknown } {
|
||||
return (metadata.twitter ?? {}) as { title?: unknown; images?: unknown };
|
||||
}
|
||||
|
||||
/** The sitemap's own static routes, read from source so the two cannot drift. */
|
||||
function sitemapStaticPaths(): string[] {
|
||||
const source = readFileSync(resolve(APP_DIR, "sitemap.ts"), "utf8");
|
||||
// The array literal, not its type: the Route type mentions
|
||||
// `Sitemap[number]["changeFrequency"]`, so a naive search for "];" stops
|
||||
// inside it and the block comes back empty.
|
||||
const start = source.indexOf("= [", source.indexOf("const STATIC_ROUTES"));
|
||||
const end = source.indexOf("\n];", start);
|
||||
return [...source.slice(start, end).matchAll(/path:\s*"([^"]+)"/g)].map((m) => m[1]);
|
||||
}
|
||||
|
||||
describe("the site's own pages each have a title", () => {
|
||||
const sitemapPaths = sitemapStaticPaths();
|
||||
|
||||
it("reads the sitemap's static routes", () => {
|
||||
expect(sitemapPaths.length).toBeGreaterThan(5);
|
||||
expect(sitemapPaths).toContain("/");
|
||||
});
|
||||
|
||||
it("every static route in the sitemap has a PAGE_META entry", () => {
|
||||
const missing = sitemapPaths.filter((path) => !PAGE_META[path]);
|
||||
expect(
|
||||
missing,
|
||||
`Add these paths to PAGE_META in src/lib/page-meta.ts, or they ship with the homepage's social card: ${missing.join(", ")}`
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
it("no PAGE_META entry is for a path nothing serves", () => {
|
||||
// /hire-us is an SPA section rather than a sitemap-listed page in its own
|
||||
// right, so it is allowed to appear here without a sitemap entry.
|
||||
const allowed = new Set([...sitemapPaths, "/hire-us"]);
|
||||
const orphans = Object.keys(PAGE_META).filter((path) => !allowed.has(path));
|
||||
expect(orphans, `These PAGE_META paths are not served: ${orphans.join(", ")}`).toEqual([]);
|
||||
});
|
||||
|
||||
it("titles and descriptions are unique per page", () => {
|
||||
const seen = new Map<string, string>();
|
||||
for (const [path, meta] of Object.entries(PAGE_META)) {
|
||||
const title = siteTitle(meta.title);
|
||||
const earlier = seen.get(title);
|
||||
expect(earlier, `"${title}" is used by both ${earlier} and ${path}`).toBeUndefined();
|
||||
seen.set(title, path);
|
||||
}
|
||||
const descriptions = Object.values(PAGE_META).map((m) => m.description);
|
||||
expect(new Set(descriptions).size).toBe(descriptions.length);
|
||||
});
|
||||
|
||||
it("only the homepage is titled with the homepage title", () => {
|
||||
const home = siteTitle(PAGE_META["/"].title);
|
||||
const sharing = Object.entries(PAGE_META)
|
||||
.filter(([path, meta]) => path !== "/" && siteTitle(meta.title) === home)
|
||||
.map(([path]) => path);
|
||||
expect(sharing).toEqual([]);
|
||||
});
|
||||
|
||||
it("every title names the site, says something, and fits a link preview", () => {
|
||||
for (const [path, meta] of Object.entries(PAGE_META)) {
|
||||
const title = siteTitle(meta.title);
|
||||
expect(title, path).toContain(SITE_NAME);
|
||||
// "Docs · LogicSRC" and "Blog · LogicSRC" are what this change replaced:
|
||||
// technically distinct, useless as a headline. Require a real subject.
|
||||
expect(title.replace(` · ${SITE_NAME}`, "").trim().split(/\s+/).length, path).toBeGreaterThan(1);
|
||||
expect(title.length, `${path} title is ${title.length} chars`).toBeLessThanOrEqual(TITLE_MAX);
|
||||
expect(meta.description.length, path).toBeGreaterThan(40);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("spec landing pages are titled from the registry", () => {
|
||||
const landings = allSpecs().filter((spec) => spec.landing);
|
||||
|
||||
it("there are spec landing pages to check", () => {
|
||||
expect(landings.length).toBeGreaterThan(20);
|
||||
});
|
||||
|
||||
it("each one gets its own title, description, og:url and image", () => {
|
||||
const titles = new Map<string, string>();
|
||||
for (const spec of landings) {
|
||||
const path = spec.landing as string;
|
||||
const metadata = specMetadata(path);
|
||||
const title = titleOf(metadata);
|
||||
|
||||
expect(title, path).toContain(spec.name);
|
||||
expect(title.length, `${path} title is ${title.length} chars: ${title}`).toBeLessThanOrEqual(TITLE_MAX);
|
||||
const earlier = titles.get(title);
|
||||
expect(earlier, `"${title}" is used by both ${earlier} and ${path}`).toBeUndefined();
|
||||
titles.set(title, path);
|
||||
|
||||
expect(ogOf(metadata).title, path).toBe(title);
|
||||
expect(twitterOf(metadata).title, path).toBe(title);
|
||||
expect(ogOf(metadata).url, path).toBe(`https://logicsrc.com${path}`);
|
||||
expect((metadata.alternates as { canonical?: unknown }).canonical, path).toBe(path);
|
||||
expect(ogOf(metadata).images, path).toEqual([SITE_OG_IMAGE]);
|
||||
}
|
||||
});
|
||||
|
||||
it("no spec landing title is the homepage's", () => {
|
||||
const home = siteTitle(PAGE_META["/"].title);
|
||||
for (const spec of landings) {
|
||||
expect(titleOf(specMetadata(spec.landing as string)), spec.slug).not.toBe(home);
|
||||
}
|
||||
});
|
||||
|
||||
it("a page's own description is kept when it passes one", () => {
|
||||
const spec = landings[0];
|
||||
const metadata = specMetadata(spec.landing as string, "A longer description written on the page.");
|
||||
expect(metadata.description).toBe("A longer description written on the page.");
|
||||
expect(ogOf(metadata).description).toBe("A longer description written on the page.");
|
||||
});
|
||||
|
||||
it("a spec page with no registry entry fails loudly rather than falling back", () => {
|
||||
expect(() => specMetadata("/not-a-spec")).toThrow(/lib\/specs\.ts/);
|
||||
expect(() => pageMetadata("/not-a-page")).toThrow(/PAGE_META/);
|
||||
});
|
||||
|
||||
it("composeTitle trims a long subject line at a clause boundary", () => {
|
||||
const long = composeTitle(
|
||||
"OpenObject",
|
||||
"A bucket you can mount: keyed objects encrypted by their owner, kept on paid disks at a stated redundancy, verified, repaired and read back by path"
|
||||
);
|
||||
expect(siteTitle(long).length).toBeLessThanOrEqual(TITLE_MAX);
|
||||
expect(long.startsWith("OpenObject: A bucket you can mount")).toBe(true);
|
||||
expect(long.endsWith(",")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("content routes carry their own social title too", () => {
|
||||
it("a doc, report or post gets openGraph and twitter titles and an image", () => {
|
||||
const metadata = buildMetadata({
|
||||
title: "OpenStream: A lossless byte-stream relay envelope",
|
||||
description: "How one program hands another the exact bytes of a stream.",
|
||||
path: "/docs/openstream",
|
||||
type: "article",
|
||||
});
|
||||
expect(titleOf(metadata)).toBe(
|
||||
"OpenStream: A lossless byte-stream relay envelope · LogicSRC"
|
||||
);
|
||||
expect(ogOf(metadata).title).toBe(titleOf(metadata));
|
||||
expect(twitterOf(metadata).title).toBe(titleOf(metadata));
|
||||
expect(ogOf(metadata).url).toBe("https://logicsrc.com/docs/openstream");
|
||||
expect(ogOf(metadata).images).toEqual([SITE_OG_IMAGE]);
|
||||
});
|
||||
|
||||
/**
|
||||
* Next.js only supplies the file-convention app/opengraph-image.tsx card to
|
||||
* routes that declare no `openGraph`. Every route declares one now, so the
|
||||
* card has to be named explicitly or every page loses its social image --
|
||||
* which is what already happened to blog posts, the one route that used to
|
||||
* declare openGraph. They shipped with no og:image at all.
|
||||
*/
|
||||
it("a post with a featured image uses it instead of the site card", () => {
|
||||
const metadata = buildMetadata({
|
||||
title: "A post with art",
|
||||
description: "It has its own image.",
|
||||
path: "/blog/art",
|
||||
type: "article",
|
||||
images: ["https://cdn.example.com/art.png"],
|
||||
});
|
||||
expect(ogOf(metadata).images).toEqual(["https://cdn.example.com/art.png"]);
|
||||
});
|
||||
|
||||
it("a syndicated post points og:url at its canonical original", () => {
|
||||
const metadata = buildMetadata({
|
||||
title: "A guest post",
|
||||
description: "Published first somewhere else.",
|
||||
path: "/blog/guest",
|
||||
canonical: "https://example.com/guest",
|
||||
type: "article",
|
||||
});
|
||||
expect(ogOf(metadata).url).toBe("https://example.com/guest");
|
||||
});
|
||||
});
|
||||
|
||||
describe("no page declares raw metadata", () => {
|
||||
function pageFiles(dir: string, out: string[] = []): string[] {
|
||||
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
||||
const path = resolve(dir, entry.name);
|
||||
if (entry.isDirectory()) pageFiles(path, out);
|
||||
else if (entry.name === "page.tsx" || entry.name === "layout.tsx") out.push(path);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
it("every page and layout builds its metadata through lib/page-meta", () => {
|
||||
const offenders: string[] = [];
|
||||
for (const file of pageFiles(APP_DIR)) {
|
||||
const source = readFileSync(file, "utf8");
|
||||
const declares = /export\s+(const\s+metadata|async\s+function\s+generateMetadata)/.test(source);
|
||||
if (!declares) continue;
|
||||
if (!source.includes('from "@/lib/page-meta"')) offenders.push(file.replace(`${APP_DIR}/`, ""));
|
||||
}
|
||||
expect(
|
||||
offenders,
|
||||
`These routes declare metadata without lib/page-meta, so they inherit the layout's openGraph title: ${offenders.join(", ")}`
|
||||
).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("doc pages are titled by their subject, not just their name", () => {
|
||||
it("every published doc has a subject line", () => {
|
||||
const missing = DOC_SLUGS.filter((slug) => docSubtitle(slug).length < 10);
|
||||
expect(
|
||||
missing,
|
||||
`These docs have no subject line: give the spec a registry entry in lib/specs.ts, or the guide an entry in GUIDE_SUBTITLES: ${missing.join(", ")}`
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
it("doc titles are distinct, name the doc, and fit a link preview", () => {
|
||||
const titles = new Map<string, string>();
|
||||
for (const slug of DOC_SLUGS) {
|
||||
const md = readDoc(slug);
|
||||
expect(md, `docs/${slug}.md should be readable`).toBeTruthy();
|
||||
const name = docTitle(md as string, slug);
|
||||
const title = siteTitle(composeTitle(name, docSubtitle(slug)));
|
||||
const earlier = titles.get(title);
|
||||
expect(earlier, `"${title}" is used by both ${earlier} and ${slug}`).toBeUndefined();
|
||||
titles.set(title, slug);
|
||||
expect(title).toContain(name);
|
||||
expect(title.length, `${slug} title is ${title.length} chars: ${title}`).toBeLessThanOrEqual(TITLE_MAX);
|
||||
}
|
||||
});
|
||||
|
||||
it("no doc description is a Status:/Slug: header or a bare list lead-in", () => {
|
||||
for (const slug of DOC_SLUGS) {
|
||||
const excerpt = docExcerpt(readDoc(slug) as string);
|
||||
expect(excerpt, slug).not.toMatch(/^(status|slug|version)\b\s*:/i);
|
||||
expect(excerpt, slug).not.toMatch(/:$/);
|
||||
// Either a real paragraph, or nothing -- in which case the page and the
|
||||
// docs index both fall back to the doc's subject line.
|
||||
if (excerpt) expect(excerpt.length, slug).toBeGreaterThan(40);
|
||||
}
|
||||
});
|
||||
|
||||
it("every doc has a usable description one way or the other", () => {
|
||||
for (const slug of DOC_SLUGS) {
|
||||
const md = readDoc(slug) as string;
|
||||
const name = docTitle(md, slug);
|
||||
const description =
|
||||
docExcerpt(md) || `${name}: ${docSubtitle(slug)}. Part of the LogicSRC open-standards surface.`;
|
||||
expect(description.length, slug).toBeGreaterThan(40);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
@ -2,6 +2,7 @@ import { notFound } from "next/navigation";
|
|||
import type { Metadata } from "next";
|
||||
import type { ReactNode } from "react";
|
||||
import { renderPageMarkup } from "@/lib/page-markup";
|
||||
import { pageMetadata, specMetadata } from "@/lib/page-meta";
|
||||
import { HomeInteractivity } from "@/components/home-interactivity";
|
||||
|
||||
// The legacy SPA served the same single page for every top-level path and just
|
||||
|
|
@ -14,28 +15,16 @@ import { HomeInteractivity } from "@/components/home-interactivity";
|
|||
// have a matching section id in `renderPageMarkup` -- /privacy used to be
|
||||
// listed without one, so it served the whole homepage and scrolled to a card
|
||||
// that only described the page that did not exist.
|
||||
const ROUTE_META: Record<string, { title: string; description: string }> = {
|
||||
openspec: {
|
||||
title: "LogicSRC vs OpenSpec.dev · LogicSRC",
|
||||
description: "How LogicSRC's coordination standard compares with OpenSpec.dev, including MCP and agent support.",
|
||||
},
|
||||
"credential-sharing": {
|
||||
title: "Credential Sharing · LogicSRC",
|
||||
description: "End-to-end-encrypted team vaults plus source/target credential diffs, approval, sync, rollback, and audit across .env, Doppler, Railway, GitHub Secrets, and sh1pt.",
|
||||
},
|
||||
"hire-us": {
|
||||
title: "Hire Us · LogicSRC",
|
||||
description: "Implementation help for LogicSRC, AgentSwarm, and Credential Sharing at $400/hour/agent for accepted work, paid via CoinPay.",
|
||||
},
|
||||
// /agent-swarm was the AgentSwarm placeholder band. It is now the OpenFleet
|
||||
// spec at app/openfleet; next.config.ts redirects the old path there.
|
||||
agentbyte: {
|
||||
title: "AgentByte · LogicSRC",
|
||||
description: "Agent screening sessions, AI-assisted humans, policy events, and APIs.",
|
||||
},
|
||||
};
|
||||
|
||||
const KNOWN_ROUTES = new Set(Object.keys(ROUTE_META));
|
||||
// These render the homepage scrolled to their own section, so with no metadata
|
||||
// of their own they were published under the homepage's title -- the bug that
|
||||
// made every shared link read "LogicSRC — Open Coordination Standards".
|
||||
// openspec, credential-sharing and agentbyte are specs, titled from
|
||||
// lib/specs.ts; hire-us is one of the site's own pages.
|
||||
// /agent-swarm was the AgentSwarm placeholder band. It is now the OpenFleet
|
||||
// spec at app/openfleet; next.config.ts redirects the old path there.
|
||||
const SPEC_SECTIONS = new Set(["openspec", "credential-sharing", "agentbyte"]);
|
||||
const SITE_SECTIONS = new Set(["hire-us"]);
|
||||
const KNOWN_ROUTES = new Set([...SPEC_SECTIONS, ...SITE_SECTIONS]);
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
|
|
@ -44,14 +33,9 @@ export async function generateMetadata({
|
|||
}): Promise<Metadata> {
|
||||
const { slug } = await params;
|
||||
const key = slug?.[0];
|
||||
if (key && ROUTE_META[key]) {
|
||||
return {
|
||||
title: ROUTE_META[key].title,
|
||||
description: ROUTE_META[key].description,
|
||||
alternates: { canonical: `/${key}` },
|
||||
};
|
||||
}
|
||||
return {};
|
||||
if (key && SPEC_SECTIONS.has(key)) return specMetadata(`/${key}`);
|
||||
if (key && SITE_SECTIONS.has(key)) return pageMetadata(`/${key}`);
|
||||
return pageMetadata("/");
|
||||
}
|
||||
|
||||
export default async function Page({
|
||||
|
|
|
|||
|
|
@ -1,13 +1,9 @@
|
|||
import type { ReactNode } from "react";
|
||||
import type { Metadata } from "next";
|
||||
import { pageMetadata } from "@/lib/page-meta";
|
||||
import { SiteShell } from "@/components/site-shell";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "About · LogicSRC",
|
||||
description:
|
||||
"LogicSRC is the Profullstack, Inc. open-specification project for coordination between humans and AI agents — schemas, primitives, and conventions that products implement without owning the standard.",
|
||||
alternates: { canonical: "/about" },
|
||||
};
|
||||
export const metadata: Metadata = pageMetadata("/about");
|
||||
|
||||
export default function AboutPage(): ReactNode {
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -1,15 +1,14 @@
|
|||
import Link from "next/link";
|
||||
import type { ReactNode } from "react";
|
||||
import type { Metadata } from "next";
|
||||
import { specMetadata } from "@/lib/page-meta";
|
||||
import { SiteShell } from "@/components/site-shell";
|
||||
import { mono, pre, table, td, th } from "../openontology/ui";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "ASDLC · LogicSRC",
|
||||
description:
|
||||
"ASDLC is the Agentic Software Development Lifecycle: nine phases for building software when agents work in parallel and CI/CD is the only gate, with four conformance levels and a ratchet rule that makes testing in production defensible.",
|
||||
alternates: { canonical: "/asdlc" }
|
||||
};
|
||||
export const metadata: Metadata = specMetadata(
|
||||
"/asdlc",
|
||||
"ASDLC is the Agentic Software Development Lifecycle: nine phases for building software when agents work in parallel and CI/CD is the only gate, with four conformance levels and a ratchet rule that makes testing in production defensible."
|
||||
);
|
||||
|
||||
const PHASES: Array<[string, string]> = [
|
||||
["Frame", "A human states intent; the agent restates scope and names what it is leaving out. The one phase where being wrong is expensive."],
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import Link from "next/link";
|
|||
import { notFound } from "next/navigation";
|
||||
import type { ReactNode } from "react";
|
||||
import type { Metadata } from "next";
|
||||
import { contentMetadata, notFoundMetadata } from "@/lib/page-meta";
|
||||
import { publicClient } from "@/lib/supabase";
|
||||
import { SiteShell } from "@/components/site-shell";
|
||||
import { AdUnit } from "@/components/ad-unit";
|
||||
|
|
@ -57,20 +58,21 @@ export async function generateMetadata({
|
|||
}): Promise<Metadata> {
|
||||
const { slug } = await params;
|
||||
const post = await loadPost(slug);
|
||||
if (!post) return { title: "Not found · LogicSRC" };
|
||||
return {
|
||||
title: `${post.title} · LogicSRC`,
|
||||
description: post.excerpt ?? undefined,
|
||||
if (!post) return notFoundMetadata("post");
|
||||
// This route already set openGraph, which is why its og:title was right --
|
||||
// and why it had no og:image: declaring openGraph suppresses the
|
||||
// file-convention card, and `images: undefined` did not bring it back.
|
||||
// contentMetadata names the card explicitly.
|
||||
return contentMetadata({
|
||||
title: post.title,
|
||||
description: post.excerpt ?? `${post.title} — from the LogicSRC blog.`,
|
||||
path: `/blog/${post.slug}`,
|
||||
// A guest post points its canonical at the original; an original post is
|
||||
// canonical to itself.
|
||||
alternates: { canonical: post.canonical_url ?? `/blog/${post.slug}` },
|
||||
openGraph: {
|
||||
title: post.title,
|
||||
description: post.excerpt ?? undefined,
|
||||
type: "article",
|
||||
images: post.featured_image?.url ? [post.featured_image.url] : undefined,
|
||||
},
|
||||
};
|
||||
canonical: post.canonical_url ?? `/blog/${post.slug}`,
|
||||
type: "article",
|
||||
...(post.featured_image?.url ? { images: [post.featured_image.url] } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
function formatDate(value: string): string {
|
||||
|
|
|
|||
|
|
@ -1,18 +1,16 @@
|
|||
import Link from "next/link";
|
||||
import type { ReactNode } from "react";
|
||||
import type { Metadata } from "next";
|
||||
import { pageMetadata } from "@/lib/page-meta";
|
||||
import { publicClient } from "@/lib/supabase";
|
||||
import { SiteShell } from "@/components/site-shell";
|
||||
import { AdUnit } from "@/components/ad-unit";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Blog · LogicSRC",
|
||||
description:
|
||||
"Project notes for LogicSRC OpenSpec standards, AgentSwarm, AgentByte, SDKs, MCP, and reference implementations.",
|
||||
alternates: { types: { "application/rss+xml": "/blog/rss.xml" } },
|
||||
};
|
||||
export const metadata: Metadata = pageMetadata("/blog", {
|
||||
alternateTypes: { "application/rss+xml": "/blog/rss.xml" },
|
||||
});
|
||||
|
||||
type PostRow = {
|
||||
slug: string;
|
||||
|
|
|
|||
|
|
@ -3,7 +3,8 @@ import { notFound } from "next/navigation";
|
|||
import type { ReactNode } from "react";
|
||||
import type { Metadata } from "next";
|
||||
import { marked } from "marked";
|
||||
import { DOC_SLUGS, docExcerpt, docTitle, readDoc } from "@/lib/docs";
|
||||
import { DOC_SLUGS, docExcerpt, docSubtitle, docTitle, readDoc } from "@/lib/docs";
|
||||
import { composeTitle, contentMetadata, notFoundMetadata } from "@/lib/page-meta";
|
||||
import { SiteShell } from "@/components/site-shell";
|
||||
import { sanitizeRenderedHtml } from "@/lib/html";
|
||||
|
||||
|
|
@ -21,12 +22,18 @@ export async function generateMetadata({
|
|||
}): Promise<Metadata> {
|
||||
const { slug } = await params;
|
||||
const md = readDoc(slug);
|
||||
if (!md) return { title: "Not found · LogicSRC" };
|
||||
return {
|
||||
title: `${docTitle(md, slug)} · LogicSRC Docs`,
|
||||
description: docExcerpt(md) || undefined,
|
||||
alternates: { canonical: `/docs/${slug}` },
|
||||
};
|
||||
if (!md) return notFoundMetadata("doc");
|
||||
// Every spec's H1 is just its name, so "OpenStream" alone was the whole
|
||||
// title. The subject line comes from the registry (or the guide list).
|
||||
const name = docTitle(md, slug);
|
||||
const subtitle = docSubtitle(slug);
|
||||
return contentMetadata({
|
||||
title: subtitle ? composeTitle(name, subtitle) : name,
|
||||
description:
|
||||
docExcerpt(md) || `${name}: ${subtitle}. Part of the LogicSRC open-standards surface.`,
|
||||
path: `/docs/${slug}`,
|
||||
type: "article",
|
||||
});
|
||||
}
|
||||
|
||||
export default async function DocPage({
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ import Link from "next/link";
|
|||
import { notFound } from "next/navigation";
|
||||
import type { ReactNode } from "react";
|
||||
import type { Metadata } from "next";
|
||||
import { contentMetadata, notFoundMetadata } from "@/lib/page-meta";
|
||||
import { docTitle, readDoc } from "@/lib/docs";
|
||||
import { marked } from "marked";
|
||||
import { hasReports, readReportJson, readReportMarkdown, reportIds, REPORTED_SPECS } from "@/lib/reports";
|
||||
import { SiteShell } from "@/components/site-shell";
|
||||
|
|
@ -25,12 +27,15 @@ export async function generateMetadata({
|
|||
}): Promise<Metadata> {
|
||||
const { slug, id } = await params;
|
||||
const r = readReportJson(slug, id);
|
||||
if (!r) return { title: "Not found · LogicSRC" };
|
||||
return {
|
||||
title: `${r.implementation.name} ${r.implementation.version} benchmark · ${slug} · LogicSRC`,
|
||||
description: `Reproducible ${slug} benchmark: ${r.environment.runtime} on ${r.environment.os}, generated ${r.generatedAt}.`,
|
||||
alternates: { canonical: `/docs/${slug}/reports/${id}` },
|
||||
};
|
||||
if (!r) return notFoundMetadata("benchmark report");
|
||||
const md = readDoc(slug);
|
||||
const name = md ? docTitle(md, slug) : slug;
|
||||
return contentMetadata({
|
||||
title: `${r.implementation.name} ${r.implementation.version} ${name} benchmark`,
|
||||
description: `Reproducible ${name} benchmark: ${r.environment.runtime} on ${r.environment.os}, generated ${r.generatedAt}.`,
|
||||
path: `/docs/${slug}/reports/${id}`,
|
||||
type: "article",
|
||||
});
|
||||
}
|
||||
|
||||
export default async function ReportPage({
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import Link from "next/link";
|
|||
import { notFound } from "next/navigation";
|
||||
import type { ReactNode } from "react";
|
||||
import type { Metadata } from "next";
|
||||
import { contentMetadata } from "@/lib/page-meta";
|
||||
import { docTitle, readDoc } from "@/lib/docs";
|
||||
import { hasReports, listReports, REPORTED_SPECS } from "@/lib/reports";
|
||||
import { SiteShell } from "@/components/site-shell";
|
||||
|
|
@ -20,11 +21,13 @@ export async function generateMetadata({
|
|||
params: Promise<{ slug: string }>;
|
||||
}): Promise<Metadata> {
|
||||
const { slug } = await params;
|
||||
return {
|
||||
title: `Benchmark reports · ${slug} · LogicSRC`,
|
||||
description: `Reproducible benchmark reports published with each release of the ${slug} reference implementation.`,
|
||||
alternates: { canonical: `/docs/${slug}/reports` },
|
||||
};
|
||||
const md = readDoc(slug);
|
||||
const name = md ? docTitle(md, slug) : slug;
|
||||
return contentMetadata({
|
||||
title: `${name} benchmark reports`,
|
||||
description: `Reproducible benchmark reports published with each release of the ${name} reference implementation.`,
|
||||
path: `/docs/${slug}/reports`,
|
||||
});
|
||||
}
|
||||
|
||||
export default async function ReportsIndex({
|
||||
|
|
|
|||
|
|
@ -1,16 +1,12 @@
|
|||
import Link from "next/link";
|
||||
import type { ReactNode } from "react";
|
||||
import type { Metadata } from "next";
|
||||
import { pageMetadata } from "@/lib/page-meta";
|
||||
import { listDocs } from "@/lib/docs";
|
||||
import { FAMILIES, GUIDES, familyTree } from "@/lib/specs";
|
||||
import { SiteShell } from "@/components/site-shell";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Docs · LogicSRC",
|
||||
description:
|
||||
"The specification text of every LogicSRC spec, grouped by family, and the guides: data model, CLI and TUI conventions, config, permissions, plugins.",
|
||||
alternates: { canonical: "/docs" },
|
||||
};
|
||||
export const metadata: Metadata = pageMetadata("/docs");
|
||||
|
||||
const row: React.CSSProperties = { padding: "0.9rem 0", borderTop: "1px solid #e3e6e0" };
|
||||
const h3: React.CSSProperties = { margin: "0 0 0.3rem", fontSize: "1.1rem", color: "#101418" };
|
||||
|
|
|
|||
|
|
@ -4,34 +4,24 @@ import "../styles.css";
|
|||
import Script from "next/script";
|
||||
import { FeedbackWidget } from "@profullstack/stack/feedback";
|
||||
import { CopyButtons } from "@/components/copy-buttons";
|
||||
import { PAGE_META, pageMetadata, SITE_URL } from "@/lib/page-meta";
|
||||
|
||||
const SITE_URL = (process.env.PUBLIC_URL ?? "https://logicsrc.com").replace(/\/$/, "");
|
||||
const DESCRIPTION =
|
||||
"Open schemas, primitives, and conventions for coordination between humans, AI agents, plugins, payment systems, and hosted products.";
|
||||
const DESCRIPTION = PAGE_META["/"].description;
|
||||
|
||||
// The layout's metadata is the homepage's, and every child page overrides it
|
||||
// through specMetadata/pageMetadata/contentMetadata. That matters most for
|
||||
// openGraph: Next.js shallow-merges metadata, so a page that omits `openGraph`
|
||||
// inherits this object whole -- which is how every spec came to share the
|
||||
// homepage's og:title on Reddit. See lib/page-meta.ts.
|
||||
export const metadata: Metadata = {
|
||||
metadataBase: new URL(SITE_URL),
|
||||
title: "LogicSRC — Open Coordination Standards for Humans & AI Agents",
|
||||
description: DESCRIPTION,
|
||||
applicationName: "LogicSRC",
|
||||
manifest: "/manifest.webmanifest",
|
||||
alternates: { canonical: "/" },
|
||||
icons: {
|
||||
icon: [{ url: "/icon.svg", type: "image/svg+xml" }],
|
||||
apple: "/icon.svg",
|
||||
},
|
||||
openGraph: {
|
||||
type: "website",
|
||||
siteName: "LogicSRC",
|
||||
url: SITE_URL,
|
||||
title: "LogicSRC — Open Coordination Standards for Humans & AI Agents",
|
||||
description: DESCRIPTION,
|
||||
},
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
title: "LogicSRC — Open Coordination Standards",
|
||||
description: DESCRIPTION,
|
||||
},
|
||||
...pageMetadata("/"),
|
||||
};
|
||||
|
||||
export const viewport: Viewport = {
|
||||
|
|
|
|||
|
|
@ -1,15 +1,14 @@
|
|||
import Link from "next/link";
|
||||
import type { ReactNode } from "react";
|
||||
import type { Metadata } from "next";
|
||||
import { specMetadata } from "@/lib/page-meta";
|
||||
import { SiteShell } from "@/components/site-shell";
|
||||
import { mono, pre, table, td, th } from "../openontology/ui";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "OpenAccess · LogicSRC",
|
||||
description:
|
||||
"OpenAccess is OAuth 2.1 with a grant you can carry: one hub account per person or agent, every app keeps its own users and links them once, permissions delegate narrower to agents, and a subscription bought in one app is honoured by every app that honours the product. Reference hub at openaccess.logicsrc.com.",
|
||||
alternates: { canonical: "/openaccess" }
|
||||
};
|
||||
export const metadata: Metadata = specMetadata(
|
||||
"/openaccess",
|
||||
"OpenAccess is OAuth 2.1 with a grant you can carry: one hub account per person or agent, every app keeps its own users and links them once, permissions delegate narrower to agents, and a subscription bought in one app is honoured by every app that honours the product. Reference hub at openaccess.logicsrc.com."
|
||||
);
|
||||
|
||||
const DESCRIPTOR = `{
|
||||
"openaccess": "0.1",
|
||||
|
|
|
|||
|
|
@ -1,15 +1,14 @@
|
|||
import Link from "next/link";
|
||||
import type { ReactNode } from "react";
|
||||
import type { Metadata } from "next";
|
||||
import { specMetadata } from "@/lib/page-meta";
|
||||
import { SiteShell } from "@/components/site-shell";
|
||||
import { mono, pre, table, td, th } from "../openontology/ui";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "OpenAffiliate · LogicSRC",
|
||||
description:
|
||||
"OpenAffiliate is one file a merchant serves about the commission it pays, at /.well-known/openaffiliate.json, and four calls that let a person or an agent earn it: join with a profile, link with one parameter, read your own ledger, get paid to your own address. No network in the money.",
|
||||
alternates: { canonical: "/openaffiliate" }
|
||||
};
|
||||
export const metadata: Metadata = specMetadata(
|
||||
"/openaffiliate",
|
||||
"OpenAffiliate is one file a merchant serves about the commission it pays, at /.well-known/openaffiliate.json, and four calls that let a person or an agent earn it: join with a profile, link with one parameter, read your own ledger, get paid to your own address. No network in the money."
|
||||
);
|
||||
|
||||
const DESCRIPTOR = `{
|
||||
"merchant": { "name": "CrawlProof", "web": "https://crawlproof.com", "currency": "USD",
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
import Link from "next/link";
|
||||
import type { Metadata } from "next";
|
||||
import { specMetadata } from "@/lib/page-meta";
|
||||
import { SiteShell } from "@/components/site-shell";
|
||||
import profile from "../../../public/examples/openagent.json";
|
||||
import { mono, pre, table, td } from "../openontology/ui";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "OpenAgent · LogicSRC",
|
||||
description: "A portable agent profile: identity, owner, skills and requested permissions in one JSON file, with bindings to OpenProfile, OpenFleet and OpenRental.",
|
||||
alternates: { canonical: "/openagent" }
|
||||
};
|
||||
export const metadata: Metadata = specMetadata(
|
||||
"/openagent",
|
||||
"A portable agent profile: identity, owner, skills and requested permissions in one JSON file, with bindings to OpenProfile, OpenFleet and OpenRental."
|
||||
);
|
||||
|
||||
const LINKS = [
|
||||
["OpenProfile.md", "/openprofile", "The identity, accounts and topics people read. Link the JSON descriptor from the same profile page."],
|
||||
|
|
|
|||
|
|
@ -1,13 +1,12 @@
|
|||
import type { ReactNode } from "react";
|
||||
import type { Metadata } from "next";
|
||||
import { specMetadata } from "@/lib/page-meta";
|
||||
import { ResourceSpecPage, type ResourceSpec } from "@/components/resource-spec-page";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "OpenBandwidth · LogicSRC",
|
||||
description:
|
||||
"OpenBandwidth is the network block of an OpenServer offer: port speed, how traffic is metered (transfer, unmetered, 95th percentile, flat), what overage costs, IPv4 and IPv6 addresses as a priced resource, DDoS scrubbing, and a range that says what a buyer can add at checkout and for how much.",
|
||||
alternates: { canonical: "/openbandwidth" }
|
||||
};
|
||||
export const metadata: Metadata = specMetadata(
|
||||
"/openbandwidth",
|
||||
"OpenBandwidth is the network block of an OpenServer offer: port speed, how traffic is metered (transfer, unmetered, 95th percentile, flat), what overage costs, IPv4 and IPv6 addresses as a priced resource, DDoS scrubbing, and a range that says what a buyer can add at checkout and for how much."
|
||||
);
|
||||
|
||||
const SPEC: ResourceSpec = {
|
||||
name: "OpenBandwidth",
|
||||
|
|
|
|||
|
|
@ -1,13 +1,12 @@
|
|||
import type { ReactNode } from "react";
|
||||
import type { Metadata } from "next";
|
||||
import { specMetadata } from "@/lib/page-meta";
|
||||
import { ProfileSectionPage, type ProfileSection } from "@/components/profile-section-page";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "OpenBroadcast · LogicSRC",
|
||||
description:
|
||||
"OpenBroadcast is the Broadcast section of an OpenProfile.md: the show a person hosts (podcast, radio, live audio, stream), its format, cadence, audience, topics and slots, and who the host is seeking, so a host and a guest are matched from two files rather than two forms.",
|
||||
alternates: { canonical: "/openbroadcast" }
|
||||
};
|
||||
export const metadata: Metadata = specMetadata(
|
||||
"/openbroadcast",
|
||||
"OpenBroadcast is the Broadcast section of an OpenProfile.md: the show a person hosts (podcast, radio, live audio, stream), its format, cadence, audience, topics and slots, and who the host is seeking, so a host and a guest are matched from two files rather than two forms."
|
||||
);
|
||||
|
||||
const SPEC: ProfileSection = {
|
||||
name: "OpenBroadcast",
|
||||
|
|
|
|||
|
|
@ -1,15 +1,14 @@
|
|||
import Link from "next/link";
|
||||
import type { ReactNode } from "react";
|
||||
import type { Metadata } from "next";
|
||||
import { specMetadata } from "@/lib/page-meta";
|
||||
import { SiteShell } from "@/components/site-shell";
|
||||
import { mono, pre, table, td, th } from "../openontology/ui";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "OpenConnection · LogicSRC",
|
||||
description:
|
||||
"OpenConnection is a setup token you paste. A bridge that holds a person's connections issues it, any app claims it once for an access URL and a bearer the bridge can revoke, and no app registers, keeps a key or hosts a redirect. SimpleFIN's door, written down for anything a bridge holds: social accounts, a writer, a calendar, a wallet.",
|
||||
alternates: { canonical: "/openconnection" }
|
||||
};
|
||||
export const metadata: Metadata = specMetadata(
|
||||
"/openconnection",
|
||||
"OpenConnection is a setup token you paste. A bridge that holds a person's connections issues it, any app claims it once for an access URL and a bearer the bridge can revoke, and no app registers, keeps a key or hosts a redirect. SimpleFIN's door, written down for anything a bridge holds: social accounts, a writer, a calendar, a wallet."
|
||||
);
|
||||
|
||||
const CLAIM = `POST https://mynaposter.com/openconnection/claim/2f9c7d1e-…
|
||||
Content-Type: application/json
|
||||
|
|
|
|||
|
|
@ -1,15 +1,14 @@
|
|||
import Link from "next/link";
|
||||
import type { ReactNode } from "react";
|
||||
import type { Metadata } from "next";
|
||||
import { specMetadata } from "@/lib/page-meta";
|
||||
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" }
|
||||
};
|
||||
export const metadata: Metadata = specMetadata(
|
||||
"/opencoupon",
|
||||
"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."
|
||||
);
|
||||
|
||||
const DESCRIPTOR = `{
|
||||
"merchant": { "name": "Northwind Outfitters", "web": "https://northwind.example",
|
||||
|
|
|
|||
|
|
@ -1,13 +1,12 @@
|
|||
import type { ReactNode } from "react";
|
||||
import type { Metadata } from "next";
|
||||
import { specMetadata } from "@/lib/page-meta";
|
||||
import { ResourceSpecPage, type ResourceSpec } from "@/components/resource-spec-page";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "OpenCPU · LogicSRC",
|
||||
description:
|
||||
"OpenCPU is the compute block of an OpenServer offer: threads against cores, the processor by its vendor name, dedicated, shared or burstable allocation, and a range that says how many more a buyer can add at checkout and for how much.",
|
||||
alternates: { canonical: "/opencpu" }
|
||||
};
|
||||
export const metadata: Metadata = specMetadata(
|
||||
"/opencpu",
|
||||
"OpenCPU is the compute block of an OpenServer offer: threads against cores, the processor by its vendor name, dedicated, shared or burstable allocation, and a range that says how many more a buyer can add at checkout and for how much."
|
||||
);
|
||||
|
||||
const SPEC: ResourceSpec = {
|
||||
name: "OpenCPU",
|
||||
|
|
|
|||
|
|
@ -1,14 +1,13 @@
|
|||
import type { ReactNode } from "react";
|
||||
import type { Metadata } from "next";
|
||||
import { specMetadata } from "@/lib/page-meta";
|
||||
import { SiteShell } from "@/components/site-shell";
|
||||
import { card, mono, pre, table, td, th } from "../openontology/ui";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "OpenCreds · LogicSRC",
|
||||
description:
|
||||
"OpenCreds is an open standard for credential records and portable vaults: one record for logins, cards, identities, notes, keys and accounts, an end-to-end-encrypted envelope, and a single encrypted file that moves a vault between products without a plaintext CSV.",
|
||||
alternates: { canonical: "/opencreds" }
|
||||
};
|
||||
export const metadata: Metadata = specMetadata(
|
||||
"/opencreds",
|
||||
"OpenCreds is an open standard for credential records and portable vaults: one record for logins, cards, identities, notes, keys and accounts, an end-to-end-encrypted envelope, and a single encrypted file that moves a vault between products without a plaintext CSV."
|
||||
);
|
||||
|
||||
const TYPES: Array<[type: string, code: string, holds: string]> = [
|
||||
["login", "1", "Username, password, TOTP seed, matching URIs, password history"],
|
||||
|
|
|
|||
|
|
@ -1,15 +1,14 @@
|
|||
import Link from "next/link";
|
||||
import type { ReactNode } from "react";
|
||||
import type { Metadata } from "next";
|
||||
import { specMetadata } from "@/lib/page-meta";
|
||||
import { SiteShell } from "@/components/site-shell";
|
||||
import { mono, pre, table, td, th } from "../openontology/ui";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "OpenDisk · LogicSRC",
|
||||
description:
|
||||
"OpenDisk is one file a machine serves about the disk it will rent: free GiB, price per GiB-month, location, policy, proof cadence and hub standing, at /.well-known/opendisk.json. What a peer-to-peer storage market is made of. Reference marketplace d1sks.com.",
|
||||
alternates: { canonical: "/opendisk" }
|
||||
};
|
||||
export const metadata: Metadata = specMetadata(
|
||||
"/opendisk",
|
||||
"OpenDisk is one file a machine serves about the disk it will rent: free GiB, price per GiB-month, location, policy, proof cadence and hub standing, at /.well-known/opendisk.json. What a peer-to-peer storage market is made of. Reference marketplace d1sks.com."
|
||||
);
|
||||
|
||||
const DESCRIPTOR = `{
|
||||
"name": "seeder-a41e, Falkenstein",
|
||||
|
|
|
|||
|
|
@ -1,17 +1,18 @@
|
|||
import Link from "next/link";
|
||||
import type { ReactNode } from "react";
|
||||
import type { Metadata } from "next";
|
||||
import { contentMetadata } from "@/lib/page-meta";
|
||||
import { SiteShell } from "@/components/site-shell";
|
||||
import { Catalog } from "./catalog";
|
||||
import { REPO_URL } from "./set";
|
||||
import styles from "./catalog.module.css";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "OpenEmoji catalog · LogicSRC",
|
||||
export const metadata: Metadata = contentMetadata({
|
||||
title: "OpenEmoji Catalog: Every Glyph in the Set",
|
||||
description:
|
||||
"Every standard emoji in the OpenEmoji set, 3,963 in Emoji 18.0, drawn by gpt-image-2 as one family. Search by name or CLDR keyword, filter by group, skin tone, Emoji version and status, copy the character or download PNG and SVG.",
|
||||
alternates: { canonical: "/openemoji/catalog" }
|
||||
};
|
||||
path: "/openemoji/catalog"
|
||||
});
|
||||
|
||||
export default function OpenEmojiCatalogPage(): ReactNode {
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -1,15 +1,14 @@
|
|||
import Link from "next/link";
|
||||
import type { ReactNode } from "react";
|
||||
import type { Metadata } from "next";
|
||||
import { specMetadata } from "@/lib/page-meta";
|
||||
import { SiteShell } from "@/components/site-shell";
|
||||
import { mono, pre, table, td, th } from "../openontology/ui";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "OpenEmoji · LogicSRC",
|
||||
description:
|
||||
"OpenEmoji is an emoji set as a folder: one openemoji.json that states coverage, licence and who or what drew it (made_by, W3C AI disclosure), and glyphs named by fully-qualified codepoint sequence. Standard and custom emoji, PNG, SVG and colour fonts, rendered as text with the character as alt.",
|
||||
alternates: { canonical: "/openemoji" }
|
||||
};
|
||||
export const metadata: Metadata = specMetadata(
|
||||
"/openemoji",
|
||||
"OpenEmoji is an emoji set as a folder: one openemoji.json that states coverage, licence and who or what drew it (made_by, W3C AI disclosure), and glyphs named by fully-qualified codepoint sequence. Standard and custom emoji, PNG, SVG and colour fonts, rendered as text with the character as alt."
|
||||
);
|
||||
|
||||
/** Drawn by the reference implementation, `emoji` in profullstack/cli-tools. */
|
||||
const SAMPLE: Array<[string, string, string]> = [
|
||||
|
|
|
|||
|
|
@ -1,15 +1,14 @@
|
|||
import Link from "next/link";
|
||||
import type { ReactNode } from "react";
|
||||
import type { Metadata } from "next";
|
||||
import { specMetadata } from "@/lib/page-meta";
|
||||
import { SiteShell } from "@/components/site-shell";
|
||||
import { mono, pre, table, td, th } from "../openontology/ui";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "OpenFile · LogicSRC",
|
||||
description:
|
||||
"OpenFile is 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, at /.well-known/openfile.json. The web door onto an OpenSwarm ipfile swarm.",
|
||||
alternates: { canonical: "/openfile" }
|
||||
};
|
||||
export const metadata: Metadata = specMetadata(
|
||||
"/openfile",
|
||||
"OpenFile is 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, at /.well-known/openfile.json. The web door onto an OpenSwarm ipfile swarm."
|
||||
);
|
||||
|
||||
const DESCRIPTOR = `{
|
||||
"publisher": {
|
||||
|
|
|
|||
|
|
@ -1,15 +1,14 @@
|
|||
import Link from "next/link";
|
||||
import type { ReactNode } from "react";
|
||||
import type { Metadata } from "next";
|
||||
import { specMetadata } from "@/lib/page-meta";
|
||||
import { SiteShell } from "@/components/site-shell";
|
||||
import { mono, pre, table, td, th } from "../openontology/ui";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "OpenFleet · LogicSRC",
|
||||
description:
|
||||
"OpenFleet is the record an agent session carries about where it sits: which human answers for it, who spawned it, for what task, at what depth and under what ceiling. A fleet is every session one human is answerable for; a swarm is the sessions one spawner starts inside it to do one task. Fleets are always human controlled.",
|
||||
alternates: { canonical: "/openfleet" }
|
||||
};
|
||||
export const metadata: Metadata = specMetadata(
|
||||
"/openfleet",
|
||||
"OpenFleet is the record an agent session carries about where it sits: which human answers for it, who spawned it, for what task, at what depth and under what ceiling. A fleet is every session one human is answerable for; a swarm is the sessions one spawner starts inside it to do one task. Fleets are always human controlled."
|
||||
);
|
||||
|
||||
const RECORD = `{
|
||||
"openfleet": "0.1",
|
||||
|
|
|
|||
|
|
@ -1,13 +1,12 @@
|
|||
import type { ReactNode } from "react";
|
||||
import type { Metadata } from "next";
|
||||
import { specMetadata } from "@/lib/page-meta";
|
||||
import { ResourceSpecPage, type ResourceSpec } from "@/components/resource-spec-page";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "OpenGPU · LogicSRC",
|
||||
description:
|
||||
"OpenGPU is the gpu block of an OpenServer offer: the card by its vendor name, count and VRAM per device, the interconnect between them, passthrough, MIG, vGPU or shared access, and a range that says how many more a buyer can add at checkout and for how much.",
|
||||
alternates: { canonical: "/opengpu" }
|
||||
};
|
||||
export const metadata: Metadata = specMetadata(
|
||||
"/opengpu",
|
||||
"OpenGPU is the gpu block of an OpenServer offer: the card by its vendor name, count and VRAM per device, the interconnect between them, passthrough, MIG, vGPU or shared access, and a range that says how many more a buyer can add at checkout and for how much."
|
||||
);
|
||||
|
||||
const SPEC: ResourceSpec = {
|
||||
name: "OpenGPU",
|
||||
|
|
|
|||
|
|
@ -1,13 +1,12 @@
|
|||
import type { ReactNode } from "react";
|
||||
import type { Metadata } from "next";
|
||||
import { specMetadata } from "@/lib/page-meta";
|
||||
import { ProfileSectionPage, type ProfileSection } from "@/components/profile-section-page";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "OpenGuest · LogicSRC",
|
||||
description:
|
||||
"OpenGuest is the Guest section of an OpenProfile.md: that a person is available to appear on shows, their expertise and credentials, formats, availability, rate, past appearances and dealbreakers, so a guest and a host are matched from two files rather than two forms. An expert is a guest with Expertise and Credentials.",
|
||||
alternates: { canonical: "/openguest" }
|
||||
};
|
||||
export const metadata: Metadata = specMetadata(
|
||||
"/openguest",
|
||||
"OpenGuest is the Guest section of an OpenProfile.md: that a person is available to appear on shows, their expertise and credentials, formats, availability, rate, past appearances and dealbreakers, so a guest and a host are matched from two files rather than two forms. An expert is a guest with Expertise and Credentials."
|
||||
);
|
||||
|
||||
const SPEC: ProfileSection = {
|
||||
name: "OpenGuest",
|
||||
|
|
|
|||
|
|
@ -1,17 +1,16 @@
|
|||
import Link from "next/link";
|
||||
import type { ReactNode } from "react";
|
||||
import type { Metadata } from "next";
|
||||
import { specMetadata } from "@/lib/page-meta";
|
||||
import { SiteShell } from "@/components/site-shell";
|
||||
import { mono, pre, table, td, th } from "../openontology/ui";
|
||||
import { Gallery } from "./gallery";
|
||||
import { OPENICON_REPO } from "./set";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "OpenIcon · LogicSRC",
|
||||
description:
|
||||
"OpenIcon is an icon set as a folder: one openicon.json naming every icon with aliases and keywords, 24x24 currentColor SVGs, and three terminal glyphs per icon (Nerd Font, Unicode, ASCII) so a TUI draws the best one it can. Brand logos say they are brands.",
|
||||
alternates: { canonical: "/openicon" }
|
||||
};
|
||||
export const metadata: Metadata = specMetadata(
|
||||
"/openicon",
|
||||
"OpenIcon is an icon set as a folder: one openicon.json naming every icon with aliases and keywords, 24x24 currentColor SVGs, and three terminal glyphs per icon (Nerd Font, Unicode, ASCII) so a TUI draws the best one it can. Brand logos say they are brands."
|
||||
);
|
||||
|
||||
|
||||
/** key, Nerd Font codepoint, Unicode, ASCII: straight from openicon.json. */
|
||||
|
|
|
|||
|
|
@ -1,15 +1,14 @@
|
|||
import Link from "next/link";
|
||||
import type { ReactNode } from "react";
|
||||
import type { Metadata } from "next";
|
||||
import { specMetadata } from "@/lib/page-meta";
|
||||
import { SiteShell } from "@/components/site-shell";
|
||||
import { mono, pre, table, td, th } from "../openontology/ui";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "OpenMCP · LogicSRC",
|
||||
description:
|
||||
"OpenMCP is an open catalog protocol for MCP relays: a relay serves /.well-known/openmcp.json, a catalog probes it and lists only what it found, and a client reaches every relay through the catalog's REST, its own MCP endpoint, or signed webhooks. Reference implementation at github.com/logicsrc/openmcp.",
|
||||
alternates: { canonical: "/openmcp" }
|
||||
};
|
||||
export const metadata: Metadata = specMetadata(
|
||||
"/openmcp",
|
||||
"OpenMCP is an open catalog protocol for MCP relays: a relay serves /.well-known/openmcp.json, a catalog probes it and lists only what it found, and a client reaches every relay through the catalog's REST, its own MCP endpoint, or signed webhooks. Reference implementation at github.com/logicsrc/openmcp."
|
||||
);
|
||||
|
||||
const DESCRIPTOR = `{
|
||||
"openmcp": "0.1",
|
||||
|
|
|
|||
|
|
@ -1,13 +1,12 @@
|
|||
import type { ReactNode } from "react";
|
||||
import type { Metadata } from "next";
|
||||
import { specMetadata } from "@/lib/page-meta";
|
||||
import { ResourceSpecPage, type ResourceSpec } from "@/components/resource-spec-page";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "OpenMemory · LogicSRC",
|
||||
description:
|
||||
"OpenMemory is the memory block of an OpenServer offer: RAM in mebibytes, DDR generation and speed, ECC as three states, reserved or balloonable allocation, and a range that says how much more a buyer can add at checkout and for how much.",
|
||||
alternates: { canonical: "/openmemory" }
|
||||
};
|
||||
export const metadata: Metadata = specMetadata(
|
||||
"/openmemory",
|
||||
"OpenMemory is the memory block of an OpenServer offer: RAM in mebibytes, DDR generation and speed, ECC as three states, reserved or balloonable allocation, and a range that says how much more a buyer can add at checkout and for how much."
|
||||
);
|
||||
|
||||
const SPEC: ResourceSpec = {
|
||||
name: "OpenMemory",
|
||||
|
|
|
|||
|
|
@ -1,15 +1,14 @@
|
|||
import Link from "next/link";
|
||||
import type { ReactNode } from "react";
|
||||
import type { Metadata } from "next";
|
||||
import { specMetadata } from "@/lib/page-meta";
|
||||
import { SiteShell } from "@/components/site-shell";
|
||||
import { mono, pre, table, td, th } from "../openontology/ui";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "OpenObject · LogicSRC",
|
||||
description:
|
||||
"OpenObject is a bucket you can mount: keyed objects encrypted by their owner, placed on paid OpenDisk disks under a stated redundancy such as three replicas, verified every period, repaired when a disk fails, listed by prefix and read back by path. Reference store d1sks.com.",
|
||||
alternates: { canonical: "/openobject" }
|
||||
};
|
||||
export const metadata: Metadata = specMetadata(
|
||||
"/openobject",
|
||||
"OpenObject is a bucket you can mount: keyed objects encrypted by their owner, placed on paid OpenDisk disks under a stated redundancy such as three replicas, verified every period, repaired when a disk fails, listed by prefix and read back by path. Reference store d1sks.com."
|
||||
);
|
||||
|
||||
const DESCRIPTOR = `{
|
||||
"name": "d1sks.com",
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import Link from "next/link";
|
|||
import { notFound } from "next/navigation";
|
||||
import type { ReactNode } from "react";
|
||||
import type { Metadata } from "next";
|
||||
import { contentMetadata, notFoundMetadata } from "@/lib/page-meta";
|
||||
import { getService } from "@/lib/ontology-service";
|
||||
import { SiteShell } from "@/components/site-shell";
|
||||
import { Confidence, StatusBadge, formatObject, mono, table, td, th } from "../../../ui";
|
||||
|
|
@ -14,10 +15,12 @@ export async function generateMetadata({
|
|||
params: Promise<{ claimId: string }>;
|
||||
}): Promise<Metadata> {
|
||||
const { claimId } = await params;
|
||||
return {
|
||||
title: `${decodeURIComponent(claimId)} · OpenOntology · LogicSRC`,
|
||||
description: "A single OpenOntology claim, with the sources, evidence, and history behind it."
|
||||
};
|
||||
const id = decodeURIComponent(claimId);
|
||||
return contentMetadata({
|
||||
title: `Claim ${id} · OpenOntology`,
|
||||
description: `The OpenOntology claim ${id}, with the sources, evidence, and history behind it.`,
|
||||
path: `/openontology/explore/claim/${encodeURIComponent(id)}`
|
||||
});
|
||||
}
|
||||
|
||||
export default async function ClaimPage({
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import Link from "next/link";
|
|||
import { notFound } from "next/navigation";
|
||||
import type { ReactNode } from "react";
|
||||
import type { Metadata } from "next";
|
||||
import { contentMetadata, notFoundMetadata } from "@/lib/page-meta";
|
||||
import { getService } from "@/lib/ontology-service";
|
||||
import { SiteShell } from "@/components/site-shell";
|
||||
import { CLAIM_STATUS, Confidence, StatusBadge, formatObject, mono, table, td, th } from "../../../ui";
|
||||
|
|
@ -17,12 +18,15 @@ export async function generateMetadata({
|
|||
const id = decodeURIComponent(entityId);
|
||||
const state = await getService();
|
||||
const entity = state.engine?.store.getEntity(id);
|
||||
return {
|
||||
title: `${entity?.canonicalName ?? id} · OpenOntology · LogicSRC`,
|
||||
return contentMetadata({
|
||||
title: entity
|
||||
? `${entity.canonicalName} (${entity.type}) · OpenOntology`
|
||||
: `${id} · OpenOntology`,
|
||||
description: entity
|
||||
? `${entity.canonicalName} (${entity.type}) and the source-backed claims about it.`
|
||||
: "OpenOntology entity"
|
||||
};
|
||||
: `The OpenOntology entity ${id}, and the source-backed claims about it.`,
|
||||
path: `/openontology/explore/entity/${encodeURIComponent(id)}`
|
||||
});
|
||||
}
|
||||
|
||||
export default async function EntityPage({
|
||||
|
|
|
|||
|
|
@ -1,18 +1,14 @@
|
|||
import Link from "next/link";
|
||||
import type { ReactNode } from "react";
|
||||
import type { Metadata } from "next";
|
||||
import { pageMetadata } from "@/lib/page-meta";
|
||||
import { getService } from "@/lib/ontology-service";
|
||||
import { SiteShell } from "@/components/site-shell";
|
||||
import { CLAIM_STATUS, card, mono, table, th, td, StatusBadge } from "../ui";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Explore · OpenOntology · LogicSRC",
|
||||
description:
|
||||
"A read-only explorer over the OpenOntology example package: entity types, entities, claims with provenance, and the saved queries that answer real questions.",
|
||||
alternates: { canonical: "/openontology/explore" }
|
||||
};
|
||||
export const metadata: Metadata = pageMetadata("/openontology/explore");
|
||||
|
||||
export default async function ExplorePage(): Promise<ReactNode> {
|
||||
const state = await getService();
|
||||
|
|
|
|||
|
|
@ -1,14 +1,13 @@
|
|||
import Link from "next/link";
|
||||
import type { ReactNode } from "react";
|
||||
import type { Metadata } from "next";
|
||||
import { specMetadata } from "@/lib/page-meta";
|
||||
import { SiteShell } from "@/components/site-shell";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "OpenOntology · LogicSRC",
|
||||
description:
|
||||
"LogicSRC OpenOntology is an open contract for durable, source-backed domain knowledge shared by humans and AI agents: typed entities, claims with provenance and time, portable queries, and governed change sets.",
|
||||
alternates: { canonical: "/openontology" },
|
||||
};
|
||||
export const metadata: Metadata = specMetadata(
|
||||
"/openontology",
|
||||
"LogicSRC OpenOntology is an open contract for durable, source-backed domain knowledge shared by humans and AI agents: typed entities, claims with provenance and time, portable queries, and governed change sets."
|
||||
);
|
||||
|
||||
const card = {
|
||||
border: "1px solid #e3e6e0",
|
||||
|
|
|
|||
|
|
@ -1,15 +1,14 @@
|
|||
import Link from "next/link";
|
||||
import type { ReactNode } from "react";
|
||||
import type { Metadata } from "next";
|
||||
import { specMetadata } from "@/lib/page-meta";
|
||||
import { SiteShell } from "@/components/site-shell";
|
||||
import { card, mono, pre } from "../openontology/ui";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "OpenPRD · LogicSRC",
|
||||
description:
|
||||
"OpenPRD is a lightweight open standard for product requirements documents: a numbered, committed collection under prd/, one Markdown file each, with front-matter, ten fixed sections, and an enforced lifecycle.",
|
||||
alternates: { canonical: "/openprd" }
|
||||
};
|
||||
export const metadata: Metadata = specMetadata(
|
||||
"/openprd",
|
||||
"OpenPRD is a lightweight open standard for product requirements documents: a numbered, committed collection under prd/, one Markdown file each, with front-matter, ten fixed sections, and an enforced lifecycle."
|
||||
);
|
||||
|
||||
const SECTIONS: Array<[string, string]> = [
|
||||
["Problem", "The user or business problem, and why it matters now."],
|
||||
|
|
|
|||
|
|
@ -1,15 +1,14 @@
|
|||
import Link from "next/link";
|
||||
import type { ReactNode } from "react";
|
||||
import type { Metadata } from "next";
|
||||
import { specMetadata } from "@/lib/page-meta";
|
||||
import { SiteShell } from "@/components/site-shell";
|
||||
import { mono, pre, table, td, th } from "../openontology/ui";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "OpenProfile.md · LogicSRC",
|
||||
description:
|
||||
"OpenProfile.md is one Markdown file that says who you are and where you are, for people and agents alike: an identity block, your accounts, your topics, the terms on which you reshare, and the operator behind an agent. Served at /.well-known/openprofile.md, linked with rel=\"openprofile\", verified by linking back.",
|
||||
alternates: { canonical: "/openprofile" }
|
||||
};
|
||||
export const metadata: Metadata = specMetadata(
|
||||
"/openprofile",
|
||||
"OpenProfile.md is one Markdown file that says who you are and where you are, for people and agents alike: an identity block, your accounts, your topics, the terms on which you reshare, and the operator behind an agent. Served at /.well-known/openprofile.md, linked with rel=\"openprofile\", verified by linking back."
|
||||
);
|
||||
|
||||
const EXAMPLE = `# Ada Lovelace
|
||||
|
||||
|
|
|
|||
|
|
@ -1,15 +1,14 @@
|
|||
import Link from "next/link";
|
||||
import type { ReactNode } from "react";
|
||||
import type { Metadata } from "next";
|
||||
import { specMetadata } from "@/lib/page-meta";
|
||||
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" }
|
||||
};
|
||||
export const metadata: Metadata = specMetadata(
|
||||
"/openrecipe",
|
||||
"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."
|
||||
);
|
||||
|
||||
const EXAMPLE = `# Shakshuka
|
||||
|
||||
|
|
|
|||
|
|
@ -1,15 +1,14 @@
|
|||
import Link from "next/link";
|
||||
import type { ReactNode } from "react";
|
||||
import type { Metadata } from "next";
|
||||
import { specMetadata } from "@/lib/page-meta";
|
||||
import { SiteShell } from "@/components/site-shell";
|
||||
import { mono, pre, table, td, th } from "../openontology/ui";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "OpenSaaS · LogicSRC",
|
||||
description:
|
||||
"OpenSaaS is one file a subscription service serves at /.well-known/opensaas.json about how to deal with it: its plans, and how to subscribe, cancel, pause, change plan, stop the mail, export and delete, each as the page a person opens and the endpoint an agent calls with an OpenAccess scope. The way out, written down beside the way in.",
|
||||
alternates: { canonical: "/opensaas" }
|
||||
};
|
||||
export const metadata: Metadata = specMetadata(
|
||||
"/opensaas",
|
||||
"OpenSaaS is one file a subscription service serves at /.well-known/opensaas.json about how to deal with it: its plans, and how to subscribe, cancel, pause, change plan, stop the mail, export and delete, each as the page a person opens and the endpoint an agent calls with an OpenAccess scope. The way out, written down beside the way in."
|
||||
);
|
||||
|
||||
const DESCRIPTOR = `{
|
||||
"service": { "name": "NicheDB", "web": "https://nichedb.dev",
|
||||
|
|
|
|||
|
|
@ -1,15 +1,14 @@
|
|||
import Link from "next/link";
|
||||
import type { ReactNode } from "react";
|
||||
import type { Metadata } from "next";
|
||||
import { specMetadata } from "@/lib/page-meta";
|
||||
import { SiteShell } from "@/components/site-shell";
|
||||
import { mono, pre, table, td, th } from "../openontology/ui";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "OpenServer · LogicSRC",
|
||||
description:
|
||||
"OpenServer is one file a hosting provider serves about what it sells: every server, instance, rack, function and peer-market listing, with specs, price, location and stock, at /.well-known/openserver.json. Cloud, VPS, dedicated, bare metal, colocation, on-prem, managed, unmanaged, PaaS, serverless, storage, GPU, edge and p2p.",
|
||||
alternates: { canonical: "/openserver" }
|
||||
};
|
||||
export const metadata: Metadata = specMetadata(
|
||||
"/openserver",
|
||||
"OpenServer is one file a hosting provider serves about what it sells: every server, instance, rack, function and peer-market listing, with specs, price, location and stock, at /.well-known/openserver.json. Cloud, VPS, dedicated, bare metal, colocation, on-prem, managed, unmanaged, PaaS, serverless, storage, GPU, edge and p2p."
|
||||
);
|
||||
|
||||
const DESCRIPTOR = `{
|
||||
"provider": {
|
||||
|
|
|
|||
|
|
@ -1,15 +1,14 @@
|
|||
import Link from "next/link";
|
||||
import type { ReactNode } from "react";
|
||||
import type { Metadata } from "next";
|
||||
import { specMetadata } from "@/lib/page-meta";
|
||||
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" }
|
||||
};
|
||||
export const metadata: Metadata = specMetadata(
|
||||
"/opensite",
|
||||
"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."
|
||||
);
|
||||
|
||||
const RECORD = `{
|
||||
"opensite": "0.1",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import Link from "next/link";
|
||||
import { notFound } from "next/navigation";
|
||||
import type { Metadata } from "next";
|
||||
import { contentMetadata, notFoundMetadata } from "@/lib/page-meta";
|
||||
import { marked } from "marked";
|
||||
import { SiteShell } from "@/components/site-shell";
|
||||
import { sanitizeRenderedHtml } from "@/lib/html";
|
||||
|
|
@ -14,13 +15,15 @@ export function generateStaticParams() {
|
|||
export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }): Promise<Metadata> {
|
||||
const { slug } = await params;
|
||||
const source = readSkill(slug);
|
||||
if (!source) return { title: "Not found · OpenSkill" };
|
||||
if (!source) return notFoundMetadata("skill");
|
||||
const concept = summarizeSkill(source, slug);
|
||||
return {
|
||||
title: `${concept.name} · OpenSkill · LogicSRC`,
|
||||
return contentMetadata({
|
||||
title: `${concept.name}: An OpenSkill Record`,
|
||||
description: concept.description,
|
||||
alternates: { canonical: `/openskill/${slug}`, types: { "text/markdown": `/openskill/${slug}/openskill.md` } }
|
||||
};
|
||||
path: `/openskill/${slug}`,
|
||||
alternateTypes: { "text/markdown": `/openskill/${slug}/openskill.md` },
|
||||
type: "article"
|
||||
});
|
||||
}
|
||||
|
||||
export default async function SkillPage({ params }: { params: Promise<{ slug: string }> }) {
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
import Link from "next/link";
|
||||
import type { Metadata } from "next";
|
||||
import { specMetadata } from "@/lib/page-meta";
|
||||
import { SiteShell } from "@/components/site-shell";
|
||||
import { listSkills } from "@/lib/skills";
|
||||
import { pre, table, td, th } from "../openontology/ui";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "OpenSkill · Human skills and knowledge · LogicSRC",
|
||||
description: "Portable descriptions of human skills, knowledge and occupations, linked to ESCO and O*NET and carried in OpenProfile.",
|
||||
alternates: { canonical: "/openskill" }
|
||||
};
|
||||
export const metadata: Metadata = specMetadata(
|
||||
"/openskill",
|
||||
"Portable descriptions of human skills, knowledge and occupations, linked to ESCO and O*NET and carried in OpenProfile."
|
||||
);
|
||||
|
||||
export default function OpenSkillPage() {
|
||||
const concepts = listSkills();
|
||||
|
|
|
|||
|
|
@ -1,15 +1,14 @@
|
|||
import Link from "next/link";
|
||||
import type { ReactNode } from "react";
|
||||
import type { Metadata } from "next";
|
||||
import { specMetadata } from "@/lib/page-meta";
|
||||
import { SiteShell } from "@/components/site-shell";
|
||||
import { mono, pre, table, td, th } from "../openontology/ui";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "OpenSlice · LogicSRC",
|
||||
description:
|
||||
"OpenSlice is a container whose compute is rented from one market and whose disk is mounted from another: a host serves /.well-known/openslice.json, a slice is one signed file, the state lives in an OpenObject bucket, so the host is interchangeable. Reference marketplace slic3s.com.",
|
||||
alternates: { canonical: "/openslice" }
|
||||
};
|
||||
export const metadata: Metadata = specMetadata(
|
||||
"/openslice",
|
||||
"OpenSlice is a container whose compute is rented from one market and whose disk is mounted from another: a host serves /.well-known/openslice.json, a slice is one signed file, the state lives in an OpenObject bucket, so the host is interchangeable. Reference marketplace slic3s.com."
|
||||
);
|
||||
|
||||
const DESCRIPTOR = `{
|
||||
"name": "rig-7b, Helsinki",
|
||||
|
|
|
|||
|
|
@ -1,15 +1,14 @@
|
|||
import Link from "next/link";
|
||||
import type { ReactNode } from "react";
|
||||
import type { Metadata } from "next";
|
||||
import { specMetadata } from "@/lib/page-meta";
|
||||
import { SiteShell } from "@/components/site-shell";
|
||||
import { mono, pre, table, td, th } from "../openontology/ui";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "OpenSong · LogicSRC",
|
||||
description:
|
||||
"OpenSong is one plain-text file that is a song: TITLE, STYLE, EXCLUDE STYLES and LYRICS as labelled blocks that paste straight into a music generator, [Section - direction] tags in the lyrics, no Markdown. Kept beside the audio as the same name with .txt; ID3 tags are derived from it, never the reverse.",
|
||||
alternates: { canonical: "/opensong" }
|
||||
};
|
||||
export const metadata: Metadata = specMetadata(
|
||||
"/opensong",
|
||||
"OpenSong is one plain-text file that is a song: TITLE, STYLE, EXCLUDE STYLES and LYRICS as labelled blocks that paste straight into a music generator, [Section - direction] tags in the lyrics, no Markdown. Kept beside the audio as the same name with .txt; ID3 tags are derived from it, never the reverse."
|
||||
);
|
||||
|
||||
const EXAMPLE = `TITLE:
|
||||
Who Fired First
|
||||
|
|
|
|||
|
|
@ -1,15 +1,14 @@
|
|||
import Link from "next/link";
|
||||
import type { ReactNode } from "react";
|
||||
import type { Metadata } from "next";
|
||||
import { specMetadata } from "@/lib/page-meta";
|
||||
import { SiteShell } from "@/components/site-shell";
|
||||
import { mono, pre, table, td, th } from "../openontology/ui";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "OpenStack.md · LogicSRC",
|
||||
description:
|
||||
"OpenStack.md is one Markdown file that says what a piece of software is built on: languages, runtimes, every interface and what each is made of, data, services, tooling, hosting, the rules, and what it must never depend on. Read by a person in a minute, by an agent in one prompt, and inherited by the next project through Extends.",
|
||||
alternates: { canonical: "/openstack" }
|
||||
};
|
||||
export const metadata: Metadata = specMetadata(
|
||||
"/openstack",
|
||||
"OpenStack.md is one Markdown file that says what a piece of software is built on: languages, runtimes, every interface and what each is made of, data, services, tooling, hosting, the rules, and what it must never depend on. Read by a person in a minute, by an agent in one prompt, and inherited by the next project through Extends."
|
||||
);
|
||||
|
||||
const FILE = `# LogicSRC
|
||||
|
||||
|
|
|
|||
|
|
@ -1,14 +1,13 @@
|
|||
import type { ReactNode } from "react";
|
||||
import type { Metadata } from "next";
|
||||
import { specMetadata } from "@/lib/page-meta";
|
||||
import { SiteShell } from "@/components/site-shell";
|
||||
import { card, mono, pre, table, td, th } from "../openontology/ui";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "OpenSwarm · LogicSRC",
|
||||
description:
|
||||
"OpenSwarm is an open specification family for paid, encrypted, peer-to-peer distribution of files and media: BitTorrent plus payment plus encryption as extension messages, with audio, video, live streams and a replicated catalogue on top, and c0mpute.com nodes that seed, relay and index for money.",
|
||||
alternates: { canonical: "/openswarm" }
|
||||
};
|
||||
export const metadata: Metadata = specMetadata(
|
||||
"/openswarm",
|
||||
"OpenSwarm is an open specification family for paid, encrypted, peer-to-peer distribution of files and media: BitTorrent plus payment plus encryption as extension messages, with audio, video, live streams and a replicated catalogue on top, and c0mpute.com nodes that seed, relay and index for money."
|
||||
);
|
||||
|
||||
const PROTOCOLS: Array<[name: string, line: string]> = [
|
||||
["ipfile", "Paid, encrypted file swarms as BitTorrent extension messages"],
|
||||
|
|
|
|||
|
|
@ -1,15 +1,14 @@
|
|||
import Link from "next/link";
|
||||
import type { ReactNode } from "react";
|
||||
import type { Metadata } from "next";
|
||||
import { specMetadata } from "@/lib/page-meta";
|
||||
import { SiteShell } from "@/components/site-shell";
|
||||
import { mono, pre, table, td, th } from "../openontology/ui";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "OpenThreat · LogicSRC",
|
||||
description:
|
||||
"OpenThreat is one file a security tool serves about what it found in the open, at /.well-known/openthreat.json: findings in public repositories, attacks on the reporter's own infrastructure, indicators and advisories, with severity, rule, subject and status. Private subjects are never in it and secrets are never located while open.",
|
||||
alternates: { canonical: "/openthreat" }
|
||||
};
|
||||
export const metadata: Metadata = specMetadata(
|
||||
"/openthreat",
|
||||
"OpenThreat is one file a security tool serves about what it found in the open, at /.well-known/openthreat.json: findings in public repositories, attacks on the reporter's own infrastructure, indicators and advisories, with severity, rule, subject and status. Private subjects are never in it and secrets are never located while open."
|
||||
);
|
||||
|
||||
const DESCRIPTOR = `{
|
||||
"openthreat": "0.1",
|
||||
|
|
|
|||
|
|
@ -1,15 +1,14 @@
|
|||
import Link from "next/link";
|
||||
import type { ReactNode } from "react";
|
||||
import type { Metadata } from "next";
|
||||
import { specMetadata } from "@/lib/page-meta";
|
||||
import { SiteShell } from "@/components/site-shell";
|
||||
import { mono, pre, table, td, th } from "../openontology/ui";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "OpenWebring · LogicSRC",
|
||||
description:
|
||||
"OpenWebring is a webring that says who made it: one file a ring serves about its members at /.well-known/openwebring.json, one file a member may serve about itself, plain links between them, and a made_by declaration on every member, human, ai or both, so a reader can follow the human web or the machine web on purpose.",
|
||||
alternates: { canonical: "/openwebring" }
|
||||
};
|
||||
export const metadata: Metadata = specMetadata(
|
||||
"/openwebring",
|
||||
"OpenWebring is a webring that says who made it: one file a ring serves about its members at /.well-known/openwebring.json, one file a member may serve about itself, plain links between them, and a made_by declaration on every member, human, ai or both, so a reader can follow the human web or the machine web on purpose."
|
||||
);
|
||||
|
||||
const MEMBER = `{
|
||||
"openwebring": "0.1",
|
||||
|
|
|
|||
|
|
@ -1,15 +1,14 @@
|
|||
import Link from "next/link";
|
||||
import type { ReactNode } from "react";
|
||||
import type { Metadata } from "next";
|
||||
import { specMetadata } from "@/lib/page-meta";
|
||||
import { SiteShell } from "@/components/site-shell";
|
||||
import { mono, pre, table, td, th } from "../openontology/ui";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "OpenWiki · LogicSRC",
|
||||
description:
|
||||
"OpenWiki is a wiki that is a folder of Markdown files: [[Page]] links, a small front matter vocabulary, a descriptor at /.well-known/openwiki.json, an index of pages, every page and every revision readable as Markdown, and made_by on every revision, human, ai or both. Opens unchanged in Obsidian and Logseq.",
|
||||
alternates: { canonical: "/openwiki" }
|
||||
};
|
||||
export const metadata: Metadata = specMetadata(
|
||||
"/openwiki",
|
||||
"OpenWiki is a wiki that is a folder of Markdown files: [[Page]] links, a small front matter vocabulary, a descriptor at /.well-known/openwiki.json, an index of pages, every page and every revision readable as Markdown, and made_by on every revision, human, ai or both. Opens unchanged in Obsidian and Logseq."
|
||||
);
|
||||
|
||||
const PAGE = `---
|
||||
title: Going viral
|
||||
|
|
|
|||
|
|
@ -1,13 +1,9 @@
|
|||
import type { ReactNode } from "react";
|
||||
import type { Metadata } from "next";
|
||||
import { pageMetadata } from "@/lib/page-meta";
|
||||
import { SiteShell } from "@/components/site-shell";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Pricing · LogicSRC",
|
||||
description:
|
||||
"LogicSRC the open specification, schemas, SDKs, and CLI are free and open source. Implementation help is $400/hour/agent for accepted work, paid via CoinPay.",
|
||||
alternates: { canonical: "/pricing" },
|
||||
};
|
||||
export const metadata: Metadata = pageMetadata("/pricing");
|
||||
|
||||
const FAQ: Array<{ q: string; a: string }> = [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,13 +1,9 @@
|
|||
import type { ReactNode } from "react";
|
||||
import type { Metadata } from "next";
|
||||
import { pageMetadata } from "@/lib/page-meta";
|
||||
import { SiteShell } from "@/components/site-shell";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Privacy · LogicSRC",
|
||||
description:
|
||||
"What logicsrc.com collects and what it does not: privacy-friendly analytics, the Hire Us project form, the CoinPay sign-in cookie, and the boundary that keeps credential values off our servers.",
|
||||
alternates: { canonical: "/privacy" },
|
||||
};
|
||||
export const metadata: Metadata = pageMetadata("/privacy");
|
||||
|
||||
export default function PrivacyPage(): ReactNode {
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import Link from "next/link";
|
|||
import { notFound } from "next/navigation";
|
||||
import type { ReactNode } from "react";
|
||||
import type { Metadata } from "next";
|
||||
import { contentMetadata, notFoundMetadata } from "@/lib/page-meta";
|
||||
import { SiteShell } from "@/components/site-shell";
|
||||
import { SpecList } from "@/components/spec-list";
|
||||
import { FAMILIES, familyBySlug } from "@/lib/specs";
|
||||
|
|
@ -17,12 +18,12 @@ export async function generateMetadata({
|
|||
}): Promise<Metadata> {
|
||||
const { family: slug } = await params;
|
||||
const family = familyBySlug(slug);
|
||||
if (!family) return {};
|
||||
return {
|
||||
title: `${family.name} · Specs · LogicSRC`,
|
||||
if (!family) return notFoundMetadata("spec family");
|
||||
return contentMetadata({
|
||||
title: `${family.name}: LogicSRC Specs`,
|
||||
description: `${family.line}. ${family.specs.map((s) => s.name).join(", ")}.`,
|
||||
alternates: { canonical: `/specs/${family.slug}` }
|
||||
};
|
||||
path: `/specs/${family.slug}`
|
||||
});
|
||||
}
|
||||
|
||||
export default async function FamilyPage({
|
||||
|
|
|
|||
|
|
@ -1,15 +1,11 @@
|
|||
import Link from "next/link";
|
||||
import type { ReactNode } from "react";
|
||||
import type { Metadata } from "next";
|
||||
import { pageMetadata } from "@/lib/page-meta";
|
||||
import { SiteShell } from "@/components/site-shell";
|
||||
import { FAMILIES, familyTree } from "@/lib/specs";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Specs · LogicSRC",
|
||||
description:
|
||||
"Every LogicSRC specification in four families: people and agents, access and credentials, catalogs a site serves about itself, and agents and process. Each family drills down to its specs, each spec to an overview and the specification text.",
|
||||
alternates: { canonical: "/specs" }
|
||||
};
|
||||
export const metadata: Metadata = pageMetadata("/specs");
|
||||
|
||||
export default function SpecsIndex(): ReactNode {
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -1,13 +1,9 @@
|
|||
import type { ReactNode } from "react";
|
||||
import type { Metadata } from "next";
|
||||
import { pageMetadata } from "@/lib/page-meta";
|
||||
import { SiteShell } from "@/components/site-shell";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Terms · LogicSRC",
|
||||
description:
|
||||
"Terms of engagement for LogicSRC: the specification and tooling are open source and free; Profullstack implementation work is billed at $400/hour/agent against approved hours, with a 10-agent-hour minimum.",
|
||||
alternates: { canonical: "/terms" },
|
||||
};
|
||||
export const metadata: Metadata = pageMetadata("/terms");
|
||||
|
||||
export default function TermsPage(): ReactNode {
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { readFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
import { docSlugs } from "./specs";
|
||||
import { allSpecs, docSlugs } from "./specs";
|
||||
|
||||
// Repo-root docs/ (read at build time during static generation, so there is
|
||||
// no runtime filesystem dependency in the deployed image).
|
||||
|
|
@ -31,14 +31,83 @@ export function docTitle(markdown: string, slug: string): string {
|
|||
return h1 ? h1.replace(/^#\s+/, "").trim() : slug;
|
||||
}
|
||||
|
||||
/**
|
||||
* The guides have no registry `line`, so their subject is stated here.
|
||||
* The specs' subjects come from lib/specs.ts and are never duplicated.
|
||||
*/
|
||||
const GUIDE_SUBTITLES: Record<string, string> = {
|
||||
"data-model": "The tables behind the reference implementation",
|
||||
cli: "Command grammar, flags, and output conventions",
|
||||
tui: "Terminal rendering for the dashboard and Waiting Arcade",
|
||||
config: "Where config lives, and what each key does",
|
||||
permissions: "Explicit, scoped, auditable permission grants",
|
||||
plugins: "The plugin system, and the plugins that ship with it",
|
||||
"openontology-governance": "An agent proposes, a human applies",
|
||||
"openontology-interoperability": "Mapping to RDF, SHACL and PROV-O, and where it stops",
|
||||
};
|
||||
|
||||
/**
|
||||
* A one-line subject for a doc, used in its page title.
|
||||
*
|
||||
* Every spec's H1 is just its name ("# OpenStream"), so a title built from the
|
||||
* H1 alone reads "OpenStream · LogicSRC" and tells a reader on Reddit or in
|
||||
* search results nothing about what the spec is. For a spec this is the same
|
||||
* line the sidebar, /specs and the home page already show.
|
||||
*/
|
||||
export function docSubtitle(slug: string): string {
|
||||
const spec = allSpecs().find((s) => s.doc === `/docs/${slug}`);
|
||||
return spec?.line ?? GUIDE_SUBTITLES[slug] ?? "";
|
||||
}
|
||||
|
||||
function isHeaderLine(line: string): boolean {
|
||||
return /^(status|slug|version|editor|authors?|updated|date)\b\s*:/i.test(line.replace(/^\*\*/, ""));
|
||||
}
|
||||
|
||||
function isSkippableLine(line: string): boolean {
|
||||
return (
|
||||
line.startsWith("#") ||
|
||||
line.startsWith("```") ||
|
||||
line.startsWith(">") ||
|
||||
line.startsWith("|") ||
|
||||
line.startsWith("- ") ||
|
||||
line.startsWith("* ") ||
|
||||
isHeaderLine(line)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* The first real paragraph, joined across its wrapped lines.
|
||||
*
|
||||
* One line was not enough. The specs hard-wrap their prose, so the old
|
||||
* single-line excerpt cut "OpenSwarm is a LogicSRC OpenSpec family for paid,
|
||||
* encrypted, peer-to-peer" off mid-sentence, and the several specs that open
|
||||
* with "Status: 0.1 draft" were described to search engines as exactly that.
|
||||
* Some docs open with a lead-in ("Core tables:", "Primary command style:")
|
||||
* whose paragraph is a list or a code block; there is no sentence to quote, so
|
||||
* those return "" and the caller falls back to the doc's subject line.
|
||||
*/
|
||||
export function docExcerpt(markdown: string): string {
|
||||
const paragraph: string[] = [];
|
||||
for (const raw of markdown.split("\n")) {
|
||||
const line = raw.trim();
|
||||
if (line && !line.startsWith("#") && !line.startsWith("```") && !line.startsWith(">")) {
|
||||
return line.replace(/[*_`#>[\]()]/g, "").trim().slice(0, 160);
|
||||
if (!line || isSkippableLine(line)) {
|
||||
if (paragraph.length > 0) break;
|
||||
continue;
|
||||
}
|
||||
if (paragraph.length === 0 && line.endsWith(":")) return "";
|
||||
paragraph.push(line);
|
||||
if (paragraph.join(" ").length >= 200) break;
|
||||
}
|
||||
return "";
|
||||
|
||||
const text = paragraph
|
||||
.join(" ")
|
||||
.replace(/[*_`#>[\]()]/g, "")
|
||||
.replace(/\s+/g, " ")
|
||||
.trim();
|
||||
if (text.length < 40) return "";
|
||||
if (text.length <= 200) return text;
|
||||
const cut = text.slice(0, 200);
|
||||
return cut.slice(0, cut.lastIndexOf(" ")).replace(/[,;:]$/, "");
|
||||
}
|
||||
|
||||
export type DocSummary = { slug: DocSlug; title: string; excerpt: string };
|
||||
|
|
@ -48,7 +117,9 @@ export function listDocs(): DocSummary[] {
|
|||
for (const slug of DOC_SLUGS) {
|
||||
const md = readDoc(slug);
|
||||
if (!md) continue;
|
||||
out.push({ slug, title: docTitle(md, slug), excerpt: docExcerpt(md) });
|
||||
// A doc whose first paragraph is a list has no quotable sentence, so the
|
||||
// index card shows the doc's own subject line instead of nothing.
|
||||
out.push({ slug, title: docTitle(md, slug), excerpt: docExcerpt(md) || docSubtitle(slug) });
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
|
|
|||
240
apps/logicsrc-web/src/lib/page-meta.ts
Normal file
240
apps/logicsrc-web/src/lib/page-meta.ts
Normal file
|
|
@ -0,0 +1,240 @@
|
|||
import type { Metadata } from "next";
|
||||
import { allSpecs } from "./specs";
|
||||
|
||||
/**
|
||||
* Every page's title, description, canonical URL and social card, from one
|
||||
* builder.
|
||||
*
|
||||
* Why this exists: pages exported `{ title, description, alternates }` and
|
||||
* nothing else. Next.js shallow-merges metadata, so a page that never declares
|
||||
* `openGraph` inherits the root layout's object wholesale -- which meant
|
||||
* og:title, og:description and og:url were the HOMEPAGE's on every page of the
|
||||
* site. Reddit, Slack, X and Facebook read og:title, so every spec we posted
|
||||
* was announced as "LogicSRC - Open Coordination Standards for Humans & AI
|
||||
* Agents" no matter which spec the link pointed at. The <title> tag was correct
|
||||
* the whole time, which is why looking at the page never caught it.
|
||||
*
|
||||
* So no page declares raw metadata any more. Everything goes through
|
||||
* `specMetadata` (a spec landing page, titled from lib/specs.ts),
|
||||
* `pageMetadata` (the site's own pages, titled from PAGE_META below) or
|
||||
* `contentMetadata` (a doc, post, skill or ontology record, titled from the
|
||||
* content), and all three always emit openGraph and twitter titles.
|
||||
*/
|
||||
|
||||
export const SITE_URL = (process.env.PUBLIC_URL ?? "https://logicsrc.com").replace(/\/$/, "");
|
||||
export const SITE_NAME = "LogicSRC";
|
||||
|
||||
/**
|
||||
* The branded card from app/opengraph-image.tsx, named explicitly.
|
||||
*
|
||||
* Next.js only falls back to the file-convention image for routes that declare
|
||||
* no `openGraph`. Every page declares one now, and blog posts already did --
|
||||
* which is why live posts shipped with no og:image whatsoever. So the default
|
||||
* image is stated here rather than inherited.
|
||||
*/
|
||||
export const SITE_OG_IMAGE = `${SITE_URL}/opengraph-image`;
|
||||
|
||||
/** Long enough to say something, short enough to survive a link preview. */
|
||||
export const TITLE_MAX = 100;
|
||||
|
||||
export type PageMeta = { title: string; description: string };
|
||||
|
||||
/**
|
||||
* The site's own pages -- the ones that are not a spec and not content.
|
||||
* Keyed by path, so `contract/page-metadata.contract.test.ts` can require an
|
||||
* entry for every static route in sitemap.ts: a new page reaching the sitemap
|
||||
* with no title here fails the test instead of silently shipping the
|
||||
* homepage's social card.
|
||||
*/
|
||||
export const PAGE_META: Record<string, PageMeta> = {
|
||||
"/": {
|
||||
title: "LogicSRC — Open Coordination Standards for Humans & AI Agents",
|
||||
description:
|
||||
"Open schemas, primitives, and conventions for coordination between humans, AI agents, plugins, payment systems, and hosted products.",
|
||||
},
|
||||
"/specs": {
|
||||
title: "LogicSRC Specs: Four Families of Open Coordination Standards",
|
||||
description:
|
||||
"Every LogicSRC specification in four families: people and agents, access and credentials, catalogs a site serves about itself, and agents and process. Each family drills down to its specs, each spec to an overview and the specification text.",
|
||||
},
|
||||
"/docs": {
|
||||
title: "LogicSRC Docs: Specification Text, CLI, Config, and Plugins",
|
||||
description:
|
||||
"The specification text of every LogicSRC spec, grouped by family, and the guides: data model, CLI and TUI conventions, config, permissions, plugins.",
|
||||
},
|
||||
"/openontology/explore": {
|
||||
title: "Explore OpenOntology: Entities, Claims, and Saved Queries",
|
||||
description:
|
||||
"A read-only explorer over the OpenOntology example package: entity types, entities, claims with provenance, and the saved queries that answer real questions.",
|
||||
},
|
||||
"/hire-us": {
|
||||
title: "Hire Us: Implementation Help for LogicSRC Standards",
|
||||
description:
|
||||
"Implementation help for LogicSRC, OpenFleet, and Credential Sharing at $400/hour/agent for accepted work, paid via CoinPay.",
|
||||
},
|
||||
"/pricing": {
|
||||
title: "Pricing: Free Standard, $400 per Agent-Hour Implementation",
|
||||
description:
|
||||
"LogicSRC the open specification, schemas, SDKs, and CLI are free and open source. Implementation help is $400/hour/agent for accepted work, paid via CoinPay.",
|
||||
},
|
||||
"/blog": {
|
||||
title: "Blog: LogicSRC Project Notes and Release Posts",
|
||||
description:
|
||||
"Project notes for LogicSRC OpenSpec standards, AgentSwarm, AgentByte, SDKs, MCP, and reference implementations.",
|
||||
},
|
||||
"/about": {
|
||||
title: "About LogicSRC: The Profullstack Open-Specification Project",
|
||||
description:
|
||||
"LogicSRC is the Profullstack, Inc. open-specification project for coordination between humans and AI agents — schemas, primitives, and conventions that products implement without owning the standard.",
|
||||
},
|
||||
"/terms": {
|
||||
title: "Terms of Engagement",
|
||||
description:
|
||||
"Terms of engagement for LogicSRC: the specification and tooling are open source and free; Profullstack implementation work is billed at $400/hour/agent against approved hours, with a 10-agent-hour minimum.",
|
||||
},
|
||||
"/privacy": {
|
||||
title: "Privacy: What logicsrc.com Collects",
|
||||
description:
|
||||
"What logicsrc.com collects and what it does not: privacy-friendly analytics, the Hire Us project form, the CoinPay sign-in cookie, and the boundary that keeps credential values off our servers.",
|
||||
},
|
||||
};
|
||||
|
||||
/** The site suffix, skipped when the title already names the site. */
|
||||
export function siteTitle(title: string): string {
|
||||
return title.includes(SITE_NAME) ? title : `${title} · ${SITE_NAME}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* "<Name>: <what it is>", trimmed at a clause boundary to fit TITLE_MAX.
|
||||
*
|
||||
* The registry's `line` is a full sentence written for a list, and some run
|
||||
* well past what a link preview shows. Cutting at a comma or a colon keeps the
|
||||
* first clause, which is the part that identifies the spec.
|
||||
*/
|
||||
const DANGLING = new Set([
|
||||
"a", "an", "and", "as", "at", "by", "for", "from", "in", "of", "on", "or",
|
||||
"so", "that", "the", "to", "under", "with",
|
||||
]);
|
||||
|
||||
export function composeTitle(name: string, line: string): string {
|
||||
const room = TITLE_MAX - ` · ${SITE_NAME}`.length - `${name}: `.length;
|
||||
if (line.length <= room) return `${name}: ${line}`;
|
||||
const cut = line.slice(0, room);
|
||||
const boundary = Math.max(cut.lastIndexOf(", "), cut.lastIndexOf(": "), cut.lastIndexOf(" ("));
|
||||
let clause = boundary > 24 ? cut.slice(0, boundary) : cut.slice(0, cut.lastIndexOf(" "));
|
||||
// A cut at a word boundary can leave the sentence hanging on a preposition
|
||||
// ("...the way in and the way out of"), which reads worse than stopping one
|
||||
// word earlier.
|
||||
for (;;) {
|
||||
clause = clause.replace(/[\s,;:(]+$/, "");
|
||||
const words = clause.split(" ");
|
||||
if (words.length > 3 && DANGLING.has(words[words.length - 1].toLowerCase())) {
|
||||
clause = words.slice(0, -1).join(" ");
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
return `${name}: ${clause}`;
|
||||
}
|
||||
|
||||
function absolute(path: string): string {
|
||||
if (/^https?:\/\//.test(path)) return path;
|
||||
return path === "/" ? SITE_URL : `${SITE_URL}${path.startsWith("/") ? path : `/${path}`}`;
|
||||
}
|
||||
|
||||
type BuildInput = PageMeta & {
|
||||
/** Route path, used for og:url and (unless `canonical` overrides it) rel=canonical. */
|
||||
path: string;
|
||||
/** Absolute canonical for syndicated content whose original lives elsewhere. */
|
||||
canonical?: string;
|
||||
type?: "website" | "article";
|
||||
images?: string[];
|
||||
/** Extra alternates, e.g. the blog's RSS feed. */
|
||||
alternateTypes?: Record<string, string>;
|
||||
};
|
||||
|
||||
/**
|
||||
* The one place page metadata is assembled. openGraph and twitter are never
|
||||
* omitted, because omitting them inherits the layout's.
|
||||
*/
|
||||
export function buildMetadata(input: BuildInput): Metadata {
|
||||
const title = siteTitle(input.title);
|
||||
const canonical = input.canonical ?? input.path;
|
||||
const images = input.images?.length ? input.images : [SITE_OG_IMAGE];
|
||||
return {
|
||||
title,
|
||||
description: input.description,
|
||||
alternates: {
|
||||
canonical,
|
||||
...(input.alternateTypes ? { types: input.alternateTypes } : {}),
|
||||
},
|
||||
openGraph: {
|
||||
type: input.type ?? "website",
|
||||
siteName: SITE_NAME,
|
||||
url: absolute(canonical),
|
||||
title,
|
||||
description: input.description,
|
||||
images,
|
||||
},
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
title,
|
||||
description: input.description,
|
||||
images,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/** Metadata for one of the site's own pages, from its entry in `PAGE_META`. */
|
||||
export function pageMetadata(path: string, overrides: Partial<BuildInput> = {}): Metadata {
|
||||
const meta = PAGE_META[path];
|
||||
if (!meta) {
|
||||
throw new Error(
|
||||
`No PAGE_META entry for "${path}". Add one so the page gets its own social title.`
|
||||
);
|
||||
}
|
||||
return buildMetadata({ ...meta, path, ...overrides });
|
||||
}
|
||||
|
||||
/**
|
||||
* Metadata for a spec landing page.
|
||||
*
|
||||
* The name and the one-line subject come from lib/specs.ts, so a spec's title
|
||||
* cannot drift from the way it is described everywhere else on the site, and a
|
||||
* new spec page gets a real title the moment its registry entry exists. The
|
||||
* longer marketing description stays on the page that wrote it.
|
||||
*/
|
||||
export function specMetadata(path: string, description?: string): Metadata {
|
||||
const slug = path.replace(/^\//, "");
|
||||
const spec = allSpecs().find((s) => s.slug === slug || s.landing === path);
|
||||
if (!spec) {
|
||||
throw new Error(
|
||||
`No spec in lib/specs.ts for "${path}". Add its registry entry so the page is titled and listed.`
|
||||
);
|
||||
}
|
||||
return buildMetadata({
|
||||
title: composeTitle(spec.name, spec.line),
|
||||
description: description ?? spec.line,
|
||||
path,
|
||||
});
|
||||
}
|
||||
|
||||
/** The name and subject line a spec is titled with, for tests and listings. */
|
||||
export function specTitle(slug: string): string | null {
|
||||
const spec = allSpecs().find((s) => s.slug === slug);
|
||||
return spec ? composeTitle(spec.name, spec.line) : null;
|
||||
}
|
||||
|
||||
/** Metadata for a route whose title comes from content (a doc, post, skill, record). */
|
||||
export function contentMetadata(input: BuildInput): Metadata {
|
||||
return buildMetadata(input);
|
||||
}
|
||||
|
||||
/** The 404 case, so a missing slug still gets its own title rather than the site's. */
|
||||
export function notFoundMetadata(what: string): Metadata {
|
||||
return {
|
||||
title: `Not found · ${SITE_NAME}`,
|
||||
description: `This ${what} does not exist on ${SITE_NAME}.`,
|
||||
robots: { index: false, follow: true },
|
||||
};
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue