Files
docudjeex/CUSTOMIZATIONS.md
T

15 KiB

Customizations over base Docus

This project starts from the docus i18n starter template (extends: ['docus'] in nuxt.config.ts, Docus v5.x on Nuxt ^4.4.8). This file tracks everything added or changed on top of that base, and why, so a future contributor doesn't have to diff node_modules/docus to find out.

Packages

  • better-sqlite3 — Nuxt Content v3 stores all parsed markdown content in a local SQLite database (.data/content/contents.sqlite) using its own DB layer (db0) rather than reading files at request time. db0 needs an actual SQLite driver to talk to that file, and lists better-sqlite3 as a peer dependency (alongside alternatives like sqlite3 or @libsql/client) — peer dependencies aren't auto-installed by npm, so without declaring it explicitly, @nuxt/content has no driver to write to and the local content database silently fails to build.
  • @nuxtjs/i18n module added explicitly in nuxt.config.ts. The starter ships an i18n-shaped content structure (content/en/, content/fr/) out of the box, but that's just a folder convention — nothing routes /fr/... URLs, switches locales, or auto-detects the browser's language unless the module itself is registered.

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.
  • @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.
  • 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.
  • 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.
  • site.trailingSlash: true. The site builds as a static export (nuxt build, deployed as static files on a web server), and Nitro's default prerender.autoSubfolderIndex writes every route as path/index.html. A static web server serving that structure 301/308-redirects a bare path request to path/, 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 two items below exist alongside it.
  • experimental.defaults.nuxtLink.trailingSlash: 'append'. The native Nuxt-core (not @nuxtjs/i18n's own, separate trailingSlash option — that one only affects switchLocalePath(), and combining it with the middleware below 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 — so internal navigation never triggers the redirect from app/middleware/trailing-slash.global.ts in the first place.

app/middleware/trailing-slash.global.ts

Global route middleware (new; no Docus equivalent) that 301-redirects any route whose path doesn't already end in / to the slash-terminated version (skipping paths with a ., so actual files like /sitemap.xml or /favicon.ico are left alone). This exists because setting site.trailingSlash alone does nothing for the incoming request: Docus's useSeo.ts reads the current route.path as-is, so a visitor (or crawler) landing on a bare path without the slash still gets a canonical tag pointing at that same bare path. Redirecting first means route.path already carries the slash by the time useSeo.ts runs, which fixes canonical, og:url, hreflang, and JSON-LD all at once without duplicating any of Docus's composable. This also runs during prerendering, so Nitro's link-crawler discovers and renders each page under its slash-terminated URL.

server/routes/sitemap.xml.ts

Overrides Docus's own sitemap.xml route (node_modules/docus/server/routes/sitemap.xml.ts), for two reasons:

  • Docus's version resolves the site URL via inferSiteURL(), which only reads deployment-platform env vars (Vercel/Netlify/Cloudflare Pages, or NUXT_PUBLIC_SITE_URL/NUXT_SITE_URL) — never the site.url set in this project's nuxt.config.ts. In nuxt dev none of those env vars exist, so every <loc> came out as a bare relative path instead of an absolute URL, which is invalid per the sitemap spec.
  • Even where that env var happens to be set, Docus's version builds each <loc> with plain string concatenation and has no concept of site.trailingSlash at all, so it could never match the trailing-slash canonical/og:url above.

This override is otherwise a straight copy of Docus's route, with the URL-building swapped for createSitePathResolver() (from nuxt-site-config), which resolves from the same site config as canonical/og:url and honors trailingSlash correctly.

content.config.ts

Nuxt Content validates every page's frontmatter against a Zod schema per collection, and silently drops any key that isn't declared in that schema — it doesn't error, the field just isn't there at render time. This file reimplements docus's own createDocsSchema() (not something the docus package actually exports, so it has to be copied rather than imported) and extends it with the custom frontmatter toggles the page template relies on:

  • hideHeader — skip the title/description block on a page (used for pages that want a custom hero instead of the standard header).
  • hideCopyPage — hide the "Copy page" button group (for pages where "copy as markdown for an LLM" doesn't make sense).
  • hideToc — hide the right-hand table of contents (for short pages where a TOC would be mostly empty space).
  • contributors — the array populated by the getContributors() hook above; without this line in the schema, the hook's output would be computed and then thrown away.

This was a real bug during development: hideHeader/hideCopyPage did nothing at all until this schema was extended, because the fields were being stripped before the page component ever saw them.

app/app.config.ts

The old production site (docu.djeex.fr) has an established visual identity that a "generic Nuxt UI theme" migration would have lost. These overrides were measured directly against the live old site (colors picked from its actual computed styles, not eyeballed) so the new stack keeps the same look rather than just being a documentation theme:

  • docus.colorMode: 'dark' — the old site never had a light mode either; hard-locking it here removes the need for the toggle UI and light-theme variants entirely, rather than half-supporting a mode nobody uses.
  • ui.colors: primary cyan, neutral zinc — the site's brand accent color and its neutral gray scale.
  • ui.prose.card / ui.prose.pre / ui.header / ui.contentSearchButton / ui.contentSurround / ui.kbd: exact background/border hex values (a shared rgba(12,13,12,0.8) translucent-dark family, e.g. #121110 borders) matching the old site's card, code-block, header, search button, and prev/next-link chrome, since Nuxt UI's defaults use a different neutral scale that didn't match.
  • ui.prose.callout.compoundVariants: exact colors for all four admonition severities (info/success/warning/error), overriding Nuxt UI's default callout palette so ::note, ::tip, ::warning, ::caution render in the same colors the old site's ::alert boxes used, rather than Nuxt UI's stock blue/green/amber/red.

Custom / overridden components (app/components/)

Nuxt's convention is that a file at app/components/<any-subfolder>/<ExactComponentName>.vue overrides a layer's (here, docus's) auto-registered component of the same name — no explicit registration needed, just matching the filename. Each one below was diffed against the actual stock file in node_modules/docus to confirm it's a real, deliberate change and not an accidental untouched copy:

  • app/AppHeader.vue — added a Gitea social icon link alongside the stock GitHub link. The project's canonical repository lives on the user's self-hosted Gitea instance; GitHub is only a mirror, so a GitHub-only link would point visitors to the secondary copy.
  • app/AppHeaderCenter.vue — the most heavily rewritten component. Stock Docus sizes the header's nav menu to the header's own container width, but this site's actual docs pages use a narrower, off-center content column (a two-level 10-column grid: an outer sidebar column plus an inner article/TOC split) — so the stock menu didn't visually line up under the content it was supposed to sit above. This override renders the nav as an absolutely-positioned overlay that replicates that exact two-level grid, so it lines up with the real article column instead of the header's own slot. Also fixes a real bug found during development: pointer-events-auto was originally applied to the full-width wrapper div, which silently blocked clicks on the logo and the right-side icons (search, color mode, socials) everywhere except the homepage (a different code path with an empty nav). It's now scoped to only the innermost column div that actually contains clickable content.
  • app/AppHeaderBottom.vue — emptied to a no-op <div />. Once navigation moved into AppHeaderCenter above, the stock second nav row would have shown the same links twice and wasted vertical space in the header.
  • docs/DocsAsideLeftBody.vue — the left doc-tree sidebar is now collapsible and closed by default (stock: always fully expanded, not collapsible). With this site's number of nested sections, a fully-expanded tree was one very long scrollable list on every page load; collapsed-by-default lets a visitor see the top-level structure first and open only the section they need.
  • docs/DocsAsideLeftTop.vue — added a full-width search button above the sidebar for the header-based subnav mode (stock rendered nothing there in that mode, only in the "aside" subnav mode). Without it, visitors on pages using header-mode subnav had no visible way to open search from the sidebar area at all.
  • docs/DocsPageHeaderLinks.vue — gave the "Copy page" button group the same translucent-dark card styling used everywhere else on the site. Purely cosmetic: the stock Nuxt UI button styling didn't match the rest of the page chrome and stood out as an unstyled default.
  • prose/ProseNote.vue, ProseTip.vue, ProseWarning.vue, ProseCaution.vue (new files, no stock equivalent to override against — these are thin wrappers around Nuxt UI's own Callout.vue). Nuxt UI's admonition icon is normally set once, globally, per icon slot — there's no built-in way to omit it on just one specific admonition without changing it for every admonition of that type site-wide. These wrappers read an optional icon prop so a single instance can hide its icon (::note{icon=""}) when the emoji or leading text already conveys the same meaning, while every other ::note on the site keeps its default icon.
  • content/Ellipsis.vue (new; no Docus or Nuxt UI equivalent exists at all). The old site had a decorative blurred gradient glow behind section headers, and reproducing the content 1:1 meant this cosmetic effect needed some markdown-usable component to exist, since neither Docus nor Nuxt UI ships anything similar. Registered as the inline MDC component :ellipsis{left= width= top= blur= zIndex=}, used across content wherever the old site had that effect.

Page-level features (app/pages/[[lang]]/[...slug].vue)

This catch-all page isn't a docus override (docus doesn't ship one to override — this project defines its own), but it layers frontmatter-driven behavior on top of stock Nuxt Content rendering:

  • hideHeader / hideCopyPage / hideToc — read the three frontmatter toggles declared in content.config.ts above and conditionally skip rendering each block.
  • Contributors + history block. Below the "Edit this page" / "Report an issue" links, renders "Contributor(s): " from the contributors frontmatter field (populated by the git-log hook), with the names linking to that specific file's Gitea commit history. The goal is to give credit to everyone who's worked on a page — not just whoever last edited it — and let a reader jump straight to the full history of a page without leaving the site or knowing the underlying file path.

License

MIT (see LICENSE), same as the Docus theme this project is built on.