mirror of
https://github.com/facebook/docusaurus.git
synced 2025-05-10 15:47:23 +02:00
refactor(v2): change plugin api (#1547)
* misc(v2): new plugin format example * refactor(v2): make all plugins a function returning objects * misc: add CHANGELOG * misc(v2): update CHANGELOG * misc(v2): fix tests * misc(v2): convert swizzle command * misc(v2): convert sitemap back to commonjs
This commit is contained in:
parent
9feb7b2c64
commit
6a814ac64a
18 changed files with 709 additions and 725 deletions
|
@ -31,336 +31,334 @@ const DEFAULT_OPTIONS = {
|
||||||
blogTagsPostsComponent: '@theme/BlogTagsPostsPage',
|
blogTagsPostsComponent: '@theme/BlogTagsPostsPage',
|
||||||
};
|
};
|
||||||
|
|
||||||
class DocusaurusPluginContentBlog {
|
module.exports = function(context, opts) {
|
||||||
constructor(context, opts) {
|
const options = {...DEFAULT_OPTIONS, ...opts};
|
||||||
this.options = {...DEFAULT_OPTIONS, ...opts};
|
const contentPath = path.resolve(context.siteDir, options.path);
|
||||||
this.context = context;
|
|
||||||
this.contentPath = path.resolve(this.context.siteDir, this.options.path);
|
|
||||||
}
|
|
||||||
|
|
||||||
getName() {
|
return {
|
||||||
return 'docusaurus-plugin-content-blog';
|
name: 'docusaurus-plugin-content-blog',
|
||||||
}
|
|
||||||
|
|
||||||
getPathsToWatch() {
|
getPathsToWatch() {
|
||||||
const {include = []} = this.options;
|
const {include = []} = options;
|
||||||
const globPattern = include.map(
|
const globPattern = include.map(pattern => `${contentPath}/${pattern}`);
|
||||||
pattern => `${this.contentPath}/${pattern}`,
|
return [...globPattern];
|
||||||
);
|
},
|
||||||
return [...globPattern];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetches blog contents and returns metadata for the necessary routes.
|
// Fetches blog contents and returns metadata for the necessary routes.
|
||||||
async loadContent() {
|
async loadContent() {
|
||||||
const {postsPerPage, include, routeBasePath} = this.options;
|
const {postsPerPage, include, routeBasePath} = options;
|
||||||
const {siteConfig} = this.context;
|
const {siteConfig} = context;
|
||||||
const blogDir = this.contentPath;
|
const blogDir = contentPath;
|
||||||
|
|
||||||
if (!fs.existsSync(blogDir)) {
|
if (!fs.existsSync(blogDir)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const {baseUrl} = siteConfig;
|
const {baseUrl} = siteConfig;
|
||||||
const blogFiles = await globby(include, {
|
const blogFiles = await globby(include, {
|
||||||
cwd: blogDir,
|
cwd: blogDir,
|
||||||
});
|
|
||||||
|
|
||||||
const blogPosts = [];
|
|
||||||
|
|
||||||
await Promise.all(
|
|
||||||
blogFiles.map(async relativeSource => {
|
|
||||||
const source = path.join(blogDir, relativeSource);
|
|
||||||
|
|
||||||
const blogFileName = path.basename(relativeSource);
|
|
||||||
// Extract, YYYY, MM, DD from the file name.
|
|
||||||
const filePathDateArr = blogFileName.split('-');
|
|
||||||
const date = new Date(
|
|
||||||
`${filePathDateArr[0]}-${filePathDateArr[1]}-${
|
|
||||||
filePathDateArr[2]
|
|
||||||
}T06:00:00.000Z`,
|
|
||||||
);
|
|
||||||
|
|
||||||
const fileString = await fs.readFile(source, 'utf-8');
|
|
||||||
const {frontMatter, excerpt} = parse(fileString);
|
|
||||||
|
|
||||||
blogPosts.push({
|
|
||||||
id: blogFileName,
|
|
||||||
metadata: {
|
|
||||||
permalink: normalizeUrl([
|
|
||||||
baseUrl,
|
|
||||||
routeBasePath,
|
|
||||||
fileToUrl(blogFileName),
|
|
||||||
]),
|
|
||||||
source,
|
|
||||||
description: frontMatter.description || excerpt,
|
|
||||||
date,
|
|
||||||
tags: frontMatter.tags,
|
|
||||||
title: frontMatter.title || blogFileName,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
blogPosts.sort((a, b) => b.metadata.date - a.metadata.date);
|
|
||||||
|
|
||||||
// Blog pagination routes.
|
|
||||||
// Example: `/blog`, `/blog/page/1`, `/blog/page/2`
|
|
||||||
const totalCount = blogPosts.length;
|
|
||||||
const numberOfPages = Math.ceil(totalCount / postsPerPage);
|
|
||||||
const basePageUrl = normalizeUrl([baseUrl, routeBasePath]);
|
|
||||||
|
|
||||||
const blogListPaginated = [];
|
|
||||||
|
|
||||||
function blogPaginationPermalink(page) {
|
|
||||||
return page > 0
|
|
||||||
? normalizeUrl([basePageUrl, `page/${page + 1}`])
|
|
||||||
: basePageUrl;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let page = 0; page < numberOfPages; page += 1) {
|
|
||||||
blogListPaginated.push({
|
|
||||||
metadata: {
|
|
||||||
permalink: blogPaginationPermalink(page),
|
|
||||||
page: page + 1,
|
|
||||||
postsPerPage,
|
|
||||||
totalPages: numberOfPages,
|
|
||||||
totalCount,
|
|
||||||
previousPage: page !== 0 ? blogPaginationPermalink(page - 1) : null,
|
|
||||||
nextPage:
|
|
||||||
page < numberOfPages - 1 ? blogPaginationPermalink(page + 1) : null,
|
|
||||||
},
|
|
||||||
items: blogPosts
|
|
||||||
.slice(page * postsPerPage, (page + 1) * postsPerPage)
|
|
||||||
.map(item => item.id),
|
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
const blogTags = {};
|
const blogPosts = [];
|
||||||
const tagsPath = normalizeUrl([basePageUrl, 'tags']);
|
|
||||||
blogPosts.forEach(blogPost => {
|
await Promise.all(
|
||||||
const {tags} = blogPost.metadata;
|
blogFiles.map(async relativeSource => {
|
||||||
if (!tags || tags.length === 0) {
|
const source = path.join(blogDir, relativeSource);
|
||||||
// TODO: Extract tags out into a separate plugin.
|
|
||||||
|
const blogFileName = path.basename(relativeSource);
|
||||||
|
// Extract, YYYY, MM, DD from the file name.
|
||||||
|
const filePathDateArr = blogFileName.split('-');
|
||||||
|
const date = new Date(
|
||||||
|
`${filePathDateArr[0]}-${filePathDateArr[1]}-${
|
||||||
|
filePathDateArr[2]
|
||||||
|
}T06:00:00.000Z`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const fileString = await fs.readFile(source, 'utf-8');
|
||||||
|
const {frontMatter, excerpt} = parse(fileString);
|
||||||
|
|
||||||
|
blogPosts.push({
|
||||||
|
id: blogFileName,
|
||||||
|
metadata: {
|
||||||
|
permalink: normalizeUrl([
|
||||||
|
baseUrl,
|
||||||
|
routeBasePath,
|
||||||
|
fileToUrl(blogFileName),
|
||||||
|
]),
|
||||||
|
source,
|
||||||
|
description: frontMatter.description || excerpt,
|
||||||
|
date,
|
||||||
|
tags: frontMatter.tags,
|
||||||
|
title: frontMatter.title || blogFileName,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
blogPosts.sort((a, b) => b.metadata.date - a.metadata.date);
|
||||||
|
|
||||||
|
// Blog pagination routes.
|
||||||
|
// Example: `/blog`, `/blog/page/1`, `/blog/page/2`
|
||||||
|
const totalCount = blogPosts.length;
|
||||||
|
const numberOfPages = Math.ceil(totalCount / postsPerPage);
|
||||||
|
const basePageUrl = normalizeUrl([baseUrl, routeBasePath]);
|
||||||
|
|
||||||
|
const blogListPaginated = [];
|
||||||
|
|
||||||
|
function blogPaginationPermalink(page) {
|
||||||
|
return page > 0
|
||||||
|
? normalizeUrl([basePageUrl, `page/${page + 1}`])
|
||||||
|
: basePageUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let page = 0; page < numberOfPages; page += 1) {
|
||||||
|
blogListPaginated.push({
|
||||||
|
metadata: {
|
||||||
|
permalink: blogPaginationPermalink(page),
|
||||||
|
page: page + 1,
|
||||||
|
postsPerPage,
|
||||||
|
totalPages: numberOfPages,
|
||||||
|
totalCount,
|
||||||
|
previousPage: page !== 0 ? blogPaginationPermalink(page - 1) : null,
|
||||||
|
nextPage:
|
||||||
|
page < numberOfPages - 1
|
||||||
|
? blogPaginationPermalink(page + 1)
|
||||||
|
: null,
|
||||||
|
},
|
||||||
|
items: blogPosts
|
||||||
|
.slice(page * postsPerPage, (page + 1) * postsPerPage)
|
||||||
|
.map(item => item.id),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const blogTags = {};
|
||||||
|
const tagsPath = normalizeUrl([basePageUrl, 'tags']);
|
||||||
|
blogPosts.forEach(blogPost => {
|
||||||
|
const {tags} = blogPost.metadata;
|
||||||
|
if (!tags || tags.length === 0) {
|
||||||
|
// TODO: Extract tags out into a separate plugin.
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
blogPost.metadata.tags = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line no-param-reassign
|
// eslint-disable-next-line no-param-reassign
|
||||||
blogPost.metadata.tags = [];
|
blogPost.metadata.tags = tags.map(tag => {
|
||||||
|
const normalizedTag = _.kebabCase(tag);
|
||||||
|
const permalink = normalizeUrl([tagsPath, normalizedTag]);
|
||||||
|
if (!blogTags[normalizedTag]) {
|
||||||
|
blogTags[normalizedTag] = {
|
||||||
|
name: tag.toLowerCase(), // Will only use the name of the first occurrence of the tag.
|
||||||
|
items: [],
|
||||||
|
permalink,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
blogTags[normalizedTag].items.push(blogPost.id);
|
||||||
|
|
||||||
|
return {
|
||||||
|
label: tag,
|
||||||
|
permalink,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const blogTagsListPath =
|
||||||
|
Object.keys(blogTags).length > 0 ? tagsPath : null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
blogPosts,
|
||||||
|
blogListPaginated,
|
||||||
|
blogTags,
|
||||||
|
blogTagsListPath,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
async contentLoaded({content: blogContents, actions}) {
|
||||||
|
if (!blogContents) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line no-param-reassign
|
const {
|
||||||
blogPost.metadata.tags = tags.map(tag => {
|
blogListComponent,
|
||||||
const normalizedTag = _.kebabCase(tag);
|
blogPostComponent,
|
||||||
const permalink = normalizeUrl([tagsPath, normalizedTag]);
|
blogTagsListComponent,
|
||||||
if (!blogTags[normalizedTag]) {
|
blogTagsPostsComponent,
|
||||||
blogTags[normalizedTag] = {
|
} = options;
|
||||||
name: tag.toLowerCase(), // Will only use the name of the first occurrence of the tag.
|
|
||||||
items: [],
|
const {addRoute, createData} = actions;
|
||||||
permalink,
|
const {
|
||||||
|
blogPosts,
|
||||||
|
blogListPaginated,
|
||||||
|
blogTags,
|
||||||
|
blogTagsListPath,
|
||||||
|
} = blogContents;
|
||||||
|
|
||||||
|
const blogItemsToModules = {};
|
||||||
|
// Create routes for blog entries.
|
||||||
|
const blogItems = await Promise.all(
|
||||||
|
blogPosts.map(async blogPost => {
|
||||||
|
const {id, metadata} = blogPost;
|
||||||
|
const {permalink} = metadata;
|
||||||
|
const metadataPath = await createData(
|
||||||
|
`${docuHash(permalink)}.json`,
|
||||||
|
JSON.stringify(metadata, null, 2),
|
||||||
|
);
|
||||||
|
const temp = {
|
||||||
|
metadata,
|
||||||
|
metadataPath,
|
||||||
};
|
};
|
||||||
}
|
|
||||||
|
|
||||||
blogTags[normalizedTag].items.push(blogPost.id);
|
blogItemsToModules[id] = temp;
|
||||||
|
return temp;
|
||||||
return {
|
}),
|
||||||
label: tag,
|
|
||||||
permalink,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const blogTagsListPath = Object.keys(blogTags).length > 0 ? tagsPath : null;
|
|
||||||
|
|
||||||
return {
|
|
||||||
blogPosts,
|
|
||||||
blogListPaginated,
|
|
||||||
blogTags,
|
|
||||||
blogTagsListPath,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async contentLoaded({content: blogContents, actions}) {
|
|
||||||
if (!blogContents) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const {
|
|
||||||
blogListComponent,
|
|
||||||
blogPostComponent,
|
|
||||||
blogTagsListComponent,
|
|
||||||
blogTagsPostsComponent,
|
|
||||||
} = this.options;
|
|
||||||
|
|
||||||
const {addRoute, createData} = actions;
|
|
||||||
const {
|
|
||||||
blogPosts,
|
|
||||||
blogListPaginated,
|
|
||||||
blogTags,
|
|
||||||
blogTagsListPath,
|
|
||||||
} = blogContents;
|
|
||||||
|
|
||||||
const blogItemsToModules = {};
|
|
||||||
// Create routes for blog entries.
|
|
||||||
const blogItems = await Promise.all(
|
|
||||||
blogPosts.map(async blogPost => {
|
|
||||||
const {id, metadata} = blogPost;
|
|
||||||
const {permalink} = metadata;
|
|
||||||
const metadataPath = await createData(
|
|
||||||
`${docuHash(permalink)}.json`,
|
|
||||||
JSON.stringify(metadata, null, 2),
|
|
||||||
);
|
|
||||||
const temp = {
|
|
||||||
metadata,
|
|
||||||
metadataPath,
|
|
||||||
};
|
|
||||||
|
|
||||||
blogItemsToModules[id] = temp;
|
|
||||||
return temp;
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
blogItems.forEach((blogItem, index) => {
|
|
||||||
const prevItem = index > 0 ? blogItems[index - 1] : null;
|
|
||||||
const nextItem =
|
|
||||||
index < blogItems.length - 1 ? blogItems[index + 1] : null;
|
|
||||||
const {metadata, metadataPath} = blogItem;
|
|
||||||
const {source, permalink} = metadata;
|
|
||||||
|
|
||||||
addRoute({
|
|
||||||
path: permalink,
|
|
||||||
component: blogPostComponent,
|
|
||||||
exact: true,
|
|
||||||
modules: {
|
|
||||||
content: source,
|
|
||||||
metadata: metadataPath,
|
|
||||||
prevItem: prevItem && prevItem.metadataPath,
|
|
||||||
nextItem: nextItem && nextItem.metadataPath,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create routes for blog's paginated list entries.
|
|
||||||
await Promise.all(
|
|
||||||
blogListPaginated.map(async listPage => {
|
|
||||||
const {metadata, items} = listPage;
|
|
||||||
const {permalink} = metadata;
|
|
||||||
const pageMetadataPath = await createData(
|
|
||||||
`${docuHash(permalink)}.json`,
|
|
||||||
JSON.stringify(metadata, null, 2),
|
|
||||||
);
|
|
||||||
|
|
||||||
addRoute({
|
|
||||||
path: permalink,
|
|
||||||
component: blogListComponent,
|
|
||||||
exact: true,
|
|
||||||
modules: {
|
|
||||||
items: items.map(postID => {
|
|
||||||
const {metadata: postMetadata, metadataPath} = blogItemsToModules[
|
|
||||||
postID
|
|
||||||
];
|
|
||||||
// To tell routes.js this is an import and not a nested object to recurse.
|
|
||||||
return {
|
|
||||||
content: {
|
|
||||||
__import: true,
|
|
||||||
path: postMetadata.source,
|
|
||||||
query: {
|
|
||||||
truncated: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
metadata: metadataPath,
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
metadata: pageMetadataPath,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Tags.
|
|
||||||
const tagsModule = {};
|
|
||||||
|
|
||||||
await Promise.all(
|
|
||||||
Object.keys(blogTags).map(async tag => {
|
|
||||||
const {name, items, permalink} = blogTags[tag];
|
|
||||||
|
|
||||||
tagsModule[tag] = {
|
|
||||||
allTagsPath: blogTagsListPath,
|
|
||||||
slug: tag,
|
|
||||||
name,
|
|
||||||
count: items.length,
|
|
||||||
permalink,
|
|
||||||
};
|
|
||||||
|
|
||||||
const tagsMetadataPath = await createData(
|
|
||||||
`${docuHash(permalink)}.json`,
|
|
||||||
JSON.stringify(tagsModule[tag], null, 2),
|
|
||||||
);
|
|
||||||
|
|
||||||
addRoute({
|
|
||||||
path: permalink,
|
|
||||||
component: blogTagsPostsComponent,
|
|
||||||
exact: true,
|
|
||||||
modules: {
|
|
||||||
items: items.map(postID => {
|
|
||||||
const {metadata: postMetadata, metadataPath} = blogItemsToModules[
|
|
||||||
postID
|
|
||||||
];
|
|
||||||
return {
|
|
||||||
content: {
|
|
||||||
__import: true,
|
|
||||||
path: postMetadata.source,
|
|
||||||
query: {
|
|
||||||
truncated: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
metadata: metadataPath,
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
metadata: tagsMetadataPath,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Only create /tags page if there are tags.
|
|
||||||
if (Object.keys(blogTags).length > 0) {
|
|
||||||
const tagsListPath = await createData(
|
|
||||||
`${docuHash(`${blogTagsListPath}-tags`)}.json`,
|
|
||||||
JSON.stringify(tagsModule, null, 2),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
addRoute({
|
blogItems.forEach((blogItem, index) => {
|
||||||
path: blogTagsListPath,
|
const prevItem = index > 0 ? blogItems[index - 1] : null;
|
||||||
component: blogTagsListComponent,
|
const nextItem =
|
||||||
exact: true,
|
index < blogItems.length - 1 ? blogItems[index + 1] : null;
|
||||||
modules: {
|
const {metadata, metadataPath} = blogItem;
|
||||||
tags: tagsListPath,
|
const {source, permalink} = metadata;
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getThemePath() {
|
addRoute({
|
||||||
return path.resolve(__dirname, './theme');
|
path: permalink,
|
||||||
}
|
component: blogPostComponent,
|
||||||
|
exact: true,
|
||||||
configureWebpack(config, isServer, {getBabelLoader, getCacheLoader}) {
|
modules: {
|
||||||
return {
|
content: source,
|
||||||
module: {
|
metadata: metadataPath,
|
||||||
rules: [
|
prevItem: prevItem && prevItem.metadataPath,
|
||||||
{
|
nextItem: nextItem && nextItem.metadataPath,
|
||||||
test: /(\.mdx?)$/,
|
|
||||||
include: [this.contentPath],
|
|
||||||
use: [
|
|
||||||
getCacheLoader(isServer),
|
|
||||||
getBabelLoader(isServer),
|
|
||||||
'@docusaurus/mdx-loader',
|
|
||||||
{
|
|
||||||
loader: path.resolve(__dirname, './markdownLoader.js'),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
],
|
});
|
||||||
},
|
});
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = DocusaurusPluginContentBlog;
|
// Create routes for blog's paginated list entries.
|
||||||
|
await Promise.all(
|
||||||
|
blogListPaginated.map(async listPage => {
|
||||||
|
const {metadata, items} = listPage;
|
||||||
|
const {permalink} = metadata;
|
||||||
|
const pageMetadataPath = await createData(
|
||||||
|
`${docuHash(permalink)}.json`,
|
||||||
|
JSON.stringify(metadata, null, 2),
|
||||||
|
);
|
||||||
|
|
||||||
|
addRoute({
|
||||||
|
path: permalink,
|
||||||
|
component: blogListComponent,
|
||||||
|
exact: true,
|
||||||
|
modules: {
|
||||||
|
items: items.map(postID => {
|
||||||
|
const {
|
||||||
|
metadata: postMetadata,
|
||||||
|
metadataPath,
|
||||||
|
} = blogItemsToModules[postID];
|
||||||
|
// To tell routes.js this is an import and not a nested object to recurse.
|
||||||
|
return {
|
||||||
|
content: {
|
||||||
|
__import: true,
|
||||||
|
path: postMetadata.source,
|
||||||
|
query: {
|
||||||
|
truncated: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
metadata: metadataPath,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
metadata: pageMetadataPath,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Tags.
|
||||||
|
const tagsModule = {};
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
Object.keys(blogTags).map(async tag => {
|
||||||
|
const {name, items, permalink} = blogTags[tag];
|
||||||
|
|
||||||
|
tagsModule[tag] = {
|
||||||
|
allTagsPath: blogTagsListPath,
|
||||||
|
slug: tag,
|
||||||
|
name,
|
||||||
|
count: items.length,
|
||||||
|
permalink,
|
||||||
|
};
|
||||||
|
|
||||||
|
const tagsMetadataPath = await createData(
|
||||||
|
`${docuHash(permalink)}.json`,
|
||||||
|
JSON.stringify(tagsModule[tag], null, 2),
|
||||||
|
);
|
||||||
|
|
||||||
|
addRoute({
|
||||||
|
path: permalink,
|
||||||
|
component: blogTagsPostsComponent,
|
||||||
|
exact: true,
|
||||||
|
modules: {
|
||||||
|
items: items.map(postID => {
|
||||||
|
const {
|
||||||
|
metadata: postMetadata,
|
||||||
|
metadataPath,
|
||||||
|
} = blogItemsToModules[postID];
|
||||||
|
return {
|
||||||
|
content: {
|
||||||
|
__import: true,
|
||||||
|
path: postMetadata.source,
|
||||||
|
query: {
|
||||||
|
truncated: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
metadata: metadataPath,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
metadata: tagsMetadataPath,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Only create /tags page if there are tags.
|
||||||
|
if (Object.keys(blogTags).length > 0) {
|
||||||
|
const tagsListPath = await createData(
|
||||||
|
`${docuHash(`${blogTagsListPath}-tags`)}.json`,
|
||||||
|
JSON.stringify(tagsModule, null, 2),
|
||||||
|
);
|
||||||
|
|
||||||
|
addRoute({
|
||||||
|
path: blogTagsListPath,
|
||||||
|
component: blogTagsListComponent,
|
||||||
|
exact: true,
|
||||||
|
modules: {
|
||||||
|
tags: tagsListPath,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
getThemePath() {
|
||||||
|
return path.resolve(__dirname, './theme');
|
||||||
|
},
|
||||||
|
|
||||||
|
configureWebpack(config, isServer, {getBabelLoader, getCacheLoader}) {
|
||||||
|
return {
|
||||||
|
module: {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
test: /(\.mdx?)$/,
|
||||||
|
include: [contentPath],
|
||||||
|
use: [
|
||||||
|
getCacheLoader(isServer),
|
||||||
|
getBabelLoader(isServer),
|
||||||
|
'@docusaurus/mdx-loader',
|
||||||
|
{
|
||||||
|
loader: path.resolve(__dirname, './markdownLoader.js'),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import DocusaurusPluginContentDocs from '../index';
|
import pluginContentDocs from '../index';
|
||||||
|
|
||||||
describe('loadDocs', () => {
|
describe('loadDocs', () => {
|
||||||
test('simple website', async () => {
|
test('simple website', async () => {
|
||||||
|
@ -17,7 +17,7 @@ describe('loadDocs', () => {
|
||||||
url: 'https://docusaurus.io',
|
url: 'https://docusaurus.io',
|
||||||
};
|
};
|
||||||
const sidebarPath = path.join(siteDir, 'sidebars.json');
|
const sidebarPath = path.join(siteDir, 'sidebars.json');
|
||||||
const plugin = new DocusaurusPluginContentDocs(
|
const plugin = pluginContentDocs(
|
||||||
{
|
{
|
||||||
siteDir,
|
siteDir,
|
||||||
siteConfig,
|
siteConfig,
|
||||||
|
@ -41,6 +41,7 @@ describe('loadDocs', () => {
|
||||||
title: 'Hello, World !',
|
title: 'Hello, World !',
|
||||||
description: `Hi, Endilie here :)`,
|
description: `Hi, Endilie here :)`,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(docsMetadata['foo/bar']).toEqual({
|
expect(docsMetadata['foo/bar']).toEqual({
|
||||||
category: 'Test',
|
category: 'Test',
|
||||||
id: 'foo/bar',
|
id: 'foo/bar',
|
||||||
|
|
|
@ -25,166 +25,162 @@ const DEFAULT_OPTIONS = {
|
||||||
docItemComponent: '@theme/DocItem',
|
docItemComponent: '@theme/DocItem',
|
||||||
};
|
};
|
||||||
|
|
||||||
class DocusaurusPluginContentDocs {
|
module.exports = function(context, opts) {
|
||||||
constructor(context, opts) {
|
const options = {...DEFAULT_OPTIONS, ...opts};
|
||||||
this.options = {...DEFAULT_OPTIONS, ...opts};
|
const contentPath = path.resolve(context.siteDir, options.path);
|
||||||
this.context = context;
|
let globalContents = {};
|
||||||
this.contentPath = path.resolve(this.context.siteDir, this.options.path);
|
|
||||||
this.content = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
getName() {
|
return {
|
||||||
return 'docusaurus-plugin-content-docs';
|
name: 'docusaurus-plugin-content-docs',
|
||||||
}
|
|
||||||
|
|
||||||
getPathsToWatch() {
|
contentPath,
|
||||||
const {include = []} = this.options;
|
|
||||||
const globPattern = include.map(
|
|
||||||
pattern => `${this.contentPath}/${pattern}`,
|
|
||||||
);
|
|
||||||
return [...globPattern, this.options.sidebarPath];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetches blog contents and returns metadata for the contents.
|
getPathsToWatch() {
|
||||||
async loadContent() {
|
const {include = []} = options;
|
||||||
const {include, routeBasePath, sidebarPath} = this.options;
|
const globPattern = include.map(pattern => `${contentPath}/${pattern}`);
|
||||||
const {siteConfig} = this.context;
|
return [...globPattern, options.sidebarPath];
|
||||||
const docsDir = this.contentPath;
|
},
|
||||||
|
|
||||||
if (!fs.existsSync(docsDir)) {
|
// Fetches blog contents and returns metadata for the contents.
|
||||||
return null;
|
async loadContent() {
|
||||||
}
|
const {include, routeBasePath, sidebarPath} = options;
|
||||||
|
const {siteConfig} = context;
|
||||||
|
const docsDir = contentPath;
|
||||||
|
|
||||||
const docsSidebars = loadSidebars(sidebarPath);
|
if (!fs.existsSync(docsDir)) {
|
||||||
|
return null;
|
||||||
// Build the docs ordering such as next, previous, category and sidebar
|
|
||||||
const order = createOrder(docsSidebars);
|
|
||||||
|
|
||||||
// Prepare metadata container.
|
|
||||||
const docs = {};
|
|
||||||
|
|
||||||
// Metadata for default docs files.
|
|
||||||
const docsFiles = await globby(include, {
|
|
||||||
cwd: docsDir,
|
|
||||||
});
|
|
||||||
await Promise.all(
|
|
||||||
docsFiles.map(async source => {
|
|
||||||
const metadata = await processMetadata(
|
|
||||||
source,
|
|
||||||
docsDir,
|
|
||||||
order,
|
|
||||||
siteConfig,
|
|
||||||
routeBasePath,
|
|
||||||
);
|
|
||||||
docs[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']);
|
|
||||||
if (previousID) {
|
|
||||||
const previousTitle = idx(docs, [previousID, 'title']);
|
|
||||||
docs[currentID].previous_title = previousTitle || 'Previous';
|
|
||||||
}
|
}
|
||||||
const nextID = idx(docs, [currentID, 'next']);
|
|
||||||
if (nextID) {
|
const docsSidebars = loadSidebars(sidebarPath);
|
||||||
const nextTitle = idx(docs, [nextID, 'title']);
|
|
||||||
docs[currentID].next_title = nextTitle || 'Next';
|
// Build the docs ordering such as next, previous, category and sidebar
|
||||||
|
const order = createOrder(docsSidebars);
|
||||||
|
|
||||||
|
// Prepare metadata container.
|
||||||
|
const docs = {};
|
||||||
|
|
||||||
|
// Metadata for default docs files.
|
||||||
|
const docsFiles = await globby(include, {
|
||||||
|
cwd: docsDir,
|
||||||
|
});
|
||||||
|
await Promise.all(
|
||||||
|
docsFiles.map(async source => {
|
||||||
|
const metadata = await processMetadata(
|
||||||
|
source,
|
||||||
|
docsDir,
|
||||||
|
order,
|
||||||
|
siteConfig,
|
||||||
|
routeBasePath,
|
||||||
|
);
|
||||||
|
docs[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']);
|
||||||
|
if (previousID) {
|
||||||
|
const previousTitle = idx(docs, [previousID, 'title']);
|
||||||
|
docs[currentID].previous_title = previousTitle || 'Previous';
|
||||||
|
}
|
||||||
|
const nextID = idx(docs, [currentID, 'next']);
|
||||||
|
if (nextID) {
|
||||||
|
const nextTitle = idx(docs, [nextID, 'title']);
|
||||||
|
docs[currentID].next_title = nextTitle || 'Next';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const sourceToPermalink = {};
|
||||||
|
const permalinkToId = {};
|
||||||
|
Object.values(docs).forEach(({id, source, permalink}) => {
|
||||||
|
sourceToPermalink[source] = permalink;
|
||||||
|
permalinkToId[permalink] = id;
|
||||||
|
});
|
||||||
|
|
||||||
|
globalContents = {
|
||||||
|
docs,
|
||||||
|
docsDir,
|
||||||
|
docsSidebars,
|
||||||
|
sourceToPermalink,
|
||||||
|
permalinkToId,
|
||||||
|
};
|
||||||
|
|
||||||
|
return globalContents;
|
||||||
|
},
|
||||||
|
|
||||||
|
async contentLoaded({content, actions}) {
|
||||||
|
if (!content) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
const sourceToPermalink = {};
|
const {docLayoutComponent, docItemComponent, routeBasePath} = options;
|
||||||
const permalinkToId = {};
|
const {addRoute, createData} = actions;
|
||||||
Object.values(docs).forEach(({id, source, permalink}) => {
|
|
||||||
sourceToPermalink[source] = permalink;
|
|
||||||
permalinkToId[permalink] = id;
|
|
||||||
});
|
|
||||||
|
|
||||||
this.content = {
|
const routes = await Promise.all(
|
||||||
docs,
|
Object.values(content.docs).map(async metadataItem => {
|
||||||
docsDir,
|
const metadataPath = await createData(
|
||||||
docsSidebars,
|
`${docuHash(metadataItem.permalink)}.json`,
|
||||||
sourceToPermalink,
|
JSON.stringify(metadataItem, null, 2),
|
||||||
permalinkToId,
|
);
|
||||||
};
|
return {
|
||||||
|
path: metadataItem.permalink,
|
||||||
|
component: docItemComponent,
|
||||||
|
exact: true,
|
||||||
|
modules: {
|
||||||
|
content: metadataItem.source,
|
||||||
|
metadata: metadataPath,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
return this.content;
|
const docsBaseRoute = normalizeUrl([
|
||||||
}
|
context.siteConfig.baseUrl,
|
||||||
|
routeBasePath,
|
||||||
|
]);
|
||||||
|
const docsMetadataPath = await createData(
|
||||||
|
`${docuHash(docsBaseRoute)}.json`,
|
||||||
|
JSON.stringify(content, null, 2),
|
||||||
|
);
|
||||||
|
|
||||||
async contentLoaded({content, actions}) {
|
addRoute({
|
||||||
if (!content) {
|
path: docsBaseRoute,
|
||||||
return;
|
component: docLayoutComponent,
|
||||||
}
|
routes,
|
||||||
const {docLayoutComponent, docItemComponent, routeBasePath} = this.options;
|
modules: {
|
||||||
const {addRoute, createData} = actions;
|
docsMetadata: docsMetadataPath,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
const routes = await Promise.all(
|
getThemePath() {
|
||||||
Object.values(content.docs).map(async metadataItem => {
|
return path.resolve(__dirname, './theme');
|
||||||
const metadataPath = await createData(
|
},
|
||||||
`${docuHash(metadataItem.permalink)}.json`,
|
|
||||||
JSON.stringify(metadataItem, null, 2),
|
|
||||||
);
|
|
||||||
return {
|
|
||||||
path: metadataItem.permalink,
|
|
||||||
component: docItemComponent,
|
|
||||||
exact: true,
|
|
||||||
modules: {
|
|
||||||
content: metadataItem.source,
|
|
||||||
metadata: metadataPath,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
const docsBaseRoute = normalizeUrl([
|
configureWebpack(config, isServer, {getBabelLoader, getCacheLoader}) {
|
||||||
this.context.siteConfig.baseUrl,
|
return {
|
||||||
routeBasePath,
|
module: {
|
||||||
]);
|
rules: [
|
||||||
const docsMetadataPath = await createData(
|
{
|
||||||
`${docuHash(docsBaseRoute)}.json`,
|
test: /(\.mdx?)$/,
|
||||||
JSON.stringify(content, null, 2),
|
include: [contentPath],
|
||||||
);
|
use: [
|
||||||
|
getCacheLoader(isServer),
|
||||||
addRoute({
|
getBabelLoader(isServer),
|
||||||
path: docsBaseRoute,
|
'@docusaurus/mdx-loader',
|
||||||
component: docLayoutComponent,
|
{
|
||||||
routes,
|
loader: path.resolve(__dirname, './markdown/index.js'),
|
||||||
modules: {
|
options: {
|
||||||
docsMetadata: docsMetadataPath,
|
siteConfig: context.siteConfig,
|
||||||
},
|
docsDir: globalContents.docsDir,
|
||||||
});
|
sourceToPermalink: globalContents.sourceToPermalink,
|
||||||
}
|
},
|
||||||
|
|
||||||
getThemePath() {
|
|
||||||
return path.resolve(__dirname, './theme');
|
|
||||||
}
|
|
||||||
|
|
||||||
configureWebpack(config, isServer, {getBabelLoader, getCacheLoader}) {
|
|
||||||
return {
|
|
||||||
module: {
|
|
||||||
rules: [
|
|
||||||
{
|
|
||||||
test: /(\.mdx?)$/,
|
|
||||||
include: [this.contentPath],
|
|
||||||
use: [
|
|
||||||
getCacheLoader(isServer),
|
|
||||||
getBabelLoader(isServer),
|
|
||||||
'@docusaurus/mdx-loader',
|
|
||||||
{
|
|
||||||
loader: path.resolve(__dirname, './markdown/index.js'),
|
|
||||||
options: {
|
|
||||||
siteConfig: this.context.siteConfig,
|
|
||||||
docsDir: this.content.docsDir,
|
|
||||||
sourceToPermalink: this.content.sourceToPermalink,
|
|
||||||
},
|
},
|
||||||
},
|
],
|
||||||
],
|
},
|
||||||
},
|
],
|
||||||
],
|
},
|
||||||
},
|
};
|
||||||
};
|
},
|
||||||
}
|
};
|
||||||
}
|
};
|
||||||
|
|
||||||
module.exports = DocusaurusPluginContentDocs;
|
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
|
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
|
||||||
import DocusaurusPluginContentPages from '../index';
|
import pluginContentPages from '../index';
|
||||||
|
|
||||||
describe('docusaurus-plugin-content-pages', () => {
|
describe('docusaurus-plugin-content-pages', () => {
|
||||||
test('simple pages', async () => {
|
test('simple pages', async () => {
|
||||||
|
@ -17,7 +17,7 @@ describe('docusaurus-plugin-content-pages', () => {
|
||||||
url: 'https://docusaurus.io',
|
url: 'https://docusaurus.io',
|
||||||
};
|
};
|
||||||
const siteDir = path.join(__dirname, '__fixtures__', 'website');
|
const siteDir = path.join(__dirname, '__fixtures__', 'website');
|
||||||
const plugin = new DocusaurusPluginContentPages({
|
const plugin = pluginContentPages({
|
||||||
siteDir,
|
siteDir,
|
||||||
siteConfig,
|
siteConfig,
|
||||||
});
|
});
|
||||||
|
|
|
@ -16,75 +16,70 @@ const DEFAULT_OPTIONS = {
|
||||||
include: ['**/*.{js,jsx}'], // Extensions to include.
|
include: ['**/*.{js,jsx}'], // Extensions to include.
|
||||||
};
|
};
|
||||||
|
|
||||||
class DocusaurusPluginContentPages {
|
module.exports = function(context, opts) {
|
||||||
constructor(context, opts) {
|
const options = {...DEFAULT_OPTIONS, ...opts};
|
||||||
this.options = {...DEFAULT_OPTIONS, ...opts};
|
const contentPath = path.resolve(context.siteDir, options.path);
|
||||||
this.context = context;
|
|
||||||
this.contentPath = path.resolve(this.context.siteDir, this.options.path);
|
|
||||||
}
|
|
||||||
|
|
||||||
getName() {
|
return {
|
||||||
return 'docusaurus-plugin-content-pages';
|
name: 'docusaurus-plugin-content-pages',
|
||||||
}
|
|
||||||
|
|
||||||
getPathsToWatch() {
|
contentPath,
|
||||||
const {include = []} = this.options;
|
|
||||||
const globPattern = include.map(
|
|
||||||
pattern => `${this.contentPath}/${pattern}`,
|
|
||||||
);
|
|
||||||
return [...globPattern];
|
|
||||||
}
|
|
||||||
|
|
||||||
async loadContent() {
|
getPathsToWatch() {
|
||||||
const {include} = this.options;
|
const {include = []} = options;
|
||||||
const {siteConfig} = this.context;
|
const globPattern = include.map(pattern => `${contentPath}/${pattern}`);
|
||||||
const pagesDir = this.contentPath;
|
return [...globPattern];
|
||||||
|
},
|
||||||
|
|
||||||
if (!fs.existsSync(pagesDir)) {
|
async loadContent() {
|
||||||
return null;
|
const {include} = options;
|
||||||
}
|
const {siteConfig} = context;
|
||||||
|
const pagesDir = contentPath;
|
||||||
|
|
||||||
const {baseUrl} = siteConfig;
|
if (!fs.existsSync(pagesDir)) {
|
||||||
const pagesFiles = await globby(include, {
|
return null;
|
||||||
cwd: pagesDir,
|
}
|
||||||
});
|
|
||||||
|
|
||||||
return pagesFiles.map(relativeSource => {
|
const {baseUrl} = siteConfig;
|
||||||
const source = path.join(pagesDir, relativeSource);
|
const pagesFiles = await globby(include, {
|
||||||
const pathName = encodePath(fileToPath(relativeSource));
|
cwd: pagesDir,
|
||||||
// Default Language.
|
});
|
||||||
return {
|
|
||||||
permalink: pathName.replace(/^\//, baseUrl),
|
|
||||||
source,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async contentLoaded({content, actions}) {
|
return pagesFiles.map(relativeSource => {
|
||||||
if (!content) {
|
const source = path.join(pagesDir, relativeSource);
|
||||||
return;
|
const pathName = encodePath(fileToPath(relativeSource));
|
||||||
}
|
// Default Language.
|
||||||
|
return {
|
||||||
|
permalink: pathName.replace(/^\//, baseUrl),
|
||||||
|
source,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
const {addRoute, createData} = actions;
|
async contentLoaded({content, actions}) {
|
||||||
|
if (!content) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await Promise.all(
|
const {addRoute, createData} = actions;
|
||||||
content.map(async metadataItem => {
|
|
||||||
const {permalink, source} = metadataItem;
|
|
||||||
const metadataPath = await createData(
|
|
||||||
`${docuHash(permalink)}.json`,
|
|
||||||
JSON.stringify(metadataItem, null, 2),
|
|
||||||
);
|
|
||||||
addRoute({
|
|
||||||
path: permalink,
|
|
||||||
component: source,
|
|
||||||
exact: true,
|
|
||||||
modules: {
|
|
||||||
metadata: metadataPath,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = DocusaurusPluginContentPages;
|
await Promise.all(
|
||||||
|
content.map(async metadataItem => {
|
||||||
|
const {permalink, source} = metadataItem;
|
||||||
|
const metadataPath = await createData(
|
||||||
|
`${docuHash(permalink)}.json`,
|
||||||
|
JSON.stringify(metadataItem, null, 2),
|
||||||
|
);
|
||||||
|
addRoute({
|
||||||
|
path: permalink,
|
||||||
|
component: source,
|
||||||
|
exact: true,
|
||||||
|
modules: {
|
||||||
|
metadata: metadataPath,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
|
@ -0,0 +1,30 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2017-present, Facebook, Inc.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import createSitemap from '../createSitemap';
|
||||||
|
|
||||||
|
describe('createSitemap', () => {
|
||||||
|
test('simple site', () => {
|
||||||
|
const sitemap = createSitemap({
|
||||||
|
siteConfig: {
|
||||||
|
url: 'https://example.com',
|
||||||
|
},
|
||||||
|
routesPaths: ['/', '/test'],
|
||||||
|
});
|
||||||
|
expect(sitemap).toContain(
|
||||||
|
`<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:news="http://www.google.com/schemas/sitemap-news/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml" xmlns:mobile="http://www.google.com/schemas/sitemap-mobile/1.0" xmlns:image="http://www.google.com/schemas/sitemap-image/1.1" xmlns:video="http://www.google.com/schemas/sitemap-video/1.1">`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('empty site', () => {
|
||||||
|
expect(() => {
|
||||||
|
createSitemap({});
|
||||||
|
}).toThrowErrorMatchingInlineSnapshot(
|
||||||
|
`"Url in docusaurus.config.js cannot be empty/undefined"`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,36 +0,0 @@
|
||||||
/**
|
|
||||||
* Copyright (c) 2017-present, Facebook, Inc.
|
|
||||||
*
|
|
||||||
* This source code is licensed under the MIT license found in the
|
|
||||||
* LICENSE file in the root directory of this source tree.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import DocusaurusPluginSitemap from '../index';
|
|
||||||
|
|
||||||
describe('docusaurus-plugin-sitemap', () => {
|
|
||||||
describe('createSitemap', () => {
|
|
||||||
test('simple site', async () => {
|
|
||||||
const context = {
|
|
||||||
siteConfig: {
|
|
||||||
url: 'https://example.com',
|
|
||||||
},
|
|
||||||
routesPaths: ['/', '/test'],
|
|
||||||
};
|
|
||||||
const plugin = new DocusaurusPluginSitemap(context, null);
|
|
||||||
const sitemap = await plugin.createSitemap(context);
|
|
||||||
expect(sitemap).toContain(
|
|
||||||
`<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:news="http://www.google.com/schemas/sitemap-news/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml" xmlns:mobile="http://www.google.com/schemas/sitemap-mobile/1.0" xmlns:image="http://www.google.com/schemas/sitemap-image/1.1" xmlns:video="http://www.google.com/schemas/sitemap-video/1.1">`,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('empty site', async () => {
|
|
||||||
const context = {};
|
|
||||||
const plugin = new DocusaurusPluginSitemap(context, null);
|
|
||||||
expect(
|
|
||||||
plugin.createSitemap(context),
|
|
||||||
).rejects.toThrowErrorMatchingInlineSnapshot(
|
|
||||||
`"Url in docusaurus.config.js cannot be empty/undefined"`,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
33
packages/docusaurus-plugin-sitemap/src/createSitemap.js
Normal file
33
packages/docusaurus-plugin-sitemap/src/createSitemap.js
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2017-present, Facebook, Inc.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const sitemap = require('sitemap');
|
||||||
|
|
||||||
|
module.exports = function createSitemap({
|
||||||
|
siteConfig = {},
|
||||||
|
routesPaths,
|
||||||
|
options = {},
|
||||||
|
}) {
|
||||||
|
const {url: hostname} = siteConfig;
|
||||||
|
if (!hostname) {
|
||||||
|
throw new Error('Url in docusaurus.config.js cannot be empty/undefined');
|
||||||
|
}
|
||||||
|
|
||||||
|
const urls = routesPaths.map(routesPath => ({
|
||||||
|
url: routesPath,
|
||||||
|
changefreq: options.changefreq,
|
||||||
|
priority: options.priority,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return sitemap
|
||||||
|
.createSitemap({
|
||||||
|
hostname,
|
||||||
|
cacheTime: options.cacheTime,
|
||||||
|
urls,
|
||||||
|
})
|
||||||
|
.toString();
|
||||||
|
};
|
|
@ -6,61 +6,37 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const sitemap = require('sitemap');
|
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
|
||||||
|
const createSitemap = require('./createSitemap');
|
||||||
|
|
||||||
const DEFAULT_OPTIONS = {
|
const DEFAULT_OPTIONS = {
|
||||||
cacheTime: 600 * 1000, // 600 sec - cache purge period
|
cacheTime: 600 * 1000, // 600 sec - cache purge period
|
||||||
changefreq: 'weekly',
|
changefreq: 'weekly',
|
||||||
priority: 0.5,
|
priority: 0.5,
|
||||||
};
|
};
|
||||||
|
|
||||||
class DocusaurusPluginSitemap {
|
module.exports = function(context, opts) {
|
||||||
constructor(context, opts) {
|
const options = {...DEFAULT_OPTIONS, ...opts};
|
||||||
this.options = {...DEFAULT_OPTIONS, ...opts};
|
|
||||||
this.context = context;
|
|
||||||
}
|
|
||||||
|
|
||||||
getName() {
|
return {
|
||||||
return 'docusaurus-plugin-sitemap';
|
name: 'docusaurus-plugin-sitemap',
|
||||||
}
|
|
||||||
|
|
||||||
async createSitemap({siteConfig = {}, routesPaths}) {
|
async postBuild({siteConfig = {}, routesPaths = [], outDir}) {
|
||||||
const {url: hostname} = siteConfig;
|
// Generate sitemap
|
||||||
if (!hostname) {
|
const generatedSitemap = createSitemap({
|
||||||
throw new Error(`Url in docusaurus.config.js cannot be empty/undefined`);
|
siteConfig,
|
||||||
}
|
routesPaths,
|
||||||
|
options,
|
||||||
|
}).toString();
|
||||||
|
|
||||||
const urls = routesPaths.map(routesPath => ({
|
// Write sitemap file
|
||||||
url: routesPath,
|
const sitemapPath = path.join(outDir, 'sitemap.xml');
|
||||||
changefreq: this.changefreq,
|
fs.writeFile(sitemapPath, generatedSitemap, err => {
|
||||||
priority: this.priority,
|
if (err) {
|
||||||
}));
|
throw new Error(`Sitemap error: ${err}`);
|
||||||
|
}
|
||||||
return sitemap
|
});
|
||||||
.createSitemap({
|
},
|
||||||
hostname,
|
};
|
||||||
cacheTime: this.cacheTime,
|
};
|
||||||
urls,
|
|
||||||
})
|
|
||||||
.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
async postBuild({siteConfig = {}, routesPaths = [], outDir}) {
|
|
||||||
// Generate sitemap
|
|
||||||
const generatedSitemap = await this.createSitemap({
|
|
||||||
siteConfig,
|
|
||||||
routesPaths,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Write sitemap file
|
|
||||||
const sitemapPath = path.join(outDir, 'sitemap.xml');
|
|
||||||
fs.writeFile(sitemapPath, generatedSitemap, err => {
|
|
||||||
if (err) {
|
|
||||||
throw new Error(`Sitemap error: ${err}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = DocusaurusPluginSitemap;
|
|
||||||
|
|
|
@ -9,27 +9,27 @@ module.exports = function preset(context, opts = {}) {
|
||||||
return {
|
return {
|
||||||
themes: [
|
themes: [
|
||||||
{
|
{
|
||||||
name: '@docusaurus/theme-classic',
|
module: '@docusaurus/theme-classic',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: '@docusaurus/theme-search-algolia',
|
module: '@docusaurus/theme-search-algolia',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
plugins: [
|
plugins: [
|
||||||
{
|
{
|
||||||
name: '@docusaurus/plugin-content-docs',
|
module: '@docusaurus/plugin-content-docs',
|
||||||
options: opts.docs,
|
options: opts.docs,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: '@docusaurus/plugin-content-blog',
|
module: '@docusaurus/plugin-content-blog',
|
||||||
options: opts.blog,
|
options: opts.blog,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: '@docusaurus/plugin-content-pages',
|
module: '@docusaurus/plugin-content-pages',
|
||||||
options: opts.pages,
|
options: opts.pages,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: '@docusaurus/plugin-sitemap',
|
module: '@docusaurus/plugin-sitemap',
|
||||||
options: opts.sitemap,
|
options: opts.sitemap,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
|
@ -7,21 +7,12 @@
|
||||||
|
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
|
||||||
const DEFAULT_OPTIONS = {};
|
module.exports = function() {
|
||||||
|
return {
|
||||||
|
name: 'docusaurus-theme-classic',
|
||||||
|
|
||||||
class DocusaurusThemeClassic {
|
getThemePath() {
|
||||||
constructor(context, opts) {
|
return path.resolve(__dirname, './theme');
|
||||||
this.options = {...DEFAULT_OPTIONS, ...opts};
|
},
|
||||||
this.context = context;
|
};
|
||||||
}
|
};
|
||||||
|
|
||||||
getName() {
|
|
||||||
return 'docusaurus-theme-classic';
|
|
||||||
}
|
|
||||||
|
|
||||||
getThemePath() {
|
|
||||||
return path.resolve(__dirname, './theme');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = DocusaurusThemeClassic;
|
|
||||||
|
|
|
@ -7,21 +7,12 @@
|
||||||
|
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
|
||||||
const DEFAULT_OPTIONS = {};
|
module.exports = function() {
|
||||||
|
return {
|
||||||
|
name: 'docusaurus-theme-search-algolia',
|
||||||
|
|
||||||
class DocusaurusThemeSearchAlgolia {
|
getThemePath() {
|
||||||
constructor(context, opts) {
|
return path.resolve(__dirname, './theme');
|
||||||
this.options = {...DEFAULT_OPTIONS, ...opts};
|
},
|
||||||
this.context = context;
|
};
|
||||||
}
|
};
|
||||||
|
|
||||||
getName() {
|
|
||||||
return 'docusaurus-theme-search-algolia';
|
|
||||||
}
|
|
||||||
|
|
||||||
getThemePath() {
|
|
||||||
return path.resolve(__dirname, './theme');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = DocusaurusThemeSearchAlgolia;
|
|
||||||
|
|
|
@ -1,4 +1,12 @@
|
||||||
# Breaking Changes
|
# Docusaurus 2 Changelog
|
||||||
|
|
||||||
|
## 2.0.0-alpha.19
|
||||||
|
|
||||||
|
- Changed plugin definitions from classes to functions. Refer to the new plugin docs.
|
||||||
|
- Added sun and moon emoji to the dark mode toggle.
|
||||||
|
- Add a sensible default for browserslist config.
|
||||||
|
|
||||||
|
## V2 Changelog
|
||||||
|
|
||||||
### `siteConfig.js` changes
|
### `siteConfig.js` changes
|
||||||
|
|
|
@ -15,10 +15,9 @@ export async function swizzle(
|
||||||
themeName: string,
|
themeName: string,
|
||||||
componentName?: string,
|
componentName?: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const Plugin = importFresh(themeName);
|
const plugin = importFresh(themeName);
|
||||||
const context = {siteDir};
|
const pluginInstance = plugin({siteDir});
|
||||||
const PluginInstance = new Plugin(context);
|
let fromPath = pluginInstance.getThemePath();
|
||||||
let fromPath = PluginInstance.getThemePath();
|
|
||||||
|
|
||||||
if (fromPath) {
|
if (fromPath) {
|
||||||
let toPath = path.resolve(siteDir, 'theme');
|
let toPath = path.resolve(siteDir, 'theme');
|
||||||
|
|
|
@ -15,14 +15,14 @@ module.exports = {
|
||||||
favicon: 'img/docusaurus.ico',
|
favicon: 'img/docusaurus.ico',
|
||||||
plugins: [
|
plugins: [
|
||||||
{
|
{
|
||||||
name: '@docusaurus/plugin-content-docs',
|
module: '@docusaurus/plugin-content-docs',
|
||||||
options: {
|
options: {
|
||||||
path: '../docs',
|
path: '../docs',
|
||||||
sidebarPath: require.resolve('./sidebars.json'),
|
sidebarPath: require.resolve('./sidebars.json'),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: '@docusaurus/plugin-content-pages',
|
module: '@docusaurus/plugin-content-pages',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
|
@ -15,14 +15,14 @@ module.exports = {
|
||||||
favicon: 'img/docusaurus.ico',
|
favicon: 'img/docusaurus.ico',
|
||||||
plugins: [
|
plugins: [
|
||||||
{
|
{
|
||||||
name: '@docusaurus/plugin-content-docs',
|
module: '@docusaurus/plugin-content-docs',
|
||||||
options: {
|
options: {
|
||||||
path: '../docs',
|
path: '../docs',
|
||||||
sidebarPath: require.resolve('./sidebars.json'),
|
sidebarPath: require.resolve('./sidebars.json'),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: '@docusaurus/plugin-content-pages',
|
module: '@docusaurus/plugin-content-pages',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
|
@ -19,14 +19,10 @@ export async function loadPlugins({
|
||||||
context: LoadContext;
|
context: LoadContext;
|
||||||
}) {
|
}) {
|
||||||
// 1. Plugin Lifecycle - Initialization/Constructor
|
// 1. Plugin Lifecycle - Initialization/Constructor
|
||||||
const plugins = pluginConfigs.map(({name, path: pluginPath, options}) => {
|
const plugins = pluginConfigs.map(({module, options}) => {
|
||||||
let Plugin;
|
// module is any valid module identifier - npm package or locally-resolved path.
|
||||||
if (pluginPath && fs.existsSync(pluginPath)) {
|
const plugin = importFresh(module);
|
||||||
Plugin = importFresh(pluginPath);
|
return plugin(context, options);
|
||||||
} else {
|
|
||||||
Plugin = importFresh(name);
|
|
||||||
}
|
|
||||||
return new Plugin(context, options);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// 2. Plugin lifecycle - loadContent
|
// 2. Plugin lifecycle - loadContent
|
||||||
|
@ -51,8 +47,11 @@ export async function loadPlugins({
|
||||||
if (!plugin.contentLoaded) {
|
if (!plugin.contentLoaded) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const pluginName = plugin.getName();
|
|
||||||
const pluginContentDir = path.join(context.generatedFilesDir, pluginName);
|
const pluginContentDir = path.join(
|
||||||
|
context.generatedFilesDir,
|
||||||
|
plugin.name,
|
||||||
|
);
|
||||||
const actions = {
|
const actions = {
|
||||||
addRoute: config => pluginsRouteConfigs.push(config),
|
addRoute: config => pluginsRouteConfigs.push(config),
|
||||||
createData: async (name, content) => {
|
createData: async (name, content) => {
|
||||||
|
|
|
@ -3,7 +3,7 @@ id: plugins-api
|
||||||
title: Plugins
|
title: Plugins
|
||||||
---
|
---
|
||||||
|
|
||||||
Plugins are one of the best ways to add functionality to our Docusaurus. Plugins allow third-party developers to extend or modify the default functionality that Docusaurus provides.
|
Plugins are one of the best ways to add functionality to our Docusaurus. Plugins allow third-party developers to extend or modify the default functionality that Docusaurus provides.
|
||||||
|
|
||||||
## Installing a Plugin
|
## Installing a Plugin
|
||||||
|
|
||||||
|
@ -19,11 +19,11 @@ Then you add it in your site's `docusaurus.config.js` plugin arrays:
|
||||||
module.exports = {
|
module.exports = {
|
||||||
plugins: [
|
plugins: [
|
||||||
{
|
{
|
||||||
name: '@docusaurus/plugin-content-pages',
|
module: '@docusaurus/plugin-content-pages',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// Plugin with options
|
// Plugin with options
|
||||||
name: '@docusaurus/plugin-content-blog',
|
module: '@docusaurus/plugin-content-blog',
|
||||||
options: {
|
options: {
|
||||||
include: ['*.md', '*.mdx'],
|
include: ['*.md', '*.mdx'],
|
||||||
path: 'blog',
|
path: 'blog',
|
||||||
|
@ -33,81 +33,84 @@ module.exports = {
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
Docusaurus can also load plugins from your local folder, you can do something like below:
|
Docusaurus can also load plugins from your local directory, you can do something like the following:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
plugins: [
|
plugins: [
|
||||||
{
|
{
|
||||||
path: '/path/to/docusaurus-local-plugin',
|
module: path.resolve(__dirname, '/path/to/docusaurus-local-plugin'),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
## Basic Plugin Architecture
|
## Basic Plugin Definition
|
||||||
|
|
||||||
|
Plugins are modules which export a function that takes in the context, options and returns a plain JavaScript object that has some properties defined.
|
||||||
|
|
||||||
For examples, please refer to several official plugins created.
|
For examples, please refer to several official plugins created.
|
||||||
|
|
||||||
```js
|
```js
|
||||||
// A JavaScript class
|
const DEFAULT_OPTIONS = {
|
||||||
class DocusaurusPlugin {
|
// Some defaults.
|
||||||
constructor(context, options) {
|
};
|
||||||
// Initialization hook
|
|
||||||
|
|
||||||
// options are the plugin options set on config file
|
|
||||||
this.options = {...options};
|
|
||||||
|
|
||||||
// context are provided from docusaurus. Example: siteConfig can be accessed from context
|
|
||||||
this.context = context;
|
|
||||||
}
|
|
||||||
|
|
||||||
getName() {
|
// A JavaScript function that returns an object.
|
||||||
// plugin name identifier
|
// `context` is provided by Docusaurus. Example: siteConfig can be accessed from context.
|
||||||
}
|
// `opts` is the user-defined options.
|
||||||
|
module.exports = function(context, opts) {
|
||||||
|
// Merge defaults with user-defined options.
|
||||||
|
const options = {...DEFAULT_OPTIONS, ...options};
|
||||||
|
|
||||||
|
return {
|
||||||
|
// Namespace used for directories to cache the intermediate data for each plugin.
|
||||||
|
name: 'docusaurus-cool-plugin',
|
||||||
|
|
||||||
async loadContent()) {
|
async loadContent() {
|
||||||
// The loadContent hook is executed after siteConfig and env has been loaded
|
// The loadContent hook is executed after siteConfig and env has been loaded
|
||||||
// You can return a JavaScript object that will be passed to contentLoaded hook
|
// You can return a JavaScript object that will be passed to contentLoaded hook
|
||||||
}
|
},
|
||||||
|
|
||||||
async contentLoaded({content, actions}) {
|
async contentLoaded({content, actions}) {
|
||||||
// contentLoaded hook is done after loadContent hook is done
|
// contentLoaded hook is done after loadContent hook is done
|
||||||
// actions are set of functional API provided by Docusaurus. e.g: addRoute
|
// actions are set of functional API provided by Docusaurus. e.g: addRoute
|
||||||
}
|
},
|
||||||
|
|
||||||
async postBuild(props) {
|
async postBuild(props) {
|
||||||
// after docusaurus <build> finish
|
// after docusaurus <build> finish
|
||||||
}
|
},
|
||||||
|
|
||||||
// TODO
|
// TODO
|
||||||
async postStart(props) {
|
async postStart(props) {
|
||||||
// docusaurus <start> finish
|
// docusaurus <start> finish
|
||||||
}
|
},
|
||||||
|
|
||||||
// TODO
|
// TODO
|
||||||
afterDevServer(app, server) {
|
afterDevServer(app, server) {
|
||||||
// https://webpack.js.org/configuration/dev-server/#devserverbefore
|
// https://webpack.js.org/configuration/dev-server/#devserverbefore
|
||||||
}
|
},
|
||||||
|
|
||||||
// TODO
|
// TODO
|
||||||
beforeDevServer(app, server) {
|
beforeDevServer(app, server) {
|
||||||
// https://webpack.js.org/configuration/dev-server/#devserverafter
|
// https://webpack.js.org/configuration/dev-server/#devserverafter
|
||||||
|
},
|
||||||
|
|
||||||
}
|
configureWebpack(config, isServer) {
|
||||||
|
// Modify internal webpack config. If returned value is an Object, it
|
||||||
|
// will be merged into the final config using webpack-merge;
|
||||||
|
// If the returned value is a function, it will receive the config as the 1st argument and an isServer flag as the 2nd argument.
|
||||||
|
},
|
||||||
|
|
||||||
configureWebpack(config, isServer) {
|
getPathsToWatch() {
|
||||||
// Modify internal webpack config. If returned value is an Object, it will be merged into the final config using webpack-merge; If returned value is a function, it will receive the config as the 1st argument and an isServer flag as the 2nd argument.
|
// Path to watch
|
||||||
}
|
},
|
||||||
|
};
|
||||||
getPathsToWatch() {
|
};
|
||||||
// path to watch
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
#### References
|
#### References
|
||||||
|
|
||||||
- https://v1.vuepress.vuejs.org/plugin/option-api.html
|
- https://v1.vuepress.vuejs.org/plugin/option-api.html
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue