Files
heroui-inc--heroui/apps/docs/next.config.ts
T
Junior Garcia 6146a08a58 fix(docs): repair canonical URLs, sitemap coverage and robots.txt (#6735)
* fix(docs): canonicalize pages to the URLs that are actually served

Every docs page declared `https://www.heroui.com/docs/<slug>` as its
canonical, but that URL answers with two permanent redirects: `www` ->
apex, then `/docs/...` -> `/<lang>/docs/...`. Google therefore dropped
the crawled page in favour of a redirect, and both `/en` and `/cn`
variants pointed at the same English URL.

The root layout also declared the home page as canonical, which every
page without its own `alternates` inherited, self-excluding `/cn`,
`/<lang>/themes` and `/<lang>/showcase` from the index.

Canonicals now use the served locale-prefixed URL on the apex domain and
carry an hreflang cluster (en / zh-Hans / x-default). Untranslated blog
posts point at the default locale instead of self-canonicalizing a
duplicate.

* fix(docs): build the sitemap from the docs source instead of the prerender manifest

`next-sitemap` derives its URL list from Next's prerender manifest, and
every docs, blog and themes route is server-rendered on demand. The
published sitemap therefore listed 19 URLs — no documentation pages at
all — while advertising `llms-*.txt`, `manifest.webmanifest` and
`rss.xml` as indexable pages.

The App Router `sitemap.ts` enumerates the Fumadocs loader, the blog
collection and the showcase registry instead, producing 501 canonical
URLs with hreflang alternates, all of which answer 200 without a
redirect.

* fix(docs): serve robots.txt from the app and keep non-production hosts out of the index

The committed `public/robots.txt` was the artefact of an earlier
`next-sitemap` run, so its `Sitemap` and `Host` lines were pinned to
whichever host generated them — canary still advertised
`www.heroui.com`. Serving it from the app derives the sitemap URL from
the deployed host and keeps the Content Signals directive that
`MetadataRoute.Robots` cannot express.

Every route stays allowed for every crawler; nothing indexable is
blocked. Preview and development deployments opt out through
`X-Robots-Tag` instead, which is host-scoped and cannot de-index
production the way a stray `Disallow` would.

* fix(docs): exclude untranslated blog fallbacks from sitemap

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: WK Wong <wingkwong.code@gmail.com>
2026-07-29 18:10:37 +08:00

111 lines
2.9 KiB
TypeScript

import type {NextConfig} from "next";
import {createMDX} from "fumadocs-mdx/next";
import {getRedirects} from "./next-redirects";
// TODO: remove it for next typegen
// validate environment variables
// import "./env";
const withMDX = createMDX();
// Preview and development deployments serve the same content as production on a
// different host, so they must opt out of indexing. Anything other than an
// explicit non-production value stays indexable: a missing env var must never
// silently de-index the production site.
const appEnv = process.env["NEXT_PUBLIC_APP_ENV"];
const isIndexable = appEnv !== "preview" && appEnv !== "development";
const config: NextConfig = {
compress: true,
experimental: {
optimizePackageImports: [
"@heroui/react",
"@gravity-ui/icons",
"@iconify/react",
"lucide-react",
"motion",
"fumadocs-ui",
"fumadocs-core",
"react-aria-components",
],
},
async headers() {
return [
{
headers: [
{
key: "X-Robots-Tag",
value: isIndexable ? "index, follow" : "noindex, nofollow",
},
],
source: "/:path*",
},
{
// Apple requires the AASA file to be served with `application/json`
// exactly — any other content type (the default `application/octet-stream`
// for extensionless files, or `text/plain`) causes silent rejection by
// iOS, which then refuses to handle Universal Links for the domain.
// The short cache window lets us roll out AASA changes without waiting
// hours for stale CDN/iOS caches to expire.
headers: [
{key: "Content-Type", value: "application/json"},
{key: "Cache-Control", value: "public, max-age=3600, must-revalidate"},
],
source: "/.well-known/apple-app-site-association",
},
];
},
images: {
dangerouslyAllowLocalIP: true,
remotePatterns: [
{
hostname: "heroui-assets.nyc3.cdn.digitaloceanspaces.com",
pathname: "/**",
protocol: "https",
},
{
hostname: "img.heroui.chat",
pathname: "/**",
protocol: "https",
},
{
hostname: "avatars.githubusercontent.com",
pathname: "/**",
protocol: "https",
},
],
},
logging: {
fetches: {
fullUrl: true,
},
},
reactCompiler: true,
reactStrictMode: true,
async redirects() {
return getRedirects();
},
async rewrites() {
return [
{
destination: "/llms.mdx/:lang/:path*",
source: "/:lang(en|cn)/docs/:path*.mdx",
},
{
destination: "/llms.mdx/:path*",
source: "/docs/:path*.mdx",
},
];
},
trailingSlash: false,
transpilePackages: ["@heroui/react", "@heroui/styles"],
typedRoutes: true,
typescript: {
ignoreBuildErrors: true,
},
};
export default withMDX(config);