mirror of
https://github.com/hanzo-docs/docs.git
synced 2026-10-25 04:20:02 +00:00
Core: Support pages without title
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
---
|
||||
'fumadocs-core': patch
|
||||
'@fumadocs/content-collections': patch
|
||||
---
|
||||
|
||||
Support pages without `title`
|
||||
@@ -127,11 +127,12 @@ Enable the notebook layout with `fumadocs-ui/layouts/notebook`, it's a more comp
|
||||
```tsx title="layout.tsx"
|
||||
import { DocsLayout } from 'fumadocs-ui/layouts/notebook';
|
||||
import { baseOptions } from '@/app/layout.config';
|
||||
import { source } from '@/lib/source';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
export default function Layout({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<DocsLayout {...baseOptions} tree={tree}>
|
||||
<DocsLayout {...baseOptions} tree={source.pageTree}>
|
||||
{children}
|
||||
</DocsLayout>
|
||||
);
|
||||
|
||||
@@ -148,7 +148,7 @@ export async function transformMDX<D extends BaseDoc>(
|
||||
|
||||
export function createDocSchema(z: typeof Zod) {
|
||||
return {
|
||||
title: z.string(),
|
||||
title: z.string().optional(),
|
||||
description: z.string().optional(),
|
||||
icon: z.string().optional(),
|
||||
full: z.boolean().optional(),
|
||||
|
||||
@@ -21,15 +21,17 @@ function pageToIndex(page: Page): AdvancedIndex {
|
||||
);
|
||||
}
|
||||
|
||||
const structuredData = page.data.structuredData as StructuredData;
|
||||
|
||||
return {
|
||||
title: page.data.title,
|
||||
title: page.data.title ?? page.file.name,
|
||||
description:
|
||||
'description' in page.data
|
||||
? (page.data.description as string)
|
||||
: undefined,
|
||||
url: page.url,
|
||||
id: page.url,
|
||||
structuredData: page.data.structuredData as StructuredData,
|
||||
structuredData,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -126,47 +126,44 @@ export interface LoaderOutput<Config extends LoaderConfig> {
|
||||
function indexPages(
|
||||
storage: Storage,
|
||||
getUrl: UrlFn,
|
||||
languages: string[] = [],
|
||||
i18n?: I18nConfig,
|
||||
): {
|
||||
// locale -> (slugs -> page[])
|
||||
i18n: Map<string, Map<string, Page>>;
|
||||
// (locale.slugs -> page[])
|
||||
pages: Map<string, Page>;
|
||||
|
||||
pathToPage: Map<string, Page>;
|
||||
pathToMeta: Map<string, Meta>;
|
||||
} {
|
||||
const i18n = new Map<string, Map<string, Page>>();
|
||||
const defaultLanguage = i18n?.defaultLanguage ?? '';
|
||||
const map = new Map<string, Page>();
|
||||
const pages = new Map<string, Page>();
|
||||
const metas = new Map<string, Meta>();
|
||||
|
||||
const defaultMap = new Map<string, Page>();
|
||||
for (const item of storage.list()) {
|
||||
if (item.format === 'meta') metas.set(item.file.path, fileToMeta(item));
|
||||
|
||||
i18n.set('', defaultMap);
|
||||
for (const file of storage.list()) {
|
||||
if (file.format === 'meta') metas.set(file.file.path, fileToMeta(file));
|
||||
if (item.format === 'page') {
|
||||
const page = fileToPage(item, getUrl, item.file.locale);
|
||||
pages.set(item.file.path, page);
|
||||
|
||||
if (file.format === 'page') {
|
||||
const page = fileToPage(file, getUrl, file.file.locale);
|
||||
pages.set(file.file.path, page);
|
||||
|
||||
if (file.file.locale) continue;
|
||||
defaultMap.set(page.slugs.join('/'), page);
|
||||
|
||||
for (const lang of languages) {
|
||||
const langMap = i18n.get(lang) ?? new Map<string, Page>();
|
||||
if (item.file.locale) continue;
|
||||
map.set(`${defaultLanguage}.${page.slugs.join('/')}`, page);
|
||||
if (!i18n) continue;
|
||||
|
||||
for (const lang of i18n.languages) {
|
||||
const localized = storage.read(
|
||||
`${file.file.flattenedPath}.${lang}`,
|
||||
`${item.file.flattenedPath}.${lang}`,
|
||||
'page',
|
||||
);
|
||||
const localizedPage = fileToPage(localized ?? file, getUrl, lang);
|
||||
langMap.set(localizedPage.slugs.join('/'), localizedPage);
|
||||
i18n.set(lang, langMap);
|
||||
|
||||
const localizedPage = fileToPage(localized ?? item, getUrl, lang);
|
||||
map.set(`${lang}.${localizedPage.slugs.join('/')}`, localizedPage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
i18n,
|
||||
pages: map,
|
||||
pathToPage: pages,
|
||||
pathToMeta: metas,
|
||||
};
|
||||
@@ -233,7 +230,7 @@ function createOutput(options: LoaderOptions): LoaderOutput<LoaderConfig> {
|
||||
getSlugs: slugsFn,
|
||||
},
|
||||
);
|
||||
const walker = indexPages(storage, getUrl, options.i18n?.languages);
|
||||
const walker = indexPages(storage, getUrl, options.i18n);
|
||||
const builder = createPageTreeBuilder();
|
||||
const pageTree =
|
||||
options.i18n === undefined
|
||||
@@ -255,24 +252,29 @@ function createOutput(options: LoaderOptions): LoaderOutput<LoaderConfig> {
|
||||
_i18n: options.i18n,
|
||||
pageTree: pageTree as LoaderOutput<LoaderConfig>['pageTree'],
|
||||
getPages(language = options.i18n?.defaultLanguage ?? '') {
|
||||
return Array.from(walker.i18n.get(language)?.values() ?? []);
|
||||
const pages: Page[] = [];
|
||||
|
||||
for (const key of walker.pages.keys()) {
|
||||
if (key.startsWith(`${language}.`)) pages.push(walker.pages.get(key)!);
|
||||
}
|
||||
|
||||
return pages;
|
||||
},
|
||||
getLanguages() {
|
||||
const list: LanguageEntry[] = [];
|
||||
if (!options.i18n) return list;
|
||||
|
||||
for (const [language, pages] of walker.i18n) {
|
||||
if (language === '') continue;
|
||||
|
||||
for (const language of options.i18n.languages) {
|
||||
list.push({
|
||||
language,
|
||||
pages: Array.from(pages.values()),
|
||||
pages: this.getPages(language),
|
||||
});
|
||||
}
|
||||
|
||||
return list;
|
||||
},
|
||||
getPage(slugs = [], language = options.i18n?.defaultLanguage ?? '') {
|
||||
return walker.i18n.get(language)?.get(slugs.join('/'));
|
||||
return walker.pages.get(`${language}.${slugs.join('/')}`);
|
||||
},
|
||||
getNodeMeta(node) {
|
||||
if (!node.$ref?.metaFile) return;
|
||||
@@ -304,7 +306,7 @@ function createOutput(options: LoaderOptions): LoaderOutput<LoaderConfig> {
|
||||
);
|
||||
}
|
||||
|
||||
return Array.from(walker.i18n.get('')?.values() ?? []).map((page) => ({
|
||||
return this.getPages().map((page) => ({
|
||||
[slug ?? 'slug']: page.slugs,
|
||||
}));
|
||||
},
|
||||
|
||||
@@ -222,7 +222,11 @@ function buildFolderNode(
|
||||
|
||||
const node: PageTree.Folder = {
|
||||
type: 'folder',
|
||||
name: metadata?.title ?? index?.name ?? pathToName(folder.file.name, true),
|
||||
name:
|
||||
metadata?.title ??
|
||||
index?.name ??
|
||||
// resolve folder groups like (group_name)
|
||||
pathToName(group.exec(folder.file.name)?.[1] ?? folder.file.name),
|
||||
icon: ctx.options.resolveIcon?.(metadata?.icon) ?? index?.icon,
|
||||
root: metadata?.root,
|
||||
defaultOpen: metadata?.defaultOpen,
|
||||
@@ -250,7 +254,7 @@ function buildFileNode(
|
||||
|
||||
const item: PageTree.Item = {
|
||||
type: 'page',
|
||||
name: localized.data.data.title,
|
||||
name: localized.data.data.title ?? pathToName(localized.file.name),
|
||||
icon: ctx.options.resolveIcon?.(localized.data.data.icon),
|
||||
url: ctx.options.getUrl(localized.data.slugs, ctx.lang),
|
||||
$ref: !ctx.options.noRef
|
||||
@@ -314,13 +318,10 @@ function findLocalizedFile<F extends File['format']>(
|
||||
* Get item name from file name
|
||||
*
|
||||
* @param name - file name
|
||||
* @param resolveGroup - resolve folder groups like (group_name)
|
||||
*/
|
||||
function pathToName(name: string, resolveGroup = false): string {
|
||||
const resolved = resolveGroup ? (group.exec(name)?.[1] ?? name) : name;
|
||||
|
||||
function pathToName(name: string): string {
|
||||
const result = [];
|
||||
for (const c of resolved) {
|
||||
for (const c of name) {
|
||||
if (result.length === 0) result.push(c.toLocaleUpperCase());
|
||||
else if (c === '-') result.push(' ');
|
||||
else result.push(c);
|
||||
|
||||
@@ -12,7 +12,7 @@ export interface MetaData {
|
||||
|
||||
export interface PageData {
|
||||
icon?: string | undefined;
|
||||
title: string;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- infer types
|
||||
|
||||
@@ -340,7 +340,129 @@ test('Internationalized Routing', () => {
|
||||
}
|
||||
`);
|
||||
|
||||
expect(result.getPages().length).toBe(2);
|
||||
expect(result.getPages()).toMatchInlineSnapshot(`
|
||||
[
|
||||
{
|
||||
"data": {
|
||||
"title": "Hello",
|
||||
},
|
||||
"file": {
|
||||
"dirname": "",
|
||||
"flattenedPath": "test",
|
||||
"locale": undefined,
|
||||
"name": "test",
|
||||
"path": "test.mdx",
|
||||
},
|
||||
"locale": "en",
|
||||
"slugs": [
|
||||
"test",
|
||||
],
|
||||
"url": "/en/test",
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"title": "Nested Page",
|
||||
},
|
||||
"file": {
|
||||
"dirname": "nested",
|
||||
"flattenedPath": "nested/test",
|
||||
"locale": undefined,
|
||||
"name": "test",
|
||||
"path": "nested/test.mdx",
|
||||
},
|
||||
"locale": "en",
|
||||
"slugs": [
|
||||
"nested",
|
||||
"test",
|
||||
],
|
||||
"url": "/en/nested/test",
|
||||
},
|
||||
]
|
||||
`);
|
||||
expect(result.getLanguages()).toMatchInlineSnapshot(`
|
||||
[
|
||||
{
|
||||
"language": "cn",
|
||||
"pages": [
|
||||
{
|
||||
"data": {
|
||||
"title": "Hello Chinese",
|
||||
},
|
||||
"file": {
|
||||
"dirname": "",
|
||||
"flattenedPath": "test.cn",
|
||||
"locale": "cn",
|
||||
"name": "test",
|
||||
"path": "test.cn.mdx",
|
||||
},
|
||||
"locale": "cn",
|
||||
"slugs": [
|
||||
"test",
|
||||
],
|
||||
"url": "/cn/test",
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"title": "Nested Page Chinese",
|
||||
},
|
||||
"file": {
|
||||
"dirname": "nested",
|
||||
"flattenedPath": "nested/test.cn",
|
||||
"locale": "cn",
|
||||
"name": "test",
|
||||
"path": "nested/test.cn.mdx",
|
||||
},
|
||||
"locale": "cn",
|
||||
"slugs": [
|
||||
"nested",
|
||||
"test",
|
||||
],
|
||||
"url": "/cn/nested/test",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"language": "en",
|
||||
"pages": [
|
||||
{
|
||||
"data": {
|
||||
"title": "Hello",
|
||||
},
|
||||
"file": {
|
||||
"dirname": "",
|
||||
"flattenedPath": "test",
|
||||
"locale": undefined,
|
||||
"name": "test",
|
||||
"path": "test.mdx",
|
||||
},
|
||||
"locale": "en",
|
||||
"slugs": [
|
||||
"test",
|
||||
],
|
||||
"url": "/en/test",
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"title": "Nested Page",
|
||||
},
|
||||
"file": {
|
||||
"dirname": "nested",
|
||||
"flattenedPath": "nested/test",
|
||||
"locale": undefined,
|
||||
"name": "test",
|
||||
"path": "nested/test.mdx",
|
||||
},
|
||||
"locale": "en",
|
||||
"slugs": [
|
||||
"nested",
|
||||
"test",
|
||||
],
|
||||
"url": "/en/nested/test",
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
`);
|
||||
expect(result.getPage(['test'])?.url).toBe('/en/test');
|
||||
expect(result.getPage(['test'], 'cn')?.url).toBe('/cn/test');
|
||||
});
|
||||
|
||||
@@ -10,7 +10,7 @@ export const metaSchema = z.object({
|
||||
});
|
||||
|
||||
export const frontmatterSchema = z.object({
|
||||
title: z.string(),
|
||||
title: z.string().optional(),
|
||||
description: z.string().optional(),
|
||||
icon: z.string().optional(),
|
||||
full: z.boolean().optional(),
|
||||
|
||||
Reference in New Issue
Block a user