From c7c286007986904d4afcf39a9f5e8b7e6f8b5241 Mon Sep 17 00:00:00 2001 From: Gleb Galkin Date: Sat, 25 Apr 2026 00:10:06 +0200 Subject: [PATCH 1/5] feat(website): BrowserRouter with GitHub Pages 404 fallback HashRouter fragments aren't crawlable, so every route showed identical meta tags and sitemap entries. Switch to BrowserRouter for clean URLs; public/404.html stashes the intended pathname in sessionStorage and redirects to '/', which main.tsx replays before the router boots. .nojekyll disables Jekyll processing so dotfiles (e.g. .well-known) ship in the Pages artifact. --- website/public/.nojekyll | 0 website/public/404.html | 34 ++++++++++++++++++++++++++++++++++ website/src/main.tsx | 15 +++++++++++++-- 3 files changed, 47 insertions(+), 2 deletions(-) create mode 100644 website/public/.nojekyll create mode 100644 website/public/404.html diff --git a/website/public/.nojekyll b/website/public/.nojekyll new file mode 100644 index 0000000..e69de29 diff --git a/website/public/404.html b/website/public/404.html new file mode 100644 index 0000000..ccd20a7 --- /dev/null +++ b/website/public/404.html @@ -0,0 +1,34 @@ + + + + + + + KNDL — redirecting… + + + + +

+ You were headed somewhere specific. If you are not redirected automatically, go to + kndl.artdaw.com. +

+

+ Agents looking for machine-readable content: + /llms.txt · + /spec/SPECIFICATION.md · + /spec/kndl.ebnf. +

