Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions config/defaults/development.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@ const config = {
swagger: {
enable: true,
},
docs: {
// Grouping primitive for guide sections. See `publicDocs.tree.js` JSDoc for full schema details.
guideSections: [
{ title: 'Get Started', prefixMin: 0, prefixMax: 9 },
],
},
api: {
protocol: 'http',
port: 3000,
Expand Down
20 changes: 9 additions & 11 deletions lib/helpers/guides.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
/**
* Markdown guide loader for the Redoc API reference.
* Markdown guide loader for the OpenAPI reference.
*
* Per-module markdown guides live under `modules/{name}/doc/guides/*.md`
* and are discovered by the same globbing mechanism as OpenAPI YAML files
* (see `config/assets.js` → `allGuides`).
*
* Guides are merged into the OpenAPI spec via `info.description`, which
* Redoc renders as a top-level "Introduction" section in the sidebar and
* splits on markdown H1/H2 headings.
* Guides are merged into the OpenAPI spec via `info.description`, which an
* OpenAPI viewer renders as a top-level "Introduction" section, split on
* markdown H1/H2 headings.
*
* When `mergeGuidesIntoSpec` is called with a `{ sections }` option, guides
* are grouped under H1 section dividers (with each guide rendered as H2).
* Redoc auto-nests H2 entries under their parent H1 in the sidebar, giving
* the 5-section IA structure instead of a flat list.
* are grouped under H1 section dividers (with each guide rendered as H2),
* giving a sectioned IA structure instead of a flat list.
*/
import fs from 'fs';
import path from 'path';
Expand Down Expand Up @@ -105,17 +104,16 @@ const prefixFromPath = (filePath) => {
* Merge loaded guides into an OpenAPI spec's `info.description`.
*
* **Flat mode (default)** — each guide becomes a top-level H1 section.
* Redoc renders each H1 as a sidebar entry.
* An OpenAPI viewer renders each H1 as a sidebar entry.
*
* **Sectioned mode** — when `options.sections` is provided, guides are
* grouped under H1 dividers (one per section) with each guide rendered as
* H2. Redoc auto-nests H2 entries under their parent H1 in the sidebar,
* giving a compact 5-section IA instead of a flat list of 18 guides.
* H2, giving a compact sectioned IA instead of a flat list.
* Guides whose filename prefix does not fall in any section range are
* appended at the end as H2 (never silently dropped).
*
* The original spec is mutated (and returned) to match the merge style used
* by `initSwagger` in `lib/services/express.js`.
* by `initApiSpec` in `lib/services/express.js`.
*
* @param {object} spec - OpenAPI spec object (will be mutated).
* @param {{ title: string, body: string, path?: string }[]} guides - Loaded guide entries.
Expand Down
74 changes: 10 additions & 64 deletions lib/services/express.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import cors from 'cors';
import morgan from 'morgan';
import fs from 'fs';
import YAML from 'js-yaml';
import redoc from 'redoc-express';

import config from '../../config/index.js';
import configHelper from '../helpers/config.js';
Expand All @@ -41,44 +40,14 @@ export const computeOpenApiServerUrl = (domain) => {
};

/**
* Default Redoc theme — Inter + JetBrains Mono, tighter sidebar, refined right panel.
* No hardcoded brand color. Downstream projects override via config.docs.redocTheme
* (deep-merged, zero devkit edits required).
*/
const defaultRedocTheme = {
typography: {
fontFamily: '"Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif',
headings: { fontFamily: '"Inter", -apple-system, sans-serif', fontWeight: '600' },
code: { fontFamily: '"JetBrains Mono", Menlo, Consolas, monospace' },
},
sidebar: {
width: '260px',
textTransform: 'none',
},
rightPanel: { backgroundColor: '#1a1a1a' },
spacing: { unit: 4 },
};

/**
* Custom CSS injected into the Redoc UI to cover what the theme schema cannot.
* ≤ 30 lines.
*/
const redocCustomCss = `
.menu-content { letter-spacing: 0; }
.menu-content label, .menu-content .operation-type { text-transform: none !important; }
pre, code { font-feature-settings: "calt" 0, "liga" 0; }
.api-content blockquote { border-left: 3px solid #888; padding-left: 12px; color: #555; }
`.trim();

/**
* Initialize API documentation (Redoc UI + JSON spec endpoint)
* Initialize API documentation (OpenAPI JSON spec endpoint)
* @param {object} app - express application instance
* @returns {void}
*/
const initSwagger = (app) => {
// Secure-by-default: the API docs surface (/api/spec.json + /api/docs) is
// UNAUTHENTICATED, so it is only mounted in dev-grade envs. Any production-grade
// env (the literal `production` OR a deployment env label) skips it even when
const initApiSpec = (app) => {
// Secure-by-default: the API spec surface (/api/spec.json) is UNAUTHENTICATED,
// so it is only mounted in dev-grade envs. Any production-grade env (the
// literal `production` OR a deployment env label) skips it even when
// config.swagger.enable is still truthy — opt-OUT by default in non-dev.
if (config.swagger.enable && !configHelper.isProd()) {
if (!config.files.swagger || config.files.swagger.length === 0) {
Expand Down Expand Up @@ -136,8 +105,8 @@ const initSwagger = (app) => {
};
spec.servers = [{ url: computeOpenApiServerUrl(config.domain) }];

// Merge per-module markdown guides into info.description so Redoc
// renders them in its sidebar alongside the OpenAPI reference.
// Merge per-module markdown guides into info.description so an OpenAPI
// viewer renders them alongside the reference.
const guides = guidesHelper.loadGuides(config.files.guides || []);
guidesHelper.mergeGuidesIntoSpec(spec, guides);
if (guides.length > 0) {
Comment thread
PierreBrisorgueil marked this conversation as resolved.
Expand All @@ -156,29 +125,6 @@ const initSwagger = (app) => {

// Serve the merged spec as JSON
app.get('/api/spec.json', serveSpec);

// Deep-merge devkit default theme with optional per-project override from
// config.docs.redocTheme — downstream projects need zero devkit edits.
const theme = _.merge({}, defaultRedocTheme, (config.docs && config.docs.redocTheme) || {});

// Mount Redoc API reference UI — consumes the spec via URL (not inline).
// Equivalents for the previous Scalar `hideModels` behavior: hide the
// download button and schema titles, and expand common success responses
// so the reference feels compact and consumer-focused.
app.get(
'/api/docs',
redoc({
title: config.app.title,
specUrl: '/api/spec.json',
redocOptions: {
hideDownloadButton: true,
hideSchemaTitles: true,
expandResponses: '200,201',
theme,
customCss: redocCustomCss,
},
}),
);
}
};

Expand Down Expand Up @@ -371,8 +317,8 @@ const initErrorRoutes = (app) => {
const init = async () => {
// Initialize express app
const app = express();
// Initialize modules swagger doc
initSwagger(app);
// Initialize the OpenAPI JSON spec endpoint
initApiSpec(app);
// Initialize local variables
initLocalVariables(app);
// Assign a unique request ID before any route registration
Expand Down Expand Up @@ -415,7 +361,7 @@ const init = async () => {
};

export default {
initSwagger,
initApiSpec,
initLocalVariables,
initPreParserRoutes,
initMiddleware,
Expand Down
Loading
Loading