diff --git a/.changeset/examples-markdown-passthrough.md b/.changeset/examples-markdown-passthrough.md
new file mode 100644
index 00000000..833b39fe
--- /dev/null
+++ b/.changeset/examples-markdown-passthrough.md
@@ -0,0 +1,5 @@
+---
+'@pmndrs/docs': patch
+---
+
+Pass the example gallery through as published rather than rendering it here. `pmndrs/examples` now writes the documents at build time and links each one from its page with `rel="alternate"`, so `examples://index` and `get_example` hand on the same text an agent would get from the open web — one rendering instead of two that could drift. The agents page documents both tools.
diff --git a/docs/agents/introduction.mdx b/docs/agents/introduction.mdx
index 4400068c..e6baa196 100644
--- a/docs/agents/introduction.mdx
+++ b/docs/agents/introduction.mdx
@@ -31,6 +31,8 @@ One endpoint for all libraries, over streamable HTTP: `https://docs.pmnd.rs/api/
| resource | `docs://pmndrs/manifest` | which libraries are served, and how to query them |
| resource | `docs://{lib}/index` | that library's pages, one `{path} - {title}` per line |
| tool | `get_page_content` | the markdown of a single page, given a `lib` + a `path` from the index |
+| resource | `examples://index` | the [example gallery](https://pmndrs.github.io/examples), one line per demo |
+| tool | `get_example` | one demo in full, given a `name` from that index |
A typical round-trip — *"how do I use TypeScript with Zustand?"*:
@@ -44,6 +46,34 @@ call get_page_content(lib="zustand", path="/learn/guides/beginner-typescript")
Two pages transferred instead of the whole site.
+### Examples
+
+The docs say what an API takes. Most three.js questions are *how is this put together*, and the [gallery](https://pmndrs.github.io/examples) has 167 demos that answer it — so the same endpoint serves them, on the same read-the-index-then-fetch shape.
+
+```txt
+read examples://index
+ → [...] caustics · #transmission [...]
+ diamond-refraction · Diamon refraction material · +postprocessing,leva · #refraction,bvh
+
+call get_example(name="caustics")
+ → the demo: its source, its live URL, its asset attribution, and the exact
+ dependency versions the code is written against
+```
+
+An index line drops whatever the demo does not carry, so it costs what it is worth. `+` lists only what a demo uses on top of `@react-three/fiber` and `@react-three/drei`, which all of them use. A trailing `~23k` is what `get_example` will cost in tokens — only eight demos carry one, and its absence means the demo is small enough to open without weighing it.
+
+An example is a snapshot, not a spec: it pins its own versions, `get_example` reports them, and an answer that turns on a current signature still belongs in the docs above.
+
+> [!NOTE]
+>
+> These documents are published, not rendered here — `pmndrs/examples` writes them at build time and every example page links its own:
+>
+> ```html
+>
+> ```
+>
+> So an agent that lands on an example page, or is handed its URL, reads the same text without going through MCP at all.
+
> [!TIP]
>
> [`pmndrs/claude-code-plugin`](https://github.com/pmndrs/claude-code-plugin) supports it natively
@@ -65,7 +95,7 @@ In the web UI it opens: **Add Servers** › **Add manually**, transport `streama
### Getting your library served
-Only libraries publishing a `/llms-full.txt` dump are served — the ones badged `MCP` on [docs.pmnd.rs](https://docs.pmnd.rs). To join them:
+Only libraries publishing a `/llms-full.txt` dump are served — the ones badged `MCP` on [docs.pmnd.rs](https://docs.pmnd.rs). The examples gallery is not one of them; it publishes its own catalog, and the two resources above are wired to it directly. To join the libraries:
1. **Publish the dump.** If your site is already built with this generator, bump it to a version shipping `/llms-full.txt` and redeploy. Otherwise, either migrate to this generator, or emit that file yourself with the same XML shape (`…`).
diff --git a/src/app/api/[transport]/route.test.ts b/src/app/api/[transport]/route.test.ts
index f94d00f1..76d2693a 100644
--- a/src/app/api/[transport]/route.test.ts
+++ b/src/app/api/[transport]/route.test.ts
@@ -41,61 +41,36 @@ const llmsFullHandlers = Object.values(libs)
return http.get(`${origin}/llms-full.txt`, () => HttpResponse.text(mockLlmsFullTxt))
})
-// The examples catalog, as pmndrs/examples publishes it -- an index to pick from
-// and one file per example. Two entries is enough to tell "served the index" from
-// "served an example"; `src/utils/examples.test.ts` covers the rendering itself.
-const mockExampleIndex = {
- site: 'https://pmndrs.github.io/examples',
- count: 2,
- examples: [
- {
- name: 'caustics',
- title: 'Caustics',
- description: '',
- tags: ['transmission'],
- authors: ['Paul Henschel'],
- libraries: ['@react-three/drei', '@react-three/fiber'],
- source: 'https://codesandbox.io/s/szj6p7',
- demo: 'https://pmndrs.github.io/examples/examples/caustics',
- thumbnail: 'https://pmndrs.github.io/examples/caustics/thumbnail.webp',
- bytes: 4_000,
- },
- {
- name: 'arkanoid',
- title: 'Arkanoid',
- description: 'Simple arkanoid implementation using cannon physics.',
- tags: ['physics', 'game'],
- authors: ['Paul Henschel'],
- libraries: ['@react-three/fiber', '@react-three/cannon'],
- source: 'https://codesandbox.io/s/arkanoid',
- demo: 'https://pmndrs.github.io/examples/examples/arkanoid',
- thumbnail: 'https://pmndrs.github.io/examples/arkanoid/thumbnail.webp',
- bytes: 90_000,
- },
- ],
-}
-
-const mockExample = {
- ...mockExampleIndex.examples[0],
- repository: 'https://github.com/pmndrs/examples/tree/main/examples/caustics',
- install: 'npx degit pmndrs/examples/examples/caustics',
- dependencies: { '@react-three/drei': '10.7.8' },
- files: [{ path: 'src/App.tsx', content: 'const caustics = true' }],
- binaries: ['src/glass-transformed.glb'],
- oversized: [],
- assets: [],
-}
+// The examples catalog, as pmndrs/examples publishes it: markdown, already
+// rendered, one document per example plus the index. This server passes them on
+// untouched, so the fixtures are text and the assertions are about routing --
+// the rendering itself is tested where it is produced.
+const mockExampleIndex = `aquarium · #transmission
+arkanoid · Simple arkanoid implementation using cannon physics. · +cannon · #physics,game · ~23k
+`
+
+const mockExample = `# Caustics
+
+Demo: https://pmndrs.github.io/examples/examples/caustics
+Dependencies: @react-three/drei@10.7.8
+
+## src/App.tsx
+
+\`\`\`tsx
+const caustics = true
+\`\`\`
+`
// Setup MSW server
const server = setupServer(
...llmsFullHandlers,
- http.get('https://pmndrs.github.io/examples/catalog/index.json', () =>
- HttpResponse.json(mockExampleIndex),
+ http.get('https://pmndrs.github.io/examples/catalog/index.md', () =>
+ HttpResponse.text(mockExampleIndex),
),
- http.get('https://pmndrs.github.io/examples/catalog/caustics.json', () =>
- HttpResponse.json(mockExample),
+ http.get('https://pmndrs.github.io/examples/catalog/caustics.md', () =>
+ HttpResponse.text(mockExample),
),
// Hosts the standalone fetch-and-parse tests below call directly
@@ -558,15 +533,14 @@ Content with <special> characters & symbols.
it('serves the whole gallery as one line per example', async () => {
const body = await call('resources/read', { uri: 'examples://index' })
- // caustics' fixture is under the size threshold and arkanoid's is over it
- expect(body).toContain('caustics · #transmission')
+ expect(body).toContain('aquarium · #transmission')
expect(body).toContain(
'arkanoid · Simple arkanoid implementation using cannon physics. · +cannon · #physics,game · ~23k',
)
expect(body).not.toContain('MCP server error')
})
- it('serves one example with its source', async () => {
+ it('passes one example through as published', async () => {
const body = await call('tools/call', {
name: 'get_example',
arguments: { name: 'caustics' },
@@ -575,8 +549,6 @@ Content with <special> characters & symbols.
expect(body).toContain('# Caustics')
expect(body).toContain('const caustics = true')
expect(body).toContain('Dependencies: @react-three/drei@10.7.8')
- // Named, not inlined -- a reader that needs the model knows where it is
- expect(body).toContain('src/glass-transformed.glb')
expect(body).not.toContain('MCP server error')
})
@@ -594,7 +566,7 @@ Content with <special> characters & symbols.
it('errors, rather than serving an empty gallery, when the catalog is missing', async () => {
server.use(
- http.get('https://pmndrs.github.io/examples/catalog/index.json', () => {
+ http.get('https://pmndrs.github.io/examples/catalog/index.md', () => {
return new HttpResponse('Not Found', { status: 404 })
}),
)
diff --git a/src/app/api/[transport]/route.ts b/src/app/api/[transport]/route.ts
index 5de2a66d..4f5e3f7e 100644
--- a/src/app/api/[transport]/route.ts
+++ b/src/app/api/[transport]/route.ts
@@ -5,14 +5,7 @@ import { headers } from 'next/headers'
import { revalidateTag } from 'next/cache'
import { libs, type SUPPORTED_LIBRARY_NAMES } from '@/app/page'
import packageJson from '@/package.json' with { type: 'json' }
-import {
- assertExampleName,
- catalogUrl,
- renderExample,
- renderIndex,
- type Example,
- type ExampleIndex,
-} from '@/utils/examples'
+import { assertExampleName, catalogUrl } from '@/utils/examples'
// Extract entries and library names as constants for efficiency
// Only support libraries whose site actually publishes a /llms-full.txt dump -- see
@@ -31,11 +24,12 @@ async function baseUrl() {
}
/**
- * One file out of the examples catalog. Cached and tagged like the docs dumps,
- * except the catalog is already split per example, so a request pulls the ~20 kB
- * that was asked for rather than slicing it out of a bundle.
+ * One document out of the examples catalog, as pmndrs/examples published it.
+ * Cached and tagged like the docs dumps, except the catalog is already split per
+ * example and already rendered, so a request pulls the few kB that was asked for
+ * and passes it straight on.
*/
-async function fetchCatalog(file: string): Promise {
+async function fetchCatalog(file: string): Promise {
const response = await fetch(catalogUrl(file), {
next: { revalidate: 300, tags: ['examples-catalog'] },
})
@@ -44,7 +38,7 @@ async function fetchCatalog(file: string): Promise {
if (!response.ok) {
throw new Error(`Failed to fetch ${catalogUrl(file)}: ${response.statusText}`)
}
- return response.json() as Promise
+ return response.text()
}
const handler = createMcpHandler(
@@ -197,8 +191,10 @@ Always handle errors gracefully and consider alternative approaches when a speci
SSE transport would need a Redis instance to relay messages, which this deployment
does not have, so \`/api/sse\` is not usable.
- Documentation is parsed from XML-tagged full-text dumps (\`/llms-full.txt\`)
-- Examples come from the JSON catalog the gallery publishes at
- \`https://pmndrs.github.io/examples/catalog/\`, already split one file per example
+- Examples are passed through from the catalog the gallery publishes at
+ \`https://pmndrs.github.io/examples/catalog/\`, one already-rendered document per
+ example. They are public: every example page links its own with
+ \`rel="alternate"\`, so the same text is reachable without this server
### Security
- CSS selector injection protection via \`.filter()\` instead of direct selectors
@@ -329,13 +325,11 @@ Always handle errors gracefully and consider alternative approaches when a speci
mimeType: 'text/plain',
},
async () => {
- const index = await fetchCatalog('index')
-
return {
contents: [
{
uri: 'examples://index',
- text: renderIndex(index),
+ text: await fetchCatalog('index'),
},
],
}
@@ -420,13 +414,11 @@ Always handle errors gracefully and consider alternative approaches when a speci
try {
// The catalog is one file per example, so `name` reaches a URL. Keep it
// to the shape every published example has rather than trusting it.
- const example = await fetchCatalog(assertExampleName(name))
-
return {
content: [
{
type: 'text',
- text: renderExample(example),
+ text: await fetchCatalog(assertExampleName(name)),
},
],
}
diff --git a/src/utils/examples.test.ts b/src/utils/examples.test.ts
index 7f076e09..1ea6c80f 100644
--- a/src/utils/examples.test.ts
+++ b/src/utils/examples.test.ts
@@ -1,218 +1,45 @@
import { describe, it, expect } from 'vitest'
-import {
- assertExampleName,
- catalogUrl,
- renderExample,
- renderIndex,
- summaryLine,
- type Example,
- type ExampleSummary,
-} from './examples'
+import { assertExampleName, catalogUrl } from './examples'
-const summary = (over: Partial = {}): ExampleSummary => ({
- name: 'caustics',
- title: 'Caustics',
- description: '',
- tags: [],
- authors: ['Paul Henschel'],
- libraries: ['@react-three/drei', '@react-three/fiber'],
- source: 'https://codesandbox.io/s/szj6p7',
- demo: 'https://pmndrs.github.io/examples/examples/caustics',
- thumbnail: 'https://pmndrs.github.io/examples/caustics/thumbnail.webp',
- bytes: 4_000,
- ...over,
-})
-
-const example = (over: Partial = {}): Example => ({
- ...summary(),
- repository: 'https://github.com/pmndrs/examples/tree/main/examples/caustics',
- install: 'npx degit pmndrs/examples/examples/caustics',
- dependencies: { '@react-three/drei': '10.7.8', three: '0.165.0' },
- files: [{ path: 'src/App.tsx', content: 'export default function App() {}' }],
- binaries: [],
- oversized: [],
- assets: [],
- ...over,
-})
-
-describe('summaryLine', () => {
- it('drops a title that is just the prettified name', () => {
- expect(summaryLine(summary())).toBe('caustics')
- })
-
- it('keeps a title that carries something the name cannot', () => {
- expect(
- summaryLine(summary({ name: 'bounds-and-makedefault', title: 'Bounds and makeDefault' })),
- ).toBe('bounds-and-makedefault (Bounds and makeDefault)')
- })
-
- it('omits fiber and drei, which every example uses', () => {
- const line = summaryLine(
- summary({
- libraries: ['@react-three/fiber', '@react-three/drei', '@react-three/cannon', 'zustand'],
- }),
- )
-
- expect(line).toBe('caustics · +cannon,zustand')
- })
-
- it('collapses the react-spring entry points into one name', () => {
- const line = summaryLine(
- summary({ libraries: ['@react-spring/three', '@react-spring/web', '@react-spring/core'] }),
- )
-
- expect(line).toBe('caustics · +react-spring')
- })
-
- it('assembles description, libraries and tags in that order', () => {
- const line = summaryLine(
- summary({
- name: 'arkanoid',
- title: 'Arkanoid',
- description: 'Simple arkanoid implementation using cannon physics.',
- libraries: ['@react-three/fiber', '@react-three/cannon'],
- tags: ['physics', 'game'],
- }),
- )
-
- expect(line).toBe(
- 'arkanoid · Simple arkanoid implementation using cannon physics. · +cannon · #physics,game',
- )
- })
-
- it('flattens a description that wraps', () => {
- const line = summaryLine(summary({ description: 'One idea,\n spread over lines.' }))
-
- expect(line).toBe('caustics · One idea, spread over lines.')
- })
-
- it('says nothing about size for an example that is cheap to open', () => {
- // The marker has to stay rare to mean anything: its absence is the signal
- // that a reader can open two of these without thinking about the budget.
- expect(summaryLine(summary({ bytes: 24 * 1024 }))).toBe('caustics')
- })
-
- it('marks an example large enough that opening it is a decision', () => {
- expect(summaryLine(summary({ bytes: 90_048 }))).toBe('caustics · ~23k')
- })
-
- it('puts the size last, after everything that helps choose', () => {
- const line = summaryLine(summary({ description: 'A shield.', tags: ['shader'], bytes: 90_048 }))
-
- expect(line).toBe('caustics · A shield. · #shader · ~23k')
- })
-})
-
-describe('renderIndex', () => {
- it('is one line per example', () => {
- const text = renderIndex({
- site: 'https://pmndrs.github.io/examples',
- count: 2,
- examples: [summary(), summary({ name: 'aquarium', title: 'Aquarium', tags: ['water'] })],
- })
-
- expect(text).toBe('caustics\naquarium · #water')
- })
-})
+/**
+ * The rendering these used to cover now lives in pmndrs/examples, which
+ * publishes the documents this server passes on (`test/render.test.ts` there).
+ * What is left is the part that belongs to a server taking a name from a model:
+ * that name reaches a URL only if it is the shape a published example has.
+ */
describe('assertExampleName', () => {
it('passes the shape every published example has', () => {
expect(assertExampleName('gltfjsx-400kb-drone')).toBe('gltfjsx-400kb-drone')
})
- it.each(['../../etc/passwd', 'Caustics', 'a b', 'caustics/../index', '', 'caustics?x=1'])(
- 'rejects %j before it can reach a URL',
- (name) => {
- expect(() => assertExampleName(name)).toThrow(/Not an example name/)
- },
- )
+ it.each([
+ '../../etc/passwd',
+ 'caustics/../index',
+ 'caustics?x=1',
+ 'caustics#fragment',
+ 'Caustics',
+ 'a b',
+ '',
+ ])('rejects %j before it can reach a URL', (name) => {
+ expect(() => assertExampleName(name)).toThrow(/Not an example name/)
+ })
it('rejects "index", which is the one name that collides with the catalog itself', () => {
- // Legal kebab-case, same directory: it would fetch the index and then be
- // rendered as an example with no title and no files.
+ // Legal kebab-case, same directory: it would serve the index of the whole
+ // gallery under the guise of a single example.
expect(() => assertExampleName('index')).toThrow(/Not an example name/)
})
-
- it('puts a name where the catalog publishes it', () => {
- expect(catalogUrl('caustics')).toBe('https://pmndrs.github.io/examples/catalog/caustics.json')
- })
})
-describe('renderExample', () => {
- it('leads with the title and the facts a reader needs', () => {
- const text = renderExample(example({ description: 'Glass, and what it does to light.' }))
-
- expect(text).toContain('# Caustics')
- expect(text).toContain('Glass, and what it does to light.')
- expect(text).toContain('Demo: https://pmndrs.github.io/examples/examples/caustics')
- expect(text).toContain('Scaffold: npx degit pmndrs/examples/examples/caustics')
- expect(text).toContain('Dependencies: @react-three/drei@10.7.8, three@0.165.0')
- })
-
- it('omits the facts an example does not carry', () => {
- const text = renderExample(example({ authors: [], tags: [] }))
-
- expect(text).not.toContain('Authors:')
- expect(text).not.toContain('Tags:')
- })
-
- it('fences each file under its own path, tagged by extension', () => {
- const text = renderExample(
- example({
- files: [
- { path: 'src/App.tsx', content: 'const a = 1' },
- { path: 'src/styles.css', content: 'body { margin: 0 }' },
- ],
- }),
- )
-
- expect(text).toContain('## src/App.tsx\n\n```tsx\nconst a = 1\n```')
- expect(text).toContain('## src/styles.css\n\n```css\nbody { margin: 0 }\n```')
- })
-
- it('opens a longer fence than the backticks inside the file', () => {
- // A file whose comments quote code would otherwise close the block early and
- // hand the reader half a file plus whatever followed it as prose.
- const text = renderExample(
- example({ files: [{ path: 'src/App.tsx', content: '// ```tsx\nconst a = 1' }] }),
- )
-
- expect(text).toContain('````tsx\n// ```tsx\nconst a = 1\n````')
- })
-
- it('names the binaries rather than pretending they are not there', () => {
- const text = renderExample(example({ binaries: ['src/glass-transformed.glb'] }))
-
- expect(text).toContain('src/glass-transformed.glb')
+describe('catalogUrl', () => {
+ it('points at the markdown the gallery publishes, not the JSON beside it', () => {
+ expect(catalogUrl('caustics')).toBe('https://pmndrs.github.io/examples/catalog/caustics.md')
})
- it('says which files were skipped on size, and how big they are', () => {
- const text = renderExample(
- example({ oversized: [{ path: 'src/realism-effects/v2.js', bytes: 256656 }] }),
- )
-
- expect(text).toContain(
- 'Too large to inline, in the repository: src/realism-effects/v2.js (251 kB)',
- )
- })
-
- it('carries asset attribution through', () => {
- const text = renderExample(
- example({
- assets: [
- {
- name: 'Fruit Cake Slice',
- creator: 'matousekfoto',
- license: 'CC-BY-4.0',
- source: 'https://sketchfab.com/3d-models/fruit-cake-slice',
- },
- ],
- }),
- )
-
- expect(text).toContain('## Asset attribution')
- expect(text).toContain(
- '- Fruit Cake Slice — by matousekfoto — CC-BY-4.0 (https://sketchfab.com/3d-models/fruit-cake-slice)',
+ it('follows a local build when one is given', () => {
+ expect(catalogUrl('index', 'http://localhost:3001')).toBe(
+ 'http://localhost:3001/catalog/index.md',
)
})
})
diff --git a/src/utils/examples.ts b/src/utils/examples.ts
index e79a5e1d..d004af51 100644
--- a/src/utils/examples.ts
+++ b/src/utils/examples.ts
@@ -1,19 +1,21 @@
/**
- * Reads the catalog that pmndrs/examples publishes alongside its website
- * (`bin/build-catalog.mjs` there), so the MCP server can serve the example
- * gallery next to the docs.
+ * The examples gallery, as served by this MCP server.
*
- * The docs are page dumps parsed out of one `llms-full.txt` per library; the
- * examples are not. They arrive as JSON, already split into an index and one
- * file per example, so nothing here has to parse or slice a bundle -- it only
- * has to render the pieces as the text an agent reads.
+ * There is very little here on purpose. pmndrs/examples publishes the documents
+ * an agent reads -- `/catalog/index.md` and `/catalog/.md` -- already
+ * rendered, because they are published for the open web too: every example page
+ * points at its markdown with `rel="alternate"`, and a static host cannot render
+ * on demand. So this server hands them on verbatim rather than building a second
+ * rendering that would drift from the first.
+ *
+ * That leaves one job worth doing here: making sure a name from a model reaches
+ * a URL only if it is the shape a published example has.
*/
/**
- * Where the gallery lives. Overridable so the MCP server can be developed against
- * a local `pnpm build` of pmndrs/examples -- the catalog only exists once that
- * repo has built, and pointing at production while changing its shape tests the
- * old shape.
+ * Where the gallery lives. Overridable so the MCP server can be developed
+ * against a local `pnpm build` of pmndrs/examples -- the catalog only exists
+ * once that repo has built.
*/
export const EXAMPLES_URL = process.env.EXAMPLES_URL || 'https://pmndrs.github.io/examples'
@@ -21,75 +23,13 @@ export const EXAMPLES_URL = process.env.EXAMPLES_URL || 'https://pmndrs.github.i
const EXAMPLE_NAME = /^[a-z0-9]+(?:-[a-z0-9]+)*$/
/**
- * `index` is legal kebab-case and sits in the same directory, so it would fetch
- * the index and then be rendered as an example that has no title and no files.
+ * `index` is legal kebab-case and sits in the same directory, so it would serve
+ * the index of the whole gallery under the guise of one example.
*/
const RESERVED_NAME = 'index'
-export interface ExampleSummary {
- name: string
- title: string
- description: string
- tags: string[]
- authors: string[]
- publishedAt?: string
- notes?: string
- libraries: string[]
- source: string
- demo: string
- thumbnail: string
- bytes: number
-}
-
-export interface ExampleIndex {
- site: string
- count: number
- examples: ExampleSummary[]
-}
-
-export interface Example extends ExampleSummary {
- repository: string
- install: string
- dependencies: Record
- files: { path: string; content: string }[]
- binaries: string[]
- oversized: { path: string; bytes: number }[]
- assets: {
- name: string
- files?: string[]
- creator?: string
- source?: string
- license?: string
- licenseUrl?: string
- modified?: boolean
- notes?: string
- }[]
-}
-
-/**
- * Every example depends on these, so spelling them out on 167 index lines says
- * nothing. The per-example view lists the real dependencies, versions included.
- */
-const IMPLIED_LIBRARIES = new Set(['@react-three/fiber', '@react-three/drei'])
-
-const SHORT_LIBRARY_NAMES: Record = {
- '@react-spring/core': 'react-spring',
- '@react-spring/three': 'react-spring',
- '@react-spring/web': 'react-spring',
- '@use-gesture/react': 'use-gesture',
-}
-
-const shortenLibrary = (library: string) =>
- SHORT_LIBRARY_NAMES[library] ?? library.replace(/^@react-three\//, '')
-
-const titleCase = (name: string) =>
- name
- .split('-')
- .map((word) => word[0].toUpperCase() + word.slice(1))
- .join(' ')
-
export function catalogUrl(file: string, base = EXAMPLES_URL) {
- return `${base}/catalog/${file}.json`
+ return `${base}/catalog/${file}.md`
}
export function assertExampleName(name: string) {
@@ -98,125 +38,3 @@ export function assertExampleName(name: string) {
}
return name
}
-
-/**
- * Where an example stops being cheap. The median is 5 kB and nine tenths are
- * under 15 kB, so reading three of them costs less than this index does -- the
- * size is worth saying only for the handful where it changes the decision.
- * Eight examples are over this line; two of them are most of the distance.
- */
-const LARGE_BYTES = 24 * 1024
-
-/**
- * One index line. Everything past the name is optional and omitted when the
- * example does not carry it, so an entry costs what it is worth -- and a line
- * with no size marker is one you can open without thinking about it:
- *
- * aquarium · #transmission
- * arkanoid · Simple arkanoid implementation using cannon physics. · +cannon,zustand · #physics,game
- * bounds-and-makedefault (Bounds and makeDefault) · #bounds
- * flow-shield · Interactive energy shield. · +postprocessing,leva · #shader · ~23k
- */
-export function summaryLine({ name, title, description, libraries, tags, bytes }: ExampleSummary) {
- // A title that is just the prettified directory name is noise -- but not
- // always: `makeDefault`, `GLTF`, `Bruno Simon's` only exist in the title.
- const head = title && title !== titleCase(name) ? `${name} (${title})` : name
-
- const extras = [
- ...new Set(libraries.filter((l) => !IMPLIED_LIBRARIES.has(l)).map(shortenLibrary)),
- ]
-
- return [
- head,
- description.trim().replace(/\s+/g, ' '),
- extras.length ? `+${extras.join(',')}` : '',
- tags.length ? `#${tags.join(',')}` : '',
- bytes > LARGE_BYTES ? `~${Math.round(bytes / 4000)}k` : '',
- ]
- .filter(Boolean)
- .join(' · ')
-}
-
-export function renderIndex({ examples }: ExampleIndex) {
- return examples.map(summaryLine).join('\n')
-}
-
-const FENCE_LANGUAGES: Record = {
- '.ts': 'ts',
- '.tsx': 'tsx',
- '.js': 'js',
- '.jsx': 'jsx',
- '.mjs': 'js',
- '.cjs': 'js',
- '.css': 'css',
- '.json': 'json',
- '.glsl': 'glsl',
- '.vert': 'glsl',
- '.frag': 'glsl',
-}
-
-/** A fence long enough to survive whatever backticks the file itself contains. */
-function fence(content: string) {
- const longest = Math.max(0, ...(content.match(/`+/g) ?? []).map((run) => run.length))
- return '`'.repeat(Math.max(3, longest + 1))
-}
-
-function attribution(assets: Example['assets']) {
- return assets
- .map((asset) => {
- const parts = [asset.name]
- if (asset.creator) parts.push(`by ${asset.creator}`)
- if (asset.license) parts.push(asset.license)
- if (asset.modified) parts.push('modified')
- const line = `- ${parts.join(' — ')}`
- return asset.source || asset.licenseUrl
- ? `${line} (${asset.source ?? asset.licenseUrl})`
- : line
- })
- .join('\n')
-}
-
-/**
- * The whole example as one document: what it is, what it pins, then its source.
- * Versions are part of the answer -- the code is written against the `three` and
- * drei that sit next to it, and reading it without them invites an API that
- * moved.
- */
-export function renderExample(example: Example) {
- const facts = [
- `Demo: ${example.demo}`,
- `Source: ${example.repository}`,
- `Scaffold: ${example.install}`,
- example.publishedAt && `Published: ${example.publishedAt}`,
- example.authors.length && `Authors: ${example.authors.join(', ')}`,
- example.tags.length && `Tags: ${example.tags.join(', ')}`,
- `Ported from: ${example.source}`,
- `Dependencies: ${Object.entries(example.dependencies)
- .map(([name, version]) => `${name}@${version}`)
- .join(', ')}`,
- example.binaries.length &&
- `Binary files, in the repository but not below: ${example.binaries.join(', ')}`,
- // Vendored bundles, font atlases, gltfjsx dumps. Named rather than hidden:
- // a reader that finds an unexplained import wants to know it was skipped on
- // size, not wonder whether the example is broken.
- example.oversized.length &&
- `Too large to inline, in the repository: ${example.oversized
- .map(({ path, bytes }) => `${path} (${Math.round(bytes / 1024)} kB)`)
- .join(', ')}`,
- ].filter(Boolean)
-
- const sections = [
- `# ${example.title}`,
- example.description.trim(),
- facts.join('\n'),
- example.notes?.trim(),
- example.assets.length && `## Asset attribution\n\n${attribution(example.assets)}`,
- ...example.files.map((file) => {
- const language = FENCE_LANGUAGES[file.path.slice(file.path.lastIndexOf('.'))] ?? ''
- const marks = fence(file.content)
- return `## ${file.path}\n\n${marks}${language}\n${file.content.trimEnd()}\n${marks}`
- }),
- ].filter(Boolean)
-
- return sections.join('\n\n')
-}