+ + diff --git a/website/src/main.tsx b/website/src/main.tsx index 10f524a..ded9718 100644 --- a/website/src/main.tsx +++ b/website/src/main.tsx @@ -1,6 +1,6 @@ import { StrictMode } from "react"; import { createRoot } from "react-dom/client"; -import { createHashRouter, RouterProvider } from "react-router"; +import { createBrowserRouter, RouterProvider } from "react-router"; import App from "./App"; import LandingPage from "./pages/LandingPage"; import SpecPage from "./pages/SpecPage"; @@ -10,7 +10,18 @@ import McpPage from "./pages/McpPage"; import ExplorerPage from "./pages/ExplorerPage"; import "./styles/tokens.css"; -const router = createHashRouter([ +// GitHub Pages SPA fallback: 404.html stashes the intended path in +// sessionStorage and redirects to '/'. Here we replay it before the +// router reads window.location. +const redirectedPath = sessionStorage.getItem("kndl:redirect"); +if (redirectedPath) { + sessionStorage.removeItem("kndl:redirect"); + if (redirectedPath !== window.location.pathname + window.location.search) { + window.history.replaceState(null, "", redirectedPath); + } +} + +const router = createBrowserRouter([ { path: "/", element: , From fd4967685e90483bda334738c3ebd936406b7bbf Mon Sep 17 00:00:00 2001 From: Gleb Galkin Date: Sat, 25 Apr 2026 00:12:12 +0200 Subject: [PATCH 2/5] feat(website): per-page SEO metadata with JSON-LD Add src/components/SEO.tsx, a small runtime component each page renders to update , description/keywords/robots meta, canonical, Open Graph, Twitter cards, and a page-specific JSON-LD block. Wire it into all six pages with route-specific copy (TechArticle for docs pages, SoftwareSourceCode for the landing page). Enrich index.html with a site-wide @graph JSON-LD (Organization, WebSite, SoftwareSourceCode, TechArticle), canonical, theme-color, font preconnect, and a <noscript> pointer to the machine-readable surfaces for non-JS crawlers. --- website/index.html | 101 ++++++++++++- website/src/components/SEO.tsx | 167 ++++++++++++++++++++++ website/src/pages/ExplorerPage.tsx | 14 ++ website/src/pages/LandingPage.tsx | 9 ++ website/src/pages/McpPage.tsx | 14 ++ website/src/pages/SpecFullPage.module.css | 7 + website/src/pages/SpecFullPage.tsx | 38 +++-- website/src/pages/SpecPage.tsx | 14 ++ website/src/pages/WorkflowPage.tsx | 14 ++ 9 files changed, 367 insertions(+), 11 deletions(-) create mode 100644 website/src/components/SEO.tsx diff --git a/website/index.html b/website/index.html index f0f95b1..4eb0181 100644 --- a/website/index.html +++ b/website/index.html @@ -3,14 +3,23 @@ <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> + <meta name="theme-color" content="#0b0f14" /> + + <!-- Default SEO — per-route pages override these via <SEO> at runtime. --> <title>KNDL — Knowledge Node Description Language - + + + + + + + - + @@ -19,16 +28,102 @@ - + + + + + + + + + + + + + +
+ + + + diff --git a/website/src/components/SEO.tsx b/website/src/components/SEO.tsx new file mode 100644 index 0000000..08447e7 --- /dev/null +++ b/website/src/components/SEO.tsx @@ -0,0 +1,167 @@ +import { useEffect } from "react"; + +const ORIGIN = "https://kndl.artdaw.com"; +const DEFAULT_IMAGE = `${ORIGIN}/kndl.png`; + +export interface SEOProps { + title: string; + description: string; + /** Path including leading slash, e.g. "/spec". Used for canonical + OG URLs. */ + path: string; + /** Absolute image URL; defaults to site-wide OG image. */ + image?: string; + /** Open Graph type — "website" for index pages, "article" for spec/docs. */ + type?: "website" | "article"; + /** Optional JSON-LD object (schema.org). Gets serialised into a script tag. */ + jsonLd?: Record | Record[]; + /** ISO 8601 date for schema.org dateModified. */ + dateModified?: string; + /** Optional comma-separated keywords. */ + keywords?: string; +} + +export function SEO({ + title, + description, + path, + image = DEFAULT_IMAGE, + type = "website", + jsonLd, + dateModified, + keywords, +}: SEOProps) { + useEffect(() => { + const url = ORIGIN + path; + + document.title = title; + document.documentElement.setAttribute("lang", "en"); + + setMeta("description", description, "name"); + if (keywords) setMeta("keywords", keywords, "name"); + setMeta("robots", "index,follow,max-image-preview:large", "name"); + + setMeta("og:title", title, "property"); + setMeta("og:description", description, "property"); + setMeta("og:url", url, "property"); + setMeta("og:type", type, "property"); + setMeta("og:image", image, "property"); + setMeta("og:site_name", "KNDL", "property"); + + setMeta("twitter:card", "summary_large_image", "name"); + setMeta("twitter:title", title, "name"); + setMeta("twitter:description", description, "name"); + setMeta("twitter:url", url, "name"); + setMeta("twitter:image", image, "name"); + + if (dateModified) { + setMeta("article:modified_time", dateModified, "property"); + } + + setLink("canonical", url); + setLink("alternate", "/llms.txt", "llm"); + + if (jsonLd) { + setJsonLd(jsonLd); + } else { + clearJsonLd(); + } + }, [title, description, path, image, type, dateModified, keywords, JSON.stringify(jsonLd)]); + + return null; +} + +function setMeta(key: string, content: string, attr: "name" | "property") { + const selector = `meta[${attr}="${key}"]`; + let tag = document.head.querySelector(selector); + if (!tag) { + tag = document.createElement("meta"); + tag.setAttribute(attr, key); + document.head.appendChild(tag); + } + tag.setAttribute("content", content); +} + +function setLink(rel: string, href: string, id?: string) { + const selector = id + ? `link[rel="${rel}"][data-id="${id}"]` + : `link[rel="${rel}"]:not([data-id])`; + let tag = document.head.querySelector(selector); + if (!tag) { + tag = document.createElement("link"); + tag.rel = rel; + if (id) tag.setAttribute("data-id", id); + document.head.appendChild(tag); + } + tag.href = href; +} + +function setJsonLd(data: Record | Record[]) { + let tag = document.head.querySelector( + 'script[type="application/ld+json"][data-seo="page"]', + ); + if (!tag) { + tag = document.createElement("script"); + tag.type = "application/ld+json"; + tag.setAttribute("data-seo", "page"); + document.head.appendChild(tag); + } + tag.textContent = JSON.stringify(data); +} + +function clearJsonLd() { + const tag = document.head.querySelector( + 'script[type="application/ld+json"][data-seo="page"]', + ); + if (tag) tag.remove(); +} + +// ── Schema.org helpers ───────────────────────────────────────────────────── + +export const ORG_SCHEMA = { + "@context": "https://schema.org", + "@type": "Organization", + name: "KNDL", + url: ORIGIN, + logo: DEFAULT_IMAGE, + sameAs: [ + "https://github.com/artdaw/KNDL", + ], +}; + +export function techArticleSchema(params: { + headline: string; + description: string; + path: string; + dateModified?: string; +}) { + return { + "@context": "https://schema.org", + "@type": "TechArticle", + headline: params.headline, + description: params.description, + url: ORIGIN + params.path, + mainEntityOfPage: ORIGIN + params.path, + dateModified: params.dateModified ?? new Date().toISOString().slice(0, 10), + inLanguage: "en", + publisher: { + "@type": "Organization", + name: "KNDL", + url: ORIGIN, + }, + image: DEFAULT_IMAGE, + }; +} + +export function softwareSourceCodeSchema() { + return { + "@context": "https://schema.org", + "@type": "SoftwareSourceCode", + name: "KNDL — Knowledge Node Description Language", + codeRepository: "https://github.com/artdaw/KNDL", + programmingLanguage: "KNDL", + url: ORIGIN, + description: + "A graph-based knowledge representation language for AI agents — typed nodes, confidence scores, temporal decay, cryptographic provenance, and native intent/process blocks.", + license: "https://opensource.org/licenses/MIT", + }; +} diff --git a/website/src/pages/ExplorerPage.tsx b/website/src/pages/ExplorerPage.tsx index 3a33eb0..acfa1c7 100644 --- a/website/src/pages/ExplorerPage.tsx +++ b/website/src/pages/ExplorerPage.tsx @@ -1,6 +1,7 @@ import { useState, useEffect, useRef, useCallback } from "react"; import { parseKNDL, typeColor, GraphData, GraphNodeData } from "../utils/kndlParser"; import { useForceLayout, Position } from "../hooks/useForceLayout"; +import { SEO, techArticleSchema } from "../components/SEO"; import styles from "./ExplorerPage.module.css"; // ── Sample KNDL ─────────────────────────────────────────────────────────────── @@ -273,6 +274,19 @@ export default function ExplorerPage() { return (
+ {/* Toolbar */}
diff --git a/website/src/pages/LandingPage.tsx b/website/src/pages/LandingPage.tsx index f15bb48..703de6d 100644 --- a/website/src/pages/LandingPage.tsx +++ b/website/src/pages/LandingPage.tsx @@ -1,5 +1,6 @@ import { Link } from "react-router"; import CodeBlock from "../components/CodeBlock"; +import { SEO, softwareSourceCodeSchema } from "../components/SEO"; import styles from "./LandingPage.module.css"; const HERO_EXAMPLE = `node @sensor_t001 :: Temperature { @@ -54,6 +55,14 @@ const FEATURES = [ export default function LandingPage() { return (
+ {/* Hero */}
diff --git a/website/src/pages/McpPage.tsx b/website/src/pages/McpPage.tsx index b72e7ce..b48867d 100644 --- a/website/src/pages/McpPage.tsx +++ b/website/src/pages/McpPage.tsx @@ -1,4 +1,5 @@ import CodeBlock from "../components/CodeBlock"; +import { SEO, techArticleSchema } from "../components/SEO"; import styles from "./McpPage.module.css"; const TOOLS = [ @@ -67,6 +68,19 @@ confidence to 0.6 due to sensor uncertainty"`; export default function McpPage() { return (
+
{/* Header */} diff --git a/website/src/pages/SpecFullPage.module.css b/website/src/pages/SpecFullPage.module.css index 5eb358b..1db596d 100644 --- a/website/src/pages/SpecFullPage.module.css +++ b/website/src/pages/SpecFullPage.module.css @@ -56,6 +56,13 @@ color: var(--text-dim); } +.headerActions { + display: flex; + align-items: center; + gap: 18px; + flex-wrap: wrap; +} + .rawLink { font-family: var(--font-mono); font-size: 12px; diff --git a/website/src/pages/SpecFullPage.tsx b/website/src/pages/SpecFullPage.tsx index 7268331..c15a1e2 100644 --- a/website/src/pages/SpecFullPage.tsx +++ b/website/src/pages/SpecFullPage.tsx @@ -11,6 +11,7 @@ import { BlockRenderer, } from "../utils/mdRenderer"; import type { TocEntry } from "../utils/mdRenderer"; +import { SEO, techArticleSchema } from "../components/SEO"; import styles from "./SpecFullPage.module.css"; // ── Parse once at module level ──────────────────────────────────────────────── @@ -90,6 +91,21 @@ export default function SpecFullPage() { return (
+ {/* Page header */}
@@ -98,14 +114,20 @@ export default function SpecFullPage() {

Language Specification

v1.0.0 · April 2026
- - View raw ↗ - +
diff --git a/website/src/pages/SpecPage.tsx b/website/src/pages/SpecPage.tsx index 5757eb2..99a5dba 100644 --- a/website/src/pages/SpecPage.tsx +++ b/website/src/pages/SpecPage.tsx @@ -6,6 +6,7 @@ import { useState } from "react"; import { Link } from "react-router"; import { highlightKNDL } from "../components/CodeBlock"; +import { SEO, techArticleSchema } from "../components/SEO"; import styles from "./SpecPage.module.css"; // ── Inline code block ───────────────────────────────────────────────────────── @@ -730,6 +731,19 @@ export default function SpecPage() { const [activeDomain, setActiveDomain] = useState("iot"); return (
+ {/* Hero */}
diff --git a/website/src/pages/WorkflowPage.tsx b/website/src/pages/WorkflowPage.tsx index 4d99e1e..654fd4e 100644 --- a/website/src/pages/WorkflowPage.tsx +++ b/website/src/pages/WorkflowPage.tsx @@ -6,6 +6,7 @@ import { useState, useEffect } from "react"; import styles from "./WorkflowPage.module.css"; import { highlightKNDL } from "../components/CodeBlock"; +import { SEO, techArticleSchema } from "../components/SEO"; // ── Integration architecture layers (rendered per-stage with one highlighted) ─ @@ -421,6 +422,19 @@ export default function WorkflowPage() { return (
+
{/* Header */}
From c841b6d76ba864890d9f1b4bf236118992ce1974 Mon Sep 17 00:00:00 2001 From: Gleb Galkin Date: Sat, 25 Apr 2026 00:12:47 +0200 Subject: [PATCH 3/5] feat(website): expose spec, grammar, llms.txt, examples to AI agents MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a Vite plugin that mirrors spec/SPECIFICATION.md and spec/grammar/kndl.ebnf from the repo root into /spec/* on both dev and build, and regenerates /llms-full.txt (spec + EBNF + example index) at build time so agents can slurp everything in one request. Ship the discovery stack under public/: - /llms.txt — concise index in the llmstxt.org format - /robots.txt — explicit allows for GPTBot, ClaudeBot, PerplexityBot, Google-Extended, CCBot, Applebot-Extended, Meta-ExternalAgent, et al. - /sitemap.xml — all six SPA routes plus the raw machine-readable URLs - /.well-known/security.txt — security contact per securitytxt.org - /examples/*.kndl — eight curated snippets (basic-building, intent- overheat, process-shipment, query-aggregation, healthcare-observation, fintech-transaction, robotics-pose, logistics-trace) plus index.md and a standalone index.html so /examples/ returns 200 The Vite plugin needs node types, so add @types/node and set tsconfig.node.json "types": ["node"]. --- website/package.json | 1 + website/pnpm-lock.yaml | 41 +++-- website/public/.well-known/security.txt | 7 + website/public/examples/basic-building.kndl | 26 +++ .../public/examples/fintech-transaction.kndl | 41 +++++ .../examples/healthcare-observation.kndl | 55 +++++++ website/public/examples/index.html | 151 ++++++++++++++++++ website/public/examples/index.md | 18 +++ website/public/examples/intent-overheat.kndl | 44 +++++ website/public/examples/logistics-trace.kndl | 41 +++++ website/public/examples/process-shipment.kndl | 43 +++++ .../public/examples/query-aggregation.kndl | 31 ++++ website/public/examples/robotics-pose.kndl | 48 ++++++ website/public/llms.txt | 66 ++++++++ website/public/robots.txt | 63 ++++++++ website/public/sitemap.xml | 69 ++++++++ website/tsconfig.node.json | 3 +- website/vite.config.ts | 107 ++++++++++++- 18 files changed, 840 insertions(+), 15 deletions(-) create mode 100644 website/public/.well-known/security.txt create mode 100644 website/public/examples/basic-building.kndl create mode 100644 website/public/examples/fintech-transaction.kndl create mode 100644 website/public/examples/healthcare-observation.kndl create mode 100644 website/public/examples/index.html create mode 100644 website/public/examples/index.md create mode 100644 website/public/examples/intent-overheat.kndl create mode 100644 website/public/examples/logistics-trace.kndl create mode 100644 website/public/examples/process-shipment.kndl create mode 100644 website/public/examples/query-aggregation.kndl create mode 100644 website/public/examples/robotics-pose.kndl create mode 100644 website/public/llms.txt create mode 100644 website/public/robots.txt create mode 100644 website/public/sitemap.xml diff --git a/website/package.json b/website/package.json index a23ef43..01af6bd 100644 --- a/website/package.json +++ b/website/package.json @@ -21,6 +21,7 @@ "@testing-library/jest-dom": "^6.4.6", "@testing-library/react": "^16.0.0", "@testing-library/user-event": "^14.5.2", + "@types/node": "^25.6.0", "@types/react": "19.2.14", "@types/react-dom": "19.2.3", "@vitejs/plugin-react": "6.0.1", diff --git a/website/pnpm-lock.yaml b/website/pnpm-lock.yaml index 0b76386..c8e9194 100644 --- a/website/pnpm-lock.yaml +++ b/website/pnpm-lock.yaml @@ -30,6 +30,9 @@ importers: '@testing-library/user-event': specifier: ^14.5.2 version: 14.6.1(@testing-library/dom@10.4.1) + '@types/node': + specifier: ^25.6.0 + version: 25.6.0 '@types/react': specifier: 19.2.14 version: 19.2.14 @@ -38,7 +41,7 @@ importers: version: 19.2.3(@types/react@19.2.14) '@vitejs/plugin-react': specifier: 6.0.1 - version: 6.0.1(vite@8.0.8) + version: 6.0.1(vite@8.0.8(@types/node@25.6.0)) '@vitest/ui': specifier: 4.1.4 version: 4.1.4(vitest@4.1.4) @@ -50,10 +53,10 @@ importers: version: 6.0.2 vite: specifier: 8.0.8 - version: 8.0.8 + version: 8.0.8(@types/node@25.6.0) vitest: specifier: 4.1.4 - version: 4.1.4(@vitest/ui@4.1.4)(jsdom@26.1.0)(vite@8.0.8) + version: 4.1.4(@types/node@25.6.0)(@vitest/ui@4.1.4)(jsdom@26.1.0)(vite@8.0.8(@types/node@25.6.0)) packages: @@ -275,6 +278,9 @@ packages: '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + '@types/node@25.6.0': + resolution: {integrity: sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==} + '@types/react-dom@19.2.3': resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} peerDependencies: @@ -717,6 +723,9 @@ packages: engines: {node: '>=14.17'} hasBin: true + undici-types@7.19.2: + resolution: {integrity: sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==} + vite@8.0.8: resolution: {integrity: sha512-dbU7/iLVa8KZALJyLOBOQ88nOXtNG8vxKuOT4I2mD+Ya70KPceF4IAmDsmU0h1Qsn5bPrvsY9HJstCRh3hG6Uw==} engines: {node: ^20.19.0 || >=22.12.0} @@ -1021,6 +1030,10 @@ snapshots: '@types/estree@1.0.8': {} + '@types/node@25.6.0': + dependencies: + undici-types: 7.19.2 + '@types/react-dom@19.2.3(@types/react@19.2.14)': dependencies: '@types/react': 19.2.14 @@ -1029,10 +1042,10 @@ snapshots: dependencies: csstype: 3.2.3 - '@vitejs/plugin-react@6.0.1(vite@8.0.8)': + '@vitejs/plugin-react@6.0.1(vite@8.0.8(@types/node@25.6.0))': dependencies: '@rolldown/pluginutils': 1.0.0-rc.7 - vite: 8.0.8 + vite: 8.0.8(@types/node@25.6.0) '@vitest/expect@4.1.4': dependencies: @@ -1043,13 +1056,13 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.1.0 - '@vitest/mocker@4.1.4(vite@8.0.8)': + '@vitest/mocker@4.1.4(vite@8.0.8(@types/node@25.6.0))': dependencies: '@vitest/spy': 4.1.4 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 8.0.8 + vite: 8.0.8(@types/node@25.6.0) '@vitest/pretty-format@4.1.4': dependencies: @@ -1078,7 +1091,7 @@ snapshots: sirv: 3.0.2 tinyglobby: 0.2.16 tinyrainbow: 3.1.0 - vitest: 4.1.4(@vitest/ui@4.1.4)(jsdom@26.1.0)(vite@8.0.8) + vitest: 4.1.4(@types/node@25.6.0)(@vitest/ui@4.1.4)(jsdom@26.1.0)(vite@8.0.8(@types/node@25.6.0)) '@vitest/utils@4.1.4': dependencies: @@ -1410,7 +1423,9 @@ snapshots: typescript@6.0.2: {} - vite@8.0.8: + undici-types@7.19.2: {} + + vite@8.0.8(@types/node@25.6.0): dependencies: lightningcss: 1.32.0 picomatch: 4.0.4 @@ -1418,12 +1433,13 @@ snapshots: rolldown: 1.0.0-rc.15 tinyglobby: 0.2.16 optionalDependencies: + '@types/node': 25.6.0 fsevents: 2.3.3 - vitest@4.1.4(@vitest/ui@4.1.4)(jsdom@26.1.0)(vite@8.0.8): + vitest@4.1.4(@types/node@25.6.0)(@vitest/ui@4.1.4)(jsdom@26.1.0)(vite@8.0.8(@types/node@25.6.0)): dependencies: '@vitest/expect': 4.1.4 - '@vitest/mocker': 4.1.4(vite@8.0.8) + '@vitest/mocker': 4.1.4(vite@8.0.8(@types/node@25.6.0)) '@vitest/pretty-format': 4.1.4 '@vitest/runner': 4.1.4 '@vitest/snapshot': 4.1.4 @@ -1440,9 +1456,10 @@ snapshots: tinyexec: 1.1.1 tinyglobby: 0.2.16 tinyrainbow: 3.1.0 - vite: 8.0.8 + vite: 8.0.8(@types/node@25.6.0) why-is-node-running: 2.3.0 optionalDependencies: + '@types/node': 25.6.0 '@vitest/ui': 4.1.4(vitest@4.1.4) jsdom: 26.1.0 transitivePeerDependencies: diff --git a/website/public/.well-known/security.txt b/website/public/.well-known/security.txt new file mode 100644 index 0000000..0c51c2f --- /dev/null +++ b/website/public/.well-known/security.txt @@ -0,0 +1,7 @@ +# Security contact for the KNDL project. +# See https://securitytxt.org/ for the format. + +Contact: mailto:me@artdaw.com +Expires: 2027-04-24T00:00:00Z +Preferred-Languages: en +Canonical: https://kndl.artdaw.com/.well-known/security.txt diff --git a/website/public/examples/basic-building.kndl b/website/public/examples/basic-building.kndl new file mode 100644 index 0000000..fe23679 --- /dev/null +++ b/website/public/examples/basic-building.kndl @@ -0,0 +1,26 @@ +// basic-building.kndl — sensor reading in a building, with decay. +// +// Demonstrates: node declaration, inline edge, ~confidence, ~source, +// ~valid (temporal range), ~decay (confidence degradation over time). + +node @building_7 :: Building { + name = "Klingelhöferstraße 7" + city = "Berlin" +} + +node @floor_3 :: Floor { + number = 3 + ~source "system://digital-twin" +} + +edge @floor_3 -[located_in]-> @building_7 + +node @sensor_t001 :: Temperature { + value = 22.5 °C + location -> @floor_3 + ~confidence 0.94 + ~source "sensor://bldg-7/floor-3/t-001" + ~valid 2026-04-10T14:00Z .. 2026-04-10T14:05Z + ~recorded 2026-04-10T14:05:03Z + ~decay 0.95 / 1h +} diff --git a/website/public/examples/fintech-transaction.kndl b/website/public/examples/fintech-transaction.kndl new file mode 100644 index 0000000..9ea8bef --- /dev/null +++ b/website/public/examples/fintech-transaction.kndl @@ -0,0 +1,41 @@ +// fintech-transaction.kndl — double-entry transaction with a signed source. +// +// Demonstrates: Money literals (Decimal + ISO 4217), balanced debits/credits +// as edges, ~signature for cryptographic provenance, ~tenant isolation. + +import { Account, Transaction } from "kndl://std/fin" + +context @tenant_acme { + ~tenant "acme" + ~access { read: ["role:acme-ops"], write: ["role:acme-admins"] } + + node @acct_cash :: Account { + name = "Operating Cash" + currency = "USD" + } + + node @acct_revenue :: Account { + name = "SaaS Revenue" + currency = "USD" + } + + // Transaction itself is a node; the two `posting` edges are the legs. + node @txn_0001 :: Transaction { + memo = "Invoice #A-1984 paid" + posted_at = 2026-04-22T14:05:00Z + ~source "db://erp/journal/0001" + ~signature { + alg = "ed25519" + key = "did:web:acme.example#sign-1" + sig = b"u8f+2lEq…==" + } + } + + // Debit 1200.00 USD to cash, credit 1200.00 USD to revenue. + edge @txn_0001 -[debits]-> @acct_cash { + amount = 1200.00d USD + } + edge @txn_0001 -[credits]-> @acct_revenue { + amount = 1200.00d USD + } +} diff --git a/website/public/examples/healthcare-observation.kndl b/website/public/examples/healthcare-observation.kndl new file mode 100644 index 0000000..7cead98 --- /dev/null +++ b/website/public/examples/healthcare-observation.kndl @@ -0,0 +1,55 @@ +// healthcare-observation.kndl — clinical observation with FHIR alignment. +// +// Demonstrates: Code for standard terminologies, bitemporal +// (~valid / ~observed / ~recorded), ~negated (strong negation under +// open-world assumption), ~classification for PHI, ~consent scoping. + +import { Patient, Observation, Condition } from "kndl://std/fhir" + +node @pat_001 :: Patient { + mrn = "MRN-42193" + ~classification "PHI" + ~consent -> @consent_0001 +} + +node @consent_0001 :: Consent { + scope = "treatment" + granted = 2026-03-12 +} + +// Positive observation: SpO2 reading from a calibrated device. +node @obs_4821 :: Observation { + subject -> @pat_001 + code = Code<"LOINC">("59408-5") // SpO2 in arterial blood + value = 96.0 + unit = "%" + ~source "fhir://hospital/Observation/4821" + ~observed 2026-04-20T09:14Z + ~recorded 2026-04-20T09:14:03Z + ~valid 2026-04-20T09:14Z .. 2026-04-20T09:14Z + ~confidence 0.98 + ~classification "PHI" +} + +// Strong negation: patient has *no* history of diabetes. +// Absence of this node would NOT mean the same thing — open-world. +node @pat_001.hx_diabetes :: Condition { + subject -> @pat_001 + code = Code<"ICD-10">("E11") + ~negated true + ~confidence 0.95 + ~source "user://dr-wong" + ~recorded 2026-04-20T09:10Z + ~classification "PHI" +} + +// Differential diagnosis — categorical uncertainty. +node @dx_working :: Condition { + subject -> @pat_001 + ~source "agent://clinical-reasoner" + ~uncertainty categorical { + "J45.9": 0.60, // asthma + "J44.9": 0.30, // COPD + "R05.9": 0.10 // cough, unspecified + } +} diff --git a/website/public/examples/index.html b/website/public/examples/index.html new file mode 100644 index 0000000..07fad03 --- /dev/null +++ b/website/public/examples/index.html @@ -0,0 +1,151 @@ + + + + + + + KNDL Examples — curated .kndl snippets + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/website/public/examples/index.md b/website/public/examples/index.md new file mode 100644 index 0000000..eff7290 --- /dev/null +++ b/website/public/examples/index.md @@ -0,0 +1,18 @@ +# KNDL Examples + +Curated `.kndl` snippets. Each file is self-contained and parses against the +v1.0 grammar (`/spec/kndl.ebnf`). Use them as starting points, test fixtures, +or tutorial material. + +| File | Demonstrates | +|------|--------------| +| [`basic-building.kndl`](basic-building.kndl) | Node declaration, typed edges, `~confidence`, `~source`, `~valid`, `~decay`. | +| [`intent-overheat.kndl`](intent-overheat.kndl) | Reactive intent with a query trigger and multiple `emit` actions. | +| [`process-shipment.kndl`](process-shipment.kndl) | Stateful process with five states, transitions, and a `compensate` block. | +| [`query-aggregation.kndl`](query-aggregation.kndl) | Multi-hop path pattern, `group by`, aggregation functions. | +| [`healthcare-observation.kndl`](healthcare-observation.kndl) | `Code` for SNOMED/LOINC, bitemporal annotations, `~negated`, `~classification`. | +| [`fintech-transaction.kndl`](fintech-transaction.kndl) | `Money` literals, double-entry via balanced edges, `~signature`. | +| [`robotics-pose.kndl`](robotics-pose.kndl) | `Frame`, `Pose`, Gaussian `~uncertainty`. | +| [`logistics-trace.kndl`](logistics-trace.kndl) | GTIN identifiers, multi-hop `ships_to*` path, chain-of-custody signatures. | + +Everything here is MIT-licensed, same as the rest of the KNDL project. diff --git a/website/public/examples/intent-overheat.kndl b/website/public/examples/intent-overheat.kndl new file mode 100644 index 0000000..0627f5c --- /dev/null +++ b/website/public/examples/intent-overheat.kndl @@ -0,0 +1,44 @@ +// intent-overheat.kndl — reactive rule that emits an alert and work order +// when a high-confidence temperature exceeds a threshold. +// +// Demonstrates: intent declaration, query trigger, multiple emits, +// ~priority, ~cooldown. + +import { Temperature, Alert, WorkOrder, Notification } from "kndl://std/iot" + +intent @overheat_response :: AutoAction { + trigger = query { + match ?t :: Temperature + -[located_in]-> ?zone :: Zone + where + ?t.value > 30 °C + && ?t.~confidence > 0.8 + && ?t.~valid overlaps now + && ?zone.occupied == true + } + + do { + emit node :: Alert { + severity = "critical" + message = "Overheat detected" + related -> ?t + ~source "agent://claude-sonnet-4.6" + } + + emit node :: WorkOrder { + title = "HVAC review" + assignee -> @facilities_team + related -> ?zone + ~source "agent://claude-sonnet-4.6" + } + + emit node :: Notification { + channel = "slack://facilities" + message = "Auto-adjusted HVAC, see WO-{uuid()}" + ~priority 0.7 + } + } + + ~priority 0.9 + ~cooldown 15m +} diff --git a/website/public/examples/logistics-trace.kndl b/website/public/examples/logistics-trace.kndl new file mode 100644 index 0000000..7a4d382 --- /dev/null +++ b/website/public/examples/logistics-trace.kndl @@ -0,0 +1,41 @@ +// logistics-trace.kndl — package trace across multiple hubs. +// +// Demonstrates: GTIN source identifiers, multi-hop path queries, per-edge +// timestamps, chain-of-custody signatures via ~attestation. + +node @pkg_4821 :: Package { + gtin = "gtin:04012345678901" + sku = "KNDL-BOOK-001" + ~source "db://wms/packages/4821" +} + +node @hub_frankfurt :: Hub { code = "FRA" } +node @hub_singapore :: Hub { code = "SIN" } +node @hub_tokyo :: Hub { code = "NRT" } + +// Each edge has its own timestamp and signature of the scanning agent. +edge @pkg_4821 -[ships_to]-> @hub_frankfurt { + scanned_at = 2026-04-18T08:14:00Z + ~attestation -> @scan_fra_4821 +} +edge @pkg_4821 -[ships_to]-> @hub_singapore { + scanned_at = 2026-04-19T22:07:00Z + ~attestation -> @scan_sin_4821 +} +edge @pkg_4821 -[ships_to]-> @hub_tokyo { + scanned_at = 2026-04-20T11:52:00Z + ~attestation -> @scan_nrt_4821 +} + +node @scan_fra_4821 :: Attestation { + issuer = "did:web:logistics.example#fra-scanner" + claim = "scan:origin" + evidence = b"u8f+2lEq…==" +} +// (other scan attestations elided for brevity) + +// Example query: reconstruct the path for any package. +query trace { + match ?p = @pkg_4821 -[ships_to*]-> ?dest :: Hub + return { path: ?p, hops: len(?p), destination: ?dest } +} diff --git a/website/public/examples/process-shipment.kndl b/website/public/examples/process-shipment.kndl new file mode 100644 index 0000000..44da3cf --- /dev/null +++ b/website/public/examples/process-shipment.kndl @@ -0,0 +1,43 @@ +// process-shipment.kndl — stateful workflow for a shipment lifecycle. +// +// Demonstrates: process declaration, states, transitions with guard +// (`where`), `do` side effects, and `compensate` rollback on reversal. + +import { Alert, Workflow } from "kndl://std/agents" + +process @shipment_sm :: Workflow { + state picked + state packed + state shipped + state delivered + state lost { ~priority 1.0 } + + on pack_complete in picked -> packed + do { + emit update @shipment { packed_at = now() } + } + + on scan_at_dock in packed -> shipped + where ?event.location == "dock" + do { + emit update @shipment { shipped_at = now() } + } + + on delivery_scan in shipped -> delivered + compensate { + // Runs if a later event reverses delivery (e.g. recall). + emit node :: Alert { + severity = "warn" + message = "delivery rollback — shipment returned" + } + } + + on no_contact_48h in shipped -> lost + where elapsed(?shipment.shipped_at) > 48h + do { + emit node :: Alert { + severity = "critical" + message = "shipment missing for 48h" + } + } +} diff --git a/website/public/examples/query-aggregation.kndl b/website/public/examples/query-aggregation.kndl new file mode 100644 index 0000000..8c2f7c3 --- /dev/null +++ b/website/public/examples/query-aggregation.kndl @@ -0,0 +1,31 @@ +// query-aggregation.kndl — multi-hop paths + group by + aggregation. +// +// Demonstrates: path patterns with repetition, optional matches, group +// by clause (distinct from aggregation functions), `with edges N`. + +query campus_power_by_site_day { + // 1-to-5 `contains` hops from a campus down to any meter. + match ?m :: PowerMeasurement + <-[contains*1..5]- @campus + + match ?m -[at]-> ?site :: Site + + optional match ?fault :: SystemFault + -[affects]-> ?site + where ?fault.~valid overlaps now + + where + ?m.~confidence > 0.8 + && ?m.~observed overlaps 2026-Q1 + + group by ?site, day(?m.~observed) + + return { + site = ?site, + day = day(?m.~observed), + total_kwh = sum(?m.value), + peak_kwh = max(?m.value), + sample_n = count(?m), + has_fault = ?fault != null + } +} diff --git a/website/public/examples/robotics-pose.kndl b/website/public/examples/robotics-pose.kndl new file mode 100644 index 0000000..96f76a3 --- /dev/null +++ b/website/public/examples/robotics-pose.kndl @@ -0,0 +1,48 @@ +// robotics-pose.kndl — end-effector pose with coordinate frames and Gaussian uncertainty. +// +// Demonstrates: Frame as a first-class node, Pose parameterised type, +// ~uncertainty gaussian { mean, stddev } for aleatoric variability separate +// from the scalar ~confidence (epistemic). + +import { Frame, Pose } from "kndl://std/frames" + +node @world :: Frame { + id = "world" +} + +node @base :: Frame { + id = "base_link" + parent -> @world +} + +node @tool :: Frame { + id = "tool0" + parent -> @base +} + +// End-effector pose in the base frame. Scalar confidence = 0.99 +// (calibrated camera, bright scene). Aleatoric stddev of 3 cm on each axis. +node @grasp_target_0421 :: Pose<@base> { + x = 0.320 + y = 0.015 + z = 0.505 + qx = 0.0 + qy = 0.0 + qz = 0.0 + qw = 1.0 + + ~source "agent://perception/estimator-v3" + ~observed 2026-04-23T10:02:17.512Z + ~confidence 0.99 + ~uncertainty gaussian { mean: 0.0, stddev: 0.03 } +} + +// An uncertain candidate grasp — bounded by an interval instead. +node @grasp_candidate_a :: Pose<@base> { + x = 0.28 + y = 0.11 + z = 0.49 + qx = 0.0; qy = 0.0; qz = 0.0; qw = 1.0 + ~uncertainty interval { min: -0.05, max: 0.05 } + ~confidence 0.72 +} diff --git a/website/public/llms.txt b/website/public/llms.txt new file mode 100644 index 0000000..5fd322a --- /dev/null +++ b/website/public/llms.txt @@ -0,0 +1,66 @@ +# KNDL — Knowledge Node Description Language + +> KNDL (pronounced "kindle") is a graph-based knowledge representation language +> purpose-built for AI agents. Every assertion carries **confidence** (0.0–1.0), +> **provenance** (source URI or cryptographic signature), **temporal scope** +> (bitemporal: valid / observed / recorded), and **typed relationships** as +> first-class constructs. Knowledge + behaviour (intents and processes) live in +> the same language. Current version: **v1.0** (April 2026). + +This file follows the [llmstxt.org](https://llmstxt.org) convention. Everything +below links to plain-text or markdown resources optimised for machine reading. + +## Canonical sources + +- [Language specification (markdown)](https://kndl.artdaw.com/spec/SPECIFICATION.md): Complete v1.0 reference — types, meta-annotations, queries, processes, binary format, conformance levels. +- [EBNF grammar](https://kndl.artdaw.com/spec/kndl.ebnf): Authoritative formal grammar. +- [Full bundle](https://kndl.artdaw.com/llms-full.txt): Spec + EBNF + example index concatenated for single-fetch LLM consumption. + +## Examples + +- [Examples index](https://kndl.artdaw.com/examples/index.md): Curated list of runnable .kndl snippets. +- [Basic IoT / building](https://kndl.artdaw.com/examples/basic-building.kndl): Sensor node with confidence, decay, and typed edges. +- [Intent (overheat alert)](https://kndl.artdaw.com/examples/intent-overheat.kndl): Reactive trigger-action pattern over a query. +- [Process (shipment state machine)](https://kndl.artdaw.com/examples/process-shipment.kndl): Stateful workflow with compensation. +- [Query (aggregation)](https://kndl.artdaw.com/examples/query-aggregation.kndl): Multi-hop path query with group-by. +- [Healthcare (FHIR observation)](https://kndl.artdaw.com/examples/healthcare-observation.kndl): Coded terminologies and bitemporal annotations. +- [FinTech (transaction)](https://kndl.artdaw.com/examples/fintech-transaction.kndl): Money type, double-entry constraint, signature. +- [Robotics (pose with frame)](https://kndl.artdaw.com/examples/robotics-pose.kndl): Coordinate frames and Gaussian uncertainty. + +## Key concepts + +- **Node**: A typed, identified container for fields and meta-annotations. `node @sensor_01 :: Temperature { value = 22.5 °C; ~confidence 0.94 }`. +- **Edge**: Typed, directed relationship. `edge @room_204 -[located_in]-> @floor_2`. +- **Meta-annotations** (`~key value`): `~confidence`, `~valid`, `~recorded`, `~observed`, `~decay`, `~source`, `~negated`, `~uncertainty`, `~signature`, `~access`, ... +- **Intent**: Reactive trigger-action rule fired when a query matches. +- **Process**: Stateful workflow with `state`, `on ... in ... -> ...`, `compensate` clauses. +- **Query**: Declarative match with multi-hop paths, confidence predicates, `group by` aggregation. +- **Quantity**: Dimensionally safe values — `22.5 °C`, `5 m/s`, `100 kWh`. +- **Money**: `19.99d USD` with ISO 4217 currency codes. + +## Domain profiles + +KNDL ships conventional types and source-URI schemes for each major domain: +IoT/PropTech, FinTech, Healthcare (FHIR), Logistics, Robotics, Smart Factory +(ISA-95), Networking/Security, eCommerce. See §B of the specification. + +## Discovery + +- [Sitemap](https://kndl.artdaw.com/sitemap.xml) +- [Robots](https://kndl.artdaw.com/robots.txt) +- [Security contact](https://kndl.artdaw.com/.well-known/security.txt) +- [Source on GitHub](https://github.com/artdaw/KNDL) + +## Reference implementations + +- **Python library** (`pip install kndl`): parser → AST → compiler → KNDLGraph → serializer, with SQLite/PostgreSQL storage. +- **MCP server** (`pip install kndl-mcp`): exposes the graph as Model Context Protocol tools for Claude Desktop and other agents. + +## Pages (rendered for humans, but safe to fetch) + +- [Home](https://kndl.artdaw.com/): Design goals and quick-start. +- [Spec overview](https://kndl.artdaw.com/spec): Concepts with domain-tabbed examples. +- [Full spec (rendered)](https://kndl.artdaw.com/spec/full): Same content as SPECIFICATION.md with a sticky TOC. +- [Agent workflow](https://kndl.artdaw.com/workflow): 6-stage pipeline from ingest to communicate. +- [MCP server](https://kndl.artdaw.com/mcp): Tool reference and install guide. +- [Graph explorer](https://kndl.artdaw.com/explorer): Interactive force-directed visualiser with a live editor. diff --git a/website/public/robots.txt b/website/public/robots.txt new file mode 100644 index 0000000..01e73bd --- /dev/null +++ b/website/public/robots.txt @@ -0,0 +1,63 @@ +# KNDL — Knowledge Node Description Language +# https://kndl.artdaw.com/ + +User-agent: * +Allow: / + +# AI crawlers / training bots — explicitly allowed. +# KNDL is an open specification; we want it to be easy for agents to find. + +User-agent: GPTBot +Allow: / + +User-agent: OAI-SearchBot +Allow: / + +User-agent: ChatGPT-User +Allow: / + +User-agent: ClaudeBot +Allow: / + +User-agent: Claude-Web +Allow: / + +User-agent: anthropic-ai +Allow: / + +User-agent: PerplexityBot +Allow: / + +User-agent: Perplexity-User +Allow: / + +User-agent: Google-Extended +Allow: / + +User-agent: GoogleOther +Allow: / + +User-agent: CCBot +Allow: / + +User-agent: Applebot-Extended +Allow: / + +User-agent: Bytespider +Allow: / + +User-agent: FacebookBot +Allow: / + +User-agent: Meta-ExternalAgent +Allow: / + +# Primary machine-readable entry points: +# /llms.txt — concise index for LLMs (llmstxt.org) +# /llms-full.txt — spec + EBNF + examples bundled +# /spec/SPECIFICATION.md — canonical language reference +# /spec/kndl.ebnf — authoritative grammar +# /examples/ — curated .kndl examples +# /sitemap.xml — full URL index + +Sitemap: https://kndl.artdaw.com/sitemap.xml diff --git a/website/public/sitemap.xml b/website/public/sitemap.xml new file mode 100644 index 0000000..730fb9b --- /dev/null +++ b/website/public/sitemap.xml @@ -0,0 +1,69 @@ + + + + https://kndl.artdaw.com/ + 2026-04-24 + weekly + 1.0 + + + https://kndl.artdaw.com/spec + 2026-04-24 + weekly + 0.9 + + + https://kndl.artdaw.com/spec/full + 2026-04-24 + weekly + 0.9 + + + https://kndl.artdaw.com/workflow + 2026-04-24 + monthly + 0.7 + + + https://kndl.artdaw.com/mcp + 2026-04-24 + monthly + 0.7 + + + https://kndl.artdaw.com/explorer + 2026-04-24 + monthly + 0.6 + + + https://kndl.artdaw.com/spec/SPECIFICATION.md + 2026-04-24 + weekly + 0.9 + + + https://kndl.artdaw.com/spec/kndl.ebnf + 2026-04-24 + weekly + 0.8 + + + https://kndl.artdaw.com/llms.txt + 2026-04-24 + weekly + 0.9 + + + https://kndl.artdaw.com/llms-full.txt + 2026-04-24 + weekly + 0.9 + + + https://kndl.artdaw.com/examples/ + 2026-04-24 + monthly + 0.7 + + diff --git a/website/tsconfig.node.json b/website/tsconfig.node.json index 9c43072..f1f1afc 100644 --- a/website/tsconfig.node.json +++ b/website/tsconfig.node.json @@ -11,7 +11,8 @@ "noEmit": true, "strict": true, "noUnusedLocals": true, - "noUnusedParameters": true + "noUnusedParameters": true, + "types": ["node"] }, "include": ["vite.config.ts"] } diff --git a/website/vite.config.ts b/website/vite.config.ts index 45931a3..283601d 100644 --- a/website/vite.config.ts +++ b/website/vite.config.ts @@ -1,8 +1,110 @@ -import { defineConfig } from "vitest/config"; +import { defineConfig, type Plugin } from "vitest/config"; import react from "@vitejs/plugin-react"; +import { readFileSync, existsSync } from "node:fs"; +import { resolve } from "node:path"; + +// ── Plugin: expose spec/SPECIFICATION.md and spec/grammar/kndl.ebnf at +// stable URLs (/spec/SPECIFICATION.md, /spec/kndl.ebnf) in both dev +// and build. Also emits /llms-full.txt = preamble + spec + EBNF so +// agents can slurp everything in one request. +function kndlSpecAssets(): Plugin { + const repoRoot = resolve(__dirname, ".."); + const specPath = resolve(repoRoot, "spec/SPECIFICATION.md"); + const ebnfPath = resolve(repoRoot, "spec/grammar/kndl.ebnf"); + const examplesIndexPath = resolve(__dirname, "public/examples/index.md"); + + const read = (p: string) => (existsSync(p) ? readFileSync(p, "utf8") : ""); + + const buildLlmsFull = () => { + const spec = read(specPath); + const ebnf = read(ebnfPath); + const examplesIdx = read(examplesIndexPath); + return [ + "# KNDL — Full Machine-Readable Bundle", + "", + "This file bundles the KNDL specification, EBNF grammar, and example", + "index into a single document for LLM consumption. It is regenerated", + "at build time from the canonical sources in the repository.", + "", + "Canonical URLs:", + "- Spec: https://kndl.artdaw.com/spec/SPECIFICATION.md", + "- Grammar: https://kndl.artdaw.com/spec/kndl.ebnf", + "- Examples: https://kndl.artdaw.com/examples/", + "- Index: https://kndl.artdaw.com/llms.txt", + "", + "---", + "", + "# PART 1 — SPECIFICATION", + "", + spec, + "", + "---", + "", + "# PART 2 — EBNF GRAMMAR", + "", + "```ebnf", + ebnf, + "```", + "", + "---", + "", + "# PART 3 — EXAMPLE INDEX", + "", + examplesIdx, + "", + ].join("\n"); + }; + + const serveMap: Record string> = { + "/spec/SPECIFICATION.md": () => read(specPath), + "/spec/kndl.ebnf": () => read(ebnfPath), + "/llms-full.txt": () => buildLlmsFull(), + }; + + return { + name: "kndl-spec-assets", + + // Dev: serve files straight from disk at their stable URLs. + configureServer(server) { + server.middlewares.use((req, res, next) => { + const url = (req.url || "").split("?")[0]; + const handler = serveMap[url]; + if (!handler) return next(); + const body = handler(); + const ext = url.endsWith(".ebnf") + ? "text/plain; charset=utf-8" + : url.endsWith(".md") + ? "text/markdown; charset=utf-8" + : "text/plain; charset=utf-8"; + res.setHeader("Content-Type", ext); + res.setHeader("Access-Control-Allow-Origin", "*"); + res.end(body); + }); + }, + + // Build: emit the files into the final bundle. + generateBundle() { + this.emitFile({ + type: "asset", + fileName: "spec/SPECIFICATION.md", + source: read(specPath), + }); + this.emitFile({ + type: "asset", + fileName: "spec/kndl.ebnf", + source: read(ebnfPath), + }); + this.emitFile({ + type: "asset", + fileName: "llms-full.txt", + source: buildLlmsFull(), + }); + }, + }; +} export default defineConfig({ - plugins: [react()], + plugins: [react(), kndlSpecAssets()], resolve: { alias: { "@": "/src", @@ -20,3 +122,4 @@ export default defineConfig({ css: false, }, }); + From 3d04921bc407389ab693c3b19d2f00b5b94ba833 Mon Sep 17 00:00:00 2001 From: Gleb Galkin Date: Sat, 25 Apr 2026 00:16:18 +0200 Subject: [PATCH 4/5] feat(website): prerender per-route HTML shells for GitHub Pages SEO MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GitHub Pages has no server-side SPA fallback, so direct hits on /spec, /spec/full, /workflow, /mcp, /explorer would return the 404.html with a 404 status — poor for SEO even though the content renders after the JS redirect. Stamp one HTML shell per route at build time so each URL is served with status 200 and the right , <meta>, canonical, Open Graph, Twitter cards, and route-specific JSON-LD already in the markup. At runtime the <SEO> component finds the same tags (via data-seo selectors) and overwrites them in place with matching values, so there is no duplication or flash of old meta. Wire as the final `build` step and a standalone `prerender` script. --- website/package.json | 3 +- website/scripts/prerender.mjs | 243 ++++++++++++++++++++++++++++++++++ 2 files changed, 245 insertions(+), 1 deletion(-) create mode 100644 website/scripts/prerender.mjs diff --git a/website/package.json b/website/package.json index 01af6bd..e6c5f3d 100644 --- a/website/package.json +++ b/website/package.json @@ -5,7 +5,8 @@ "type": "module", "scripts": { "dev": "vite", - "build": "tsc -b && vite build", + "build": "tsc -b && vite build && node scripts/prerender.mjs", + "prerender": "node scripts/prerender.mjs", "preview": "vite preview", "test": "vitest run", "test:watch": "vitest", diff --git a/website/scripts/prerender.mjs b/website/scripts/prerender.mjs new file mode 100644 index 0000000..c979187 --- /dev/null +++ b/website/scripts/prerender.mjs @@ -0,0 +1,243 @@ +#!/usr/bin/env node +// Post-build prerender for GitHub Pages SEO. +// +// Vite builds a single dist/index.html. Direct hits on /spec, /workflow, etc. +// would otherwise return the 404 fallback (with HTTP 404 status). We stamp +// out one HTML shell per route so each path is served with status 200 and +// the correct <title>, <meta>, <link rel="canonical">, Open Graph, Twitter, +// and JSON-LD already in the markup. +// +// At runtime, the <SEO> component in src/components/SEO.tsx re-applies the +// same values in place (no duplication, no flash) so we stay consistent. +// +// The SEO copy below MUST match the <SEO ... /> props in the corresponding +// page components. Keep them in sync; if they drift, the runtime values +// win but crawlers will see whatever we prerendered here. + +import { readFileSync, writeFileSync, mkdirSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +const here = dirname(fileURLToPath(import.meta.url)); +const root = dirname(here); +const DIST = join(root, "dist"); +const ORIGIN = "https://kndl.artdaw.com"; +const DEFAULT_IMAGE = `${ORIGIN}/kndl.png`; + +// ── Schema.org helpers ──────────────────────────────────────────────────── + +function techArticle({ headline, description, path, dateModified }) { + return { + "@context": "https://schema.org", + "@type": "TechArticle", + headline, + description, + url: ORIGIN + path, + mainEntityOfPage: ORIGIN + path, + dateModified: dateModified ?? new Date().toISOString().slice(0, 10), + inLanguage: "en", + publisher: { "@type": "Organization", name: "KNDL", url: ORIGIN }, + image: DEFAULT_IMAGE, + }; +} + +// ── Route metadata ──────────────────────────────────────────────────────── + +const ROUTES = [ + { + path: "/spec", + outDir: "spec", + title: "KNDL Language Specification — types, meta-annotations, domain profiles", + description: + "KNDL language reference: primitive types (Quantity, Money, Vector), meta-annotations (~confidence, ~valid, ~recorded, ~negated, ~uncertainty), query language with multi-hop paths, processes, and eight domain profiles (IoT, FinTech, Healthcare, Logistics, Robotics, Smart Factory, Networking, eCommerce).", + type: "article", + keywords: + "KNDL specification, knowledge graph language, AI agent memory, confidence score, temporal decay, provenance, EBNF grammar", + jsonLd: techArticle({ + headline: "KNDL Language Specification", + description: + "Reference for the Knowledge Node Description Language — types, meta-annotations, queries, processes, and domain profiles.", + path: "/spec", + }), + }, + { + path: "/spec/full", + outDir: "spec/full", + title: "KNDL Specification v1.0 — Full Reference", + description: + "Full KNDL v1.0 specification: lexical structure, type system (Quantity, Money, Vector, Frame, Code, Localized), core constructs, query language with multi-hop paths, processes, uncertainty model, serialization (text + binary), and conformance levels. Raw markdown available at /spec/SPECIFICATION.md.", + type: "article", + keywords: + "KNDL spec v1.0, EBNF grammar, knowledge graph, agent memory, semantic data, confidence, provenance", + dateModified: "2026-04-23", + jsonLd: techArticle({ + headline: "KNDL Language Specification v1.0", + description: "Complete reference for the Knowledge Node Description Language version 1.0.", + path: "/spec/full", + dateModified: "2026-04-23", + }), + }, + { + path: "/workflow", + outDir: "workflow", + title: "KNDL Agent Workflow — 6-Stage Pipeline (Ingest → Communicate)", + description: + "Walk through how an AI agent actually uses KNDL: Ingest raw input, Produce confidence-scored nodes, Merge into the knowledge graph, Reason with probabilistic queries, Act via intents, Communicate grounded responses. Per-stage insights and integration architecture.", + type: "article", + keywords: + "AI agent workflow, KNDL pipeline, knowledge graph reasoning, intent-action pattern, agent memory", + jsonLd: techArticle({ + headline: "KNDL Agent Workflow — 6-Stage Pipeline", + description: + "How an AI agent uses KNDL as a cognitive substrate across Ingest, Produce, Merge, Reason, Act, and Communicate stages.", + path: "/workflow", + }), + }, + { + path: "/mcp", + outDir: "mcp", + title: "KNDL MCP Server — Use KNDL from Claude & AI Agents", + description: + "KNDL MCP server docs: 13 Model Context Protocol tools (kndl_parse, kndl_add_node, kndl_query_nodes, kndl_neighborhood, kndl_add_intent, and more). Install with pip, connect to Claude Desktop or any MCP-compatible agent.", + type: "article", + keywords: + "KNDL MCP, Model Context Protocol, Claude Desktop, AI agent tools, knowledge graph tools, MCP server", + jsonLd: techArticle({ + headline: "KNDL MCP Server", + description: + "Expose the KNDL knowledge graph as Model Context Protocol tools for Claude and other AI agents.", + path: "/mcp", + }), + }, + { + path: "/explorer", + outDir: "explorer", + title: "KNDL Graph Explorer — Interactive Force-Directed Visualization", + description: + "Visualise a KNDL knowledge graph live. Edit KNDL source in the browser and watch nodes, typed edges, and confidence scores render as a force-directed graph. Zoom, pan, drag, and inspect node details.", + type: "website", + keywords: + "KNDL graph explorer, knowledge graph visualization, force-directed layout, KNDL playground", + jsonLd: techArticle({ + headline: "KNDL Graph Explorer", + description: + "Interactive force-directed visualization of KNDL knowledge graphs with a live editor.", + path: "/explorer", + }), + }, +]; + +// ── HTML stamping ───────────────────────────────────────────────────────── + +function esc(s) { + return String(s) + .replace(/&/g, "&") + .replace(/</g, "<") + .replace(/>/g, ">") + .replace(/"/g, """); +} + +function setTagContent(html, matcher, replacement) { + if (!matcher.test(html)) { + throw new Error(`Prerender: couldn't find tag matching ${matcher}`); + } + return html.replace(matcher, replacement); +} + +function stamp(template, route) { + const url = ORIGIN + route.path; + let html = template; + + html = setTagContent( + html, + /<title>[\s\S]*?<\/title>/, + `<title>${esc(route.title)}`, + ); + + html = setTagContent( + html, + /]*>/, + ``, + ); + + html = setTagContent( + html, + /]*>/, + ``, + ); + + html = setTagContent( + html, + /]*>/, + ``, + ); + + html = setTagContent( + html, + /]*>/, + ``, + ); + html = setTagContent( + html, + /]*>/, + ``, + ); + html = setTagContent( + html, + /]*>/, + ``, + ); + html = setTagContent( + html, + /]*>/, + ``, + ); + + html = setTagContent( + html, + /]*>/, + ``, + ); + html = setTagContent( + html, + /]*>/, + ``, + ); + html = setTagContent( + html, + /]*>/, + ``, + ); + + // Append per-route JSON-LD right before . The runtime + // component will target the same `data-seo="page"` script tag and + // overwrite its textContent with the same value on mount. + if (route.jsonLd) { + const jsonLdScript = ` \n `; + html = html.replace(/\s*<\/head>/, `\n${jsonLdScript}`); + } + + return html; +} + +// ── Main ────────────────────────────────────────────────────────────────── + +function main() { + const template = readFileSync(join(DIST, "index.html"), "utf8"); + let count = 0; + for (const r of ROUTES) { + const html = stamp(template, r); + const outDir = join(DIST, r.outDir); + mkdirSync(outDir, { recursive: true }); + writeFileSync(join(outDir, "index.html"), html); + count++; + } + console.log(`prerender: stamped ${count} route shell${count === 1 ? "" : "s"}`); + for (const r of ROUTES) { + console.log(` ${r.path.padEnd(14)} -> dist/${r.outDir}/index.html`); + } +} + +main(); From cab2b15c0ce07d2c4d6d4d9b613e65a06994bf7e Mon Sep 17 00:00:00 2001 From: Gleb Galkin Date: Sat, 25 Apr 2026 00:16:21 +0200 Subject: [PATCH 5/5] docs(website): document new URLs, discovery surfaces, and prerender --- website/README.md | 51 +++++++++++++++++++++++++++++++++++++---------- 1 file changed, 41 insertions(+), 10 deletions(-) diff --git a/website/README.md b/website/README.md index e7d5094..b1eec7a 100644 --- a/website/README.md +++ b/website/README.md @@ -6,16 +6,47 @@ Vite + React 19 + TypeScript documentation site for the KNDL project. ## Routes -Uses `createHashRouter` — all routes are prefixed with `#` (e.g. `/#/spec`) so the site works on static hosting without a server-side fallback. - -| Hash path | Page | Description | -|-----------|------|-------------| -| `/#/` | LandingPage | Hero, v0.2 feature highlights, quick-start snippet | -| `/#/spec` | SpecPage | Language reference with 8-domain tabbed examples + live playground | -| `/#/spec/full` | SpecFullPage | Full rendered SPECIFICATION.md | -| `/#/workflow` | WorkflowPage | 6-stage agent pipeline animation | -| `/#/explorer` | ExplorerPage | Force-directed graph explorer (pan/zoom/drag, detail panel) | -| `/#/mcp` | McpPage | MCP server docs and tool reference | +Uses `createBrowserRouter` with a GitHub Pages SPA fallback (`public/404.html` stashes the intended pathname in `sessionStorage`; `main.tsx` replays it on boot). Clean URLs (no `#`) are required for real SEO. + +| Path | Page | Description | +|------|------|-------------| +| `/` | LandingPage | Hero, v1.0 feature highlights, quick-start snippet | +| `/spec` | SpecPage | Language reference with 8-domain tabbed examples + live playground | +| `/spec/full` | SpecFullPage | Full rendered SPECIFICATION.md with sticky TOC | +| `/workflow` | WorkflowPage | 6-stage agent pipeline animation (per-stage insight + highlighted layer) | +| `/explorer` | ExplorerPage | Force-directed graph explorer (pan/zoom/drag, detail panel) | +| `/mcp` | McpPage | MCP server docs and tool reference | + +## Machine-readable discovery surfaces + +Everything below is served as a static file and is meant to be fetched by AI agents, search engines, and scripts. + +| URL | Format | Purpose | +|-----|--------|---------| +| `/llms.txt` | markdown | Concise [llmstxt.org](https://llmstxt.org) index of the whole project | +| `/llms-full.txt` | markdown | Spec + EBNF + example index concatenated — single-fetch bundle for LLMs | +| `/spec/SPECIFICATION.md` | markdown | Canonical language reference (mirrored from repo `spec/`) | +| `/spec/kndl.ebnf` | text | Authoritative EBNF grammar (mirrored from repo `spec/grammar/`) | +| `/examples/index.md` | markdown | Index of curated `.kndl` snippets | +| `/examples/*.kndl` | text | Runnable examples (basic-building, intent-overheat, process-shipment, query-aggregation, healthcare-observation, fintech-transaction, robotics-pose, logistics-trace) | +| `/sitemap.xml` | xml | Every indexable URL on the site | +| `/robots.txt` | text | Explicitly allows major AI crawlers (GPTBot, ClaudeBot, PerplexityBot, Google-Extended, CCBot, …) | +| `/.well-known/security.txt` | text | Security contact per [securitytxt.org](https://securitytxt.org) | + +The Vite plugin `kndlSpecAssets` in `vite.config.ts` mirrors `spec/SPECIFICATION.md` and `spec/grammar/kndl.ebnf` from the repo root into the built output at these URLs and serves them in dev. It also regenerates `llms-full.txt` from source on each build. + +## SEO per route + +`src/components/SEO.tsx` is a tiny runtime component that each page renders. It updates: + +- `` and `<meta name="description">` +- `<meta name="robots">` with `max-image-preview:large` +- Open Graph (`og:title`, `og:description`, `og:url`, `og:type`, `og:image`, `og:site_name`) +- Twitter cards (`twitter:*`) +- `<link rel="canonical">` and `<link rel="alternate">` (llm index) +- Per-page JSON-LD (`TechArticle` for docs, `SoftwareSourceCode` for the landing page) + +`index.html` also contains a page-level JSON-LD `@graph` covering Organization, WebSite, SoftwareSourceCode, and TechArticle, plus a `<noscript>` pointer to the machine-readable surfaces for crawlers that don't execute JS. ## Stack