Recipes
Practical registry-build patterns for docs sites, custom manifests, external data files, and framework-specific output.
This page focuses on patterns that come up in real consumers, not just field-by-field reference material.
Recipe 1: Docs-site registry
This is the common shape for a docs app that needs:
index.json- per-item component payloads
- a generated component index file
- theme and color outputs
import {
bannerExtension,
colorsExtension,
componentIndexExtension,
componentsExtension,
defineConfig,
indexBuildExtension,
validateExtension,
} from '@gentleduck/registry-build'
export default defineConfig({
sources: {
'registry:ui': {
path: '../../packages/registry-ui/src',
packageName: '@example/registry-ui',
referencePath: '/registry-ui/src',
},
'registry:example': {
path: '../../packages/registry-examples/src',
packageName: '@example/registry-examples',
referencePath: '/registry-examples/src',
indexStrategy: 'file',
},
},
registries: {
uis: [...],
examples: [...],
},
output: {
dir: '.',
},
targetPaths: {
'registry:ui': 'components/ui',
'registry:example': 'components',
},
extensions: [
bannerExtension({ name: 'My Docs' }),
validateExtension(),
indexBuildExtension(),
componentsExtension(),
componentIndexExtension({
framework: 'nextjs',
}),
colorsExtension(),
],
})import {
bannerExtension,
colorsExtension,
componentIndexExtension,
componentsExtension,
defineConfig,
indexBuildExtension,
validateExtension,
} from '@gentleduck/registry-build'
export default defineConfig({
sources: {
'registry:ui': {
path: '../../packages/registry-ui/src',
packageName: '@example/registry-ui',
referencePath: '/registry-ui/src',
},
'registry:example': {
path: '../../packages/registry-examples/src',
packageName: '@example/registry-examples',
referencePath: '/registry-examples/src',
indexStrategy: 'file',
},
},
registries: {
uis: [...],
examples: [...],
},
output: {
dir: '.',
},
targetPaths: {
'registry:ui': 'components/ui',
'registry:example': 'components',
},
extensions: [
bannerExtension({ name: 'My Docs' }),
validateExtension(),
indexBuildExtension(),
componentsExtension(),
componentIndexExtension({
framework: 'nextjs',
}),
colorsExtension(),
],
})Recipe 2: Arch-style package repository
This pattern treats registry-build as a generic build core plus a custom extension. The core UI phases are disabled, and the custom extension emits repository metadata, package-file manifests, and search artifacts.
import { defineConfig } from '@gentleduck/registry-build'
import { archRepositoryExtension } from './arch-repository.extension'
export default defineConfig({
collections: {
packages: {
data: './data/packages.json',
metadata: {
repoOrder: ['core', 'extra'],
},
sources: {
pkgbuilds: {
glob: '**/PKGBUILD',
path: './pkgbuilds',
},
},
},
},
extensions: [
archRepositoryExtension({
collection: 'packages',
}),
],
output: {
dir: './dist',
},
})import { defineConfig } from '@gentleduck/registry-build'
import { archRepositoryExtension } from './arch-repository.extension'
export default defineConfig({
collections: {
packages: {
data: './data/packages.json',
metadata: {
repoOrder: ['core', 'extra'],
},
sources: {
pkgbuilds: {
glob: '**/PKGBUILD',
path: './pkgbuilds',
},
},
},
},
extensions: [
archRepositoryExtension({
collection: 'packages',
}),
],
output: {
dir: './dist',
},
})This is the direction for non-UI consumers: collections plus extensions, not empty componentIndex or themes objects.
See the in-repo example in packages/registry-build/examples/arch-package-index.
If you want the full guided build, continue to Course and Arch Package Index.
Recipe 3: Custom manifest extension
If another tool needs a derived manifest after the registry build finishes, expose it through an afterBuild extension:
{
name: 'manifest',
stage: 'afterBuild',
async run(api) {
const manifest = {
outputs: api.listOutputs().map((output) => output.name),
itemCount: (api.getArtifact('index') as Array<unknown> | undefined)?.length ?? 0,
}
// write file, register output, set artifact
},
}{
name: 'manifest',
stage: 'afterBuild',
async run(api) {
const manifest = {
outputs: api.listOutputs().map((output) => output.name),
itemCount: (api.getArtifact('index') as Array<unknown> | undefined)?.length ?? 0,
}
// write file, register output, set artifact
},
}Use this pattern instead of patching the core runner.
Recipe 4: External static data files
Use root config file paths when colors/themes or registry entries live in static JSON files:
export default defineConfig({
registrySource: './data/registry-entries.json',
colors: {
data: './data/colors.json',
},
themes: {
data: './data/themes.json',
},
output: {
dir: '.',
},
})export default defineConfig({
registrySource: './data/registry-entries.json',
colors: {
data: './data/colors.json',
},
themes: {
data: './data/themes.json',
},
output: {
dir: '.',
},
})This keeps static data portable and avoids importing giant objects into every consumer config file manually.
Recipe 5: Extend a base config
File-based composition:
export default defineConfig({
extends: ['./registry-build.base.ts', './registry-build.theme.ts'],
output: {
dir: '.',
},
})export default defineConfig({
extends: ['./registry-build.base.ts', './registry-build.theme.ts'],
output: {
dir: '.',
},
})Code-based composition:
import { defineConfig, mergeRegistryBuildConfigs } from '@gentleduck/registry-build'
import { base } from './registry-build.base'
import { theme } from './registry-build.theme'
export default defineConfig(mergeRegistryBuildConfigs(base, theme))import { defineConfig, mergeRegistryBuildConfigs } from '@gentleduck/registry-build'
import { base } from './registry-build.base'
import { theme } from './registry-build.theme'
export default defineConfig(mergeRegistryBuildConfigs(base, theme))Use file-based composition when the config should stay mostly declarative. Use code-based composition when you need parameterized factories.
Recipe 6: Custom framework component index
If the built-in framework adapters are not enough, provide a custom generator:
componentIndexExtension({
generator(items) {
return [
'// custom generated file',
`export const items = ${JSON.stringify(items, null, 2)}`,
].join('\n')
},
})componentIndexExtension({
generator(items) {
return [
'// custom generated file',
`export const items = ${JSON.stringify(items, null, 2)}`,
].join('\n')
},
})That keeps the rest of the build intact while taking full control over the generated file.
Recipe 7: Fast local rebuild loop
Use split scripts so the raw CLI path remains available:
{
"scripts": {
"build:reg": "bun run build:reg:cli && bun run format:reg",
"build:reg:cli": "registry-build build",
"format:reg": "bunx biome format --write ./__ui_registry__/index.tsx"
}
}{
"scripts": {
"build:reg": "bun run build:reg:cli && bun run format:reg",
"build:reg:cli": "registry-build build",
"format:reg": "bunx biome format --write ./__ui_registry__/index.tsx"
}
}Then for targeted local updates:
bun run build:reg:cli -- --changed-only --changed ../../packages/registry-ui/src/button/button.tsxbun run build:reg:cli -- --changed-only --changed ../../packages/registry-ui/src/button/button.tsxRule of thumb
If you can describe it as static data, put it in config. If you need to compute or emit something after the registry exists, write an extension. If you need to reuse a config shape across consumers, extract a preset or config factory.