mirror of
https://github.com/facebook/docusaurus.git
synced 2025-05-19 03:57:01 +02:00
feat(v2): blog sidebar (#3593)
* blog sidebar POC * polish blog post sidebar * add doc for blogSidebarCount * Update packages/docusaurus-theme-classic/src/theme/BlogSidebar/styles.module.css Co-authored-by: Alexey Pyltsyn <lex61rus@gmail.com> Co-authored-by: Alexey Pyltsyn <lex61rus@gmail.com>
This commit is contained in:
parent
da6268911c
commit
e4c1626106
14 changed files with 226 additions and 14 deletions
|
@ -5,10 +5,27 @@
|
||||||
* LICENSE file in the root directory of this source tree.
|
* LICENSE file in the root directory of this source tree.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/* eslint-disable import/no-duplicates */
|
||||||
/* eslint-disable camelcase */
|
/* eslint-disable camelcase */
|
||||||
|
|
||||||
|
declare module '@theme/BlogSidebar' {
|
||||||
|
export type BlogSidebarItem = {title: string; permalink: string};
|
||||||
|
export type BlogSidebar = {
|
||||||
|
title: string;
|
||||||
|
items: BlogSidebarItem[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Props = {
|
||||||
|
readonly sidebar: BlogSidebar;
|
||||||
|
};
|
||||||
|
|
||||||
|
const BlogSidebar: (props: Props) => JSX.Element;
|
||||||
|
export default BlogSidebar;
|
||||||
|
}
|
||||||
|
|
||||||
declare module '@theme/BlogPostPage' {
|
declare module '@theme/BlogPostPage' {
|
||||||
import type {MarkdownRightTableOfContents} from '@docusaurus/types';
|
import type {MarkdownRightTableOfContents} from '@docusaurus/types';
|
||||||
|
import type {BlogSidebar} from '@theme/BlogSidebar';
|
||||||
|
|
||||||
export type FrontMatter = {
|
export type FrontMatter = {
|
||||||
readonly title: string;
|
readonly title: string;
|
||||||
|
@ -49,6 +66,7 @@ declare module '@theme/BlogPostPage' {
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Props = {
|
export type Props = {
|
||||||
|
readonly sidebar: BlogSidebar;
|
||||||
readonly content: Content;
|
readonly content: Content;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -57,8 +75,8 @@ declare module '@theme/BlogPostPage' {
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module '@theme/BlogListPage' {
|
declare module '@theme/BlogListPage' {
|
||||||
// eslint-disable-next-line import/no-duplicates
|
|
||||||
import type {Content} from '@theme/BlogPostPage';
|
import type {Content} from '@theme/BlogPostPage';
|
||||||
|
import type {BlogSidebar} from '@theme/BlogSidebar';
|
||||||
|
|
||||||
export type Item = {
|
export type Item = {
|
||||||
readonly content: () => JSX.Element;
|
readonly content: () => JSX.Element;
|
||||||
|
@ -77,6 +95,7 @@ declare module '@theme/BlogListPage' {
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Props = {
|
export type Props = {
|
||||||
|
readonly sidebar: BlogSidebar;
|
||||||
readonly metadata: Metadata;
|
readonly metadata: Metadata;
|
||||||
readonly items: readonly {readonly content: Content}[];
|
readonly items: readonly {readonly content: Content}[];
|
||||||
};
|
};
|
||||||
|
@ -86,6 +105,8 @@ declare module '@theme/BlogListPage' {
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module '@theme/BlogTagsListPage' {
|
declare module '@theme/BlogTagsListPage' {
|
||||||
|
import type {BlogSidebar} from '@theme/BlogSidebar';
|
||||||
|
|
||||||
export type Tag = {
|
export type Tag = {
|
||||||
permalink: string;
|
permalink: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
@ -94,18 +115,22 @@ declare module '@theme/BlogTagsListPage' {
|
||||||
slug: string;
|
slug: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Props = {readonly tags: Readonly<Record<string, Tag>>};
|
export type Props = {
|
||||||
|
readonly sidebar: BlogSidebar;
|
||||||
|
readonly tags: Readonly<Record<string, Tag>>;
|
||||||
|
};
|
||||||
|
|
||||||
const BlogTagsListPage: (props: Props) => JSX.Element;
|
const BlogTagsListPage: (props: Props) => JSX.Element;
|
||||||
export default BlogTagsListPage;
|
export default BlogTagsListPage;
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module '@theme/BlogTagsPostsPage' {
|
declare module '@theme/BlogTagsPostsPage' {
|
||||||
|
import type {BlogSidebar} from '@theme/BlogSidebar';
|
||||||
import type {Tag} from '@theme/BlogTagsListPage';
|
import type {Tag} from '@theme/BlogTagsListPage';
|
||||||
// eslint-disable-next-line import/no-duplicates
|
|
||||||
import type {Content} from '@theme/BlogPostPage';
|
import type {Content} from '@theme/BlogPostPage';
|
||||||
|
|
||||||
export type Props = {
|
export type Props = {
|
||||||
|
readonly sidebar: BlogSidebar;
|
||||||
readonly metadata: Tag;
|
readonly metadata: Tag;
|
||||||
readonly items: readonly {readonly content: Content}[];
|
readonly items: readonly {readonly content: Content}[];
|
||||||
};
|
};
|
||||||
|
|
|
@ -27,7 +27,7 @@
|
||||||
"fs-extra": "^8.1.0",
|
"fs-extra": "^8.1.0",
|
||||||
"globby": "^10.0.1",
|
"globby": "^10.0.1",
|
||||||
"loader-utils": "^1.2.3",
|
"loader-utils": "^1.2.3",
|
||||||
"lodash.kebabcase": "^4.1.1",
|
"lodash": "^4.5.2",
|
||||||
"reading-time": "^1.2.0",
|
"reading-time": "^1.2.0",
|
||||||
"remark-admonitions": "^1.2.1",
|
"remark-admonitions": "^1.2.1",
|
||||||
"webpack": "^4.44.1"
|
"webpack": "^4.44.1"
|
||||||
|
|
|
@ -81,3 +81,42 @@ test('should convert all feed type to array with other feed type', () => {
|
||||||
feedOptions: {type: ['rss', 'atom']},
|
feedOptions: {type: ['rss', 'atom']},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('blog sidebar', () => {
|
||||||
|
test('should accept 0 sidebar count', () => {
|
||||||
|
const userOptions = {blogSidebarCount: 0};
|
||||||
|
const {value, error} = PluginOptionSchema.validate(userOptions);
|
||||||
|
expect(value).toEqual({...DEFAULT_OPTIONS, ...userOptions});
|
||||||
|
expect(error).toBe(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should accept "ALL" sidebar count', () => {
|
||||||
|
const userOptions = {blogSidebarCount: 'ALL'};
|
||||||
|
const {value, error} = PluginOptionSchema.validate(userOptions);
|
||||||
|
expect(value).toEqual({...DEFAULT_OPTIONS, ...userOptions});
|
||||||
|
expect(error).toBe(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should reject "abcdef" sidebar count', () => {
|
||||||
|
const userOptions = {blogSidebarCount: 'abcdef'};
|
||||||
|
const {error} = PluginOptionSchema.validate(userOptions);
|
||||||
|
expect(error).toMatchInlineSnapshot(
|
||||||
|
`[ValidationError: "blogSidebarCount" must be one of [ALL, number]]`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should accept "all posts" sidebar title', () => {
|
||||||
|
const userOptions = {blogSidebarTitle: 'all posts'};
|
||||||
|
const {value, error} = PluginOptionSchema.validate(userOptions);
|
||||||
|
expect(value).toEqual({...DEFAULT_OPTIONS, ...userOptions});
|
||||||
|
expect(error).toBe(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should reject 42 sidebar title', () => {
|
||||||
|
const userOptions = {blogSidebarTitle: 42};
|
||||||
|
const {error} = PluginOptionSchema.validate(userOptions);
|
||||||
|
expect(error).toMatchInlineSnapshot(
|
||||||
|
`[ValidationError: "blogSidebarTitle" must be a string]`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
@ -6,7 +6,6 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import fs from 'fs-extra';
|
import fs from 'fs-extra';
|
||||||
import kebabCase from 'lodash.kebabcase';
|
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import admonitions from 'remark-admonitions';
|
import admonitions from 'remark-admonitions';
|
||||||
import {normalizeUrl, docuHash, aliasedSitePath} from '@docusaurus/utils';
|
import {normalizeUrl, docuHash, aliasedSitePath} from '@docusaurus/utils';
|
||||||
|
@ -15,6 +14,7 @@ import {
|
||||||
DEFAULT_PLUGIN_ID,
|
DEFAULT_PLUGIN_ID,
|
||||||
} from '@docusaurus/core/lib/constants';
|
} from '@docusaurus/core/lib/constants';
|
||||||
import {ValidationError} from '@hapi/joi';
|
import {ValidationError} from '@hapi/joi';
|
||||||
|
import {take, kebabCase} from 'lodash';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
PluginOptions,
|
PluginOptions,
|
||||||
|
@ -50,12 +50,13 @@ export default function pluginContentBlog(
|
||||||
|
|
||||||
const {siteDir, generatedFilesDir} = context;
|
const {siteDir, generatedFilesDir} = context;
|
||||||
const contentPath = path.resolve(siteDir, options.path);
|
const contentPath = path.resolve(siteDir, options.path);
|
||||||
|
const pluginId = options.id ?? DEFAULT_PLUGIN_ID;
|
||||||
|
|
||||||
const pluginDataDirRoot = path.join(
|
const pluginDataDirRoot = path.join(
|
||||||
generatedFilesDir,
|
generatedFilesDir,
|
||||||
'docusaurus-plugin-content-blog',
|
'docusaurus-plugin-content-blog',
|
||||||
);
|
);
|
||||||
const dataDir = path.join(pluginDataDirRoot, options.id ?? DEFAULT_PLUGIN_ID);
|
const dataDir = path.join(pluginDataDirRoot, pluginId);
|
||||||
const aliasedSource = (source: string) =>
|
const aliasedSource = (source: string) =>
|
||||||
`~blog/${path.relative(pluginDataDirRoot, source)}`;
|
`~blog/${path.relative(pluginDataDirRoot, source)}`;
|
||||||
|
|
||||||
|
@ -217,6 +218,29 @@ export default function pluginContentBlog(
|
||||||
|
|
||||||
const blogItemsToMetadata: BlogItemsToMetadata = {};
|
const blogItemsToMetadata: BlogItemsToMetadata = {};
|
||||||
|
|
||||||
|
const sidebarBlogPosts =
|
||||||
|
options.blogSidebarCount === 'ALL'
|
||||||
|
? blogPosts
|
||||||
|
: take(blogPosts, options.blogSidebarCount);
|
||||||
|
|
||||||
|
// This prop is useful to provide the blog list sidebar
|
||||||
|
const sidebarProp = await createData(
|
||||||
|
// Note that this created data path must be in sync with
|
||||||
|
// metadataPath provided to mdx-loader.
|
||||||
|
`blog-post-list-prop-${pluginId}.json`,
|
||||||
|
JSON.stringify(
|
||||||
|
{
|
||||||
|
title: options.blogSidebarTitle,
|
||||||
|
items: sidebarBlogPosts.map((blogPost) => ({
|
||||||
|
title: blogPost.metadata.title,
|
||||||
|
permalink: blogPost.metadata.permalink,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
// Create routes for blog entries.
|
// Create routes for blog entries.
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
loadedBlogPosts.map(async (blogPost) => {
|
loadedBlogPosts.map(async (blogPost) => {
|
||||||
|
@ -233,6 +257,7 @@ export default function pluginContentBlog(
|
||||||
component: blogPostComponent,
|
component: blogPostComponent,
|
||||||
exact: true,
|
exact: true,
|
||||||
modules: {
|
modules: {
|
||||||
|
sidebar: sidebarProp,
|
||||||
content: metadata.source,
|
content: metadata.source,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -256,6 +281,7 @@ export default function pluginContentBlog(
|
||||||
component: blogListComponent,
|
component: blogListComponent,
|
||||||
exact: true,
|
exact: true,
|
||||||
modules: {
|
modules: {
|
||||||
|
sidebar: sidebarProp,
|
||||||
items: items.map((postID) => {
|
items: items.map((postID) => {
|
||||||
// To tell routes.js this is an import and not a nested object to recurse.
|
// To tell routes.js this is an import and not a nested object to recurse.
|
||||||
return {
|
return {
|
||||||
|
@ -303,6 +329,7 @@ export default function pluginContentBlog(
|
||||||
component: blogTagsPostsComponent,
|
component: blogTagsPostsComponent,
|
||||||
exact: true,
|
exact: true,
|
||||||
modules: {
|
modules: {
|
||||||
|
sidebar: sidebarProp,
|
||||||
items: items.map((postID) => {
|
items: items.map((postID) => {
|
||||||
const metadata = blogItemsToMetadata[postID];
|
const metadata = blogItemsToMetadata[postID];
|
||||||
return {
|
return {
|
||||||
|
@ -333,6 +360,7 @@ export default function pluginContentBlog(
|
||||||
component: blogTagsListComponent,
|
component: blogTagsListComponent,
|
||||||
exact: true,
|
exact: true,
|
||||||
modules: {
|
modules: {
|
||||||
|
sidebar: sidebarProp,
|
||||||
tags: aliasedSource(tagsListPath),
|
tags: aliasedSource(tagsListPath),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -28,6 +28,8 @@ export const DEFAULT_OPTIONS = {
|
||||||
blogListComponent: '@theme/BlogListPage',
|
blogListComponent: '@theme/BlogListPage',
|
||||||
blogDescription: 'Blog',
|
blogDescription: 'Blog',
|
||||||
blogTitle: 'Blog',
|
blogTitle: 'Blog',
|
||||||
|
blogSidebarCount: 5,
|
||||||
|
blogSidebarTitle: 'Recent posts',
|
||||||
postsPerPage: 10,
|
postsPerPage: 10,
|
||||||
include: ['*.md', '*.mdx'],
|
include: ['*.md', '*.mdx'],
|
||||||
routeBasePath: 'blog',
|
routeBasePath: 'blog',
|
||||||
|
@ -57,6 +59,10 @@ export const PluginOptionSchema = Joi.object({
|
||||||
blogDescription: Joi.string()
|
blogDescription: Joi.string()
|
||||||
.allow('')
|
.allow('')
|
||||||
.default(DEFAULT_OPTIONS.blogDescription),
|
.default(DEFAULT_OPTIONS.blogDescription),
|
||||||
|
blogSidebarCount: Joi.alternatives()
|
||||||
|
.try(Joi.equal('ALL').required(), Joi.number().required())
|
||||||
|
.default(DEFAULT_OPTIONS.blogSidebarCount),
|
||||||
|
blogSidebarTitle: Joi.string().default(DEFAULT_OPTIONS.blogSidebarTitle),
|
||||||
showReadingTime: Joi.bool().default(DEFAULT_OPTIONS.showReadingTime),
|
showReadingTime: Joi.bool().default(DEFAULT_OPTIONS.showReadingTime),
|
||||||
remarkPlugins: RemarkPluginsSchema.default(DEFAULT_OPTIONS.remarkPlugins),
|
remarkPlugins: RemarkPluginsSchema.default(DEFAULT_OPTIONS.remarkPlugins),
|
||||||
rehypePlugins: RehypePluginsSchema.default(DEFAULT_OPTIONS.rehypePlugins),
|
rehypePlugins: RehypePluginsSchema.default(DEFAULT_OPTIONS.rehypePlugins),
|
||||||
|
|
|
@ -31,6 +31,8 @@ export interface PluginOptions {
|
||||||
blogTagsPostsComponent: string;
|
blogTagsPostsComponent: string;
|
||||||
blogTitle: string;
|
blogTitle: string;
|
||||||
blogDescription: string;
|
blogDescription: string;
|
||||||
|
blogSidebarCount: number | 'ALL';
|
||||||
|
blogSidebarTitle: string;
|
||||||
remarkPlugins: ([Function, object] | Function)[];
|
remarkPlugins: ([Function, object] | Function)[];
|
||||||
beforeDefaultRehypePlugins: ([Function, object] | Function)[];
|
beforeDefaultRehypePlugins: ([Function, object] | Function)[];
|
||||||
beforeDefaultRemarkPlugins: ([Function, object] | Function)[];
|
beforeDefaultRemarkPlugins: ([Function, object] | Function)[];
|
||||||
|
|
|
@ -12,9 +12,10 @@ import Layout from '@theme/Layout';
|
||||||
import BlogPostItem from '@theme/BlogPostItem';
|
import BlogPostItem from '@theme/BlogPostItem';
|
||||||
import BlogListPaginator from '@theme/BlogListPaginator';
|
import BlogListPaginator from '@theme/BlogListPaginator';
|
||||||
import type {Props} from '@theme/BlogListPage';
|
import type {Props} from '@theme/BlogListPage';
|
||||||
|
import BlogSidebar from '@theme/BlogSidebar';
|
||||||
|
|
||||||
function BlogListPage(props: Props): JSX.Element {
|
function BlogListPage(props: Props): JSX.Element {
|
||||||
const {metadata, items} = props;
|
const {metadata, items, sidebar} = props;
|
||||||
const {
|
const {
|
||||||
siteConfig: {title: siteTitle},
|
siteConfig: {title: siteTitle},
|
||||||
} = useDocusaurusContext();
|
} = useDocusaurusContext();
|
||||||
|
@ -25,7 +26,10 @@ function BlogListPage(props: Props): JSX.Element {
|
||||||
<Layout title={title} description={blogDescription}>
|
<Layout title={title} description={blogDescription}>
|
||||||
<div className="container margin-vert--lg">
|
<div className="container margin-vert--lg">
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<main className="col col--8 col--offset-2">
|
<div className="col col--2">
|
||||||
|
<BlogSidebar sidebar={sidebar} />
|
||||||
|
</div>
|
||||||
|
<main className="col col--8">
|
||||||
{items.map(({content: BlogPostContent}) => (
|
{items.map(({content: BlogPostContent}) => (
|
||||||
<BlogPostItem
|
<BlogPostItem
|
||||||
key={BlogPostContent.metadata.permalink}
|
key={BlogPostContent.metadata.permalink}
|
||||||
|
|
|
@ -11,10 +11,11 @@ import Layout from '@theme/Layout';
|
||||||
import BlogPostItem from '@theme/BlogPostItem';
|
import BlogPostItem from '@theme/BlogPostItem';
|
||||||
import BlogPostPaginator from '@theme/BlogPostPaginator';
|
import BlogPostPaginator from '@theme/BlogPostPaginator';
|
||||||
import type {Props} from '@theme/BlogPostPage';
|
import type {Props} from '@theme/BlogPostPage';
|
||||||
|
import BlogSidebar from '@theme/BlogSidebar';
|
||||||
import TOC from '@theme/TOC';
|
import TOC from '@theme/TOC';
|
||||||
|
|
||||||
function BlogPostPage(props: Props): JSX.Element {
|
function BlogPostPage(props: Props): JSX.Element {
|
||||||
const {content: BlogPostContents} = props;
|
const {content: BlogPostContents, sidebar} = props;
|
||||||
const {frontMatter, metadata} = BlogPostContents;
|
const {frontMatter, metadata} = BlogPostContents;
|
||||||
const {title, description, nextItem, prevItem, editUrl} = metadata;
|
const {title, description, nextItem, prevItem, editUrl} = metadata;
|
||||||
const {hide_table_of_contents: hideTableOfContents} = frontMatter;
|
const {hide_table_of_contents: hideTableOfContents} = frontMatter;
|
||||||
|
@ -24,7 +25,10 @@ function BlogPostPage(props: Props): JSX.Element {
|
||||||
{BlogPostContents && (
|
{BlogPostContents && (
|
||||||
<div className="container margin-vert--lg">
|
<div className="container margin-vert--lg">
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<div className="col col--8 col--offset-2">
|
<div className="col col--2">
|
||||||
|
<BlogSidebar sidebar={sidebar} />
|
||||||
|
</div>
|
||||||
|
<div className="col col--8">
|
||||||
<BlogPostItem
|
<BlogPostItem
|
||||||
frontMatter={frontMatter}
|
frontMatter={frontMatter}
|
||||||
metadata={metadata}
|
metadata={metadata}
|
||||||
|
|
|
@ -0,0 +1,37 @@
|
||||||
|
/**
|
||||||
|
* 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/BlogSidebar';
|
||||||
|
import styles from './styles.module.css';
|
||||||
|
|
||||||
|
export default function BlogSidebar({sidebar}: Props) {
|
||||||
|
if (sidebar.items.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className={styles.sidebar}>
|
||||||
|
<h3 className={styles.sidebarItemTitle}>{sidebar.title}</h3>
|
||||||
|
<ul className={styles.sidebarItemList}>
|
||||||
|
{sidebar.items.map((item) => {
|
||||||
|
return (
|
||||||
|
<li key={item.permalink} className={styles.sidebarItem}>
|
||||||
|
<Link
|
||||||
|
isNavLink
|
||||||
|
to={item.permalink}
|
||||||
|
className={styles.sidebarItemLink}
|
||||||
|
activeClassName={styles.sidebarItemLinkActive}>
|
||||||
|
{item.title}
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,47 @@
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
display: inherit;
|
||||||
|
max-height: calc(100vh - (var(--ifm-navbar-height) + 2rem));
|
||||||
|
overflow-y: auto;
|
||||||
|
position: sticky;
|
||||||
|
top: calc(var(--ifm-navbar-height) + 2rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebarItemTitle {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebarItemList {
|
||||||
|
overflow-y: auto;
|
||||||
|
font-size: .9rem;
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebarItem {
|
||||||
|
list-style-type: none;
|
||||||
|
margin-top: 0.8rem;
|
||||||
|
margin-bottom: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebarItemLink {
|
||||||
|
color: var(--ifm-font-color-base);
|
||||||
|
}
|
||||||
|
.sidebarItemLink:hover {
|
||||||
|
color: var(--ifm-color-primary);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.sidebarItemLinkActive {
|
||||||
|
color: var(--ifm-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: 996px) {
|
||||||
|
.sidebar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
|
@ -10,6 +10,7 @@ import React from 'react';
|
||||||
import Layout from '@theme/Layout';
|
import Layout from '@theme/Layout';
|
||||||
import Link from '@docusaurus/Link';
|
import Link from '@docusaurus/Link';
|
||||||
import type {Props} from '@theme/BlogTagsListPage';
|
import type {Props} from '@theme/BlogTagsListPage';
|
||||||
|
import BlogSidebar from '@theme/BlogSidebar';
|
||||||
|
|
||||||
function getCategoryOfTag(tag: string) {
|
function getCategoryOfTag(tag: string) {
|
||||||
// tag's category should be customizable
|
// tag's category should be customizable
|
||||||
|
@ -17,7 +18,7 @@ function getCategoryOfTag(tag: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function BlogTagsListPage(props: Props): JSX.Element {
|
function BlogTagsListPage(props: Props): JSX.Element {
|
||||||
const {tags} = props;
|
const {tags, sidebar} = props;
|
||||||
|
|
||||||
const tagCategories: {[category: string]: string[]} = {};
|
const tagCategories: {[category: string]: string[]} = {};
|
||||||
Object.keys(tags).forEach((tag) => {
|
Object.keys(tags).forEach((tag) => {
|
||||||
|
@ -52,7 +53,10 @@ function BlogTagsListPage(props: Props): JSX.Element {
|
||||||
<Layout title="Tags" description="Blog Tags">
|
<Layout title="Tags" description="Blog Tags">
|
||||||
<div className="container margin-vert--lg">
|
<div className="container margin-vert--lg">
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<main className="col col--8 col--offset-2">
|
<div className="col col--2">
|
||||||
|
<BlogSidebar sidebar={sidebar} />
|
||||||
|
</div>
|
||||||
|
<main className="col col--8">
|
||||||
<h1>Tags</h1>
|
<h1>Tags</h1>
|
||||||
<div className="margin-vert--lg">{tagsSection}</div>
|
<div className="margin-vert--lg">{tagsSection}</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
|
@ -11,13 +11,14 @@ import Layout from '@theme/Layout';
|
||||||
import BlogPostItem from '@theme/BlogPostItem';
|
import BlogPostItem from '@theme/BlogPostItem';
|
||||||
import Link from '@docusaurus/Link';
|
import Link from '@docusaurus/Link';
|
||||||
import type {Props} from '@theme/BlogTagsPostsPage';
|
import type {Props} from '@theme/BlogTagsPostsPage';
|
||||||
|
import BlogSidebar from '@theme/BlogSidebar';
|
||||||
|
|
||||||
function pluralize(count: number, word: string) {
|
function pluralize(count: number, word: string) {
|
||||||
return count > 1 ? `${word}s` : word;
|
return count > 1 ? `${word}s` : word;
|
||||||
}
|
}
|
||||||
|
|
||||||
function BlogTagsPostPage(props: Props): JSX.Element {
|
function BlogTagsPostPage(props: Props): JSX.Element {
|
||||||
const {metadata, items} = props;
|
const {metadata, items, sidebar} = props;
|
||||||
const {allTagsPath, name: tagName, count} = metadata;
|
const {allTagsPath, name: tagName, count} = metadata;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -26,7 +27,10 @@ function BlogTagsPostPage(props: Props): JSX.Element {
|
||||||
description={`Blog | Tagged "${tagName}"`}>
|
description={`Blog | Tagged "${tagName}"`}>
|
||||||
<div className="container margin-vert--lg">
|
<div className="container margin-vert--lg">
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<main className="col col--8 col--offset-2">
|
<div className="col col--2">
|
||||||
|
<BlogSidebar sidebar={sidebar} />
|
||||||
|
</div>
|
||||||
|
<main className="col col--8">
|
||||||
<h1>
|
<h1>
|
||||||
{count} {pluralize(count, 'post')} tagged with "{tagName}
|
{count} {pluralize(count, 'post')} tagged with "{tagName}
|
||||||
"
|
"
|
||||||
|
|
|
@ -208,6 +208,16 @@ module.exports = {
|
||||||
* Blog page meta description for better SEO
|
* Blog page meta description for better SEO
|
||||||
*/
|
*/
|
||||||
blogDescription: 'Blog',
|
blogDescription: 'Blog',
|
||||||
|
/**
|
||||||
|
* Number of blog post elements to show in the blog sidebar
|
||||||
|
* 'ALL' to show all blog posts
|
||||||
|
* 0 to disable
|
||||||
|
*/
|
||||||
|
blogSidebarCount: 5,
|
||||||
|
/**
|
||||||
|
* Title of the blog sidebar
|
||||||
|
*/
|
||||||
|
blogSidebarTitle: 'All our posts',
|
||||||
/**
|
/**
|
||||||
* URL route for the blog section of your site.
|
* URL route for the blog section of your site.
|
||||||
* *DO NOT* include a trailing slash.
|
* *DO NOT* include a trailing slash.
|
||||||
|
|
|
@ -216,6 +216,8 @@ module.exports = {
|
||||||
type: 'all',
|
type: 'all',
|
||||||
copyright: `Copyright © ${new Date().getFullYear()} Facebook, Inc.`,
|
copyright: `Copyright © ${new Date().getFullYear()} Facebook, Inc.`,
|
||||||
},
|
},
|
||||||
|
blogSidebarCount: 'ALL',
|
||||||
|
blogSidebarTitle: 'All our posts',
|
||||||
},
|
},
|
||||||
pages: {
|
pages: {
|
||||||
remarkPlugins: [require('@docusaurus/remark-plugin-npm2yarn')],
|
remarkPlugins: [require('@docusaurus/remark-plugin-npm2yarn')],
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue