Document today's nuxt.config.ts fixes in CUSTOMIZATIONS.md
This commit is contained in:
+7
-3
@@ -10,18 +10,22 @@ This project starts from the `docus` i18n starter template (`extends: ['docus']`
|
|||||||
## `nuxt.config.ts`
|
## `nuxt.config.ts`
|
||||||
|
|
||||||
- **Git-based page contributors.** `getContributors()` runs `git log --format=%an --follow -- <file>` for each markdown file and dedupes the author list, injected into the page's content via the `content:file:afterParse` hook. This was chosen over the Gitea/GitHub API because it needs no access token, no network call, and no rate limiting — the info is already in the checkout. The trade-off: CI must do a **full** (non-shallow) `git checkout`, otherwise `git log` only sees one commit per file and every page shows just its most recent author instead of everyone who ever touched it.
|
- **Git-based page contributors.** `getContributors()` runs `git log --format=%an --follow -- <file>` for each markdown file and dedupes the author list, injected into the page's content via the `content:file:afterParse` hook. This was chosen over the Gitea/GitHub API because it needs no access token, no network call, and no rate limiting — the info is already in the checkout. The trade-off: CI must do a **full** (non-shallow) `git checkout`, otherwise `git log` only sees one commit per file and every page shows just its most recent author instead of everyone who ever touched it.
|
||||||
- **`@nuxt/image` dev/prod path branch.** `@nuxt/image` resolves where to read local files from differently depending on context: in `nuxi dev` it needs an absolute filesystem path to `public/`, but in a production build it needs the plain relative string `'public/'` — passing the absolute path there breaks the `Content-Type` header on the built `/_ipx` image-proxy route specifically for SVGs (they'd get served with the wrong MIME type). The `isDev` check branches on the actual `nuxi` subcommand so both environments get the value they each require.
|
- **`@nuxt/image` dir is always an absolute path.** An earlier revision branched this on `nuxi dev` vs a production build, passing the plain relative string `'public/'` in production on the theory that an absolute path there broke the `/_ipx` route's SVG `Content-Type`. In production that relative path doesn't reliably resolve to the project root at the moment the prerender crawler actually requests an `/_ipx/*` URL. The real-world result was every single image on the site 404ing (`IPX_FILE_NOT_FOUND`) in a from-scratch production build, not a content-type quirk on some of them. Always resolving the absolute path (`fileURLToPath(new URL('./public', import.meta.url))`) fixed it in both modes; since the whole site is prerendered to static files anyway, each image's correct extension is what actually decides its serving content-type, not this setting.
|
||||||
|
- **`icon.serverBundle.collections`.** Every icon collection referenced dynamically (built from a variable/string at runtime rather than written as a literal `i-xxx` somewhere) has to be listed here explicitly, or Nuxt Icon's static usage scanner never finds it and falls back to a live `api.iconify.design` request at prerender time, which times out (`[Icon] loading icon X timed out`) wherever outbound network access is restricted, exactly as it was on the actual deployment host. Two places build icon names this way: `app/app.config.ts`'s `ui.prose.codeIcon` map (`simple-icons`, `lucide`) and `app/components/content/FileTreeNode.vue`'s per-extension file-type icons (`vscode-icons`). Add any new collection here the moment a new dynamic icon source is introduced. Check it's actually installed first with `ls node_modules/@iconify-json/`.
|
||||||
|
- **`components:` array: keep `'~/components'` as the first entry.** Passing a `components` array to Nuxt *replaces* its default `~/components` auto-scan instead of adding to it. Every local override in `app/components/` (`AppHeaderCenter`, `DocsAsideLeftTop`, etc., all documented below) was silently shadowed by the docus layer's originals the moment a custom array was added without also re-listing the default scan. The override files were still there, just never picked up. The second entry, `{ path: '~/components/prose', pathPrefix: false, global: true }`, exists because `ProseNote`/`ProseTip`/`ProseWarning`/`ProseCaution` are *only* ever resolved dynamically by name from Nuxt Content's MDC tag map (`note` → `ProseNote`, etc.), and nothing statically writes `<ProseNote>` in a template, so Vite's production build tree-shook all four out of both the client and server bundles entirely, and every admonition on the live site rendered as a raw, unstyled `<ProseNote icon to="...">` tag instead of the actual callout. `nuxt dev` never surfaces either of these: it doesn't tree-shake, and it resolves components on demand regardless of the scan config.
|
||||||
- **Custom icon collection.** `icon.customCollections` registers a `brand` prefix pointing at `app/assets/brand-icons/`, so logos for the user's other projects (Instameex, Lumeex) can be referenced from content as `i-brand-instameex` etc., exactly like any Iconify icon — without needing to publish them to an actual Iconify icon set first.
|
- **Custom icon collection.** `icon.customCollections` registers a `brand` prefix pointing at `app/assets/brand-icons/`, so logos for the user's other projects (Instameex, Lumeex) can be referenced from content as `i-brand-instameex` etc., exactly like any Iconify icon — without needing to publish them to an actual Iconify icon set first.
|
||||||
- **Markdown highlight.** Forces the `github-dark` Shiki theme for *both* the light and dark slots, because the site never actually offers a light mode (see `docus.colorMode: 'dark'` below) — maintaining two highlight themes for a mode nobody sees would just be dead config. The extra languages (`nginx, properties, php, toml, console, sh, yaml`) were added because the tutorial content includes config-file snippets and terminal output in all of these syntaxes, and none of them are in Shiki's minimal default bundle for Nuxt Content.
|
- **Markdown highlight.** Forces the `github-dark` Shiki theme for *both* the light and dark slots, because the site never actually offers a light mode (see `docus.colorMode: 'dark'` below) — maintaining two highlight themes for a mode nobody sees would just be dead config. The extra languages (`nginx, properties, php, toml, console, sh, yaml`) were added because the tutorial content includes config-file snippets and terminal output in all of these syntaxes, and none of them are in Shiki's minimal default bundle for Nuxt Content.
|
||||||
- **`darkreader-lock` meta tag.** The Dark Reader browser extension rewrites elements' inline `style` attributes on the client, after Nuxt has already server-rendered them — so any component using an inline `style` (like the cyan "·" separator spans) ends up with mismatched HTML between server and client, and Vue logs a hydration-mismatch warning on every page load for any visitor running that extension. This meta tag is Dark Reader's own opt-out signal, telling the extension to leave the page alone instead of trying to work around the mismatch after the fact.
|
- **`darkreader-lock` meta tag.** The Dark Reader browser extension rewrites elements' inline `style` attributes on the client, after Nuxt has already server-rendered them — so any component using an inline `style` (like the cyan "·" separator spans) ends up with mismatched HTML between server and client, and Vue logs a hydration-mismatch warning on every page load for any visitor running that extension. This meta tag is Dark Reader's own opt-out signal, telling the extension to leave the page alone instead of trying to work around the mismatch after the fact.
|
||||||
- **301 redirects (`routeRules`).** The old site served French content at root-level URLs (e.g. `/generalites/reseau/nat`, no locale prefix, on a separate `french` git branch). Restructuring into a single repo with `@nuxtjs/i18n`'s `/fr/...` prefix changed every French URL, which would otherwise break external links, bookmarks, and search-engine rankings for those pages built up over time. All 44 mappings use `statusCode: 301` explicitly — Nitro's default redirect status is 307 (temporary), which search engines don't treat as "please re-index this at the new URL" the way a 301 (permanent) does. There's deliberately **no** `/` → `/fr` redirect: root already serves English by default, and `@nuxtjs/i18n`'s `detectBrowserLanguage` already handles sending French-browser visitors to `/fr` automatically — a static redirect rule would just fight with that.
|
- **301 redirects (`routeRules`).** The old site (pre-rewrite, on the separate `docudjeex` repo's `main`/`french` branches) served English at the site root with no locale prefix (`/serveex/introduction/`) and French under `/fr/` with French-language slugs (`/fr/serveex/coeur/installation/`). This rewrite moved every URL under an explicit `/en/`/`/fr/` prefix with English-based slugs on both, which would otherwise break every external link, bookmark, and search-engine ranking built up on the old URLs. All 72 mappings use `statusCode: 301` explicitly, since Nitro's default redirect status is 307 (temporary), which search engines don't treat as "please re-index this at the new URL" the way a 301 (permanent) does. French pages whose old slug already matches the new one (`dozzle`, `immich`, and similar words that are spelled the same in both languages) are deliberately absent, since a rule there would redirect the page to itself. See the warning callout above this section: none of these 72 rules produce a real 301 on the actual production host, only a `location =` block in that host's nginx config does.
|
||||||
- **`site.trailingSlash: true`.** The site builds as a static export (`nuxt build`, deployed as static files on a web server) and the production host 301-redirects a bare `path` request to `path/` (verified against `docu.djeex.fr`), so canonical/og:url/sitemap URLs need to already carry the trailing slash — otherwise the canonical tag points at the very URL the server redirects away from, a loop that keeps the page out of search results. This is documented, official behavior for the wider Nuxt SEO ecosystem (`nuxtseo.com`'s "Trailing Slashes" guide), but Docus doesn't depend on `nuxt-seo-utils` for its canonical/og:url logic — it hand-rolls its own in `useSeo.ts` via a plain `joinURL(site.url, route.path)` that never checks this setting. That gap is why the items below exist alongside it.
|
- **`site.trailingSlash: true`.** The site builds as a static export (`nuxt build`, deployed as static files on a web server) and the production host 301-redirects a bare `path` request to `path/` (verified against `docu.djeex.fr`), so canonical/og:url/sitemap URLs need to already carry the trailing slash — otherwise the canonical tag points at the very URL the server redirects away from, a loop that keeps the page out of search results. This is documented, official behavior for the wider Nuxt SEO ecosystem (`nuxtseo.com`'s "Trailing Slashes" guide), but Docus doesn't depend on `nuxt-seo-utils` for its canonical/og:url logic — it hand-rolls its own in `useSeo.ts` via a plain `joinURL(site.url, route.path)` that never checks this setting. That gap is why the items below exist alongside it.
|
||||||
- **`nitro.prerender.autoSubfolderIndex: true`.** Docus sets this to `false` in its own `nuxt.config.ts`, which writes every route as `path.html` instead of `path/index.html` — the exact opposite of what the trailing-slash setup above needs, since the host would then redirect `/path` to `/path/` and find no directory there. Restoring the Nitro default puts the files back where the advertised URLs actually point.
|
- **`nitro.prerender.autoSubfolderIndex: true`.** Docus sets this to `false` in its own `nuxt.config.ts`, which writes every route as `path.html` instead of `path/index.html` — the exact opposite of what the trailing-slash setup above needs, since the host would then redirect `/path` to `/path/` and find no directory there. Restoring the Nitro default puts the files back where the advertised URLs actually point.
|
||||||
- **`nitro.prerender.routes: ['/', '/robots.txt']`.** Docus's `nitro:config` hook seeds one prerender route per locale (`/en`, `/fr`) and `/sitemap.xml`, but never `/` or `/robots.txt` — both exist as server routes yet were never written to the static output, so both 404 on a static host. `/` loses i18n's redirect to the default locale, and robots.txt loses the `Sitemap:` line that points crawlers at the sitemap (the live site has this gap today: requesting `/robots.txt` returns the site's HTML). Prerendering them is all that's needed. (Deliberately not a `routeRules` redirect for `/`: a hard redirect there would override i18n's `detectBrowserLanguage`, which is what sends French-browser visitors to `/fr`.)
|
- **`nitro.prerender.routes: ['/', '/robots.txt']`.** Docus's `nitro:config` hook seeds one prerender route per locale (`/en`, `/fr`) and `/sitemap.xml`, but never `/robots.txt`. It exists as a server route yet was never written to the static output, so it 404s on a static host and loses the `Sitemap:` line that points crawlers at the sitemap. `/` is listed too, though it turns out this doesn't actually matter: Nitro's crawler always visits `/` on its own regardless (it's the crawl's entry point), confirmed by removing it from this array and finding it prerendered anyway. `@nuxtjs/i18n`'s `detectBrowserLanguage` never actually runs on this static host: `/` is baked as a static `<meta http-equiv="refresh">` stub whose target is a fixed string decided once at build time (`defaultLocale: 'en'`), not read per visitor, and Nitro bakes *any* route carrying a `redirect` routeRule the same way, with no real HTTP status. The real 301 for `/` lives in the production host's nginx config instead, see the warning callout above.
|
||||||
- **`experimental.defaults.nuxtLink.trailingSlash: 'append'`.** The native Nuxt-core (not `@nuxtjs/i18n`'s own, separate `trailingSlash` option — that one only affects `switchLocalePath()` and double-appends the slash on hreflang alternate links) way to make every `<NuxtLink>` href, including the ones i18n's `switchLocalePath` builds for hreflang tags, resolve with a trailing slash already, matching both `site.trailingSlash` and the directory-style files on disk.
|
- **`experimental.defaults.nuxtLink.trailingSlash: 'append'`.** The native Nuxt-core (not `@nuxtjs/i18n`'s own, separate `trailingSlash` option — that one only affects `switchLocalePath()` and double-appends the slash on hreflang alternate links) way to make every `<NuxtLink>` href, including the ones i18n's `switchLocalePath` builds for hreflang tags, resolve with a trailing slash already, matching both `site.trailingSlash` and the directory-style files on disk.
|
||||||
|
|
||||||
> **Do not add a global trailing-slash redirect middleware here.** An earlier revision had `app/middleware/trailing-slash.global.ts` 301-redirecting bare paths to their slash form. It broke the production build outright: Nitro's prerender crawler seeds on `/en` and `/fr`, the middleware turned both into redirect responses, and since Nitro extracts no links from a redirect the crawl stopped immediately — 31 routes and 22 HTML files instead of 557 and 146, with every content page missing. The host already performs that redirect server-side, so the middleware bought nothing.
|
> **Do not add a global trailing-slash redirect middleware here.** An earlier revision had `app/middleware/trailing-slash.global.ts` 301-redirecting bare paths to their slash form. It broke the production build outright: Nitro's prerender crawler seeds on `/en` and `/fr`, the middleware turned both into redirect responses, and since Nitro extracts no links from a redirect the crawl stopped immediately — 31 routes and 22 HTML files instead of 557 and 146, with every content page missing. The host already performs that redirect server-side, so the middleware bought nothing.
|
||||||
|
|
||||||
|
> **A `routeRules` redirect can't produce a real HTTP redirect on this deployment.** The production host serves this site as plain static files (nginx `root` + `try_files`, no `proxy_pass` to a running Nuxt/Nitro process at all). Any route with a `redirect` rule still gets crawled and prerendered like any other page, but since a static file can't carry a custom HTTP status, Nitro bakes it as a client-side `<meta http-equiv="refresh">` stub instead. That's invisible to anything that doesn't execute the page (search-engine link-equity, most link-preview tools, `curl`). This was tried for `/` (redirecting to `/en/`) and reverted after confirming the real 301 only appears when running `node .output/server/index.mjs` directly, never through the actual production nginx config. The 72 legacy-URL redirects in `routeRules` below have the exact same limitation: they were made real 301s by adding matching `location = /path { return 301 ...; }` blocks directly in the site's nginx config, outside this repo entirely, not by anything in `nuxt.config.ts`. If this project ever moves to a host that runs the Nitro server itself instead of serving `.output/public` as static files, all of this becomes unnecessary and `routeRules` redirects will work as real 301s on their own.
|
||||||
|
|
||||||
## `server/routes/sitemap.xml.ts`
|
## `server/routes/sitemap.xml.ts`
|
||||||
|
|
||||||
Overrides Docus's own `sitemap.xml` route (`node_modules/docus/server/routes/sitemap.xml.ts`), for two reasons:
|
Overrides Docus's own `sitemap.xml` route (`node_modules/docus/server/routes/sitemap.xml.ts`), for two reasons:
|
||||||
|
|||||||
Reference in New Issue
Block a user