fix(docs): improve heroui.com agent readiness (#6810)

* fix(docs): improve heroui.com agent readiness

* fix(docs): refine agent trust metadata

* fix(docs): use junior@heroui.com on trust pages

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
This commit is contained in:
Junior Garcia
2026-08-25 18:39:11 -03:00
committed by GitHub
parent 1d2164e7b9
commit 35189b5ff1
30 changed files with 1287 additions and 342 deletions
+3 -1
View File
@@ -9,6 +9,7 @@
"prebuild": "node scripts/build-skills.mjs && node scripts/build-theme-presets.mjs",
"build": "next build",
"start": "next start",
"test": "vitest run",
"lint": "eslint",
"lint:fix": "eslint --fix",
"typecheck": "tsc --noEmit",
@@ -79,6 +80,7 @@
"postcss": "8.5.23",
"source-map-support": "0.5.21",
"tailwindcss": "4.3.0",
"tar": "7.5.21"
"tar": "7.5.21",
"vitest": "4.1.11"
}
}
@@ -21,12 +21,20 @@ export async function GET(request: Request) {
{
anchor: agentApi,
"service-desc": [
{
href: absoluteUrl(origin, "/openapi.json"),
type: "application/vnd.oai.openapi+json",
},
{
href: absoluteUrl(origin, "/.well-known/openapi/heroui-agent-api.json"),
type: "application/vnd.oai.openapi+json",
},
],
"service-doc": [
{
href: absoluteUrl(origin, "/api/agent"),
type: "application/json",
},
{
href: absoluteUrl(origin, "/docs/react/getting-started/llms-txt"),
type: "text/html",
@@ -43,6 +51,25 @@ export async function GET(request: Request) {
},
],
},
{
anchor: absoluteUrl(origin, "/.well-known/mcp"),
"service-desc": [
{
href: absoluteUrl(origin, "/.well-known/mcp/server-card.json"),
type: "application/json",
},
],
"service-doc": [
{
href: absoluteUrl(origin, "/docs/react/getting-started/mcp-server"),
type: "text/html",
},
{
href: absoluteUrl(origin, "/docs/native/getting-started/mcp-server"),
type: "text/html",
},
],
},
{
anchor: REACT_MCP_API_URL,
"service-desc": [
@@ -0,0 +1,13 @@
import {getRequestOrigin, jsonResponse} from "@/lib/agent-discovery";
import {getMcpServerCard} from "@/lib/agent-mcp";
export const dynamic = "force-dynamic";
export const revalidate = false;
export function GET(request: Request) {
return jsonResponse(getMcpServerCard(getRequestOrigin(request)), {
headers: {
Link: '</.well-known/mcp/server-card.json>; rel="service-desc"; type="application/json"',
},
});
}
@@ -1,112 +1,9 @@
import {
MCP_PACKAGE_VERSION,
NATIVE_MCP_API_URL,
REACT_MCP_API_URL,
absoluteUrl,
getRequestOrigin,
jsonResponse,
} from "@/lib/agent-discovery";
import {getRequestOrigin, jsonResponse} from "@/lib/agent-discovery";
import {getMcpServerCard} from "@/lib/agent-mcp";
export const dynamic = "force-dynamic";
export const revalidate = false;
export async function GET(request: Request) {
const origin = getRequestOrigin(request);
return jsonResponse({
$schema: "https://modelcontextprotocol.io/schemas/server-card/draft.json",
capabilities: {
prompts: false,
resources: false,
tools: true,
},
description:
"HeroUI MCP servers expose read-only HeroUI React and HeroUI Native documentation, component metadata, source references, styles, and theme variables to AI coding agents.",
endpoint: absoluteUrl(origin, "/.well-known/mcp/server-card.json"),
links: {
docs: [
absoluteUrl(origin, "/docs/react/getting-started/mcp-server"),
absoluteUrl(origin, "/docs/native/getting-started/mcp-server"),
],
npm: [
"https://www.npmjs.com/package/@heroui/react-mcp",
"https://www.npmjs.com/package/@heroui/native-mcp",
],
source: "https://github.com/heroui-inc/heroui-mcp",
},
notes:
"The supported MCP transport today is stdio through the published npm packages. The endpoint field identifies this server card for browser and catalog discovery; it is not a Streamable HTTP MCP endpoint.",
serverInfo: {
name: "HeroUI MCP",
version: MCP_PACKAGE_VERSION,
},
tools: [
{
description: "List all available HeroUI v3 React components.",
name: "list_components",
package: "@heroui/react-mcp",
},
{
description: "Get complete React component documentation.",
name: "get_component_docs",
package: "@heroui/react-mcp",
},
{
description: "Get React component TypeScript source code.",
name: "get_component_source_code",
package: "@heroui/react-mcp",
},
{
description: "Get React component CSS source styles.",
name: "get_component_source_styles",
package: "@heroui/react-mcp",
},
{
description: "Get HeroUI React theme variables.",
name: "get_theme_variables",
package: "@heroui/react-mcp",
},
{
description: "Browse full HeroUI React documentation.",
name: "get_docs",
package: "@heroui/react-mcp",
},
{
description: "List all available HeroUI Native components.",
name: "list_components",
package: "@heroui/native-mcp",
},
{
description: "Get complete Native component documentation.",
name: "get_component_docs",
package: "@heroui/native-mcp",
},
{
description: "Get HeroUI Native theme variables.",
name: "get_theme_variables",
package: "@heroui/native-mcp",
},
{
description: "Browse full HeroUI Native documentation.",
name: "get_docs",
package: "@heroui/native-mcp",
},
],
transports: [
{
args: ["-y", "@heroui/react-mcp@latest"],
command: "npx",
dataApi: REACT_MCP_API_URL,
package: "@heroui/react-mcp",
type: "stdio",
},
{
args: ["-y", "@heroui/native-mcp@latest"],
command: "npx",
dataApi: NATIVE_MCP_API_URL,
package: "@heroui/native-mcp",
type: "stdio",
},
],
});
export function GET(request: Request) {
return jsonResponse(getMcpServerCard(getRequestOrigin(request)));
}
@@ -1,225 +1,22 @@
import {
AGENT_API_VERSION,
NATIVE_MCP_API_URL,
OPENAPI_HEADERS,
REACT_MCP_API_URL,
absoluteUrl,
getAgentServiceBaseUrl,
getRequestOrigin,
} from "@/lib/agent-discovery";
import {OPENAPI_HEADERS, absoluteUrl, getRequestOrigin, jsonResponse} from "@/lib/agent-discovery";
import {OPENAPI_BUILDERS} from "@/lib/agent-openapi";
export const dynamic = "force-dynamic";
export const revalidate = false;
type OpenAPIDocument = Record<string, unknown>;
function baseDocument(
title: string,
version: string,
serverUrl: string,
description: string,
): OpenAPIDocument {
return {
info: {
description,
title,
version,
},
openapi: "3.1.0",
servers: [{url: serverUrl}],
};
}
function agentApiDocument(origin: string): OpenAPIDocument {
return {
...baseDocument(
"HeroUI Docs Agent API",
AGENT_API_VERSION,
getAgentServiceBaseUrl(origin),
"Read-only endpoints that help agents search HeroUI documentation and retrieve markdown page content.",
),
paths: {
"/health": {
get: {
operationId: "getAgentApiHealth",
responses: {
"200": {
description: "Agent API health status",
},
},
summary: "Check service health",
},
},
"/page": {
get: {
operationId: "getHeroUIDocPageMarkdown",
parameters: [
{
description: "HeroUI docs page URL, for example /docs/react/components/button",
in: "query",
name: "url",
required: true,
schema: {type: "string"},
},
],
responses: {
"200": {description: "Documentation page metadata and markdown"},
"404": {description: "Documentation page not found"},
},
summary: "Retrieve a documentation page as markdown",
},
},
"/search": {
get: {
operationId: "searchHeroUIDocs",
parameters: [
{in: "query", name: "q", required: true, schema: {type: "string"}},
{
in: "query",
name: "platform",
schema: {default: "all", enum: ["all", "react", "native"], type: "string"},
},
{
in: "query",
name: "limit",
schema: {default: 10, maximum: 20, minimum: 1, type: "integer"},
},
],
responses: {
"200": {description: "Matching documentation pages"},
"400": {description: "Invalid request"},
},
summary: "Search HeroUI documentation",
},
},
},
};
}
function mcpApiDocument(kind: "react" | "native"): OpenAPIDocument {
const isReact = kind === "react";
const serverUrl = isReact ? REACT_MCP_API_URL : NATIVE_MCP_API_URL;
const title = isReact ? "HeroUI React MCP Data API" : "HeroUI Native MCP Data API";
const componentSourcePaths = isReact
? {
"/v1/components/source": {
post: {
operationId: "getComponentSource",
requestBody: {
content: {
"application/json": {
schema: {
properties: {components: {items: {type: "string"}, type: "array"}},
type: "object",
},
},
},
},
responses: {"200": {description: "Component source code"}},
summary: "Get component TypeScript source",
},
},
"/v1/components/styles": {
post: {
operationId: "getComponentStyles",
requestBody: {
content: {
"application/json": {
schema: {
properties: {components: {items: {type: "string"}, type: "array"}},
type: "object",
},
},
},
},
responses: {"200": {description: "Component CSS styles"}},
summary: "Get component styles",
},
},
}
: {};
return {
...baseDocument(
title,
"1.1.0",
serverUrl,
`Read-only data API used by the ${isReact ? "@heroui/react-mcp" : "@heroui/native-mcp"} package and HeroUI agent skills.`,
),
paths: {
"/health": {
get: {
operationId: "getHealth",
responses: {"200": {description: "Health status"}},
summary: "Check API health",
},
},
"/v1/components": {
get: {
operationId: "listComponents",
responses: {"200": {description: "Available components"}},
summary: "List available components",
},
},
"/v1/components/docs": {
post: {
operationId: "getComponentDocs",
requestBody: {
content: {
"application/json": {
schema: {
properties: {components: {items: {type: "string"}, type: "array"}},
type: "object",
},
},
},
},
responses: {"200": {description: "Component documentation"}},
summary: "Get component documentation",
},
},
...componentSourcePaths,
"/v1/docs/{path}": {
get: {
operationId: "getDocs",
parameters: [{in: "path", name: "path", required: true, schema: {type: "string"}}],
responses: {"200": {description: "Documentation content"}},
summary: "Get documentation by path",
},
},
"/v1/themes/variables": {
get: {
operationId: "getThemeVariables",
parameters: [{in: "query", name: "theme", schema: {default: "default", type: "string"}}],
responses: {"200": {description: "Theme variables"}},
summary: "Get theme variables",
},
},
},
};
}
const OPENAPI_BUILDERS: Record<string, (origin: string) => OpenAPIDocument> = {
"heroui-agent-api.json": agentApiDocument,
"heroui-native-mcp-api.json": () => mcpApiDocument("native"),
"heroui-react-mcp-api.json": () => mcpApiDocument("react"),
};
export async function GET(request: Request, {params}: {params: Promise<{api: string}>}) {
const {api} = await params;
const builder = OPENAPI_BUILDERS[api];
if (!builder) {
return new Response(
JSON.stringify(
{available: Object.keys(OPENAPI_BUILDERS), error: "OpenAPI description not found"},
null,
2,
),
return jsonResponse(
{
headers: {"Content-Type": "application/json; charset=utf-8"},
status: 404,
code: "OPENAPI_NOT_FOUND",
error: true,
hint: `Use one of: ${Object.keys(OPENAPI_BUILDERS).join(", ")}.`,
message: "The requested HeroUI OpenAPI description was not found.",
},
{status: 404},
);
}
@@ -0,0 +1,52 @@
import type {Metadata} from "next";
import {TrustPage} from "../components/trust-page";
export const metadata: Metadata = {
alternates: {canonical: "/about"},
description:
"Learn about HeroUI, the open-source React and React Native UI library founded by Junior Garcia and backed by Y Combinator.",
title: {absolute: "About HeroUI"},
};
export default function AboutPage() {
return (
<TrustPage
description="HeroUI is an open-source UI library for building accessible, customizable web and mobile interfaces."
title="About HeroUI"
>
<section>
<h2>What HeroUI publishes</h2>
<p>
HeroUI provides components for React applications on the web and for React Native
applications on mobile. The project focuses on accessible behavior, customizable
composition, typed developer APIs, and polished defaults that teams can adapt to their own
products. The public documentation at <a href="/">heroui.com</a> includes installation
guides, component references, examples, theming resources, release notes, and migration
guidance.
</p>
</section>
<section>
<h2>Open source and developer tools</h2>
<p>
HeroUI source code is available in the{" "}
<a href="https://github.com/heroui-inc/heroui">official GitHub repository</a>. Official
packages include <code>@heroui/react</code>, <code>@heroui/react-mcp</code>, and{" "}
<code>@heroui/native-mcp</code>. The official <code>heroui-cli</code> is documented in the{" "}
<a href="/docs/react/getting-started/cli">HeroUI CLI guide</a>. Developers and coding
agents can inspect the same public source and documentation rather than relying on an
undocumented integration.
</p>
</section>
<section>
<h2>Company and founder</h2>
<p>
HeroUI is a Y Combinator S24 company founded by Junior Garcia. The library remains openly
documented so developers can evaluate how it works, review its source, and use its
published packages under the terms provided in the repository. For ways to reach the team,
visit the <a href="/contact">HeroUI contact page</a>.
</p>
</section>
</TrustPage>
);
}
@@ -0,0 +1,32 @@
import type {ReactNode} from "react";
import {Footer} from "@/components/footer";
import {getDictionary} from "@/lib/dictionaries";
export async function TrustPage({
children,
description,
title,
}: {
children: ReactNode;
description: string;
title: string;
}) {
const dict = await getDictionary("en");
return (
<main className="flex min-h-[calc(100vh-4rem)] flex-col">
<article className="mx-auto w-full max-w-3xl flex-1 px-6 py-16 sm:py-24">
<header className="mb-10 border-b border-border pb-8">
<p className="mb-3 text-sm font-medium text-muted">HeroUI</p>
<h1 className="text-4xl font-bold tracking-tight text-foreground">{title}</h1>
<p className="mt-4 text-lg leading-8 text-muted">{description}</p>
</header>
<div className="space-y-8 text-base leading-7 text-muted [&_a]:font-medium [&_a]:text-foreground [&_a]:underline [&_a]:underline-offset-4 [&_h2]:text-2xl [&_h2]:font-semibold [&_h2]:tracking-tight [&_h2]:text-foreground [&_p]:mt-3">
{children}
</div>
</article>
<Footer dict={dict.footer} />
</main>
);
}
@@ -0,0 +1,53 @@
import type {Metadata} from "next";
import {TrustPage} from "../components/trust-page";
export const metadata: Metadata = {
alternates: {canonical: "/contact"},
description:
"Contact HeroUI for sales, partnerships, general questions, and privacy questions using verified public email addresses.",
title: {absolute: "Contact HeroUI"},
};
export default function ContactPage() {
return (
<TrustPage
description="Use these verified channels for questions about HeroUI, its open-source libraries, and its developer resources."
title="Contact HeroUI"
>
<section>
<h2>Email contacts</h2>
<p>
For sales and partnership questions, email{" "}
<a href="mailto:sales@heroui.com">sales@heroui.com</a>. For general or privacy-related
questions, email <a href="mailto:junior@heroui.com">junior@heroui.com</a>. HeroUI does not
list a public street address or phone number in this repository, so this page does not
present one. These email addresses are the direct, verifiable contact options currently
published by the project.
</p>
</section>
<section>
<h2>Technical questions</h2>
<p>
Before writing, check the <a href="/docs/react/getting-started">React documentation</a>,{" "}
<a href="/docs/native/getting-started">React Native documentation</a>, and the{" "}
<a href="https://github.com/heroui-inc/heroui">HeroUI GitHub repository</a>. They contain
installation guides, component APIs, examples, migration resources, and issue tracking.
When asking a technical question, include the platform, package version, a small
reproduction, and what you already tried. That context makes the request easier to
understand without requiring access to a private application.
</p>
</section>
<section>
<h2>What not to send</h2>
<p>
This public documentation site does not provide a contact form or secure support portal.
Do not email passwords, authentication tokens, payment card numbers, government
identification, or other sensitive personal information. For more context about this
documentation site and email communication, read the{" "}
<a href="/privacy">HeroUI privacy page</a>.
</p>
</section>
</TrustPage>
);
}
+25 -1
View File
@@ -73,7 +73,7 @@ export default async function HomePage({params}: {params: Promise<{lang: string}
<div className="mx-auto flex max-w-2xl flex-col items-center justify-center">
<ReleaseBadges badges={home.releaseBadges} />
<h1 className="mt-2 text-3xl font-bold tracking-tight text-foreground sm:text-4xl lg:mt-4 lg:text-5xl">
{home.titleMain} <div className="text-muted/70">{home.titleMuted}</div>
{home.titleMain} <span className="block text-muted/70">{home.titleMuted}</span>
</h1>
<p className="text-balance text-muted md:text-lg">{home.description}</p>
<div className="mt-4 flex gap-3">
@@ -102,6 +102,30 @@ export default async function HomePage({params}: {params: Promise<{lang: string}
{home.githubStarsSuffix}
</span>
</a>
<section
aria-labelledby="heroui-overview"
className="sr-only"
lang={lang === "cn" ? "en" : undefined}
>
<h2 id="heroui-overview">Build accessible products with HeroUI</h2>
<p>
HeroUI is an open-source component library for React web and React Native
applications. The web library combines React Aria Components with Tailwind CSS v4 to
provide accessible behavior, typed APIs, customizable composition, and polished
defaults. Teams can inspect the source, use the free packages, and adapt each
component to their own design system.
</p>
<h3>Documentation for developers and coding agents</h3>
<p>
The HeroUI documentation includes installation guides, component APIs, examples,
theming references, migration instructions, and an official CLI guide. Coding agents
can use the generated llms.txt indexes, request documentation as markdown, search the
public Docs Agent API, read its OpenAPI specification, or connect the existing HeroUI
React and HeroUI Native MCP packages. These machine-readable resources expose the same
public documentation without requiring a paid plan or weakening authentication for
private services.
</p>
</section>
</div>
<DemoShowcase />
</section>
@@ -0,0 +1,61 @@
import type {Metadata} from "next";
import {TrustPage} from "../components/trust-page";
export const metadata: Metadata = {
alternates: {canonical: "/privacy"},
description:
"Plain-language privacy information for the public HeroUI documentation website and HeroUI contact emails.",
title: {absolute: "HeroUI Privacy"},
};
export default function PrivacyPage() {
return (
<TrustPage
description="Plain-language context for the public HeroUI documentation website and the contact addresses listed here."
title="HeroUI Privacy"
>
<section>
<h2>Scope of this page</h2>
<p>
This page applies to the public HeroUI documentation website at heroui.com, including the
About, Contact, and Privacy pages, and to messages people choose to send to the contact
addresses published on this site. HeroUI primarily uses this website to publish
documentation and resources for its open-source React web and React Native UI libraries.
Browsing these public pages does not require a HeroUI account.
</p>
</section>
<section>
<h2>Information you choose to share</h2>
<p>
If you email <a href="mailto:sales@heroui.com">sales@heroui.com</a> or{" "}
<a href="mailto:junior@heroui.com">junior@heroui.com</a>, you control what you include in
the message. Do not send passwords, authentication tokens, financial account details,
government identification numbers, or other sensitive personal information by email.
</p>
</section>
<section>
<h2>Site analytics and skill installation</h2>
<p>
HeroUI uses Vercel Analytics on the documentation site. When the public{" "}
<code>/install</code> route is used to download an agent-skill installer, the production
service records an installation event in PostHog with request metadata supplied by the
hosting platform: IP address, city, region, postal code, country, user agent, referring
page, and requested URL. This page does not claim a retention period or other processing
detail that HeroUI has not published.
</p>
</section>
<section>
<h2>Library use and questions</h2>
<p>
Using HeroUI components in an application is separate from visiting this documentation
website. Developers who build with HeroUI are responsible for the privacy practices of
their own applications and for explaining those practices to their users. For a
privacy-related question about heroui.com or email sent to the addresses above, contact{" "}
<a href="mailto:junior@heroui.com">junior@heroui.com</a>. If you need a detail that is not
documented here, ask directly rather than assuming a policy or practice.
</p>
</section>
</TrustPage>
);
}
@@ -0,0 +1,19 @@
import {agentErrorResponse} from "@/lib/agent-discovery";
export const dynamic = "force-dynamic";
export const revalidate = false;
function notFoundResponse() {
return agentErrorResponse({
code: "AGENT_ENDPOINT_NOT_FOUND",
hint: "Read /openapi.json or GET /api/agent for the available endpoints.",
message: "The requested HeroUI agent API endpoint does not exist.",
status: 404,
});
}
export const DELETE = notFoundResponse;
export const GET = notFoundResponse;
export const PATCH = notFoundResponse;
export const POST = notFoundResponse;
export const PUT = notFoundResponse;
+13 -3
View File
@@ -1,7 +1,7 @@
import type {NextRequest} from "next/server";
import {getAgentDocPage} from "@/lib/agent-api";
import {jsonResponse} from "@/lib/agent-discovery";
import {agentErrorResponse, jsonResponse} from "@/lib/agent-discovery";
export const dynamic = "force-dynamic";
export const revalidate = false;
@@ -10,13 +10,23 @@ export async function GET(request: NextRequest) {
const url = request.nextUrl.searchParams.get("url") ?? request.nextUrl.searchParams.get("path");
if (!url) {
return jsonResponse({error: "Missing required query parameter: url"}, {status: 400});
return agentErrorResponse({
code: "MISSING_PAGE_URL",
hint: "Add a HeroUI docs path, for example ?url=/docs/react/components/button.",
message: "The url query parameter is required.",
status: 400,
});
}
const page = await getAgentDocPage(url);
if (!page) {
return jsonResponse({error: "Documentation page not found", url}, {status: 404});
return agentErrorResponse({
code: "PAGE_NOT_FOUND",
hint: "Use /api/agent/search to find a canonical HeroUI documentation URL.",
message: `No HeroUI documentation page was found for ${url}.`,
status: 404,
});
}
return jsonResponse(page);
+18
View File
@@ -0,0 +1,18 @@
import {AGENT_API_VERSION, jsonResponse} from "@/lib/agent-discovery";
export const dynamic = "force-dynamic";
export const revalidate = false;
export function GET() {
return jsonResponse({
description: "Public, read-only HeroUI documentation API for coding agents.",
endpoints: {
health: "/api/agent/health",
openapi: "/openapi.json",
page: "/api/agent/page?url=/docs/react/components/button",
search: "/api/agent/search?q=button&platform=react",
},
name: "HeroUI Docs Agent API",
version: AGENT_API_VERSION,
});
}
+35 -4
View File
@@ -1,7 +1,7 @@
import type {NextRequest} from "next/server";
import {parseAgentLimit, parseAgentSearchPlatform, searchAgentDocs} from "@/lib/agent-api";
import {jsonResponse} from "@/lib/agent-discovery";
import {agentErrorResponse, jsonResponse} from "@/lib/agent-discovery";
export const dynamic = "force-dynamic";
export const revalidate = false;
@@ -10,11 +10,42 @@ export async function GET(request: NextRequest) {
const query = request.nextUrl.searchParams.get("q")?.trim() ?? "";
if (!query) {
return jsonResponse({error: "Missing required query parameter: q"}, {status: 400});
return agentErrorResponse({
code: "MISSING_QUERY",
hint: "Add a non-empty q query parameter, for example ?q=button.",
message: "The q query parameter is required.",
status: 400,
});
}
const platform = parseAgentSearchPlatform(request.nextUrl.searchParams.get("platform"));
const limit = parseAgentLimit(request.nextUrl.searchParams.get("limit"));
const platformParam = request.nextUrl.searchParams.get("platform");
if (platformParam && !["all", "react", "native"].includes(platformParam)) {
return agentErrorResponse({
code: "INVALID_PLATFORM",
hint: "Use one of: all, react, native.",
message: `Unsupported platform: ${platformParam}.`,
status: 400,
});
}
const limitParam = request.nextUrl.searchParams.get("limit");
const numericLimit = limitParam === null ? null : Number(limitParam);
if (
numericLimit !== null &&
(!Number.isInteger(numericLimit) || numericLimit < 1 || numericLimit > 20)
) {
return agentErrorResponse({
code: "INVALID_LIMIT",
hint: "Use an integer from 1 through 20.",
message: `Invalid result limit: ${limitParam}.`,
status: 400,
});
}
const platform = parseAgentSearchPlatform(platformParam);
const limit = parseAgentLimit(limitParam);
const results = searchAgentDocs(query, platform, limit);
return jsonResponse({
+16
View File
@@ -0,0 +1,16 @@
import {OPENAPI_HEADERS, absoluteUrl, getRequestOrigin} from "@/lib/agent-discovery";
import {agentApiDocument} from "@/lib/agent-openapi";
export const dynamic = "force-dynamic";
export const revalidate = false;
export function GET(request: Request) {
const origin = getRequestOrigin(request);
return new Response(JSON.stringify(agentApiDocument(origin), null, 2), {
headers: {
...OPENAPI_HEADERS,
Link: `<${absoluteUrl(origin, "/.well-known/openapi/heroui-agent-api.json")}>; rel="alternate"; type="application/vnd.oai.openapi+json"`,
},
});
}
+7 -1
View File
@@ -51,6 +51,10 @@ function parseDate(value: string | undefined): Date | undefined {
export default function sitemap(): MetadataRoute.Sitemap {
const generatedAt = new Date();
const paths: LocalizedPaths = new Map();
const trustEntries: SitemapEntry[] = ["/about", "/contact", "/privacy"].map((path) => ({
lastModified: generatedAt,
url: absoluteUrl(path),
}));
for (const locale of LOCALES) {
addPath(paths, "/", locale);
@@ -77,5 +81,7 @@ export default function sitemap(): MetadataRoute.Sitemap {
// Keep one canonical entry per URL. Machine-readable `.mdx`/`llms` routes
// and the unprefixed native-showcase fallback are intentionally excluded.
return [...new Map(localizedEntries.map((entry) => [entry.url, entry])).values()];
return [
...new Map([...trustEntries, ...localizedEntries].map((entry) => [entry.url, entry])).values(),
];
}
+11
View File
@@ -8,6 +8,17 @@ export function Footer({dict}: {dict: Dictionary["footer"]}) {
<p className="text-sm">
&copy; {new Date().getFullYear()} NextUI Inc. {dict.allRightsReserved}
</p>
<nav aria-label="HeroUI company information" className="flex items-center gap-2 text-sm">
<a className="hover:text-foreground" href="/about">
{dict.about}
</a>
<a className="hover:text-foreground" href="/contact">
{dict.contact}
</a>
<a className="hover:text-foreground" href="/privacy">
{dict.privacy}
</a>
</nav>
<SocialLinks />
</footer>
);
+25
View File
@@ -15,6 +15,7 @@ export const LINKSET_HEADERS = {
export const MARKDOWN_HEADERS = {
...AGENT_DISCOVERY_CACHE_HEADERS,
"Content-Type": "text/markdown; charset=utf-8",
Vary: "Accept, Accept-Encoding",
} as const;
export const OPENAPI_HEADERS = {
@@ -70,8 +71,10 @@ export function getHomepageLinkHeader(): string {
return [
'</.well-known/api-catalog>; rel="api-catalog"; type="application/linkset+json"',
'</.well-known/agent-skills/index.json>; rel="describedby"; type="application/json"',
'</.well-known/mcp>; rel="service"; type="application/json"',
'</.well-known/mcp/server-card.json>; rel="service-desc"; type="application/json"',
'</.well-known/oauth-protected-resource>; rel="oauth-protected-resource"; type="application/json"',
'</openapi.json>; rel="service-desc"; type="application/vnd.oai.openapi+json"',
'</llms.txt>; rel="service-doc"; type="text/plain"',
'</llms-full.txt>; rel="describedby"; type="text/plain"',
].join(", ");
@@ -87,6 +90,28 @@ export function jsonResponse(data: unknown, init: ResponseInit = {}): Response {
});
}
export function agentErrorResponse({
code,
hint,
message,
status,
}: {
code: string;
hint: string;
message: string;
status: number;
}): Response {
return jsonResponse(
{
code,
error: true,
hint,
message,
},
{status},
);
}
export function getAgentServiceBaseUrl(origin: string): string {
return absoluteUrl(origin, "/api/agent");
}
+10
View File
@@ -80,7 +80,10 @@ HeroUI is the modern UI library for web and mobile, built to help teams move fas
- [Full LLM documentation](${baseUrl}/llms-full.txt)
- [React LLM documentation](${baseUrl}/react/llms-full.txt)
- [Native LLM documentation](${baseUrl}/native/llms-full.txt)
- [HeroUI OpenAPI specification](${baseUrl}/openapi.json)
- [HeroUI Docs Agent API](${baseUrl}/api/agent)
- [API catalog](${baseUrl}/.well-known/api-catalog)
- [MCP discovery handshake](${baseUrl}/.well-known/mcp)
- [MCP server card](${baseUrl}/.well-known/mcp/server-card.json)
- [Agent skills index](${baseUrl}/.well-known/agent-skills/index.json)
@@ -90,6 +93,13 @@ HeroUI is the modern UI library for web and mobile, built to help teams move fas
- [HeroUI Native MCP server](${baseUrl}/docs/native/getting-started/mcp-server)
- [HeroUI React Agent Skills](${baseUrl}/docs/react/getting-started/agent-skills)
- [HeroUI Native Agent Skills](${baseUrl}/docs/native/getting-started/agent-skills)
- [HeroUI CLI](${baseUrl}/docs/react/getting-started/cli)
## About HeroUI
- [About HeroUI](${baseUrl}/about)
- [Contact HeroUI](${baseUrl}/contact)
- [HeroUI privacy information](${baseUrl}/privacy)
`;
}
+106
View File
@@ -0,0 +1,106 @@
import {
MCP_PACKAGE_VERSION,
NATIVE_MCP_API_URL,
REACT_MCP_API_URL,
absoluteUrl,
} from "@/lib/agent-discovery";
export function getMcpServerCard(origin: string) {
return {
$schema: "https://modelcontextprotocol.io/schemas/server-card/draft.json",
capabilities: {
prompts: false,
resources: false,
tools: true,
},
description:
"HeroUI MCP servers expose read-only HeroUI React and HeroUI Native documentation, component metadata, source references, styles, and theme variables to AI coding agents.",
endpoint: absoluteUrl(origin, "/.well-known/mcp/server-card.json"),
links: {
docs: [
absoluteUrl(origin, "/docs/react/getting-started/mcp-server"),
absoluteUrl(origin, "/docs/native/getting-started/mcp-server"),
],
npm: [
"https://www.npmjs.com/package/@heroui/react-mcp",
"https://www.npmjs.com/package/@heroui/native-mcp",
],
serverCard: absoluteUrl(origin, "/.well-known/mcp/server-card.json"),
source: "https://github.com/heroui-inc/heroui-mcp",
},
notes:
"HeroUI's supported MCP transport is stdio through the published npm packages. This well-known document is a discovery handshake for those existing servers, not a Streamable HTTP MCP endpoint.",
serverInfo: {
name: "HeroUI MCP",
version: MCP_PACKAGE_VERSION,
},
tools: [
{
description: "List all available HeroUI v3 React components.",
name: "list_components",
package: "@heroui/react-mcp",
},
{
description: "Get complete React component documentation.",
name: "get_component_docs",
package: "@heroui/react-mcp",
},
{
description: "Get React component TypeScript source code.",
name: "get_component_source_code",
package: "@heroui/react-mcp",
},
{
description: "Get React component CSS source styles.",
name: "get_component_source_styles",
package: "@heroui/react-mcp",
},
{
description: "Get HeroUI React theme variables.",
name: "get_theme_variables",
package: "@heroui/react-mcp",
},
{
description: "Browse full HeroUI React documentation.",
name: "get_docs",
package: "@heroui/react-mcp",
},
{
description: "List all available HeroUI Native components.",
name: "list_components",
package: "@heroui/native-mcp",
},
{
description: "Get complete Native component documentation.",
name: "get_component_docs",
package: "@heroui/native-mcp",
},
{
description: "Get HeroUI Native theme variables.",
name: "get_theme_variables",
package: "@heroui/native-mcp",
},
{
description: "Browse full HeroUI Native documentation.",
name: "get_docs",
package: "@heroui/native-mcp",
},
],
transports: [
{
args: ["-y", "@heroui/react-mcp@latest"],
command: "npx",
dataApi: REACT_MCP_API_URL,
package: "@heroui/react-mcp",
type: "stdio",
},
{
args: ["-y", "@heroui/native-mcp@latest"],
command: "npx",
dataApi: NATIVE_MCP_API_URL,
package: "@heroui/native-mcp",
type: "stdio",
},
],
};
}
+333
View File
@@ -0,0 +1,333 @@
import {
AGENT_API_VERSION,
NATIVE_MCP_API_URL,
REACT_MCP_API_URL,
getAgentServiceBaseUrl,
} from "@/lib/agent-discovery";
export type OpenAPIDocument = Record<string, unknown>;
function baseDocument(
title: string,
version: string,
serverUrl: string,
description: string,
): OpenAPIDocument {
return {
info: {
description,
title,
version,
},
openapi: "3.1.0",
security: [],
servers: [{url: serverUrl}],
};
}
const errorResponse = {
content: {
"application/json": {
schema: {$ref: "#/components/schemas/ErrorResponse"},
},
},
description: "A structured error with a stable code and recovery hint.",
};
export function agentApiDocument(origin: string): OpenAPIDocument {
return {
...baseDocument(
"HeroUI Docs Agent API",
AGENT_API_VERSION,
getAgentServiceBaseUrl(origin),
"Public, read-only endpoints for searching HeroUI documentation and retrieving markdown page content. No authentication or OAuth scopes are required.",
),
components: {
schemas: {
AgentPage: {
additionalProperties: false,
properties: {
description: {type: "string"},
markdown: {type: "string"},
title: {type: "string"},
url: {type: "string"},
},
required: ["description", "markdown", "title", "url"],
type: "object",
},
ErrorResponse: {
additionalProperties: false,
properties: {
code: {type: "string"},
error: {const: true, type: "boolean"},
hint: {type: "string"},
message: {type: "string"},
},
required: ["error", "code", "message", "hint"],
type: "object",
},
HealthResponse: {
additionalProperties: false,
properties: {
service: {const: "heroui-docs-agent-api", type: "string"},
status: {const: "ok", type: "string"},
version: {type: "string"},
},
required: ["service", "status", "version"],
type: "object",
},
SearchResponse: {
additionalProperties: false,
properties: {
count: {minimum: 0, type: "integer"},
platform: {enum: ["all", "react", "native"], type: "string"},
query: {type: "string"},
results: {
items: {$ref: "#/components/schemas/SearchResult"},
type: "array",
},
},
required: ["count", "platform", "query", "results"],
type: "object",
},
SearchResult: {
additionalProperties: false,
properties: {
description: {type: "string"},
platform: {enum: ["all", "react", "native"], type: "string"},
title: {type: "string"},
url: {type: "string"},
},
required: ["description", "platform", "title", "url"],
type: "object",
},
},
},
paths: {
"/health": {
get: {
description:
"Returns the current status and version of the public HeroUI Docs Agent API.",
operationId: "getAgentApiHealth",
responses: {
"200": {
content: {
"application/json": {
schema: {$ref: "#/components/schemas/HealthResponse"},
},
},
description: "The agent API is available.",
},
},
summary: "Check HeroUI agent API health",
},
},
"/page": {
get: {
description:
"Retrieves one public HeroUI documentation page, including its metadata and markdown body.",
operationId: "getHeroUIDocPageMarkdown",
parameters: [
{
description: "HeroUI docs page URL, for example /docs/react/components/button.",
in: "query",
name: "url",
required: true,
schema: {minLength: 1, type: "string"},
},
],
responses: {
"200": {
content: {
"application/json": {
schema: {$ref: "#/components/schemas/AgentPage"},
},
},
description: "Documentation page metadata and markdown.",
},
"400": errorResponse,
"404": errorResponse,
},
summary: "Retrieve HeroUI documentation as markdown",
},
},
"/search": {
get: {
description:
"Searches public HeroUI documentation titles, descriptions, URLs, and slugs, optionally restricted to React or React Native.",
operationId: "searchHeroUIDocs",
parameters: [
{
description: "Case-insensitive text to find in HeroUI documentation.",
in: "query",
name: "q",
required: true,
schema: {minLength: 1, type: "string"},
},
{
description: "Limit results to one HeroUI platform or search all documentation.",
in: "query",
name: "platform",
schema: {default: "all", enum: ["all", "react", "native"], type: "string"},
},
{
description: "Maximum number of matches to return.",
in: "query",
name: "limit",
schema: {default: 10, maximum: 20, minimum: 1, type: "integer"},
},
],
responses: {
"200": {
content: {
"application/json": {
schema: {$ref: "#/components/schemas/SearchResponse"},
},
},
description: "Matching HeroUI documentation pages.",
},
"400": errorResponse,
},
summary: "Search HeroUI documentation",
},
},
},
};
}
function mcpApiDocument(kind: "react" | "native"): OpenAPIDocument {
const isReact = kind === "react";
const serverUrl = isReact ? REACT_MCP_API_URL : NATIVE_MCP_API_URL;
const title = isReact ? "HeroUI React MCP Data API" : "HeroUI Native MCP Data API";
const componentSourcePaths = isReact
? {
"/v1/components/source": {
post: {
description: "Returns TypeScript source for requested HeroUI React components.",
operationId: "getComponentSource",
requestBody: {
content: {
"application/json": {
schema: {
properties: {components: {items: {type: "string"}, type: "array"}},
required: ["components"],
type: "object",
},
},
},
required: true,
},
responses: {"200": {description: "Component source code."}},
summary: "Get component TypeScript source",
},
},
"/v1/components/styles": {
post: {
description: "Returns CSS source for requested HeroUI React components.",
operationId: "getComponentStyles",
requestBody: {
content: {
"application/json": {
schema: {
properties: {components: {items: {type: "string"}, type: "array"}},
required: ["components"],
type: "object",
},
},
},
required: true,
},
responses: {"200": {description: "Component CSS styles."}},
summary: "Get component styles",
},
},
}
: {};
return {
...baseDocument(
title,
"1.1.0",
serverUrl,
`Public, read-only data API used by the ${isReact ? "@heroui/react-mcp" : "@heroui/native-mcp"} package and HeroUI agent skills.`,
),
paths: {
"/health": {
get: {
description: "Returns the current availability of the HeroUI MCP data API.",
operationId: "getHealth",
responses: {"200": {description: "Health status."}},
summary: "Check API health",
},
},
"/v1/components": {
get: {
description: "Lists the component names available for this HeroUI platform.",
operationId: "listComponents",
responses: {"200": {description: "Available components."}},
summary: "List available components",
},
},
"/v1/components/docs": {
post: {
description: "Returns documentation for requested HeroUI components.",
operationId: "getComponentDocs",
requestBody: {
content: {
"application/json": {
schema: {
properties: {components: {items: {type: "string"}, type: "array"}},
required: ["components"],
type: "object",
},
},
},
required: true,
},
responses: {"200": {description: "Component documentation."}},
summary: "Get component documentation",
},
},
...componentSourcePaths,
"/v1/docs/{path}": {
get: {
description: "Returns a HeroUI documentation resource by its canonical path.",
operationId: "getDocs",
parameters: [
{
description: "Canonical documentation path.",
in: "path",
name: "path",
required: true,
schema: {type: "string"},
},
],
responses: {"200": {description: "Documentation content."}},
summary: "Get documentation by path",
},
},
"/v1/themes/variables": {
get: {
description: "Returns the design tokens for a named HeroUI theme.",
operationId: "getThemeVariables",
parameters: [
{
description: "Theme identifier.",
in: "query",
name: "theme",
schema: {default: "default", type: "string"},
},
],
responses: {"200": {description: "Theme variables."}},
summary: "Get theme variables",
},
},
},
};
}
export const OPENAPI_BUILDERS: Record<string, (origin: string) => OpenAPIDocument> = {
"heroui-agent-api.json": agentApiDocument,
"heroui-native-mcp-api.json": () => mcpApiDocument("native"),
"heroui-react-mcp-api.json": () => mcpApiDocument("react"),
};
+4 -1
View File
@@ -67,7 +67,10 @@
"description": "探索展示 HeroUI 组件在真实场景中应用的互动示例。"
},
"footer": {
"allRightsReserved": "保留所有权利。"
"about": "关于",
"allRightsReserved": "保留所有权利。",
"contact": "联系",
"privacy": "隐私"
},
"newsletter": {
"label": "Hero 新闻通讯",
+4 -1
View File
@@ -67,7 +67,10 @@
"description": "Explore interactive examples showcasing HeroUI components in real-world scenarios."
},
"footer": {
"allRightsReserved": "All rights reserved."
"about": "About",
"allRightsReserved": "All rights reserved.",
"contact": "Contact",
"privacy": "Privacy"
},
"newsletter": {
"label": "Hero Newsletter",
+5
View File
@@ -4,6 +4,11 @@ export function getOrganizationJsonLd() {
return {
"@context": "https://schema.org",
"@type": "Organization",
contactPoint: {
"@type": "ContactPoint",
contactType: "customer support",
email: "sales@heroui.com",
},
logo: "https://heroui.com/icons/favicon.svg",
name: "HeroUI",
sameAs: [siteConfig.links.github, siteConfig.links.twitter, siteConfig.links.discord],
+25
View File
@@ -79,6 +79,19 @@ export function generateIndexHeader(): string[] {
const nativeUrl = "/native/llms.txt";
const reactAbsoluteUrl = formatAbsoluteUrl(reactUrl);
const nativeAbsoluteUrl = formatAbsoluteUrl(nativeUrl);
const agentResources = [
["HeroUI OpenAPI specification", "/openapi.json"],
["HeroUI Docs Agent API", "/api/agent"],
["HeroUI Agent API health", "/api/agent/health"],
["HeroUI Agent API search", "/api/agent/search"],
["HeroUI Agent API page retrieval", "/api/agent/page"],
["HeroUI API catalog", "/.well-known/api-catalog"],
["HeroUI MCP discovery handshake", "/.well-known/mcp"],
["HeroUI MCP server card", "/.well-known/mcp/server-card.json"],
["HeroUI React MCP documentation", "/docs/react/getting-started/mcp-server"],
["HeroUI Native MCP documentation", "/docs/native/getting-started/mcp-server"],
["HeroUI CLI documentation", "/docs/react/getting-started/cli"],
] as const;
return [
"# HeroUI v3 Documentation",
@@ -107,6 +120,18 @@ export function generateIndexHeader(): string[] {
`- **React (Web)**: [${reactUrl}](${reactAbsoluteUrl}) - React component library for web applications`,
`- **React Native**: [${nativeUrl}](${nativeAbsoluteUrl}) - React Native component library for mobile applications`,
"",
"## HeroUI Agent and Developer Resources",
"",
"HeroUI v3 web and mobile libraries are free, open-source software. The public docs agent API and existing MCP packages are read-only and require no account or paid plan.",
"",
...agentResources.map(([title, path]) => `- [${title}](${formatAbsoluteUrl(path)})`),
"",
"## HeroUI Trust Pages",
"",
`- [About HeroUI](${formatAbsoluteUrl("/about")})`,
`- [Contact HeroUI](${formatAbsoluteUrl("/contact")})`,
`- [HeroUI Privacy](${formatAbsoluteUrl("/privacy")})`,
"",
"## Documentation Index",
"",
];
+3 -2
View File
@@ -31,6 +31,7 @@ const MARKDOWN_EXCLUDED_PATHS = new Set([
const PUBLIC_FILE_PATTERN = /\.[a-z0-9]+$/i;
const UNRESOLVED_TEMPLATE_PATTERN = /(?:\{[^/{}]+\}|%7B[^/]+%7D)/i;
const PERMANENT_LOCALIZED_PREFIXES = ["/blog", "/docs", "/showcase", "/themes"];
const TRUST_PATHS = new Set(["/about", "/contact", "/privacy"]);
// Routes that live outside `app/[lang]` and must never receive a locale prefix.
const LOCALE_REDIRECT_EXCLUDED_PREFIXES = [
@@ -136,8 +137,8 @@ export function proxy(request: NextRequest) {
if (!getLocaleFromPath(pathname) && !shouldSkipLocaleRedirect(pathname)) {
const url = request.nextUrl.clone();
if (pathname === "/") {
url.pathname = `/${DEFAULT_LOCALE}`;
if (pathname === "/" || TRUST_PATHS.has(pathname)) {
url.pathname = `/${DEFAULT_LOCALE}${pathname === "/" ? "" : pathname}`;
return addHomepageDiscoveryHeaders(NextResponse.rewrite(url), pathname);
}
+180
View File
@@ -0,0 +1,180 @@
import type {ReactElement, ReactNode} from "react";
import {NextRequest} from "next/server";
import {isValidElement} from "react";
import {describe, expect, it, vi} from "vitest";
import AboutPage from "@/app/[lang]/(home)/about/page";
import ContactPage from "@/app/[lang]/(home)/contact/page";
import HomePage from "@/app/[lang]/(home)/page";
import PrivacyPage from "@/app/[lang]/(home)/privacy/page";
import {GET as getMarkdown} from "@/app/agent-markdown/route";
import {GET as getUnknownAgentEndpoint} from "@/app/api/agent/[...path]/route";
import {GET as getAgentPage} from "@/app/api/agent/page/route";
import {GET as searchAgentDocs} from "@/app/api/agent/search/route";
import {GET as getOpenApi} from "@/app/openapi.json/route";
import {getOrganizationJsonLd} from "@/lib/json-ld";
import {generateIndexHeader} from "@/lib/llms-utils";
import {GET as getMcpHandshake} from "@/app/.well-known/mcp/route";
vi.mock("@/lib/get-llm-text", () => ({
getLLMText: vi.fn(),
}));
vi.mock("@/lib/source", () => ({
source: {
getPage: vi.fn(),
getPages: vi.fn(() => []),
},
}));
function extractText(node: ReactNode): string {
if (typeof node === "string" || typeof node === "number") return String(node);
if (Array.isArray(node)) return node.map(extractText).join(" ");
if (!isValidElement(node)) return "";
const element = node as ReactElement<{
children?: ReactNode;
description?: string;
title?: string;
}>;
return [
element.props.title ?? "",
element.props.description ?? "",
extractText(element.props.children),
].join(" ");
}
function collectHeadingLevels(node: ReactNode): number[] {
if (Array.isArray(node)) return node.flatMap(collectHeadingLevels);
if (!isValidElement(node)) return [];
const element = node as ReactElement<{children?: ReactNode}>;
const level =
typeof element.type === "string" && /^h[1-6]$/.test(element.type)
? Number(element.type.slice(1))
: null;
return [...(level === null ? [] : [level]), ...collectHeadingLevels(element.props.children)];
}
describe("HeroUI agent readiness", () => {
it("publishes a typed OpenAPI alias with unique documented operations", async () => {
const response = getOpenApi(new Request("https://heroui.com/openapi.json"));
const document = (await response.json()) as {
info: {title: string};
openapi: string;
paths: Record<string, Record<string, {description?: string; operationId?: string}>>;
};
const operations = Object.values(document.paths).flatMap((path) => Object.values(path));
const operationIds = operations.map((operation) => operation.operationId);
expect(response.headers.get("content-type")).toContain("application/vnd.oai.openapi+json");
expect(document.info.title).toBe("HeroUI Docs Agent API");
expect(document.openapi).toBe("3.1.0");
expect(operations.every((operation) => Boolean(operation.description))).toBe(true);
expect(new Set(operationIds).size).toBe(operationIds.length);
});
it("returns structured JSON errors from agent API routes", async () => {
const responses = [
await searchAgentDocs(new NextRequest("https://heroui.com/api/agent/search")),
await getAgentPage(new NextRequest("https://heroui.com/api/agent/page")),
getUnknownAgentEndpoint(),
];
for (const response of responses) {
const body = (await response.json()) as Record<string, unknown>;
expect(response.status).toBeGreaterThanOrEqual(400);
expect(response.headers.get("content-type")).toContain("application/json");
expect(body).toEqual({
code: expect.any(String),
error: true,
hint: expect.any(String),
message: expect.any(String),
});
}
});
it("varies negotiated markdown by Accept and Accept-Encoding", async () => {
const request = new NextRequest("https://heroui.com/agent-markdown?path=/", {
headers: {
accept: "text/markdown",
"x-heroui-markdown-path": "/",
},
});
const response = await getMarkdown(request);
expect(response.headers.get("content-type")).toContain("text/markdown");
expect(response.headers.get("vary")).toBe("Accept, Accept-Encoding");
expect(await response.text()).toContain("# HeroUI");
});
it("serves the existing HeroUI MCP packages from the well-known handshake", async () => {
const response = getMcpHandshake(new Request("https://heroui.com/.well-known/mcp"));
const card = (await response.json()) as {
endpoint: string;
transports: {package: string; type: string}[];
};
expect(card.endpoint).toBe("https://heroui.com/.well-known/mcp/server-card.json");
expect(card.transports).toEqual(
expect.arrayContaining([
expect.objectContaining({package: "@heroui/react-mcp", type: "stdio"}),
expect.objectContaining({package: "@heroui/native-mcp", type: "stdio"}),
]),
);
});
it("lists predictable HeroUI developer resources in llms.txt", () => {
const header = generateIndexHeader().join("\n");
expect(header).toContain("HeroUI OpenAPI specification");
expect(header).toContain("/openapi.json");
expect(header).toContain("/.well-known/mcp");
expect(header).toContain("/docs/react/getting-started/cli");
});
it("adds a verified HeroUI support contact without fabricating an address", () => {
const organization = getOrganizationJsonLd();
expect(organization.contactPoint).toEqual({
"@type": "ContactPoint",
contactType: "customer support",
email: "sales@heroui.com",
});
expect(organization).not.toHaveProperty("address");
});
it("ships substantial trust-page copy", () => {
for (const page of [AboutPage(), ContactPage(), PrivacyPage()]) {
expect(extractText(page).replace(/\s+/g, " ").trim().length).toBeGreaterThanOrEqual(500);
expect(collectHeadingLevels(page)).toEqual(expect.arrayContaining([2]));
}
const contactText = extractText(ContactPage());
const privacyText = extractText(PrivacyPage());
expect(contactText).toContain("sales@heroui.com");
expect(contactText).toContain("junior@heroui.com");
expect(contactText).not.toContain("jrgarciadev@gmail.com");
expect(privacyText).toContain("sales@heroui.com");
expect(privacyText).toContain("junior@heroui.com");
expect(privacyText).not.toContain("jrgarciadev@gmail.com");
expect(privacyText).toContain("Vercel Analytics");
expect(privacyText).toContain("PostHog");
expect(privacyText).toContain("IP address");
});
it("server-renders meaningful homepage text with a hierarchical outline", async () => {
const page = await HomePage({params: Promise.resolve({lang: "en"})});
const text = extractText(page).replace(/\s+/g, " ").trim();
const headings = collectHeadingLevels(page);
expect(text.length).toBeGreaterThanOrEqual(500);
expect(headings).toEqual(expect.arrayContaining([1, 2, 3]));
expect(headings.filter((level) => level === 1)).toHaveLength(1);
});
});
+1
View File
@@ -0,0 +1 @@
export {};
+38
View File
@@ -0,0 +1,38 @@
import {fileURLToPath} from "node:url";
import {defineConfig} from "vitest/config";
export default defineConfig({
esbuild: {
jsx: "automatic",
},
resolve: {
alias: [
{
find: "@/.source",
replacement: fileURLToPath(new URL("./.source/server.ts", import.meta.url)),
},
{
find: "@",
replacement: fileURLToPath(new URL("./src", import.meta.url)),
},
{
find: "server-only",
replacement: fileURLToPath(new URL("./tests/server-only.ts", import.meta.url)),
},
{
find: "~env",
replacement: fileURLToPath(new URL("./env.ts", import.meta.url)),
},
],
},
test: {
env: {
NEXT_PUBLIC_APP_ENV: "development",
NEXT_PUBLIC_CDN_URL: "http://localhost:3000",
NODE_ENV: "development",
},
environment: "node",
include: ["tests/**/*.test.ts"],
},
});
+156 -10
View File
@@ -349,6 +349,9 @@ importers:
tar:
specifier: 7.5.21
version: 7.5.21
vitest:
specifier: 4.1.11
version: 4.1.11(@opentelemetry/api@1.9.1)(@types/node@25.0.3)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(jsdom@30.0.1)(vite@7.3.5(@types/node@25.0.3)(jiti@2.7.0)(lightningcss@1.33.0)(tsx@4.21.0)(yaml@2.9.0))
packages/react:
dependencies:
@@ -2893,8 +2896,8 @@ packages:
react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
'@react-grab/cli@0.1.50':
resolution: {integrity: sha512-Px/Hwhhyk2PubCA4ZaRFsfvwxhbxXsetJyvqC6aFFi8WhJhA+oVC33aTzuAeWmM3fhb4/8ce8YsHXI1d6ChcKg==}
'@react-grab/cli@0.2.0':
resolution: {integrity: sha512-LVfA+j5cFT0Szd958WRD8W/B8ucCtz4ViiveDKWLqAwBkZFW9VRW1ZvwOUhp/M3SmVPtjDaRkW12GvAuTArP+Q==}
hasBin: true
'@react-spectrum/color@3.2.1':
@@ -4242,6 +4245,9 @@ packages:
'@vitest/expect@4.1.10':
resolution: {integrity: sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==}
'@vitest/expect@4.1.11':
resolution: {integrity: sha512-VX2x5vNJXET47KAFzwERI+KRMtTTCSWTfSMKsW7JsUsXV4psq++e3DvZpuTDOpHcxytiDs6p2nhVb2tVDiiUYw==}
'@vitest/mocker@4.1.10':
resolution: {integrity: sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==}
peerDependencies:
@@ -4253,30 +4259,56 @@ packages:
vite:
optional: true
'@vitest/mocker@4.1.11':
resolution: {integrity: sha512-2XJVD55d1o5AZous5CCGKS74g/riOj9odEt2bQpCVZeblHyHdnMeFl4jl0XjU21stf4mbjUkew2eXQZt65g5CQ==}
peerDependencies:
msw: ^2.4.9
vite: ^6.0.0 || ^7.0.0 || ^8.0.0
peerDependenciesMeta:
msw:
optional: true
vite:
optional: true
'@vitest/pretty-format@3.2.4':
resolution: {integrity: sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==}
'@vitest/pretty-format@4.1.10':
resolution: {integrity: sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==}
'@vitest/pretty-format@4.1.11':
resolution: {integrity: sha512-yiZzPbGTS9Sr/JpFl8zHrcIkAofNbFV6k21vIgQN/cY/oxZeXhJv5sc/MBJ5jFKWmWs+oJHw0UXLZjmf931+Vw==}
'@vitest/runner@4.1.10':
resolution: {integrity: sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==}
'@vitest/runner@4.1.11':
resolution: {integrity: sha512-LztvUgdwMNJMIkj3hQnnxiC2Xy1zNxq928W/xhjCLaNCzqTZOudjwbQf6v9IntZGPw132i2Lq2rgTRZHD3JHNw==}
'@vitest/snapshot@4.1.10':
resolution: {integrity: sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==}
'@vitest/snapshot@4.1.11':
resolution: {integrity: sha512-pN7ikn1ON7h8ee4gIAp4AzyK+zBtJPzVbqOgu5LCEh4VaJVbPQcgYQYJIMGQPXVeJJq1fnfazis7a5pFNPahog==}
'@vitest/spy@3.2.4':
resolution: {integrity: sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==}
'@vitest/spy@4.1.10':
resolution: {integrity: sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==}
'@vitest/spy@4.1.11':
resolution: {integrity: sha512-apNa/prQy2qCeywhnixOHPRCgGNhvg7T4Dapfl1GahLp/R+uhBm5cPyFoNVyqsNd2h1nJxL6BqqdIjiABL60YA==}
'@vitest/utils@3.2.4':
resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==}
'@vitest/utils@4.1.10':
resolution: {integrity: sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==}
'@vitest/utils@4.1.11':
resolution: {integrity: sha512-zTCVGpyFsGWBhllOyKlTw/vnr6D9qxsfSDyfbyZmTyjHw5N/VuvzHpHoQjm2ZJzn4RJgx5w4r7V0er69CmLgPQ==}
acorn-jsx@5.3.2:
resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
peerDependencies:
@@ -5298,6 +5330,7 @@ packages:
eslint@9.39.2:
resolution: {integrity: sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options.
hasBin: true
peerDependencies:
jiti: '*'
@@ -7384,8 +7417,8 @@ packages:
peerDependencies:
react: ^19.2.6
react-grab@0.1.50:
resolution: {integrity: sha512-zRkHKq/8a1msCpEOp8BDROeQZT50m0OH2XPrP6jk5op+JAHrlsm3pj7eAQMOsct87EZDeGNnu4r+sGsJJzyw1Q==}
react-grab@0.2.0:
resolution: {integrity: sha512-ohhsfXD4qN0j6cMQd56aaVJBPDF3kUdviF/Od1Eak/rEIXQ3svQ4gjkwTfuTZwTBnxHRL16p9JqdlVQO5nUHqA==}
hasBin: true
peerDependencies:
react: '>=17.0.0'
@@ -8403,6 +8436,47 @@ packages:
jsdom:
optional: true
vitest@4.1.11:
resolution: {integrity: sha512-fhACrNXUidIbGSBr5FlbuBkO7VWC1ZyLl0DO4CU2DrQoAPxX84Ysxs+HeGQpii5lZWV1Q4gBZTTu49mF+A6Edw==}
engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0}
hasBin: true
peerDependencies:
'@edge-runtime/vm': '*'
'@opentelemetry/api': ^1.9.0
'@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0
'@vitest/browser-playwright': 4.1.11
'@vitest/browser-preview': 4.1.11
'@vitest/browser-webdriverio': 4.1.11
'@vitest/coverage-istanbul': 4.1.11
'@vitest/coverage-v8': 4.1.11
'@vitest/ui': 4.1.11
happy-dom: '*'
jsdom: '*'
vite: ^6.0.0 || ^7.0.0 || ^8.0.0
peerDependenciesMeta:
'@edge-runtime/vm':
optional: true
'@opentelemetry/api':
optional: true
'@types/node':
optional: true
'@vitest/browser-playwright':
optional: true
'@vitest/browser-preview':
optional: true
'@vitest/browser-webdriverio':
optional: true
'@vitest/coverage-istanbul':
optional: true
'@vitest/coverage-v8':
optional: true
'@vitest/ui':
optional: true
happy-dom:
optional: true
jsdom:
optional: true
vscode-jsonrpc@8.2.0:
resolution: {integrity: sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==}
engines: {node: '>=14.0.0'}
@@ -11008,7 +11082,7 @@ snapshots:
react-dom: 19.2.6(react@19.2.6)
react-stately: 3.49.0(react@19.2.6)
'@react-grab/cli@0.1.50':
'@react-grab/cli@0.2.0':
dependencies:
agent-install: 0.0.6
commander: 14.0.3
@@ -12255,9 +12329,9 @@ snapshots:
obug: 2.1.4
std-env: 4.2.0
tinyrainbow: 3.1.1
vitest: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@25.0.3)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(jsdom@30.0.1)(vite@7.3.5(@types/node@25.0.3)(jiti@2.7.0)(lightningcss@1.33.0)(tsx@4.21.0)(yaml@2.9.0))
vitest: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@25.0.3)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(jsdom@30.0.1)(vite@7.3.5(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.33.0)(tsx@4.21.0)(yaml@2.9.0))
optionalDependencies:
'@vitest/browser': 4.1.10(vite@7.3.5(@types/node@25.0.3)(jiti@2.7.0)(lightningcss@1.33.0)(tsx@4.21.0)(yaml@2.9.0))(vitest@4.1.10)
'@vitest/browser': 4.1.10(vite@7.3.5(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.33.0)(tsx@4.21.0)(yaml@2.9.0))(vitest@4.1.10)
'@vitest/expect@3.2.4':
dependencies:
@@ -12276,6 +12350,15 @@ snapshots:
chai: 6.2.2
tinyrainbow: 3.1.1
'@vitest/expect@4.1.11':
dependencies:
'@standard-schema/spec': 1.1.0
'@types/chai': 5.2.3
'@vitest/spy': 4.1.11
'@vitest/utils': 4.1.11
chai: 6.2.2
tinyrainbow: 3.1.1
'@vitest/mocker@4.1.10(vite@7.3.5(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.33.0)(tsx@4.21.0)(yaml@2.9.0))':
dependencies:
'@vitest/spy': 4.1.10
@@ -12292,6 +12375,14 @@ snapshots:
optionalDependencies:
vite: 7.3.5(@types/node@25.0.3)(jiti@2.7.0)(lightningcss@1.33.0)(tsx@4.21.0)(yaml@2.9.0)
'@vitest/mocker@4.1.11(vite@7.3.5(@types/node@25.0.3)(jiti@2.7.0)(lightningcss@1.33.0)(tsx@4.21.0)(yaml@2.9.0))':
dependencies:
'@vitest/spy': 4.1.11
estree-walker: 3.0.3
magic-string: 0.30.21
optionalDependencies:
vite: 7.3.5(@types/node@25.0.3)(jiti@2.7.0)(lightningcss@1.33.0)(tsx@4.21.0)(yaml@2.9.0)
'@vitest/pretty-format@3.2.4':
dependencies:
tinyrainbow: 2.0.0
@@ -12300,11 +12391,20 @@ snapshots:
dependencies:
tinyrainbow: 3.1.1
'@vitest/pretty-format@4.1.11':
dependencies:
tinyrainbow: 3.1.1
'@vitest/runner@4.1.10':
dependencies:
'@vitest/utils': 4.1.10
pathe: 2.0.3
'@vitest/runner@4.1.11':
dependencies:
'@vitest/utils': 4.1.11
pathe: 2.0.3
'@vitest/snapshot@4.1.10':
dependencies:
'@vitest/pretty-format': 4.1.10
@@ -12312,12 +12412,21 @@ snapshots:
magic-string: 0.30.21
pathe: 2.0.3
'@vitest/snapshot@4.1.11':
dependencies:
'@vitest/pretty-format': 4.1.11
'@vitest/utils': 4.1.11
magic-string: 0.30.21
pathe: 2.0.3
'@vitest/spy@3.2.4':
dependencies:
tinyspy: 4.0.4
'@vitest/spy@4.1.10': {}
'@vitest/spy@4.1.11': {}
'@vitest/utils@3.2.4':
dependencies:
'@vitest/pretty-format': 3.2.4
@@ -12330,6 +12439,12 @@ snapshots:
convert-source-map: 2.0.0
tinyrainbow: 3.1.1
'@vitest/utils@4.1.11':
dependencies:
'@vitest/pretty-format': 4.1.11
convert-source-map: 2.0.0
tinyrainbow: 3.1.1
acorn-jsx@5.3.2(acorn@7.4.1):
dependencies:
acorn: 7.4.1
@@ -15987,9 +16102,9 @@ snapshots:
react: 19.2.6
scheduler: 0.27.0
react-grab@0.1.50(react@19.2.6):
react-grab@0.2.0(react@19.2.6):
dependencies:
'@react-grab/cli': 0.1.50
'@react-grab/cli': 0.2.0
bippy: 0.6.1(react@19.2.6)
optionalDependencies:
react: 19.2.6
@@ -16035,7 +16150,7 @@ snapshots:
react: 19.2.6
react-doctor: 0.9.12(@opentelemetry/core@2.10.0(@opentelemetry/api@1.9.1))(eslint@9.39.2(jiti@2.7.0))
react-dom: 19.2.6(react@19.2.6)
react-grab: 0.1.50(react@19.2.6)
react-grab: 0.2.0(react@19.2.6)
optionalDependencies:
esbuild: 0.27.7
unplugin: 3.3.0(esbuild@0.27.7)(rollup@4.62.4)(vite@7.3.5(@types/node@25.0.3)(jiti@2.7.0)(lightningcss@1.33.0)(tsx@4.21.0)(yaml@2.9.0))
@@ -17315,6 +17430,37 @@ snapshots:
transitivePeerDependencies:
- msw
vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@25.0.3)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(jsdom@30.0.1)(vite@7.3.5(@types/node@25.0.3)(jiti@2.7.0)(lightningcss@1.33.0)(tsx@4.21.0)(yaml@2.9.0)):
dependencies:
'@vitest/expect': 4.1.11
'@vitest/mocker': 4.1.11(vite@7.3.5(@types/node@25.0.3)(jiti@2.7.0)(lightningcss@1.33.0)(tsx@4.21.0)(yaml@2.9.0))
'@vitest/pretty-format': 4.1.11
'@vitest/runner': 4.1.11
'@vitest/snapshot': 4.1.11
'@vitest/spy': 4.1.11
'@vitest/utils': 4.1.11
es-module-lexer: 2.3.1
expect-type: 1.4.0
magic-string: 0.30.21
obug: 2.1.4
pathe: 2.0.3
picomatch: 4.0.5
std-env: 4.2.0
tinybench: 2.9.0
tinyexec: 1.3.0
tinyglobby: 0.2.17
tinyrainbow: 3.1.1
vite: 7.3.5(@types/node@25.0.3)(jiti@2.7.0)(lightningcss@1.33.0)(tsx@4.21.0)(yaml@2.9.0)
why-is-node-running: 2.3.0
optionalDependencies:
'@opentelemetry/api': 1.9.1
'@types/node': 25.0.3
'@vitest/browser-playwright': 4.1.10(playwright@1.62.1)(vite@7.3.5(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.33.0)(tsx@4.21.0)(yaml@2.9.0))(vitest@4.1.10)
'@vitest/coverage-v8': 4.1.10(@vitest/browser@4.1.10)(vitest@4.1.10)
jsdom: 30.0.1
transitivePeerDependencies:
- msw
vscode-jsonrpc@8.2.0: {}
vscode-languageserver-protocol@3.17.5: