From 525b449478a9cc28427a8cad1393feec804a5d1b Mon Sep 17 00:00:00 2001 From: alexanderkirtzel Date: Tue, 25 Aug 2026 15:12:49 +0200 Subject: [PATCH 1/3] file not found --- website/docusaurus.config.ts | 74 ++++- .../components/organisms/markdownActions.tsx | 112 +++++++ website/src/theme/DocItem/Content/index.js | 2 + website/src/utils/markdown-export.ts | 23 ++ website/static/bunnycdn_errors/404.html | 303 ++++++++++++++++++ 5 files changed, 511 insertions(+), 3 deletions(-) create mode 100644 website/src/components/organisms/markdownActions.tsx create mode 100644 website/src/utils/markdown-export.ts create mode 100644 website/static/bunnycdn_errors/404.html diff --git a/website/docusaurus.config.ts b/website/docusaurus.config.ts index ff5fffae3..0af5c7c27 100644 --- a/website/docusaurus.config.ts +++ b/website/docusaurus.config.ts @@ -9,8 +9,25 @@ import normalizeExportLinks from './src/remark/normalize-export-links'; const vars = { github: 'https://github.com/elbwalker/walkerOS/', linkedin: 'https://www.linkedin.com/company/elbwalker/', + site: 'https://www.walkeros.io', + npm: 'https://www.npmjs.com/org/walkeros', }; +// The llms.txt header is a single blockquote: the plugin writes one `> ` in +// front of `siteDescription` and nothing else, so a multi-paragraph preamble +// carries its own continuation markers. +// +// Keep it short and factual. It is the first thing a model reads about +// walkerOS, and its job is to correct the two things a training corpus gets +// wrong: the package namespace, and what category the project is in. +const llmsTxtPreamble = [ + 'Privacy-first, composable event data collection (Source → Collector → Destination).', + 'Current namespace: packages are published under `@walkeros/*` and the command line binary is `walkeros`. Package names from the walker.js era are historical and should not be suggested for new work.', + 'walkerOS is not a product analytics tool, not a consent management platform, and not a business intelligence layer. It collects events and routes them to those tools.', + `To prove an integration works without calling a real endpoint, run \`walkeros push flow.json --event '{"name":"product add"}' --simulate destination.NAME\`. It runs the flow and reports what the destination would have sent.`, + `Canonical index: ${vars.site}/llms.txt. Generated ${new Date().toISOString().slice(0, 10)}.`, +].join('\n>\n> '); + const config: Config = { title: 'walkerOS', tagline: 'Open-source event data collection platform', @@ -32,7 +49,10 @@ const config: Config = { }, }, - // Set the production url of your site here + // Set the production url of your site here. + // Keep this a plain string literal matching `vars.site`: the LLM export guard + // reads the value straight out of this file rather than importing the config, + // so a reference here leaves it with no url to check links against. url: 'https://www.walkeros.io', // Set the // pathname under which your site is served // For GitHub pages deployment, it is often '//' @@ -64,6 +84,48 @@ const config: Config = { }, }, + // Site-wide head tags. Everything here is published, machine-read copy, so it + // states facts about the project and nothing else. + headTags: [ + // Docusaurus emits og:title, og:description, og:image, og:url and og:locale + // from the theme, but never og:type. + { + tagName: 'meta', + attributes: { + property: 'og:type', + content: 'website', + }, + }, + // llms.txt v2 discovery: the index that describes this site. The per-page + // `rel="alternate" type="text/markdown"` half is emitted from the doc item + // itself, since only doc routes have a Markdown companion. + { + tagName: 'link', + attributes: { + rel: 'describedby', + href: `${vars.site}/llms.txt`, + }, + }, + { + tagName: 'script', + attributes: { + type: 'application/ld+json', + }, + innerHTML: JSON.stringify({ + '@context': 'https://schema.org', + '@type': 'SoftwareApplication', + name: 'walkerOS', + description: + 'Open source event data collection. Sources capture events, a collector processes them, and destinations route them to analytics and marketing tools.', + url: vars.site, + applicationCategory: 'DeveloperApplication', + operatingSystem: 'Browser, Node.js', + license: 'https://opensource.org/licenses/MIT', + sameAs: [vars.github, vars.npm], + }), + }, + ], + themes: [ '@docusaurus/theme-live-codeblock', '@docusaurus/theme-mermaid', @@ -249,6 +311,13 @@ const config: Config = { // The plugin fails the build when a `to` is not a real route, so these // stay honest as the docs move. redirects: [ + // `/contact` is the path a reader or a crawler guesses for the legal + // contact details. Forward it to the imprint rather than growing a + // second contact surface that then drifts from it. + { + from: '/contact', + to: '/legal/imprint', + }, { from: '/docs/sources/web/session/detection', to: '/docs/sources/web/session', @@ -662,8 +731,7 @@ const config: Config = { '@signalwire/docusaurus-plugin-llms-txt', { siteTitle: 'walkerOS Documentation', - siteDescription: - 'Privacy-first, composable event data collection (Source → Collector → Destination).', + siteDescription: llmsTxtPreamble, // depth: 2 groups routes like /docs/destinations/web/amplitude into the // "docs/destinations" category, mirroring the pipeline taxonomy. depth: 2, diff --git a/website/src/components/organisms/markdownActions.tsx b/website/src/components/organisms/markdownActions.tsx new file mode 100644 index 000000000..dac337671 --- /dev/null +++ b/website/src/components/organisms/markdownActions.tsx @@ -0,0 +1,112 @@ +import React, { useEffect, useState } from 'react'; +import Head from '@docusaurus/Head'; +import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; +import { useDoc } from '@docusaurus/plugin-content-docs/client'; +import { + markdownExportPath, + markdownExportUrl, +} from '@site/src/utils/markdown-export'; + +type CopyState = 'idle' | 'copied' | 'failed'; + +const COPY_LABEL: Record = { + idle: 'Copy as Markdown', + copied: 'Copied', + failed: 'Copy failed', +}; + +export default function MarkdownActions(): React.JSX.Element { + const { siteConfig } = useDocusaurusContext(); + const { metadata } = useDoc(); + + const path = markdownExportPath(metadata.permalink); + const url = markdownExportUrl(siteConfig.url, metadata.permalink); + + // Rendered on the server so the controls exist without JavaScript, then + // confirmed against the served file. Only a response that says the export is + // missing takes them away: a request that never completed is not evidence of + // absence, and hiding a working link because someone is offline is worse than + // leaving it. Rechecked on client-side navigation to another doc. + const [available, setAvailable] = useState(true); + const [copyState, setCopyState] = useState('idle'); + + useEffect(() => { + let current = true; + setAvailable(true); + setCopyState('idle'); + + fetch(path, { method: 'HEAD' }) + .then((response) => { + if (current && !response.ok) setAvailable(false); + }) + .catch(() => {}); + + return () => { + current = false; + }; + }, [path]); + + async function copyMarkdown(): Promise { + try { + const response = await fetch(path); + if (!response.ok) throw new Error(`Request failed: ${response.status}`); + await navigator.clipboard.writeText(await response.text()); + setCopyState('copied'); + } catch { + setCopyState('failed'); + } + } + + return ( + <> + {/* Only doc routes render a doc item, and every doc route has an export, + so this link relation can never point at a missing file. */} + + + + {available && ( + + )} + + ); +} diff --git a/website/src/theme/DocItem/Content/index.js b/website/src/theme/DocItem/Content/index.js index fe34b729f..caa011bf0 100644 --- a/website/src/theme/DocItem/Content/index.js +++ b/website/src/theme/DocItem/Content/index.js @@ -1,10 +1,12 @@ import React from 'react'; import Content from '@theme-original/DocItem/Content'; +import MarkdownActions from '@site/src/components/organisms/markdownActions'; import SupportNotice from '@site/src/components/organisms/supportNotice'; export default function ContentWrapper(props) { return ( <> + diff --git a/website/src/utils/markdown-export.ts b/website/src/utils/markdown-export.ts new file mode 100644 index 000000000..0dbf07b66 --- /dev/null +++ b/website/src/utils/markdown-export.ts @@ -0,0 +1,23 @@ +/** + * Resolves the Markdown companion of a doc route. + * + * The LLM export writes one `.md` file per doc route, mirroring the route path + * with any trailing slash removed: `/docs/` is written to `/docs.md` and + * `/docs/mapping/` to `/docs/mapping.md`. Only routes rendered as a doc item + * have one. Generated category indexes and client-redirect stubs are separate + * route types and carry no export, so never derive a link for them. + */ + +/** Path of the Markdown companion, e.g. `/docs/mapping.md`. */ +export function markdownExportPath(permalink: string): string { + const withoutTrailingSlash = permalink.replace(/\/+$/, ''); + return `${withoutTrailingSlash || '/index'}.md`; +} + +/** + * Fully qualified URL of the Markdown companion. The permalink already carries + * the baseUrl, so the site url contributes the origin only. + */ +export function markdownExportUrl(siteUrl: string, permalink: string): string { + return `${siteUrl.replace(/\/+$/, '')}${markdownExportPath(permalink)}`; +} diff --git a/website/static/bunnycdn_errors/404.html b/website/static/bunnycdn_errors/404.html new file mode 100644 index 000000000..fa0884dd9 --- /dev/null +++ b/website/static/bunnycdn_errors/404.html @@ -0,0 +1,303 @@ + + + + + + + Page not found | walkerOS + + + +
+ + + + +

