Core: Support pages without title

This commit is contained in:
Fuma Nama
2025-02-10 19:27:41 +08:00
parent e9225e1c91
commit a06af26ed1
9 changed files with 178 additions and 44 deletions
+6
View File
@@ -0,0 +1,6 @@
---
'fumadocs-core': patch
'@fumadocs/content-collections': patch
---
Support pages without `title`
+2 -1
View File
@@ -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,
};
}
+32 -30
View File
@@ -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);
+1 -1
View File
@@ -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
+123 -1
View File
@@ -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');
});
+1 -1
View File
@@ -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(),