6146a08a58
* 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>
111 lines
2.9 KiB
TypeScript
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);
|