fix(cli): add Accept and User-Agent headers to support content negotiation (fixes #10164)

This commit is contained in:
Ethan Davidson
2026-03-26 00:24:48 -07:00
parent 6a070bf8c5
commit 6525227036
3 changed files with 110 additions and 0 deletions

View File

@@ -144,6 +144,47 @@ npm run dev
Your files will now be served at `http://localhost:3000/r/[NAME].json` eg. `http://localhost:3000/r/hello-world.json`.
## Content Negotiation
The `shadcn` CLI supports **HTTP Content Negotiation**. This allows you to host your registry at any endpoint (including the root of your domain) and serve different content based on the client.
### Identity Headers
When the CLI makes a request to a registry, it sends the following headers:
- **User-Agent**: `shadcn-ui`
- **Accept**: `application/vnd.shadcn.v1+json, application/json;q=0.9`
### Root Hosting
By checking these headers on your server, you can serve human-readable documentation (HTML) to browser users and the registry index (JSON) to the CLI—all from the exact same URL.
Here's an example of how to implement this in an Express.js server:
```javascript title="server.js" showLineNumbers
app.get("/", (req, res) => {
const accept = req.get("Accept")
const userAgent = req.get("User-Agent")
// 1. Check if the request is from the shadcn CLI
if (
accept?.includes("application/vnd.shadcn.v1+json") ||
userAgent === "shadcn-ui"
) {
// Return the registry JSON for the CLI
return res.json(registryData)
}
// 2. Otherwise, serve your documentation or homepage
res.send(htmlContent)
})
```
This enables:
- **Branded Registry URLs**: `shadcn add https://ui.example.com`
- **Shorter URLs**: No need for dedicated `/r/` or `/registry/` sub-paths.
- **Easy Mnemonics**: Easier for users to remember and share your registry.
## Publish your registry
To make your registry available to other developers, you can publish it by deploying your project to a public URL.

View File

@@ -204,6 +204,73 @@ describe("fetchRegistry", () => {
expect(result[0]).toMatchObject({ name: "button" })
expect(result[1]).toMatchObject({ name: "card" })
})
it("should send specific Accept and User-Agent headers", async () => {
let acceptHeader: string | null = null
let userAgentHeader: string | null = null
server.use(
http.get(`${REGISTRY_URL}/header-test.json`, ({ request }) => {
acceptHeader = request.headers.get("accept")
userAgentHeader = request.headers.get("user-agent")
return HttpResponse.json({
name: "header-test",
type: "registry:ui",
})
})
)
await fetchRegistry(["header-test.json"], { useCache: false })
expect(acceptHeader).toBe(
"application/vnd.shadcn.v1+json, application/json;q=0.9"
)
expect(userAgentHeader).toBe("shadcn-ui")
})
it("should send specific Accept header for direct external URLs", async () => {
let acceptHeader: string | null = null
server.use(
http.get("https://external.com/registry/item.json", ({ request }) => {
acceptHeader = request.headers.get("accept")
return HttpResponse.json({
name: "item",
type: "registry:ui",
})
})
)
await fetchRegistry(["https://external.com/registry/item.json"], {
useCache: false,
})
expect(acceptHeader).toBe(
"application/vnd.shadcn.v1+json, application/json;q=0.9"
)
})
it("should successfully fetch when the server requires the specific Shadcn Accept header (Content Negotiation)", async () => {
server.use(
http.get(`${REGISTRY_URL}/content-negotiation.json`, ({ request }) => {
const accept = request.headers.get("accept")
if (!accept?.includes("application/vnd.shadcn.v1+json")) {
return new HttpResponse(
"<!DOCTYPE html><html><body>Error: Specific header missing</body></html>",
{
status: 200,
headers: { "Content-Type": "text/html" },
}
)
}
return HttpResponse.json({
name: "content-negotiation",
type: "registry:ui",
})
})
)
const [result] = await fetchRegistry(["content-negotiation.json"], {
useCache: false,
})
expect(result).toMatchObject({ name: "content-negotiation" })
})
})
describe("clearRegistryCache", () => {

View File

@@ -54,6 +54,8 @@ export async function fetchRegistry(
const response = await fetch(url, {
agent,
headers: {
Accept: "application/vnd.shadcn.v1+json, application/json;q=0.9",
"User-Agent": "shadcn-ui",
...headers,
},
})