openmanual 0.15.2 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin.js CHANGED
@@ -15,6 +15,18 @@ const LogoSchema = z.union([z.string(), z.object({
15
15
  light: z.string(),
16
16
  dark: z.string()
17
17
  })]);
18
+ /** Logo 显示位置 */
19
+ const LogoPositionSchema = z.enum(["sidebar", "header"]);
20
+ /**
21
+ * 顶级 Logo 配置(支持字符串简写和对象形式)
22
+ * - 字符串简写: "/logo.svg" → { light, dark: 同值, position: 'sidebar' }
23
+ * - 对象形式: { light, dark, position? }
24
+ */
25
+ const TopLevelLogoSchema = z.union([z.string(), z.object({
26
+ light: z.string(),
27
+ dark: z.string(),
28
+ position: LogoPositionSchema.optional()
29
+ })]);
18
30
  const FaviconSchema = z.string();
19
31
  const NavbarSchema = z.object({
20
32
  logo: LogoSchema.optional(),
@@ -96,6 +108,7 @@ const OpenManualConfigSchema = z.object({
96
108
  locale: z.string().optional(),
97
109
  contentPolicy: z.enum(["strict", "all"]).optional(),
98
110
  favicon: FaviconSchema.optional(),
111
+ logo: TopLevelLogoSchema.optional(),
99
112
  navbar: NavbarSchema.optional(),
100
113
  header: TopBarSchema.optional(),
101
114
  footer: FooterSchema.optional(),
@@ -145,6 +158,51 @@ function isSeparateTabMode(config) {
145
158
  function isHeaderEnabled(config) {
146
159
  return config.header !== void 0;
147
160
  }
161
+ /**
162
+ * 将顶级 logo 配置标准化为 { light, dark } 形式
163
+ */
164
+ function normalizeTopLevelLogo(logo) {
165
+ if (typeof logo === "string") return {
166
+ light: logo,
167
+ dark: logo,
168
+ position: "sidebar"
169
+ };
170
+ return {
171
+ light: logo.light,
172
+ dark: logo.dark,
173
+ position: logo.position ?? "sidebar"
174
+ };
175
+ }
176
+ /**
177
+ * 解析有效的 Logo 配置(统一优先级链)
178
+ *
179
+ * 优先级:
180
+ * 1. config.logo(新顶级配置)
181
+ * 2. config.navbar.logo(旧 sidebar logo)
182
+ * 3. config.header.logo(旧 header logo)
183
+ * 4. undefined(调用方回退到 config.name)
184
+ */
185
+ function resolveEffectiveLogo(config) {
186
+ if (config.logo) {
187
+ const { position, ...source } = normalizeTopLevelLogo(config.logo);
188
+ return {
189
+ source,
190
+ position
191
+ };
192
+ }
193
+ if (config.navbar?.logo) return {
194
+ source: config.navbar.logo,
195
+ position: "sidebar"
196
+ };
197
+ if (config.header?.logo) return {
198
+ source: config.header.logo,
199
+ position: "header"
200
+ };
201
+ return {
202
+ source: void 0,
203
+ position: "sidebar"
204
+ };
205
+ }
148
206
 
149
207
  //#endregion
150
208
  //#region src/core/config/loader.ts
@@ -183,17 +241,31 @@ async function loadConfig(cwd = process.cwd()) {
183
241
  return mergeDefaults(result.data);
184
242
  }
185
243
  function mergeDefaults(config) {
244
+ const topLevelLogo = config.logo ? normalizeTopLevelLogo(config.logo) : null;
245
+ const topLevelLogoSource = topLevelLogo ? topLevelLogo.light === topLevelLogo.dark ? topLevelLogo.light : {
246
+ light: topLevelLogo.light,
247
+ dark: topLevelLogo.dark
248
+ } : null;
186
249
  return {
187
250
  ...config,
188
251
  contentPolicy: config.contentPolicy ?? "strict",
189
252
  contentDir: config.contentDir ?? DEFAULT_CONFIG.contentDir ?? "content",
190
253
  outputDir: config.outputDir ?? DEFAULT_CONFIG.outputDir ?? "dist",
191
254
  locale: config.locale ?? DEFAULT_CONFIG.locale ?? "zh",
255
+ logo: topLevelLogo ? typeof config.logo === "string" ? config.logo : {
256
+ light: topLevelLogo.light,
257
+ dark: topLevelLogo.dark,
258
+ position: topLevelLogo.position
259
+ } : void 0,
192
260
  navbar: {
193
261
  ...DEFAULT_CONFIG.navbar,
194
262
  ...config.navbar,
195
- logo: config.navbar?.logo ?? config.name
263
+ logo: config.navbar?.logo ?? (topLevelLogo && topLevelLogo.position === "sidebar" ? topLevelLogoSource ?? config.name : config.name)
196
264
  },
265
+ header: config.header ? {
266
+ ...config.header,
267
+ logo: config.header.logo ?? (topLevelLogo && topLevelLogo.position === "header" ? topLevelLogoSource ?? void 0 : void 0)
268
+ } : void 0,
197
269
  footer: {
198
270
  ...DEFAULT_CONFIG.footer,
199
271
  ...config.footer,
@@ -486,11 +558,6 @@ ${config.search?.position === "header" ? `
486
558
  display: none;
487
559
  }
488
560
  ` : ""}
489
-
490
- /* 隐藏侧边栏顶部导航区域(logo/标题 + 折叠按钮容器) */
491
- #nd-sidebar > div:first-child {
492
- display: none;
493
- }
494
561
  `;
495
562
  }
496
563
 
@@ -604,7 +671,7 @@ function generateLibSource(ctx) {
604
671
  if (isOpenApiEnabled(ctx.config)) {
605
672
  const separateTab = isSeparateTabMode(ctx.config);
606
673
  const groupBy = ctx.config.openapi?.groupBy ?? "tag";
607
- if (isI18n) return `import { docs } from 'collections/server';
674
+ if (isI18n) return `import { docs } from '@/.source/server';
608
675
  import { loader, multiple } from 'fumadocs-core/source';
609
676
  import { openapiPlugin, openapiSource } from 'fumadocs-openapi/server';
610
677
  import { openapi } from '@/lib/openapi';
@@ -631,7 +698,7 @@ export const source = loader(
631
698
  },
632
699
  );
633
700
  `;
634
- return `import { docs } from 'collections/server';
701
+ return `import { docs } from '@/.source/server';
635
702
  import { loader, multiple } from 'fumadocs-core/source';
636
703
  import { openapiPlugin, openapiSource } from 'fumadocs-openapi/server';
637
704
  import { openapi } from '@/lib/openapi';
@@ -651,7 +718,7 @@ ${!separateTab ? ` meta: true,\n groupBy: '${groupBy}',` : ""}
651
718
  );
652
719
  `;
653
720
  }
654
- if (isI18n) return `import { docs } from 'collections/server';
721
+ if (isI18n) return `import { docs } from '@/.source/server';
655
722
  import { loader } from 'fumadocs-core/source';
656
723
  import { i18n } from '@/lib/i18n';
657
724
 
@@ -661,7 +728,7 @@ export const source = loader({
661
728
  i18n,
662
729
  });
663
730
  `;
664
- return `import { docs } from 'collections/server';
731
+ return `import { docs } from '@/.source/server';
665
732
  import { loader } from 'fumadocs-core/source';
666
733
 
667
734
  export const source = loader({
@@ -733,11 +800,6 @@ const config = {
733
800
  images: {
734
801
  unoptimized: true,
735
802
  },${rewritesBlock}
736
- turbopack: {
737
- resolveAlias: {
738
- 'collections/*': './.source/*',
739
- },
740
- },
741
803
  };
742
804
 
743
805
  export default withMDX(config);
@@ -794,7 +856,7 @@ export const APIPage = createAPIPage(openapi, {
794
856
  //#endregion
795
857
  //#region src/core/generator/package-json.ts
796
858
  function getOpenManualVersion() {
797
- return "0.15.2";
859
+ return "0.16.0";
798
860
  }
799
861
  function generatePackageJson(ctx) {
800
862
  const openmanualVersion = getOpenManualVersion();
@@ -874,7 +936,13 @@ import { Callout, CalloutTitle, CalloutDescription } from '@/components/callout'
874
936
  ${allowedSlugsSnippet}
875
937
  export default async function Page({ params }: { params: Promise<{ slug?: string[] }> }) {
876
938
  const { slug } = await params;
877
- const page = source.getPage(slug);
939
+ let page = source.getPage(slug);
940
+ // Fallback: when slug is empty (root path /) and getPage returns undefined,
941
+ // try ['index'] — fumadocs-core's slugsPlugin may assign ["index"] to index.mdx
942
+ // due to slug de-duplication conflict, causing getPage([]) to miss it.
943
+ if (!page && (!slug || slug.length === 0)) {
944
+ page = source.getPage(['index']);
945
+ }
878
946
  ${isStrict ? `
879
947
  if (!isAllowed(slug)) {
880
948
  notFound();
@@ -922,14 +990,16 @@ export function generateStaticParams() {
922
990
  let params = source.generateParams();
923
991
  params = params.filter((p: { slug: string[] }) => isAllowed(p.slug));
924
992
  if (!params.some((p: { slug: string[] }) => p.slug.length === 0)) {
925
- params.unshift({ ...params[0], slug: [] });
993
+ const homepage = params.find((p: { slug: string[] }) => p.slug.length === 1 && p.slug[0] === 'index');
994
+ params.unshift({ ...(homepage ?? params[0]), slug: [] });
926
995
  }
927
996
  return params;
928
997
  }` : `
929
998
  export function generateStaticParams() {
930
999
  const params = source.generateParams();
931
1000
  if (!params.some((p: { slug: string[] }) => p.slug.length === 0)) {
932
- params.unshift({ ...params[0], slug: [] });
1001
+ const homepage = params.find((p: { slug: string[] }) => p.slug.length === 1 && p.slug[0] === 'index');
1002
+ params.unshift({ ...(homepage ?? params[0]), slug: [] });
933
1003
  }
934
1004
  return params;
935
1005
  }`}
@@ -960,7 +1030,13 @@ import { Callout, CalloutTitle, CalloutDescription } from '@/components/callout'
960
1030
  ${allowedSlugsSnippet}
961
1031
  export default async function Page({ params }: { params: Promise<{ slug?: string[]; lang: string }> }) {
962
1032
  const { slug, lang } = await params;
963
- const page = source.getPage(slug, lang);
1033
+ let page = source.getPage(slug, lang);
1034
+ // Fallback: when slug is empty (root path /) and getPage returns undefined,
1035
+ // try ['index'] — fumadocs-core's slugsPlugin may assign ["index"] to index.mdx
1036
+ // due to slug de-duplication conflict, causing getPage([], lang) to miss it.
1037
+ if (!page && (!slug || slug.length === 0)) {
1038
+ page = source.getPage(['index'], lang);
1039
+ }
964
1040
  ${isStrict ? `
965
1041
  if (!isAllowed(slug, lang)) {
966
1042
  notFound();
@@ -1011,9 +1087,10 @@ export function generateStaticParams() {
1011
1087
  const languages = [...new Set(params.map((p: { lang: string }) => p.lang))];
1012
1088
  for (const lang of languages) {
1013
1089
  if (!params.some((p: { slug: string[]; lang: string }) => p.slug.length === 0 && p.lang === lang)) {
1014
- const firstForLang = params.find((p: { slug: string[]; lang: string }) => p.lang === lang);
1015
- if (firstForLang) {
1016
- params.unshift({ ...firstForLang, slug: [] });
1090
+ const homepage = params.find((p: { slug: string[]; lang: string }) => p.slug.length === 1 && p.slug[0] === 'index' && p.lang === lang);
1091
+ const fallback = params.find((p: { slug: string[]; lang: string }) => p.lang === lang);
1092
+ if (homepage || fallback) {
1093
+ params.unshift({ ...(homepage ?? fallback!), slug: [], lang });
1017
1094
  }
1018
1095
  }
1019
1096
  }
@@ -1025,9 +1102,10 @@ export function generateStaticParams() {
1025
1102
  const languages = [...new Set(params.map((p: { lang: string }) => p.lang))];
1026
1103
  for (const lang of languages) {
1027
1104
  if (!params.some((p: { slug: string[]; lang: string }) => p.slug.length === 0 && p.lang === lang)) {
1028
- const firstForLang = params.find((p: { slug: string[]; lang: string }) => p.lang === lang);
1029
- if (firstForLang) {
1030
- params.unshift({ ...firstForLang, slug: [] });
1105
+ const homepage = params.find((p: { slug: string[]; lang: string }) => p.slug.length === 1 && p.slug[0] === 'index' && p.lang === lang);
1106
+ const fallback = params.find((p: { slug: string[]; lang: string }) => p.lang === lang);
1107
+ if (homepage || fallback) {
1108
+ params.unshift({ ...(homepage ?? fallback!), slug: [], lang });
1031
1109
  }
1032
1110
  }
1033
1111
  }
@@ -1769,10 +1847,13 @@ async function generateAll(ctx) {
1769
1847
  await mkdir(join(fullPath, ".."), { recursive: true });
1770
1848
  await writeFile(fullPath, file.content, "utf-8");
1771
1849
  }
1772
- const logo = ctx.config.navbar?.logo;
1773
- if (logo && typeof logo === "string" && isImagePath(logo)) await ensureLogoFile(ctx, logo, "light");
1774
- else if (logo && typeof logo === "object") {
1775
- const { light, dark } = resolveLogoPaths(logo);
1850
+ const rawLogo = ctx.config.logo != null ? typeof ctx.config.logo === "string" ? ctx.config.logo : {
1851
+ light: ctx.config.logo.light,
1852
+ dark: ctx.config.logo.dark
1853
+ } : ctx.config.navbar?.logo;
1854
+ if (rawLogo && typeof rawLogo === "string" && isImagePath(rawLogo)) await ensureLogoFile(ctx, rawLogo, "light");
1855
+ else if (rawLogo && typeof rawLogo === "object") {
1856
+ const { light, dark } = resolveLogoPaths(rawLogo);
1776
1857
  if (isImagePath(light)) await ensureLogoFile(ctx, light, "light");
1777
1858
  if (isImagePath(dark) && dark !== light) await ensureLogoFile(ctx, dark, "dark");
1778
1859
  }
@@ -1855,6 +1936,11 @@ function generateDocsLayout(ctx) {
1855
1936
  const isOApi = isOpenApiEnabled(config);
1856
1937
  const rootGroups = ctx.rootGroups;
1857
1938
  const isHeaderSearch = config.search?.position === "header";
1939
+ const { source: logoSource, position: logoPosition } = resolveEffectiveLogo(config);
1940
+ const hasSidebarLogo = logoSource !== void 0 && logoPosition === "sidebar";
1941
+ const sidebarLogoImport = hasSidebarLogo ? "\nimport { NavLogo } from 'openmanual/components/nav-layout';" : "";
1942
+ const sidebarLogoProps = hasSidebarLogo ? resolveNavLogoProps(logoSource, config.name) : null;
1943
+ const sidebarBannerLine = hasSidebarLogo && sidebarLogoProps && !sidebarLogoProps.includes("type=\"text\"") ? `\n banner: <NavLogo ${sidebarLogoProps} />,` : "";
1858
1944
  const linksArray = navLinks.map((l) => ({
1859
1945
  text: l.label,
1860
1946
  url: l.href,
@@ -1876,7 +1962,7 @@ function generateDocsLayout(ctx) {
1876
1962
  if (isI18n) return `import { DocsLayout } from 'fumadocs-ui/layouts/docs';
1877
1963
  import { baseOptions } from '@/lib/layout';
1878
1964
  import { source } from '@/lib/source';
1879
- import type { ReactNode } from 'react';${configDesc ? `\nconst configDescription = '${configDesc.replace(/'/g, "\\'")}' as const;\n` : ""}
1965
+ import type { ReactNode } from 'react';${sidebarLogoImport}${configDesc ? `\nconst configDescription = '${configDesc.replace(/'/g, "\\'")}' as const;\n` : ""}
1880
1966
  export default async function DocsLayoutWrapper({
1881
1967
  params,
1882
1968
  children,
@@ -1894,7 +1980,8 @@ ${isOApi && separateTab ? ` const _omFirstApi = source.getPages(lang)?.find((p:
1894
1980
  const docsOptions = {
1895
1981
  ...baseOptions(lang),
1896
1982
  ${treeLine}${sidebarTabsLine}${githubLine}${linksLine}${footerLine}${configDesc ? "\n description: siteDescription," : ""}${isHeaderSearch ? "\n searchToggle: { enabled: false }," : ""}
1897
- sidebar: { collapsible: false },
1983
+ sidebar: { collapsible: false,${sidebarBannerLine}
1984
+ },
1898
1985
  };
1899
1986
 
1900
1987
  return (
@@ -1907,14 +1994,15 @@ ${isOApi && separateTab ? ` const _omFirstApi = source.getPages(lang)?.find((p:
1907
1994
  return `import { DocsLayout } from 'fumadocs-ui/layouts/docs';
1908
1995
  import { baseOptions } from '@/lib/layout';
1909
1996
  import { source } from '@/lib/source';
1910
- import type { ReactNode } from 'react';${isOApi && separateTab ? `
1997
+ import type { ReactNode } from 'react';${sidebarLogoImport}${isOApi && separateTab ? `
1911
1998
  const _omFirstApi = source.getPages()?.find((p: any) => p.data?.type === 'openapi');
1912
1999
  const _omApiUrl = _omFirstApi?.url ?? '/openapi';
1913
2000
  ` : ""}
1914
2001
  const docsOptions = {
1915
2002
  ...baseOptions(),
1916
2003
  ${treeLine}${sidebarTabsLine}${githubLine}${linksLine}${footerLine}${descLine}${isHeaderSearch ? "\n searchToggle: { enabled: false }," : ""}
1917
- sidebar: { collapsible: false },
2004
+ sidebar: { collapsible: false,${sidebarBannerLine}
2005
+ },
1918
2006
  };
1919
2007
 
1920
2008
  export default function DocsLayoutWrapper({ children }: { children: ReactNode }) {
@@ -2544,7 +2632,7 @@ const regenerateCommand = new Command("_regenerate").description("内部命令
2544
2632
  //#endregion
2545
2633
  //#region src/cli/bin.ts
2546
2634
  function getVersion() {
2547
- return "0.15.2";
2635
+ return "0.16.0";
2548
2636
  }
2549
2637
  const program = new Command();
2550
2638
  const commandName = basename(process.argv[1] ?? "openmanual");