Some checks failed
Server CI / Postgres (shard 0) (push) Has been skipped
Server CI / Postgres (shard 3) (push) Has been skipped
BuildEnv Docker Image / build-image (push) Failing after 4s
BuildEnv Docker Image / build-image-fips (push) Failing after 4s
Migration Review & Release Notes / Generate migration review & release notes (push) Failing after 3s
Server CI / Build Environment (push) Failing after 3s
Server CI / Postgres (shard 1) (push) Has been skipped
Tools CI / check-style (mattermost-govet) (push) Failing after 4s
Server CI / Postgres (shard 2) (push) Has been skipped
Tools CI / check-style (mmgotool) (push) Failing after 3s
Server CI / Elasticsearch v8 Compatibility (push) Has been skipped
Server CI / OpenSearch v2 Compatibility (push) Has been skipped
Server CI / Postgres FIPS (shard 0) (push) Has been skipped
Tools CI / Test (mattermost-govet) (push) Failing after 3s
Server CI / Postgres FIPS (shard 1) (push) Has been skipped
Server CI / Postgres FIPS (shard 2) (push) Has been skipped
Tools CI / Test (mmgotool) (push) Failing after 3s
Server CI / Postgres FIPS (shard 3) (push) Has been skipped
Server CI / Run mmctl tests (push) Has been skipped
Server CI / Run mmctl tests (FIPS) (push) Has been skipped
Server CI / Merge Postgres Test Results (push) Has been skipped
Server CI / Merge Postgres FIPS Test Results (push) Has been skipped
Server CI / Check backport migrations (push) Has been skipped
Server CI / Check generated files (push) Has been skipped
Server CI / check-style (push) Has been skipped
Server CI / Vet API (push) Has been skipped
Server CI / Build mattermost server app (push) Has been skipped
Server CI / Server CI Complete (push) Failing after 2s
Server CI / Server CI Artifacts (push) Has been skipped
Server CI / Sentry Upload (push) Has been skipped
Server CI / Server CI Report (push) Failing after 5m1s
Server CI Nightly Race / Race Detector (push) Has been skipped
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Webapp CI Cache Warm / Warm Webapp node_modules Cache (push) Has been cancelled
E2E CI Cache Warm / Warm E2E npm Registry Cache (push) Has been cancelled
API / build (push) Has been cancelled
Web App CI / check-types (push) Has been cancelled
Web App CI / check-lint (push) Has been cancelled
Web App CI / check-i18n (push) Has been cancelled
Web App CI / check-external-links (push) Has been cancelled
Web App CI / test (platform) (push) Has been cancelled
Web App CI / test (mattermost-redux) (push) Has been cancelled
Web App CI / test (channels shard 1/4) (push) Has been cancelled
Web App CI / test (channels shard 2/4) (push) Has been cancelled
Web App CI / test (channels shard 3/4) (push) Has been cancelled
Web App CI / test (channels shard 4/4) (push) Has been cancelled
Web App CI / upload-coverage (push) Has been cancelled
Web App CI / build (push) Has been cancelled
YAML Lint / yamllint (push) Has been cancelled
Push mirrored docker images / cd/Push mirrored docker images (push) Has been cancelled
docs-cd / Build and deploy docs (push) Has been cancelled
Docs CI / build (push) Has been cancelled
E2E Cache Warm: Cypress deps / Warm Cypress deps cache (push) Has been cancelled
Scorecards supply-chain security / Scorecard analysis (push) Has been cancelled
Server CI Weekly / Postgres FIPS (push) Has been skipped
Server CI Weekly / Run mmctl tests (FIPS) (push) Has been skipped
Server CI Weekly / Postgres with binary parameters (push) Has been skipped
Server CI Weekly / Build Environment (push) Failing after 1s
Server CI Cache Warm / Warm Go Module and Build Cache (push) Failing after 2s
Server CI Nightly Race / Build Environment (push) Failing after 1s
2125 lines
77 KiB
JavaScript
2125 lines
77 KiB
JavaScript
#!/usr/bin/env node
|
|
// Generate the Documentation sidebar from the content tree under main/.
|
|
// Output: docs-site/sidebars/documentation.generated.json
|
|
//
|
|
// Mirrors gen-developer-sidebar.mjs in structure. Grouping config lives at
|
|
// the top of the file — that's what you touch to add or move a page. The
|
|
// builder functions below it rarely change for a content-only edit.
|
|
//
|
|
// Usage: node docs-site/scripts/gen-documentation-sidebar.mjs
|
|
|
|
import {readFileSync, writeFileSync, readdirSync, statSync, existsSync} from 'node:fs';
|
|
import {join, resolve, basename, dirname} from 'node:path';
|
|
import {fileURLToPath} from 'node:url';
|
|
|
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
const SITE_ROOT = resolve(HERE, '..');
|
|
const REPO_ROOT = resolve(SITE_ROOT, '..');
|
|
const SRC = join(REPO_ROOT, 'main');
|
|
const OUT = join(SITE_ROOT, 'sidebars', 'documentation.generated.json');
|
|
|
|
// ===========================================================================
|
|
// CONFIG — top-level sections, and manual grouping overrides.
|
|
// ===========================================================================
|
|
//
|
|
// By default a section's sidebar is built straight from the filesystem: each
|
|
// subdirectory becomes a category, each file a doc, sorted by
|
|
// `sidebar_position` frontmatter then filename. Sections that are flat piles
|
|
// of 15-49 files get a manual override instead, applied at render time only —
|
|
// the files stay where they are on disk, so URLs don't move.
|
|
//
|
|
// An override is a `*_GROUPS` map (key -> {label, landing?, items}) plus a
|
|
// `*_ORDER` array for the top level: strings for standalone docs,
|
|
// `{group: 'key'}` for a group. Group `items` can nest `{label, items}`
|
|
// sub-groups for deeper levels. A `*_HIDDEN` set lists files re-parented into
|
|
// a group so the orphan check doesn't re-append them at the section root; a
|
|
// page that belongs in no sidebar at all carries `unlisted: true` instead.
|
|
//
|
|
// Adding a file: add its basename to a group's `items`, or to the root order
|
|
// if it's standalone. Forgetting logs `WARN: N file(s) missing from *_ORDER`
|
|
// and appends it at the section root, so it surfaces as a build warning
|
|
// rather than disappearing.
|
|
|
|
const TOP_LEVEL = [
|
|
{dir: 'product-overview', label: 'Overview'},
|
|
{dir: 'use-case-guide', label: 'Use Case Guide'},
|
|
{dir: 'deployment-guide', label: 'Deployment Guide'},
|
|
{dir: 'administration-guide', label: 'Administration Guide'},
|
|
{dir: 'security-guide', label: 'Security Guide'},
|
|
{dir: 'end-user-guide', label: 'End User Guide'},
|
|
{dir: 'integrations-guide', label: 'Integrations Guide'},
|
|
{dir: 'get-help', label: 'Support and Community'},
|
|
];
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Overview — manual grouping override.
|
|
// ---------------------------------------------------------------------------
|
|
//
|
|
// The Overview directory stays flat (~40 .mdx files at one level) for URL
|
|
// stability — moving files into sub-directories would break the redirect
|
|
// table. Grouping here mirrors the live docs.mattermost.com structure.
|
|
|
|
const OVERVIEW_GROUPS = {
|
|
subscription: {
|
|
label: 'Subscription Overview',
|
|
landing: 'subscription',
|
|
items: [
|
|
'self-hosted-subscriptions',
|
|
{label: 'Cloud', landing: 'cloud-subscriptions', items: [
|
|
'cloud-dedicated',
|
|
'cloud-shared',
|
|
'cloud-vpc-private-connectivity',
|
|
]},
|
|
'non-profit-subscriptions',
|
|
],
|
|
},
|
|
releases: {
|
|
label: 'Releases and Life Cycle',
|
|
landing: 'releases-lifecycle',
|
|
items: [
|
|
'release-policy',
|
|
{label: 'Server', landing: 'server', items: [
|
|
'mattermost-server-releases',
|
|
'mattermost-v11-changelog',
|
|
'mattermost-v10-changelog',
|
|
'unsupported-legacy-releases',
|
|
'version-archive',
|
|
'ui-ada-changelog',
|
|
]},
|
|
{label: 'Desktop', landing: 'desktop', items: [
|
|
'mattermost-desktop-releases',
|
|
'desktop-app-changelog',
|
|
]},
|
|
{label: 'Mobile', landing: 'mobile', items: [
|
|
'mattermost-mobile-releases',
|
|
'mobile-app-changelog',
|
|
]},
|
|
'deprecated-features',
|
|
],
|
|
},
|
|
faq: {
|
|
label: 'Frequently Asked Questions',
|
|
landing: 'frequently-asked-questions',
|
|
items: [
|
|
'faq-general',
|
|
'faq-enterprise',
|
|
'faq-federal-procurement',
|
|
{label: 'Business & Licensing', landing: 'faq-license', items: [
|
|
'faq-mattermost-source-available-license',
|
|
]},
|
|
],
|
|
},
|
|
};
|
|
|
|
const OVERVIEW_ROOT_ORDER = [
|
|
'editions-and-offerings',
|
|
'plans',
|
|
{group: 'subscription'},
|
|
'certifications-and-compliance',
|
|
'accessibility-compliance-policy',
|
|
{group: 'releases'},
|
|
{group: 'faq'},
|
|
];
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Deployment Guide — manual grouping override.
|
|
// ---------------------------------------------------------------------------
|
|
//
|
|
// The operator's end-to-end path is one group, `serverDeployment`, ordered as
|
|
// a progression: Plan → Prepare → choose a method → make it highly available →
|
|
// secure it → back it up. That keeps the guide's top level to five entries —
|
|
// evaluate, pick a scenario, deploy the server, then the three client-side
|
|
// sections — instead of thirteen. The `server/` directory is dissolved into
|
|
// the Plan / Prepare / Deploy groups.
|
|
//
|
|
// There is no `scaling` group. Sizing is a planning question and lives in
|
|
// Plan; the search and cache pages are setup steps against real infrastructure
|
|
// and live in Prepare. What survives of the old Scale section is the
|
|
// `referenceArch` group, a holding pen for the nine `scale-to-*` pages pending
|
|
// the consolidation gated in docs/PARITY-reference-architectures.md.
|
|
//
|
|
// Troubleshooting is deliberately NOT one category. Each surface-specific page
|
|
// sits next to the step that produces its errors — the database pages under
|
|
// Prepare, Docker under Containers, and the app pages at the end of their own
|
|
// sections — because that is where the reader already is when it breaks. Only
|
|
// the cross-cutting page (logs, environment review, support-ticket data) has
|
|
// no section home, so it closes Server deployment as the single entry point
|
|
// for a reader who can't yet tell which layer failed.
|
|
|
|
const DEPLOYMENT_GROUPS = {
|
|
// Near the top rather than buried: these are the patterns regulated and
|
|
// disconnected deployments start from.
|
|
deploymentScenarios: {
|
|
label: 'Deployment Scenarios',
|
|
landing: 'deployment-scenarios/deployment-scenarios-index',
|
|
items: [
|
|
'deployment-scenarios/deploy-oob',
|
|
'deployment-scenarios/deploy-mission-partner',
|
|
'deployment-scenarios/deploy-ddil-operations',
|
|
'deployment-scenarios/deploy-sovereign-collaboration',
|
|
'deployment-scenarios/air-gapped-deployment',
|
|
],
|
|
},
|
|
|
|
// What you decide before touching a server. Deployment Solution Programs
|
|
// deliberately isn't here — most of that page is a compliance spec for
|
|
// third parties building installers, so it lives under Support and
|
|
// Community, linked from the Plan and Deploy landing pages.
|
|
//
|
|
// Software and hardware requirements sorts last: it states minimums for
|
|
// components the reader has already chosen and sized.
|
|
plan: {
|
|
label: 'Plan',
|
|
landing: 'server/server-deployment-planning',
|
|
items: [
|
|
'application-architecture',
|
|
'deployment-architecture',
|
|
{group: 'referenceArch'},
|
|
'software-hardware-requirements',
|
|
],
|
|
},
|
|
|
|
// The tested reference architectures, which "Size your deployment" is meant
|
|
// to replace. They are still here because the two disagree on the numbers
|
|
// they both publish — see docs/PARITY-reference-architectures.md. This group
|
|
// disappears when that report is signed off and the pages are deleted.
|
|
referenceArch: {
|
|
label: 'Reference architectures',
|
|
landing: 'scale/scaling-for-enterprise',
|
|
items: [
|
|
'scale/scale-to-200-users',
|
|
'scale/scale-to-2000-users',
|
|
'scale/scale-to-15000-users',
|
|
'scale/scale-to-30000-users',
|
|
'scale/scale-to-50000-users',
|
|
'scale/scale-to-80000-users',
|
|
'scale/scale-to-90000-users',
|
|
'scale/scale-to-100000-users',
|
|
'scale/scale-to-200000-users',
|
|
'scale/server-architecture',
|
|
'scale/backing-storage-benchmarks',
|
|
],
|
|
},
|
|
|
|
// Prerequisites that must exist before the install runs. Sits BEFORE Deploy
|
|
// deliberately: a reader following the sidebar top to bottom must not finish
|
|
// installing before reaching NGINX and TLS. The hard prerequisites lead, the
|
|
// production hardening follows, and search and cache close the group: they
|
|
// are setup steps against real infrastructure, but only large deployments
|
|
// provision them.
|
|
//
|
|
// The two MySQL pages are `unlisted: true` rather than listed here. MySQL is
|
|
// removed in v11, so they are no longer part of any supported install path,
|
|
// but they stay published because a reader mid-migration still needs them.
|
|
prepare: {
|
|
label: 'Prepare',
|
|
landing: 'server/preparations',
|
|
items: [
|
|
'server/prepare-database',
|
|
'server/prepare-file-storage',
|
|
'server/prepare-network',
|
|
'server/setup-nginx-proxy',
|
|
'server/setup-tls',
|
|
'server/image-proxy',
|
|
'server/pre-authentication-secrets',
|
|
'server/high-availability-cluster-based-deployment',
|
|
{label: 'Search infrastructure', landing: 'scale/enterprise-search', items: [
|
|
'scale/elasticsearch-setup',
|
|
'scale/opensearch-setup',
|
|
]},
|
|
{doc: 'scale/redis', label: 'Caching with Redis'},
|
|
],
|
|
},
|
|
|
|
// One sub-group per deployment method, with a landing page comparing them so
|
|
// the trade-offs sit next to the pages they describe. Named for the choice
|
|
// the reader is making here — Linux, Kubernetes, or containers — rather than
|
|
// for the act of deploying, which is what the whole parent group is about.
|
|
install: {
|
|
label: 'Choose a deployment method',
|
|
landing: 'server/deploy-server',
|
|
items: [
|
|
{label: 'Linux', landing: 'server/deploy-linux', items: [
|
|
'server/linux/deploy-ubuntu',
|
|
'server/linux/deploy-rhel',
|
|
'server/linux/deploy-tar',
|
|
'server/linux/deploy-azure-native-vm',
|
|
]},
|
|
{label: 'Kubernetes', landing: 'server/deploy-kubernetes', items: [
|
|
'server/kubernetes/deploy-k8s',
|
|
'server/kubernetes/deploy-k8s-oke',
|
|
]},
|
|
{label: 'Containers', landing: 'server/deploy-containers', items: [
|
|
'server/containers/fips-stig',
|
|
'server/docker-troubleshooting',
|
|
]},
|
|
],
|
|
},
|
|
|
|
// Encryption at rest and in transit depend on both the proxy from Prepare
|
|
// and a running server from Deploy, so this is its own step after both
|
|
// rather than a child of either.
|
|
secure: {
|
|
label: 'Secure your deployment',
|
|
landing: 'encryption-options',
|
|
items: [
|
|
'transport-encryption',
|
|
],
|
|
},
|
|
|
|
// Deployment-time architecture (HA-vs-DR, active/passive across two sites),
|
|
// not routine administration.
|
|
backupDr: {
|
|
label: 'Back up and recover',
|
|
landing: 'backup-disaster-recovery',
|
|
items: [
|
|
'disaster-recovery-aws',
|
|
],
|
|
},
|
|
|
|
// The operator's path, start to finish. Every child is a step in it, which
|
|
// is why the securing and backup steps are siblings of the install methods
|
|
// rather than children: each depends on a running server, not on which
|
|
// method produced it.
|
|
serverDeployment: {
|
|
label: 'Server deployment',
|
|
items: [
|
|
{group: 'plan'},
|
|
{group: 'prepare'},
|
|
{group: 'install'},
|
|
{group: 'secure'},
|
|
{group: 'backupDr'},
|
|
// Keeps its auto-generated tree: it has an index file and its children
|
|
// already set sidebar_position.
|
|
{auto: 'air-gapped-operations'},
|
|
{doc: 'server/troubleshooting', label: 'General deployment troubleshooting'},
|
|
],
|
|
},
|
|
|
|
// Moved here from Administration Guide → Configure: RTCD, Offloader, and
|
|
// the rest are deployment concerns, not settings-reference material.
|
|
calls: {
|
|
label: 'Calls Deployment',
|
|
landing: 'calls/calls-deployment-guide',
|
|
items: [
|
|
'calls/calls-rtcd-setup',
|
|
'calls/calls-offloader-setup',
|
|
'calls/calls-kubernetes',
|
|
'calls/calls-logging',
|
|
'calls/calls-metrics-monitoring',
|
|
],
|
|
},
|
|
|
|
// Explicit rather than auto-generated, so the section overview is the
|
|
// landing page and the children run in procedural order. Troubleshooting
|
|
// closes the section: symptom triage is what a reader reaches for after
|
|
// working through the rollout pages above it.
|
|
desktop: {
|
|
label: 'Desktop App Deployment',
|
|
landing: 'desktop/desktop-app-deployment',
|
|
items: [
|
|
'desktop/linux-desktop-install',
|
|
'desktop/distribute-a-custom-desktop-app',
|
|
'desktop/silent-windows-desktop-distribution',
|
|
'desktop/desktop-msi-installer-and-group-policy-install',
|
|
'desktop/desktop-custom-dictionaries',
|
|
'desktop/desktop-app-managed-resources',
|
|
'desktop/desktop-troubleshooting',
|
|
],
|
|
},
|
|
|
|
// Same treatment as Desktop. The FAQ and troubleshooting pages pair up at
|
|
// the end — both are question-shaped rather than procedural.
|
|
mobile: {
|
|
label: 'Mobile App Deployment',
|
|
landing: 'mobile/mobile-app-deployment',
|
|
items: [
|
|
'mobile/deploy-mobile-apps-using-emm-provider',
|
|
'mobile/configure-microsoft-intune-mam',
|
|
'mobile/distribute-custom-mobile-apps',
|
|
'mobile/host-your-own-push-proxy-service',
|
|
'mobile/consider-mobile-vpn-options',
|
|
'mobile/mobile-security-features',
|
|
'mobile/secure-mobile-file-storage',
|
|
'mobile/mobile-faq',
|
|
'mobile/mobile-troubleshooting',
|
|
],
|
|
},
|
|
};
|
|
|
|
// Deployment Scenarios stays a sibling of Server deployment rather than a
|
|
// child of Plan: it addresses evaluation, DNS failover, and zero-trust access
|
|
// constraints that shape the whole deployment, above the level of the
|
|
// server-specific planning pages.
|
|
const DEPLOYMENT_ROOT_ORDER = [
|
|
'quick-start-evaluation',
|
|
{group: 'deploymentScenarios'},
|
|
{group: 'serverDeployment'},
|
|
{group: 'calls'},
|
|
{group: 'desktop'},
|
|
{group: 'mobile'},
|
|
];
|
|
|
|
// Empty because every Deployment Guide page is placed explicitly above. An
|
|
// empty hidden list is the signal that nothing here is unreachable — keep it
|
|
// that way.
|
|
const DEPLOYMENT_HIDDEN = new Set([]);
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Administration Guide — Configure — manual grouping override.
|
|
// ---------------------------------------------------------------------------
|
|
//
|
|
// Grouped by task/subsystem so the ~13 "*-configuration-settings" reference
|
|
// pages don't drown the task-oriented pages sitting alongside them.
|
|
|
|
const ADMIN_CONFIGURE_GROUPS = {
|
|
settingsReference: {
|
|
// Ordered to follow the System Console's own left-hand nav.
|
|
label: 'System Console settings',
|
|
landing: 'configuration-settings',
|
|
items: [
|
|
'site-configuration-settings',
|
|
'authentication-configuration-settings',
|
|
'user-management-configuration-settings',
|
|
'system-attributes',
|
|
'environment-configuration-settings',
|
|
'reporting-configuration-settings',
|
|
'compliance-configuration-settings',
|
|
'integrations-configuration-settings',
|
|
'plugins-configuration-settings',
|
|
'self-hosted-account-settings',
|
|
'cloud-billing-account-settings',
|
|
'experimental-configuration-settings',
|
|
'deprecated-configuration-settings',
|
|
],
|
|
},
|
|
search: {
|
|
label: 'Search',
|
|
items: [
|
|
'bleve-search',
|
|
'enabling-chinese-japanese-korean-search',
|
|
],
|
|
},
|
|
email: {
|
|
label: 'Email',
|
|
items: [
|
|
'smtp-email',
|
|
'email-templates',
|
|
],
|
|
},
|
|
branding: {
|
|
label: 'Branding and customization',
|
|
landingDoc: 'administration-guide/manage/admin/customize-branding',
|
|
items: [
|
|
'customize-mattermost',
|
|
'custom-branding-tools',
|
|
{doc: 'administration-guide/manage/code-signing-custom-builds'},
|
|
],
|
|
},
|
|
// The Agents plugin's own provider/setup pages, staged into main/agents/docs/
|
|
// from the mattermost-plugin-agents submodule by stage-agents-docs.mjs. They
|
|
// nest under the admin guide landing page rather than getting a top-level
|
|
// section, and use the {doc: '<full id>'} form since they live outside
|
|
// administration-guide/configure/.
|
|
agents: {
|
|
label: 'AI agents',
|
|
landing: 'agents-admin-guide',
|
|
items: [
|
|
{doc: 'agents/docs/providers'},
|
|
{doc: 'agents/docs/aws_bedrock_setup'},
|
|
{doc: 'agents/docs/sovereign_ai'},
|
|
],
|
|
},
|
|
};
|
|
|
|
// Settings reference first, then the subsystems you configure at setup time,
|
|
// then the capabilities you turn on afterwards.
|
|
const ADMIN_CONFIGURE_ORDER = [
|
|
{group: 'settingsReference'},
|
|
'configuration-in-your-database',
|
|
'environment-variables',
|
|
{group: 'search'},
|
|
{group: 'email'},
|
|
'azure-blob-storage',
|
|
{group: 'branding'},
|
|
{group: 'agents'},
|
|
'manage-plugins',
|
|
'install-boards',
|
|
{doc: 'administration-guide/manage/admin/autotranslation'},
|
|
{doc: 'administration-guide/manage/admin/content-flagging', label: 'Set up content flagging'},
|
|
{doc: 'administration-guide/onboard/connected-workspaces'},
|
|
];
|
|
|
|
const ADMIN_CONFIGURE_HIDDEN = new Set([
|
|
'site-configuration-settings', 'authentication-configuration-settings',
|
|
'integrations-configuration-settings', 'plugins-configuration-settings',
|
|
'compliance-configuration-settings', 'reporting-configuration-settings',
|
|
'user-management-configuration-settings', 'environment-configuration-settings',
|
|
'experimental-configuration-settings', 'deprecated-configuration-settings',
|
|
'system-attributes', 'self-hosted-account-settings', 'cloud-billing-account-settings',
|
|
'bleve-search', 'enabling-chinese-japanese-korean-search',
|
|
'smtp-email', 'email-templates',
|
|
'custom-branding-tools', 'customize-mattermost',
|
|
// Listed under Monitor and troubleshoot / Manage.
|
|
'optimize-your-workspace', 'manage-user-surveys',
|
|
]);
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Administration Guide — Manage — manual grouping override.
|
|
// ---------------------------------------------------------------------------
|
|
//
|
|
// The flat top level (19 files) vs. nested manage/admin/ (18) split on disk is
|
|
// a filesystem artifact, and an inconsistent one — monitoring and billing
|
|
// pages are each scattered across both. Replaced here with task-based groups.
|
|
|
|
const ADMIN_MANAGE_GROUPS = {
|
|
// "Access control" rather than "Users and access": every page here is about
|
|
// what a user is allowed to do, not about the user records themselves.
|
|
//
|
|
// User attributes nests under ABAC as its first child. Custom profile
|
|
// attributes are usable on their own, and the page stays directly linkable,
|
|
// but they are a prerequisite of the access rules that follow — you define
|
|
// the attributes before you can write a policy against them.
|
|
//
|
|
// Advanced permissions is a two-page topic, so the main page is the group's
|
|
// landing and the backend-infrastructure page is its child.
|
|
accessControl: {
|
|
label: 'Access control',
|
|
landing: 'admin/user-management',
|
|
items: [
|
|
'team-channel-members',
|
|
{label: 'Advanced permissions', landingDoc: 'administration-guide/onboard/advanced-permissions', items: [
|
|
{doc: 'administration-guide/onboard/advanced-permissions-backend-infrastructure'},
|
|
]},
|
|
{doc: 'administration-guide/onboard/delegated-granular-administration'},
|
|
{label: 'Attribute-based access control', landing: 'admin/attribute-based-access-control', items: [
|
|
'admin/user-attributes',
|
|
'admin/abac-system-wide-policies',
|
|
'admin/abac-team-membership',
|
|
'admin/abac-team-channel-policies',
|
|
'admin/abac-channel-access-rules',
|
|
]},
|
|
],
|
|
},
|
|
serverOps: {
|
|
label: 'Server operations',
|
|
items: [
|
|
'mmctl-command-line-tool',
|
|
'command-line-tools',
|
|
'logging',
|
|
],
|
|
},
|
|
licensing: {
|
|
label: 'Licensing and billing',
|
|
items: [
|
|
'admin/self-hosted-billing',
|
|
'admin/installing-license-key',
|
|
],
|
|
},
|
|
cloudWorkspace: {
|
|
label: 'Cloud workspace management',
|
|
landing: 'cloud-workspace-management',
|
|
items: [
|
|
'cloud-data-export',
|
|
'cloud-data-residency',
|
|
'cloud-ip-filtering',
|
|
'cloud-byok',
|
|
],
|
|
},
|
|
notices: {
|
|
label: 'Notices and surveys',
|
|
items: [
|
|
'system-wide-notifications',
|
|
'in-product-notices',
|
|
{doc: 'administration-guide/upgrade/notify-admin'},
|
|
{doc: 'administration-guide/configure/manage-user-surveys'},
|
|
'user-satisfaction-surveys',
|
|
],
|
|
},
|
|
reference: {
|
|
label: 'Reference',
|
|
items: [
|
|
'product-limits',
|
|
'feature-labels',
|
|
],
|
|
},
|
|
};
|
|
|
|
// `admin/`-prefixed basenames live in the nested sub-folder.
|
|
const ADMIN_MANAGE_ORDER = [
|
|
{group: 'accessControl'},
|
|
{group: 'serverOps'},
|
|
{group: 'licensing'},
|
|
{group: 'cloudWorkspace'},
|
|
{group: 'notices'},
|
|
{group: 'reference'},
|
|
];
|
|
|
|
const ADMIN_MANAGE_HIDDEN = new Set([
|
|
'admin/server-maintenance',
|
|
'admin/user-management', 'admin/user-attributes', 'team-channel-members',
|
|
'admin/attribute-based-access-control', 'admin/abac-system-wide-policies',
|
|
'admin/abac-team-channel-policies', 'admin/abac-team-membership', 'admin/abac-channel-access-rules',
|
|
'command-line-tools', 'mmctl-command-line-tool', 'logging',
|
|
'admin/self-hosted-billing', 'admin/installing-license-key',
|
|
'cloud-workspace-management', 'cloud-data-export', 'cloud-data-residency', 'cloud-ip-filtering',
|
|
'cloud-byok',
|
|
'in-product-notices', 'system-wide-notifications', 'user-satisfaction-surveys',
|
|
'product-limits', 'feature-labels',
|
|
// Listed under Migration.
|
|
'bulk-export-tool', 'admin/migration', 'admin/postgres-migration',
|
|
'admin/postgres-migration-assist-tool', 'admin/manual-postgres-migration',
|
|
'admin/fips-migration',
|
|
// Listed under Configure.
|
|
'admin/content-flagging', 'admin/autotranslation', 'admin/customize-branding',
|
|
'code-signing-custom-builds',
|
|
// Listed under Monitor and troubleshoot.
|
|
'admin/monitoring-and-performance', 'statistics', 'telemetry',
|
|
'configure-health-check-probes', 'request-server-health-check',
|
|
'admin/error-codes', 'admin/generating-support-packet',
|
|
]);
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// End User Guide — Collaborate — manual grouping override.
|
|
// ---------------------------------------------------------------------------
|
|
//
|
|
// 48 files with Channels, Messaging, Calls, Teams, and Accessibility topics
|
|
// interleaved alphabetically, grouped here by topic. Item order within a group
|
|
// is the reader's task sequence, not alphabetical — joining a channel comes
|
|
// before creating one, archiving last.
|
|
|
|
const COLLABORATE_GROUPS = {
|
|
channels: {
|
|
label: 'Channels',
|
|
landing: 'collaborate-within-channels',
|
|
items: [
|
|
'channel-types',
|
|
'browse-channels',
|
|
'join-leave-channels',
|
|
'create-channels',
|
|
'channel-naming-conventions',
|
|
'channel-header-purpose',
|
|
'rename-channels',
|
|
'navigate-between-channels',
|
|
'favorite-channels',
|
|
'mark-channels-unread',
|
|
'manage-channel-members',
|
|
'manage-channel-bookmarks',
|
|
'display-channel-banners',
|
|
'convert-public-channels',
|
|
'convert-group-messages',
|
|
'archive-unarchive-channels',
|
|
],
|
|
},
|
|
messaging: {
|
|
label: 'Messages and threads',
|
|
landing: 'communicate-with-messages',
|
|
items: [
|
|
'send-messages',
|
|
'reply-to-messages',
|
|
'organize-conversations',
|
|
'format-messages',
|
|
'mention-people',
|
|
'react-with-emojis-gifs',
|
|
'share-files-in-messages',
|
|
'share-links',
|
|
'message-priority',
|
|
'schedule-messages',
|
|
'message-reminders',
|
|
'mark-messages-unread',
|
|
'save-pin-messages',
|
|
'forward-messages',
|
|
'search-for-messages',
|
|
'autotranslate-messages',
|
|
'flag-messages',
|
|
],
|
|
},
|
|
calls: {
|
|
label: 'Calls and screen sharing',
|
|
landing: 'audio-and-screensharing',
|
|
items: [
|
|
'make-calls',
|
|
],
|
|
},
|
|
teamsAndRoles: {
|
|
label: 'Teams, groups, and roles',
|
|
items: [
|
|
'learn-about-roles',
|
|
'organize-using-teams',
|
|
'team-settings',
|
|
'organize-using-custom-user-groups',
|
|
],
|
|
},
|
|
integrations: {
|
|
label: 'Integrations and connected apps',
|
|
items: [
|
|
'extend-mattermost-with-integrations',
|
|
'collaborate-within-connected-microsoft-teams',
|
|
],
|
|
},
|
|
accessibility: {
|
|
label: 'Keyboard shortcuts and accessibility',
|
|
items: [
|
|
'keyboard-shortcuts',
|
|
'team-keyboard-shortcuts',
|
|
'keyboard-accessibility',
|
|
],
|
|
},
|
|
};
|
|
|
|
const COLLABORATE_ORDER = [
|
|
'invite-people',
|
|
{group: 'channels'},
|
|
{group: 'messaging'},
|
|
{group: 'calls'},
|
|
{group: 'teamsAndRoles'},
|
|
{group: 'integrations'},
|
|
{group: 'accessibility'},
|
|
];
|
|
|
|
// agents-context-management leaves Collaborate entirely — it sits under AI
|
|
// Agents.
|
|
const COLLABORATE_HIDDEN = new Set([
|
|
'channel-types', 'browse-channels', 'create-channels', 'join-leave-channels',
|
|
'navigate-between-channels', 'channel-naming-conventions', 'channel-header-purpose',
|
|
'rename-channels', 'archive-unarchive-channels', 'favorite-channels',
|
|
'mark-channels-unread', 'manage-channel-members', 'manage-channel-bookmarks',
|
|
'display-channel-banners', 'autotranslate-messages', 'convert-public-channels',
|
|
'convert-group-messages',
|
|
'send-messages', 'communicate-with-messages', 'reply-to-messages', 'organize-conversations',
|
|
'format-messages', 'mark-messages-unread', 'mention-people', 'message-priority',
|
|
'message-reminders', 'schedule-messages', 'save-pin-messages', 'flag-messages',
|
|
'forward-messages', 'search-for-messages', 'share-links', 'share-files-in-messages',
|
|
'react-with-emojis-gifs',
|
|
'make-calls', 'audio-and-screensharing',
|
|
'organize-using-teams', 'team-settings', 'organize-using-custom-user-groups',
|
|
'learn-about-roles', 'invite-people',
|
|
'extend-mattermost-with-integrations', 'agents-context-management',
|
|
'collaborate-within-connected-microsoft-teams',
|
|
'keyboard-shortcuts', 'team-keyboard-shortcuts', 'keyboard-accessibility',
|
|
]);
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Administration Guide — Onboard — manual grouping override.
|
|
// ---------------------------------------------------------------------------
|
|
//
|
|
// SAML nests inside the single sign-on group since it alone accounts for 8
|
|
// files. AD/LDAP is a sibling group, not an SSO child — an admin can run
|
|
// directory synchronization without SSO.
|
|
|
|
const ADMIN_ONBOARD_GROUPS = {
|
|
sso: {
|
|
label: 'Single sign-on',
|
|
landing: 'corporate-directory-integration',
|
|
items: [
|
|
{
|
|
label: 'SAML',
|
|
landing: 'sso-saml',
|
|
items: [
|
|
'sso-saml-adfs',
|
|
'sso-saml-adfs-msws2016',
|
|
'sso-saml-entraid',
|
|
'sso-saml-keycloak',
|
|
'sso-saml-okta',
|
|
'sso-saml-onelogin',
|
|
'sso-saml-technical',
|
|
],
|
|
},
|
|
'sso-openidconnect',
|
|
'sso-google',
|
|
'sso-gitlab',
|
|
'sso-entraid',
|
|
'convert-oauth20-service-providers-to-openidconnect',
|
|
],
|
|
},
|
|
adldap: {
|
|
label: 'AD/LDAP',
|
|
items: [
|
|
'ad-ldap',
|
|
'ad-ldap-groups-synchronization',
|
|
'managing-team-channel-membership-using-ad-ldap-sync-groups',
|
|
],
|
|
},
|
|
};
|
|
|
|
// Identity setup first, then getting accounts in. Platform migration moved out
|
|
// to the Migration section — arriving from another platform is a one-time
|
|
// project, not part of standing up authentication.
|
|
//
|
|
// Multi-factor authentication and SSL client certificates are sibling leaves
|
|
// rather than a "Multi-factor and certificate authentication" group. That
|
|
// group existed to hold experimental certificate-based authentication, which
|
|
// is deprecated from v11 and now `unlisted: true`; grouping the two survivors
|
|
// under a joint heading buries MFA, which is the one most admins want.
|
|
const ADMIN_ONBOARD_ORDER = [
|
|
{group: 'sso'},
|
|
{group: 'adldap'},
|
|
'multi-factor-authentication',
|
|
'ssl-client-certificate',
|
|
'user-provisioning-workflows',
|
|
'guest-accounts',
|
|
];
|
|
|
|
const ADMIN_ONBOARD_HIDDEN = new Set([
|
|
'sso-saml', 'sso-saml-adfs', 'sso-saml-adfs-msws2016',
|
|
'sso-saml-entraid', 'sso-saml-keycloak', 'sso-saml-okta',
|
|
'sso-saml-onelogin', 'sso-saml-technical',
|
|
'sso-openidconnect', 'sso-google', 'sso-gitlab', 'sso-entraid',
|
|
'convert-oauth20-service-providers-to-openidconnect',
|
|
'corporate-directory-integration',
|
|
'ad-ldap', 'ad-ldap-groups-synchronization', 'managing-team-channel-membership-using-ad-ldap-sync-groups',
|
|
// Listed under Manage > Access control.
|
|
'advanced-permissions', 'advanced-permissions-backend-infrastructure',
|
|
'delegated-granular-administration',
|
|
// Listed under Configure.
|
|
'connected-workspaces',
|
|
// Listed under Migration.
|
|
'migrating-to-mattermost', 'migrate-from-slack', 'migrate-from-rocketchat', 'migrate-gitlab-omnibus',
|
|
'migration-announcement-email', 'bulk-loading-data',
|
|
]);
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Administration Guide — Scale — manual grouping override.
|
|
// ---------------------------------------------------------------------------
|
|
//
|
|
// The capacity planning, HA, search, and caching pages that used to live here
|
|
// were physically moved to `deployment-guide/scale/` (see the `scaling` group
|
|
// in DEPLOYMENT_GROUPS). What's left is monitoring, so regroupAdminScale
|
|
// re-labels the category "Monitor and troubleshoot" and pulls in the
|
|
// monitoring pages that live in manage/ on disk, by full doc id.
|
|
|
|
const ADMIN_SCALE_GROUPS = {
|
|
metrics: {
|
|
label: 'Metrics and dashboards',
|
|
items: [
|
|
'collect-performance-metrics',
|
|
'deploy-prometheus-grafana-for-performance-monitoring',
|
|
'performance-monitoring-metrics',
|
|
'performance-alerting',
|
|
'push-notification-health-targets',
|
|
],
|
|
},
|
|
health: {
|
|
label: 'Health and diagnostics',
|
|
items: [
|
|
{doc: 'administration-guide/configure/optimize-your-workspace'},
|
|
{doc: 'administration-guide/manage/statistics'},
|
|
{doc: 'administration-guide/manage/configure-health-check-probes'},
|
|
{doc: 'administration-guide/manage/request-server-health-check'},
|
|
{doc: 'administration-guide/manage/admin/generating-support-packet'},
|
|
{doc: 'administration-guide/manage/admin/error-codes'},
|
|
],
|
|
},
|
|
};
|
|
|
|
const ADMIN_SCALE_ORDER = [
|
|
{group: 'metrics'},
|
|
'deploy-grafana-loki-for-centralized-logging',
|
|
{group: 'health'},
|
|
{doc: 'administration-guide/manage/telemetry'},
|
|
'ensuring-releases-perform-at-scale',
|
|
];
|
|
|
|
const ADMIN_SCALE_HIDDEN = new Set([
|
|
'collect-performance-metrics', 'deploy-prometheus-grafana-for-performance-monitoring',
|
|
'performance-monitoring-metrics', 'performance-alerting', 'push-notification-health-targets',
|
|
]);
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Administration Guide — Comply and Upgrade — manual ordering overrides.
|
|
// ---------------------------------------------------------------------------
|
|
//
|
|
// Both are small enough not to need groups, but read badly in filename order.
|
|
// Comply runs by how widely each capability is used, reference material last;
|
|
// Upgrade follows the upgrade procedure.
|
|
|
|
const ADMIN_COMPLY_GROUPS = {};
|
|
|
|
const ADMIN_COMPLY_ORDER = [
|
|
'compliance-export',
|
|
'compliance-monitoring',
|
|
'electronic-discovery',
|
|
'data-retention-policy',
|
|
'export-mattermost-channel-data',
|
|
'legal-hold',
|
|
'custom-terms-of-service',
|
|
'embedded-json-audit-log-schema',
|
|
];
|
|
|
|
const ADMIN_COMPLY_HIDDEN = new Set([]);
|
|
|
|
const ADMIN_UPGRADE_GROUPS = {
|
|
afterUpgrade: {
|
|
label: 'After you upgrade',
|
|
items: [
|
|
'admin-onboarding-tasks',
|
|
'enterprise-roll-out-checklist',
|
|
'welcome-email-to-end-users',
|
|
],
|
|
},
|
|
};
|
|
|
|
const ADMIN_UPGRADE_ORDER = [
|
|
'important-upgrade-notes',
|
|
'prepare-to-upgrade-mattermost',
|
|
'communicate-scheduled-maintenance',
|
|
'upgrading-mattermost-server',
|
|
'upgrade-mattermost-kubernetes-ha',
|
|
'upgrading-postgres',
|
|
'enterprise-install-upgrade',
|
|
'downgrading-mattermost-server',
|
|
{group: 'afterUpgrade'},
|
|
'open-source-components',
|
|
];
|
|
|
|
const ADMIN_UPGRADE_HIDDEN = new Set([
|
|
'admin-onboarding-tasks', 'enterprise-roll-out-checklist', 'welcome-email-to-end-users',
|
|
// Listed under Manage > Notices and surveys.
|
|
'notify-admin',
|
|
]);
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Administration Guide — Migration — synthetic top-level section.
|
|
// ---------------------------------------------------------------------------
|
|
//
|
|
// Not built from a directory: these pages live in manage/ and onboard/ on
|
|
// disk. They are one section because bulk import and export exist for the same
|
|
// reason a database or FIPS migration does — someone is moving a deployment
|
|
// in, out, or between configurations. Splitting import/export away from
|
|
// migration would separate things that serve one job.
|
|
//
|
|
// Full doc ids throughout, since the pages come from two directories.
|
|
|
|
const ADMIN_MIGRATION = {
|
|
label: 'Migrate',
|
|
landing: 'administration-guide/manage/admin/migration',
|
|
items: [
|
|
'administration-guide/onboard/bulk-loading-data',
|
|
'administration-guide/manage/bulk-export-tool',
|
|
{label: 'Migrate from MySQL to PostgreSQL', landing: 'administration-guide/manage/admin/postgres-migration', items: [
|
|
'administration-guide/manage/admin/postgres-migration-assist-tool',
|
|
'administration-guide/manage/admin/manual-postgres-migration',
|
|
]},
|
|
'administration-guide/manage/admin/fips-migration',
|
|
{label: 'Migrate from another platform', landing: 'administration-guide/onboard/migrating-to-mattermost', items: [
|
|
'administration-guide/onboard/migrate-from-slack',
|
|
'administration-guide/onboard/migrate-from-rocketchat',
|
|
'administration-guide/onboard/migrate-gitlab-omnibus',
|
|
'administration-guide/onboard/migration-announcement-email',
|
|
]},
|
|
],
|
|
};
|
|
|
|
// Migrate sits last, after Upgrade: a customer builds the infrastructure,
|
|
// upgrades it, and then moves data into it.
|
|
const ADMIN_ROOT_ORDER = [
|
|
'Configure', 'Onboard users', 'Manage', 'Monitor and troubleshoot', 'Comply', 'Upgrade',
|
|
'Migrate',
|
|
];
|
|
|
|
// AI Agents above Project Management: Boards is in maintenance mode (see
|
|
// administration-guide/configure/install-boards), Agents is not.
|
|
const ENDUSER_ROOT_ORDER = [
|
|
'Access your workspace', 'Collaborate', 'Workflow Automation', 'AI Agents',
|
|
'Project Management', 'Preferences',
|
|
];
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// End User Guide — Access / Workflow Automation / Project Management /
|
|
// Preferences ordering overrides.
|
|
// ---------------------------------------------------------------------------
|
|
//
|
|
// None of the pages in these four sections set `sidebar_position`, so without
|
|
// an override they'd fall through to alphabetical order.
|
|
|
|
const ENDUSER_SECTION_OVERRIDES = {
|
|
access: {
|
|
// access/ has no landing file for the auto-generated label to come from.
|
|
label: 'Access your workspace',
|
|
landing: 'access-your-workspace',
|
|
order: [
|
|
'install-desktop-app',
|
|
'install-ios-app',
|
|
'install-android-app',
|
|
'client-availability',
|
|
'log-out',
|
|
],
|
|
},
|
|
'workflow-automation': {
|
|
order: [
|
|
'learn-about-playbooks',
|
|
'work-with-playbooks',
|
|
'work-with-runs',
|
|
'work-with-tasks',
|
|
'notifications-and-updates',
|
|
'metrics-and-goals',
|
|
'share-and-collaborate',
|
|
'interact-with-playbooks',
|
|
],
|
|
},
|
|
'project-management': {
|
|
order: [
|
|
'navigate-boards',
|
|
'work-with-boards',
|
|
'work-with-cards',
|
|
'work-with-views',
|
|
'groups-filter-sort',
|
|
'calculations',
|
|
'share-and-collaborate',
|
|
'migrate-to-boards',
|
|
'boards-settings',
|
|
],
|
|
},
|
|
preferences: {
|
|
order: [
|
|
'manage-your-profile',
|
|
'set-your-status-availability',
|
|
'manage-your-security-preferences',
|
|
{
|
|
label: 'Notifications',
|
|
landing: 'manage-your-notifications',
|
|
items: [
|
|
'manage-your-mentions-keywords-notifications',
|
|
'manage-your-thread-reply-notifications',
|
|
'manage-your-channel-specific-notifications',
|
|
'manage-your-desktop-notifications',
|
|
'manage-your-mobile-notifications',
|
|
'manage-your-web-notifications',
|
|
'troubleshoot-notifications',
|
|
],
|
|
},
|
|
'customize-your-theme',
|
|
'manage-your-display-options',
|
|
'customize-your-channel-sidebar',
|
|
'manage-your-sidebar-options',
|
|
'manage-advanced-options',
|
|
'manage-your-plugin-preferences',
|
|
'customize-desktop-app-experience',
|
|
'connect-multiple-workspaces',
|
|
],
|
|
},
|
|
};
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Integrations Guide — manual grouping override.
|
|
// ---------------------------------------------------------------------------
|
|
//
|
|
// Pre-built integrations open the guide: a reader arriving here should first
|
|
// see what already exists, then how the delivery mechanism works (Plugins),
|
|
// and only then how to build their own (webhooks, slash commands, the API).
|
|
//
|
|
// Vendor pages nest under their catalogue and mirror the tables on
|
|
// popular-integrations, split Microsoft / third-party. Microsoft stays its own
|
|
// group even though Microsoft is a third party: it reflects the partnership,
|
|
// and a reader looking for Teams looks under Microsoft before looking under a
|
|
// capability heading. Alphabetical within each group so a reader scanning for
|
|
// a vendor name can predict where to look.
|
|
|
|
const INTEGRATIONS_GROUPS = {
|
|
prebuilt: {
|
|
label: 'Pre-built integrations',
|
|
landing: 'popular-integrations',
|
|
items: [
|
|
{label: 'Microsoft integrations', items: [
|
|
'mattermost-mission-collaboration-for-m365',
|
|
'microsoft-calendar',
|
|
'microsoft-teams-meetings',
|
|
'microsoft-teams-sync',
|
|
]},
|
|
{label: 'Third-party integrations', items: [
|
|
'github',
|
|
'gitlab',
|
|
'jira',
|
|
'servicenow',
|
|
'zoom',
|
|
]},
|
|
],
|
|
},
|
|
webhooks: {
|
|
label: 'Webhooks',
|
|
landing: 'webhook-integrations',
|
|
items: [
|
|
'incoming-webhooks',
|
|
'outgoing-webhooks',
|
|
],
|
|
},
|
|
slashCommands: {
|
|
label: 'Slash Commands',
|
|
landing: 'slash-commands',
|
|
items: [
|
|
'run-slash-commands',
|
|
'built-in-slash-commands',
|
|
],
|
|
},
|
|
};
|
|
|
|
const INTEGRATIONS_ROOT_ORDER = [
|
|
{group: 'prebuilt'},
|
|
'plugins',
|
|
{group: 'webhooks'},
|
|
{group: 'slashCommands'},
|
|
'restful-api',
|
|
'no-code-automation',
|
|
'faq',
|
|
];
|
|
|
|
const INTEGRATIONS_HIDDEN = new Set([
|
|
'popular-integrations',
|
|
'mattermost-mission-collaboration-for-m365', 'microsoft-calendar',
|
|
'microsoft-teams-meetings', 'microsoft-teams-sync',
|
|
'github', 'gitlab', 'jira', 'servicenow', 'zoom',
|
|
'webhook-integrations', 'incoming-webhooks', 'outgoing-webhooks',
|
|
'slash-commands', 'run-slash-commands', 'built-in-slash-commands',
|
|
]);
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Security Guide — manual grouping override.
|
|
// ---------------------------------------------------------------------------
|
|
//
|
|
// Ordered by reader intent: harden the deployment, then the architectural
|
|
// posture model, then the platform-specific surface, then "prove it to an
|
|
// auditor" last. Flat filename order put the regulatory pages ahead of the
|
|
// hardening guidance most readers arrive for.
|
|
|
|
const SECURITY_GROUPS = {
|
|
frameworks: {
|
|
// Grouped so the section reads as "secure it" then "certify it" rather
|
|
// than interleaving the two.
|
|
label: 'Compliance Frameworks',
|
|
items: [
|
|
'cmmc-compliance',
|
|
'finra-compliance',
|
|
'hipaa-compliance',
|
|
],
|
|
},
|
|
};
|
|
|
|
const SECURITY_ROOT_ORDER = [
|
|
'security-updates',
|
|
'secure-mattermost',
|
|
'zero-trust',
|
|
'mobile-security',
|
|
{group: 'frameworks'},
|
|
];
|
|
|
|
const SECURITY_HIDDEN = new Set([
|
|
'cmmc-compliance', 'finra-compliance', 'hipaa-compliance',
|
|
]);
|
|
|
|
// ===========================================================================
|
|
// FUNCTIONS — generic helpers, per-section builders, main.
|
|
// ===========================================================================
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Generic helpers, shared by the auto-generator and every manual override.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
function humanize(name) {
|
|
return name.replace(/[-_]/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
|
|
}
|
|
|
|
function readFm(filePath, key) {
|
|
try {
|
|
const text = readFileSync(filePath, 'utf8').slice(0, 4000);
|
|
const m = text.match(/^---\n([\s\S]*?)\n---/);
|
|
if (!m) return null;
|
|
const r = new RegExp(`^${key}:\\s*"?([^"\\n]+)"?`, 'm');
|
|
const x = m[1].match(r);
|
|
return x ? x[1].trim().replace(/^"|"$/g, '') : null;
|
|
} catch { return null; }
|
|
}
|
|
|
|
// Docusaurus already drops `draft` and `unlisted` pages from the production
|
|
// sidebar, so the generator has to agree or dev and prod disagree. MDX snippet
|
|
// includes use `unlisted: true` to keep their URL while leaving the sidebar.
|
|
function isHidden(filePath) {
|
|
return readFm(filePath, 'draft') === 'true' || readFm(filePath, 'unlisted') === 'true';
|
|
}
|
|
|
|
function pathToDocId(relPath) { return relPath.replace(/\.(md|mdx)$/, ''); }
|
|
|
|
// Landing pages are named either `index.md(x)` or `<something>-index.md(x)`;
|
|
// the latter avoids filename collisions when files are flattened for URL
|
|
// stability, and doesn't always match its directory name. Both forms have to
|
|
// resolve, or category headers, sorting, and labels disagree.
|
|
function findIndexFile(absDir) {
|
|
let entries;
|
|
try { entries = readdirSync(absDir); } catch { return null; }
|
|
return entries.find((e) => /^index\.(md|mdx)$/.test(e)) ||
|
|
entries.find((e) => /-index\.(md|mdx)$/.test(e)) ||
|
|
null;
|
|
}
|
|
|
|
function buildCategory(absDir, docsRelDir) {
|
|
const entries = readdirSync(absDir);
|
|
const indexFile = findIndexFile(absDir);
|
|
let categoryLink = null;
|
|
if (indexFile && !isHidden(join(absDir, indexFile))) {
|
|
categoryLink = {type: 'doc', id: pathToDocId(join(docsRelDir, indexFile))};
|
|
}
|
|
|
|
const subDirs = [];
|
|
const leafDocs = [];
|
|
for (const name of entries) {
|
|
if (name === indexFile) continue;
|
|
const abs = join(absDir, name);
|
|
const st = statSync(abs);
|
|
if (st.isDirectory()) subDirs.push(name);
|
|
else if (st.isFile() && /\.(md|mdx)$/.test(name) && !isHidden(abs)) leafDocs.push(name);
|
|
}
|
|
|
|
function key(name, abs) {
|
|
const p = readFm(abs, 'sidebar_position');
|
|
return [p ? Number(p) : 9999, name.toLowerCase()];
|
|
}
|
|
leafDocs.sort((a, b) => {
|
|
const ka = key(a, join(absDir, a));
|
|
const kb = key(b, join(absDir, b));
|
|
return ka[0] - kb[0] || ka[1].localeCompare(kb[1]);
|
|
});
|
|
function subDirKey(name) {
|
|
const subAbs = join(absDir, name);
|
|
const subIndex = findIndexFile(subAbs);
|
|
return key(name, subIndex ? join(subAbs, subIndex) : join(subAbs, 'index.mdx'));
|
|
}
|
|
subDirs.sort((a, b) => {
|
|
const ka = subDirKey(a);
|
|
const kb = subDirKey(b);
|
|
return ka[0] - kb[0] || ka[1].localeCompare(kb[1]);
|
|
});
|
|
|
|
const items = [];
|
|
for (const name of leafDocs) {
|
|
const id = pathToDocId(join(docsRelDir, name));
|
|
const filePath = join(absDir, name);
|
|
const label = readFm(filePath, 'sidebar_label') ||
|
|
readFm(filePath, 'title') ||
|
|
humanize(basename(name, /\.(md|mdx)$/.exec(name)[0]));
|
|
items.push({type: 'doc', id, label});
|
|
}
|
|
for (const name of subDirs) {
|
|
const sub = buildCategory(join(absDir, name), join(docsRelDir, name));
|
|
if (sub) items.push(sub);
|
|
}
|
|
|
|
const label =
|
|
(indexFile && (readFm(join(absDir, indexFile), 'sidebar_label') || readFm(join(absDir, indexFile), 'title'))) ||
|
|
humanize(basename(absDir));
|
|
|
|
if (!categoryLink && items.length === 0) return null;
|
|
|
|
return {type: 'category', label, collapsed: true, ...(categoryLink ? {link: categoryLink} : {}), items};
|
|
}
|
|
|
|
// The content sub-directory a category was built from (e.g. 'comply' for
|
|
// administration-guide/comply/), read off its landing page or first doc. Lets
|
|
// the section builders below find a sub-category to regroup without depending
|
|
// on its display label.
|
|
function categoryDirName(cat) {
|
|
if (cat.link && cat.link.id) return cat.link.id.split('/')[1];
|
|
const items = cat.items || [];
|
|
const firstDoc = items.find((c) => c.type === 'doc' && c.id);
|
|
if (firstDoc) return firstDoc.id.split('/')[1];
|
|
// Regrouped categories (e.g. Onboard) hold only sub-groups at the top level.
|
|
for (const it of items) {
|
|
if (it.type !== 'category') continue;
|
|
const nested = categoryDirName(it);
|
|
if (nested) return nested;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
// Reorder a guide's top-level categories by label. Anything not named in
|
|
// `order` keeps its relative position at the end.
|
|
function orderRootCategories(sectionCat, order, sectionLabel) {
|
|
const rank = new Map(order.map((label, i) => [label, i]));
|
|
const listed = [];
|
|
const rest = [];
|
|
for (const it of sectionCat.items) {
|
|
if (it.type === 'category' && rank.has(it.label)) listed.push(it);
|
|
else rest.push(it);
|
|
}
|
|
listed.sort((a, b) => rank.get(a.label) - rank.get(b.label));
|
|
|
|
const missing = order.filter((label) => !listed.some((it) => it.label === label));
|
|
if (missing.length > 0) {
|
|
console.warn(`[sidebar] WARN: ${sectionLabel} root order names categor(y/ies) that don't exist: ${missing.join(', ')}`);
|
|
}
|
|
|
|
sectionCat.items = [...listed, ...rest];
|
|
return sectionCat;
|
|
}
|
|
|
|
// Pull every doc label from an auto-generated category so a manual ordering
|
|
// keeps the frontmatter-derived titles.
|
|
function collectLeafLabels(cat, acc = {}) {
|
|
if (!cat || !cat.items) return acc;
|
|
for (const it of cat.items) {
|
|
if (it.type === 'doc' && it.id && it.label) acc[it.id] = it.label;
|
|
else if (it.type === 'category') collectLeafLabels(it, acc);
|
|
}
|
|
return acc;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Overview — builder.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
function buildOverviewItem(spec, leafLabels) {
|
|
if (typeof spec === 'string') {
|
|
const id = `product-overview/${spec}`;
|
|
return {type: 'doc', id, label: leafLabels[id] || humanize(spec)};
|
|
}
|
|
if (spec.group) {
|
|
const g = OVERVIEW_GROUPS[spec.group];
|
|
if (!g) throw new Error(`unknown overview group: ${spec.group}`);
|
|
return buildOverviewGroup(g, leafLabels);
|
|
}
|
|
return buildOverviewGroup(spec, leafLabels);
|
|
}
|
|
|
|
function buildOverviewGroup(g, leafLabels) {
|
|
const items = g.items.map((it) => buildOverviewItem(it, leafLabels));
|
|
const cat = {type: 'category', label: g.label, collapsed: true, items};
|
|
if (g.landing) {
|
|
cat.link = {type: 'doc', id: `product-overview/${g.landing}`};
|
|
}
|
|
return cat;
|
|
}
|
|
|
|
function buildOverviewSidebar(autoCat) {
|
|
const leafLabels = collectLeafLabels(autoCat);
|
|
const items = OVERVIEW_ROOT_ORDER.map((spec) => buildOverviewItem(spec, leafLabels));
|
|
|
|
const known = new Set();
|
|
(function walk(n) {
|
|
if (Array.isArray(n)) n.forEach(walk);
|
|
else if (n && typeof n === 'object') {
|
|
if (n.type === 'doc' && n.id) known.add(n.id);
|
|
if (n.link && n.link.id) known.add(n.link.id);
|
|
if (n.items) walk(n.items);
|
|
}
|
|
})(items);
|
|
const orphans = [];
|
|
for (const id of Object.keys(leafLabels)) {
|
|
if (!known.has(id) && id !== 'product-overview/product-overview-index') orphans.push(id);
|
|
}
|
|
if (orphans.length > 0) {
|
|
console.warn(`[sidebar] WARN: ${orphans.length} Overview file(s) missing from OVERVIEW_ROOT_ORDER — falling through to root:`);
|
|
for (const id of orphans) console.warn(` - ${id}`);
|
|
for (const id of orphans) items.push({type: 'doc', id, label: leafLabels[id]});
|
|
}
|
|
|
|
return {
|
|
type: 'category',
|
|
label: 'Overview',
|
|
collapsed: true,
|
|
// Merged with the site landing: clicking "Overview" opens main/index.mdx
|
|
// (slug: /), the unified welcome page.
|
|
link: {type: 'doc', id: 'index'},
|
|
items,
|
|
};
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Deployment Guide — builder.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
function buildDeploymentItem(spec, leafLabels, autoCats) {
|
|
if (typeof spec === 'string') {
|
|
const id = `deployment-guide/${spec}`;
|
|
return {type: 'doc', id, label: leafLabels[id] || humanize(spec.split('/').pop())};
|
|
}
|
|
if (spec.doc) {
|
|
const id = `deployment-guide/${spec.doc}`;
|
|
return {type: 'doc', id, label: spec.label || leafLabels[id] || humanize(spec.doc.split('/').pop())};
|
|
}
|
|
if (spec.auto) {
|
|
const cat = autoCats.get(spec.auto);
|
|
if (!cat) throw new Error(`auto-category not found: deployment-guide/${spec.auto}`);
|
|
return cat;
|
|
}
|
|
if (spec.group) {
|
|
const g = DEPLOYMENT_GROUPS[spec.group];
|
|
if (!g) throw new Error(`unknown deployment group: ${spec.group}`);
|
|
return buildDeploymentGroup(g, leafLabels, autoCats);
|
|
}
|
|
return buildDeploymentGroup(spec, leafLabels, autoCats);
|
|
}
|
|
|
|
function buildDeploymentGroup(g, leafLabels, autoCats) {
|
|
const items = g.items.map((it) => buildDeploymentItem(it, leafLabels, autoCats));
|
|
const cat = {type: 'category', label: g.label, collapsed: true, items};
|
|
if (g.landing) {
|
|
cat.link = {type: 'doc', id: `deployment-guide/${g.landing}`};
|
|
}
|
|
return cat;
|
|
}
|
|
|
|
function buildDeploymentSidebar(autoCat) {
|
|
// Index the auto-generated sub-categories by directory name so the manual
|
|
// ordering can hand them off intact. Falls back to the first doc child for
|
|
// directories with no index file (desktop/, mobile/).
|
|
const autoCats = new Map();
|
|
function dirNameFromId(id) {
|
|
const parts = id.split('/');
|
|
return parts.length >= 2 ? parts[1] : null;
|
|
}
|
|
for (const it of autoCat.items) {
|
|
if (it.type !== 'category') continue;
|
|
let dirName = null;
|
|
if (it.link && it.link.id) {
|
|
dirName = dirNameFromId(it.link.id);
|
|
}
|
|
if (!dirName && it.items) {
|
|
const firstDoc = it.items.find((c) => c.type === 'doc' && c.id);
|
|
if (firstDoc) dirName = dirNameFromId(firstDoc.id);
|
|
}
|
|
if (dirName) autoCats.set(dirName, it);
|
|
}
|
|
|
|
const leafLabels = collectLeafLabels(autoCat);
|
|
const items = DEPLOYMENT_ROOT_ORDER.map((spec) => buildDeploymentItem(spec, leafLabels, autoCats));
|
|
|
|
const hiddenIds = new Set();
|
|
for (const h of DEPLOYMENT_HIDDEN) hiddenIds.add(`deployment-guide/${h}`);
|
|
const known = new Set();
|
|
(function walk(n) {
|
|
if (Array.isArray(n)) n.forEach(walk);
|
|
else if (n && typeof n === 'object') {
|
|
if (n.type === 'doc' && n.id) known.add(n.id);
|
|
if (n.link && n.link.id) known.add(n.link.id);
|
|
if (n.items) walk(n.items);
|
|
}
|
|
})(items);
|
|
const orphans = [];
|
|
for (const id of Object.keys(leafLabels)) {
|
|
if (!known.has(id) && !hiddenIds.has(id) && id !== 'deployment-guide/deployment-guide-index') {
|
|
orphans.push(id);
|
|
}
|
|
}
|
|
if (orphans.length > 0) {
|
|
console.warn(`[sidebar] WARN: ${orphans.length} Deployment Guide file(s) missing from DEPLOYMENT_ROOT_ORDER — falling through to root:`);
|
|
for (const id of orphans) console.warn(` - ${id}`);
|
|
for (const id of orphans) items.push({type: 'doc', id, label: leafLabels[id]});
|
|
}
|
|
|
|
return {
|
|
type: 'category',
|
|
label: 'Deployment Guide',
|
|
collapsed: true,
|
|
link: {type: 'doc', id: 'deployment-guide/deployment-guide-index'},
|
|
items,
|
|
};
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Administration Guide — builder (regroups the "Configure" sub-category).
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// Label for a doc that lives outside the section being built, so it won't be
|
|
// in that section's `leafLabels` map. Read from its frontmatter directly.
|
|
function docLabelById(id) {
|
|
for (const ext of ['.mdx', '.md']) {
|
|
const abs = join(SRC, `${id}${ext}`);
|
|
if (existsSync(abs)) {
|
|
return readFm(abs, 'sidebar_label') || readFm(abs, 'title') || humanize(id.split('/').pop());
|
|
}
|
|
}
|
|
throw new Error(`doc id not found on disk: ${id}`);
|
|
}
|
|
|
|
function buildAdminConfigureItem(spec, leafLabels) {
|
|
if (typeof spec === 'string') {
|
|
const id = `administration-guide/configure/${spec}`;
|
|
return {type: 'doc', id, label: leafLabels[id] || humanize(spec)};
|
|
}
|
|
if (spec.doc) {
|
|
return {type: 'doc', id: spec.doc, label: spec.label || docLabelById(spec.doc)};
|
|
}
|
|
const g = ADMIN_CONFIGURE_GROUPS[spec.group];
|
|
if (!g) throw new Error(`unknown admin configure group: ${spec.group}`);
|
|
const items = g.items.map((it) => buildAdminConfigureItem(it, leafLabels));
|
|
const cat = {type: 'category', label: g.label, collapsed: true, items};
|
|
if (g.landingDoc) cat.link = {type: 'doc', id: g.landingDoc};
|
|
else if (g.landing) cat.link = {type: 'doc', id: `administration-guide/configure/${g.landing}`};
|
|
return cat;
|
|
}
|
|
|
|
// Every regroup* function below replaces a sub-category's items in place, so
|
|
// the sub-category keeps its position until orderRootCategories runs.
|
|
function regroupAdminConfigure(configureCat) {
|
|
const leafLabels = collectLeafLabels(configureCat);
|
|
const items = ADMIN_CONFIGURE_ORDER.map((spec) => buildAdminConfigureItem(spec, leafLabels));
|
|
|
|
const known = new Set();
|
|
(function walk(n) {
|
|
if (Array.isArray(n)) n.forEach(walk);
|
|
else if (n && typeof n === 'object') {
|
|
if (n.type === 'doc' && n.id) known.add(n.id);
|
|
if (n.link && n.link.id) known.add(n.link.id);
|
|
if (n.items) walk(n.items);
|
|
}
|
|
})(items);
|
|
const hiddenIds = new Set();
|
|
for (const h of ADMIN_CONFIGURE_HIDDEN) hiddenIds.add(`administration-guide/configure/${h}`);
|
|
const orphans = [];
|
|
for (const id of Object.keys(leafLabels)) {
|
|
if (!known.has(id) && !hiddenIds.has(id) && id !== 'administration-guide/configure/configuration-settings') {
|
|
orphans.push(id);
|
|
}
|
|
}
|
|
if (orphans.length > 0) {
|
|
console.warn(`[sidebar] WARN: ${orphans.length} Configure file(s) missing from ADMIN_CONFIGURE_ORDER — falling through to root:`);
|
|
for (const id of orphans) console.warn(` - ${id}`);
|
|
for (const id of orphans) items.push({type: 'doc', id, label: leafLabels[id]});
|
|
}
|
|
|
|
configureCat.items = items;
|
|
return configureCat;
|
|
}
|
|
|
|
function buildAdminManageItem(spec, leafLabels) {
|
|
if (typeof spec === 'string') {
|
|
const id = `administration-guide/manage/${spec}`;
|
|
return {type: 'doc', id, label: leafLabels[id] || humanize(spec.split('/').pop())};
|
|
}
|
|
if (spec.doc) {
|
|
return {type: 'doc', id: spec.doc, label: spec.label || docLabelById(spec.doc)};
|
|
}
|
|
if (spec.group) {
|
|
const g = ADMIN_MANAGE_GROUPS[spec.group];
|
|
if (!g) throw new Error(`unknown admin manage group: ${spec.group}`);
|
|
return buildAdminManageGroup(g, leafLabels);
|
|
}
|
|
return buildAdminManageGroup(spec, leafLabels);
|
|
}
|
|
|
|
function buildAdminManageGroup(g, leafLabels) {
|
|
const items = g.items.map((it) => buildAdminManageItem(it, leafLabels));
|
|
const cat = {type: 'category', label: g.label, collapsed: true, items};
|
|
if (g.landingDoc) cat.link = {type: 'doc', id: g.landingDoc};
|
|
else if (g.landing) cat.link = {type: 'doc', id: `administration-guide/manage/${g.landing}`};
|
|
return cat;
|
|
}
|
|
|
|
// Also flattens the manage/admin/ filesystem nesting into the task-based
|
|
// groups above.
|
|
function regroupAdminManage(manageCat) {
|
|
const leafLabels = collectLeafLabels(manageCat);
|
|
const items = ADMIN_MANAGE_ORDER.map((spec) => buildAdminManageItem(spec, leafLabels));
|
|
// manage/ has no index file; server-maintenance is the hub Sphinx uses.
|
|
manageCat.link = {type: 'doc', id: 'administration-guide/manage/admin/server-maintenance'};
|
|
|
|
const known = new Set();
|
|
(function walk(n) {
|
|
if (Array.isArray(n)) n.forEach(walk);
|
|
else if (n && typeof n === 'object') {
|
|
if (n.type === 'doc' && n.id) known.add(n.id);
|
|
if (n.link && n.link.id) known.add(n.link.id);
|
|
if (n.items) walk(n.items);
|
|
}
|
|
})(items);
|
|
const hiddenIds = new Set();
|
|
for (const h of ADMIN_MANAGE_HIDDEN) hiddenIds.add(`administration-guide/manage/${h}`);
|
|
const orphans = [];
|
|
for (const id of Object.keys(leafLabels)) {
|
|
if (!known.has(id) && !hiddenIds.has(id)) orphans.push(id);
|
|
}
|
|
if (orphans.length > 0) {
|
|
console.warn(`[sidebar] WARN: ${orphans.length} Manage file(s) missing from ADMIN_MANAGE_ORDER — falling through to root:`);
|
|
for (const id of orphans) console.warn(` - ${id}`);
|
|
for (const id of orphans) items.push({type: 'doc', id, label: leafLabels[id]});
|
|
}
|
|
|
|
manageCat.items = items;
|
|
return manageCat;
|
|
}
|
|
|
|
function buildAdminOnboardItem(spec, leafLabels) {
|
|
if (typeof spec === 'string') {
|
|
const id = `administration-guide/onboard/${spec}`;
|
|
return {type: 'doc', id, label: leafLabels[id] || humanize(spec.split('/').pop())};
|
|
}
|
|
if (spec.doc) {
|
|
return {type: 'doc', id: spec.doc, label: spec.label || docLabelById(spec.doc)};
|
|
}
|
|
if (spec.group) {
|
|
const g = ADMIN_ONBOARD_GROUPS[spec.group];
|
|
if (!g) throw new Error(`unknown admin onboard group: ${spec.group}`);
|
|
return buildAdminOnboardGroup(g, leafLabels);
|
|
}
|
|
return buildAdminOnboardGroup(spec, leafLabels);
|
|
}
|
|
|
|
function buildAdminOnboardGroup(g, leafLabels) {
|
|
const items = g.items.map((it) => buildAdminOnboardItem(it, leafLabels));
|
|
const cat = {type: 'category', label: g.label, collapsed: true, items};
|
|
if (g.landingDoc) cat.link = {type: 'doc', id: g.landingDoc};
|
|
else if (g.landing) cat.link = {type: 'doc', id: `administration-guide/onboard/${g.landing}`};
|
|
return cat;
|
|
}
|
|
|
|
function regroupAdminOnboard(onboardCat) {
|
|
const leafLabels = collectLeafLabels(onboardCat);
|
|
const items = ADMIN_ONBOARD_ORDER.map((spec) => buildAdminOnboardItem(spec, leafLabels));
|
|
|
|
const known = new Set();
|
|
(function walk(n) {
|
|
if (Array.isArray(n)) n.forEach(walk);
|
|
else if (n && typeof n === 'object') {
|
|
if (n.type === 'doc' && n.id) known.add(n.id);
|
|
if (n.link && n.link.id) known.add(n.link.id);
|
|
if (n.items) walk(n.items);
|
|
}
|
|
})(items);
|
|
const hiddenIds = new Set();
|
|
for (const h of ADMIN_ONBOARD_HIDDEN) hiddenIds.add(`administration-guide/onboard/${h}`);
|
|
const orphans = [];
|
|
for (const id of Object.keys(leafLabels)) {
|
|
if (!known.has(id) && !hiddenIds.has(id)) orphans.push(id);
|
|
}
|
|
if (orphans.length > 0) {
|
|
console.warn(`[sidebar] WARN: ${orphans.length} Onboard file(s) missing from ADMIN_ONBOARD_ORDER — falling through to root:`);
|
|
for (const id of orphans) console.warn(` - ${id}`);
|
|
for (const id of orphans) items.push({type: 'doc', id, label: leafLabels[id]});
|
|
}
|
|
|
|
onboardCat.items = items;
|
|
return onboardCat;
|
|
}
|
|
|
|
function buildAdminScaleItem(spec, leafLabels) {
|
|
if (typeof spec === 'string') {
|
|
const id = `administration-guide/scale/${spec}`;
|
|
return {type: 'doc', id, label: leafLabels[id] || humanize(spec.split('/').pop())};
|
|
}
|
|
if (spec.doc) {
|
|
return {type: 'doc', id: spec.doc, label: spec.label || docLabelById(spec.doc)};
|
|
}
|
|
if (spec.group) {
|
|
const g = ADMIN_SCALE_GROUPS[spec.group];
|
|
if (!g) throw new Error(`unknown admin scale group: ${spec.group}`);
|
|
return buildAdminScaleGroup(g, leafLabels);
|
|
}
|
|
return buildAdminScaleGroup(spec, leafLabels);
|
|
}
|
|
|
|
function buildAdminScaleGroup(g, leafLabels) {
|
|
const items = g.items.map((it) => buildAdminScaleItem(it, leafLabels));
|
|
const cat = {type: 'category', label: g.label, collapsed: true, items};
|
|
if (g.landingDoc) cat.link = {type: 'doc', id: g.landingDoc};
|
|
else if (g.landing) cat.link = {type: 'doc', id: `administration-guide/scale/${g.landing}`};
|
|
return cat;
|
|
}
|
|
|
|
// Re-labelled because the `scale/` directory name no longer describes what's
|
|
// listed here.
|
|
function regroupAdminScale(scaleCat) {
|
|
const leafLabels = collectLeafLabels(scaleCat);
|
|
const items = ADMIN_SCALE_ORDER.map((spec) => buildAdminScaleItem(spec, leafLabels));
|
|
scaleCat.label = 'Monitor and troubleshoot';
|
|
scaleCat.link = {type: 'doc', id: 'administration-guide/manage/admin/monitoring-and-performance'};
|
|
|
|
const known = new Set();
|
|
(function walk(n) {
|
|
if (Array.isArray(n)) n.forEach(walk);
|
|
else if (n && typeof n === 'object') {
|
|
if (n.type === 'doc' && n.id) known.add(n.id);
|
|
if (n.link && n.link.id) known.add(n.link.id);
|
|
if (n.items) walk(n.items);
|
|
}
|
|
})(items);
|
|
const hiddenIds = new Set();
|
|
for (const h of ADMIN_SCALE_HIDDEN) hiddenIds.add(`administration-guide/scale/${h}`);
|
|
const orphans = [];
|
|
for (const id of Object.keys(leafLabels)) {
|
|
if (!known.has(id) && !hiddenIds.has(id)) orphans.push(id);
|
|
}
|
|
if (orphans.length > 0) {
|
|
console.warn(`[sidebar] WARN: ${orphans.length} Scale file(s) missing from ADMIN_SCALE_ORDER — falling through to root:`);
|
|
for (const id of orphans) console.warn(` - ${id}`);
|
|
for (const id of orphans) items.push({type: 'doc', id, label: leafLabels[id]});
|
|
}
|
|
|
|
scaleCat.items = items;
|
|
return scaleCat;
|
|
}
|
|
|
|
function buildAdminComplyItem(spec, leafLabels) {
|
|
if (typeof spec === 'string') {
|
|
const id = `administration-guide/comply/${spec}`;
|
|
return {type: 'doc', id, label: leafLabels[id] || humanize(spec.split('/').pop())};
|
|
}
|
|
if (spec.doc) {
|
|
return {type: 'doc', id: spec.doc, label: spec.label || docLabelById(spec.doc)};
|
|
}
|
|
if (spec.group) {
|
|
const g = ADMIN_COMPLY_GROUPS[spec.group];
|
|
if (!g) throw new Error(`unknown admin comply group: ${spec.group}`);
|
|
return buildAdminComplyGroup(g, leafLabels);
|
|
}
|
|
return buildAdminComplyGroup(spec, leafLabels);
|
|
}
|
|
|
|
function buildAdminComplyGroup(g, leafLabels) {
|
|
const items = g.items.map((it) => buildAdminComplyItem(it, leafLabels));
|
|
const cat = {type: 'category', label: g.label, collapsed: true, items};
|
|
if (g.landingDoc) cat.link = {type: 'doc', id: g.landingDoc};
|
|
else if (g.landing) cat.link = {type: 'doc', id: `administration-guide/comply/${g.landing}`};
|
|
return cat;
|
|
}
|
|
|
|
function regroupAdminComply(complyCat) {
|
|
const leafLabels = collectLeafLabels(complyCat);
|
|
const items = ADMIN_COMPLY_ORDER.map((spec) => buildAdminComplyItem(spec, leafLabels));
|
|
|
|
const known = new Set();
|
|
(function walk(n) {
|
|
if (Array.isArray(n)) n.forEach(walk);
|
|
else if (n && typeof n === 'object') {
|
|
if (n.type === 'doc' && n.id) known.add(n.id);
|
|
if (n.link && n.link.id) known.add(n.link.id);
|
|
if (n.items) walk(n.items);
|
|
}
|
|
})(items);
|
|
const hiddenIds = new Set();
|
|
for (const h of ADMIN_COMPLY_HIDDEN) hiddenIds.add(`administration-guide/comply/${h}`);
|
|
const orphans = [];
|
|
for (const id of Object.keys(leafLabels)) {
|
|
if (!known.has(id) && !hiddenIds.has(id)) orphans.push(id);
|
|
}
|
|
if (orphans.length > 0) {
|
|
console.warn(`[sidebar] WARN: ${orphans.length} Comply file(s) missing from ADMIN_COMPLY_ORDER — falling through to root:`);
|
|
for (const id of orphans) console.warn(` - ${id}`);
|
|
for (const id of orphans) items.push({type: 'doc', id, label: leafLabels[id]});
|
|
}
|
|
|
|
complyCat.items = items;
|
|
return complyCat;
|
|
}
|
|
|
|
function buildAdminUpgradeItem(spec, leafLabels) {
|
|
if (typeof spec === 'string') {
|
|
const id = `administration-guide/upgrade/${spec}`;
|
|
return {type: 'doc', id, label: leafLabels[id] || humanize(spec.split('/').pop())};
|
|
}
|
|
if (spec.doc) {
|
|
return {type: 'doc', id: spec.doc, label: spec.label || docLabelById(spec.doc)};
|
|
}
|
|
if (spec.group) {
|
|
const g = ADMIN_UPGRADE_GROUPS[spec.group];
|
|
if (!g) throw new Error(`unknown admin upgrade group: ${spec.group}`);
|
|
return buildAdminUpgradeGroup(g, leafLabels);
|
|
}
|
|
return buildAdminUpgradeGroup(spec, leafLabels);
|
|
}
|
|
|
|
function buildAdminUpgradeGroup(g, leafLabels) {
|
|
const items = g.items.map((it) => buildAdminUpgradeItem(it, leafLabels));
|
|
const cat = {type: 'category', label: g.label, collapsed: true, items};
|
|
if (g.landingDoc) cat.link = {type: 'doc', id: g.landingDoc};
|
|
else if (g.landing) cat.link = {type: 'doc', id: `administration-guide/upgrade/${g.landing}`};
|
|
return cat;
|
|
}
|
|
|
|
function regroupAdminUpgrade(upgradeCat) {
|
|
const leafLabels = collectLeafLabels(upgradeCat);
|
|
const items = ADMIN_UPGRADE_ORDER.map((spec) => buildAdminUpgradeItem(spec, leafLabels));
|
|
|
|
const known = new Set();
|
|
(function walk(n) {
|
|
if (Array.isArray(n)) n.forEach(walk);
|
|
else if (n && typeof n === 'object') {
|
|
if (n.type === 'doc' && n.id) known.add(n.id);
|
|
if (n.link && n.link.id) known.add(n.link.id);
|
|
if (n.items) walk(n.items);
|
|
}
|
|
})(items);
|
|
const hiddenIds = new Set();
|
|
for (const h of ADMIN_UPGRADE_HIDDEN) hiddenIds.add(`administration-guide/upgrade/${h}`);
|
|
const orphans = [];
|
|
for (const id of Object.keys(leafLabels)) {
|
|
if (!known.has(id) && !hiddenIds.has(id)) orphans.push(id);
|
|
}
|
|
if (orphans.length > 0) {
|
|
console.warn(`[sidebar] WARN: ${orphans.length} Upgrade file(s) missing from ADMIN_UPGRADE_ORDER — falling through to root:`);
|
|
for (const id of orphans) console.warn(` - ${id}`);
|
|
for (const id of orphans) items.push({type: 'doc', id, label: leafLabels[id]});
|
|
}
|
|
|
|
upgradeCat.items = items;
|
|
return upgradeCat;
|
|
}
|
|
|
|
// Migration takes fully-qualified doc ids rather than basenames, so it needs
|
|
// no section prefix and reads labels straight off each file.
|
|
function buildAdminMigrationItem(spec) {
|
|
if (typeof spec === 'string') {
|
|
return {type: 'doc', id: spec, label: docLabelById(spec)};
|
|
}
|
|
if (spec.doc) {
|
|
return {type: 'doc', id: spec.doc, label: spec.label || docLabelById(spec.doc)};
|
|
}
|
|
return buildAdminMigrationGroup(spec);
|
|
}
|
|
|
|
function buildAdminMigrationGroup(g) {
|
|
const items = g.items.map(buildAdminMigrationItem);
|
|
const cat = {type: 'category', label: g.label, collapsed: true, items};
|
|
if (g.landing) cat.link = {type: 'doc', id: g.landing};
|
|
return cat;
|
|
}
|
|
|
|
function buildAdminGuideSidebar(autoCat) {
|
|
const regroupers = {
|
|
configure: regroupAdminConfigure,
|
|
manage: regroupAdminManage,
|
|
onboard: regroupAdminOnboard,
|
|
scale: regroupAdminScale,
|
|
comply: regroupAdminComply,
|
|
upgrade: regroupAdminUpgrade,
|
|
};
|
|
const found = new Set();
|
|
for (const it of autoCat.items) {
|
|
if (it.type !== 'category') continue;
|
|
const dirName = categoryDirName(it);
|
|
const regroup = regroupers[dirName];
|
|
if (!regroup) continue;
|
|
regroup(it);
|
|
found.add(dirName);
|
|
}
|
|
for (const dirName of Object.keys(regroupers)) {
|
|
if (!found.has(dirName)) {
|
|
console.warn(`[sidebar] WARN: Administration Guide "${dirName}" sub-category not found — its ordering override was not applied.`);
|
|
}
|
|
}
|
|
|
|
autoCat.items.push(buildAdminMigrationGroup(ADMIN_MIGRATION));
|
|
|
|
orderRootCategories(autoCat, ADMIN_ROOT_ORDER, 'Administration Guide');
|
|
return autoCat;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// End User Guide — builder (regroups the "Collaborate" sub-category).
|
|
// ---------------------------------------------------------------------------
|
|
|
|
function buildCollaborateItem(spec, leafLabels) {
|
|
if (typeof spec === 'string') {
|
|
const id = `end-user-guide/collaborate/${spec}`;
|
|
return {type: 'doc', id, label: leafLabels[id] || humanize(spec)};
|
|
}
|
|
if (spec.items) {
|
|
// Inline sub-group, no COLLABORATE_GROUPS lookup.
|
|
return buildCollaborateGroup(spec, leafLabels);
|
|
}
|
|
const g = COLLABORATE_GROUPS[spec.group];
|
|
if (!g) throw new Error(`unknown collaborate group: ${spec.group}`);
|
|
return buildCollaborateGroup(g, leafLabels);
|
|
}
|
|
|
|
function buildCollaborateGroup(g, leafLabels) {
|
|
const items = g.items.map((it) => buildCollaborateItem(it, leafLabels));
|
|
const cat = {type: 'category', label: g.label, collapsed: true, items};
|
|
if (g.landing) cat.link = {type: 'doc', id: `end-user-guide/collaborate/${g.landing}`};
|
|
return cat;
|
|
}
|
|
|
|
function regroupCollaborate(collaborateCat) {
|
|
const leafLabels = collectLeafLabels(collaborateCat);
|
|
const items = COLLABORATE_ORDER.map((spec) => buildCollaborateItem(spec, leafLabels));
|
|
|
|
const known = new Set();
|
|
(function walk(n) {
|
|
if (Array.isArray(n)) n.forEach(walk);
|
|
else if (n && typeof n === 'object') {
|
|
if (n.type === 'doc' && n.id) known.add(n.id);
|
|
if (n.link && n.link.id) known.add(n.link.id);
|
|
if (n.items) walk(n.items);
|
|
}
|
|
})(items);
|
|
const hiddenIds = new Set();
|
|
for (const h of COLLABORATE_HIDDEN) hiddenIds.add(`end-user-guide/collaborate/${h}`);
|
|
const orphans = [];
|
|
for (const id of Object.keys(leafLabels)) {
|
|
if (!known.has(id) && !hiddenIds.has(id)) orphans.push(id);
|
|
}
|
|
if (orphans.length > 0) {
|
|
console.warn(`[sidebar] WARN: ${orphans.length} Collaborate file(s) missing from COLLABORATE_ORDER — falling through to root:`);
|
|
for (const id of orphans) console.warn(` - ${id}`);
|
|
for (const id of orphans) items.push({type: 'doc', id, label: leafLabels[id]});
|
|
}
|
|
|
|
collaborateCat.items = items;
|
|
return collaborateCat;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Integrations Guide — builder.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
function buildIntegrationsItem(spec, leafLabels) {
|
|
if (typeof spec === 'string') {
|
|
const id = `integrations-guide/${spec}`;
|
|
return {type: 'doc', id, label: leafLabels[id] || humanize(spec)};
|
|
}
|
|
if (spec.group) {
|
|
const g = INTEGRATIONS_GROUPS[spec.group];
|
|
if (!g) throw new Error(`unknown integrations group: ${spec.group}`);
|
|
return buildIntegrationsGroup(g, leafLabels);
|
|
}
|
|
return buildIntegrationsGroup(spec, leafLabels);
|
|
}
|
|
|
|
function buildIntegrationsGroup(g, leafLabels) {
|
|
const items = g.items.map((it) => buildIntegrationsItem(it, leafLabels));
|
|
const cat = {type: 'category', label: g.label, collapsed: true, items};
|
|
if (g.landing) cat.link = {type: 'doc', id: `integrations-guide/${g.landing}`};
|
|
return cat;
|
|
}
|
|
|
|
function buildIntegrationsSidebar(autoCat) {
|
|
const leafLabels = collectLeafLabels(autoCat);
|
|
const items = INTEGRATIONS_ROOT_ORDER.map((spec) => buildIntegrationsItem(spec, leafLabels));
|
|
|
|
const known = new Set();
|
|
(function walk(n) {
|
|
if (Array.isArray(n)) n.forEach(walk);
|
|
else if (n && typeof n === 'object') {
|
|
if (n.type === 'doc' && n.id) known.add(n.id);
|
|
if (n.link && n.link.id) known.add(n.link.id);
|
|
if (n.items) walk(n.items);
|
|
}
|
|
})(items);
|
|
const hiddenIds = new Set();
|
|
for (const h of INTEGRATIONS_HIDDEN) hiddenIds.add(`integrations-guide/${h}`);
|
|
const orphans = [];
|
|
for (const id of Object.keys(leafLabels)) {
|
|
if (!known.has(id) && !hiddenIds.has(id) && id !== 'integrations-guide/integrations-guide-index') {
|
|
orphans.push(id);
|
|
}
|
|
}
|
|
if (orphans.length > 0) {
|
|
console.warn(`[sidebar] WARN: ${orphans.length} Integrations Guide file(s) missing from INTEGRATIONS_ROOT_ORDER — falling through to root:`);
|
|
for (const id of orphans) console.warn(` - ${id}`);
|
|
for (const id of orphans) items.push({type: 'doc', id, label: leafLabels[id]});
|
|
}
|
|
|
|
return {
|
|
type: 'category',
|
|
label: 'Integrations Guide',
|
|
collapsed: true,
|
|
link: {type: 'doc', id: 'integrations-guide/integrations-guide-index'},
|
|
items,
|
|
};
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Security Guide — builder.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
function buildSecurityItem(spec, leafLabels) {
|
|
if (typeof spec === 'string') {
|
|
const id = `security-guide/${spec}`;
|
|
return {type: 'doc', id, label: leafLabels[id] || humanize(spec)};
|
|
}
|
|
if (spec.group) {
|
|
const g = SECURITY_GROUPS[spec.group];
|
|
if (!g) throw new Error(`unknown security group: ${spec.group}`);
|
|
return buildSecurityGroup(g, leafLabels);
|
|
}
|
|
return buildSecurityGroup(spec, leafLabels);
|
|
}
|
|
|
|
function buildSecurityGroup(g, leafLabels) {
|
|
const items = g.items.map((it) => buildSecurityItem(it, leafLabels));
|
|
const cat = {type: 'category', label: g.label, collapsed: true, items};
|
|
if (g.landing) cat.link = {type: 'doc', id: `security-guide/${g.landing}`};
|
|
return cat;
|
|
}
|
|
|
|
function buildSecuritySidebar(autoCat) {
|
|
const leafLabels = collectLeafLabels(autoCat);
|
|
const items = SECURITY_ROOT_ORDER.map((spec) => buildSecurityItem(spec, leafLabels));
|
|
|
|
const known = new Set();
|
|
(function walk(n) {
|
|
if (Array.isArray(n)) n.forEach(walk);
|
|
else if (n && typeof n === 'object') {
|
|
if (n.type === 'doc' && n.id) known.add(n.id);
|
|
if (n.link && n.link.id) known.add(n.link.id);
|
|
if (n.items) walk(n.items);
|
|
}
|
|
})(items);
|
|
const hiddenIds = new Set();
|
|
for (const h of SECURITY_HIDDEN) hiddenIds.add(`security-guide/${h}`);
|
|
const orphans = [];
|
|
for (const id of Object.keys(leafLabels)) {
|
|
if (!known.has(id) && !hiddenIds.has(id) && id !== 'security-guide/security-guide-index') {
|
|
orphans.push(id);
|
|
}
|
|
}
|
|
if (orphans.length > 0) {
|
|
console.warn(`[sidebar] WARN: ${orphans.length} Security Guide file(s) missing from SECURITY_ROOT_ORDER — falling through to root:`);
|
|
for (const id of orphans) console.warn(` - ${id}`);
|
|
for (const id of orphans) items.push({type: 'doc', id, label: leafLabels[id]});
|
|
}
|
|
|
|
autoCat.items = items;
|
|
return autoCat;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// End User Guide — builder.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// Replaces the `docId` leaf anywhere in `items` with a category that links to
|
|
// that same doc and nests `children` (doc ids, or {doc, label} pairs) under it.
|
|
// Returns false if the leaf wasn't found, so callers can warn.
|
|
function promoteDocToCategory(items, docId, children) {
|
|
for (let i = 0; i < items.length; i++) {
|
|
const it = items[i];
|
|
if (it.type === 'doc' && it.id === docId) {
|
|
items[i] = {
|
|
type: 'category',
|
|
label: it.label,
|
|
collapsed: true,
|
|
link: {type: 'doc', id: docId},
|
|
items: children.map((child) => {
|
|
const id = typeof child === 'string' ? child : child.doc;
|
|
const label = (typeof child === 'string' ? null : child.label) || docLabelById(id);
|
|
return {type: 'doc', id, label};
|
|
}),
|
|
};
|
|
return true;
|
|
}
|
|
if (it.type === 'category' && it.items && promoteDocToCategory(it.items, docId, children)) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function regroupEndUserSection(sectionCat, dirName, override) {
|
|
const leafLabels = collectLeafLabels(sectionCat);
|
|
const prefix = `end-user-guide/${dirName}/`;
|
|
|
|
function buildItem(spec) {
|
|
if (typeof spec === 'string') {
|
|
const id = prefix + spec;
|
|
return {type: 'doc', id, label: leafLabels[id] || humanize(spec)};
|
|
}
|
|
const cat = {type: 'category', label: spec.label, collapsed: true, items: spec.items.map(buildItem)};
|
|
if (spec.landing) cat.link = {type: 'doc', id: prefix + spec.landing};
|
|
return cat;
|
|
}
|
|
|
|
const items = override.order.map(buildItem);
|
|
if (override.label) sectionCat.label = override.label;
|
|
if (override.landing) sectionCat.link = {type: 'doc', id: prefix + override.landing};
|
|
|
|
const known = new Set();
|
|
(function walk(n) {
|
|
if (Array.isArray(n)) n.forEach(walk);
|
|
else if (n && typeof n === 'object') {
|
|
if (n.type === 'doc' && n.id) known.add(n.id);
|
|
if (n.link && n.link.id) known.add(n.link.id);
|
|
if (n.items) walk(n.items);
|
|
}
|
|
})([...items, sectionCat.link].filter(Boolean));
|
|
const orphans = Object.keys(leafLabels).filter((id) => !known.has(id));
|
|
if (orphans.length > 0) {
|
|
console.warn(`[sidebar] WARN: ${orphans.length} ${dirName} file(s) missing from ENDUSER_SECTION_OVERRIDES — falling through to the end of the section:`);
|
|
for (const id of orphans) console.warn(` - ${id}`);
|
|
for (const id of orphans) items.push({type: 'doc', id, label: leafLabels[id]});
|
|
}
|
|
|
|
sectionCat.items = items;
|
|
return sectionCat;
|
|
}
|
|
|
|
function buildEndUserGuideSidebar(autoCat) {
|
|
// usage_tips' label is overridden here because agents/docs/ is staged from
|
|
// the plugin submodule and gitignored, so its frontmatter title can't be
|
|
// corrected in this repo.
|
|
const promoted = promoteDocToCategory(autoCat.items, 'end-user-guide/agents', [
|
|
{doc: 'agents/docs/usage_tips', label: 'Agents usage tips and best practices'},
|
|
'end-user-guide/collaborate/agents-context-management',
|
|
]);
|
|
if (!promoted) {
|
|
console.warn('[sidebar] WARN: End User Guide "agents" doc not found — Agents child pages were not nested.');
|
|
}
|
|
|
|
let foundCollaborate = false;
|
|
const foundSections = new Set();
|
|
for (const it of autoCat.items) {
|
|
if (it.type !== 'category') continue;
|
|
const dirName = categoryDirName(it);
|
|
if (dirName === 'collaborate') {
|
|
regroupCollaborate(it);
|
|
foundCollaborate = true;
|
|
} else if (ENDUSER_SECTION_OVERRIDES[dirName]) {
|
|
regroupEndUserSection(it, dirName, ENDUSER_SECTION_OVERRIDES[dirName]);
|
|
foundSections.add(dirName);
|
|
}
|
|
}
|
|
if (!foundCollaborate) {
|
|
console.warn('[sidebar] WARN: End User Guide "Collaborate" sub-category not found — COLLABORATE_GROUPS override was not applied.');
|
|
}
|
|
for (const dirName of Object.keys(ENDUSER_SECTION_OVERRIDES)) {
|
|
if (!foundSections.has(dirName)) {
|
|
console.warn(`[sidebar] WARN: End User Guide "${dirName}" sub-category not found — its ordering override was not applied.`);
|
|
}
|
|
}
|
|
|
|
orderRootCategories(autoCat, ENDUSER_ROOT_ORDER, 'End User Guide');
|
|
return autoCat;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Entry point.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
function main() {
|
|
if (!existsSync(SRC)) { console.error(`SRC not found at ${SRC}`); process.exit(1); }
|
|
|
|
const sidebar = [];
|
|
for (const {dir, label} of TOP_LEVEL) {
|
|
const abs = join(SRC, dir);
|
|
if (!existsSync(abs)) continue;
|
|
let cat = buildCategory(abs, dir);
|
|
if (!cat) continue;
|
|
cat.label = label;
|
|
cat.collapsed = true;
|
|
if (dir === 'product-overview') {
|
|
cat = buildOverviewSidebar(cat);
|
|
} else if (dir === 'deployment-guide') {
|
|
cat = buildDeploymentSidebar(cat);
|
|
} else if (dir === 'administration-guide') {
|
|
cat = buildAdminGuideSidebar(cat);
|
|
} else if (dir === 'end-user-guide') {
|
|
cat = buildEndUserGuideSidebar(cat);
|
|
} else if (dir === 'integrations-guide') {
|
|
cat = buildIntegrationsSidebar(cat);
|
|
} else if (dir === 'security-guide') {
|
|
cat = buildSecuritySidebar(cat);
|
|
}
|
|
sidebar.push(cat);
|
|
}
|
|
writeFileSync(OUT, JSON.stringify(sidebar, null, 2));
|
|
|
|
let cats = 0, docs = 0;
|
|
(function walk(n) {
|
|
if (Array.isArray(n)) n.forEach(walk);
|
|
else if (n && typeof n === 'object') {
|
|
if (n.type === 'category') cats++;
|
|
if (n.type === 'doc') docs++;
|
|
if (n.items) walk(n.items);
|
|
}
|
|
})(sidebar);
|
|
console.log(`[sidebar] wrote ${OUT}: ${cats} categories, ${docs} docs`);
|
|
}
|
|
|
|
main();
|