Skip to content

Commit 24bc428

Browse files
committed
Configure docs.json for Mintlify with navigation, branding, and test suite
1 parent 5040c7b commit 24bc428

3 files changed

Lines changed: 370 additions & 1 deletion

File tree

‎docs.json‎

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,204 @@
11
{
2+
"$schema": "https://mintlify.com/docs.json",
3+
"name": "MTVL",
4+
"theme": "mint",
5+
"description": "Modular Media & Literature Tracking Platform, API, and Developer Guide",
6+
"colors": {
7+
"primary": "#6366f1",
8+
"light": "#818cf8",
9+
"dark": "#4f46e5"
10+
},
11+
"logo": {
12+
"light": "/static/img/logo.svg",
13+
"dark": "/static/img/logo.svg",
14+
"href": "https://docs.mylists.cc"
15+
},
16+
"favicon": "/static/img/favicon.ico",
17+
"appearance": {
18+
"default": "dark"
19+
},
20+
"icons": {
21+
"library": "lucide"
22+
},
23+
"navbar": {
24+
"links": [
25+
{
26+
"type": "github",
27+
"href": "https://github.com/mylists/mtvl"
28+
}
29+
],
30+
"primary": {
31+
"type": "button",
32+
"label": "GitHub",
33+
"href": "https://github.com/mylists/mtvl"
34+
}
35+
},
36+
"footer": {
37+
"socials": {
38+
"github": "https://github.com/mylists/mtvl"
39+
},
40+
"links": [
41+
{
42+
"header": "Documentation",
43+
"items": [
44+
{
45+
"label": "Introduction",
46+
"href": "/docs/intro"
47+
},
48+
{
49+
"label": "App User Guide",
50+
"href": "/docs/app/overview"
51+
},
52+
{
53+
"label": "API Reference",
54+
"href": "/docs/api/overview"
55+
}
56+
]
57+
},
58+
{
59+
"header": "Developers",
60+
"items": [
61+
{
62+
"label": "Architecture",
63+
"href": "/docs/developer/architecture"
64+
},
65+
{
66+
"label": "Adding Categories",
67+
"href": "/docs/developer/adding-categories"
68+
},
69+
{
70+
"label": "Neon Postgres Setup",
71+
"href": "/docs/neon/overview"
72+
}
73+
]
74+
},
75+
{
76+
"header": "Ecosystem & Repositories",
77+
"items": [
78+
{
79+
"label": "Backend (mtvl)",
80+
"href": "https://github.com/mylists/mtvl"
81+
},
82+
{
83+
"label": "Frontend (mtvl-frontend)",
84+
"href": "https://github.com/mylists/mtvl-frontend"
85+
},
86+
{
87+
"label": "Kubernetes Helm Charts",
88+
"href": "https://github.com/mylists/kubernetes"
89+
}
90+
]
91+
}
92+
]
93+
},
94+
"redirects": [
95+
{
96+
"source": "/",
97+
"destination": "/docs/intro"
98+
},
99+
{
100+
"source": "/intro",
101+
"destination": "/docs/intro"
102+
},
103+
{
104+
"source": "/app/:slug*",
105+
"destination": "/docs/app/:slug*"
106+
},
107+
{
108+
"source": "/api/:slug*",
109+
"destination": "/docs/api/:slug*"
110+
},
111+
{
112+
"source": "/developer/:slug*",
113+
"destination": "/docs/developer/:slug*"
114+
},
115+
{
116+
"source": "/neon/:slug*",
117+
"destination": "/docs/neon/:slug*"
118+
}
119+
],
120+
"navigation": {
121+
"tabs": [
122+
{
123+
"tab": "Documentation",
124+
"icon": "book-open",
125+
"groups": [
126+
{
127+
"group": "Overview",
128+
"pages": [
129+
"docs/intro"
130+
]
131+
},
132+
{
133+
"group": "MTVL App User Guide",
134+
"pages": [
135+
"docs/app/overview",
136+
"docs/app/catalogs",
137+
"docs/app/tracking-lists",
138+
"docs/app/ratings-and-progress",
139+
"docs/app/search-and-discovery",
140+
"docs/app/import-export",
141+
"docs/app/profile-and-tokens"
142+
]
143+
}
144+
]
145+
},
146+
{
147+
"tab": "API Reference",
148+
"icon": "terminal",
149+
"groups": [
150+
{
151+
"group": "MTVL REST API Reference",
152+
"pages": [
153+
"docs/api/overview",
154+
"docs/api/authentication",
155+
"docs/api/categories",
156+
"docs/api/movies",
157+
"docs/api/tvshows",
158+
"docs/api/books",
159+
"docs/api/search",
160+
"docs/api/stats",
161+
"docs/api/import-export",
162+
"docs/api/tokens",
163+
"docs/api/health-probes"
164+
]
165+
}
166+
]
167+
},
168+
{
169+
"tab": "Developer Guide",
170+
"icon": "code",
171+
"groups": [
172+
{
173+
"group": "Developer Guide",
174+
"pages": [
175+
"docs/developer/architecture",
176+
"docs/developer/getting-started",
177+
"docs/developer/adding-categories",
178+
"docs/developer/pluggable-auth",
179+
"docs/developer/frontend-development",
180+
"docs/developer/testing"
181+
]
182+
}
183+
]
184+
},
185+
{
186+
"tab": "Neon Setup",
187+
"icon": "database",
188+
"groups": [
189+
{
190+
"group": "Neon Postgres Setup",
191+
"pages": [
192+
"docs/neon/overview",
193+
"docs/neon/quickstart",
194+
"docs/neon/connection-pooling",
195+
"docs/neon/migrations",
196+
"docs/neon/branching-workflow",
197+
"docs/neon/deployment"
198+
]
199+
}
200+
]
201+
}
202+
]
203+
}
2204
}

