Skip to content

Commit d95a6e9

Browse files
committed
Integrate dynamic site config via objectdocs.json
Added objectdocs.json for centralized site configuration and implemented dynamic loading and merging of config in the site and CLI. Updated Next.js, docs layout, and CLI commands to use the new config, enabling runtime updates and improved customization for branding, navigation, and build output.
1 parent 813a36d commit d95a6e9

13 files changed

Lines changed: 407 additions & 20 deletions

File tree

‎content/docs-zh-CN/02-objectql/meta.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
{
22
"title": "ObjectQL",
3+
"root": true,
34
"pages": [
45
"index",
56
"core-concepts",

‎content/docs/02-objectql/meta.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
{
22
"title": "ObjectQL",
3+
"root": true,
34
"pages": [
45
"index",
56
"core-concepts",

‎objectdocs.json‎

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
{
2+
"meta": {
3+
"title": "ObjectStack Docs",
4+
"description": "Enterprise-grade low-code platform documentation",
5+
"url": "https://docs.objectstack.ai",
6+
"favicon": "/favicon.ico"
7+
},
8+
"build": {
9+
"output": ""
10+
},
11+
"branding": {
12+
"logo": {
13+
"text": "ObjectStack"
14+
},
15+
"theme": {
16+
"accentColor": "blue",
17+
"radius": "0.5rem"
18+
}
19+
},
20+
"layout": {
21+
"navbar": {
22+
"enabled": true,
23+
"transparentMode": "top",
24+
"links": [
25+
{
26+
"text": "Home",
27+
"url": "https://www.objectstack.ai",
28+
"external": true
29+
}
30+
],
31+
"socials": [
32+
{ "platform": "github", "url": "https://github.com/objectstack-ai/" }
33+
]
34+
},
35+
"sidebar": {
36+
"enabled": true,
37+
"prefetch": true,
38+
"defaultOpenLevel": 1,
39+
"collapsible": true,
40+
"tabs": [
41+
{
42+
"title": "v2.0",
43+
"url": "/docs/v2",
44+
"description": "Latest version"
45+
},
46+
{
47+
"title": "v1.0",
48+
"url": "/docs/v1",
49+
"description": "Legacy version"
50+
}
51+
]
52+
},
53+
"toc": {
54+
"enabled": true,
55+
"depth": 3
56+
},
57+
"footer": {
58+
"enabled": false,
59+
"copyright": "© 2026 ObjectStack Inc."
60+
}
61+
},
62+
"page": {
63+
"showLastUpdate": true,
64+
"showEditLink": true,
65+
"repoBaseUrl": "https://github.com/objectstack-ai/docs"
66+
},
67+
"content": {
68+
"math": false,
69+
"imageZoom": true,
70+
"codeBlock": {
71+
"theme": "vesper",
72+
"showLineNumbers": true
73+
}
74+
}
75+
}

‎packages/cli/src/commands/build.mjs‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,9 @@ export function registerBuildCommand(cli) {
5555
}
5656
fs.cpSync(src, dest, { recursive: true });
5757
console.log(`Build successfully output to: ${dest}`);
58+
} else {
59+
console.log(`\nNo 'out' directory generated in ${src}.`);
60+
console.log(`This is expected if 'output: export' is disabled.`);
5861
}
5962
}
6063
process.exit(code);

‎packages/cli/src/commands/dev.mjs‎

Lines changed: 40 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { spawn } from 'child_process';
22
import path from 'path';
3+
import fs from 'fs';
34
import { fileURLToPath } from 'url';
45
import { createRequire } from 'module';
56

@@ -37,14 +38,45 @@ export function registerDevCommand(cli) {
3738
const nextCmd = 'npm';
3839
const args = ['run', 'dev', '--', '-p', options.port];
3940

40-
const child = spawn(nextCmd, args, {
41-
stdio: 'inherit',
42-
env,
43-
cwd: nextAppDir // CRITICAL: Run in the Next.js app directory
44-
});
41+
let child;
42+
let isRestarting = false;
43+
let debounceTimer;
4544

46-
child.on('close', (code) => {
47-
process.exit(code);
48-
});
45+
const startServer = () => {
46+
child = spawn(nextCmd, args, {
47+
stdio: 'inherit',
48+
env,
49+
cwd: nextAppDir // CRITICAL: Run in the Next.js app directory
50+
});
51+
52+
child.on('close', (code) => {
53+
if (isRestarting) {
54+
isRestarting = false;
55+
startServer();
56+
} else {
57+
// Only exit if we are not restarting.
58+
// Null code means killed by signal (like our kill() call), but we handle that via flag.
59+
process.exit(code || 0);
60+
}
61+
});
62+
};
63+
64+
startServer();
65+
66+
// Watch for config changes
67+
const configFile = path.resolve(process.cwd(), 'objectdocs.json');
68+
if (fs.existsSync(configFile)) {
69+
console.log(`Watching config: ${configFile}`);
70+
fs.watch(configFile, (eventType) => {
71+
if (eventType === 'change') {
72+
clearTimeout(debounceTimer);
73+
debounceTimer = setTimeout(() => {
74+
console.log('\nConfig changed. Restarting server...');
75+
isRestarting = true;
76+
child.kill();
77+
}, 500);
78+
}
79+
});
80+
}
4981
});
5082
}

