refactor(v2): better docs metadata (#1815)

This commit is contained in:
Endi 2019-10-09 12:07:08 +08:00 committed by GitHub
parent a04bd440b4
commit e7ba8af6d9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 185 additions and 149 deletions

View file

@ -28,14 +28,15 @@ describe('loadDocs', () => {
path: pluginPath, path: pluginPath,
sidebarPath, sidebarPath,
}); });
const {docs: docsMetadata} = await plugin.loadContent(); const {docsMetadata} = await plugin.loadContent();
expect(docsMetadata.hello).toEqual({ expect(docsMetadata.hello).toEqual({
category: 'Guides',
id: 'hello', id: 'hello',
permalink: '/docs/hello', permalink: '/docs/hello',
previous: 'foo/baz', previous: {
previous_title: 'baz', title: 'baz',
permalink: '/docs/foo/baz',
},
sidebar: 'docs', sidebar: 'docs',
source: path.join('@site', pluginPath, 'hello.md'), source: path.join('@site', pluginPath, 'hello.md'),
title: 'Hello, World !', title: 'Hello, World !',
@ -43,10 +44,11 @@ describe('loadDocs', () => {
}); });
expect(docsMetadata['foo/bar']).toEqual({ expect(docsMetadata['foo/bar']).toEqual({
category: 'Test',
id: 'foo/bar', id: 'foo/bar',
next: 'foo/baz', next: {
next_title: 'baz', title: 'baz',
permalink: '/docs/foo/baz',
},
permalink: '/docs/foo/bar', permalink: '/docs/foo/bar',
sidebar: 'docs', sidebar: 'docs',
source: path.join('@site', pluginPath, 'foo', 'bar.md'), source: path.join('@site', pluginPath, 'foo', 'bar.md'),

View file

@ -43,36 +43,26 @@ describe('createOrder', () => {
}); });
expect(result).toEqual({ expect(result).toEqual({
doc1: { doc1: {
category: 'Category1',
subCategory: 'Subcategory 1',
next: 'doc2', next: 'doc2',
previous: undefined, previous: undefined,
sidebar: 'docs', sidebar: 'docs',
}, },
doc2: { doc2: {
category: 'Category1',
subCategory: 'Subcategory 2',
next: 'doc3', next: 'doc3',
previous: 'doc1', previous: 'doc1',
sidebar: 'docs', sidebar: 'docs',
}, },
doc3: { doc3: {
category: 'Category2',
subCategory: undefined,
next: 'doc4', next: 'doc4',
previous: 'doc2', previous: 'doc2',
sidebar: 'docs', sidebar: 'docs',
}, },
doc4: { doc4: {
category: 'Category2',
subCategory: undefined,
next: undefined, next: undefined,
previous: 'doc3', previous: 'doc3',
sidebar: 'docs', sidebar: 'docs',
}, },
doc5: { doc5: {
category: 'Category1',
subCategory: undefined,
next: undefined, next: undefined,
previous: undefined, previous: undefined,
sidebar: 'otherDocs', sidebar: 'otherDocs',
@ -103,36 +93,26 @@ describe('createOrder', () => {
}); });
expect(result).toEqual({ expect(result).toEqual({
doc1: { doc1: {
category: 'Category1',
subCategory: undefined,
next: 'doc2', next: 'doc2',
previous: undefined, previous: undefined,
sidebar: 'docs', sidebar: 'docs',
}, },
doc2: { doc2: {
category: 'Category1',
subCategory: undefined,
next: 'doc3', next: 'doc3',
previous: 'doc1', previous: 'doc1',
sidebar: 'docs', sidebar: 'docs',
}, },
doc3: { doc3: {
category: 'Category2',
subCategory: undefined,
next: 'doc4', next: 'doc4',
previous: 'doc2', previous: 'doc2',
sidebar: 'docs', sidebar: 'docs',
}, },
doc4: { doc4: {
category: 'Category2',
subCategory: undefined,
next: undefined, next: undefined,
previous: 'doc3', previous: 'doc3',
sidebar: 'docs', sidebar: 'docs',
}, },
doc5: { doc5: {
category: 'Category1',
subCategory: undefined,
next: undefined, next: undefined,
previous: undefined, previous: undefined,
sidebar: 'otherDocs', sidebar: 'otherDocs',
@ -164,22 +144,16 @@ describe('createOrder', () => {
}); });
expect(result).toEqual({ expect(result).toEqual({
doc1: { doc1: {
category: 'Category1',
subCategory: undefined,
next: undefined, next: undefined,
previous: undefined, previous: undefined,
sidebar: 'docs', sidebar: 'docs',
}, },
'version-1.2.3-doc1': { 'version-1.2.3-doc1': {
category: 'Category2',
subCategory: undefined,
next: undefined, next: undefined,
previous: 'version-1.2.3-doc2', previous: 'version-1.2.3-doc2',
sidebar: 'version-1.2.3-docs', sidebar: 'version-1.2.3-docs',
}, },
'version-1.2.3-doc2': { 'version-1.2.3-doc2': {
category: 'Category1',
subCategory: undefined,
next: 'version-1.2.3-doc1', next: 'version-1.2.3-doc1',
previous: undefined, previous: undefined,
sidebar: 'version-1.2.3-docs', sidebar: 'version-1.2.3-docs',
@ -227,22 +201,16 @@ describe('createOrder', () => {
}); });
expect(result).toEqual({ expect(result).toEqual({
doc2: { doc2: {
category: 'Category1',
subCategory: 'Subcategory 2',
next: 'doc3', next: 'doc3',
previous: undefined, previous: undefined,
sidebar: 'docs', sidebar: 'docs',
}, },
doc3: { doc3: {
category: 'Category2',
subCategory: undefined,
next: undefined, next: undefined,
previous: 'doc2', previous: 'doc2',
sidebar: 'docs', sidebar: 'docs',
}, },
doc5: { doc5: {
category: 'Category1',
subCategory: undefined,
next: undefined, next: undefined,
previous: undefined, previous: undefined,
sidebar: 'otherDocs', sidebar: 'otherDocs',

View file

@ -18,11 +18,17 @@ import {
PluginOptions, PluginOptions,
Sidebar, Sidebar,
Order, Order,
Metadata,
DocsMetadata, DocsMetadata,
LoadedContent, LoadedContent,
SourceToPermalink, SourceToPermalink,
PermalinkToId, PermalinkToSidebar,
DocsSidebarItemCategory,
SidebarItemLink,
SidebarItemDoc,
SidebarItemCategory,
DocsSidebar,
DocsBaseMetadata,
MetadataRaw,
} from './types'; } from './types';
import {Configuration} from 'webpack'; import {Configuration} from 'webpack';
@ -64,13 +70,15 @@ export default function pluginContentDocs(
return null; return null;
} }
const docsSidebars: Sidebar = loadSidebars(sidebarPath); const loadedSidebars: Sidebar = loadSidebars(sidebarPath);
// Build the docs ordering such as next, previous, category and sidebar. // Build the docs ordering such as next, previous, category and sidebar.
const order: Order = createOrder(docsSidebars); const order: Order = createOrder(loadedSidebars);
// Prepare metadata container. // Prepare metadata container.
const docs: DocsMetadata = {}; const docsMetadataRaw: {
[id: string]: MetadataRaw;
} = {};
// Metadata for default docs files. // Metadata for default docs files.
const docsFiles = await globby(include, { const docsFiles = await globby(include, {
@ -78,7 +86,7 @@ export default function pluginContentDocs(
}); });
await Promise.all( await Promise.all(
docsFiles.map(async source => { docsFiles.map(async source => {
const metadata: Metadata = await processMetadata( const metadata: MetadataRaw = await processMetadata(
source, source,
docsDir, docsDir,
order, order,
@ -86,36 +94,99 @@ export default function pluginContentDocs(
routeBasePath, routeBasePath,
siteDir, siteDir,
); );
docs[metadata.id] = metadata; docsMetadataRaw[metadata.id] = metadata;
}), }),
); );
// Get the titles of the previous and next ids so that we can use them. // Construct docsMetadata
Object.keys(docs).forEach(currentID => { const docsMetadata: DocsMetadata = {};
const previousID = idx(docs, [currentID, 'previous']); Object.keys(docsMetadataRaw).forEach(currentID => {
let previous;
let next;
const previousID = idx(docsMetadataRaw, [currentID, 'previous']);
if (previousID) { if (previousID) {
const previousTitle = idx(docs, [previousID, 'title']); previous = {
docs[currentID].previous_title = previousTitle || 'Previous'; title: idx(docsMetadataRaw, [previousID, 'title']) || 'Previous',
permalink: idx(docsMetadataRaw, [previousID, 'permalink']),
};
} }
const nextID = idx(docs, [currentID, 'next']); const nextID = idx(docsMetadataRaw, [currentID, 'next']);
if (nextID) { if (nextID) {
const nextTitle = idx(docs, [nextID, 'title']); next = {
docs[currentID].next_title = nextTitle || 'Next'; title: idx(docsMetadataRaw, [nextID, 'title']) || 'Next',
permalink: idx(docsMetadataRaw, [nextID, 'permalink']),
};
}
docsMetadata[currentID] = {
...docsMetadataRaw[currentID],
previous,
next,
};
});
const permalinkToSidebar: PermalinkToSidebar = {};
Object.values(docsMetadataRaw).forEach(({source, permalink, sidebar}) => {
sourceToPermalink[source] = permalink;
if (sidebar) {
permalinkToSidebar[permalink] = sidebar;
} }
}); });
const permalinkToId: PermalinkToId = {}; const convertDocLink = (item: SidebarItemDoc): SidebarItemLink => {
Object.values(docs).forEach(({id, source, permalink}) => { const linkID = item.id;
sourceToPermalink[source] = permalink; const linkMetadata = docsMetadataRaw[linkID];
permalinkToId[permalink] = id;
}); if (!linkMetadata) {
throw new Error(
`Improper sidebars file, document with id '${linkID}' not found.`,
);
}
return {
type: 'link',
label: linkMetadata.sidebar_label || linkMetadata.title,
href: linkMetadata.permalink,
};
};
const normalizeCategory = (
category: SidebarItemCategory,
): DocsSidebarItemCategory => {
const items = category.items.map(item => {
switch (item.type) {
case 'category':
return normalizeCategory(item as SidebarItemCategory);
case 'ref':
case 'doc':
return convertDocLink(item as SidebarItemDoc);
case 'link':
break;
default:
throw new Error(`Unknown sidebar item type: ${item.type}`);
}
return item as SidebarItemLink;
});
return {...category, items};
};
// Transform the sidebar so that all sidebar item will be in the form of 'link' or 'category' only
// This is what will be passed as props to the UI component
const docsSidebars: DocsSidebar = Object.entries(loadedSidebars).reduce(
(acc: DocsSidebar, [sidebarId, sidebarItemCategories]) => {
acc[sidebarId] = sidebarItemCategories.map(sidebarItemCategory =>
normalizeCategory(sidebarItemCategory),
);
return acc;
},
{},
);
return { return {
docs, docsMetadata,
docsDir, docsDir,
docsSidebars, docsSidebars,
sourceToPermalink, sourceToPermalink,
permalinkToId, permalinkToSidebar,
}; };
}, },
@ -128,7 +199,7 @@ export default function pluginContentDocs(
const {addRoute, createData} = actions; const {addRoute, createData} = actions;
const routes = await Promise.all( const routes = await Promise.all(
Object.values(content.docs).map(async metadataItem => { Object.values(content.docsMetadata).map(async metadataItem => {
const metadataPath = await createData( const metadataPath = await createData(
`${docuHash(metadataItem.permalink)}.json`, `${docuHash(metadataItem.permalink)}.json`,
JSON.stringify(metadataItem, null, 2), JSON.stringify(metadataItem, null, 2),
@ -145,13 +216,18 @@ export default function pluginContentDocs(
}), }),
); );
const docsBaseMetadata: DocsBaseMetadata = {
docsSidebars: content.docsSidebars,
permalinkToSidebar: content.permalinkToSidebar,
};
const docsBaseRoute = normalizeUrl([ const docsBaseRoute = normalizeUrl([
(context.siteConfig as DocusaurusConfig).baseUrl, (context.siteConfig as DocusaurusConfig).baseUrl,
routeBasePath, routeBasePath,
]); ]);
const docsMetadataPath = await createData( const docsBaseMetadataPath = await createData(
`${docuHash(docsBaseRoute)}.json`, `${docuHash(docsBaseRoute)}.json`,
JSON.stringify(content, null, 2), JSON.stringify(docsBaseMetadata, null, 2),
); );
addRoute({ addRoute({
@ -159,7 +235,7 @@ export default function pluginContentDocs(
component: docLayoutComponent, component: docLayoutComponent,
routes, routes,
modules: { modules: {
docsMetadata: docsMetadataPath, docsMetadata: docsBaseMetadataPath,
}, },
}); });
}, },

View file

@ -74,8 +74,6 @@ export default async function processMetadata(
const {id} = metadata; const {id} = metadata;
if (order[id]) { if (order[id]) {
metadata.sidebar = order[id].sidebar; metadata.sidebar = order[id].sidebar;
metadata.category = order[id].category;
metadata.subCategory = order[id].subCategory;
if (order[id].next) { if (order[id].next) {
metadata.next = order[id].next; metadata.next = order[id].next;
} }

View file

@ -21,26 +21,12 @@ export default function createOrder(allSidebars: Sidebar = {}): Order {
const sidebar = allSidebars[sidebarId]; const sidebar = allSidebars[sidebarId];
const ids: string[] = []; const ids: string[] = [];
const categoryOrder: (string | undefined)[] = []; const indexItems = ({items}: {items: SidebarItem[]}) => {
const subCategoryOrder: (string | undefined)[] = [];
const indexItems = ({
items,
categoryLabel,
subCategoryLabel,
}: {
items: SidebarItem[];
categoryLabel?: string;
subCategoryLabel?: string;
}) => {
items.forEach(item => { items.forEach(item => {
switch (item.type) { switch (item.type) {
case 'category': case 'category':
indexItems({ indexItems({
items: (item as SidebarItemCategory).items, items: (item as SidebarItemCategory).items,
categoryLabel:
categoryLabel || (item as SidebarItemCategory).label,
subCategoryLabel:
categoryLabel && (item as SidebarItemCategory).label,
}); });
break; break;
case 'ref': case 'ref':
@ -49,8 +35,6 @@ export default function createOrder(allSidebars: Sidebar = {}): Order {
break; break;
case 'doc': case 'doc':
ids.push((item as SidebarItemDoc).id); ids.push((item as SidebarItemDoc).id);
categoryOrder.push(categoryLabel);
subCategoryOrder.push(subCategoryLabel);
break; break;
default: default:
throw new Error( throw new Error(
@ -80,8 +64,6 @@ export default function createOrder(allSidebars: Sidebar = {}): Order {
previous, previous,
next, next,
sidebar: sidebarId, sidebar: sidebarId,
category: categoryOrder[i],
subCategory: subCategoryOrder[i],
}; };
} }
}); });

View file

@ -61,12 +61,20 @@ export interface Sidebar {
[sidebarId: string]: SidebarItemCategory[]; [sidebarId: string]: SidebarItemCategory[];
} }
export interface DocsSidebarItemCategory {
type: string;
label: string;
items: (SidebarItemLink | DocsSidebarItemCategory)[];
}
export interface DocsSidebar {
[sidebarId: string]: DocsSidebarItemCategory[];
}
export interface OrderMetadata { export interface OrderMetadata {
previous?: string; previous?: string;
next?: string; next?: string;
sidebar?: string; sidebar?: string;
category?: string;
subCategory?: string;
} }
export interface Order { export interface Order {
@ -79,11 +87,18 @@ export interface MetadataRaw extends OrderMetadata {
description: string; description: string;
source: string; source: string;
permalink: string; permalink: string;
sidebar_label?: string;
[key: string]: any;
} }
export interface Metadata extends MetadataRaw { export interface Paginator {
previous_title?: string; title: string;
next_title?: string; permalink: string;
}
export interface Metadata extends Omit<MetadataRaw, 'previous' | 'next'> {
previous?: Paginator;
next?: Paginator;
} }
export interface DocsMetadata { export interface DocsMetadata {
@ -94,14 +109,19 @@ export interface SourceToPermalink {
[source: string]: string; [source: string]: string;
} }
export interface PermalinkToId { export interface PermalinkToSidebar {
[permalink: string]: string; [permalink: string]: string;
} }
export interface LoadedContent { export interface LoadedContent {
docs: DocsMetadata; docsMetadata: DocsMetadata;
docsDir: string; docsDir: string;
docsSidebars: Sidebar; docsSidebars: Sidebar;
sourceToPermalink: SourceToPermalink; sourceToPermalink: SourceToPermalink;
permalinkToId: PermalinkToId; permalinkToSidebar: PermalinkToSidebar;
} }
export type DocsBaseMetadata = Pick<
LoadedContent,
'docsSidebars' | 'permalinkToSidebar'
>;

View file

@ -8,6 +8,8 @@
import React from 'react'; import React from 'react';
import Head from '@docusaurus/Head'; import Head from '@docusaurus/Head';
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
import withBaseUrl from '@docusaurus/withBaseUrl';
import DocLegacyPaginator from '@theme/DocLegacyPaginator'; import DocLegacyPaginator from '@theme/DocLegacyPaginator';
import styles from './styles.module.css'; import styles from './styles.module.css';
@ -29,12 +31,35 @@ function Headings({headings, isChild}) {
} }
function DocLegacyItem(props) { function DocLegacyItem(props) {
const {metadata, content: DocContent, docsMetadata} = props; const {siteConfig = {}} = useDocusaurusContext();
const {url: siteUrl} = siteConfig;
const {metadata, content: DocContent} = props;
const {description, title, permalink, image: metaImage} = metadata;
return ( return (
<div> <div>
<Head> <Head>
{metadata && metadata.title && <title>{metadata.title}</title>} {title && <title>{title}</title>}
{description && <meta name="description" content={description} />}
{description && (
<meta property="og:description" content={description} />
)}
{metaImage && (
<meta
property="og:image"
content={siteUrl + withBaseUrl(metaImage)}
/>
)}
{metaImage && (
<meta
property="twitter:image"
content={siteUrl + withBaseUrl(metaImage)}
/>
)}
{metaImage && (
<meta name="twitter:image:alt" content={`Image for ${title}`} />
)}
{permalink && <meta property="og:url" content={siteUrl + permalink} />}
</Head> </Head>
<div className="padding-vert--lg"> <div className="padding-vert--lg">
<div className="row"> <div className="row">
@ -49,10 +74,7 @@ function DocLegacyItem(props) {
</div> </div>
</article> </article>
<div className="margin-top--xl margin-bottom--lg"> <div className="margin-top--xl margin-bottom--lg">
<DocLegacyPaginator <DocLegacyPaginator metadata={metadata} />
docsMetadata={docsMetadata}
metadata={metadata}
/>
</div> </div>
</div> </div>
</div> </div>

View file

@ -15,27 +15,20 @@ import MDXComponents from '@theme/MDXComponents';
function DocLegacyPage(props) { function DocLegacyPage(props) {
const {route, docsMetadata, location} = props; const {route, docsMetadata, location} = props;
const {permalinkToId} = docsMetadata; const {permalinkToSidebar, docsSidebars} = docsMetadata;
const id = const sidebar =
permalinkToId[location.pathname] || permalinkToSidebar[location.pathname] ||
permalinkToId[location.pathname.replace(/\/$/, '')]; permalinkToSidebar[location.pathname.replace(/\/$/, '')];
const metadata = docsMetadata.docs[id] || {};
const {sidebar, description, title, permalink, image} = metadata;
return ( return (
<Layout <Layout noFooter>
noFooter
description={description}
title={title}
image={image}
permalink={permalink}>
<div className="container container--fluid"> <div className="container container--fluid">
<div className="row"> <div className="row">
<div className="col col--3"> <div className="col col--3">
<DocLegacySidebar docsMetadata={docsMetadata} sidebar={sidebar} /> <DocLegacySidebar docsSidebars={docsSidebars} sidebar={sidebar} />
</div> </div>
<main className="col"> <main className="col">
<MDXProvider components={MDXComponents}> <MDXProvider components={MDXComponents}>
{renderRoutes(route.routes, {docsMetadata})} {renderRoutes(route.routes)}
</MDXProvider> </MDXProvider>
</main> </main>
</div> </div>

View file

@ -9,33 +9,28 @@ import React from 'react';
import Link from '@docusaurus/Link'; import Link from '@docusaurus/Link';
function DocLegacyPaginator(props) { function DocLegacyPaginator(props) {
const { const {metadata} = props;
docsMetadata: {docs},
metadata,
} = props;
return ( return (
<nav className="pagination-nav"> <nav className="pagination-nav">
<div className="pagination-nav__item"> <div className="pagination-nav__item">
{metadata.previous && docs[metadata.previous] && ( {metadata.previous && (
<Link <Link
className="pagination-nav__link" className="pagination-nav__link"
to={docs[metadata.previous].permalink}> to={metadata.previous.permalink}>
<h5 className="pagination-nav__link--sublabel">Previous</h5> <h5 className="pagination-nav__link--sublabel">Previous</h5>
<h4 className="pagination-nav__link--label"> <h4 className="pagination-nav__link--label">
&laquo; {metadata.previous_title} &laquo; {metadata.previous.title}
</h4> </h4>
</Link> </Link>
)} )}
</div> </div>
<div className="pagination-nav__item pagination-nav__item--next"> <div className="pagination-nav__item pagination-nav__item--next">
{metadata.next && docs[metadata.next] && ( {metadata.next && (
<Link <Link className="pagination-nav__link" to={metadata.next.permalink}>
className="pagination-nav__link"
to={docs[metadata.next].permalink}>
<h5 className="pagination-nav__link--sublabel">Next</h5> <h5 className="pagination-nav__link--sublabel">Next</h5>
<h4 className="pagination-nav__link--label"> <h4 className="pagination-nav__link--label">
{metadata.next_title} &raquo; {metadata.next.title} &raquo;
</h4> </h4>
</Link> </Link>
)} )}

View file

@ -16,35 +16,18 @@ const MOBILE_TOGGLE_SIZE = 24;
function DocLegacySidebar(props) { function DocLegacySidebar(props) {
const [showResponsiveSidebar, setShowResponsiveSidebar] = useState(false); const [showResponsiveSidebar, setShowResponsiveSidebar] = useState(false);
const {docsMetadata, sidebar} = props; const {docsSidebars, sidebar} = props;
if (!sidebar) { if (!sidebar) {
return null; return null;
} }
const thisSidebar = docsMetadata.docsSidebars[sidebar]; const thisSidebar = docsSidebars[sidebar];
if (!thisSidebar) { if (!thisSidebar) {
throw new Error(`Can not find ${sidebar} config`); throw new Error(`Can not find ${sidebar} config`);
} }
const convertDocLink = item => {
const linkID = item.id;
const linkMetadata = docsMetadata.docs[linkID];
if (!linkMetadata) {
throw new Error(
`Improper sidebars file, document with id '${linkID}' not found.`,
);
}
return {
type: 'link',
label: linkMetadata.sidebar_label || linkMetadata.title,
href: linkMetadata.permalink,
};
};
const renderItem = item => { const renderItem = item => {
switch (item.type) { switch (item.type) {
case 'category': case 'category':
@ -58,6 +41,7 @@ function DocLegacySidebar(props) {
); );
case 'link': case 'link':
default:
return ( return (
<li className="menu__list-item" key={item.label}> <li className="menu__list-item" key={item.label}>
<Link <Link
@ -71,10 +55,6 @@ function DocLegacySidebar(props) {
</Link> </Link>
</li> </li>
); );
case 'ref':
default:
return renderItem(convertDocLink(item));
} }
}; };