mirror of
https://github.com/shadcn-ui/ui.git
synced 2026-06-29 07:34:11 +00:00
fix(cli): add Accept and User-Agent headers to support content negotiation (fixes #10164)
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user