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
8 changes: 8 additions & 0 deletions .changeset/shared-next-fragments.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
'@pmndrs/docs': patch
---

Render HTML fragments through the same Next.js build and static export as the website.
Fragments retain their output without layout, stylesheets, or scripts, including when read
from stdin or a single file. These inputs now run a full Next.js build, with build logs sent
to stderr so stdout remains HTML only.
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,14 @@ $ npx @pmndrs/docs@latest build docs out # one .html per .mdx
$ npx @pmndrs/docs@latest build docs out --format website # the whole website, statically exported
```

`--format fragment` (the default) runs in Node without a Next build. A fragment is the
compiled MDX and nothing else: no layout, no stylesheet, no script. Mermaid
Both formats use the same Next.js build and static export. `--format fragment` (the default)
extracts the compiled MDX from that export: no layout, no stylesheet, no script. Mermaid
diagrams stay fenced blocks, and Sandpack shows its code without the editor.

Rendering a document runs `next build`, including a single file or stdin. For those inputs,
stdout contains only the HTML; build logs go to stderr. esbuild is used only to package the
CLI for publication, not to load consumer components or render documents.

`build --help` lists every website option — `--libname`, `--base-path`, `--icon`, `--theme-*`… Each
one falls back to the environment variable it maps to, the same ones
[configuration](docs/getting-started/introduction.mdx#configuration) documents.
Expand Down
13 changes: 13 additions & 0 deletions docs/getting-started/introduction.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,19 @@ Then go to: http://localhost:3000

`npx @pmndrs/docs@latest build --help` lists the CLI flags, including `--config` for a configuration outside the repository root.

Both `--format website` and `--format fragment` use the same Next.js build and static export.
Fragments contain only the compiled document: no layout, stylesheet or script. The CLI
copies assets alongside folder fragments and preserves static Mermaid and Sandpack content.

```sh
$ npx @pmndrs/docs@latest build page.mdx
$ cat page.mdx | npx @pmndrs/docs@latest build
```

A single file or stdin writes HTML to stdout and build logs to stderr. These inputs also
run a full Next.js build. Consumer components and data use the same `docs.config.ts` as
the website.

## Agents

`llms.txt` dumps and the pmndrs MCP server moved to their own page: [Agents](/agents/introduction).
3 changes: 1 addition & 2 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,7 @@ const eslintConfig = [
'react/jsx-no-target-blank': 0,
'import/no-anonymous-default-export': 0,
'react-hooks/set-state-in-effect': 0,
// `next/image` buys nothing here: `images.unoptimized` is set in next.config.mts,
// and the CLI renders these components outside Next, where next/image throws.
// Images are emitted unchanged: `images.unoptimized` is set in next.config.mts.
'@next/next/no-img-element': 0,
},
},
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"d3-sankey": "^0.12.3",
"d3-selection": "^3.0.0",
"d3-transition": "^3.0.1",
"esbuild": "^0.28.2",
"eslint": "^9.39.5",
"eslint-config-next": "^16.4.0",
"eslint-plugin-storybook": "^10.6.2",
Expand Down Expand Up @@ -62,7 +63,6 @@
"cmdk": "^1.1.1",
"cn": "^0.4.0",
"commander": "^15.0.0",
"esbuild": "^0.28.2",
"gray-matter": "^4.0.3",
"image-size": "^2.0.4",
"ink": "^8.0.0",
Expand Down Expand Up @@ -138,6 +138,7 @@
"postcss.config.mjs",
"tsconfig.json",
"!src/stories",
"!src/test-utils.ts",
"!src/app/api",
"!src/app/mcp/live",
"!**/*.test.*",
Expand Down
6 changes: 3 additions & 3 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

48 changes: 3 additions & 45 deletions scripts/build-cli.mjs
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
// Bundles the CLI so that `node` can run what only Next could run: TSX, the `@/` paths of
// tsconfig, and the CSS and image imports our components make.
// Bundles the CLI and its Ink terminal UI. Next handles all MDX rendering, CSS and images.

import * as esbuild from 'esbuild'
import { imageSize } from 'image-size'
import { readFile } from 'node:fs/promises'
import { dirname, extname, resolve } from 'node:path'
import { dirname, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'

const root = resolve(dirname(fileURLToPath(import.meta.url)), '..')
Expand All @@ -28,45 +25,6 @@ const tsconfigPaths = {
},
}

/** Stylesheets are the website's business; a fragment carries none. */
const stylesheets = {
name: 'stylesheets',
setup(build) {
build.onLoad({ filter: /\.css$/ }, () => ({ contents: '', loader: 'js' }))
},
}

const MIME = {
'.svg': 'image/svg+xml',
'.png': 'image/png',
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.gif': 'image/gif',
'.webp': 'image/webp',
'.avif': 'image/avif',
}

/**
* Static image imports, in the shape Next gives them — `{ src, width, height }` — so that
* components need not know which of the two compiled them. `src` is a data URI, since a
* fragment travels alone.
*/
const staticImages = {
name: 'static-images',
setup(build) {
build.onLoad({ filter: /\.(svg|png|jpe?g|gif|webp|avif)$/ }, async (args) => {
const data = await readFile(args.path)
const { width, height } = imageSize(data)
const src = `data:${MIME[extname(args.path)]};base64,${data.toString('base64')}`

return {
contents: `export default ${JSON.stringify({ src, width, height })}`,
loader: 'js',
}
})
},
}

await esbuild.build({
entryPoints: [resolve(root, 'src/cli/main.ts')],
outfile: resolve(root, 'dist/cli.mjs'),
Expand All @@ -76,6 +34,6 @@ await esbuild.build({
target: 'node22.12',
packages: 'external',
jsx: 'automatic',
plugins: [tsconfigPaths, stylesheets, staticImages],
plugins: [tsconfigPaths],
logLevel: 'info',
})
Loading
Loading