active-redirects.json entries whose `from` lives outside the docs CloudFront function's frozen legacy-Sphinx prefix set currently 404 in production. The stub file exists in S3 at <from>/index.html but the CFF (mattermost-iac-docs-prod -> 05-cdn -> clean-url-rewrite.js) only serves that key for the frozen legacy prefixes; for any other first path segment it falls back to <from>.html, which doesn't exist for plugin-client-redirects stubs. Add a postbuild step that duplicates every stub at the sibling flat key. The legacy branch keeps hitting <from>/index.html unchanged; the fallback branch now finds <from>.html. Real content pages are never overwritten (the script skips any path where <from>.html already exists), .html-suffixed from-values are skipped (CFF Phase A canonicalizes them before Phase B routes them), and .. traversal in a malformed from is rejected before any filesystem call. Smoke-tested against a local build: 741 stubs duplicated, 0 real content pages touched, byte-identical meta-refresh content.
Mattermost Documentation Site
Docusaurus workspace for docs.mattermost.com,
living in the mattermost/mattermost
monorepo at docs/site/.
Content layout
| Directory | Route | Description |
|---|---|---|
docs/main/ |
/ |
User and admin documentation |
docs/develop/ |
/developers |
Developer documentation |
docs/api/ |
/api |
API reference intro + generated OpenAPI pages |
Paths are relative to the repository root. The Docusaurus site reads them
via the relative paths ../main, ../develop, ../api (from docs/site/).
Prerequisites
- Node.js ≥ 20 — use
nvm useinsidedocs/site/to pick up.nvmrc - Go and
make(required for the OpenAPI prebuild step, and for two of the three plugin SDK reference generators — see below) - Vale ≥ 3 (for content linting)
Local development
cd docs/site
npm ci
npm start # dev server at http://localhost:3000
Sidebar generation
The documentation and developers sidebars are generated from the content
directories (sidebars/documentation.generated.json /
developers.generated.json, both gitignored) by npm run build:sidebars.
Docusaurus imports these files directly, so they must exist before
docusaurus start or docusaurus build runs — on a fresh checkout there's
no other source for them. This is wired automatically via the prestart and
prebuild npm lifecycle hooks, so plain npm start / npm run build just
work.
(sidebars/active-redirects.json, by contrast, is committed — it's
regenerated and checked in manually via node scripts/gen-active-redirects.mjs
when the legacy redirect map changes, not on every build.)
Manual grouping overrides
Most top-level sections build their sidebar straight from the filesystem:
each subdirectory becomes a category, each file a doc, sorted by
sidebar_position frontmatter then filename. But a few sections are flat
piles of 15-40 files (or split across an inconsistent filesystem nesting
that doesn't reflect any real grouping) that read badly as-is, so
gen-documentation-sidebar.mjs layers a manual grouping override on
top of the auto-generated tree for those sections only: Overview
(OVERVIEW_GROUPS/OVERVIEW_ROOT_ORDER), Deployment Guide
(DEPLOYMENT_GROUPS/DEPLOYMENT_ROOT_ORDER), Administration Guide →
Configure (ADMIN_CONFIGURE_GROUPS/ADMIN_CONFIGURE_ORDER),
Administration Guide → Manage (ADMIN_MANAGE_GROUPS/ADMIN_MANAGE_ORDER),
Administration Guide → Onboard (ADMIN_ONBOARD_GROUPS/ADMIN_ONBOARD_ORDER),
Administration Guide → Scale (ADMIN_SCALE_GROUPS/ADMIN_SCALE_ORDER),
End User Guide → Collaborate (COLLABORATE_GROUPS/COLLABORATE_ORDER), and
Integrations Guide (INTEGRATIONS_GROUPS/INTEGRATIONS_ROOT_ORDER).
Usually the override only changes how the sidebar renders — files stay flat on disk at their existing paths, so URLs don't move. There are two exceptions, both cases where the nav home and the URL would otherwise disagree:
- Administration Guide → Scale: 21 of its 28 files (capacity planning,
HA/architecture, search infrastructure, caching) were physically moved to
deployment-guide/scale/and are grouped viaDEPLOYMENT_GROUPS.scaling, matching where Sphinx's live nav lists that content. Only the 7 monitoring/observability pages stayed underADMIN_SCALE_GROUPS. - The PostgreSQL and FIPS migration pages moved the other way, from
deployment-guide/toadministration-guide/manage/admin/, where Sphinx's only toctree for them lives. They're grouped underADMIN_MANAGE_GROUPS.dataMigration.
Since those moves change URLs, the Sphinx→Docusaurus redirect targets in
sidebars/active-redirects.json were updated to point at the new paths,
and internal .mdx links across the docs were rewritten accordingly.
Each override is a pair of constants near the top of the script:
- A
*_GROUPSmap of group key →{label, landing?, items}, whereitemsare doc basenames (relative to that section's directory) or nested inline group objects. A group'sitemscan itself contain a nested{label, items}sub-group, which adds a third category level — a four-level path of Guide → Group → Sub-group → page — for a section large enough to need it — seeOVERVIEW_GROUPS.subscription's "Cloud" sub-group, orADMIN_MANAGE_GROUPS.userAccess's "Attribute-Based Access Control" sub-group, for existing examples. - A
*_ROOT_ORDER/*_ORDERarray listing the top-level order: plain strings for standalone docs,{group: 'key'}for a group from the map above.
There's no single shared "grouping engine" — each overridden section gets
its own small buildXItem/regroupX (or buildXSidebar) pair that mirrors
the shape of the others (see buildCollaborateItem/regroupCollaborate for
the End User Guide → Collaborate one). Adding an override for a new section
means copying that shape for the new section, not extending a shared
function.
Adding a new file to one of these sections: the script fails loudly if
you forget it — it logs a WARN: N file(s) missing from *_ORDER and falls
back to appending the orphaned file(s) at the root of that section, so a
forgotten file surfaces as a warning during npm run build:sidebars
rather than silently disappearing. Add the new file's basename to the
relevant group's items (or to the root order array, if it's a
standalone/uncategorized page) to place it deliberately instead of
leaving it at the root.
Adding a whole new manually-grouped section: copy the pattern of an
existing one (Integrations Guide is the simplest example), then wire it
into main() alongside the existing dir === '...' checks.
Nesting a doc from one section under a page in another section: most
group items are plain basenames relative to that section's own
directory, but buildAdminConfigureItem also accepts {doc: '<full id>'}
for cross-directory references (their label is read directly from the
target file's frontmatter via docLabelById, since it won't be in that
section's leafLabels map). This is how ADMIN_CONFIGURE_GROUPS.agents
nests the vendored Agents plugin pages (main/agents/docs/, staged by
stage-agents-docs.mjs — not one of the TOP_LEVEL sections, so it has
no top-level nav entry of its own) under
administration-guide/configure/agents-admin-guide. The same page is
also nested for End User Guide's end-user-guide/agents doc, but since
that section has no manual grouping override at all, it uses the smaller
standalone promoteDocToCategory helper instead of a full *_GROUPS
override — copy that pattern for other one-off single-doc nestings rather
than building a whole grouping override for a section that's otherwise
fine auto-generated.
Inlining another doc's content onto a page (rather than just linking
or nesting it): Docusaurus's built-in Markdown
partials
feature — any .md/.mdx file with a leading underscore in its name is
excluded from the docs plugin's routing/sidebars and can be imported
into another MDX file and rendered as <Component />. stage-agents-docs.mjs
uses this to reproduce Sphinx's .. include:: /agents/docs/admin_guide.md
behavior: it stages admin_guide.md/user_guide.md as normal (but
unlisted: true) docs for direct-link parity, and as
_admin_guide_partial.mdx/_user_guide_partial.mdx partials that
administration-guide/configure/agents-admin-guide.mdx and
end-user-guide/agents.mdx import and render inline — so those two pages
show the full vendored guide content directly, with zero extra clicks,
instead of just linking out to a separate page.
Plugin SDK reference generators
Three developer pages render content generated at build time from the plugin SDK's own source
rather than hand-written prose, each backed by a gitignored JSON file under data/:
| Page | Generator | Reads |
|---|---|---|
| Server plugin SDK reference | scripts/gen-plugin-godocs (Go) |
server/public/plugin |
| Web app plugin SDK reference | scripts/gen-plugin-jsdocs.mjs (Node) |
webapp/channels/src/plugins/registry.ts |
| Manifest reference | scripts/gen-plugin-manifest-docs (Go) |
server/public/model's Manifest struct |
They're consumed by the <PluginGoDocs />, <PluginGoExample />, <PluginJsDocs />, and
<PluginManifestDocs /> components (registered globally in src/theme/MDXComponents.tsx), and run
via npm run build:plugin-docs, wired into prestart/prebuild like everything else in this
section. The two Go generators parse their target packages with go/parser + go/doc rather than
type-checking them via golang.org/x/tools/go/packages, so they have no dependency on the Go
toolchain version declared in server/public/go.mod — only stdlib, no go.sum.
The API reference section (docs/api/reference/, also gitignored) has the
same requirement: docusaurus-plugin-openapi-docs needs docusaurus gen-api-docs mattermost run before it has any pages to render. prestart
handles this too — using the existing OpenAPI spec if present, only falling
back to the slow make -C api build spec rebuild if it's missing.
Full production build
The production build also includes an OpenAPI prebuild step (npm run build:openapi, wired to run automatically before npm run build via the
same prebuild hook) that invokes make -C api build. This requires Go and
takes ~2 minutes.
cd docs/site
npm ci
npm run build # runs build:sidebars + build:openapi (via prebuild), then Docusaurus build
To skip the OpenAPI rebuild during iterative content work:
cd docs/site
npm run build -- --no-minify # still runs build:sidebars + build:openapi first
If you need to bypass the prebuild step entirely (e.g., all generated artifacts already exist and are current), run:
cd docs/site
npm run docusaurus build # calls docusaurus directly, skips prebuild
Algolia search
There's a single Algolia DocSearch app for docs.mattermost.com — credentials
aren't distributed to individual developers. They're set as repository
variables (vars.ALGOLIA_APP_ID, vars.ALGOLIA_SEARCH_API_KEY) and injected
only in CI/CD. Local builds simply run without them: the site builds cleanly
and the search bar is omitted (see the conditional in
docusaurus.config.ts).
If you need to test search locally, export the same two variables in your
shell before running npm start/npm run build.
Scripts
| Command | Description |
|---|---|
npm start |
Dev server with hot reload (runs build:sidebars + build:openapi:docs first via prestart) |
npm run build |
Production build to build/ (runs build:sidebars + build:openapi first via prebuild) |
npm run build:sidebars |
Regenerate the documentation + developer sidebar JSON |
npm run build:openapi:spec |
Regenerate the OpenAPI spec only (slow — invokes make -C api build) |
npm run build:openapi:docs |
Regenerate the API reference MDX pages from the existing spec (fast) |
npm run build:openapi |
Full OpenAPI pipeline: spec then docs |
npm run build:plugin-docs |
Regenerate all three plugin SDK reference data files (see above) |
npm run serve |
Serve the build/ output locally |
npm run typecheck |
TypeScript type check |
node scripts/gen-active-redirects.mjs |
Regenerate legacy redirect map (committed to git; run manually when it changes) |
Content linting
vale main develop api