Error 404

+

Page not found

+

+ This address does not exist. It may be mistyped, or the page has moved. + These are good places to pick the thread back up. +

+ + + +

+ Every page on this site is listed in the + sitemap.
+ For AI agents: the full documentation index is at + https://www.walkeros.io/llms.txt +

+
+ + From e58cbac7c0c510a960d0441df3d25e3ab1705a02 Mon Sep 17 00:00:00 2001 From: alexanderkirtzel Date: Tue, 25 Aug 2026 17:08:37 +0200 Subject: [PATCH 2/3] thx --- CONTRIBUTING.md | 79 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..fb5dfaf35 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,79 @@ +# Contributing to walkerOS + +walkerOS is open source and will remain open source. We believe companies should +own their data infrastructure. True data ownership only comes when you control +your data collection. Thanks for considering a contribution, we appreciate them +all. + +## Ways to contribute + +- **Report bugs** via + [GitHub issues](https://github.com/elbwalker/walkerOS/issues) (issue templates + available) +- **Suggest features**: open an issue first so we can discuss the approach + before you invest time in code +- **Improve documentation**: the docs live in `website/docs/` +- **Contribute code**: fix bugs, improve packages, or create new destinations, + sources, or transformers (the `skills/` folder has step-by-step guides) +- **Help other users**: answer questions in issues and discussions + +## Getting started + +The easiest setup is the devcontainer, which installs all dependencies and +tooling automatically. A manual setup works too: + +```bash +npm install # Install dependencies +npm run build # Build all packages +npm run dev # Watch mode +``` + +For the full setup guide, package structure, and verification scripts, see the +[contributing documentation](https://www.walkeros.io/docs/contributing) and +[AGENT.md](./AGENT.md), the quick reference for contributors and AI assistants. + +## Development workflow + +- **Test first.** walkerOS follows test-driven development with Jest. Write the + test, watch it fail, then implement. +- **Verify the smallest scope that proves your change:** + + ```bash + npm run verify:touched -- # One package: typecheck + lint + test + npm run verify:affected # Everything affected since origin/main + ``` + +- **Event naming** is `"entity action"` with a space (`"page view"`, not + `"page_view"`). +- **No `any`** in production code. If types don't fit, fix the code, not the + types. + +## Pull requests + +1. For anything larger than a small fix, open an issue first and outline the + approach. +2. Keep the PR scoped: one concern per pull request. +3. Include tests for the change and make sure verification passes. +4. Add a changeset (`npx changeset`) when the change affects published packages. + Skip it for docs, CI, or internal refactoring. +5. CI runs typecheck, lint, and tests on every PR. + +## Licensing + +walkerOS is licensed under the [MIT license](./LICENSE). By submitting a +contribution, you agree that: + +- your contribution is provided under the same MIT license that covers the + project (inbound = outbound), and +- you have the right to submit the work under this license: it is your own work, + or you are permitted to contribute it (for example by your employer, if you + contribute in the course of your employment). + +There is no CLA to sign. If your company's legal team has questions about +contributing, we are happy to talk to them directly: +[hello@elbwalker.com](mailto:hello@elbwalker.com). + +## Questions + +- [Open an issue](https://github.com/elbwalker/walkerOS/issues) +- [Send an email](mailto:hello@elbwalker.com) From 0727eda4d0bea69e69c4eb8b423dced4aa13f8c5 Mon Sep 17 00:00:00 2001 From: alexanderkirtzel Date: Tue, 25 Aug 2026 17:08:58 +0200 Subject: [PATCH 3/3] ai context --- website/docusaurus.config.ts | 18 +++- .../scripts/prepend-export-context.test.mjs | 102 ++++++++++++++++++ website/src/components/atoms/brandMarks.tsx | 75 +++++++++++++ .../organisms/markdownActions.module.css | 70 ++++++++++++ .../components/organisms/markdownActions.tsx | 74 +++++-------- website/src/remark/prepend-export-context.ts | 69 ++++++++++++ website/static/bunnycdn_errors/404.html | 6 +- 7 files changed, 357 insertions(+), 57 deletions(-) create mode 100644 website/scripts/prepend-export-context.test.mjs create mode 100644 website/src/components/atoms/brandMarks.tsx create mode 100644 website/src/components/organisms/markdownActions.module.css create mode 100644 website/src/remark/prepend-export-context.ts diff --git a/website/docusaurus.config.ts b/website/docusaurus.config.ts index 0af5c7c27..ef1d9d77d 100644 --- a/website/docusaurus.config.ts +++ b/website/docusaurus.config.ts @@ -5,6 +5,7 @@ import type { PluginOptions as LlmsTxtOptions } from '@signalwire/docusaurus-plu import { version as coreVersion } from '../packages/core/package.json'; import restoreExpressionIndent from './src/remark/restore-expression-indent'; import normalizeExportLinks from './src/remark/normalize-export-links'; +import prependExportContext from './src/remark/prepend-export-context'; const vars = { github: 'https://github.com/elbwalker/walkerOS/', @@ -763,11 +764,18 @@ const config: Config = { // non-root baseUrl. relativePaths: Boolean(process.env.DOCUSAURUS_BASEURL), excludeRoutes: ['/search', '/404', '/tags/**'], - // The export appends `.md` to the route path, so a trailing-slash - // route yields `/docs/mapping/.md` while the page is written to - // `/docs/mapping.md`. Rewrite those targets after the export's own - // link handling. - remarkPlugins: [normalizeExportLinks], + // These run on the mdast of the per-page exports only, so neither + // touches llms.txt: + // - The export appends `.md` to the route path, so a trailing-slash + // route yields `/docs/mapping/.md` while the page is written to + // `/docs/mapping.md`. Rewrite those targets after the export's own + // link handling. + // - An export is read detached from the site, often by an agent that + // landed on one narrow page. Give it a pointer to the index. + remarkPlugins: [ + normalizeExportLinks, + [prependExportContext, { indexUrl: `${vars.site}/llms.txt` }], + ], }, } satisfies LlmsTxtOptions, ], diff --git a/website/scripts/prepend-export-context.test.mjs b/website/scripts/prepend-export-context.test.mjs new file mode 100644 index 000000000..73348a382 --- /dev/null +++ b/website/scripts/prepend-export-context.test.mjs @@ -0,0 +1,102 @@ +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import { unified } from 'unified'; +import remarkParse from 'remark-parse'; +import remarkStringify from 'remark-stringify'; +import remarkGfm from 'remark-gfm'; + +const SRC = new URL('../src/remark', import.meta.url).pathname; +const { default: prependExportContext } = await import( + `${SRC}/prepend-export-context.ts` +); +const { default: normalizeExportLinks } = await import( + `${SRC}/normalize-export-links.ts` +); + +const INDEX = 'https://www.walkeros.io/llms.txt'; +const EXPECTED = + '> Part of the walkerOS documentation. Project overview and full index: '; + +// Mirrors the export pipeline: remark-gfm is on, the site plugins run after the +// built-ins, then remark-stringify emits the .md file. +function run( + markdown, + plugins = [[prependExportContext, { indexUrl: INDEX }]], +) { + const processor = unified().use(remarkParse).use(remarkGfm); + for (const p of plugins) { + if (Array.isArray(p)) processor.use(p[0], p[1]); + else processor.use(p); + } + return String(processor.use(remarkStringify).processSync(markdown)); +} + +const PAGE = [ + '# Mapping', + '', + 'Transform events on the way to a destination.', + '', + '- [Sources](/docs/sources/.md)', + '', + '```js', + 'const a = 1;', + '```', +].join('\n'); + +test('exact line, as a blockquote, at the very top', () => { + const out = run(PAGE); + assert.equal(out.split('\n')[0], EXPECTED); +}); + +test('the index URL is emitted unescaped and machine readable', () => { + const out = run(PAGE); + assert.doesNotMatch(out, /\\/); + assert.match(out, //); +}); + +test('the page keeps its title, prose, list and code block', () => { + const out = run(PAGE); + const body = out.slice(out.indexOf('# Mapping')); + assert.equal(body.trim(), run(PAGE, []).trim()); +}); + +test('the note is added exactly once when the transform runs twice', () => { + const opts = [prependExportContext, { indexUrl: INDEX }]; + const out = run(PAGE, [opts, opts]); + assert.equal(out.split(EXPECTED).length - 1, 1); +}); + +test('re-running over an already prepended export does not duplicate', () => { + const out = run(run(PAGE)); + assert.equal(out.split(EXPECTED).length - 1, 1); +}); + +test('composes with normalizeExportLinks: /.md targets still get rewritten', () => { + const out = run(PAGE, [ + normalizeExportLinks, + [prependExportContext, { indexUrl: INDEX }], + ]); + assert.equal(out.split('\n')[0], EXPECTED); + assert.match(out, /\(\/docs\/sources\.md\)/); + assert.doesNotMatch(out, /\/docs\/sources\/\.md/); +}); + +test('the note itself is left alone by normalizeExportLinks', () => { + const out = run(PAGE, [ + [prependExportContext, { indexUrl: INDEX }], + normalizeExportLinks, + ]); + assert.equal(out.split('\n')[0], EXPECTED); +}); + +test('a page with no leading heading still gets the note first', () => { + const out = run('Just a paragraph.'); + assert.equal(out, `${EXPECTED}\n\nJust a paragraph.\n`); +}); + +test('a malformed tree is left alone instead of throwing', () => { + const transform = prependExportContext({ indexUrl: INDEX }); + for (const tree of [null, undefined, 'text', 42, {}, { children: 'no' }]) { + assert.doesNotThrow(() => transform(tree)); + } +}); diff --git a/website/src/components/atoms/brandMarks.tsx b/website/src/components/atoms/brandMarks.tsx new file mode 100644 index 000000000..142b31f0a --- /dev/null +++ b/website/src/components/atoms/brandMarks.tsx @@ -0,0 +1,75 @@ +/** + * Official brand marks for the assistant actions. + * + * Each path is the vendor's own artwork, copied verbatim from the source noted + * above it. They are drawn in `currentColor` so the surrounding link controls + * the tone, which keeps the three consistent in both themes. + */ + +import React from 'react'; + +interface MarkProps { + className?: string; +} + +/** + * Markdown mark by Dustin Curtis, dedicated to the public domain (CC0). + * Source: https://raw.githubusercontent.com/dcurtis/markdown-mark/master/svg/markdown-mark.svg + */ +export function MarkdownMark({ className }: MarkProps): React.JSX.Element { + return ( + + ); +} + +/** + * OpenAI mark, served inline by OpenAI at https://openai.com/favicon.svg + */ +export function OpenAIMark({ className }: MarkProps): React.JSX.Element { + return ( + + ); +} + +/** + * Claude mark, served by Anthropic at https://claude.ai/favicon.svg + * The source artwork is filled with Anthropic's #D97757; it is redrawn in + * `currentColor` here so all three marks share one tone. + */ +export function ClaudeMark({ className }: MarkProps): React.JSX.Element { + return ( + + ); +} diff --git a/website/src/components/organisms/markdownActions.module.css b/website/src/components/organisms/markdownActions.module.css new file mode 100644 index 000000000..0f7513b02 --- /dev/null +++ b/website/src/components/organisms/markdownActions.module.css @@ -0,0 +1,70 @@ +/** + * A quiet toolbar above the doc title: one muted label and three brand marks. + * + * No borders and no fill at rest, so the row reads as a caption rather than a + * button bar and never competes with the heading below it. Hover is the only + * lift. Every colour comes from the Infima emphasis scale, which inverts with + * the theme, so one set of rules covers light and dark. + */ + +.bar { + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: flex-end; + gap: 0.125rem; + margin-bottom: 0.5rem; +} + +.note { + margin-right: 0.5rem; + color: var(--ifm-color-emphasis-600); + font-size: 0.75rem; + line-height: 1.75rem; + white-space: nowrap; +} + +.action { + display: inline-flex; + align-items: center; + justify-content: center; + width: 1.75rem; + height: 1.75rem; + border-radius: 0.375rem; + color: var(--ifm-color-emphasis-600); + text-decoration: none; + transition: + color var(--ifm-transition-fast) ease, + background-color var(--ifm-transition-fast) ease; +} + +.action:hover, +.action:focus-visible { + color: var(--ifm-color-emphasis-900); + /* 300 rather than 200: the page background is already emphasis-200 light, so + the lighter chip is invisible on the theme this site actually ships. */ + background-color: var(--ifm-color-emphasis-300); + text-decoration: none; +} + +/** + * Sized per mark rather than uniformly. The three have different silhouettes: + * the Markdown mark is a 13:8 rectangle, the OpenAI knot is a square with open + * counters, and the Claude burst fills its square to the edges. Matching their + * box sizes would leave the burst reading largest and the rectangle smallest, + * so each is tuned to the same optical weight instead. + */ +.markdownMark { + width: 1.4375rem; + height: 0.875rem; +} + +.openaiMark { + width: 1rem; + height: 1rem; +} + +.claudeMark { + width: 0.9375rem; + height: 0.9375rem; +} diff --git a/website/src/components/organisms/markdownActions.tsx b/website/src/components/organisms/markdownActions.tsx index dac337671..1aa42025b 100644 --- a/website/src/components/organisms/markdownActions.tsx +++ b/website/src/components/organisms/markdownActions.tsx @@ -2,18 +2,16 @@ import React, { useEffect, useState } from 'react'; import Head from '@docusaurus/Head'; import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; import { useDoc } from '@docusaurus/plugin-content-docs/client'; +import { + MarkdownMark, + OpenAIMark, + ClaudeMark, +} from '@site/src/components/atoms/brandMarks'; import { markdownExportPath, markdownExportUrl, } from '@site/src/utils/markdown-export'; - -type CopyState = 'idle' | 'copied' | 'failed'; - -const COPY_LABEL: Record = { - idle: 'Copy as Markdown', - copied: 'Copied', - failed: 'Copy failed', -}; +import styles from './markdownActions.module.css'; export default function MarkdownActions(): React.JSX.Element { const { siteConfig } = useDocusaurusContext(); @@ -21,6 +19,7 @@ export default function MarkdownActions(): React.JSX.Element { const path = markdownExportPath(metadata.permalink); const url = markdownExportUrl(siteConfig.url, metadata.permalink); + const prompt = encodeURIComponent(`Read ${url} and help me use walkerOS.`); // Rendered on the server so the controls exist without JavaScript, then // confirmed against the served file. Only a response that says the export is @@ -28,12 +27,10 @@ export default function MarkdownActions(): React.JSX.Element { // absence, and hiding a working link because someone is offline is worse than // leaving it. Rechecked on client-side navigation to another doc. const [available, setAvailable] = useState(true); - const [copyState, setCopyState] = useState('idle'); useEffect(() => { let current = true; setAvailable(true); - setCopyState('idle'); fetch(path, { method: 'HEAD' }) .then((response) => { @@ -46,17 +43,6 @@ export default function MarkdownActions(): React.JSX.Element { }; }, [path]); - async function copyMarkdown(): Promise { - try { - const response = await fetch(path); - if (!response.ok) throw new Error(`Request failed: ${response.status}`); - await navigator.clipboard.writeText(await response.text()); - setCopyState('copied'); - } catch { - setCopyState('failed'); - } - } - return ( <> {/* Only doc routes render a doc item, and every doc route has an export, @@ -65,45 +51,35 @@ export default function MarkdownActions(): React.JSX.Element { {available && ( - )} diff --git a/website/src/remark/prepend-export-context.ts b/website/src/remark/prepend-export-context.ts new file mode 100644 index 000000000..4b5ff9049 --- /dev/null +++ b/website/src/remark/prepend-export-context.ts @@ -0,0 +1,69 @@ +/** + * Prepends one orientation line to every generated Markdown export. + * + * A single page read on its own says nothing about what walkerOS is or where + * the rest of the documentation lives, and the exports are built to be read + * that way: fetched directly, or opened by an agent that followed a link into + * a niche page. One blockquote at the top points at the canonical index. + * + * Registered under the LLM export plugin's `content.remarkPlugins`, which run + * on the mdast of the per-page exports only. llms.txt is assembled from route + * metadata on a separate path and never reaches this transform. + */ + +interface Options { + /** Fully qualified URL of the canonical index. */ + indexUrl: string; +} + +const NOTE_PREFIX = + 'Part of the walkerOS documentation. Project overview and full index: '; + +/** + * The index goes in as a link rather than as text. remark-gfm reads a bare URL + * in a text node as an autolink literal, so remark-stringify escapes it back + * out as `https\://www\.walkeros.io`; a link node stringifies to the plain + * `` autolink instead. + */ +function noteNodes(indexUrl: string): unknown[] { + return [ + { type: 'text', value: NOTE_PREFIX }, + { + type: 'link', + url: indexUrl, + children: [{ type: 'text', value: indexUrl }], + }, + ]; +} + +function isRecord(value: unknown): value is Record { + return typeof value === 'object' && value !== null; +} + +function firstChild(node: unknown, type: string): unknown { + if (!isRecord(node) || node.type !== type) return undefined; + const children = node.children; + return Array.isArray(children) ? children[0] : undefined; +} + +/** Recognises a note a previous run wrote, so a second run is a no-op. */ +function hasNote(node: unknown): boolean { + const paragraph = firstChild(node, 'blockquote'); + const text = firstChild(paragraph, 'paragraph'); + return isRecord(text) && text.type === 'text' && text.value === NOTE_PREFIX; +} + +export default function prependExportContext(options: Options) { + return function transformer(tree: unknown): void { + if (!isRecord(tree)) return; + + const children = tree.children; + if (!Array.isArray(children)) return; + if (hasNote(children[0])) return; + + children.unshift({ + type: 'blockquote', + children: [{ type: 'paragraph', children: noteNodes(options.indexUrl) }], + }); + }; +} diff --git a/website/static/bunnycdn_errors/404.html b/website/static/bunnycdn_errors/404.html index fa0884dd9..886affe06 100644 --- a/website/static/bunnycdn_errors/404.html +++ b/website/static/bunnycdn_errors/404.html @@ -12,7 +12,7 @@ --surface-2: #efece7; --ink: #22252a; --ink-2: #5f5c58; - --ink-3: #918d86; + --ink-3: #76716a; --rule: #e4e0d9; --signal: #01b5e2; --signal-ink: #0a7899; @@ -32,7 +32,7 @@ --surface-2: #282a2e; --ink: #f2efeb; --ink-2: #b3aea7; - --ink-3: #8b867f; + --ink-3: #96918a; --rule: #303235; --signal-ink: #4fcdec; --glow: rgb(1 181 226 / 30%); @@ -46,7 +46,7 @@ --surface-2: #282a2e; --ink: #f2efeb; --ink-2: #b3aea7; - --ink-3: #8b867f; + --ink-3: #96918a; --rule: #303235; --signal-ink: #4fcdec; --glow: rgb(1 181 226 / 30%);