‎packages/site/app/[lang]/docs/[[...slug]]/page.tsx‎

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { source } from '@/lib/source';
22
import type { Metadata } from 'next';
33
import { DocsPage, DocsBody } from 'fumadocs-ui/page';
44
import { notFound } from 'next/navigation';
5+
import { siteConfig } from '@/lib/site-config';
56

67
interface PageProps {
78
params: Promise<{
@@ -22,14 +23,29 @@ export default async function Page({ params }: PageProps) {
2223
const MDX = page.data.body as any;
2324

2425
return (
25-
<DocsPage toc={page.data.toc as any} full={page.data.full as any}>
26+
<DocsPage
27+
toc={page.data.toc as any}
28+
full={page.data.full as any}
29+
lastUpdate={siteConfig.page.showLastUpdate ? (page.data as any).lastModified : undefined}
30+
tableOfContent={{
31+
enabled: siteConfig.layout.toc.enabled,
32+
style: siteConfig.layout.toc.depth > 2 ? 'clerk' : 'normal',
33+
}}
34+
editOnGithub={siteConfig.page.showEditLink ? {
35+
owner: siteConfig.page.repoBaseUrl.split('/')[3],
36+
repo: siteConfig.page.repoBaseUrl.split('/')[4],
37+
sha: 'main', // Defaulting to main, could be extracted
38+
path: siteConfig.page.repoBaseUrl.split('/').slice(7).join('/') // simplistic parsing
39+
} : undefined}
40+
>
2641
<DocsBody>
2742
<MDX />
2843
</DocsBody>
2944
</DocsPage>
3045
);
3146
}
3247

48+
3349
export async function generateStaticParams() {
3450
return source.generateParams();
3551
}

‎packages/site/app/[lang]/docs/layout.tsx‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { source } from '@/lib/source';
22
import { DocsLayout } from 'fumadocs-ui/layouts/docs';
33
import type { ReactNode } from 'react';
44
import { baseOptions } from '@/app/layout.config';
5+
import { siteConfig } from '@/lib/site-config';
56

67
export default async function Layout({
78
params,
@@ -16,8 +17,37 @@ export default async function Layout({
1617
<DocsLayout
1718
tree={source.getPageTree(lang)}
1819
{...baseOptions}
20+
sidebar={{
21+
enabled: siteConfig.layout.sidebar.enabled,
22+
prefetch: siteConfig.layout.sidebar.prefetch,
23+
collapsible: siteConfig.layout.sidebar.collapsible,
24+
defaultOpenLevel: siteConfig.layout.sidebar.defaultOpenLevel,
25+
footer: siteConfig.layout.sidebar.footer ? (
26+
<div className="p-4 text-xs text-muted-foreground border-t">
27+
{siteConfig.layout.sidebar.footer.html ? (
28+
<div dangerouslySetInnerHTML={{ __html: siteConfig.layout.sidebar.footer.html }} />
29+
) : (
30+
siteConfig.layout.sidebar.footer.text
31+
)}
32+
</div>
33+
) : undefined,
34+
tabs: siteConfig.layout.sidebar.tabs && siteConfig.layout.sidebar.tabs.length > 0 ?
35+
siteConfig.layout.sidebar.tabs.map(tab => ({
36+
title: tab.title,
37+
url: tab.url,
38+
description: tab.description,
39+
})) : undefined,
40+
banner: siteConfig.layout.sidebar.banner ? (
41+
<div className="-mx-2 -mt-2 rounded-lg border bg-card p-4 transition-colors hover:bg-accent/50 text-card-foreground text-sm">
42+
<a href={siteConfig.layout.sidebar.banner.url} className="block font-medium">
43+
{siteConfig.layout.sidebar.banner.text}
44+
</a>
45+
</div>
46+
) : undefined
47+
}}
1948
>
2049
{children}
2150
</DocsLayout>
2251
);
2352
}
53+
Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
11
import { type BaseLayoutProps } from 'fumadocs-ui/layouts/shared';
2+
import { siteConfig } from '@/lib/site-config';
23

34
export const baseOptions: BaseLayoutProps = {
45
nav: {
5-
title: 'ObjectStack',
6+
title: siteConfig.branding.logo.text || 'ObjectStack',
7+
transparentMode: siteConfig.layout.navbar.transparentMode,
68
},
7-
links: [
8-
{
9-
text: 'Documentation',
10-
url: '/',
11-
active: 'nested-url',
12-
},
13-
],
9+
links: siteConfig.layout.navbar.links.map(link => ({
10+
text: link.text,
11+
url: link.url,
12+
active: link.active || 'nested-url',
13+
external: link.external,
14+
})),
15+
githubUrl: siteConfig.layout.navbar.socials.find(s => s.platform === 'github')?.url,
1416
};
17+

‎packages/site/app/layout.tsx‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,19 @@
11
import 'fumadocs-ui/style.css';
22
import { RootProvider } from 'fumadocs-ui/provider/next';
33
import type { ReactNode } from 'react';
4+
import type { Metadata } from 'next';
5+
import { siteConfig } from '@/lib/site-config';
6+
7+
export const metadata: Metadata = {
8+
title: {
9+
template: `%s | ${siteConfig.meta.title}`,
10+
default: siteConfig.meta.title,
11+
},
12+
description: siteConfig.meta.description,
13+
icons: {
14+
icon: siteConfig.meta.favicon,
15+
},
16+
};
417

518
export default function RootLayout({ children }: { children: ReactNode }) {
619
return (
@@ -11,3 +24,4 @@ export default function RootLayout({ children }: { children: ReactNode }) {
1124
</html>
1225
);
1326
}
27+

‎packages/site/lib/deep-merge.ts‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
export function isObject(item: any): item is Record<string, any> {
2+
return (item && typeof item === 'object' && !Array.isArray(item));
3+
}
4+
5+
export function deepMerge<T extends Record<string, any>>(target: T, source: Record<string, any>): T {
6+
const output = { ...target };
7+
8+
if (isObject(target) && isObject(source)) {
9+
Object.keys(source).forEach(key => {
10+
if (isObject(source[key])) {
11+
if (!(key in target)) {
12+
Object.assign(output, { [key]: source[key] });
13+
} else {
14+
(output as any)[key] = deepMerge(target[key], source[key]);
15+
}
16+
} else {
17+
Object.assign(output, { [key]: source[key] });
18+
}
19+
});
20+
}
21+
22+
return output;
23+
}

0 commit comments

Comments
 (0)