‎package.json‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@
1212
"serve": "docusaurus serve",
1313
"write-translations": "docusaurus write-translations",
1414
"write-heading-ids": "docusaurus write-heading-ids",
15-
"test": "node scripts/test-docs.mjs && node scripts/test-infra.mjs"
15+
"test": "node scripts/test-docs.mjs && node scripts/test-infra.mjs",
16+
"test:mintlify": "node scripts/test-mintlify.mjs"
1617
},
1718
"dependencies": {
1819
"@docusaurus/core": "^3.7.0",

‎scripts/test-mintlify.mjs‎

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
import fs from 'node:fs';
2+
import path from 'node:path';
3+
import { fileURLToPath } from 'node:url';
4+
5+
const __filename = fileURLToPath(import.meta.url);
6+
const __dirname = path.dirname(__filename);
7+
const rootDir = path.resolve(__dirname, '..');
8+
const docsJsonPath = path.join(rootDir, 'docs.json');
9+
const docsDir = path.join(rootDir, 'docs');
10+
11+
console.log('🧪 Starting Mintlify docs.json Test Suite...\n');
12+
13+
let errorCount = 0;
14+
15+
function assert(condition, message) {
16+
if (!condition) {
17+
console.error(`❌ FAIL: ${message}`);
18+
errorCount++;
19+
} else {
20+
console.log(`✅ PASS: ${message}`);
21+
}
22+
}
23+
24+
// 1. Validate docs.json existence and JSON syntax
25+
assert(fs.existsSync(docsJsonPath), 'docs.json exists');
26+
27+
let config;
28+
try {
29+
const raw = fs.readFileSync(docsJsonPath, 'utf-8');
30+
config = JSON.parse(raw);
31+
assert(true, 'docs.json is valid JSON');
32+
} catch (err) {
33+
assert(false, `Failed to parse docs.json: ${err.message}`);
34+
process.exit(1);
35+
}
36+
37+
// 2. Validate Required Top-Level Fields
38+
assert(config.$schema === 'https://mintlify.com/docs.json', '$schema is set to https://mintlify.com/docs.json');
39+
assert(typeof config.name === 'string' && config.name.length > 0, 'name is non-empty string');
40+
assert(typeof config.theme === 'string' && config.theme.length > 0, 'theme is defined');
41+
42+
// 3. Validate Colors
43+
assert(Boolean(config.colors), 'colors configuration is present');
44+
if (config.colors) {
45+
const hexRegex = /^#([a-fA-F0-9]{6}|[a-fA-F0-9]{3})$/;
46+
assert(hexRegex.test(config.colors.primary), `colors.primary (${config.colors.primary}) is valid hex`);
47+
if (config.colors.light) {
48+
assert(hexRegex.test(config.colors.light), `colors.light (${config.colors.light}) is valid hex`);
49+
}
50+
if (config.colors.dark) {
51+
assert(hexRegex.test(config.colors.dark), `colors.dark (${config.colors.dark}) is valid hex`);
52+
}
53+
}
54+
55+
// 4. Validate Asset Paths
56+
function resolveAssetPath(assetUri) {
57+
if (!assetUri) return null;
58+
const rel = assetUri.startsWith('/') ? assetUri.slice(1) : assetUri;
59+
return path.join(rootDir, rel);
60+
}
61+
62+
if (config.favicon) {
63+
const faviconPath = resolveAssetPath(typeof config.favicon === 'string' ? config.favicon : config.favicon.light);
64+
assert(fs.existsSync(faviconPath), `favicon file exists at ${faviconPath}`);
65+
}
66+
67+
if (config.logo) {
68+
const logoLightPath = resolveAssetPath(typeof config.logo === 'string' ? config.logo : config.logo.light);
69+
const logoDarkPath = resolveAssetPath(typeof config.logo === 'string' ? config.logo : config.logo.dark);
70+
assert(fs.existsSync(logoLightPath), `logo.light exists at ${logoLightPath}`);
71+
assert(fs.existsSync(logoDarkPath), `logo.dark exists at ${logoDarkPath}`);
72+
}
73+
74+
// 5. Validate Navigation Structure & Page Paths
75+
assert(Boolean(config.navigation), 'navigation configuration is present');
76+
77+
const referencedPages = new Set();
78+
79+
function extractPages(node) {
80+
if (!node) return;
81+
if (typeof node === 'string') {
82+
referencedPages.add(node);
83+
return;
84+
}
85+
if (Array.isArray(node)) {
86+
for (const item of node) {
87+
extractPages(item);
88+
}
89+
return;
90+
}
91+
if (typeof node === 'object') {
92+
if (Array.isArray(node.pages)) {
93+
for (const p of node.pages) {
94+
extractPages(p);
95+
}
96+
}
97+
if (Array.isArray(node.groups)) {
98+
for (const g of node.groups) {
99+
extractPages(g);
100+
}
101+
}
102+
if (Array.isArray(node.tabs)) {
103+
for (const t of node.tabs) {
104+
extractPages(t);
105+
}
106+
}
107+
}
108+
}
109+
110+
extractPages(config.navigation);
111+
assert(referencedPages.size > 0, `navigation defines ${referencedPages.size} total page references`);
112+
113+
// Check that every referenced page exists on disk
114+
for (const pageRef of referencedPages) {
115+
const pagePathMd = path.join(rootDir, `${pageRef}.md`);
116+
const pagePathMdx = path.join(rootDir, `${pageRef}.mdx`);
117+
const pagePathHtml = path.join(rootDir, `${pageRef}.html`);
118+
const exists = fs.existsSync(pagePathMd) || fs.existsSync(pagePathMdx) || fs.existsSync(pagePathHtml);
119+
assert(exists, `Page exists on disk: ${pageRef} (.md / .mdx)`);
120+
}
121+
122+
// 6. Check that all files in docs/ are included in navigation
123+
function getAllDocFiles(dir) {
124+
const entries = fs.readdirSync(dir, { withFileTypes: true });
125+
let files = [];
126+
for (const entry of entries) {
127+
const full = path.join(dir, entry.name);
128+
if (entry.isDirectory()) {
129+
files = files.concat(getAllDocFiles(full));
130+
} else if (entry.name.endsWith('.md') || entry.name.endsWith('.mdx')) {
131+
const relToRoot = path.relative(rootDir, full).replace(/\.(md|mdx)$/, '');
132+
files.push(relToRoot);
133+
}
134+
}
135+
return files;
136+
}
137+
138+
const allDocFiles = getAllDocFiles(docsDir);
139+
assert(allDocFiles.length >= 18, `Found ${allDocFiles.length} documentation files in docs/`);
140+
141+
let unreferencedCount = 0;
142+
for (const docFile of allDocFiles) {
143+
if (!referencedPages.has(docFile)) {
144+
console.error(`⚠️ Unreferenced doc in docs.json: ${docFile}`);
145+
unreferencedCount++;
146+
}
147+
}
148+
assert(unreferencedCount === 0, `All ${allDocFiles.length} docs are mapped in docs.json navigation`);
149+
150+
// 7. Validate Navbar and Footer
151+
if (config.navbar) {
152+
assert(Array.isArray(config.navbar.links) || Boolean(config.navbar.primary), 'navbar has links or primary CTA');
153+
}
154+
155+
if (config.footer) {
156+
assert(Boolean(config.footer.socials) || Array.isArray(config.footer.links), 'footer has socials or links');
157+
}
158+
159+
console.log('\n========================================');
160+
if (errorCount === 0) {
161+
console.log('🎉 ALL MINTLIFY DOCS.JSON TESTS PASSED CLEANLY!\n');
162+
process.exit(0);
163+
} else {
164+
console.error(`💥 ${errorCount} test(s) failed in docs.json.\n`);
165+
process.exit(1);
166+
}

0 commit comments

Comments
 (0)