docs/solutions/developer-experience/2026-05-24-shadcn-registry-install-commands-should-use-configured-namespaces.md
Plate templates already configure the @plate registry namespace, but several docs and block surfaces still displayed or copied raw registry URLs such as http://localhost:3000/rd/table-kit.
That mixed two different contracts: raw /r/* URLs are good for resolvable registry content, while user install commands should use the shadcn namespace syntax that components.json provides.
ComponentInstallation, ComponentSource, block preview toolbars, block metadata, and MCP setup each assembled their own npx shadcn@latest add ... command.npx shadcn@latest add {name} while the copied value used a raw registry URL.@plate to components.json, then initialized from a raw siteConfig.registryUrl.build-registry.mts and build-docs-registry.mts still expanded Plate self-dependencies into https://platejs.org/r/*.json.update-template.sh --local uses shadcn local-file mode from a prepared JSON mirror, so changing generated dependencies to @plate/* without rewriting the local mirror would accidentally send local template sync back through the configured remote registry.BlockViewer loaded only the first registry source file in compiled MDX, so removing lazy hydration made non-initial code-tab files blank.OpenInV0Button was no longer rendered, but the file and a commented toolbar block still pointed at v0.dev.siteConfig.registryUrl as the general install-command source. That URL is still needed for LLM context, v0 URLs, and direct registry content links, but it is the wrong default for shadcn install UX once a namespace exists.components.json files were already correct and tooling/scripts/update-template.sh already defaults to @plate.@plate/* without updating prepare-local-template-registry.mjs. Local-file template sync needs file names such as toolbar.json, not namespaced remote specifiers.Centralize the visible install command contract:
const absoluteUrlPattern = /^https?:\/\//;
export function getRegistryItemSpecifier(name: string) {
const item = name.trim();
if (
item.startsWith('@') ||
item.startsWith('/') ||
item.startsWith('./') ||
item.startsWith('../') ||
absoluteUrlPattern.test(item)
) {
return item;
}
return `@plate/${item}`;
}
export function getRegistryInstallCommand(name: string) {
return `npx shadcn@latest add ${getRegistryItemSpecifier(name)}`;
}
Then use that helper in every user-facing install surface:
For generated registry output, use the same namespace boundary for Plate self-dependencies:
export function toRegistryDependencySpecifier(dependency: string) {
if (isDirectDependencySpecifier(dependency)) {
return dependency;
}
return `@plate/${dependency}`;
}
Use it from both registry builders:
apps/www/scripts/build-registry.mtsapps/www/scripts/build-docs-registry.mtsThen keep local template sync local by rewriting Plate namespace dependencies in the prepared mirror:
if (dependency.startsWith('@plate/')) {
return `${dependency.slice('@plate/'.length)}.json`;
}
Keep raw registry URLs where the user or tool needs a URL that resolves directly:
/r/{name} registry content linksFor the MCP dialog warning, add a real DialogDescription inside the dialog header instead of using an unassociated paragraph.
For source preview, keep compiled MDX small and hydrate full code only when the user opens the Code tab:
const item = await getRegistryItem(name, true);
Use the prefetched item for the initial preview, then let BlockViewer fetch /api/registry-source/[name] when later files have no content. The route calls getRegistryItem(name) and highlightFiles(item.files), so copy buttons and non-initial code tabs receive the full highlighted payload without putting that payload into every MDX chunk. Keep this route scoped to docs code-view source; public registry install surfaces stay on /r, /rd, @plate/*, and /init.
Delete v0-only UI instead of keeping commented code. Plate owns a registry around the shadcn contract; it does not need upstream's v0 open flow.
Shadcn owns the installer behavior and namespace resolver. Plate owns the registry content and template config.
Once components.json contains:
{
"registries": {
"@plate": "https://platejs.org/r/{name}.json"
}
}
the installer command should be npx shadcn@latest add @plate/table-kit. The CLI resolves the namespace to the URL template, so docs do not need to expose raw registry URLs as the primary install path.
Keeping raw URLs only for direct content links avoids breaking tools that need actual fetchable registry JSON.
For template local sync, the prepared mirror is the boundary. Public/dev registry JSON can use the upstream namespace contract, then prepare-local-template-registry.mjs converts @plate/foo to foo.json before shadcn local-file install runs.
Lazy source hydration is the right boundary for MDX-authored ComponentPreview blocks. The page HTML carries enough data for the initial file and tree, while the static registry route carries the expensive full highlighted file list only after the Code tab needs it.
components.json and tooling/scripts/update-template.sh; if they already use @plate, update display/copy surfaces instead of template output.registryDependencies, check tooling/scripts/prepare-local-template-registry.mjs too. Namespace output and local-file sync are coupled.rg -n 'siteConfig\.registryUrl|npx shadcn@latest add http|npx shadcn@latest add https|npx shadcn@latest add \{name\}' apps/www/src
ComponentSource, the MCP dialog, and block metadata after changing install command text.rg -n 'OpenInV0|v0\\.dev|getRegistryItem\\(name\\)' apps/www/src
ComponentPreview page by selecting a non-initial file in the Code tab and copying it; the clipboard should contain real source, not an empty string.pnpm --filter www typecheck run check-docs-source-parity.mts against the docs registry aggregate dependencies.pnpm check fails only on test:slowest fast-suite timing, rerun pnpm test:slowest once before moving tests. A clean isolated rerun points to machine load, not a real test classification fix.