Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(blog): group sidebar items by year (themeConfig.blog.sidebar.groupByYear) #10252

Merged
merged 12 commits into from
Jun 28, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
* LICENSE file in the root directory of this source tree.
*/

import {toTagsProp} from '../props';
import {fromPartial} from '@total-typescript/shoehorn';
import {toBlogSidebarProp, toTagsProp} from '../props';
import type {BlogPost} from '@docusaurus/plugin-content-blog';

describe('toTagsProp', () => {
type Tags = Parameters<typeof toTagsProp>[0]['blogTags'];
Expand Down Expand Up @@ -68,3 +70,59 @@ describe('toTagsProp', () => {
]);
});
});

describe('toBlogSidebarProp', () => {
it('creates sidebar prop', () => {
const blogPosts: BlogPost[] = [
fromPartial({
id: '1',
metadata: {
title: 'title 1',
permalink: '/blog/blog-1',
unlisted: false,
date: '2021-01-01',
frontMatter: {toto: 42},
authors: [{name: 'author'}],
source: 'xyz',
hasTruncateMarker: true,
},
}),
fromPartial({
id: '2',
metadata: {
title: 'title 2',
permalink: '/blog/blog-2',
unlisted: true,
date: '2024-01-01',
frontMatter: {hello: 'world'},
tags: [{label: 'tag1', permalink: '/tag1', inline: false}],
},
}),
];

const sidebarProp = toBlogSidebarProp({
blogSidebarTitle: 'sidebar title',
blogPosts,
});

expect(sidebarProp).toMatchInlineSnapshot(`
{
"items": [
{
"date": "2021-01-01",
"permalink": "/blog/blog-1",
"title": "title 1",
"unlisted": false,
},
{
"date": "2024-01-01",
"permalink": "/blog/blog-2",
"title": "title 2",
"unlisted": true,
},
],
"title": "sidebar title",
}
`);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -469,6 +469,7 @@ yarn workspace v1.22.19image` is a collocated image path, this entry will be the
title: string;
permalink: string;
unlisted: boolean;
date: Date | string;
};

export type BlogSidebar = {
Expand Down
25 changes: 24 additions & 1 deletion packages/docusaurus-plugin-content-blog/src/props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,12 @@
* LICENSE file in the root directory of this source tree.
*/
import type {TagsListItem, TagModule} from '@docusaurus/utils';
import type {BlogTag, BlogTags} from '@docusaurus/plugin-content-blog';
import type {
BlogPost,
BlogSidebar,
BlogTag,
BlogTags,
} from '@docusaurus/plugin-content-blog';

export function toTagsProp({blogTags}: {blogTags: BlogTags}): TagsListItem[] {
return Object.values(blogTags)
Expand Down Expand Up @@ -34,3 +39,21 @@ export function toTagProp({
unlisted: tag.unlisted,
};
}

export function toBlogSidebarProp({
blogSidebarTitle,
blogPosts,
}: {
blogSidebarTitle: string;
blogPosts: BlogPost[];
}): BlogSidebar {
return {
title: blogSidebarTitle,
items: blogPosts.map((blogPost) => ({
title: blogPost.metadata.title,
permalink: blogPost.metadata.permalink,
unlisted: blogPost.metadata.unlisted,
date: blogPost.metadata.date,
})),
};
}
17 changes: 6 additions & 11 deletions packages/docusaurus-plugin-content-blog/src/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
} from '@docusaurus/utils';
import {shouldBeListed} from './blogUtils';

import {toTagProp, toTagsProp} from './props';
import {toBlogSidebarProp, toTagProp, toTagsProp} from './props';
import type {
PluginContentLoadedActions,
RouteConfig,
Expand All @@ -26,7 +26,6 @@ import type {
BlogContent,
PluginOptions,
BlogPost,
BlogSidebar,
} from '@docusaurus/plugin-content-blog';

type CreateAllRoutesParam = {
Expand Down Expand Up @@ -88,17 +87,13 @@ export async function buildAllRoutes({
: blogPosts.slice(0, options.blogSidebarCount);

async function createSidebarModule() {
const sidebar: BlogSidebar = {
title: blogSidebarTitle,
items: sidebarBlogPosts.map((blogPost) => ({
title: blogPost.metadata.title,
permalink: blogPost.metadata.permalink,
unlisted: blogPost.metadata.unlisted,
})),
};
const sidebarProp = toBlogSidebarProp({
blogSidebarTitle,
blogPosts: sidebarBlogPosts,
});
const modulePath = await createData(
`blog-post-list-prop-${pluginId}.json`,
sidebar,
sidebarProp,
);
return aliasedSource(modulePath);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* LICENSE file in the root directory of this source tree.
*/

import _ from 'lodash';
import * as _ from 'lodash';

import {
normalizeThemeConfig,
Expand All @@ -32,6 +32,10 @@ function testValidateOptions(options: Options) {
}

describe('themeConfig', () => {
it('accepts empty theme config', () => {
expect(testValidateThemeConfig({})).toEqual(DEFAULT_CONFIG);
});

it('accepts valid theme config', () => {
const userConfig = {
prism: {
Expand All @@ -54,6 +58,11 @@ describe('themeConfig', () => {
autoCollapseCategories: false,
},
},
blog: {
sidebar: {
groupByYear: false,
},
},
announcementBar: {
id: 'supports',
content: 'pls support',
Expand Down
20 changes: 18 additions & 2 deletions packages/docusaurus-theme-classic/src/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,25 +15,39 @@ import type {
} from '@docusaurus/types';

const defaultPrismTheme = themes.palenight;

const DEFAULT_DOCS_CONFIG: ThemeConfig['docs'] = {
versionPersistence: 'localStorage',
sidebar: {
hideable: false,
autoCollapseCategories: false,
},
};
const DocsSchema = Joi.object({

const DocsSchema = Joi.object<ThemeConfig['docs']>({
versionPersistence: Joi.string()
.equal('localStorage', 'none')
.default(DEFAULT_DOCS_CONFIG.versionPersistence),
sidebar: Joi.object({
sidebar: Joi.object<ThemeConfig['docs']['sidebar']>({
hideable: Joi.bool().default(DEFAULT_DOCS_CONFIG.sidebar.hideable),
autoCollapseCategories: Joi.bool().default(
DEFAULT_DOCS_CONFIG.sidebar.autoCollapseCategories,
),
}).default(DEFAULT_DOCS_CONFIG.sidebar),
}).default(DEFAULT_DOCS_CONFIG);

const DEFAULT_BLOG_CONFIG: ThemeConfig['blog'] = {
sidebar: {
groupByYear: true,
},
};

const BlogSchema = Joi.object<ThemeConfig['blog']>({
sidebar: Joi.object<ThemeConfig['blog']['sidebar']>({
groupByYear: Joi.bool().default(DEFAULT_BLOG_CONFIG.sidebar.groupByYear),
}).default(DEFAULT_BLOG_CONFIG.sidebar),
}).default(DEFAULT_BLOG_CONFIG);

const DEFAULT_COLOR_MODE_CONFIG: ThemeConfig['colorMode'] = {
defaultMode: 'light',
disableSwitch: false,
Expand All @@ -43,6 +57,7 @@ const DEFAULT_COLOR_MODE_CONFIG: ThemeConfig['colorMode'] = {
export const DEFAULT_CONFIG: ThemeConfig = {
colorMode: DEFAULT_COLOR_MODE_CONFIG,
docs: DEFAULT_DOCS_CONFIG,
blog: DEFAULT_BLOG_CONFIG,
metadata: [],
prism: {
additionalLanguages: [],
Expand Down Expand Up @@ -333,6 +348,7 @@ export const ThemeConfigSchema = Joi.object<ThemeConfig>({
colorMode: ColorModeSchema,
image: Joi.string(),
docs: DocsSchema,
blog: BlogSchema,
metadata: Joi.array()
.items(HtmlMetadataSchema)
.default(DEFAULT_CONFIG.metadata),
Expand Down
13 changes: 13 additions & 0 deletions packages/docusaurus-theme-classic/src/theme-classic.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,19 @@ declare module '@theme/BlogListPaginator' {
export default function BlogListPaginator(props: Props): JSX.Element;
}

declare module '@theme/BlogSidebar/Content' {
import type {ReactNode, ComponentType} from 'react';
import type {BlogSidebarItem} from '@docusaurus/plugin-content-blog';

export interface Props {
readonly items: BlogSidebarItem[];
readonly ListComponent: ComponentType<{items: BlogSidebarItem[]}>;
readonly yearGroupHeadingClassName?: string;
}

export default function BlogSidebarContent(props: Props): ReactNode;
}

declare module '@theme/BlogSidebar/Desktop' {
import type {BlogSidebar} from '@docusaurus/plugin-content-blog';

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/**
* 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, {memo, type ReactNode} from 'react';
import {useThemeConfig} from '@docusaurus/theme-common';
import {groupBlogSidebarItemsByYear} from '@docusaurus/theme-common/internal';
import Heading from '@theme/Heading';
import type {Props} from '@theme/BlogSidebar/Content';

function BlogSidebarYearGroup({
year,
yearGroupHeadingClassName,
children,
}: {
year: string;
yearGroupHeadingClassName?: string;
children: ReactNode;
}) {
return (
<div role="group">
<Heading as="h3" className={yearGroupHeadingClassName}>
{year}
</Heading>
{children}
</div>
);
}

function BlogSidebarContent({
items,
yearGroupHeadingClassName,
ListComponent,
}: Props): ReactNode {
const themeConfig = useThemeConfig();
if (themeConfig.blog.sidebar.groupByYear) {
const itemsByYear = groupBlogSidebarItemsByYear(items);
return (
<>
{itemsByYear.map(([year, yearItems]) => (
<BlogSidebarYearGroup
key={year}
year={year}
yearGroupHeadingClassName={yearGroupHeadingClassName}>
<ListComponent items={yearItems} />
</BlogSidebarYearGroup>
))}
</>
);
} else {
return <ListComponent items={items} />;
}
}

export default memo(BlogSidebarContent);
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,32 @@
* LICENSE file in the root directory of this source tree.
*/

import React from 'react';
import React, {memo} from 'react';
import clsx from 'clsx';
import Link from '@docusaurus/Link';
import {translate} from '@docusaurus/Translate';
import {useVisibleBlogSidebarItems} from '@docusaurus/theme-common/internal';
import {
useVisibleBlogSidebarItems,
BlogSidebarItemList,
} from '@docusaurus/theme-common/internal';
import BlogSidebarContent from '@theme/BlogSidebar/Content';
import type {Props as BlogSidebarContentProps} from '@theme/BlogSidebar/Content';
import type {Props} from '@theme/BlogSidebar/Desktop';

import styles from './styles.module.css';

export default function BlogSidebarDesktop({sidebar}: Props): JSX.Element {
const ListComponent: BlogSidebarContentProps['ListComponent'] = ({items}) => {
return (
<BlogSidebarItemList
items={items}
ulClassName={clsx(styles.sidebarItemList, 'clean-list')}
liClassName={styles.sidebarItem}
linkClassName={styles.sidebarItemLink}
linkActiveClassName={styles.sidebarItemLinkActive}
/>
);
};

function BlogSidebarDesktop({sidebar}: Props) {
const items = useVisibleBlogSidebarItems(sidebar.items);
return (
<aside className="col col--3">
Expand All @@ -28,20 +44,14 @@ export default function BlogSidebarDesktop({sidebar}: Props): JSX.Element {
<div className={clsx(styles.sidebarItemTitle, 'margin-bottom--md')}>
{sidebar.title}
</div>
<ul className={clsx(styles.sidebarItemList, 'clean-list')}>
{items.map((item) => (
<li key={item.permalink} className={styles.sidebarItem}>
<Link
isNavLink
to={item.permalink}
className={styles.sidebarItemLink}
activeClassName={styles.sidebarItemLinkActive}>
{item.title}
</Link>
</li>
))}
</ul>
<BlogSidebarContent
items={items}
ListComponent={ListComponent}
yearGroupHeadingClassName={styles.yearGroupHeading}
/>
</nav>
</aside>
);
}

export default memo(BlogSidebarDesktop);
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,8 @@
display: none;
}
}

.yearGroupHeading {
margin-top: 1.6rem;
margin-bottom: 0.4rem;
}
Loading
Loading