Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .github/workflows/website.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@ on:
paths:
- 'docs/**'
- 'schema/**'
- 'package.json'
- '.github/workflows/website.yml'
pull_request:
branches: [main]
paths:
- 'docs/**'
- 'schema/**'
- 'package.json'
- '.github/workflows/website.yml'
workflow_dispatch:

Expand Down Expand Up @@ -51,7 +53,7 @@ jobs:
run: pnpm build
- name: Verify static website
run: |
for file in index.html zh/index.html en/index.html zh/guide/getting-started/index.html pagefind/pagefind.js pagefind/pagefind-entry.json schema/preset.schema.json CNAME; do
for file in index.html zh/index.html en/index.html zh/guide/getting-started/index.html pagefind/pagefind.js pagefind/pagefind-entry.json schema/preset.schema.json llms.txt llms-full.txt agent/guide/getting-started.md agent/reference/preset-configs.md CNAME; do
test -s ".next-static/$file"
done
test -d .next-static/_next/static
Expand Down
3 changes: 3 additions & 0 deletions docs/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,6 @@ node_modules/
out/
tsconfig.tsbuildinfo
/public/schema/preset.schema.json
/public/agent/
/public/llms.txt
/public/llms-full.txt
48 changes: 47 additions & 1 deletion docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,56 @@ pnpm build
pnpm start
```

Keep tests in a nearby `__tests__/` directory, such as
`src/locales/__tests__/locales.test.ts` or `scripts/__tests__/agent-docs.test.mjs`,
rather than beside production files. `pnpm test` runs all source tests with Vitest
and script tests with Node.js.

`pnpm build` writes the static site and Pagefind search index to `.next-static`.
Development and production builds copy `../schema/preset.schema.json` into the
public site, so the CLI and website share one schema source.

## UI translations

Shared interface messages live in `src/locales/en.json` and `zh.json`, grouped
by module (`home`, `examples`, `search`, `agent`, etc.). Components and helpers
read them through `getMessages(locale)` from `@/locales`; this works in both
Server and Client Components without a provider:

```ts
const { agent } = getMessages(locale)
```

Add new interface copy to the same module in both JSON files. English defines
the TypeScript message shape; type checks and locale tests catch missing fields.
`src/locales/config.ts` owns the supported language definitions and default
language used by the docs framework. `resolveLocale` accepts region tags such
as `zh-CN` and falls back to English for unsupported languages.

Markdown/MDX articles remain in `src/contents/<locale>`. The docs framework's
navigation/theme configuration keeps its native localized-label format.

## Agent documentation

`pnpm dev` and `pnpm build` generate one English `/llms.txt` index,
`/llms-full.txt` bundle, and Markdown pages under `/agent/`. The guides and
examples come from `src/contents/en`; full references come from the two reference
documents in this directory. Command tabs and interactive examples expand from
the same command data and fixtures used by the website. Generated files are not
committed. After editing content during development, run `pnpm build:agent-docs`
to refresh them; production builds always regenerate them.

The homepage's secondary links and guide/example titles offer a localized **Copy for AI**
button. Prompts follow the website language but always link to the same English
index and corresponding English Markdown page. All prompts use
the public production domain so they can be shared outside the current browser.
The task placeholder is intended to be filled in before sending to an agent.

When adding interactive MDX to guides or examples, add a plain Markdown renderer
in `scripts/build-agent-docs.mjs`. Unsupported blocks fail generation rather than
silently dropping useful content. Keep prose links outside code blocks: exported
links resolve to the published Markdown pages, while sample code is preserved.

## Social previews

Every canonical page gets Open Graph and X Card metadata from its existing
Expand All @@ -52,7 +98,7 @@ The workflow uses the repository's `ACCESS_TOKEN` secret to push the generated
files and trigger the Pages deployment. The token needs write access to this
repository. Pull requests only build and verify the website.

Each build checks the homepages, guide, search index, schema and `CNAME`, and
Each build checks the homepages, guide, search index, schema, agent docs and `CNAME`, and
keeps a `website-static` download artifact for seven days. After publication,
inspect `gh-pages` for `index.html`, `zh/`, `_next/`, `pagefind/`, `schema/`,
`CNAME` and `.nojekyll`. The deployment commit includes the source commit SHA.
Expand Down
16 changes: 3 additions & 13 deletions docs/content.config.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,11 @@
import { type DocsContentConfig } from '@blackwork/docs'
import { defaultLocale, localeDefinitions } from './src/locales/config'

export const docsContentConfig = {
root: 'src/contents',
defaultLocale: 'en',
defaultLocale,
enableDefaultLocaleRedirect: true,
locales: {
en: {
code: 'en',
lang: 'en-US',
label: 'English',
},
zh: {
code: 'zh',
lang: 'zh-CN',
label: '简体中文',
},
},
locales: localeDefinitions,
sections: {
showcase: { layout: 'content' },
examples: {
Expand Down
7 changes: 4 additions & 3 deletions docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@
"type": "module",
"packageManager": "pnpm@12.4.2",
"scripts": {
"dev": "node scripts/sync-schema.mjs && next dev --port 3300",
"build": "node scripts/sync-schema.mjs && NEXT_OUTPUT=export NEXT_DIST_DIR=.next-static next build && pnpm run build:search",
"dev": "node scripts/sync-schema.mjs && pnpm run build:agent-docs && next dev --port 3300",
"build": "node scripts/sync-schema.mjs && pnpm run build:agent-docs && NEXT_OUTPUT=export NEXT_DIST_DIR=.next-static next build && pnpm run build:search",
"build:agent-docs": "node scripts/build-agent-docs.mjs",
"build:search": "node ./scripts/build-search-index.mjs",
"build:static": "pnpm run build",
"start": "pnpm run start:static",
"start:static": "pnpm dlx serve .next-static --listen 4300",
"lint": "tsc --noEmit",
"test": "vitest run"
"test": "vitest run && node --test scripts/__tests__/*.test.mjs"
},
"dependencies": {
"@bassist/utils": "^0.17.0",
Expand Down
72 changes: 72 additions & 0 deletions docs/scripts/__tests__/agent-docs.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import assert from 'node:assert/strict'
import { mkdir, mkdtemp, readFile, rm, writeFile, access } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import test from 'node:test'
import { buildAgentDocs, toMarkdown } from '../build-agent-docs.mjs'

const options = { commands: {}, fixtures: {}, links: new Map([['/guide/example', '/agent/guide/example.md']]) }

test('rewrites documentation links without changing fenced example content', () => {
const example = '```md\n[Sample](/guide/example)\n<CustomComponent />\n```'
const input = `[Guide](/guide/example#heading)\n\n${example}\n\n[Schema](/schema/preset.schema.json)`
const output = toMarkdown(input, options)
assert.ok(output.includes('[Guide](https://preset.js.org/agent/guide/example.md#heading)'))
assert.ok(output.includes(example))
assert.ok(output.includes('[Schema](https://preset.js.org/schema/preset.schema.json)'))
})

