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

View file

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

View file

@ -18,11 +18,17 @@ import {
PluginOptions,
Sidebar,
Order,
Metadata,
DocsMetadata,
LoadedContent,
SourceToPermalink,
PermalinkToId,
PermalinkToSidebar,
DocsSidebarItemCategory,
SidebarItemLink,
SidebarItemDoc,
SidebarItemCategory,
DocsSidebar,
DocsBaseMetadata,
MetadataRaw,
} from './types';
import {Configuration} from 'webpack';
@ -64,13 +70,15 @@ export default function pluginContentDocs(
return null;
}
const docsSidebars: Sidebar = loadSidebars(sidebarPath);
const loadedSidebars: Sidebar = loadSidebars(sidebarPath);
// Build the docs ordering such as next, previous, category and sidebar.
const order: Order = createOrder(docsSidebars);
const order: Order = createOrder(loadedSidebars);
// Prepare metadata container.
const docs: DocsMetadata = {};
const docsMetadataRaw: {
[id: string]: MetadataRaw;
} = {};
// Metadata for default docs files.
const docsFiles = await globby(include, {
@ -78,7 +86,7 @@ export default function pluginContentDocs(
});
await Promise.all(
docsFiles.map(async source => {
const metadata: Metadata = await processMetadata(
const metadata: MetadataRaw = await processMetadata(
source,
docsDir,
order,
@ -86,36 +94,99 @@ export default function pluginContentDocs(
routeBasePath,
siteDir,
);
docs[metadata.id] = metadata;
docsMetadataRaw[metadata.id] = metadata;
}),
);
// Get the titles of the previous and next ids so that we can use them.
Object.keys(docs).forEach(currentID => {
const previousID = idx(docs, [currentID, 'previous']);
// Construct docsMetadata
const docsMetadata: DocsMetadata = {};
Object.keys(docsMetadataRaw).forEach(currentID => {
let previous;
let next;
const previousID = idx(docsMetadataRaw, [currentID, 'previous']);
if (previousID) {
const previousTitle = idx(docs, [previousID, 'title']);
docs[currentID].previous_title = previousTitle || 'Previous';
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) {
const nextTitle = idx(docs, [nextID, 'title']);
docs[currentID].next_title = nextTitle || 'Next';
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 = {};
Object.values(docs).forEach(({id, source, permalink}) => {
sourceToPermalink[source] = permalink;
permalinkToId[permalink] = id;
});
const convertDocLink = (item: SidebarItemDoc): SidebarItemLink => {
const linkID = item.id;
const linkMetadata = docsMetadataRaw[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 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 {
docs,
docsMetadata,
docsDir,
docsSidebars,
sourceToPermalink,
permalinkToId,
permalinkToSidebar,
};
},
@ -128,7 +199,7 @@ export default function pluginContentDocs(
const {addRoute, createData} = actions;
const routes = await Promise.all(
Object.values(content.docs).map(async metadataItem => {
Object.values(content.docsMetadata).map(async metadataItem => {
const metadataPath = await createData(
`${docuHash(metadataItem.permalink)}.json`,
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([
(context.siteConfig as DocusaurusConfig).baseUrl,
routeBasePath,
]);
const docsMetadataPath = await createData(
const docsBaseMetadataPath = await createData(
`${docuHash(docsBaseRoute)}.json`,
JSON.stringify(content, null, 2),
JSON.stringify(docsBaseMetadata, null, 2),
);
addRoute({
@ -159,7 +235,7 @@ export default function pluginContentDocs(
component: docLayoutComponent,
routes,
modules: {
docsMetadata: docsMetadataPath,
docsMetadata: docsBaseMetadataPath,
},
});
},

View file

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

View file

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

View file

@ -8,6 +8,8 @@
import React from 'react';
import Head from '@docusaurus/Head';
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
import withBaseUrl from '@docusaurus/withBaseUrl';
import DocLegacyPaginator from '@theme/DocLegacyPaginator';
import styles from './styles.module.css';
@ -29,12 +31,35 @@ function Headings({headings, isChild}) {
}
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 (
<div>
<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>
<div className="padding-vert--lg">
<div className="row">
@ -49,10 +74,7 @@ function DocLegacyItem(props) {
</div>
</article>
<div className="margin-top--xl margin-bottom--lg">
<DocLegacyPaginator
docsMetadata={docsMetadata}
metadata={metadata}
/>
<DocLegacyPaginator metadata={metadata} />
</div>
</div>
</div>

View file

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

View file

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

View file

@ -16,35 +16,18 @@ const MOBILE_TOGGLE_SIZE = 24;
function DocLegacySidebar(props) {
const [showResponsiveSidebar, setShowResponsiveSidebar] = useState(false);
const {docsMetadata, sidebar} = props;
const {docsSidebars, sidebar} = props;
if (!sidebar) {
return null;
}
const thisSidebar = docsMetadata.docsSidebars[sidebar];
const thisSidebar = docsSidebars[sidebar];
if (!thisSidebar) {
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 => {
switch (item.type) {
case 'category':
@ -58,6 +41,7 @@ function DocLegacySidebar(props) {
);
case 'link':
default:
return (
<li className="menu__list-item" key={item.label}>
<Link
@ -71,10 +55,6 @@ function DocLegacySidebar(props) {
</Link>
</li>
);
case 'ref':
default:
return renderItem(convertDocLink(item));
}
};