feat: doc tags (same as blog tags) (#3646)

* [v2] tags to doc, same as tags to blog - [IN PROGRESS]

- Addition of plugin-content-docs

- Addition of DocTagsListPage in `docusaurus-theme-classic`

! Error exists for this commit towards the theme aspect and help required.

Commit towards #3434

* docs: make tags list page work

* temp: disable onBrokenLinks

* theme bootstrap: create DocTagsListPage

* DocTagsPage added and functionality too

- individual doc tag page added to show docs for that specific tag

* Added all Docs Tags Link

* add some shared tag utils

* move tag tests to _dogfooding

* fix type

* fix some tests

* fix blog test

* refactor blog post tags handling

* better yaml tag examples

* better dogfood md files

* refactor and factorize theme tag components

* finish DocTagDocListPage

* Extract DocItemFooter + add inline tag list

* minor fix

* better typings

* fix versions.test.ts tests

* add tests for doc tags

* fix tests

* test toTagDocListProp

* move shared theme code to tagUtils

* Add new theme translation keys

* move common theme code to tagUtils + add tests

* update-code-translations should handle theme-common

* update french translation

* revert add translation

* fix pluralization problem in theme.docs.tagDocListPageTitle

* add theme component configuration options

* add more tags tests

* add documentation for docs tagging

Co-authored-by: slorber <lorber.sebastien@gmail.com>
This commit is contained in:
Isaac Philip 2021-08-19 14:01:15 +05:30 committed by GitHub
parent f666de7e59
commit f9c79cbd58
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
81 changed files with 1874 additions and 381 deletions

View file

@ -33,6 +33,11 @@ module.exports = {
}, },
setupFiles: ['./jest/stylelint-rule-test.js', './jest/polyfills.js'], setupFiles: ['./jest/stylelint-rule-test.js', './jest/polyfills.js'],
moduleNameMapper: { moduleNameMapper: {
// TODO we need to allow Jest to resolve core Webpack aliases automatically
'@docusaurus/router': 'react-router-dom', '@docusaurus/router': 'react-router-dom',
'@docusaurus/Translate': '@docusaurus/core/lib/client/exports/Translate',
'@docusaurus/Interpolate':
'@docusaurus/core/lib/client/exports/Interpolate',
'@generated/codeTranslations': '<rootDir>/jest/emptyModule.js',
}, },
}; };

8
jest/emptyModule.js Normal file
View file

@ -0,0 +1,8 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
module.exports = {};

View file

