Skip to content

Commit

Permalink
feat(blog): group sidebar items by year (`themeConfig.blog.sidebar.gr…
Browse files Browse the repository at this point in the history
…oupByYear`) (#10252)

Co-authored-by: sebastien <lorber.sebastien@gmail.com>
  • Loading branch information
alicelovescake and slorber committed Jun 28, 2024
1 parent 10830ce commit aab1f48
Show file tree
Hide file tree
Showing 21 changed files with 547 additions and 85 deletions.
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 @@ -473,6 +473,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
11 changes: 10 additions & 1 deletion packages/docusaurus-theme-classic/src/__tests__/options.test.ts
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

0 comments on commit aab1f48

Please sign in to comment.