test('fails on unsupported MDX instead of silently omitting information', () => {
for (const input of ['<NewExample />', '<ExampleExplorer example="missing" />', '<PackageManagerCommands mode="unknown" />', '<PackageManagerCommands mode={mode} />']) {
assert.throws(() => toMarkdown(input, options))
}
})

test('publishes one English documentation set with resolving links and current example data', async (t) => {
const output = await mkdtemp(join(tmpdir(), 'preset-agent-docs-'))
t.after(() => rm(output, { recursive: true, force: true }))
const pages = await buildAgentDocs(output)
const fixtures = JSON.parse(await readFile(new URL('../../src/examples/fixtures.json', import.meta.url), 'utf8'))
const commands = JSON.parse(await readFile(new URL('../../src/mdx/package-manager-commands.json', import.meta.url), 'utf8'))
const outputs = new Set(pages.map((page) => page.path))
outputs.add('/llms.txt')
outputs.add('/llms-full.txt')
const quickstart = await readFile(join(output, 'agent/guide/getting-started.md'), 'utf8')
for (const group of Object.values(commands)) {
for (const { command } of group) assert.ok(quickstart.includes(command))
}
const index = await readFile(join(output, 'llms.txt'), 'utf8')
assert.ok(index.length < 6000, 'index stays concise')
const bundle = await readFile(join(output, 'llms-full.txt'), 'utf8')
for (const page of pages) {
assert.ok(index.includes(`https://preset.js.org${page.path}`))
assert.ok(bundle.includes(page.markdown))
}
for (const [name, fixture] of Object.entries(fixtures)) {
const markdown = await readFile(join(output, `agent/examples/${name}.md`), 'utf8')
for (const preset of fixture.presets) {
assert.ok(markdown.includes(JSON.stringify(preset.config, null, 2)))
for (const content of Object.values(preset.outputFiles)) assert.ok(markdown.includes(content.trimEnd()))
}
for (const content of Object.values(fixture.sourceFiles)) assert.ok(markdown.includes(content.trimEnd()))
}
for (const path of outputs) {
const content = await readFile(join(output, path), 'utf8')
assert.doesNotMatch(content, /<(PackageManagerCommands|ExampleExplorer)\b/)
for (const match of content.matchAll(/\]\(https:\/\/preset\.js\.org([^\s)#]+)(?:#[^\s)]*)?\)/g)) {
const target = match[1]
if (target === '/schema/preset.schema.json') continue
assert.ok(outputs.has(target), `unresolved generated link in ${path}: ${target}`)
}
}
// Content removal must not leave obsolete published pages behind.
const stale = join(output, 'agent/removed.md')
await writeFile(stale, 'obsolete')
await mkdir(join(output, 'zh'), { recursive: true })
await writeFile(join(output, 'zh/llms.txt'), 'obsolete')
await writeFile(join(output, 'zh/llms-full.txt'), 'obsolete')
await buildAgentDocs(output)
for (const path of ['agent/removed.md', 'zh/llms.txt', 'zh/llms-full.txt', 'agent/en', 'agent/zh']) {
await assert.rejects(access(join(output, path)))
}
})
136 changes: 136 additions & 0 deletions docs/scripts/build-agent-docs.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import { mkdir, readFile, readdir, rm, writeFile } from 'node:fs/promises'
import { dirname, resolve } from 'node:path'
import { fileURLToPath, pathToFileURL } from 'node:url'
import { parseFrontmatter } from '@blackwork/machine'

const root = fileURLToPath(new URL('../', import.meta.url))
const site = 'https://preset.js.org'
const readJson = async (path) => JSON.parse(await readFile(resolve(root, path), 'utf8'))

const codeBlock = (content, language = '') => {
const longest = Math.max(2, ...(content.match(/`+/g) ?? []).map((run) => run.length))
const fence = '`'.repeat(longest + 1)
return `${fence}${language}\n${content.trimEnd()}\n${fence}`
}

const renderExample = (fixture) => {
const files = (entries) => Object.entries(entries).map(([path, content]) =>
`#### ${path}\n\n${codeBlock(content, path.endsWith('.json') ? 'json' : '')}`,
).join('\n\n')
return [
'> Fictional repository and commands. Replace them with your actual repository and config paths.',
'### Source files',
files(fixture.sourceFiles),
...fixture.presets.flatMap((preset) => [
`### ${preset.label.en}`,
codeBlock(`pnpm create preset init ${preset.target} --from ./presets/${preset.id}.json`, 'bash'),
`#### presets/${preset.id}.json`,
codeBlock(JSON.stringify(preset.config, null, 2), 'json'),
`#### Output directory: ${preset.target}`,
files(preset.outputFiles),
]),
].join('\n\n')
}

// These are the only interactive blocks in the exported guides/examples.
// Reject new MDX constructs instead of silently publishing incomplete docs.
export const toMarkdown = (body, { commands, fixtures, links }) => {
let fence
return body.split('\n').map((line) => {
const marker = line.match(/^\s{0,3}(`{3,}|~{3,})(.*)$/)
if (fence) {
if (marker && marker[1][0] === fence[0] && marker[1].length >= fence.length && !marker[2].trim()) fence = undefined
return line
}
if (marker) {
fence = marker[1]
return line
}
const component = line.match(/^\s*<(PackageManagerCommands|ExampleExplorer)\b([^<>]*?)\s*\/>\s*$/)
if (component) {
const attrs = Object.fromEntries([...component[2].matchAll(/(\w+)="([^"]*)"/g)].map((match) => [match[1], match[2]]))
const allowed = component[1] === 'PackageManagerCommands' ? ['mode', 'locale'] : ['example', 'locale']
const remaining = component[2].replace(/\w+="[^"]*"/g, '').trim()
if (remaining || Object.keys(attrs).some((key) => !allowed.includes(key))) throw new Error(`Unsupported MDX attributes: ${line}`)
if (component[1] === 'PackageManagerCommands') {
const group = commands[attrs.mode ?? 'create']
if (!group) throw new Error(`Unknown command mode: ${attrs.mode}`)
return group.map(({ label, command }) => `${label}:\n\n${codeBlock(command, 'bash')}`).join('\n\n')
}
const fixture = fixtures[attrs.example]
if (!fixture) throw new Error(`Unknown example: ${attrs.example}`)
return renderExample(fixture)
}
if (/^\s*(?:<\/?[A-Za-z]|import\s|export\s|\{)/.test(line)) throw new Error(`Unsupported MDX in agent docs: ${line}`)
// Rewrite prose links only: example code must remain byte-for-byte intact.
return line.replace(/\]\(([^\s)]+)\)/g, (match, href) => {
const [path, hash] = href.split('#')
const target = links.get(path.replace(/\/$/, ''))
if (target) return `](${site}${target}${hash ? `#${hash}` : ''})`
if (href.startsWith('/') && !href.startsWith('//')) return `](${site}${href})`
return match
})
}).join('\n').trim() + '\n'
}

export const buildAgentDocs = async (output = resolve(root, 'public')) => {
const [commands, fixtures, pkg] = await Promise.all([
readJson('src/mdx/package-manager-commands.json'),
readJson('src/examples/fixtures.json'),
readJson('../package.json'),
])
const pages = []
for (const section of ['guide', 'examples']) {
const directory = resolve(root, 'src/contents/en', section)
for (const filename of (await readdir(directory)).filter((name) => /\.mdx?$/.test(name)).sort()) {
const { frontmatter, body } = parseFrontmatter(await readFile(resolve(directory, filename), 'utf8'))
const slug = `${section}/${filename.replace(/\.mdx?$/, '')}`
pages.push({ section, title: frontmatter.title, description: frontmatter.description, order: frontmatter.order ?? 0, body,
href: `/${slug}`, path: `/agent/${slug}.md` })
}
}
for (const name of ['preset-configs', 'private-presets']) {
const body = await readFile(resolve(root, `${name}.md`), 'utf8')
pages.push({ section: 'reference', title: name, body, href: `https://github.com/preset-cli/create-preset/blob/main/docs/${name}.md`, path: `/agent/reference/${name}.md` })
}
const links = new Map(pages.map((page) => [page.href, page.path]))
for (const page of pages.filter((page) => page.section === 'reference')) links.set(`${page.title}.md`, page.path)
const rendered = pages.map((page) => ({ ...page, markdown: toMarkdown(page.body, { commands, fixtures, links }) }))

// This directory is generated exclusively by this script; remove stale pages.
await rm(resolve(output, 'agent'), { recursive: true, force: true })
// Remove files generated by the previous bilingual implementation.
for (const path of ['zh/llms.txt', 'zh/llms-full.txt']) {
await rm(resolve(output, path), { force: true })
}
const write = async (path, content) => {
const target = resolve(output, `.${path}`)
await mkdir(dirname(target), { recursive: true })
await writeFile(target, content)
}
for (const page of rendered) await write(page.path, page.markdown)
const guides = rendered.filter((page) => page.section !== 'reference').sort((a, b) => a.order - b.order)
const list = (section) => guides.filter((page) => page.section === section).map((page) => `- [${page.title}](${site}${page.path}): ${page.description}`).join('\n')
const index = [
'# Create Preset',
'> Create projects with official generators and declarative JSON presets, or apply selected files to existing projects.',
`CLI version: ${pkg.version}. Start with Getting Started, then read only the references or examples needed for the task. Links below provide plain text without website navigation.`,
'## Guides', list('guide'),
'## Examples', list('examples'),
'## Reference',
`- [Preset config reference](${site}/agent/reference/preset-configs.md): Complete field and execution rules.`,
`- [Private preset manifest](${site}/agent/reference/private-presets.md): Full manifest format.`,
`- [JSON Schema](${site}/schema/preset.schema.json): Preset validation schema.`,
'## Optional',
`- [Full documentation](${site}/llms-full.txt): Combined guides, examples and references. Prefer individual pages for smaller context.`,
].join('\n\n') + '\n'
await write('/llms.txt', index)
const bundle = [...guides, ...rendered.filter((page) => page.section === 'reference')]
await write('/llms-full.txt', `# Create Preset ${pkg.version}\n\n` + bundle.map((page) => `Source: ${site}${page.path}\n\n${page.markdown}`).join('\n---\n\n'))
return rendered
}

if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) {
const pages = await buildAgentDocs()
console.log(`[preset-agent-docs] Generated ${pages.length} Markdown pages and an English index/bundle`)
}
17 changes: 17 additions & 0 deletions docs/src/agent/__tests__/prompt.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { expect, test } from 'vitest'
import { createAgentPrompt, getAgentPagePath } from '../prompt'

test('localized prompts share the same English documentation URLs', () => {
const pagePath = getAgentPagePath({ slugSegments: ['guide', 'preset-configs'] })
const english = createAgentPrompt('en', pagePath)
const chinese = createAgentPrompt('zh', pagePath)

expect(english).toContain('My task:')
expect(chinese).toContain('我的需求:')
for (const prompt of [english, chinese]) {
expect(prompt.match(/https:\/\/\S+/g)).toEqual([
'https://preset.js.org/llms.txt',
'https://preset.js.org/agent/guide/preset-configs.md',
])
}
})
Loading
Loading