@ -9,15 +9,16 @@ import {
JoiFrontMatter as Joi, // Custom instance for frontmatter JoiFrontMatter as Joi, // Custom instance for frontmatter
URISchema, URISchema,
validateFrontMatter, validateFrontMatter,
FrontMatterTagsSchema,
} from '@docusaurus/utils-validation'; } from '@docusaurus/utils-validation';
import {Tag} from './types'; import type {FrontMatterTag} from '@docusaurus/utils';
export type BlogPostFrontMatter = { export type BlogPostFrontMatter = {
/* eslint-disable camelcase */ /* eslint-disable camelcase */
id?: string; id?: string;
title?: string; title?: string;
description?: string; description?: string;
tags?: (string | Tag)[]; tags?: FrontMatterTag[];
slug?: string; slug?: string;
draft?: boolean; draft?: boolean;
date?: Date | string; // Yaml automagically convert some string patterns as Date, but not all date?: Date | string; // Yaml automagically convert some string patterns as Date, but not all
@ -38,23 +39,11 @@ export type BlogPostFrontMatter = {
/* eslint-enable camelcase */ /* eslint-enable camelcase */
}; };
// NOTE: we don't add any default value on purpose here
// We don't want default values to magically appear in doc metadatas and props
// While the user did not provide those values explicitly
// We use default values in code instead
const BlogTagSchema = Joi.alternatives().try(
Joi.string().required(),
Joi.object<Tag>({
label: Joi.string().required(),
permalink: Joi.string().required(),
}),
);
const BlogFrontMatterSchema = Joi.object<BlogPostFrontMatter>({ const BlogFrontMatterSchema = Joi.object<BlogPostFrontMatter>({
id: Joi.string(), id: Joi.string(),
title: Joi.string().allow(''), title: Joi.string().allow(''),
description: Joi.string().allow(''), description: Joi.string().allow(''),
tags: Joi.array().items(BlogTagSchema), tags: FrontMatterTagsSchema,
draft: Joi.boolean(), draft: Joi.boolean(),
date: Joi.date().raw(), date: Joi.date().raw(),

View file

@ -16,6 +16,7 @@ import {
BlogPost, BlogPost,
BlogContentPaths, BlogContentPaths,
BlogMarkdownLoaderOptions, BlogMarkdownLoaderOptions,
BlogTags,
} from './types'; } from './types';
import { import {
parseMarkdownFile, parseMarkdownFile,
@ -26,6 +27,8 @@ import {
posixPath, posixPath,
replaceMarkdownLinks, replaceMarkdownLinks,
Globby, Globby,
normalizeFrontMatterTags,
groupTaggedItems,
} from '@docusaurus/utils'; } from '@docusaurus/utils';
import {LoadContext} from '@docusaurus/types'; import {LoadContext} from '@docusaurus/types';
import {validateBlogPostFrontMatter} from './blogFrontMatter'; import {validateBlogPostFrontMatter} from './blogFrontMatter';
@ -43,6 +46,20 @@ export function getSourceToPermalink(
); );
} }
export function getBlogTags(blogPosts: BlogPost[]): BlogTags {
const groups = groupTaggedItems(
blogPosts,
(blogPost) => blogPost.metadata.tags,
);
return mapValues(groups, (group) => {
return {
name: group.tag.label,
items: group.items.map((item) => item.id),
permalink: group.tag.permalink,
};
});
}
const DATE_FILENAME_REGEX = /^(?<date>\d{4}[-/]\d{1,2}[-/]\d{1,2})[-/]?(?<text>.*?)(\/index)?.mdx?$/; const DATE_FILENAME_REGEX = /^(?<date>\d{4}[-/]\d{1,2}[-/]\d{1,2})[-/]?(?<text>.*?)(\/index)?.mdx?$/;
type ParsedBlogFileName = { type ParsedBlogFileName = {
@ -240,6 +257,8 @@ async function processBlogSourceFile(
return undefined; return undefined;
} }
const tagsBasePath = normalizeUrl([baseUrl, options.routeBasePath, 'tags']); // make this configurable?
return { return {
id: frontMatter.slug ?? title, id: frontMatter.slug ?? title,
metadata: { metadata: {
@ -250,7 +269,7 @@ async function processBlogSourceFile(
description, description,
date, date,
formattedDate, formattedDate,
tags: frontMatter.tags ?? [], tags: normalizeFrontMatterTags(tagsBasePath, frontMatter.tags),
readingTime: showReadingTime ? readingTime(content).minutes : undefined, readingTime: showReadingTime ? readingTime(content).minutes : undefined,
truncated: truncateMarker?.test(content) || false, truncated: truncateMarker?.test(content) || false,
}, },

View file

@ -22,7 +22,7 @@ import {
STATIC_DIR_NAME, STATIC_DIR_NAME,
DEFAULT_PLUGIN_ID, DEFAULT_PLUGIN_ID,
} from '@docusaurus/core/lib/constants'; } from '@docusaurus/core/lib/constants';
import {flatten, take, kebabCase} from 'lodash'; import {flatten, take} from 'lodash';
import { import {
PluginOptions, PluginOptions,
@ -51,6 +51,7 @@ import {
generateBlogPosts, generateBlogPosts,
getContentPathList, getContentPathList,
getSourceToPermalink, getSourceToPermalink,
getBlogTags,
} from './blogUtils'; } from './blogUtils';
export default function pluginContentBlog( export default function pluginContentBlog(
@ -65,7 +66,7 @@ export default function pluginContentBlog(
const { const {
siteDir, siteDir,
siteConfig: {onBrokenMarkdownLinks}, siteConfig: {onBrokenMarkdownLinks, baseUrl},
generatedFilesDir, generatedFilesDir,
i18n: {currentLocale}, i18n: {currentLocale},
} = context; } = context;
@ -151,17 +152,14 @@ export default function pluginContentBlog(
const postsPerPage = const postsPerPage =
postsPerPageOption === 'ALL' ? totalCount : postsPerPageOption; postsPerPageOption === 'ALL' ? totalCount : postsPerPageOption;
const numberOfPages = Math.ceil(totalCount / postsPerPage); const numberOfPages = Math.ceil(totalCount / postsPerPage);
const { const baseBlogUrl = normalizeUrl([baseUrl, routeBasePath]);
siteConfig: {baseUrl = ''},
} = context;
const basePageUrl = normalizeUrl([baseUrl, routeBasePath]);
const blogListPaginated: BlogPaginated[] = []; const blogListPaginated: BlogPaginated[] = [];
function blogPaginationPermalink(page: number) { function blogPaginationPermalink(page: number) {
return page > 0 return page > 0
? normalizeUrl([basePageUrl, `page/${page + 1}`]) ? normalizeUrl([baseBlogUrl, `page/${page + 1}`])
: basePageUrl; : baseBlogUrl;
} }
for (let page = 0; page < numberOfPages; page += 1) { for (let page = 0; page < numberOfPages; page += 1) {
@ -186,41 +184,9 @@ export default function pluginContentBlog(
}); });
} }
const blogTags: BlogTags = {}; const blogTags: BlogTags = getBlogTags(blogPosts);
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 const tagsPath = normalizeUrl([baseBlogUrl, 'tags']);
blogPost.metadata.tags = tags.map((tag) => {
if (typeof tag === 'string') {
const normalizedTag = kebabCase(tag);
const permalink = normalizeUrl([tagsPath, normalizedTag]);
if (!blogTags[normalizedTag]) {
blogTags[normalizedTag] = {
// Will only use the name of the first occurrence of the tag.
name: tag.toLowerCase(),
items: [],
permalink,
};
}
blogTags[normalizedTag].items.push(blogPost.id);
return {
label: tag,
permalink,
};
}
return tag;
});
});
const blogTagsListPath = const blogTagsListPath =
Object.keys(blogTags).length > 0 ? tagsPath : null; Object.keys(blogTags).length > 0 ? tagsPath : null;
@ -348,6 +314,7 @@ export default function pluginContentBlog(
Object.keys(blogTags).map(async (tag) => { Object.keys(blogTags).map(async (tag) => {
const {name, items, permalink} = blogTags[tag]; const {name, items, permalink} = blogTags[tag];
// Refactor all this, see docs implementation
tagsModule[tag] = { tagsModule[tag] = {
allTagsPath: blogTagsListPath, allTagsPath: blogTagsListPath,
slug: tag, slug: tag,
@ -535,7 +502,6 @@ export default function pluginContentBlog(
const feedTypes = options.feedOptions.type; const feedTypes = options.feedOptions.type;
const { const {
siteConfig: {title}, siteConfig: {title},
baseUrl,
} = context; } = context;
const feedsConfig = { const feedsConfig = {
rss: { rss: {

View file

@ -6,7 +6,8 @@
*/ */
import type {RemarkAndRehypePluginOptions} from '@docusaurus/mdx-loader'; import type {RemarkAndRehypePluginOptions} from '@docusaurus/mdx-loader';
import { import type {Tag} from '@docusaurus/utils';
import type {
BrokenMarkdownLink, BrokenMarkdownLink,
ContentPaths, ContentPaths,
} from '@docusaurus/utils/lib/markdownLinks'; } from '@docusaurus/utils/lib/markdownLinks';
@ -96,7 +97,7 @@ export interface MetaData {
description: string; description: string;
date: Date; date: Date;
formattedDate: string; formattedDate: string;
tags: (Tag | string)[]; tags: Tag[];
title: string; title: string;
readingTime?: number; readingTime?: number;
prevItem?: Paginator; prevItem?: Paginator;
@ -110,11 +111,6 @@ export interface Paginator {
permalink: string; permalink: string;
} }
export interface Tag {
label: string;
permalink: string;
}
export interface BlogItemsToMetadata { export interface BlogItemsToMetadata {
[key: string]: MetaData; [key: string]: MetaData;
} }

View file

@ -3,6 +3,11 @@ id: baz
title: baz title: baz
slug: bazSlug.html slug: bazSlug.html
pagination_label: baz pagination_label pagination_label: baz pagination_label
tags:
- tag 1
- tag-1
- label: tag 2
permalink: tag2-custom-permalink
--- ---
# Baz markdown title # Baz markdown title

View file

@ -2,6 +2,7 @@
id: hello id: hello
title: Hello, World ! title: Hello, World !
sidebar_label: Hello sidebar_label sidebar_label: Hello sidebar_label
tags: [tag-1, tag 3]
--- ---
Hi, Endilie here :) Hi, Endilie here :)

View file

@ -1,4 +1,10 @@
--- ---
slug: barSlug slug: barSlug
tags:
- barTag 1
- barTag-2
- label: barTag 3
permalink: barTag-3-permalink
--- ---
This is `next` version of bar. This is `next` version of bar.

View file

@ -177,6 +177,7 @@ Object {
\\"sourceDirName\\": \\"foo\\", \\"sourceDirName\\": \\"foo\\",
\\"slug\\": \\"/foo/bar\\", \\"slug\\": \\"/foo/bar\\",
\\"permalink\\": \\"/docs/foo/bar\\", \\"permalink\\": \\"/docs/foo/bar\\",
\\"tags\\": [],
\\"version\\": \\"current\\", \\"version\\": \\"current\\",
\\"frontMatter\\": { \\"frontMatter\\": {
\\"id\\": \\"bar\\", \\"id\\": \\"bar\\",
@ -199,12 +200,30 @@ Object {
\\"sourceDirName\\": \\"foo\\", \\"sourceDirName\\": \\"foo\\",
\\"slug\\": \\"/foo/bazSlug.html\\", \\"slug\\": \\"/foo/bazSlug.html\\",
\\"permalink\\": \\"/docs/foo/bazSlug.html\\", \\"permalink\\": \\"/docs/foo/bazSlug.html\\",
\\"tags\\": [
{
\\"label\\": \\"tag 1\\",
\\"permalink\\": \\"/docs/tags/tag-1\\"
},
{
\\"label\\": \\"tag 2\\",
\\"permalink\\": \\"/docs/tags/tag2-custom-permalink\\"
}
],
\\"version\\": \\"current\\", \\"version\\": \\"current\\",
\\"frontMatter\\": { \\"frontMatter\\": {
\\"id\\": \\"baz\\", \\"id\\": \\"baz\\",
\\"title\\": \\"baz\\", \\"title\\": \\"baz\\",
\\"slug\\": \\"bazSlug.html\\", \\"slug\\": \\"bazSlug.html\\",
\\"pagination_label\\": \\"baz pagination_label\\" \\"pagination_label\\": \\"baz pagination_label\\",
\\"tags\\": [
\\"tag 1\\",
\\"tag-1\\",
{
\\"label\\": \\"tag 2\\",
\\"permalink\\": \\"tag2-custom-permalink\\"
}
]
}, },
\\"sidebar\\": \\"docs\\", \\"sidebar\\": \\"docs\\",
\\"previous\\": { \\"previous\\": {
@ -226,6 +245,7 @@ Object {
\\"sourceDirName\\": \\".\\", \\"sourceDirName\\": \\".\\",
\\"slug\\": \\"/headingAsTitle\\", \\"slug\\": \\"/headingAsTitle\\",
\\"permalink\\": \\"/docs/headingAsTitle\\", \\"permalink\\": \\"/docs/headingAsTitle\\",
\\"tags\\": [],
\\"version\\": \\"current\\", \\"version\\": \\"current\\",
\\"frontMatter\\": {} \\"frontMatter\\": {}
}", }",
@ -239,11 +259,25 @@ Object {
\\"sourceDirName\\": \\".\\", \\"sourceDirName\\": \\".\\",
\\"slug\\": \\"/\\", \\"slug\\": \\"/\\",
\\"permalink\\": \\"/docs/\\", \\"permalink\\": \\"/docs/\\",
\\"tags\\": [
{
\\"label\\": \\"tag-1\\",
\\"permalink\\": \\"/docs/tags/tag-1\\"
},
{
\\"label\\": \\"tag 3\\",
\\"permalink\\": \\"/docs/tags/tag-3\\"
}
],
\\"version\\": \\"current\\", \\"version\\": \\"current\\",
\\"frontMatter\\": { \\"frontMatter\\": {
\\"id\\": \\"hello\\", \\"id\\": \\"hello\\",
\\"title\\": \\"Hello, World !\\", \\"title\\": \\"Hello, World !\\",
\\"sidebar_label\\": \\"Hello sidebar_label\\" \\"sidebar_label\\": \\"Hello sidebar_label\\",
\\"tags\\": [
\\"tag-1\\",
\\"tag 3\\"
]
}, },
\\"sidebar\\": \\"docs\\", \\"sidebar\\": \\"docs\\",
\\"previous\\": { \\"previous\\": {
@ -262,6 +296,7 @@ Object {
\\"slug\\": \\"/ipsum\\", \\"slug\\": \\"/ipsum\\",
\\"permalink\\": \\"/docs/ipsum\\", \\"permalink\\": \\"/docs/ipsum\\",
\\"editUrl\\": null, \\"editUrl\\": null,
\\"tags\\": [],
\\"version\\": \\"current\\", \\"version\\": \\"current\\",
\\"frontMatter\\": { \\"frontMatter\\": {
\\"custom_edit_url\\": null \\"custom_edit_url\\": null
@ -278,6 +313,7 @@ Object {
\\"slug\\": \\"/lorem\\", \\"slug\\": \\"/lorem\\",
\\"permalink\\": \\"/docs/lorem\\", \\"permalink\\": \\"/docs/lorem\\",
\\"editUrl\\": \\"https://github.com/customUrl/docs/lorem.md\\", \\"editUrl\\": \\"https://github.com/customUrl/docs/lorem.md\\",
\\"tags\\": [],
\\"version\\": \\"current\\", \\"version\\": \\"current\\",
\\"frontMatter\\": { \\"frontMatter\\": {
\\"custom_edit_url\\": \\"https://github.com/customUrl/docs/lorem.md\\", \\"custom_edit_url\\": \\"https://github.com/customUrl/docs/lorem.md\\",
@ -294,6 +330,7 @@ Object {
\\"sourceDirName\\": \\".\\", \\"sourceDirName\\": \\".\\",
\\"slug\\": \\"/rootAbsoluteSlug\\", \\"slug\\": \\"/rootAbsoluteSlug\\",
\\"permalink\\": \\"/docs/rootAbsoluteSlug\\", \\"permalink\\": \\"/docs/rootAbsoluteSlug\\",
\\"tags\\": [],
\\"version\\": \\"current\\", \\"version\\": \\"current\\",
\\"frontMatter\\": { \\"frontMatter\\": {
\\"slug\\": \\"/rootAbsoluteSlug\\" \\"slug\\": \\"/rootAbsoluteSlug\\"
@ -309,6 +346,7 @@ Object {
\\"sourceDirName\\": \\".\\", \\"sourceDirName\\": \\".\\",
\\"slug\\": \\"/rootRelativeSlug\\", \\"slug\\": \\"/rootRelativeSlug\\",
\\"permalink\\": \\"/docs/rootRelativeSlug\\", \\"permalink\\": \\"/docs/rootRelativeSlug\\",
\\"tags\\": [],
\\"version\\": \\"current\\", \\"version\\": \\"current\\",
\\"frontMatter\\": { \\"frontMatter\\": {
\\"slug\\": \\"rootRelativeSlug\\" \\"slug\\": \\"rootRelativeSlug\\"
@ -324,6 +362,7 @@ Object {
\\"sourceDirName\\": \\".\\", \\"sourceDirName\\": \\".\\",
\\"slug\\": \\"/hey/rootResolvedSlug\\", \\"slug\\": \\"/hey/rootResolvedSlug\\",
\\"permalink\\": \\"/docs/hey/rootResolvedSlug\\", \\"permalink\\": \\"/docs/hey/rootResolvedSlug\\",
\\"tags\\": [],
\\"version\\": \\"current\\", \\"version\\": \\"current\\",
\\"frontMatter\\": { \\"frontMatter\\": {
\\"slug\\": \\"./hey/ho/../rootResolvedSlug\\" \\"slug\\": \\"./hey/ho/../rootResolvedSlug\\"
@ -339,6 +378,7 @@ Object {
\\"sourceDirName\\": \\".\\", \\"sourceDirName\\": \\".\\",
\\"slug\\": \\"/rootTryToEscapeSlug\\", \\"slug\\": \\"/rootTryToEscapeSlug\\",
\\"permalink\\": \\"/docs/rootTryToEscapeSlug\\", \\"permalink\\": \\"/docs/rootTryToEscapeSlug\\",
\\"tags\\": [],
\\"version\\": \\"current\\", \\"version\\": \\"current\\",
\\"frontMatter\\": { \\"frontMatter\\": {
\\"slug\\": \\"../../../../../../../../rootTryToEscapeSlug\\" \\"slug\\": \\"../../../../../../../../rootTryToEscapeSlug\\"
@ -354,6 +394,7 @@ Object {
\\"sourceDirName\\": \\"slugs\\", \\"sourceDirName\\": \\"slugs\\",
\\"slug\\": \\"/absoluteSlug\\", \\"slug\\": \\"/absoluteSlug\\",
\\"permalink\\": \\"/docs/absoluteSlug\\", \\"permalink\\": \\"/docs/absoluteSlug\\",
\\"tags\\": [],
\\"version\\": \\"current\\", \\"version\\": \\"current\\",
\\"frontMatter\\": { \\"frontMatter\\": {
\\"slug\\": \\"/absoluteSlug\\" \\"slug\\": \\"/absoluteSlug\\"
@ -369,6 +410,7 @@ Object {
\\"sourceDirName\\": \\"slugs\\", \\"sourceDirName\\": \\"slugs\\",
\\"slug\\": \\"/slugs/relativeSlug\\", \\"slug\\": \\"/slugs/relativeSlug\\",
\\"permalink\\": \\"/docs/slugs/relativeSlug\\", \\"permalink\\": \\"/docs/slugs/relativeSlug\\",
\\"tags\\": [],
\\"version\\": \\"current\\", \\"version\\": \\"current\\",
\\"frontMatter\\": { \\"frontMatter\\": {
\\"slug\\": \\"relativeSlug\\" \\"slug\\": \\"relativeSlug\\"
@ -384,6 +426,7 @@ Object {
\\"sourceDirName\\": \\"slugs\\", \\"sourceDirName\\": \\"slugs\\",
\\"slug\\": \\"/slugs/hey/resolvedSlug\\", \\"slug\\": \\"/slugs/hey/resolvedSlug\\",
\\"permalink\\": \\"/docs/slugs/hey/resolvedSlug\\", \\"permalink\\": \\"/docs/slugs/hey/resolvedSlug\\",
\\"tags\\": [],
\\"version\\": \\"current\\", \\"version\\": \\"current\\",
\\"frontMatter\\": { \\"frontMatter\\": {
\\"slug\\": \\"./hey/ho/../resolvedSlug\\" \\"slug\\": \\"./hey/ho/../resolvedSlug\\"
@ -399,11 +442,74 @@ Object {
\\"sourceDirName\\": \\"slugs\\", \\"sourceDirName\\": \\"slugs\\",
\\"slug\\": \\"/tryToEscapeSlug\\", \\"slug\\": \\"/tryToEscapeSlug\\",
\\"permalink\\": \\"/docs/tryToEscapeSlug\\", \\"permalink\\": \\"/docs/tryToEscapeSlug\\",
\\"tags\\": [],
\\"version\\": \\"current\\", \\"version\\": \\"current\\",
\\"frontMatter\\": { \\"frontMatter\\": {
\\"slug\\": \\"../../../../../../../../tryToEscapeSlug\\" \\"slug\\": \\"../../../../../../../../tryToEscapeSlug\\"
} }
}", }",
"tag-docs-tags-tag-1-b3f.json": "{
\\"name\\": \\"tag 1\\",
\\"permalink\\": \\"/docs/tags/tag-1\\",
\\"docs\\": [
{
\\"id\\": \\"foo/baz\\",
\\"title\\": \\"baz\\",
\\"description\\": \\"Images\\",
\\"permalink\\": \\"/docs/foo/bazSlug.html\\"
},
{
\\"id\\": \\"hello\\",
\\"title\\": \\"Hello, World !\\",
\\"description\\": \\"Hi, Endilie here :)\\",
\\"permalink\\": \\"/docs/\\"
}
],
\\"allTagsPath\\": \\"/docs/tags\\"
}",
"tag-docs-tags-tag-2-custom-permalink-825.json": "{
\\"name\\": \\"tag 2\\",
\\"permalink\\": \\"/docs/tags/tag2-custom-permalink\\",
\\"docs\\": [
{
\\"id\\": \\"foo/baz\\",
\\"title\\": \\"baz\\",
\\"description\\": \\"Images\\",
\\"permalink\\": \\"/docs/foo/bazSlug.html\\"
}
],
\\"allTagsPath\\": \\"/docs/tags\\"
}",
"tag-docs-tags-tag-3-ab5.json": "{
\\"name\\": \\"tag 3\\",
\\"permalink\\": \\"/docs/tags/tag-3\\",
\\"docs\\": [
{
\\"id\\": \\"hello\\",
\\"title\\": \\"Hello, World !\\",
\\"description\\": \\"Hi, Endilie here :)\\",
\\"permalink\\": \\"/docs/\\"
}
],
\\"allTagsPath\\": \\"/docs/tags\\"
}",
"tags-list-current-prop-15a.json": "[
{
\\"name\\": \\"tag 1\\",
\\"permalink\\": \\"/docs/tags/tag-1\\",
\\"count\\": 2
},
{
\\"name\\": \\"tag 2\\",
\\"permalink\\": \\"/docs/tags/tag2-custom-permalink\\",
\\"count\\": 1
},
{
\\"name\\": \\"tag 3\\",
\\"permalink\\": \\"/docs/tags/tag-3\\",
\\"count\\": 1
}
]",
"version-current-metadata-prop-751.json": "{ "version-current-metadata-prop-751.json": "{
\\"pluginId\\": \\"default\\", \\"pluginId\\": \\"default\\",
\\"version\\": \\"current\\", \\"version\\": \\"current\\",
@ -560,6 +666,38 @@ Object {
exports[`simple website content: route config 1`] = ` exports[`simple website content: route config 1`] = `
Array [ Array [
Object {
"component": "@theme/DocTagsListPage",
"exact": true,
"modules": Object {
"tags": "~docs/tags-list-current-prop-15a.json",
},
"path": "/docs/tags",
},
Object {
"component": "@theme/DocTagDocListPage",
"exact": true,
"modules": Object {
"tag": "~docs/tag-docs-tags-tag-1-b3f.json",
},
"path": "/docs/tags/tag-1",
},
Object {
"component": "@theme/DocTagDocListPage",
"exact": true,
"modules": Object {
"tag": "~docs/tag-docs-tags-tag-3-ab5.json",
},
"path": "/docs/tags/tag-3",
},
Object {
"component": "@theme/DocTagDocListPage",
"exact": true,
"modules": Object {
"tag": "~docs/tag-docs-tags-tag-2-custom-permalink-825.json",
},
"path": "/docs/tags/tag2-custom-permalink",
},
Object { Object {
"component": "@theme/DocPage", "component": "@theme/DocPage",
"exact": false, "exact": false,
@ -857,6 +995,7 @@ Object {
\\"sourceDirName\\": \\".\\", \\"sourceDirName\\": \\".\\",
\\"slug\\": \\"/team\\", \\"slug\\": \\"/team\\",
\\"permalink\\": \\"/community/team\\", \\"permalink\\": \\"/community/team\\",
\\"tags\\": [],
\\"version\\": \\"1.0.0\\", \\"version\\": \\"1.0.0\\",
\\"frontMatter\\": {}, \\"frontMatter\\": {},
\\"sidebar\\": \\"version-1.0.0/community\\" \\"sidebar\\": \\"version-1.0.0/community\\"
@ -871,12 +1010,15 @@ Object {
\\"sourceDirName\\": \\".\\", \\"sourceDirName\\": \\".\\",
\\"slug\\": \\"/team\\", \\"slug\\": \\"/team\\",
\\"permalink\\": \\"/community/next/team\\", \\"permalink\\": \\"/community/next/team\\",
\\"tags\\": [],
\\"version\\": \\"current\\", \\"version\\": \\"current\\",
\\"frontMatter\\": { \\"frontMatter\\": {
\\"title\\": \\"Team title translated\\" \\"title\\": \\"Team title translated\\"
}, },
\\"sidebar\\": \\"community\\" \\"sidebar\\": \\"community\\"
}", }",
"tags-list-1-0-0-prop-483.json": "[]",
"tags-list-current-prop-15a.json": "[]",
"version-1-0-0-metadata-prop-608.json": "{ "version-1-0-0-metadata-prop-608.json": "{
\\"pluginId\\": \\"community\\", \\"pluginId\\": \\"community\\",
\\"version\\": \\"1.0.0\\", \\"version\\": \\"1.0.0\\",
@ -954,6 +1096,22 @@ Object {
exports[`versioned website (community) content: route config 1`] = ` exports[`versioned website (community) content: route config 1`] = `
Array [ Array [
Object {
"component": "@theme/DocTagsListPage",
"exact": true,
"modules": Object {
"tags": "~docs/tags-list-current-prop-15a.json",
},
"path": "/community/next/tags",
},
Object {
"component": "@theme/DocTagsListPage",
"exact": true,
"modules": Object {
"tags": "~docs/tags-list-1-0-0-prop-483.json",
},
"path": "/community/tags",
},
Object { Object {
"component": "@theme/DocPage", "component": "@theme/DocPage",
"exact": false, "exact": false,
@ -1106,9 +1264,31 @@ Object {
\\"sourceDirName\\": \\"foo\\", \\"sourceDirName\\": \\"foo\\",
\\"slug\\": \\"/foo/barSlug\\", \\"slug\\": \\"/foo/barSlug\\",
\\"permalink\\": \\"/docs/next/foo/barSlug\\", \\"permalink\\": \\"/docs/next/foo/barSlug\\",
\\"tags\\": [
{
\\"label\\": \\"barTag 1\\",
\\"permalink\\": \\"/docs/next/tags/bar-tag-1\\"
},
{
\\"label\\": \\"barTag-2\\",
\\"permalink\\": \\"/docs/next/tags/bar-tag-2\\"
},
{
\\"label\\": \\"barTag 3\\",
\\"permalink\\": \\"/docs/next/tags/barTag-3-permalink\\"
}
],
\\"version\\": \\"current\\", \\"version\\": \\"current\\",
\\"frontMatter\\": { \\"frontMatter\\": {
\\"slug\\": \\"barSlug\\" \\"slug\\": \\"barSlug\\",
\\"tags\\": [
\\"barTag 1\\",
\\"barTag-2\\",
{
\\"label\\": \\"barTag 3\\",
\\"permalink\\": \\"barTag-3-permalink\\"
}
]
}, },
\\"sidebar\\": \\"docs\\", \\"sidebar\\": \\"docs\\",
\\"next\\": { \\"next\\": {
@ -1126,6 +1306,7 @@ Object {
\\"sourceDirName\\": \\".\\", \\"sourceDirName\\": \\".\\",
\\"slug\\": \\"/\\", \\"slug\\": \\"/\\",
\\"permalink\\": \\"/docs/next/\\", \\"permalink\\": \\"/docs/next/\\",
\\"tags\\": [],
\\"version\\": \\"current\\", \\"version\\": \\"current\\",
\\"frontMatter\\": {}, \\"frontMatter\\": {},
\\"sidebar\\": \\"docs\\", \\"sidebar\\": \\"docs\\",
@ -1144,6 +1325,7 @@ Object {
\\"sourceDirName\\": \\"slugs\\", \\"sourceDirName\\": \\"slugs\\",
\\"slug\\": \\"/absoluteSlug\\", \\"slug\\": \\"/absoluteSlug\\",
\\"permalink\\": \\"/docs/next/absoluteSlug\\", \\"permalink\\": \\"/docs/next/absoluteSlug\\",
\\"tags\\": [],
\\"version\\": \\"current\\", \\"version\\": \\"current\\",
\\"frontMatter\\": { \\"frontMatter\\": {
\\"slug\\": \\"/absoluteSlug\\" \\"slug\\": \\"/absoluteSlug\\"
@ -1159,6 +1341,7 @@ Object {
\\"sourceDirName\\": \\"slugs\\", \\"sourceDirName\\": \\"slugs\\",
\\"slug\\": \\"/slugs/relativeSlug\\", \\"slug\\": \\"/slugs/relativeSlug\\",
\\"permalink\\": \\"/docs/next/slugs/relativeSlug\\", \\"permalink\\": \\"/docs/next/slugs/relativeSlug\\",
\\"tags\\": [],
\\"version\\": \\"current\\", \\"version\\": \\"current\\",
\\"frontMatter\\": { \\"frontMatter\\": {
\\"slug\\": \\"relativeSlug\\" \\"slug\\": \\"relativeSlug\\"
@ -1174,6 +1357,7 @@ Object {
\\"sourceDirName\\": \\"slugs\\", \\"sourceDirName\\": \\"slugs\\",
\\"slug\\": \\"/slugs/hey/resolvedSlug\\", \\"slug\\": \\"/slugs/hey/resolvedSlug\\",
\\"permalink\\": \\"/docs/next/slugs/hey/resolvedSlug\\", \\"permalink\\": \\"/docs/next/slugs/hey/resolvedSlug\\",
\\"tags\\": [],
\\"version\\": \\"current\\", \\"version\\": \\"current\\",
\\"frontMatter\\": { \\"frontMatter\\": {
\\"slug\\": \\"./hey/ho/../resolvedSlug\\" \\"slug\\": \\"./hey/ho/../resolvedSlug\\"
@ -1189,6 +1373,7 @@ Object {
\\"sourceDirName\\": \\"slugs\\", \\"sourceDirName\\": \\"slugs\\",
\\"slug\\": \\"/tryToEscapeSlug\\", \\"slug\\": \\"/tryToEscapeSlug\\",
\\"permalink\\": \\"/docs/next/tryToEscapeSlug\\", \\"permalink\\": \\"/docs/next/tryToEscapeSlug\\",
\\"tags\\": [],
\\"version\\": \\"current\\", \\"version\\": \\"current\\",
\\"frontMatter\\": { \\"frontMatter\\": {
\\"slug\\": \\"../../../../../../../../tryToEscapeSlug\\" \\"slug\\": \\"../../../../../../../../tryToEscapeSlug\\"
@ -1204,6 +1389,7 @@ Object {
\\"sourceDirName\\": \\".\\", \\"sourceDirName\\": \\".\\",
\\"slug\\": \\"/\\", \\"slug\\": \\"/\\",
\\"permalink\\": \\"/docs/1.0.0/\\", \\"permalink\\": \\"/docs/1.0.0/\\",
\\"tags\\": [],
\\"version\\": \\"1.0.0\\", \\"version\\": \\"1.0.0\\",
\\"frontMatter\\": {}, \\"frontMatter\\": {},
\\"sidebar\\": \\"version-1.0.0/docs\\", \\"sidebar\\": \\"version-1.0.0/docs\\",
@ -1222,6 +1408,7 @@ Object {
\\"sourceDirName\\": \\"foo\\", \\"sourceDirName\\": \\"foo\\",
\\"slug\\": \\"/foo/barSlug\\", \\"slug\\": \\"/foo/barSlug\\",
\\"permalink\\": \\"/docs/1.0.0/foo/barSlug\\", \\"permalink\\": \\"/docs/1.0.0/foo/barSlug\\",
\\"tags\\": [],
\\"version\\": \\"1.0.0\\", \\"version\\": \\"1.0.0\\",
\\"frontMatter\\": { \\"frontMatter\\": {
\\"slug\\": \\"barSlug\\" \\"slug\\": \\"barSlug\\"
@ -1242,6 +1429,7 @@ Object {
\\"sourceDirName\\": \\"foo\\", \\"sourceDirName\\": \\"foo\\",
\\"slug\\": \\"/foo/baz\\", \\"slug\\": \\"/foo/baz\\",
\\"permalink\\": \\"/docs/1.0.0/foo/baz\\", \\"permalink\\": \\"/docs/1.0.0/foo/baz\\",
\\"tags\\": [],
\\"version\\": \\"1.0.0\\", \\"version\\": \\"1.0.0\\",
\\"frontMatter\\": {}, \\"frontMatter\\": {},
\\"sidebar\\": \\"version-1.0.0/docs\\", \\"sidebar\\": \\"version-1.0.0/docs\\",
@ -1264,6 +1452,7 @@ Object {
\\"sourceDirName\\": \\"foo\\", \\"sourceDirName\\": \\"foo\\",
\\"slug\\": \\"/foo/bar\\", \\"slug\\": \\"/foo/bar\\",
\\"permalink\\": \\"/docs/foo/bar\\", \\"permalink\\": \\"/docs/foo/bar\\",
\\"tags\\": [],
\\"version\\": \\"1.0.1\\", \\"version\\": \\"1.0.1\\",
\\"frontMatter\\": {}, \\"frontMatter\\": {},
\\"sidebar\\": \\"version-1.0.1/docs\\", \\"sidebar\\": \\"version-1.0.1/docs\\",
@ -1282,6 +1471,7 @@ Object {
\\"sourceDirName\\": \\".\\", \\"sourceDirName\\": \\".\\",
\\"slug\\": \\"/\\", \\"slug\\": \\"/\\",
\\"permalink\\": \\"/docs/\\", \\"permalink\\": \\"/docs/\\",
\\"tags\\": [],
\\"version\\": \\"1.0.1\\", \\"version\\": \\"1.0.1\\",
\\"frontMatter\\": {}, \\"frontMatter\\": {},
\\"sidebar\\": \\"version-1.0.1/docs\\", \\"sidebar\\": \\"version-1.0.1/docs\\",
@ -1300,6 +1490,7 @@ Object {
\\"sourceDirName\\": \\".\\", \\"sourceDirName\\": \\".\\",
\\"slug\\": \\"/rootAbsoluteSlug\\", \\"slug\\": \\"/rootAbsoluteSlug\\",
\\"permalink\\": \\"/docs/withSlugs/rootAbsoluteSlug\\", \\"permalink\\": \\"/docs/withSlugs/rootAbsoluteSlug\\",
\\"tags\\": [],
\\"version\\": \\"withSlugs\\", \\"version\\": \\"withSlugs\\",
\\"frontMatter\\": { \\"frontMatter\\": {
\\"slug\\": \\"/rootAbsoluteSlug\\" \\"slug\\": \\"/rootAbsoluteSlug\\"
@ -1316,6 +1507,7 @@ Object {
\\"sourceDirName\\": \\".\\", \\"sourceDirName\\": \\".\\",
\\"slug\\": \\"/rootRelativeSlug\\", \\"slug\\": \\"/rootRelativeSlug\\",
\\"permalink\\": \\"/docs/withSlugs/rootRelativeSlug\\", \\"permalink\\": \\"/docs/withSlugs/rootRelativeSlug\\",
\\"tags\\": [],
\\"version\\": \\"withSlugs\\", \\"version\\": \\"withSlugs\\",
\\"frontMatter\\": { \\"frontMatter\\": {
\\"slug\\": \\"rootRelativeSlug\\" \\"slug\\": \\"rootRelativeSlug\\"
@ -1331,6 +1523,7 @@ Object {
\\"sourceDirName\\": \\".\\", \\"sourceDirName\\": \\".\\",
\\"slug\\": \\"/hey/rootResolvedSlug\\", \\"slug\\": \\"/hey/rootResolvedSlug\\",
\\"permalink\\": \\"/docs/withSlugs/hey/rootResolvedSlug\\", \\"permalink\\": \\"/docs/withSlugs/hey/rootResolvedSlug\\",
\\"tags\\": [],
\\"version\\": \\"withSlugs\\", \\"version\\": \\"withSlugs\\",
\\"frontMatter\\": { \\"frontMatter\\": {
\\"slug\\": \\"./hey/ho/../rootResolvedSlug\\" \\"slug\\": \\"./hey/ho/../rootResolvedSlug\\"
@ -1346,6 +1539,7 @@ Object {
\\"sourceDirName\\": \\".\\", \\"sourceDirName\\": \\".\\",
\\"slug\\": \\"/rootTryToEscapeSlug\\", \\"slug\\": \\"/rootTryToEscapeSlug\\",
\\"permalink\\": \\"/docs/withSlugs/rootTryToEscapeSlug\\", \\"permalink\\": \\"/docs/withSlugs/rootTryToEscapeSlug\\",
\\"tags\\": [],
\\"version\\": \\"withSlugs\\", \\"version\\": \\"withSlugs\\",
\\"frontMatter\\": { \\"frontMatter\\": {
\\"slug\\": \\"../../../../../../../../rootTryToEscapeSlug\\" \\"slug\\": \\"../../../../../../../../rootTryToEscapeSlug\\"
@ -1361,6 +1555,7 @@ Object {
\\"sourceDirName\\": \\"slugs\\", \\"sourceDirName\\": \\"slugs\\",
\\"slug\\": \\"/absoluteSlug\\", \\"slug\\": \\"/absoluteSlug\\",
\\"permalink\\": \\"/docs/withSlugs/absoluteSlug\\", \\"permalink\\": \\"/docs/withSlugs/absoluteSlug\\",
\\"tags\\": [],
\\"version\\": \\"withSlugs\\", \\"version\\": \\"withSlugs\\",
\\"frontMatter\\": { \\"frontMatter\\": {
\\"slug\\": \\"/absoluteSlug\\" \\"slug\\": \\"/absoluteSlug\\"
@ -1376,6 +1571,7 @@ Object {
\\"sourceDirName\\": \\"slugs\\", \\"sourceDirName\\": \\"slugs\\",
\\"slug\\": \\"/slugs/relativeSlug\\", \\"slug\\": \\"/slugs/relativeSlug\\",
\\"permalink\\": \\"/docs/withSlugs/slugs/relativeSlug\\", \\"permalink\\": \\"/docs/withSlugs/slugs/relativeSlug\\",
\\"tags\\": [],
\\"version\\": \\"withSlugs\\", \\"version\\": \\"withSlugs\\",
\\"frontMatter\\": { \\"frontMatter\\": {
\\"slug\\": \\"relativeSlug\\" \\"slug\\": \\"relativeSlug\\"
@ -1391,6 +1587,7 @@ Object {
\\"sourceDirName\\": \\"slugs\\", \\"sourceDirName\\": \\"slugs\\",
\\"slug\\": \\"/slugs/hey/resolvedSlug\\", \\"slug\\": \\"/slugs/hey/resolvedSlug\\",
\\"permalink\\": \\"/docs/withSlugs/slugs/hey/resolvedSlug\\", \\"permalink\\": \\"/docs/withSlugs/slugs/hey/resolvedSlug\\",
\\"tags\\": [],
\\"version\\": \\"withSlugs\\", \\"version\\": \\"withSlugs\\",
\\"frontMatter\\": { \\"frontMatter\\": {
\\"slug\\": \\"./hey/ho/../resolvedSlug\\" \\"slug\\": \\"./hey/ho/../resolvedSlug\\"
@ -1406,11 +1603,71 @@ Object {
\\"sourceDirName\\": \\"slugs\\", \\"sourceDirName\\": \\"slugs\\",
\\"slug\\": \\"/tryToEscapeSlug\\", \\"slug\\": \\"/tryToEscapeSlug\\",
\\"permalink\\": \\"/docs/withSlugs/tryToEscapeSlug\\", \\"permalink\\": \\"/docs/withSlugs/tryToEscapeSlug\\",
\\"tags\\": [],
\\"version\\": \\"withSlugs\\", \\"version\\": \\"withSlugs\\",
\\"frontMatter\\": { \\"frontMatter\\": {
\\"slug\\": \\"../../../../../../../../tryToEscapeSlug\\" \\"slug\\": \\"../../../../../../../../tryToEscapeSlug\\"
} }
}", }",
"tag-docs-next-tags-bar-tag-1-a8f.json": "{
\\"name\\": \\"barTag 1\\",
\\"permalink\\": \\"/docs/next/tags/bar-tag-1\\",
\\"docs\\": [
{
\\"id\\": \\"foo/bar\\",
\\"title\\": \\"bar\\",
\\"description\\": \\"This is next version of bar.\\",
\\"permalink\\": \\"/docs/next/foo/barSlug\\"
}
],
\\"allTagsPath\\": \\"/docs/next/tags\\"
}",
"tag-docs-next-tags-bar-tag-2-216.json": "{
\\"name\\": \\"barTag-2\\",
\\"permalink\\": \\"/docs/next/tags/bar-tag-2\\",
\\"docs\\": [
{
\\"id\\": \\"foo/bar\\",
\\"title\\": \\"bar\\",
\\"description\\": \\"This is next version of bar.\\",
\\"permalink\\": \\"/docs/next/foo/barSlug\\"
}
],
\\"allTagsPath\\": \\"/docs/next/tags\\"
}",
"tag-docs-next-tags-bar-tag-3-permalink-94a.json": "{
\\"name\\": \\"barTag 3\\",
\\"permalink\\": \\"/docs/next/tags/barTag-3-permalink\\",
\\"docs\\": [
{
\\"id\\": \\"foo/bar\\",
\\"title\\": \\"bar\\",
\\"description\\": \\"This is next version of bar.\\",
\\"permalink\\": \\"/docs/next/foo/barSlug\\"
}
],
\\"allTagsPath\\": \\"/docs/next/tags\\"
}",
"tags-list-1-0-0-prop-483.json": "[]",
"tags-list-1-0-1-prop-c39.json": "[]",
"tags-list-current-prop-15a.json": "[
{
\\"name\\": \\"barTag 1\\",
\\"permalink\\": \\"/docs/next/tags/bar-tag-1\\",
\\"count\\": 1
},
{
\\"name\\": \\"barTag-2\\",
\\"permalink\\": \\"/docs/next/tags/bar-tag-2\\",
\\"count\\": 1
},
{
\\"name\\": \\"barTag 3\\",
\\"permalink\\": \\"/docs/next/tags/barTag-3-permalink\\",
\\"count\\": 1
}
]",
"tags-list-with-slugs-prop-1ca.json": "[]",
"version-1-0-0-metadata-prop-608.json": "{ "version-1-0-0-metadata-prop-608.json": "{
\\"pluginId\\": \\"default\\", \\"pluginId\\": \\"default\\",
\\"version\\": \\"1.0.0\\", \\"version\\": \\"1.0.0\\",
@ -1699,6 +1956,62 @@ Object {
exports[`versioned website content: route config 1`] = ` exports[`versioned website content: route config 1`] = `
Array [ Array [
Object {
"component": "@theme/DocTagsListPage",
"exact": true,
"modules": Object {
"tags": "~docs/tags-list-1-0-0-prop-483.json",
},
"path": "/docs/1.0.0/tags",
},
Object {
"component": "@theme/DocTagsListPage",
"exact": true,
"modules": Object {
"tags": "~docs/tags-list-current-prop-15a.json",
},
"path": "/docs/next/tags",
},
Object {
"component": "@theme/DocTagDocListPage",
"exact": true,
"modules": Object {
"tag": "~docs/tag-docs-next-tags-bar-tag-1-a8f.json",
},
"path": "/docs/next/tags/bar-tag-1",
},
Object {
"component": "@theme/DocTagDocListPage",
"exact": true,
"modules": Object {
"tag": "~docs/tag-docs-next-tags-bar-tag-2-216.json",
},
"path": "/docs/next/tags/bar-tag-2",
},
Object {
"component": "@theme/DocTagDocListPage",
"exact": true,
"modules": Object {
"tag": "~docs/tag-docs-next-tags-bar-tag-3-permalink-94a.json",
},
"path": "/docs/next/tags/barTag-3-permalink",
},
Object {
"component": "@theme/DocTagsListPage",
"exact": true,
"modules": Object {
"tags": "~docs/tags-list-1-0-1-prop-c39.json",
},
"path": "/docs/tags",
},
Object {
"component": "@theme/DocTagsListPage",
"exact": true,
"modules": Object {
"tags": "~docs/tags-list-with-slugs-prop-1ca.json",
},
"path": "/docs/withSlugs/tags",
},
Object { Object {
"component": "@theme/DocPage", "component": "@theme/DocPage",
"exact": false, "exact": false,

View file

@ -187,6 +187,7 @@ describe('simple site', () => {
id: 'bar', id: 'bar',
title: 'Bar', title: 'Bar',
}, },
tags: [],
}); });
await defaultTestUtils.testMeta(path.join('hello.md'), { await defaultTestUtils.testMeta(path.join('hello.md'), {
version: 'current', version: 'current',
@ -202,7 +203,18 @@ describe('simple site', () => {
id: 'hello', id: 'hello',
title: 'Hello, World !', title: 'Hello, World !',
sidebar_label: 'Hello sidebar_label', sidebar_label: 'Hello sidebar_label',
tags: ['tag-1', 'tag 3'],
}, },
tags: [
{
label: 'tag-1',
permalink: '/docs/tags/tag-1',
},
{
label: 'tag 3',
permalink: '/docs/tags/tag-3',
},
],
}); });
}); });
@ -232,7 +244,18 @@ describe('simple site', () => {
id: 'hello', id: 'hello',
title: 'Hello, World !', title: 'Hello, World !',
sidebar_label: 'Hello sidebar_label', sidebar_label: 'Hello sidebar_label',
tags: ['tag-1', 'tag 3'],
}, },
tags: [
{
label: 'tag-1',
permalink: '/docs/tags/tag-1',
},
{
label: 'tag 3',
permalink: '/docs/tags/tag-3',
},
],
}); });
}); });
@ -263,6 +286,7 @@ describe('simple site', () => {
id: 'bar', id: 'bar',
title: 'Bar', title: 'Bar',
}, },
tags: [],
}); });
}); });
@ -297,7 +321,22 @@ describe('simple site', () => {
slug: 'bazSlug.html', slug: 'bazSlug.html',
title: 'baz', title: 'baz',
pagination_label: 'baz pagination_label', pagination_label: 'baz pagination_label',
tags: [
'tag 1',
'tag-1',
{label: 'tag 2', permalink: 'tag2-custom-permalink'},
],
}, },
tags: [
{
label: 'tag 1',
permalink: '/docs/tags/tag-1',
},
{
label: 'tag 2',
permalink: '/docs/tags/tag2-custom-permalink',
},
],
}); });
}); });
@ -319,6 +358,7 @@ describe('simple site', () => {
custom_edit_url: 'https://github.com/customUrl/docs/lorem.md', custom_edit_url: 'https://github.com/customUrl/docs/lorem.md',
unrelated_frontmatter: "won't be part of metadata", unrelated_frontmatter: "won't be part of metadata",
}, },
tags: [],
}); });
}); });
@ -356,7 +396,22 @@ describe('simple site', () => {
slug: 'bazSlug.html', slug: 'bazSlug.html',
title: 'baz', title: 'baz',
pagination_label: 'baz pagination_label', pagination_label: 'baz pagination_label',
tags: [
'tag 1',
'tag-1',
{label: 'tag 2', permalink: 'tag2-custom-permalink'},
],
}, },
tags: [
{
label: 'tag 1',
permalink: '/docs/tags/tag-1',
},
{
label: 'tag 2',
permalink: '/docs/tags/tag2-custom-permalink',
},
],
}); });
expect(editUrlFunction).toHaveBeenCalledTimes(1); expect(editUrlFunction).toHaveBeenCalledTimes(1);
@ -402,6 +457,7 @@ describe('simple site', () => {
lastUpdatedAt: 1539502055, lastUpdatedAt: 1539502055,
formattedLastUpdatedAt: '10/14/2018', formattedLastUpdatedAt: '10/14/2018',
lastUpdatedBy: 'Author', lastUpdatedBy: 'Author',
tags: [],
}); });
}); });
@ -559,6 +615,7 @@ describe('versioned site', () => {
await currentVersionTestUtils.testMeta(path.join('foo', 'bar.md'), { await currentVersionTestUtils.testMeta(path.join('foo', 'bar.md'), {
id: 'foo/bar', id: 'foo/bar',
version: 'current',
unversionedId: 'foo/bar', unversionedId: 'foo/bar',
sourceDirName: 'foo', sourceDirName: 'foo',
isDocsHomePage: false, isDocsHomePage: false,
@ -566,11 +623,35 @@ describe('versioned site', () => {
slug: '/foo/barSlug', slug: '/foo/barSlug',
title: 'bar', title: 'bar',
description: 'This is next version of bar.', description: 'This is next version of bar.',
frontMatter: {slug: 'barSlug'}, frontMatter: {
version: 'current', slug: 'barSlug',
tags: [
'barTag 1',
'barTag-2',
{
label: 'barTag 3',
permalink: 'barTag-3-permalink',
},
],
},
tags: [
{
label: 'barTag 1',
permalink: '/docs/next/tags/bar-tag-1',
},
{
label: 'barTag-2',
permalink: '/docs/next/tags/bar-tag-2',
},
{
label: 'barTag 3',
permalink: '/docs/next/tags/barTag-3-permalink',
},
],
}); });
await currentVersionTestUtils.testMeta(path.join('hello.md'), { await currentVersionTestUtils.testMeta(path.join('hello.md'), {
id: 'hello', id: 'hello',
version: 'current',
unversionedId: 'hello', unversionedId: 'hello',
sourceDirName: '.', sourceDirName: '.',
isDocsHomePage: false, isDocsHomePage: false,
@ -579,7 +660,7 @@ describe('versioned site', () => {
title: 'hello', title: 'hello',
description: 'Hello next !', description: 'Hello next !',
frontMatter: {}, frontMatter: {},
version: 'current', tags: [],
}); });
}); });
@ -597,6 +678,7 @@ describe('versioned site', () => {
description: 'Bar 1.0.0 !', description: 'Bar 1.0.0 !',
frontMatter: {slug: 'barSlug'}, frontMatter: {slug: 'barSlug'},
version: '1.0.0', version: '1.0.0',
tags: [],
}); });
await version100TestUtils.testMeta(path.join('hello.md'), { await version100TestUtils.testMeta(path.join('hello.md'), {
id: 'version-1.0.0/hello', id: 'version-1.0.0/hello',
@ -611,6 +693,7 @@ describe('versioned site', () => {
version: '1.0.0', version: '1.0.0',
source: source:
'@site/i18n/en/docusaurus-plugin-content-docs/version-1.0.0/hello.md', '@site/i18n/en/docusaurus-plugin-content-docs/version-1.0.0/hello.md',
tags: [],
}); });
await version101TestUtils.testMeta(path.join('foo', 'bar.md'), { await version101TestUtils.testMeta(path.join('foo', 'bar.md'), {
id: 'version-1.0.1/foo/bar', id: 'version-1.0.1/foo/bar',
@ -623,6 +706,7 @@ describe('versioned site', () => {
description: 'Bar 1.0.1 !', description: 'Bar 1.0.1 !',
version: '1.0.1', version: '1.0.1',
frontMatter: {}, frontMatter: {},
tags: [],
}); });
await version101TestUtils.testMeta(path.join('hello.md'), { await version101TestUtils.testMeta(path.join('hello.md'), {
id: 'version-1.0.1/hello', id: 'version-1.0.1/hello',
@ -635,6 +719,7 @@ describe('versioned site', () => {
description: 'Hello 1.0.1 !', description: 'Hello 1.0.1 !',
version: '1.0.1', version: '1.0.1',
frontMatter: {}, frontMatter: {},
tags: [],
}); });
}); });
@ -729,6 +814,7 @@ describe('versioned site', () => {
source: source:
'@site/i18n/en/docusaurus-plugin-content-docs/version-1.0.0/hello.md', '@site/i18n/en/docusaurus-plugin-content-docs/version-1.0.0/hello.md',
editUrl: hardcodedEditUrl, editUrl: hardcodedEditUrl,
tags: [],
}); });
expect(editUrlFunction).toHaveBeenCalledTimes(1); expect(editUrlFunction).toHaveBeenCalledTimes(1);
@ -771,6 +857,7 @@ describe('versioned site', () => {
'@site/i18n/en/docusaurus-plugin-content-docs/version-1.0.0/hello.md', '@site/i18n/en/docusaurus-plugin-content-docs/version-1.0.0/hello.md',
editUrl: editUrl:
'https://github.com/facebook/docusaurus/edit/main/website/versioned_docs/version-1.0.0/hello.md', 'https://github.com/facebook/docusaurus/edit/main/website/versioned_docs/version-1.0.0/hello.md',
tags: [],
}); });
}); });
@ -804,6 +891,7 @@ describe('versioned site', () => {
'@site/i18n/en/docusaurus-plugin-content-docs/version-1.0.0/hello.md', '@site/i18n/en/docusaurus-plugin-content-docs/version-1.0.0/hello.md',
editUrl: editUrl:
'https://github.com/facebook/docusaurus/edit/main/website/docs/hello.md', 'https://github.com/facebook/docusaurus/edit/main/website/docs/hello.md',
tags: [],
}); });
}); });
@ -838,6 +926,7 @@ describe('versioned site', () => {
'@site/i18n/fr/docusaurus-plugin-content-docs/version-1.0.0/hello.md', '@site/i18n/fr/docusaurus-plugin-content-docs/version-1.0.0/hello.md',
editUrl: editUrl:
'https://github.com/facebook/docusaurus/edit/main/website/i18n/fr/docusaurus-plugin-content-docs/version-1.0.0/hello.md', 'https://github.com/facebook/docusaurus/edit/main/website/i18n/fr/docusaurus-plugin-content-docs/version-1.0.0/hello.md',
tags: [],
}); });
}); });
@ -873,6 +962,7 @@ describe('versioned site', () => {
'@site/i18n/fr/docusaurus-plugin-content-docs/version-1.0.0/hello.md', '@site/i18n/fr/docusaurus-plugin-content-docs/version-1.0.0/hello.md',
editUrl: editUrl:
'https://github.com/facebook/docusaurus/edit/main/website/i18n/fr/docusaurus-plugin-content-docs/current/hello.md', 'https://github.com/facebook/docusaurus/edit/main/website/i18n/fr/docusaurus-plugin-content-docs/current/hello.md',
tags: [],
}); });
}); });
}); });

View file

@ -60,6 +60,7 @@ const defaultDocMetadata: Partial<DocMetadata> = {
lastUpdatedAt: undefined, lastUpdatedAt: undefined,
lastUpdatedBy: undefined, lastUpdatedBy: undefined,
formattedLastUpdatedAt: undefined, formattedLastUpdatedAt: undefined,
tags: [],
}; };
const createFakeActions = (contentDir: string) => { const createFakeActions = (contentDir: string) => {
@ -364,7 +365,23 @@ describe('simple website', () => {
title: 'baz', title: 'baz',
slug: 'bazSlug.html', slug: 'bazSlug.html',
pagination_label: 'baz pagination_label', pagination_label: 'baz pagination_label',
tags: [
'tag 1',
'tag-1', // This one will be de-duplicated as it would lead to the same permalink as the first
{label: 'tag 2', permalink: 'tag2-custom-permalink'},
],
}, },
tags: [
{
label: 'tag 1',
permalink: '/docs/tags/tag-1',
},
{
label: 'tag 2',
permalink: '/docs/tags/tag2-custom-permalink',
},
],
}); });
expect(findDocById(currentVersion, 'hello')).toEqual({ expect(findDocById(currentVersion, 'hello')).toEqual({
@ -392,7 +409,18 @@ describe('simple website', () => {
id: 'hello', id: 'hello',
title: 'Hello, World !', title: 'Hello, World !',
sidebar_label: 'Hello sidebar_label', sidebar_label: 'Hello sidebar_label',
tags: ['tag-1', 'tag 3'],
}, },
tags: [
{
label: 'tag-1',
permalink: '/docs/tags/tag-1',
},
{
label: 'tag 3',
permalink: '/docs/tags/tag-3',
},
],
}); });
expect(getDocById(currentVersion, 'foo/bar')).toEqual({ expect(getDocById(currentVersion, 'foo/bar')).toEqual({
@ -579,6 +607,11 @@ describe('versioned website', () => {
description: 'This is next version of bar.', description: 'This is next version of bar.',
frontMatter: { frontMatter: {
slug: 'barSlug', slug: 'barSlug',
tags: [
'barTag 1',
'barTag-2',
{label: 'barTag 3', permalink: 'barTag-3-permalink'},
],
}, },
version: 'current', version: 'current',
sidebar: 'docs', sidebar: 'docs',
@ -586,6 +619,11 @@ describe('versioned website', () => {
title: 'hello', title: 'hello',
permalink: '/docs/next/', permalink: '/docs/next/',
}, },
tags: [
{label: 'barTag 1', permalink: '/docs/next/tags/bar-tag-1'},
{label: 'barTag-2', permalink: '/docs/next/tags/bar-tag-2'},
{label: 'barTag 3', permalink: '/docs/next/tags/barTag-3-permalink'},
],
}); });
expect(getDocById(currentVersion, 'hello')).toEqual({ expect(getDocById(currentVersion, 'hello')).toEqual({
...defaultDocMetadata, ...defaultDocMetadata,

View file

@ -48,6 +48,8 @@ describe('normalizeDocsPluginOptions', () => {
numberPrefixParser: DefaultNumberPrefixParser, numberPrefixParser: DefaultNumberPrefixParser,
docLayoutComponent: '@theme/DocPage', docLayoutComponent: '@theme/DocPage',
docItemComponent: '@theme/DocItem', docItemComponent: '@theme/DocItem',
docTagDocListComponent: '@theme/DocTagDocListPage',
docTagsListComponent: '@theme/DocTagsListPage',
remarkPlugins: [markdownPluginsObjectStub], remarkPlugins: [markdownPluginsObjectStub],
rehypePlugins: [markdownPluginsFunctionStub], rehypePlugins: [markdownPluginsFunctionStub],
beforeDefaultRehypePlugins: [], beforeDefaultRehypePlugins: [],

View file

@ -0,0 +1,62 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import {toTagDocListProp} from '../props';
describe('toTagDocListProp', () => {
type Params = Parameters<typeof toTagDocListProp>[0];
type Tag = Params['tag'];
type Doc = Params['docs'][number];
const allTagsPath = '/all/tags';
test('should work', () => {
const tag: Tag = {
name: 'tag1',
permalink: '/tag1',
docIds: ['id1', 'id3'],
};
const doc1: Doc = {
id: 'id1',
title: 'ZZZ 1',
description: 'Description 1',
permalink: '/doc1',
};
const doc2: Doc = {
id: 'id2',
title: 'XXX 2',
description: 'Description 2',
permalink: '/doc2',
};
const doc3: Doc = {
id: 'id3',
title: 'AAA 3',
description: 'Description 3',
permalink: '/doc3',
};
const doc4: Doc = {
id: 'id4',
title: 'UUU 4',
description: 'Description 4',
permalink: '/doc4',
};
const result = toTagDocListProp({
allTagsPath,
tag,
docs: [doc1, doc2, doc3, doc4],
});
expect(result).toEqual({
allTagsPath,
name: tag.name,
permalink: tag.permalink,
docs: [doc3, doc1], // docs sorted by title, ignore "id5" absence
});
});
});

View file

@ -77,6 +77,7 @@ describe('simple site', () => {
isLast: true, isLast: true,
routePriority: -1, routePriority: -1,
sidebarFilePath: undefined, sidebarFilePath: undefined,
tagsPath: '/docs/tags',
versionLabel: 'Next', versionLabel: 'Next',
versionName: 'current', versionName: 'current',
versionPath: '/docs', versionPath: '/docs',
@ -111,6 +112,7 @@ describe('simple site', () => {
{ {
...vCurrent, ...vCurrent,
versionPath: '/myBaseUrl/docs', versionPath: '/myBaseUrl/docs',
tagsPath: '/myBaseUrl/docs/tags',
}, },
]); ]);
}); });
@ -141,6 +143,7 @@ describe('simple site', () => {
versionLabel: 'current-label', versionLabel: 'current-label',
routePriority: undefined, routePriority: undefined,
sidebarFilePath: undefined, sidebarFilePath: undefined,
tagsPath: '/myBaseUrl/docs/current-path/tags',
versionEditUrl: undefined, versionEditUrl: undefined,
versionEditUrlLocalized: undefined, versionEditUrlLocalized: undefined,
}, },
@ -232,6 +235,7 @@ describe('versioned site, pluginId=default', () => {
isLast: false, isLast: false,
routePriority: undefined, routePriority: undefined,
sidebarFilePath: path.join(versionedSiteDir, 'sidebars.json'), sidebarFilePath: path.join(versionedSiteDir, 'sidebars.json'),
tagsPath: '/docs/next/tags',
versionLabel: 'Next', versionLabel: 'Next',
versionName: 'current', versionName: 'current',
versionPath: '/docs/next', versionPath: '/docs/next',
@ -250,6 +254,7 @@ describe('versioned site, pluginId=default', () => {
versionedSiteDir, versionedSiteDir,
'versioned_sidebars/version-1.0.1-sidebars.json', 'versioned_sidebars/version-1.0.1-sidebars.json',
), ),
tagsPath: '/docs/tags',
versionLabel: '1.0.1', versionLabel: '1.0.1',
versionName: '1.0.1', versionName: '1.0.1',
versionPath: '/docs', versionPath: '/docs',
@ -268,6 +273,7 @@ describe('versioned site, pluginId=default', () => {
versionedSiteDir, versionedSiteDir,
'versioned_sidebars/version-1.0.0-sidebars.json', 'versioned_sidebars/version-1.0.0-sidebars.json',
), ),
tagsPath: '/docs/1.0.0/tags',
versionLabel: '1.0.0', versionLabel: '1.0.0',
versionName: '1.0.0', versionName: '1.0.0',
versionPath: '/docs/1.0.0', versionPath: '/docs/1.0.0',
@ -289,6 +295,7 @@ describe('versioned site, pluginId=default', () => {
versionedSiteDir, versionedSiteDir,
'versioned_sidebars/version-withSlugs-sidebars.json', 'versioned_sidebars/version-withSlugs-sidebars.json',
), ),
tagsPath: '/docs/withSlugs/tags',
versionLabel: 'withSlugs', versionLabel: 'withSlugs',
versionName: 'withSlugs', versionName: 'withSlugs',
versionPath: '/docs/withSlugs', versionPath: '/docs/withSlugs',
@ -377,6 +384,7 @@ describe('versioned site, pluginId=default', () => {
expect(versionsMetadata).toEqual([ expect(versionsMetadata).toEqual([
{ {
...vCurrent, ...vCurrent,
tagsPath: '/docs/current-path/tags',
versionPath: '/docs/current-path', versionPath: '/docs/current-path',
versionBanner: 'unmaintained', versionBanner: 'unmaintained',
}, },
@ -384,6 +392,7 @@ describe('versioned site, pluginId=default', () => {
...v101, ...v101,
isLast: false, isLast: false,
routePriority: undefined, routePriority: undefined,
tagsPath: '/docs/1.0.1/tags',
versionPath: '/docs/1.0.1', versionPath: '/docs/1.0.1',
versionBanner: 'unreleased', versionBanner: 'unreleased',
}, },
@ -391,6 +400,7 @@ describe('versioned site, pluginId=default', () => {
...v100, ...v100,
isLast: true, isLast: true,
routePriority: -1, routePriority: -1,
tagsPath: '/docs/tags',
versionLabel: '1.0.0-label', versionLabel: '1.0.0-label',
versionPath: '/docs', versionPath: '/docs',
versionBanner: 'unreleased', versionBanner: 'unreleased',
@ -528,6 +538,7 @@ describe('versioned site, pluginId=default', () => {
...vCurrent, ...vCurrent,
isLast: true, isLast: true,
routePriority: -1, routePriority: -1,
tagsPath: '/docs/tags',
versionPath: '/docs', versionPath: '/docs',
versionBanner: 'none', versionBanner: 'none',
}, },
@ -648,6 +659,7 @@ describe('versioned site, pluginId=community', () => {
isLast: false, isLast: false,
routePriority: undefined, routePriority: undefined,
sidebarFilePath: path.join(versionedSiteDir, 'sidebars.json'), sidebarFilePath: path.join(versionedSiteDir, 'sidebars.json'),
tagsPath: '/communityBasePath/next/tags',
versionLabel: 'Next', versionLabel: 'Next',
versionName: 'current', versionName: 'current',
versionPath: '/communityBasePath/next', versionPath: '/communityBasePath/next',
@ -669,6 +681,7 @@ describe('versioned site, pluginId=community', () => {
versionedSiteDir, versionedSiteDir,
'community_versioned_sidebars/version-1.0.0-sidebars.json', 'community_versioned_sidebars/version-1.0.0-sidebars.json',
), ),
tagsPath: '/communityBasePath/tags',
versionLabel: '1.0.0', versionLabel: '1.0.0',
versionName: '1.0.0', versionName: '1.0.0',
versionPath: '/communityBasePath', versionPath: '/communityBasePath',
@ -716,6 +729,7 @@ describe('versioned site, pluginId=community', () => {
...vCurrent, ...vCurrent,
isLast: true, isLast: true,
routePriority: -1, routePriority: -1,
tagsPath: '/communityBasePath/tags',
versionPath: '/communityBasePath', versionPath: '/communityBasePath',
versionBanner: 'none', versionBanner: 'none',
}, },

View file

@ -15,6 +15,7 @@ import {
parseMarkdownString, parseMarkdownString,
posixPath, posixPath,
Globby, Globby,
normalizeFrontMatterTags,
} from '@docusaurus/utils'; } from '@docusaurus/utils';
import {LoadContext} from '@docusaurus/types'; import {LoadContext} from '@docusaurus/types';
@ -252,6 +253,7 @@ function doProcessDocMetadata({
slug: docSlug, slug: docSlug,
permalink, permalink,
editUrl: customEditURL !== undefined ? customEditURL : getDocEditUrl(), editUrl: customEditURL !== undefined ? customEditURL : getDocEditUrl(),
tags: normalizeFrontMatterTags(versionMetadata.tagsPath, frontMatter.tags),
version: versionMetadata.versionName, version: versionMetadata.versionName,
lastUpdatedBy: lastUpdate.lastUpdatedBy, lastUpdatedBy: lastUpdate.lastUpdatedBy,
lastUpdatedAt: lastUpdate.lastUpdatedAt, lastUpdatedAt: lastUpdate.lastUpdatedAt,

View file

@ -37,19 +37,22 @@ import {
LoadedVersion, LoadedVersion,
DocFile, DocFile,
DocsMarkdownOption, DocsMarkdownOption,
VersionTag,
} from './types'; } from './types';
import {RuleSetRule} from 'webpack'; import {RuleSetRule} from 'webpack';
import {cliDocsVersionCommand} from './cli'; import {cliDocsVersionCommand} from './cli';
import {VERSIONS_JSON_FILE} from './constants'; import {VERSIONS_JSON_FILE} from './constants';
import {flatten, keyBy, compact, mapValues} from 'lodash'; import {flatten, keyBy, compact, mapValues} from 'lodash';
import {toGlobalDataVersion} from './globalData'; import {toGlobalDataVersion} from './globalData';
import {toVersionMetadataProp} from './props'; import {toTagDocListProp, toVersionMetadataProp} from './props';
import { import {
translateLoadedContent, translateLoadedContent,
getLoadedContentTranslationFiles, getLoadedContentTranslationFiles,
} from './translations'; } from './translations';
import {CategoryMetadataFilenamePattern} from './sidebarItemsGenerator'; import {CategoryMetadataFilenamePattern} from './sidebarItemsGenerator';
import chalk from 'chalk'; import chalk from 'chalk';
import {getVersionTags} from './tags';
import {PropTagsListPage} from '@docusaurus/plugin-content-docs-types';
export default function pluginContentDocs( export default function pluginContentDocs(
context: LoadContext, context: LoadContext,
@ -314,9 +317,60 @@ export default function pluginContentDocs(
return routes.sort((a, b) => a.path.localeCompare(b.path)); return routes.sort((a, b) => a.path.localeCompare(b.path));
}; };
async function createVersionTagsRoutes(loadedVersion: LoadedVersion) {
const versionTags = getVersionTags(loadedVersion.docs);
async function createTagsListPage() {
const tagsProp: PropTagsListPage['tags'] = Object.values(
versionTags,
).map((tagValue) => ({
name: tagValue.name,
permalink: tagValue.permalink,
count: tagValue.docIds.length,
}));
const tagsPropPath = await createData(
`${docuHash(`tags-list-${loadedVersion.versionName}-prop`)}.json`,
JSON.stringify(tagsProp, null, 2),
);
addRoute({
path: loadedVersion.tagsPath,
exact: true,
component: options.docTagsListComponent,
modules: {
tags: aliasedSource(tagsPropPath),
},
});
}
async function createTagDocListPage(tag: VersionTag) {
const tagProps = toTagDocListProp({
allTagsPath: loadedVersion.tagsPath,
tag,
docs: loadedVersion.docs,
});
const tagPropPath = await createData(
`${docuHash(`tag-${tag.permalink}`)}.json`,
JSON.stringify(tagProps, null, 2),
);
addRoute({
path: tag.permalink,
component: options.docTagDocListComponent,
exact: true,
modules: {
tag: aliasedSource(tagPropPath),
},
});
}
await createTagsListPage();
await Promise.all(Object.values(versionTags).map(createTagDocListPage));
}
async function doCreateVersionRoutes( async function doCreateVersionRoutes(
loadedVersion: LoadedVersion, loadedVersion: LoadedVersion,
): Promise<void> { ): Promise<void> {
await createVersionTagsRoutes(loadedVersion);
const versionMetadata = toVersionMetadataProp(pluginId, loadedVersion); const versionMetadata = toVersionMetadataProp(pluginId, loadedVersion);
const versionMetadataPropPath = await createData( const versionMetadataPropPath = await createData(
`${docuHash( `${docuHash(

View file

@ -33,6 +33,8 @@ export const DEFAULT_OPTIONS: Omit<PluginOptions, 'id' | 'sidebarPath'> = {
numberPrefixParser: DefaultNumberPrefixParser, numberPrefixParser: DefaultNumberPrefixParser,
docLayoutComponent: '@theme/DocPage', docLayoutComponent: '@theme/DocPage',
docItemComponent: '@theme/DocItem', docItemComponent: '@theme/DocItem',
docTagDocListComponent: '@theme/DocTagDocListPage',
docTagsListComponent: '@theme/DocTagsListPage',
remarkPlugins: [], remarkPlugins: [],
rehypePlugins: [], rehypePlugins: [],
beforeDefaultRemarkPlugins: [], beforeDefaultRemarkPlugins: [],
@ -94,6 +96,12 @@ export const OptionsSchema = Joi.object({
.default(() => DEFAULT_OPTIONS.numberPrefixParser), .default(() => DEFAULT_OPTIONS.numberPrefixParser),
docLayoutComponent: Joi.string().default(DEFAULT_OPTIONS.docLayoutComponent), docLayoutComponent: Joi.string().default(DEFAULT_OPTIONS.docLayoutComponent),
docItemComponent: Joi.string().default(DEFAULT_OPTIONS.docItemComponent), docItemComponent: Joi.string().default(DEFAULT_OPTIONS.docItemComponent),
docTagsListComponent: Joi.string().default(
DEFAULT_OPTIONS.docTagsListComponent,
),
docTagDocListComponent: Joi.string().default(
DEFAULT_OPTIONS.docTagDocListComponent,
),
remarkPlugins: RemarkPluginsSchema.default(DEFAULT_OPTIONS.remarkPlugins), remarkPlugins: RemarkPluginsSchema.default(DEFAULT_OPTIONS.remarkPlugins),
rehypePlugins: RehypePluginsSchema.default(DEFAULT_OPTIONS.rehypePlugins), rehypePlugins: RehypePluginsSchema.default(DEFAULT_OPTIONS.rehypePlugins),
beforeDefaultRemarkPlugins: RemarkPluginsSchema.default( beforeDefaultRemarkPlugins: RemarkPluginsSchema.default(

View file

@ -9,6 +9,9 @@ declare module '@docusaurus/plugin-content-docs-types' {
type VersionBanner = import('./types').VersionBanner; type VersionBanner = import('./types').VersionBanner;
type GlobalDataVersion = import('./types').GlobalVersion; type GlobalDataVersion = import('./types').GlobalVersion;
type GlobalDataDoc = import('./types').GlobalDoc; type GlobalDataDoc = import('./types').GlobalDoc;
type VersionTag = import('./types').VersionTag;
export type {GlobalDataVersion, GlobalDataDoc};
export type PropVersionMetadata = { export type PropVersionMetadata = {
pluginId: string; pluginId: string;
@ -43,7 +46,26 @@ declare module '@docusaurus/plugin-content-docs-types' {
[sidebarId: string]: PropSidebarItem[]; [sidebarId: string]: PropSidebarItem[];
}; };
export type {GlobalDataVersion, GlobalDataDoc}; export type PropTagDocListDoc = {
id: string;
title: string;
description: string;
permalink: string;
};
export type PropTagDocList = {
allTagsPath: string;
name: string; // normalized name/label of the tag
permalink: string; // pathname of the tag
docs: PropTagDocListDoc[];
};
export type PropTagsListPage = {
tags: {
name: string;
permalink: string;
count: number;
}[];
};
} }
declare module '@theme/DocItem' { declare module '@theme/DocItem' {
@ -79,6 +101,10 @@ declare module '@theme/DocItem' {
readonly version?: string; readonly version?: string;
readonly previous?: {readonly permalink: string; readonly title: string}; readonly previous?: {readonly permalink: string; readonly title: string};
readonly next?: {readonly permalink: string; readonly title: string}; readonly next?: {readonly permalink: string; readonly title: string};
readonly tags: readonly {
readonly label: string;
readonly permalink: string;
}[];
}; };
export type Props = { export type Props = {
@ -97,6 +123,19 @@ declare module '@theme/DocItem' {
export default DocItem; export default DocItem;
} }
declare module '@theme/DocItemFooter' {
import type {Props} from '@theme/DocItem';
export default function DocItemFooter(props: Props): JSX.Element;
}
declare module '@theme/DocTagsListPage' {
import type {PropTagsListPage} from '@docusaurus/plugin-content-docs-types';
export type Props = PropTagsListPage;
export default function DocItemFooter(props: Props): JSX.Element;
}
declare module '@theme/DocVersionBanner' { declare module '@theme/DocVersionBanner' {
import type {PropVersionMetadata} from '@docusaurus/plugin-content-docs-types'; import type {PropVersionMetadata} from '@docusaurus/plugin-content-docs-types';

View file

@ -10,13 +10,17 @@ import {
SidebarItemDoc, SidebarItemDoc,
SidebarItemLink, SidebarItemLink,
SidebarItem, SidebarItem,
VersionTag,
DocMetadata,
} from './types'; } from './types';
import { import type {
PropSidebars, PropSidebars,
PropVersionMetadata, PropVersionMetadata,
PropSidebarItem, PropSidebarItem,
PropTagDocList,
PropTagDocListDoc,
} from '@docusaurus/plugin-content-docs-types'; } from '@docusaurus/plugin-content-docs-types';
import {keyBy, mapValues} from 'lodash'; import {compact, keyBy, mapValues} from 'lodash';
export function toSidebarsProp(loadedVersion: LoadedVersion): PropSidebars { export function toSidebarsProp(loadedVersion: LoadedVersion): PropSidebars {
const docsById = keyBy(loadedVersion.docs, (doc) => doc.id); const docsById = keyBy(loadedVersion.docs, (doc) => doc.id);
@ -79,3 +83,34 @@ export function toVersionMetadataProp(
docsSidebars: toSidebarsProp(loadedVersion), docsSidebars: toSidebarsProp(loadedVersion),
}; };
} }
export function toTagDocListProp({
allTagsPath,
tag,
docs,
}: {
allTagsPath: string;
tag: VersionTag;
docs: Pick<DocMetadata, 'id' | 'title' | 'description' | 'permalink'>[];
}): PropTagDocList {
function toDocListProp(): PropTagDocListDoc[] {
const list = compact(
tag.docIds.map((id) => docs.find((doc) => doc.id === id)),
);
// Sort docs by title
list.sort((doc1, doc2) => doc1.title.localeCompare(doc2.title));
return list.map((doc) => ({
id: doc.id,
title: doc.title,
description: doc.description,
permalink: doc.permalink,
}));
}
return {
name: tag.name,
permalink: tag.permalink,
docs: toDocListProp(),
allTagsPath,
};
}

View file

@ -0,0 +1,21 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import {groupTaggedItems} from '@docusaurus/utils';
import {VersionTags, DocMetadata} from './types';
import {mapValues} from 'lodash';
export function getVersionTags(docs: DocMetadata[]): VersionTags {
const groups = groupTaggedItems(docs, (doc) => doc.tags);
return mapValues(groups, (group) => {
return {
name: group.tag.label,
docIds: group.items.map((item) => item.id),
permalink: group.tag.permalink,
};
});
}

View file

@ -9,7 +9,8 @@
/// <reference types="@docusaurus/module-type-aliases" /> /// <reference types="@docusaurus/module-type-aliases" />
import type {RemarkAndRehypePluginOptions} from '@docusaurus/mdx-loader'; import type {RemarkAndRehypePluginOptions} from '@docusaurus/mdx-loader';
import { import type {Tag, FrontMatterTag} from '@docusaurus/utils';
import type {
BrokenMarkdownLink as IBrokenMarkdownLink, BrokenMarkdownLink as IBrokenMarkdownLink,
ContentPaths, ContentPaths,
} from '@docusaurus/utils/lib/markdownLinks'; } from '@docusaurus/utils/lib/markdownLinks';
@ -28,6 +29,7 @@ export type VersionMetadata = ContentPaths & {
versionName: VersionName; // 1.0.0 versionName: VersionName; // 1.0.0
versionLabel: string; // Version 1.0.0 versionLabel: string; // Version 1.0.0
versionPath: string; // /baseUrl/docs/1.0.0 versionPath: string; // /baseUrl/docs/1.0.0
tagsPath: string;
versionEditUrl?: string | undefined; versionEditUrl?: string | undefined;
versionEditUrlLocalized?: string | undefined; versionEditUrlLocalized?: string | undefined;
versionBanner: VersionBanner; versionBanner: VersionBanner;
@ -90,6 +92,8 @@ export type PluginOptions = MetadataOptions &
exclude: string[]; exclude: string[];
docLayoutComponent: string; docLayoutComponent: string;
docItemComponent: string; docItemComponent: string;
docTagDocListComponent: string;
docTagsListComponent: string;
admonitions: Record<string, unknown>; admonitions: Record<string, unknown>;
disableVersioning: boolean; disableVersioning: boolean;
includeCurrentVersion: boolean; includeCurrentVersion: boolean;
@ -200,6 +204,7 @@ export type DocFrontMatter = {
/* eslint-disable camelcase */ /* eslint-disable camelcase */
id?: string; id?: string;
title?: string; title?: string;
tags?: FrontMatterTag[];
hide_title?: boolean; hide_title?: boolean;
hide_table_of_contents?: boolean; hide_table_of_contents?: boolean;
keywords?: string[]; keywords?: string[];
@ -227,6 +232,7 @@ export type DocMetadataBase = LastUpdateData & {
permalink: string; permalink: string;
sidebarPosition?: number; sidebarPosition?: number;
editUrl?: string | null; editUrl?: string | null;
tags: Tag[];
frontMatter: DocFrontMatter & Record<string, unknown>; frontMatter: DocFrontMatter & Record<string, unknown>;
}; };
@ -244,6 +250,16 @@ export type DocMetadata = DocMetadataBase & {
export type SourceToPermalink = { export type SourceToPermalink = {
[source: string]: string; [source: string]: string;
}; };
export type VersionTag = {
name: string; // normalized name/label of the tag
docIds: string[]; // all doc ids having this tag
permalink: string; // pathname of the tag
};
export type VersionTags = {
[key: string]: VersionTag;
};
export type LoadedVersion = VersionMetadata & { export type LoadedVersion = VersionMetadata & {
versionPath: string; versionPath: string;
mainDocId: string; mainDocId: string;

View file

@ -370,10 +370,15 @@ function createVersionMetadata({
// Because /docs/:route` should always be after `/docs/versionName/:route`. // Because /docs/:route` should always be after `/docs/versionName/:route`.
const routePriority = versionPathPart === '' ? -1 : undefined; const routePriority = versionPathPart === '' ? -1 : undefined;
// the path that will be used to refer the docs tags
// example below will be using /docs/tags
const tagsPath = normalizeUrl([versionPath, 'tags']);
return { return {
versionName, versionName,
versionLabel, versionLabel,
versionPath, versionPath,
tagsPath,
versionEditUrl: versionEditUrls?.versionEditUrl, versionEditUrl: versionEditUrls?.versionEditUrl,
versionEditUrlLocalized: versionEditUrls?.versionEditUrlLocalized, versionEditUrlLocalized: versionEditUrls?.versionEditUrlLocalized,
versionBanner: getVersionBanner({ versionBanner: getVersionBanner({

View file

@ -0,0 +1,39 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import React from 'react';
import Layout from '@theme/Layout';
import Link from '@docusaurus/Link';
import type {Props} from '@theme/BlogTagsListPage';
function DocTagsListPage(props: Props): JSX.Element {
const {tags} = props;
const renderAllTags = () => (
<>
{Object.keys(tags).map((tag) => (
<Link
href={tags[tag].permalink}
key={tag}
className="btn btn-primary list-inline-item my-2">
{tags[tag].name}{' '}
<span className="badge badge-light">{tags[tag].count}</span>
</Link>
))}
</>
);
return (
<Layout title="Tags" description="Blog Tags">
<div className="container my-3 justify-content-center">
<h1 className="text-primary">Tags</h1>
<ul className="my-xl-4 list-inline">{renderAllTags()}</ul>
</div>
</Layout>
);
}
export default DocTagsListPage;

View file

@ -42,6 +42,8 @@
"theme.docs.sidebar.collapseButtonTitle": "طي الشريط الجانبي", "theme.docs.sidebar.collapseButtonTitle": "طي الشريط الجانبي",
"theme.docs.sidebar.expandButtonAriaLabel": "توسيع الشريط الجانبي", "theme.docs.sidebar.expandButtonAriaLabel": "توسيع الشريط الجانبي",
"theme.docs.sidebar.expandButtonTitle": "توسيع الشريط الجانبي", "theme.docs.sidebar.expandButtonTitle": "توسيع الشريط الجانبي",
"theme.docs.tagDocListPageTitle": "{nDocsTagged} with \"{tagName}\"",
"theme.docs.tagDocListPageTitle.nDocsTagged": "One doc tagged|{count} docs tagged",
"theme.docs.versions.latestVersionLinkLabel": "احدث اصدار", "theme.docs.versions.latestVersionLinkLabel": "احدث اصدار",
"theme.docs.versions.latestVersionSuggestionLabel": "للحصول على أحدث الوثائق، راجع {latestVersionLink} ({versionLabel}).", "theme.docs.versions.latestVersionSuggestionLabel": "للحصول على أحدث الوثائق، راجع {latestVersionLink} ({versionLabel}).",
"theme.docs.versions.unmaintainedVersionLabel": "هذه هي وثائق {siteTitle} {versionLabel}، التي لم تعد تتم صيانتها بشكل نشط.", "theme.docs.versions.unmaintainedVersionLabel": "هذه هي وثائق {siteTitle} {versionLabel}، التي لم تعد تتم صيانتها بشكل نشط.",

View file

@ -85,6 +85,10 @@
"theme.docs.sidebar.expandButtonAriaLabel___DESCRIPTION": "The ARIA label and title attribute for expand button of doc sidebar", "theme.docs.sidebar.expandButtonAriaLabel___DESCRIPTION": "The ARIA label and title attribute for expand button of doc sidebar",
"theme.docs.sidebar.expandButtonTitle": "Expand sidebar", "theme.docs.sidebar.expandButtonTitle": "Expand sidebar",
"theme.docs.sidebar.expandButtonTitle___DESCRIPTION": "The ARIA label and title attribute for expand button of doc sidebar", "theme.docs.sidebar.expandButtonTitle___DESCRIPTION": "The ARIA label and title attribute for expand button of doc sidebar",
"theme.docs.tagDocListPageTitle": "{nDocsTagged} with \"{tagName}\"",
"theme.docs.tagDocListPageTitle___DESCRIPTION": "The title of the page for a docs tag",
"theme.docs.tagDocListPageTitle.nDocsTagged": "One doc tagged|{count} docs tagged",
"theme.docs.tagDocListPageTitle.nDocsTagged___DESCRIPTION": "Pluralized label for \"{count} docs tagged\". Use as much plural forms (separated by \"|\") as your language support (see https://www.unicode.org/cldr/cldr-aux/charts/34/supplemental/language_plural_rules.html)",
"theme.docs.versions.latestVersionLinkLabel": "latest version", "theme.docs.versions.latestVersionLinkLabel": "latest version",
"theme.docs.versions.latestVersionLinkLabel___DESCRIPTION": "The label used for the latest version suggestion link label", "theme.docs.versions.latestVersionLinkLabel___DESCRIPTION": "The label used for the latest version suggestion link label",
"theme.docs.versions.latestVersionSuggestionLabel": "For up-to-date documentation, see the {latestVersionLink} ({versionLabel}).", "theme.docs.versions.latestVersionSuggestionLabel": "For up-to-date documentation, see the {latestVersionLink} ({versionLabel}).",

View file

@ -42,6 +42,8 @@
"theme.docs.sidebar.collapseButtonTitle": "সাইডবারটি সঙ্কুচিত করুন", "theme.docs.sidebar.collapseButtonTitle": "সাইডবারটি সঙ্কুচিত করুন",
"theme.docs.sidebar.expandButtonAriaLabel": "সাইডবারটি প্রসারিত করুন", "theme.docs.sidebar.expandButtonAriaLabel": "সাইডবারটি প্রসারিত করুন",
"theme.docs.sidebar.expandButtonTitle": "সাইডবারটি প্রসারিত করুন", "theme.docs.sidebar.expandButtonTitle": "সাইডবারটি প্রসারিত করুন",
"theme.docs.tagDocListPageTitle": "{nDocsTagged} with \"{tagName}\"",
"theme.docs.tagDocListPageTitle.nDocsTagged": "One doc tagged|{count} docs tagged",
"theme.docs.versions.latestVersionLinkLabel": "লেটেস্ট ভার্সন", "theme.docs.versions.latestVersionLinkLabel": "লেটেস্ট ভার্সন",
"theme.docs.versions.latestVersionSuggestionLabel": "আপ-টু-ডেট ডকুমেন্টেশনের জন্য, {latestVersionLink} ({versionLabel}) দেখুন।", "theme.docs.versions.latestVersionSuggestionLabel": "আপ-টু-ডেট ডকুমেন্টেশনের জন্য, {latestVersionLink} ({versionLabel}) দেখুন।",
"theme.docs.versions.unmaintainedVersionLabel": "এটি {siteTitle} {versionLabel} এর জন্যে ডকুমেন্টেশন, যা আর সক্রিয়ভাবে রক্ষণাবেক্ষণ করা হয় না।", "theme.docs.versions.unmaintainedVersionLabel": "এটি {siteTitle} {versionLabel} এর জন্যে ডকুমেন্টেশন, যা আর সক্রিয়ভাবে রক্ষণাবেক্ষণ করা হয় না।",

View file

@ -42,6 +42,8 @@
"theme.docs.sidebar.collapseButtonTitle": "Zavřít postranní lištu", "theme.docs.sidebar.collapseButtonTitle": "Zavřít postranní lištu",
"theme.docs.sidebar.expandButtonAriaLabel": "Otevřít postranní lištu", "theme.docs.sidebar.expandButtonAriaLabel": "Otevřít postranní lištu",
"theme.docs.sidebar.expandButtonTitle": "Otevřít postranní lištu", "theme.docs.sidebar.expandButtonTitle": "Otevřít postranní lištu",
"theme.docs.tagDocListPageTitle": "{nDocsTagged} with \"{tagName}\"",
"theme.docs.tagDocListPageTitle.nDocsTagged": "One doc tagged|{count} docs tagged",
"theme.docs.versions.latestVersionLinkLabel": "Nejnovější verze", "theme.docs.versions.latestVersionLinkLabel": "Nejnovější verze",
"theme.docs.versions.latestVersionSuggestionLabel": "Aktuální dokumentace viz {latestVersionLink} ({versionLabel}).", "theme.docs.versions.latestVersionSuggestionLabel": "Aktuální dokumentace viz {latestVersionLink} ({versionLabel}).",
"theme.docs.versions.unmaintainedVersionLabel": "Tato dokumentace je pro {siteTitle} {versionLabel}, která už není aktivně udržována.", "theme.docs.versions.unmaintainedVersionLabel": "Tato dokumentace je pro {siteTitle} {versionLabel}, která už není aktivně udržována.",

View file

@ -42,6 +42,8 @@
"theme.docs.sidebar.collapseButtonTitle": "Sammenlæg sidenavigation", "theme.docs.sidebar.collapseButtonTitle": "Sammenlæg sidenavigation",
"theme.docs.sidebar.expandButtonAriaLabel": "Udvid sidenavigation", "theme.docs.sidebar.expandButtonAriaLabel": "Udvid sidenavigation",
"theme.docs.sidebar.expandButtonTitle": "Udvid sidenavigation", "theme.docs.sidebar.expandButtonTitle": "Udvid sidenavigation",
"theme.docs.tagDocListPageTitle": "{nDocsTagged} with \"{tagName}\"",
"theme.docs.tagDocListPageTitle.nDocsTagged": "One doc tagged|{count} docs tagged",
"theme.docs.versions.latestVersionLinkLabel": "seneste version", "theme.docs.versions.latestVersionLinkLabel": "seneste version",
"theme.docs.versions.latestVersionSuggestionLabel": "For seneste dokumentation, se {latestVersionLink} ({versionLabel}).", "theme.docs.versions.latestVersionSuggestionLabel": "For seneste dokumentation, se {latestVersionLink} ({versionLabel}).",
"theme.docs.versions.unmaintainedVersionLabel": "Dette er dokumentationen for {siteTitle} {versionLabel}, som ikke længere bliver aktivt vedligeholdt.", "theme.docs.versions.unmaintainedVersionLabel": "Dette er dokumentationen for {siteTitle} {versionLabel}, som ikke længere bliver aktivt vedligeholdt.",

View file

@ -42,6 +42,8 @@
"theme.docs.sidebar.collapseButtonTitle": "Seitenleiste einklappen", "theme.docs.sidebar.collapseButtonTitle": "Seitenleiste einklappen",
"theme.docs.sidebar.expandButtonAriaLabel": "Seitenleiste ausklappen", "theme.docs.sidebar.expandButtonAriaLabel": "Seitenleiste ausklappen",
"theme.docs.sidebar.expandButtonTitle": "Seitenleiste ausklappen", "theme.docs.sidebar.expandButtonTitle": "Seitenleiste ausklappen",
"theme.docs.tagDocListPageTitle": "{nDocsTagged} with \"{tagName}\"",
"theme.docs.tagDocListPageTitle.nDocsTagged": "One doc tagged|{count} docs tagged",
"theme.docs.versions.latestVersionLinkLabel": "letzte Version", "theme.docs.versions.latestVersionLinkLabel": "letzte Version",
"theme.docs.versions.latestVersionSuggestionLabel": "Für die aktuellste Dokumentation bitte auf {latestVersionLink} ({versionLabel}) gehen.", "theme.docs.versions.latestVersionSuggestionLabel": "Für die aktuellste Dokumentation bitte auf {latestVersionLink} ({versionLabel}) gehen.",
"theme.docs.versions.unmaintainedVersionLabel": "Das ist die Dokumentation für {siteTitle} {versionLabel} und wird nicht weiter gewartet.", "theme.docs.versions.unmaintainedVersionLabel": "Das ist die Dokumentation für {siteTitle} {versionLabel} und wird nicht weiter gewartet.",

View file

@ -42,6 +42,8 @@
"theme.docs.sidebar.collapseButtonTitle": "Colapsar barra lateral", "theme.docs.sidebar.collapseButtonTitle": "Colapsar barra lateral",
"theme.docs.sidebar.expandButtonAriaLabel": "Expandir barra lateral", "theme.docs.sidebar.expandButtonAriaLabel": "Expandir barra lateral",
"theme.docs.sidebar.expandButtonTitle": "Expandir barra lateral", "theme.docs.sidebar.expandButtonTitle": "Expandir barra lateral",
"theme.docs.tagDocListPageTitle": "{nDocsTagged} with \"{tagName}\"",
"theme.docs.tagDocListPageTitle.nDocsTagged": "One doc tagged|{count} docs tagged",
"theme.docs.versions.latestVersionLinkLabel": "última versión", "theme.docs.versions.latestVersionLinkLabel": "última versión",
"theme.docs.versions.latestVersionSuggestionLabel": "Para documentación actualizada, ver {latestVersionLink} ({versionLabel}).", "theme.docs.versions.latestVersionSuggestionLabel": "Para documentación actualizada, ver {latestVersionLink} ({versionLabel}).",
"theme.docs.versions.unmaintainedVersionLabel": "Esta es documentación para {siteTitle} {versionLabel}, que ya no se mantiene activamente.", "theme.docs.versions.unmaintainedVersionLabel": "Esta es documentación para {siteTitle} {versionLabel}, que ya no se mantiene activamente.",

View file

@ -42,6 +42,8 @@
"theme.docs.sidebar.collapseButtonTitle": "بستن نوار کناری", "theme.docs.sidebar.collapseButtonTitle": "بستن نوار کناری",
"theme.docs.sidebar.expandButtonAriaLabel": "باز کردن نوار کناری", "theme.docs.sidebar.expandButtonAriaLabel": "باز کردن نوار کناری",
"theme.docs.sidebar.expandButtonTitle": "باز کردن نوار کناری", "theme.docs.sidebar.expandButtonTitle": "باز کردن نوار کناری",
"theme.docs.tagDocListPageTitle": "{nDocsTagged} with \"{tagName}\"",
"theme.docs.tagDocListPageTitle.nDocsTagged": "One doc tagged|{count} docs tagged",
"theme.docs.versions.latestVersionLinkLabel": "آخرین نسخه", "theme.docs.versions.latestVersionLinkLabel": "آخرین نسخه",
"theme.docs.versions.latestVersionSuggestionLabel": "برای دیدن آخرین نسخه این متن، نسخه {latestVersionLink} ({versionLabel}) را ببینید.", "theme.docs.versions.latestVersionSuggestionLabel": "برای دیدن آخرین نسخه این متن، نسخه {latestVersionLink} ({versionLabel}) را ببینید.",
"theme.docs.versions.unmaintainedVersionLabel": "نسخه {siteTitle} {versionLabel} دیگر به روزرسانی نمی شود.", "theme.docs.versions.unmaintainedVersionLabel": "نسخه {siteTitle} {versionLabel} دیگر به روزرسانی نمی شود.",

View file

@ -42,6 +42,8 @@
"theme.docs.sidebar.collapseButtonTitle": "Itupî ang sidebar", "theme.docs.sidebar.collapseButtonTitle": "Itupî ang sidebar",
"theme.docs.sidebar.expandButtonAriaLabel": "Palakihin ang sidebar", "theme.docs.sidebar.expandButtonAriaLabel": "Palakihin ang sidebar",
"theme.docs.sidebar.expandButtonTitle": "Palakihin ang sidebar", "theme.docs.sidebar.expandButtonTitle": "Palakihin ang sidebar",
"theme.docs.tagDocListPageTitle": "{nDocsTagged} with \"{tagName}\"",
"theme.docs.tagDocListPageTitle.nDocsTagged": "One doc tagged|{count} docs tagged",
"theme.docs.versions.latestVersionLinkLabel": "pinakahuling bersiyón", "theme.docs.versions.latestVersionLinkLabel": "pinakahuling bersiyón",
"theme.docs.versions.latestVersionSuggestionLabel": "Para sa up-to-date na dokumentasyón, tingnan ang {latestVersionLink} ({versionLabel}).", "theme.docs.versions.latestVersionSuggestionLabel": "Para sa up-to-date na dokumentasyón, tingnan ang {latestVersionLink} ({versionLabel}).",
"theme.docs.versions.unmaintainedVersionLabel": "Ito ay dokumentasyón para sa {siteTitle} {versionLabel} na hindi na aktibong mine-maintain.", "theme.docs.versions.unmaintainedVersionLabel": "Ito ay dokumentasyón para sa {siteTitle} {versionLabel} na hindi na aktibong mine-maintain.",

View file

@ -42,6 +42,8 @@
"theme.docs.sidebar.collapseButtonTitle": "Réduire le menu latéral", "theme.docs.sidebar.collapseButtonTitle": "Réduire le menu latéral",
"theme.docs.sidebar.expandButtonAriaLabel": "Déplier le menu latéral", "theme.docs.sidebar.expandButtonAriaLabel": "Déplier le menu latéral",
"theme.docs.sidebar.expandButtonTitle": "Déplier le menu latéral", "theme.docs.sidebar.expandButtonTitle": "Déplier le menu latéral",
"theme.docs.tagDocListPageTitle": "{nDocsTagged} avec \"{tagName}\"",
"theme.docs.tagDocListPageTitle.nDocsTagged": "Un document tagué|{count} documents tagués",
"theme.docs.versions.latestVersionLinkLabel": "dernière version", "theme.docs.versions.latestVersionLinkLabel": "dernière version",
"theme.docs.versions.latestVersionSuggestionLabel": "Pour une documentation à jour, consultez la {latestVersionLink} ({versionLabel}).", "theme.docs.versions.latestVersionSuggestionLabel": "Pour une documentation à jour, consultez la {latestVersionLink} ({versionLabel}).",
"theme.docs.versions.unmaintainedVersionLabel": "Ceci est la documentation de {siteTitle} {versionLabel}, qui n'est plus activement maintenue.", "theme.docs.versions.unmaintainedVersionLabel": "Ceci est la documentation de {siteTitle} {versionLabel}, qui n'est plus activement maintenue.",

View file

@ -42,6 +42,8 @@
"theme.docs.sidebar.collapseButtonTitle": "סגור", "theme.docs.sidebar.collapseButtonTitle": "סגור",
"theme.docs.sidebar.expandButtonAriaLabel": "פתח", "theme.docs.sidebar.expandButtonAriaLabel": "פתח",
"theme.docs.sidebar.expandButtonTitle": "פתח", "theme.docs.sidebar.expandButtonTitle": "פתח",
"theme.docs.tagDocListPageTitle": "{nDocsTagged} with \"{tagName}\"",
"theme.docs.tagDocListPageTitle.nDocsTagged": "One doc tagged|{count} docs tagged",
"theme.docs.versions.latestVersionLinkLabel": "גרסא אחרונה", "theme.docs.versions.latestVersionLinkLabel": "גרסא אחרונה",
"theme.docs.versions.latestVersionSuggestionLabel": "לדוקומנטאציה עדכנית, ראה {latestVersionLink} ({versionLabel}).", "theme.docs.versions.latestVersionSuggestionLabel": "לדוקומנטאציה עדכנית, ראה {latestVersionLink} ({versionLabel}).",
"theme.docs.versions.unmaintainedVersionLabel": "דוקומנטאציה זו {siteTitle} {versionLabel}, כבר לא נתמכת.", "theme.docs.versions.unmaintainedVersionLabel": "דוקומנטאציה זו {siteTitle} {versionLabel}, כבר לא נתמכת.",

View file

@ -42,6 +42,8 @@
"theme.docs.sidebar.collapseButtonTitle": "साइडबार बंद करें", "theme.docs.sidebar.collapseButtonTitle": "साइडबार बंद करें",
"theme.docs.sidebar.expandButtonAriaLabel": "साइडबार खोलें", "theme.docs.sidebar.expandButtonAriaLabel": "साइडबार खोलें",
"theme.docs.sidebar.expandButtonTitle": "साइडबार खोलें", "theme.docs.sidebar.expandButtonTitle": "साइडबार खोलें",
"theme.docs.tagDocListPageTitle": "{nDocsTagged} with \"{tagName}\"",
"theme.docs.tagDocListPageTitle.nDocsTagged": "One doc tagged|{count} docs tagged",
"theme.docs.versions.latestVersionLinkLabel": "सबसे नया वर्जन", "theme.docs.versions.latestVersionLinkLabel": "सबसे नया वर्जन",
"theme.docs.versions.latestVersionSuggestionLabel": "अप-टू-डेट डॉक्यूमेंटेशन के लिए {latestVersionLink} ({versionLabel}) देखें।", "theme.docs.versions.latestVersionSuggestionLabel": "अप-टू-डेट डॉक्यूमेंटेशन के लिए {latestVersionLink} ({versionLabel}) देखें।",
"theme.docs.versions.unmaintainedVersionLabel": "यह {siteTitle} {versionLabel} के लिए डॉक्यूमेंटेशन है, जिसे अब सक्रिय रूप से नहीं बनाए रखा गया है।", "theme.docs.versions.unmaintainedVersionLabel": "यह {siteTitle} {versionLabel} के लिए डॉक्यूमेंटेशन है, जिसे अब सक्रिय रूप से नहीं बनाए रखा गया है।",

View file

@ -42,6 +42,8 @@
"theme.docs.sidebar.collapseButtonTitle": "サイドバーを隠す", "theme.docs.sidebar.collapseButtonTitle": "サイドバーを隠す",
"theme.docs.sidebar.expandButtonAriaLabel": "サイドバーを開く", "theme.docs.sidebar.expandButtonAriaLabel": "サイドバーを開く",
"theme.docs.sidebar.expandButtonTitle": "サイドバーを開く", "theme.docs.sidebar.expandButtonTitle": "サイドバーを開く",
"theme.docs.tagDocListPageTitle": "{nDocsTagged} with \"{tagName}\"",
"theme.docs.tagDocListPageTitle.nDocsTagged": "One doc tagged|{count} docs tagged",
"theme.docs.versions.latestVersionLinkLabel": "最新バージョン", "theme.docs.versions.latestVersionLinkLabel": "最新バージョン",
"theme.docs.versions.latestVersionSuggestionLabel": "最新のドキュメントは{latestVersionLink} ({versionLabel}) を見てください。", "theme.docs.versions.latestVersionSuggestionLabel": "最新のドキュメントは{latestVersionLink} ({versionLabel}) を見てください。",
"theme.docs.versions.unmaintainedVersionLabel": "これは{siteTitle} {versionLabel}のドキュメントで現在はアクティブにメンテナンスされていません。", "theme.docs.versions.unmaintainedVersionLabel": "これは{siteTitle} {versionLabel}のドキュメントで現在はアクティブにメンテナンスされていません。",

View file

@ -42,6 +42,8 @@
"theme.docs.sidebar.collapseButtonTitle": "사이드바 숨기기", "theme.docs.sidebar.collapseButtonTitle": "사이드바 숨기기",
"theme.docs.sidebar.expandButtonAriaLabel": "사이드바 열기", "theme.docs.sidebar.expandButtonAriaLabel": "사이드바 열기",
"theme.docs.sidebar.expandButtonTitle": "사이드바 열기", "theme.docs.sidebar.expandButtonTitle": "사이드바 열기",
"theme.docs.tagDocListPageTitle": "{nDocsTagged} with \"{tagName}\"",
"theme.docs.tagDocListPageTitle.nDocsTagged": "One doc tagged|{count} docs tagged",
"theme.docs.versions.latestVersionLinkLabel": "최신 버전", "theme.docs.versions.latestVersionLinkLabel": "최신 버전",
"theme.docs.versions.latestVersionSuggestionLabel": "최신 문서는 {latestVersionLink} ({versionLabel})을 확인하세요.", "theme.docs.versions.latestVersionSuggestionLabel": "최신 문서는 {latestVersionLink} ({versionLabel})을 확인하세요.",
"theme.docs.versions.unmaintainedVersionLabel": "{siteTitle} {versionLabel} 문서는 업데이트되지 않습니다.", "theme.docs.versions.unmaintainedVersionLabel": "{siteTitle} {versionLabel} 문서는 업데이트되지 않습니다.",

View file

@ -42,6 +42,8 @@
"theme.docs.sidebar.collapseButtonTitle": "Zwiń boczny panel", "theme.docs.sidebar.collapseButtonTitle": "Zwiń boczny panel",
"theme.docs.sidebar.expandButtonAriaLabel": "Rozszerz boczny panel", "theme.docs.sidebar.expandButtonAriaLabel": "Rozszerz boczny panel",
"theme.docs.sidebar.expandButtonTitle": "Rozszerz boczny panel", "theme.docs.sidebar.expandButtonTitle": "Rozszerz boczny panel",
"theme.docs.tagDocListPageTitle": "{nDocsTagged} with \"{tagName}\"",
"theme.docs.tagDocListPageTitle.nDocsTagged": "One doc tagged|{count} docs tagged",
"theme.docs.versions.latestVersionLinkLabel": "bieżącej wersji", "theme.docs.versions.latestVersionLinkLabel": "bieżącej wersji",
"theme.docs.versions.latestVersionSuggestionLabel": "Aby zobaczyć bieżącą dokumentację, przejdź do wersji {latestVersionLink} ({versionLabel}).", "theme.docs.versions.latestVersionSuggestionLabel": "Aby zobaczyć bieżącą dokumentację, przejdź do wersji {latestVersionLink} ({versionLabel}).",
"theme.docs.versions.unmaintainedVersionLabel": "Ta dokumentacja dotyczy {siteTitle} w wersji {versionLabel} i nie jest już aktywnie aktualizowana.", "theme.docs.versions.unmaintainedVersionLabel": "Ta dokumentacja dotyczy {siteTitle} w wersji {versionLabel} i nie jest już aktywnie aktualizowana.",

View file

@ -42,6 +42,8 @@
"theme.docs.sidebar.collapseButtonTitle": "Fechar painel lateral", "theme.docs.sidebar.collapseButtonTitle": "Fechar painel lateral",
"theme.docs.sidebar.expandButtonAriaLabel": "Expandir painel lateral", "theme.docs.sidebar.expandButtonAriaLabel": "Expandir painel lateral",
"theme.docs.sidebar.expandButtonTitle": "Expandir painel lateral", "theme.docs.sidebar.expandButtonTitle": "Expandir painel lateral",
"theme.docs.tagDocListPageTitle": "{nDocsTagged} with \"{tagName}\"",
"theme.docs.tagDocListPageTitle.nDocsTagged": "One doc tagged|{count} docs tagged",
"theme.docs.versions.latestVersionLinkLabel": "última versão", "theme.docs.versions.latestVersionLinkLabel": "última versão",
"theme.docs.versions.latestVersionSuggestionLabel": "Para a documentação atualizada, veja: {latestVersionLink} ({versionLabel}).", "theme.docs.versions.latestVersionSuggestionLabel": "Para a documentação atualizada, veja: {latestVersionLink} ({versionLabel}).",
"theme.docs.versions.unmaintainedVersionLabel": "Esta é a documentação para {siteTitle} {versionLabel}, que não é mais mantida ativamente.", "theme.docs.versions.unmaintainedVersionLabel": "Esta é a documentação para {siteTitle} {versionLabel}, que não é mais mantida ativamente.",

View file

@ -42,6 +42,8 @@
"theme.docs.sidebar.collapseButtonTitle": "Colapsar barra lateral", "theme.docs.sidebar.collapseButtonTitle": "Colapsar barra lateral",
"theme.docs.sidebar.expandButtonAriaLabel": "Expandir barra lateral", "theme.docs.sidebar.expandButtonAriaLabel": "Expandir barra lateral",
"theme.docs.sidebar.expandButtonTitle": "Expandir barra lateral", "theme.docs.sidebar.expandButtonTitle": "Expandir barra lateral",
"theme.docs.tagDocListPageTitle": "{nDocsTagged} with \"{tagName}\"",
"theme.docs.tagDocListPageTitle.nDocsTagged": "One doc tagged|{count} docs tagged",
"theme.docs.versions.latestVersionLinkLabel": "última versão", "theme.docs.versions.latestVersionLinkLabel": "última versão",
"theme.docs.versions.latestVersionSuggestionLabel": "Para a documentação atualizada, veja: {latestVersionLink} ({versionLabel}).", "theme.docs.versions.latestVersionSuggestionLabel": "Para a documentação atualizada, veja: {latestVersionLink} ({versionLabel}).",
"theme.docs.versions.unmaintainedVersionLabel": "Esta é a documentação para {siteTitle} {versionLabel}, que já não é mantida ativamente.", "theme.docs.versions.unmaintainedVersionLabel": "Esta é a documentação para {siteTitle} {versionLabel}, que já não é mantida ativamente.",

View file

@ -42,6 +42,8 @@
"theme.docs.sidebar.collapseButtonTitle": "Свернуть сайдбар", "theme.docs.sidebar.collapseButtonTitle": "Свернуть сайдбар",
"theme.docs.sidebar.expandButtonAriaLabel": "Развернуть сайдбар", "theme.docs.sidebar.expandButtonAriaLabel": "Развернуть сайдбар",
"theme.docs.sidebar.expandButtonTitle": "Развернуть сайдбар", "theme.docs.sidebar.expandButtonTitle": "Развернуть сайдбар",
"theme.docs.tagDocListPageTitle": "{nDocsTagged} with \"{tagName}\"",
"theme.docs.tagDocListPageTitle.nDocsTagged": "One doc tagged|{count} docs tagged",
"theme.docs.versions.latestVersionLinkLabel": "последней версии", "theme.docs.versions.latestVersionLinkLabel": "последней версии",
"theme.docs.versions.latestVersionSuggestionLabel": "Актуальная документация находится на странице {latestVersionLink} ({versionLabel}).", "theme.docs.versions.latestVersionSuggestionLabel": "Актуальная документация находится на странице {latestVersionLink} ({versionLabel}).",
"theme.docs.versions.unmaintainedVersionLabel": "Это документация {siteTitle} для версии {versionLabel}, которая уже не поддерживается.", "theme.docs.versions.unmaintainedVersionLabel": "Это документация {siteTitle} для версии {versionLabel}, которая уже не поддерживается.",

View file

@ -42,6 +42,8 @@
"theme.docs.sidebar.collapseButtonTitle": "Kenar çubuğunu daralt", "theme.docs.sidebar.collapseButtonTitle": "Kenar çubuğunu daralt",
"theme.docs.sidebar.expandButtonAriaLabel": "Kenar çubuğunu genişlet", "theme.docs.sidebar.expandButtonAriaLabel": "Kenar çubuğunu genişlet",
"theme.docs.sidebar.expandButtonTitle": "Kenar çubuğunu genişlet", "theme.docs.sidebar.expandButtonTitle": "Kenar çubuğunu genişlet",
"theme.docs.tagDocListPageTitle": "{nDocsTagged} with \"{tagName}\"",
"theme.docs.tagDocListPageTitle.nDocsTagged": "One doc tagged|{count} docs tagged",
"theme.docs.versions.latestVersionLinkLabel": "en son sürüm", "theme.docs.versions.latestVersionLinkLabel": "en son sürüm",
"theme.docs.versions.latestVersionSuggestionLabel": "Güncel belgeler için bkz. {latestVersionLink} ({versionLabel}).", "theme.docs.versions.latestVersionSuggestionLabel": "Güncel belgeler için bkz. {latestVersionLink} ({versionLabel}).",
"theme.docs.versions.unmaintainedVersionLabel": "Bu, {siteTitle} {versionLabel} dokümantasyonudur ve bakımı sonlanmıştır.", "theme.docs.versions.unmaintainedVersionLabel": "Bu, {siteTitle} {versionLabel} dokümantasyonudur ve bakımı sonlanmıştır.",

View file

@ -42,6 +42,8 @@
"theme.docs.sidebar.collapseButtonTitle": "Thu gọn thanh bên", "theme.docs.sidebar.collapseButtonTitle": "Thu gọn thanh bên",
"theme.docs.sidebar.expandButtonAriaLabel": "Mở rộng thanh bên", "theme.docs.sidebar.expandButtonAriaLabel": "Mở rộng thanh bên",
"theme.docs.sidebar.expandButtonTitle": "Mở rộng thanh bên", "theme.docs.sidebar.expandButtonTitle": "Mở rộng thanh bên",
"theme.docs.tagDocListPageTitle": "{nDocsTagged} with \"{tagName}\"",
"theme.docs.tagDocListPageTitle.nDocsTagged": "One doc tagged|{count} docs tagged",
"theme.docs.versions.latestVersionLinkLabel": "phiên bản mới nhất", "theme.docs.versions.latestVersionLinkLabel": "phiên bản mới nhất",
"theme.docs.versions.latestVersionSuggestionLabel": "Để xem các cập nhật mới nhất, vui lòng xem phiên bản {latestVersionLink} ({versionLabel}).", "theme.docs.versions.latestVersionSuggestionLabel": "Để xem các cập nhật mới nhất, vui lòng xem phiên bản {latestVersionLink} ({versionLabel}).",
"theme.docs.versions.unmaintainedVersionLabel": "Đây là tài liệu của {siteTitle} {versionLabel}, hiện không còn được bảo trì.", "theme.docs.versions.unmaintainedVersionLabel": "Đây là tài liệu của {siteTitle} {versionLabel}, hiện không còn được bảo trì.",

View file

@ -42,6 +42,8 @@
"theme.docs.sidebar.collapseButtonTitle": "收起侧边栏", "theme.docs.sidebar.collapseButtonTitle": "收起侧边栏",
"theme.docs.sidebar.expandButtonAriaLabel": "展开侧边栏", "theme.docs.sidebar.expandButtonAriaLabel": "展开侧边栏",
"theme.docs.sidebar.expandButtonTitle": "展开侧边栏", "theme.docs.sidebar.expandButtonTitle": "展开侧边栏",
"theme.docs.tagDocListPageTitle": "{nDocsTagged} with \"{tagName}\"",
"theme.docs.tagDocListPageTitle.nDocsTagged": "One doc tagged|{count} docs tagged",
"theme.docs.versions.latestVersionLinkLabel": "最新版本", "theme.docs.versions.latestVersionLinkLabel": "最新版本",
"theme.docs.versions.latestVersionSuggestionLabel": "最新的文档请参阅 {latestVersionLink} ({versionLabel})。", "theme.docs.versions.latestVersionSuggestionLabel": "最新的文档请参阅 {latestVersionLink} ({versionLabel})。",
"theme.docs.versions.unmaintainedVersionLabel": "此为 {siteTitle} {versionLabel} 版的文档,现已不再积极维护。", "theme.docs.versions.unmaintainedVersionLabel": "此为 {siteTitle} {versionLabel} 版的文档,现已不再积极维护。",

View file

@ -42,6 +42,8 @@
"theme.docs.sidebar.collapseButtonTitle": "收起側邊欄", "theme.docs.sidebar.collapseButtonTitle": "收起側邊欄",
"theme.docs.sidebar.expandButtonAriaLabel": "展開側邊欄", "theme.docs.sidebar.expandButtonAriaLabel": "展開側邊欄",
"theme.docs.sidebar.expandButtonTitle": "展開側邊欄", "theme.docs.sidebar.expandButtonTitle": "展開側邊欄",
"theme.docs.tagDocListPageTitle": "{nDocsTagged} with \"{tagName}\"",
"theme.docs.tagDocListPageTitle.nDocsTagged": "One doc tagged|{count} docs tagged",
"theme.docs.versions.latestVersionLinkLabel": "最新版本", "theme.docs.versions.latestVersionLinkLabel": "最新版本",
"theme.docs.versions.latestVersionSuggestionLabel": "最新的文件請參閱 {latestVersionLink} ({versionLabel})。", "theme.docs.versions.latestVersionSuggestionLabel": "最新的文件請參閱 {latestVersionLink} ({versionLabel})。",
"theme.docs.versions.unmaintainedVersionLabel": "此為 {siteTitle} {versionLabel} 版的文件,現已不再積極維護。", "theme.docs.versions.unmaintainedVersionLabel": "此為 {siteTitle} {versionLabel} 版的文件,現已不再積極維護。",

View file

@ -18,6 +18,7 @@ import EditThisPage from '@theme/EditThisPage';
import type {Props} from '@theme/BlogPostItem'; import type {Props} from '@theme/BlogPostItem';
import styles from './styles.module.css'; import styles from './styles.module.css';
import TagsListInline from '@theme/TagsListInline';
// Very simple pluralization: probably good enough for now // Very simple pluralization: probably good enough for now
function useReadingTimePlural() { function useReadingTimePlural() {
@ -156,22 +157,7 @@ function BlogPostItem(props: Props): JSX.Element {
})}> })}>
{tags.length > 0 && ( {tags.length > 0 && (
<div className="col"> <div className="col">
<b> <TagsListInline tags={tags} />
<Translate
id="theme.tags.tagsListLabel"
description="The label alongside a tag list">
Tags:
</Translate>
</b>
{tags.map(({label, permalink: tagPermalink}) => (
<Link
key={tagPermalink}
className="margin-horiz--sm"
to={tagPermalink}>
{label}
</Link>
))}
</div> </div>
)} )}

View file

@ -7,51 +7,17 @@
import React from 'react'; import React from 'react';
import Link from '@docusaurus/Link';
import BlogLayout from '@theme/BlogLayout'; import BlogLayout from '@theme/BlogLayout';
import TagsListByLetter from '@theme/TagsListByLetter';
import type {Props} from '@theme/BlogTagsListPage'; import type {Props} from '@theme/BlogTagsListPage';
import {translate} from '@docusaurus/Translate'; import {
import {ThemeClassNames} from '@docusaurus/theme-common'; ThemeClassNames,
translateTagsPageTitle,
function getCategoryOfTag(tag: string) { } from '@docusaurus/theme-common';
// tag's category should be customizable
return tag[0].toUpperCase();
}
function BlogTagsListPage(props: Props): JSX.Element { function BlogTagsListPage(props: Props): JSX.Element {
const {tags, sidebar} = props; const {tags, sidebar} = props;
const title = translate({ const title = translateTagsPageTitle();
id: 'theme.tags.tagsPageTitle',
message: 'Tags',
description: 'The title of the tag list page',
});
const tagCategories: {[category: string]: string[]} = {};
Object.keys(tags).forEach((tag) => {
const category = getCategoryOfTag(tag);
tagCategories[category] = tagCategories[category] || [];
tagCategories[category].push(tag);
});
const tagsList = Object.entries(tagCategories).sort(([a], [b]) =>
a.localeCompare(b),
);
const tagsSection = tagsList
.map(([category, tagsForCategory]) => (
<article key={category}>
<h2>{category}</h2>
{tagsForCategory.map((tag) => (
<Link
className="padding-right--md"
href={tags[tag].permalink}
key={tag}>
{tags[tag].name} ({tags[tag].count})
</Link>
))}
<hr />
</article>
))
.filter((item) => item != null);
return ( return (
<BlogLayout <BlogLayout
title={title} title={title}
@ -63,7 +29,7 @@ function BlogTagsListPage(props: Props): JSX.Element {
}} }}
sidebar={sidebar}> sidebar={sidebar}>
<h1>{title}</h1> <h1>{title}</h1>
<section className="margin-vert--lg">{tagsSection}</section> <TagsListByLetter tags={Object.values(tags)} />
</BlogLayout> </BlogLayout>
); );
} }

View file

@ -49,7 +49,7 @@ function BlogTagsPostPage(props: Props): JSX.Element {
<BlogLayout <BlogLayout
title={title} title={title}
wrapperClassName={ThemeClassNames.wrapper.blogPages} wrapperClassName={ThemeClassNames.wrapper.blogPages}
pageClassName={ThemeClassNames.page.blogTagsPostPage} pageClassName={ThemeClassNames.page.blogTagPostListPage}
searchMetadatas={{ searchMetadatas={{
// assign unique search tag to exclude this page from search results! // assign unique search tag to exclude this page from search results!
tag: 'blog_tags_posts', tag: 'blog_tags_posts',

View file

@ -13,16 +13,15 @@ import useWindowSize from '@theme/hooks/useWindowSize';
import DocPaginator from '@theme/DocPaginator'; import DocPaginator from '@theme/DocPaginator';
import DocVersionBanner from '@theme/DocVersionBanner'; import DocVersionBanner from '@theme/DocVersionBanner';
import Seo from '@theme/Seo'; import Seo from '@theme/Seo';
import LastUpdated from '@theme/LastUpdated';
import type {Props} from '@theme/DocItem'; import type {Props} from '@theme/DocItem';
import DocItemFooter from '@theme/DocItemFooter';
import TOC from '@theme/TOC'; import TOC from '@theme/TOC';
import TOCCollapsible from '@theme/TOCCollapsible'; import TOCCollapsible from '@theme/TOCCollapsible';
import EditThisPage from '@theme/EditThisPage';
import {MainHeading} from '@theme/Heading'; import {MainHeading} from '@theme/Heading';
import styles from './styles.module.css'; import styles from './styles.module.css';
function DocItem(props: Props): JSX.Element { export default function DocItem(props: Props): JSX.Element {
const {content: DocContent, versionMetadata} = props; const {content: DocContent, versionMetadata} = props;
const {metadata, frontMatter} = DocContent; const {metadata, frontMatter} = DocContent;
const { const {
@ -31,14 +30,7 @@ function DocItem(props: Props): JSX.Element {
hide_title: hideTitle, hide_title: hideTitle,
hide_table_of_contents: hideTableOfContents, hide_table_of_contents: hideTableOfContents,
} = frontMatter; } = frontMatter;
const { const {description, title} = metadata;
description,
title,
editUrl,
lastUpdatedAt,
formattedLastUpdatedAt,
lastUpdatedBy,
} = metadata;
const {pluginId} = useActivePlugin({failfast: true})!; const {pluginId} = useActivePlugin({failfast: true})!;
const versions = useVersions(pluginId); const versions = useVersions(pluginId);
@ -98,23 +90,7 @@ function DocItem(props: Props): JSX.Element {
<DocContent /> <DocContent />
</div> </div>
{(editUrl || lastUpdatedAt || lastUpdatedBy) && ( <DocItemFooter {...props} />
<footer className="row docusaurus-mt-lg">
<div className="col">
{editUrl && <EditThisPage editUrl={editUrl} />}
</div>
<div className={clsx('col', styles.lastUpdated)}>
{(lastUpdatedAt || lastUpdatedBy) && (
<LastUpdated
lastUpdatedAt={lastUpdatedAt}
formattedLastUpdatedAt={formattedLastUpdatedAt}
lastUpdatedBy={lastUpdatedBy}
/>
)}
</div>
</footer>
)}
</article> </article>
<DocPaginator metadata={metadata} /> <DocPaginator metadata={metadata} />
@ -129,5 +105,3 @@ function DocItem(props: Props): JSX.Element {
</> </>
); );
} }
export default DocItem;

View file

@ -10,21 +10,11 @@
margin-top: 0; margin-top: 0;
} }
.lastUpdated {
margin-top: 0.2rem;
font-style: italic;
font-size: smaller;
}
@media only screen and (min-width: 997px) { @media only screen and (min-width: 997px) {
.docItemCol { .docItemCol {
max-width: 75% !important; max-width: 75% !important;
} }
.lastUpdated {
text-align: right;
}
/* Prevent hydration FOUC, as the mobile TOC needs to be server-rendered */ /* Prevent hydration FOUC, as the mobile TOC needs to be server-rendered */
.tocMobile { .tocMobile {
display: none; display: none;

View file

@ -0,0 +1,90 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import React from 'react';
import clsx from 'clsx';
import LastUpdated from '@theme/LastUpdated';
import type {Props} from '@theme/DocItem';
import EditThisPage from '@theme/EditThisPage';
import TagsListInline, {
Props as TagsListInlineProps,
} from '@theme/TagsListInline';
import styles from './styles.module.css';
function TagsRow(props: TagsListInlineProps) {
return (
<div className="row margin-bottom--sm">
<div className="col">
<TagsListInline {...props} />
</div>
</div>
);
}
type EditMetaRowProps = Pick<
Props['content']['metadata'],
'editUrl' | 'lastUpdatedAt' | 'lastUpdatedBy' | 'formattedLastUpdatedAt'
>;
function EditMetaRow({
editUrl,
lastUpdatedAt,
lastUpdatedBy,
formattedLastUpdatedAt,
}: EditMetaRowProps) {
return (
<div className="row">
<div className="col">{editUrl && <EditThisPage editUrl={editUrl} />}</div>
<div className={clsx('col', styles.lastUpdated)}>
{(lastUpdatedAt || lastUpdatedBy) && (
<LastUpdated
lastUpdatedAt={lastUpdatedAt}
formattedLastUpdatedAt={formattedLastUpdatedAt}
lastUpdatedBy={lastUpdatedBy}
/>
)}
</div>
</div>
);
}
export default function DocItemFooter(props: Props): JSX.Element {
const {content: DocContent} = props;
const {metadata} = DocContent;
const {
editUrl,
lastUpdatedAt,
formattedLastUpdatedAt,
lastUpdatedBy,
tags,
} = metadata;
const canDisplayTagsRow = tags.length > 0;
const canDisplayEditMetaRow = !!(editUrl || lastUpdatedAt || lastUpdatedBy);
const canDisplayFooter = canDisplayTagsRow || canDisplayEditMetaRow;
if (!canDisplayFooter) {
return <></>;
}
return (
<footer className="docusaurus-mt-lg">
{canDisplayTagsRow && <TagsRow tags={tags} />}
{canDisplayEditMetaRow && (
<EditMetaRow
editUrl={editUrl}
lastUpdatedAt={lastUpdatedAt}
lastUpdatedBy={lastUpdatedBy}
formattedLastUpdatedAt={formattedLastUpdatedAt}
/>
)}
</footer>
);
}

View file

@ -0,0 +1,18 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
.lastUpdated {
margin-top: 0.2rem;
font-style: italic;
font-size: smaller;
}
@media only screen and (min-width: 997px) {
.lastUpdated {
text-align: right;
}
}

View file

@ -56,7 +56,7 @@ function DocPageContent({
return ( return (
<Layout <Layout
wrapperClassName={ThemeClassNames.wrapper.docPages} wrapperClassName={ThemeClassNames.wrapper.docPages}
pageClassName={ThemeClassNames.page.docPage} pageClassName={ThemeClassNames.page.docsDocPage}
searchMetadatas={{ searchMetadatas={{
version, version,
tag: docVersionSearchTag(pluginId, version), tag: docVersionSearchTag(pluginId, version),

View file

@ -0,0 +1,89 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import React from 'react';
import Layout from '@theme/Layout';
import Link from '@docusaurus/Link';
import {ThemeClassNames, usePluralForm} from '@docusaurus/theme-common';
import type {
PropTagDocList,
PropTagDocListDoc,
} from '@docusaurus/plugin-content-docs-types';
import {translate} from '@docusaurus/Translate';
type Props = {
tag: PropTagDocList;
};
// Very simple pluralization: probably good enough for now
function useNDocsTaggedPlural() {
const {selectMessage} = usePluralForm();
return (count: number) =>
selectMessage(
count,
translate(
{
id: 'theme.docs.tagDocListPageTitle.nDocsTagged',
description:
'Pluralized label for "{count} docs tagged". Use as much plural forms (separated by "|") as your language support (see https://www.unicode.org/cldr/cldr-aux/charts/34/supplemental/language_plural_rules.html)',
message: 'One doc tagged|{count} docs tagged',
},
{count},
),
);
}
function DocItem({doc}: {doc: PropTagDocListDoc}): JSX.Element {
return (
<div className="margin-vert--lg">
<Link to={doc.permalink}>
<h2>{doc.title}</h2>
</Link>
{doc.description && <p>{doc.description}</p>}
</div>
);
}
export default function DocTagDocListPage({tag}: Props): JSX.Element {
const nDocsTaggedPlural = useNDocsTaggedPlural();
const title = translate(
{
id: 'theme.docs.tagDocListPageTitle',
description: 'The title of the page for a docs tag',
message: '{nDocsTagged} with "{tagName}"',
},
{nDocsTagged: nDocsTaggedPlural(tag.docs.length), tagName: tag.name},
);
return (
<Layout
title={title}
wrapperClassName={ThemeClassNames.wrapper.docPages}
pageClassName={ThemeClassNames.page.docsTagDocListPage}
searchMetadatas={{
// assign unique search tag to exclude this page from search results!
tag: 'doc_tag_doc_list',
}}>
<div className="container margin-vert--lg">
<div className="row">
<main className="col col--8 col--offset-2">
<header className="margin-bottom--xl">
<h1>{title}</h1>
<Link href={tag.allTagsPath}>View All Tags</Link>
</header>
<div className="margin-vert--lg">
{tag.docs.map((doc) => (
<DocItem key={doc.id} doc={doc} />
))}
</div>
</main>
</div>
</div>
</Layout>
);
}

View file

@ -0,0 +1,41 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import React from 'react';
import Layout from '@theme/Layout';
import {
ThemeClassNames,
translateTagsPageTitle,
} from '@docusaurus/theme-common';
import TagsListByLetter from '@theme/TagsListByLetter';
import type {Props} from '@theme/DocTagsListPage';
function DocTagsListPage({tags}: Props): JSX.Element {
const title = translateTagsPageTitle();
return (
<Layout
title={title}
wrapperClassName={ThemeClassNames.wrapper.docPages}
pageClassName={ThemeClassNames.page.docsTagsListPage}
searchMetadatas={{
// assign unique search tag to exclude this page from search results!
tag: 'doc_tags_list',
}}>
<div className="container margin-vert--lg">
<div className="row">
<main className="col col--8 col--offset-2">
<h1>{title}</h1>
<TagsListByLetter tags={tags} />
</main>
</div>
</div>
</Layout>
);
}
export default DocTagsListPage;

View file

@ -0,0 +1,44 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import React from 'react';
import Link from '@docusaurus/Link';
import type {Props} from '@theme/TagsListByLetter';
import {listTagsByLetters, TagLetterEntry} from '@docusaurus/theme-common';
function TagLetterEntryItem({letterEntry}: {letterEntry: TagLetterEntry}) {
return (
<div>
<h2>{letterEntry.letter}</h2>
{letterEntry.tags.map((tag) => (
<Link
className="padding-right--md"
href={tag.permalink}
key={tag.permalink}>
{tag.name} ({tag.count})
</Link>
))}
<hr />
</div>
);
}
function TagsListByLetter({tags}: Props): JSX.Element {
const letterList = listTagsByLetters(tags);
return (
<section className="margin-vert--lg">
{letterList.map((letterEntry) => (
<TagLetterEntryItem
key={letterEntry.letter}
letterEntry={letterEntry}
/>
))}
</section>
);
}
export default TagsListByLetter;

View file

@ -0,0 +1,30 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import React from 'react';
import Link from '@docusaurus/Link';
import Translate from '@docusaurus/Translate';
import type {Props} from '@theme/TagsListInline';
export default function TagsListInline({tags}: Props) {
return (
<>
<b>
<Translate
id="theme.tags.tagsListLabel"
description="The label alongside a tag list">
Tags:
</Translate>
</b>
{tags.map(({label, permalink: tagPermalink}) => (
<Link key={tagPermalink} className="margin-horiz--sm" to={tagPermalink}>
{label}
</Link>
))}
</>
);
}

View file

@ -703,3 +703,23 @@ declare module '@theme/IconExternalLink' {
const IconExternalLink: (props: Props) => JSX.Element; const IconExternalLink: (props: Props) => JSX.Element;
export default IconExternalLink; export default IconExternalLink;
} }
declare module '@theme/TagsListByLetter' {
export type TagsListItem = Readonly<{
name: string;
permalink: string;
count: number;
}>;
export type Props = Readonly<{
tags: readonly TagsListItem[];
}>;
export default function TagsListByLetter(props: Props): JSX.Element;
}
declare module '@theme/TagsListInline' {
export type Tag = Readonly<{label: string; permalink}>;
export type Props = Readonly<{
tags: readonly Tag[];
}>;
export default function TagsListInline(props: Props): JSX.Element;
}

View file

@ -14,6 +14,7 @@ const {mapValues, pickBy, difference, orderBy} = require('lodash');
const CodeDirPaths = [ const CodeDirPaths = [
path.join(__dirname, 'lib-next'), path.join(__dirname, 'lib-next'),
// TODO other themes should rather define their own translations in the future? // TODO other themes should rather define their own translations in the future?
path.join(__dirname, '..', 'docusaurus-theme-common', 'lib'),
path.join(__dirname, '..', 'docusaurus-theme-search-algolia', 'src', 'theme'), path.join(__dirname, '..', 'docusaurus-theme-search-algolia', 'src', 'theme'),
path.join(__dirname, '..', 'docusaurus-theme-live-codeblock', 'src', 'theme'), path.join(__dirname, '..', 'docusaurus-theme-live-codeblock', 'src', 'theme'),
path.join(__dirname, '..', 'docusaurus-plugin-pwa', 'src', 'theme'), path.join(__dirname, '..', 'docusaurus-plugin-pwa', 'src', 'theme'),

View file

@ -14,7 +14,7 @@ const {mapValues, pickBy} = require('lodash');
jest.setTimeout(15000); jest.setTimeout(15000);
describe('update-code-translations', () => { describe('update-code-translations', () => {
test(`to have base.json contain all the translations extracted from the theme. Please run "yarn workspace @docusaurus/theme-classic update-code-translations" to keep base.json up-to-date.`, async () => { test(`to have base.json contain EXACTLY all the translations extracted from the theme. Please run "yarn workspace @docusaurus/theme-classic update-code-translations" to keep base.json up-to-date.`, async () => {
const baseMessages = pickBy( const baseMessages = pickBy(
JSON.parse( JSON.parse(
await fs.readFile( await fs.readFile(

View file

@ -28,7 +28,8 @@
"tslib": "^2.1.0" "tslib": "^2.1.0"
}, },
"devDependencies": { "devDependencies": {
"@docusaurus/module-type-aliases": "2.0.0-beta.4" "@docusaurus/module-type-aliases": "2.0.0-beta.4",
"lodash": "^4.17.20"
}, },
"peerDependencies": { "peerDependencies": {
"prism-react-renderer": "^1.2.1", "prism-react-renderer": "^1.2.1",

View file

@ -68,3 +68,6 @@ export {
} from './utils/announcementBarUtils'; } from './utils/announcementBarUtils';
export {useLocalPathname} from './utils/useLocalPathname'; export {useLocalPathname} from './utils/useLocalPathname';
export {translateTagsPageTitle, listTagsByLetters} from './utils/tagsUtils';
export type {TagLetterEntry} from './utils/tagsUtils';

View file

@ -11,8 +11,12 @@ export const ThemeClassNames = {
blogListPage: 'blog-list-page', blogListPage: 'blog-list-page',
blogPostPage: 'blog-post-page', blogPostPage: 'blog-post-page',
blogTagsListPage: 'blog-tags-list-page', blogTagsListPage: 'blog-tags-list-page',
blogTagsPostPage: 'blog-tags-post-page', blogTagPostListPage: 'blog-tags-post-list-page',
docPage: 'doc-page',
docsDocPage: 'docs-doc-page',
docsTagsListPage: 'docs-tags-list-page', // List of tags
docsTagDocListPage: 'docs-tags-doc-list-page', // Docs for a tag
mdxPage: 'mdx-page', mdxPage: 'mdx-page',
}, },
wrapper: { wrapper: {

View file

@ -0,0 +1,66 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import {shuffle} from 'lodash';
import {listTagsByLetters} from '../tagsUtils';
describe('listTagsByLetters', () => {
type Param = Parameters<typeof listTagsByLetters>[0];
type Tag = Param[number];
type Result = ReturnType<typeof listTagsByLetters>;
test('Should create letters list', () => {
const tag1: Tag = {
name: 'tag1',
permalink: '/tag1',
count: 1,
};
const tag2: Tag = {
name: 'Tag2',
permalink: '/tag2',
count: 11,
};
const tagzxy: Tag = {
name: 'zxy',
permalink: '/zxy',
count: 987,
};
const tagAbc: Tag = {
name: 'Abc',
permalink: '/abc',
count: 123,
};
const tagdef: Tag = {
name: 'def',
permalink: '/def',
count: 1,
};
const tagaaa: Tag = {
name: 'aaa',
permalink: '/aaa',
count: 10,
};
const expectedResult: Result = [
{letter: 'A', tags: [tagaaa, tagAbc]},
{letter: 'D', tags: [tagdef]},
{letter: 'T', tags: [tag1, tag2]},
{letter: 'Z', tags: [tagzxy]},
];
// Input order shouldn't matter, output is always consistently sorted
expect(
listTagsByLetters([tag1, tag2, tagzxy, tagAbc, tagdef, tagaaa]),
).toEqual(expectedResult);
expect(
listTagsByLetters([tagzxy, tagdef, tagaaa, tag2, tagAbc, tag1]),
).toEqual(expectedResult);
expect(
listTagsByLetters(shuffle([tagzxy, tagdef, tagaaa, tag2, tagAbc, tag1])),
).toEqual(expectedResult);
});
});

View file

@ -0,0 +1,48 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import {translate} from '@docusaurus/Translate';
export const translateTagsPageTitle = () =>
translate({
id: 'theme.tags.tagsPageTitle',
message: 'Tags',
description: 'The title of the tag list page',
});
type TagsListItem = Readonly<{name: string; permalink: string; count: number}>; // TODO remove duplicated type :s
export type TagLetterEntry = Readonly<{letter: string; tags: TagsListItem[]}>;
function getTagLetter(tag: string): string {
return tag[0].toUpperCase();
}
export function listTagsByLetters(
tags: readonly TagsListItem[],
): TagLetterEntry[] {
// Group by letters
const groups: Record<string, TagsListItem[]> = {};
Object.values(tags).forEach((tag) => {
const letter = getTagLetter(tag.name);
groups[letter] = groups[letter] ?? [];
groups[letter].push(tag);
});
return (
Object.entries(groups)
// Sort letters
.sort(([letter1], [letter2]) => letter1.localeCompare(letter2))
.map(([letter, letterTags]) => {
// Sort tags inside a letter
const sortedTags = letterTags.sort((tag1, tag2) =>
tag1.name.localeCompare(tag2.name),
);
return {letter, tags: sortedTags};
})
);
}

View file

@ -0,0 +1,26 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import Joi from './Joi';
// Enhance the default Joi.string() type so that it can convert number to strings
// If user use frontmatter "tag: 2021", we shouldn't need to ask the user to write "tag: '2021'"
// Also yaml tries to convert patterns like "2019-01-01" to dates automatically
// see https://github.com/facebook/docusaurus/issues/4642
// see https://github.com/sideway/joi/issues/1442#issuecomment-823997884
const JoiFrontMatterString: Joi.Extension = {
type: 'string',
base: Joi.string(),
// Fix Yaml that tries to auto-convert many things to string out of the box
prepare: (value) => {
if (typeof value === 'number' || value instanceof Date) {
return {value: value.toString()};
}
return {value};
},
};
export const JoiFrontMatter: typeof Joi = Joi.extend(JoiFrontMatterString);

View file

@ -6,7 +6,8 @@
*/ */
import Joi from '../Joi'; import Joi from '../Joi';
import {JoiFrontMatter, validateFrontMatter} from '../validationUtils'; import {JoiFrontMatter} from '../JoiFrontMatter';
import {validateFrontMatter} from '../validationUtils';
describe('validateFrontMatter', () => { describe('validateFrontMatter', () => {
test('should accept good values', () => { test('should accept good values', () => {

View file

@ -7,6 +7,7 @@
// /!\ don't remove this export, as we recommend plugin authors to use it // /!\ don't remove this export, as we recommend plugin authors to use it
export {default as Joi} from './Joi'; export {default as Joi} from './Joi';
export {JoiFrontMatter} from './JoiFrontMatter';
export * from './validationUtils'; export * from './validationUtils';
export * from './validationSchemas'; export * from './validationSchemas';

View file

@ -6,6 +6,8 @@
*/ */
import Joi from './Joi'; import Joi from './Joi';
import {isValidPathname} from '@docusaurus/utils'; import {isValidPathname} from '@docusaurus/utils';
import type {Tag} from '@docusaurus/utils';
import {JoiFrontMatter} from './JoiFrontMatter';
export const PluginIdSchema = Joi.string() export const PluginIdSchema = Joi.string()
.regex(/^[a-zA-Z_-]+$/) .regex(/^[a-zA-Z_-]+$/)
@ -55,3 +57,13 @@ export const PathnameSchema = Joi.string()
.message( .message(
'{{#label}} is not a valid pathname. Pathname should start with slash and not contain any domain or query string.', '{{#label}} is not a valid pathname. Pathname should start with slash and not contain any domain or query string.',
); );
export const FrontMatterTagsSchema = JoiFrontMatter.array().items(
JoiFrontMatter.alternatives().try(
JoiFrontMatter.string().required(),
JoiFrontMatter.object<Tag>({
label: JoiFrontMatter.string().required(),
permalink: JoiFrontMatter.string().required(),
}).required(),
),
);

View file

@ -99,24 +99,6 @@ export function normalizeThemeConfig<T>(
return value; return value;
} }
// Enhance the default Joi.string() type so that it can convert number to strings
// If user use frontmatter "tag: 2021", we shouldn't need to ask the user to write "tag: '2021'"
// Also yaml tries to convert patterns like "2019-01-01" to dates automatically
// see https://github.com/facebook/docusaurus/issues/4642
// see https://github.com/sideway/joi/issues/1442#issuecomment-823997884
const JoiFrontMatterString: Joi.Extension = {
type: 'string',
base: Joi.string(),
// Fix Yaml that tries to auto-convert many things to string out of the box
prepare: (value) => {
if (typeof value === 'number' || value instanceof Date) {
return {value: value.toString()};
}
return {value};
},
};
export const JoiFrontMatter: typeof Joi = Joi.extend(JoiFrontMatterString);
export function validateFrontMatter<T>( export function validateFrontMatter<T>(
frontMatter: Record<string, unknown>, frontMatter: Record<string, unknown>,
schema: Joi.ObjectSchema<T>, schema: Joi.ObjectSchema<T>,

View file

@ -12,7 +12,6 @@ import {
genChunkName, genChunkName,
idx, idx,
getSubFolder, getSubFolder,
normalizeUrl,
posixPath, posixPath,
objectWithKeySorted, objectWithKeySorted,
aliasedSitePath, aliasedSitePath,
@ -218,113 +217,6 @@ describe('load utils', () => {
expect(getSubFolder(testE, 'docs')).toBeNull(); expect(getSubFolder(testE, 'docs')).toBeNull();
}); });
test('normalizeUrl', () => {
const asserts = [
{
input: ['/', ''],
output: '/',
},
{
input: ['', '/'],
output: '/',
},
{
input: ['/'],
output: '/',
},
{
input: [''],
output: '',
},
{
input: ['/', '/'],
output: '/',
},
{
input: ['/', 'docs'],
output: '/docs',
},
{
input: ['/', 'docs', 'en', 'next', 'blog'],
output: '/docs/en/next/blog',
},
{
input: ['/test/', '/docs', 'ro', 'doc1'],
output: '/test/docs/ro/doc1',
},
{
input: ['/test/', '/', 'ro', 'doc1'],
output: '/test/ro/doc1',
},
{
input: ['/', '/', '2020/02/29/leap-day'],
output: '/2020/02/29/leap-day',
},
{
input: ['', '/', 'ko', 'hello'],
output: '/ko/hello',
},
{
input: ['hello', 'world'],
output: 'hello/world',
},
{
input: ['http://www.google.com/', 'foo/bar', '?test=123'],
output: 'http://www.google.com/foo/bar?test=123',
},
{
input: ['http:', 'www.google.com///', 'foo/bar', '?test=123'],
output: 'http://www.google.com/foo/bar?test=123',
},
{
input: ['http://foobar.com', '', 'test'],
output: 'http://foobar.com/test',
},
{
input: ['http://foobar.com', '', 'test', '/'],
output: 'http://foobar.com/test/',
},
{
input: ['/', '', 'hello', '', '/', '/', '', '/', '/world'],
output: '/hello/world',
},
{
input: ['', '', '/tt', 'ko', 'hello'],
output: '/tt/ko/hello',
},
{
input: ['', '///hello///', '', '///world'],
output: '/hello/world',
},
{
input: ['', '/hello/', ''],
output: '/hello/',
},
{
input: ['', '/', ''],
output: '/',
},
{
input: ['///', '///'],
output: '/',
},
{
input: ['/', '/hello/world/', '///'],
output: '/hello/world/',
},
];
asserts.forEach((testCase) => {
expect(normalizeUrl(testCase.input)).toBe(testCase.output);
});
expect(() =>
// @ts-expect-error undefined for test
normalizeUrl(['http:example.com', undefined]),
).toThrowErrorMatchingInlineSnapshot(
`"Url must be a string. Received undefined"`,
);
});
test('isValidPathname', () => { test('isValidPathname', () => {
expect(isValidPathname('/')).toBe(true); expect(isValidPathname('/')).toBe(true);
expect(isValidPathname('/hey')).toBe(true); expect(isValidPathname('/hey')).toBe(true);

View file

@ -0,0 +1,117 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import {normalizeUrl} from '../normalizeUrl';
describe('normalizeUrl', () => {
test('should normalize urls correctly', () => {
const asserts = [
{
input: ['/', ''],
output: '/',
},
{
input: ['', '/'],
output: '/',
},
{
input: ['/'],
output: '/',
},
{
input: [''],
output: '',
},
{
input: ['/', '/'],
output: '/',
},
{
input: ['/', 'docs'],
output: '/docs',
},
{
input: ['/', 'docs', 'en', 'next', 'blog'],
output: '/docs/en/next/blog',
},
{
input: ['/test/', '/docs', 'ro', 'doc1'],
output: '/test/docs/ro/doc1',
},
{
input: ['/test/', '/', 'ro', 'doc1'],
output: '/test/ro/doc1',
},
{
input: ['/', '/', '2020/02/29/leap-day'],
output: '/2020/02/29/leap-day',
},
{
input: ['', '/', 'ko', 'hello'],
output: '/ko/hello',
},
{
input: ['hello', 'world'],
output: 'hello/world',
},
{
input: ['http://www.google.com/', 'foo/bar', '?test=123'],
output: 'http://www.google.com/foo/bar?test=123',
},
{
input: ['http:', 'www.google.com///', 'foo/bar', '?test=123'],
output: 'http://www.google.com/foo/bar?test=123',
},
{
input: ['http://foobar.com', '', 'test'],
output: 'http://foobar.com/test',
},
{
input: ['http://foobar.com', '', 'test', '/'],
output: 'http://foobar.com/test/',
},
{
input: ['/', '', 'hello', '', '/', '/', '', '/', '/world'],
output: '/hello/world',
},
{
input: ['', '', '/tt', 'ko', 'hello'],
output: '/tt/ko/hello',
},
{
input: ['', '///hello///', '', '///world'],
output: '/hello/world',
},
{
input: ['', '/hello/', ''],
output: '/hello/',
},
{
input: ['', '/', ''],
output: '/',
},
{
input: ['///', '///'],
output: '/',
},
{
input: ['/', '/hello/world/', '///'],
output: '/hello/world/',
},
];
asserts.forEach((testCase) => {
expect(normalizeUrl(testCase.input)).toBe(testCase.output);
});
expect(() =>
// @ts-expect-error undefined for test
normalizeUrl(['http:example.com', undefined]),
).toThrowErrorMatchingInlineSnapshot(
`"Url must be a string. Received undefined"`,
);
});
});

View file

@ -0,0 +1,183 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import {
normalizeFrontMatterTag,
normalizeFrontMatterTags,
groupTaggedItems,
Tag,
} from '../tags';
describe('normalizeFrontMatterTag', () => {
type Input = Parameters<typeof normalizeFrontMatterTag>[1];
type Output = ReturnType<typeof normalizeFrontMatterTag>;
test('should normalize simple string tag', () => {
const tagsPath = '/all/tags';
const input: Input = 'tag';
const expectedOutput: Output = {
label: 'tag',
permalink: `${tagsPath}/tag`,
};
expect(normalizeFrontMatterTag(tagsPath, input)).toEqual(expectedOutput);
});
test('should normalize complex string tag', () => {
const tagsPath = '/all/tags';
const input: Input = 'some more Complex_tag';
const expectedOutput: Output = {
label: 'some more Complex_tag',
permalink: `${tagsPath}/some-more-complex-tag`,
};
expect(normalizeFrontMatterTag(tagsPath, input)).toEqual(expectedOutput);
});
test('should normalize simple object tag', () => {
const tagsPath = '/all/tags';
const input: Input = {label: 'tag', permalink: 'tagPermalink'};
const expectedOutput: Output = {
label: 'tag',
permalink: `${tagsPath}/tagPermalink`,
};
expect(normalizeFrontMatterTag(tagsPath, input)).toEqual(expectedOutput);
});
test('should normalize complex string tag', () => {
const tagsPath = '/all/tags';
const input: Input = {
label: 'tag complex Label',
permalink: '/MoreComplex/Permalink',
};
const expectedOutput: Output = {
label: 'tag complex Label',
permalink: `${tagsPath}/MoreComplex/Permalink`,
};
expect(normalizeFrontMatterTag(tagsPath, input)).toEqual(expectedOutput);
});
});
describe('normalizeFrontMatterTags', () => {
type Input = Parameters<typeof normalizeFrontMatterTags>[1];
type Output = ReturnType<typeof normalizeFrontMatterTags>;
test('should normalize string list', () => {
const tagsPath = '/all/tags';
const input: Input = ['tag 1', 'tag-1', 'tag 3', 'tag1', 'tag-2'];
// Keep user input order but remove tags that lead to same permalink
const expectedOutput: Output = [
{
label: 'tag 1',
permalink: `${tagsPath}/tag-1`,
},
{
label: 'tag 3',
permalink: `${tagsPath}/tag-3`,
},
{
label: 'tag-2',
permalink: `${tagsPath}/tag-2`,
},
];
expect(normalizeFrontMatterTags(tagsPath, input)).toEqual(expectedOutput);
});
test('should normalize complex mixed list', () => {
const tagsPath = '/all/tags';
const input: Input = [
'tag 1',
{label: 'tag-1', permalink: '/tag-1'},
'tag 3',
'tag1',
{label: 'tag 4', permalink: '/tag4Permalink'},
];
// Keep user input order but remove tags that lead to same permalink
const expectedOutput: Output = [
{
label: 'tag 1',
permalink: `${tagsPath}/tag-1`,
},
{
label: 'tag 3',
permalink: `${tagsPath}/tag-3`,
},
{
label: 'tag 4',
permalink: `${tagsPath}/tag4Permalink`,
},
];
expect(normalizeFrontMatterTags(tagsPath, input)).toEqual(expectedOutput);
});
});
describe('groupTaggedItems', () => {
type SomeTaggedItem = {
id: string;
nested: {
tags: Tag[];
};
};
function groupItems(items: SomeTaggedItem[]) {
return groupTaggedItems(items, (item) => item.nested.tags);
}
type Input = Parameters<typeof groupItems>[0];
type Output = ReturnType<typeof groupItems>;
test('should group items by tag permalink', () => {
const tagGuide = {label: 'Guide', permalink: '/guide'};
const tagTutorial = {label: 'Tutorial', permalink: '/tutorial'};
const tagAPI = {label: 'API', permalink: '/api'};
// This one will be grouped under same permalink and label is ignored
const tagTutorialOtherLabel = {
label: 'TutorialOtherLabel',
permalink: '/tutorial',
};
const item1: SomeTaggedItem = {
id: '1',
nested: {
tags: [
tagGuide,
tagTutorial,
tagAPI,
// Add some duplicates on purpose: they should be filtered
tagGuide,
tagTutorialOtherLabel,
],
},
};
const item2: SomeTaggedItem = {
id: '2',
nested: {
tags: [tagAPI],
},
};
const item3: SomeTaggedItem = {
id: '3',
nested: {
tags: [tagTutorial],
},
};
const item4: SomeTaggedItem = {
id: '4',
nested: {
tags: [tagTutorialOtherLabel],
},
};
const input: Input = [item1, item2, item3, item4];
const expectedOutput: Output = {
'/guide': {tag: tagGuide, items: [item1]},
'/tutorial': {tag: tagTutorial, items: [item1, item3, item4]},
'/api': {tag: tagAPI, items: [item1, item2]},
};
expect(groupItems(input)).toEqual(expectedOutput);
});
});

View file

@ -23,6 +23,10 @@ import resolvePathnameUnsafe from 'resolve-pathname';
import {posixPath as posixPathImport} from './posixPath'; import {posixPath as posixPathImport} from './posixPath';
import {simpleHash, docuHash} from './hashUtils'; import {simpleHash, docuHash} from './hashUtils';
import {normalizeUrl} from './normalizeUrl';
export * from './normalizeUrl';
export * from './tags';
export const posixPath = posixPathImport; export const posixPath = posixPathImport;
@ -190,80 +194,6 @@ export function getSubFolder(file: string, refDir: string): string | null {
return match && match[1]; return match && match[1];
} }
export function normalizeUrl(rawUrls: string[]): string {
const urls = [...rawUrls];
const resultArray = [];
let hasStartingSlash = false;
let hasEndingSlash = false;
// If the first part is a plain protocol, we combine it with the next part.
if (urls[0].match(/^[^/:]+:\/*$/) && urls.length > 1) {
const first = urls.shift();
urls[0] = first + urls[0];
}
// There must be two or three slashes in the file protocol,
// two slashes in anything else.
const replacement = urls[0].match(/^file:\/\/\//) ? '$1:///' : '$1://';
urls[0] = urls[0].replace(/^([^/:]+):\/*/, replacement);
// eslint-disable-next-line
for (let i = 0; i < urls.length; i++) {
let component = urls[i];
if (typeof component !== 'string') {
throw new TypeError(`Url must be a string. Received ${typeof component}`);
}
if (component === '') {
if (i === urls.length - 1 && hasEndingSlash) {
resultArray.push('/');
}
// eslint-disable-next-line
continue;
}
if (component !== '/') {
if (i > 0) {
// Removing the starting slashes for each component but the first.
component = component.replace(
/^[/]+/,
// Special case where the first element of rawUrls is empty ["", "/hello"] => /hello
component[0] === '/' && !hasStartingSlash ? '/' : '',
);
}
hasEndingSlash = component[component.length - 1] === '/';
// Removing the ending slashes for each component but the last.
// For the last component we will combine multiple slashes to a single one.
component = component.replace(/[/]+$/, i < urls.length - 1 ? '' : '/');
}
hasStartingSlash = true;
resultArray.push(component);
}
let str = resultArray.join('/');
// Each input component is now separated by a single slash
// except the possible first plain protocol part.
// Remove trailing slash before parameters or hash.
str = str.replace(/\/(\?|&|#[^!])/g, '$1');
// Replace ? in parameters with &.
const parts = str.split('?');
str = parts.shift() + (parts.length > 0 ? '?' : '') + parts.join('&');
// Dedupe forward slashes in the entire path, avoiding protocol slashes.
str = str.replace(/([^:]\/)\/+/g, '$1');
// Dedupe forward slashes at the beginning of the path.
str = str.replace(/^\/+/g, '/');
return str;
}
/** /**
* Alias filepath relative to site directory, very useful so that we * Alias filepath relative to site directory, very useful so that we
* don't expose user's site structure. * don't expose user's site structure.

View file

@ -0,0 +1,80 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
export function normalizeUrl(rawUrls: string[]): string {
const urls = [...rawUrls];
const resultArray = [];
let hasStartingSlash = false;
let hasEndingSlash = false;
// If the first part is a plain protocol, we combine it with the next part.
if (urls[0].match(/^[^/:]+:\/*$/) && urls.length > 1) {
const first = urls.shift();
urls[0] = first + urls[0];
}
// There must be two or three slashes in the file protocol,
// two slashes in anything else.
const replacement = urls[0].match(/^file:\/\/\//) ? '$1:///' : '$1://';
urls[0] = urls[0].replace(/^([^/:]+):\/*/, replacement);
// eslint-disable-next-line
for (let i = 0; i < urls.length; i++) {
let component = urls[i];
if (typeof component !== 'string') {
throw new TypeError(`Url must be a string. Received ${typeof component}`);
}
if (component === '') {
if (i === urls.length - 1 && hasEndingSlash) {
resultArray.push('/');
}
// eslint-disable-next-line
continue;
}
if (component !== '/') {
if (i > 0) {
// Removing the starting slashes for each component but the first.
component = component.replace(
/^[/]+/,
// Special case where the first element of rawUrls is empty ["", "/hello"] => /hello
component[0] === '/' && !hasStartingSlash ? '/' : '',
);
}
hasEndingSlash = component[component.length - 1] === '/';
// Removing the ending slashes for each component but the last.
// For the last component we will combine multiple slashes to a single one.
component = component.replace(/[/]+$/, i < urls.length - 1 ? '' : '/');
}
hasStartingSlash = true;
resultArray.push(component);
}
let str = resultArray.join('/');
// Each input component is now separated by a single slash
// except the possible first plain protocol part.
// Remove trailing slash before parameters or hash.
str = str.replace(/\/(\?|&|#[^!])/g, '$1');
// Replace ? in parameters with &.
const parts = str.split('?');
str = parts.shift() + (parts.length > 0 ? '?' : '') + parts.join('&');
// Dedupe forward slashes in the entire path, avoiding protocol slashes.
str = str.replace(/([^:]\/)\/+/g, '$1');
// Dedupe forward slashes at the beginning of the path.
str = str.replace(/^\/+/g, '/');
return str;
}

View file

@ -0,0 +1,100 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import {kebabCase, uniq, uniqBy} from 'lodash';
import {normalizeUrl} from './normalizeUrl';
export type Tag = {
label: string;
permalink: string;
};
export type FrontMatterTag = string | Tag;
export function normalizeFrontMatterTag(
tagsPath: string,
frontMatterTag: FrontMatterTag,
): Tag {
function toTagObject(tagString: string): Tag {
return {
label: tagString,
permalink: kebabCase(tagString),
};
}
// TODO maybe make ensure the permalink is valid url path?
function normalizeTagPermalink(permalink: string): string {
// note: we always apply tagsPath on purpose
// for versioned docs, v1/doc.md and v2/doc.md tags with custom permalinks don't lead to the same created page
// tagsPath is different for each doc version
return normalizeUrl([tagsPath, permalink]);
}
const tag: Tag =
typeof frontMatterTag === 'string'
? toTagObject(frontMatterTag)
: frontMatterTag;
return {
label: tag.label,
permalink: normalizeTagPermalink(tag.permalink),
};
}
export function normalizeFrontMatterTags(
tagsPath: string,
frontMatterTags: FrontMatterTag[] | undefined,
): Tag[] {
const tags =
frontMatterTags?.map((tag) => normalizeFrontMatterTag(tagsPath, tag)) ?? [];
return uniqBy(tags, (tag) => tag.permalink);
}
export type TaggedItemGroup<Item> = {
tag: Tag;
items: Item[];
};
// Permits to group docs/blogPosts by tag (provided by FrontMatter)
// Note: groups are indexed by permalink, because routes must be unique in the end
// Labels may vary on 2 md files but they are normalized.
// Docs with label='some label' and label='some-label' should end-up in the same group/page in the end
// We can't create 2 routes /some-label because one would override the other
export function groupTaggedItems<Item>(
items: Item[],
getItemTags: (item: Item) => Tag[],
): Record<string, TaggedItemGroup<Item>> {
const result: Record<string, TaggedItemGroup<Item>> = {};
function handleItemTag(item: Item, tag: Tag) {
// Init missing tag groups
// TODO: it's not really clear what should be the behavior if 2 items have the same tag but the permalink is different for each
// For now, the first tag found wins
result[tag.permalink] = result[tag.permalink] ?? {
tag,
items: [],
};
// Add item to group
result[tag.permalink].items.push(item);
}
items.forEach((item) => {
getItemTags(item).forEach((tag) => {
handleItemTag(item, tag);
});
});
// If user add twice the same tag to a md doc (weird but possible),
// we don't want the item to appear twice in the list...
Object.values(result).forEach((group) => {
group.items = uniq(group.items);
});
return result;
}

View file

@ -1,5 +1,6 @@
--- ---
slug: / slug: /
tags: [a, b, c, some tag]
--- ---
# Docs tests # Docs tests

View file

@ -1,3 +1,7 @@
---
tags: [a, e, some-tag, some_tag]
---
# Another test page # Another test page
[Test link](./folder%20with%20space/doc%201.md) [Test link](./folder%20with%20space/doc%201.md)

View file

@ -1,3 +1,10 @@
---
tags:
- b
- label: d
permalink: d-custom-permalink
---
# Standalone doc # Standalone doc
This doc is not in any sidebar, on purpose, to measure the build size impact of the huge sidebar This doc is not in any sidebar, on purpose, to measure the build size impact of the huge sidebar

View file

@ -42,6 +42,8 @@ Accepted fields:
| `numberPrefixParser` | <code>boolean &#124; PrefixParser</code> | _Omitted_ | Custom parsing logic to extract number prefixes from file names. Use `false` to disable this behavior and leave the docs untouched, and `true` to use the default parser. See also [Using number prefixes](/docs/sidebar#using-number-prefixes) | | `numberPrefixParser` | <code>boolean &#124; PrefixParser</code> | _Omitted_ | Custom parsing logic to extract number prefixes from file names. Use `false` to disable this behavior and leave the docs untouched, and `true` to use the default parser. See also [Using number prefixes](/docs/sidebar#using-number-prefixes) |
| `docLayoutComponent` | `string` | `'@theme/DocPage'` | Root Layout component of each doc page. | | `docLayoutComponent` | `string` | `'@theme/DocPage'` | Root Layout component of each doc page. |
| `docItemComponent` | `string` | `'@theme/DocItem'` | Main doc container, with TOC, pagination, etc. | | `docItemComponent` | `string` | `'@theme/DocItem'` | Main doc container, with TOC, pagination, etc. |
| `docTagsListComponent` | `string` | `'@theme/DocTagsListPage'` | Root component of the tags list page |
| `docTagDocListComponent` | `string` | `'@theme/DocTagDocListPage'` | Root component of the "docs containing tag" page. |
| `remarkPlugins` | `any[]` | `[]` | Remark plugins passed to MDX. | | `remarkPlugins` | `any[]` | `[]` | Remark plugins passed to MDX. |
| `rehypePlugins` | `any[]` | `[]` | Rehype plugins passed to MDX. | | `rehypePlugins` | `any[]` | `[]` | Rehype plugins passed to MDX. |
| `beforeDefaultRemarkPlugins` | `any[]` | `[]` | Custom Remark plugins passed to MDX before the default Docusaurus Remark plugins. | | `beforeDefaultRemarkPlugins` | `any[]` | `[]` | Custom Remark plugins passed to MDX before the default Docusaurus Remark plugins. |
@ -249,9 +251,14 @@ Accepted fields:
| `description` | `string` | The first line of Markdown content | The description of your document, which will become the `<meta name="description" content="..."/>` and `<meta property="og:description" content="..."/>` in `<head>`, used by search engines. | | `description` | `string` | The first line of Markdown content | The description of your document, which will become the `<meta name="description" content="..."/>` and `<meta property="og:description" content="..."/>` in `<head>`, used by search engines. |
| `image` | `string` | `undefined` | Cover or thumbnail image that will be used when displaying the link to your post. | | `image` | `string` | `undefined` | Cover or thumbnail image that will be used when displaying the link to your post. |
| `slug` | `string` | File path | Allows to customize the document url (`/<routeBasePath>/<slug>`). Support multiple patterns: `slug: my-doc`, `slug: /my/path/myDoc`, `slug: /`. | | `slug` | `string` | File path | Allows to customize the document url (`/<routeBasePath>/<slug>`). Support multiple patterns: `slug: my-doc`, `slug: /my/path/myDoc`, `slug: /`. |
| `tags` | `Tag[]` | `undefined` | A list of strings or objects of two string fields `label` and `permalink` to tag to your docs. |
</small> </small>
```typescript
type Tag = string | {label: string; permalink: string};
```
Example: Example:
```yml ```yml