diff --git a/packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/website/blog/unlisted.md b/packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/website/blog/unlisted.md new file mode 100644 index 000000000000..0c5ca5d55022 --- /dev/null +++ b/packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/website/blog/unlisted.md @@ -0,0 +1,6 @@ +--- +date: 2020-02-27 +unlisted: true +--- + +this post is unlisted diff --git a/packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/website/build-snap/blog/unlisted/index.html b/packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/website/build-snap/blog/unlisted/index.html new file mode 100644 index 000000000000..7167942e8fff --- /dev/null +++ b/packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/website/build-snap/blog/unlisted/index.html @@ -0,0 +1,18 @@ + + + + + + + +Unlisted | My Site + + + + +
+
Skip to main content
+ + + + diff --git a/packages/docusaurus-plugin-content-blog/src/__tests__/__snapshots__/feed.test.ts.snap b/packages/docusaurus-plugin-content-blog/src/__tests__/__snapshots__/feed.test.ts.snap index bf7d608e30c6..748019d4a423 100644 --- a/packages/docusaurus-plugin-content-blog/src/__tests__/__snapshots__/feed.test.ts.snap +++ b/packages/docusaurus-plugin-content-blog/src/__tests__/__snapshots__/feed.test.ts.snap @@ -50,6 +50,14 @@ exports[`atom has feed item for each post 1`] = ` https://sebastienlorber.com + + <![CDATA[unlisted]]> + /unlisted + + 2020-02-27T00:00:00.000Z + + this post is unlisted

]]>
+
<![CDATA[some heading]]> /heading-as-title @@ -135,6 +143,15 @@ exports[`json has feed item for each post 1`] = ` }, "tags": [] }, + { + "id": "/unlisted", + "content_html": "

this post is unlisted

", + "url": "https://docusaurus.io/myBaseUrl/blog/unlisted", + "title": "unlisted", + "summary": "this post is unlisted", + "date_modified": "2020-02-27T00:00:00.000Z", + "tags": [] + }, { "id": "/heading-as-title", "content_html": "", @@ -218,6 +235,14 @@ exports[`rss has feed item for each post 1`] = ` simple url slug

]]>
+ + <![CDATA[unlisted]]> + https://docusaurus.io/myBaseUrl/blog/unlisted + /unlisted + Thu, 27 Feb 2020 00:00:00 GMT + + this post is unlisted

]]>
+
<![CDATA[some heading]]> https://docusaurus.io/myBaseUrl/blog/heading-as-title diff --git a/packages/docusaurus-plugin-content-blog/src/__tests__/frontMatter.test.ts b/packages/docusaurus-plugin-content-blog/src/__tests__/frontMatter.test.ts index 7d769afe3f69..083e3ca95ff9 100644 --- a/packages/docusaurus-plugin-content-blog/src/__tests__/frontMatter.test.ts +++ b/packages/docusaurus-plugin-content-blog/src/__tests__/frontMatter.test.ts @@ -365,6 +365,21 @@ describe('validateBlogPostFrontMatter draft', () => { }); }); +describe('validateBlogPostFrontMatter unlisted', () => { + testField({ + fieldName: 'unlisted', + validFrontMatters: [{unlisted: true}, {unlisted: false}], + convertibleFrontMatter: [ + [{unlisted: 'true'}, {unlisted: true}], + [{unlisted: 'false'}, {unlisted: false}], + ], + invalidFrontMatters: [ + [{unlisted: 'yes'}, 'must be a boolean'], + [{unlisted: 'no'}, 'must be a boolean'], + ], + }); +}); + describe('validateBlogPostFrontMatter hide_table_of_contents', () => { testField({ fieldName: 'hide_table_of_contents', diff --git a/packages/docusaurus-plugin-content-blog/src/__tests__/index.test.ts b/packages/docusaurus-plugin-content-blog/src/__tests__/index.test.ts index 64998082ff53..4e767a138b6a 100644 --- a/packages/docusaurus-plugin-content-blog/src/__tests__/index.test.ts +++ b/packages/docusaurus-plugin-content-blog/src/__tests__/index.test.ts @@ -172,6 +172,7 @@ describe('blog plugin', () => { title: 'Happy 1st Birthday Slash! (translated)', }, hasTruncateMarker: false, + unlisted: false, }); expect( @@ -215,6 +216,7 @@ describe('blog plugin', () => { title: 'date-matter', }, hasTruncateMarker: false, + unlisted: false, }); expect({ @@ -252,6 +254,7 @@ describe('blog plugin', () => { }, ], hasTruncateMarker: false, + unlisted: false, }); expect({ @@ -289,6 +292,7 @@ describe('blog plugin', () => { }, tags: [], hasTruncateMarker: false, + unlisted: false, }); expect({ @@ -314,13 +318,14 @@ describe('blog plugin', () => { title: 'date-matter', }, hasTruncateMarker: false, + unlisted: false, }); }); it('builds simple website blog with localized dates', async () => { const siteDir = path.join(__dirname, '__fixtures__', 'website'); const blogPostsFrench = await getBlogPosts(siteDir, {}, getI18n('fr')); - expect(blogPostsFrench).toHaveLength(8); + expect(blogPostsFrench).toHaveLength(9); expect(blogPostsFrench[0]!.metadata.formattedDate).toMatchInlineSnapshot( `"6 mars 2021"`, ); @@ -337,13 +342,13 @@ describe('blog plugin', () => { `"27 février 2020"`, ); expect(blogPostsFrench[5]!.metadata.formattedDate).toMatchInlineSnapshot( - `"2 janvier 2019"`, + `"27 février 2020"`, ); expect(blogPostsFrench[6]!.metadata.formattedDate).toMatchInlineSnapshot( - `"1 janvier 2019"`, + `"2 janvier 2019"`, ); expect(blogPostsFrench[7]!.metadata.formattedDate).toMatchInlineSnapshot( - `"14 décembre 2018"`, + `"1 janvier 2019"`, ); }); @@ -372,7 +377,7 @@ describe('blog plugin', () => { expect(blogPost.metadata.editUrl).toEqual(hardcodedEditUrl); }); - expect(editUrlFunction).toHaveBeenCalledTimes(8); + expect(editUrlFunction).toHaveBeenCalledTimes(9); expect(editUrlFunction).toHaveBeenCalledWith({ blogDirPath: 'blog', @@ -471,6 +476,7 @@ describe('blog plugin', () => { prevItem: undefined, nextItem: undefined, hasTruncateMarker: false, + unlisted: false, }); }); diff --git a/packages/docusaurus-plugin-content-blog/src/blogUtils.ts b/packages/docusaurus-plugin-content-blog/src/blogUtils.ts index 17e0dbb0bffc..eb7144ffee68 100644 --- a/packages/docusaurus-plugin-content-blog/src/blogUtils.ts +++ b/packages/docusaurus-plugin-content-blog/src/blogUtils.ts @@ -33,6 +33,7 @@ import type { BlogPost, BlogTags, BlogPaginated, + BlogPostFrontMatter, } from '@docusaurus/plugin-content-blog'; import type {BlogContentPaths, BlogMarkdownLoaderOptions} from './types'; @@ -186,6 +187,16 @@ async function parseBlogPostMarkdownFile(blogSourceAbsolute: string) { const defaultReadingTime: ReadingTimeFunction = ({content, options}) => readingTime(content, options).minutes; +function isHiddenForProduction({ + type, + frontMatter, +}: { + type: 'draft' | 'unlisted'; + frontMatter: BlogPostFrontMatter; +}): boolean { + return (process.env.NODE_ENV === 'production' && frontMatter[type]) ?? false; +} + async function processBlogSourceFile( blogSourceRelative: string, contentPaths: BlogContentPaths, @@ -219,7 +230,10 @@ async function processBlogSourceFile( const aliasedSource = aliasedSitePath(blogSourceAbsolute, siteDir); - if (frontMatter.draft && process.env.NODE_ENV === 'production') { + const draft = isHiddenForProduction({type: 'draft', frontMatter}); + const unlisted = isHiddenForProduction({type: 'unlisted', frontMatter}); + + if (draft) { return undefined; } @@ -326,6 +340,7 @@ async function processBlogSourceFile( hasTruncateMarker: truncateMarker.test(content), authors, frontMatter, + unlisted, }, content, }; diff --git a/packages/docusaurus-plugin-content-blog/src/frontMatter.ts b/packages/docusaurus-plugin-content-blog/src/frontMatter.ts index 417bf1df00fb..393bd79e2118 100644 --- a/packages/docusaurus-plugin-content-blog/src/frontMatter.ts +++ b/packages/docusaurus-plugin-content-blog/src/frontMatter.ts @@ -33,6 +33,7 @@ const BlogFrontMatterSchema = Joi.object({ description: Joi.string().allow(''), tags: FrontMatterTagsSchema, draft: Joi.boolean(), + unlisted: Joi.boolean(), date: Joi.date().raw(), // New multi-authors front matter: diff --git a/packages/docusaurus-plugin-content-blog/src/index.ts b/packages/docusaurus-plugin-content-blog/src/index.ts index 315c87e6a792..4e28ccc853bc 100644 --- a/packages/docusaurus-plugin-content-blog/src/index.ts +++ b/packages/docusaurus-plugin-content-blog/src/index.ts @@ -112,6 +112,9 @@ export default async function pluginContentBlog( const baseBlogUrl = normalizeUrl([baseUrl, routeBasePath]); const blogTagsListPath = normalizeUrl([baseBlogUrl, tagsBasePath]); const blogPosts = await generateBlogPosts(contentPaths, context, options); + const listedBlogPosts = blogPosts.filter( + (blogPost) => !blogPost.metadata.unlisted, + ); if (!blogPosts.length) { return { @@ -125,8 +128,8 @@ export default async function pluginContentBlog( } // Colocate next and prev metadata. - blogPosts.forEach((blogPost, index) => { - const prevItem = index > 0 ? blogPosts[index - 1] : null; + listedBlogPosts.forEach((blogPost, index) => { + const prevItem = index > 0 ? listedBlogPosts[index - 1] : null; if (prevItem) { blogPost.metadata.prevItem = { title: prevItem.metadata.title, @@ -135,7 +138,9 @@ export default async function pluginContentBlog( } const nextItem = - index < blogPosts.length - 1 ? blogPosts[index + 1] : null; + index < listedBlogPosts.length - 1 + ? listedBlogPosts[index + 1] + : null; if (nextItem) { blogPost.metadata.nextItem = { title: nextItem.metadata.title, @@ -145,7 +150,7 @@ export default async function pluginContentBlog( }); const blogListPaginated: BlogPaginated[] = paginateBlogPosts({ - blogPosts, + blogPosts: listedBlogPosts, blogTitle, blogDescription, postsPerPageOption, @@ -153,7 +158,7 @@ export default async function pluginContentBlog( }); const blogTags: BlogTags = getBlogTags({ - blogPosts, + blogPosts: listedBlogPosts, postsPerPageOption, blogDescription, blogTitle, @@ -242,6 +247,7 @@ export default async function pluginContentBlog( items: sidebarBlogPosts.map((blogPost) => ({ title: blogPost.metadata.title, permalink: blogPost.metadata.permalink, + unlisted: blogPost.metadata.unlisted, })), }, null, diff --git a/packages/docusaurus-plugin-content-blog/src/plugin-content-blog.d.ts b/packages/docusaurus-plugin-content-blog/src/plugin-content-blog.d.ts index 63d31d5f58c7..1459a5cf8599 100644 --- a/packages/docusaurus-plugin-content-blog/src/plugin-content-blog.d.ts +++ b/packages/docusaurus-plugin-content-blog/src/plugin-content-blog.d.ts @@ -90,6 +90,10 @@ declare module '@docusaurus/plugin-content-blog' { * Marks the post as draft and excludes it from the production build. */ draft?: boolean; + /** + * Marks the post as unlisted and visibly hides it unless directly accessed. + */ + unlisted?: boolean; /** * Will override the default publish date inferred from git/filename. Yaml * only converts standard yyyy-MM-dd format to dates, so this may stay as a @@ -222,6 +226,10 @@ declare module '@docusaurus/plugin-content-blog' { readonly frontMatter: BlogPostFrontMatter & {[key: string]: unknown}; /** Tags, normalized. */ readonly tags: Tag[]; + /** + * Marks the post as unlisted and visibly hides it unless directly accessed. + */ + readonly unlisted?: boolean; }; /** * @returns The edit URL that's directly plugged into metadata. @@ -409,7 +417,7 @@ declare module '@docusaurus/plugin-content-blog' { export type BlogSidebar = { title: string; - items: {title: string; permalink: string}[]; + items: {title: string; permalink: string; unlisted: boolean}[]; }; export type BlogContent = { diff --git a/packages/docusaurus-theme-classic/src/theme/BlogPostPage/Metadata/index.tsx b/packages/docusaurus-theme-classic/src/theme/BlogPostPage/Metadata/index.tsx index 515e104baa8e..ce420f793c29 100644 --- a/packages/docusaurus-theme-classic/src/theme/BlogPostPage/Metadata/index.tsx +++ b/packages/docusaurus-theme-classic/src/theme/BlogPostPage/Metadata/index.tsx @@ -11,7 +11,8 @@ import {useBlogPost} from '@docusaurus/theme-common/internal'; export default function BlogPostPageMetadata(): JSX.Element { const {assets, metadata} = useBlogPost(); - const {title, description, date, tags, authors, frontMatter} = metadata; + const {title, description, date, tags, authors, frontMatter, unlisted} = + metadata; const {keywords} = frontMatter; const image = assets.image ?? frontMatter.image; @@ -39,6 +40,7 @@ export default function BlogPostPageMetadata(): JSX.Element { content={tags.map((tag) => tag.label).join(',')} /> )} + {unlisted && } ); } diff --git a/packages/docusaurus-theme-classic/src/theme/BlogSidebar/Desktop/index.tsx b/packages/docusaurus-theme-classic/src/theme/BlogSidebar/Desktop/index.tsx index f010d2144836..d9a2ee6b8d25 100644 --- a/packages/docusaurus-theme-classic/src/theme/BlogSidebar/Desktop/index.tsx +++ b/packages/docusaurus-theme-classic/src/theme/BlogSidebar/Desktop/index.tsx @@ -9,11 +9,14 @@ import React from 'react'; import clsx from 'clsx'; import Link from '@docusaurus/Link'; import {translate} from '@docusaurus/Translate'; +import {useLocation} from '@docusaurus/router'; +import {isSamePath} from '@docusaurus/theme-common/internal'; import type {Props} from '@theme/BlogSidebar/Desktop'; import styles from './styles.module.css'; export default function BlogSidebarDesktop({sidebar}: Props): JSX.Element { + const {pathname} = useLocation(); return ( diff --git a/packages/docusaurus-theme-classic/src/theme/BlogSidebar/Mobile/index.tsx b/packages/docusaurus-theme-classic/src/theme/BlogSidebar/Mobile/index.tsx index a07967840419..a2c0433fd2ae 100644 --- a/packages/docusaurus-theme-classic/src/theme/BlogSidebar/Mobile/index.tsx +++ b/packages/docusaurus-theme-classic/src/theme/BlogSidebar/Mobile/index.tsx @@ -7,23 +7,34 @@ import React from 'react'; import Link from '@docusaurus/Link'; +import {useLocation} from '@docusaurus/router'; +import {isSamePath} from '@docusaurus/theme-common/internal'; import {NavbarSecondaryMenuFiller} from '@docusaurus/theme-common'; import type {Props} from '@theme/BlogSidebar/Mobile'; function BlogSidebarMobileSecondaryMenu({sidebar}: Props): JSX.Element { + const {pathname} = useLocation(); return (
    - {sidebar.items.map((item) => ( -
  • - - {item.title} - -
  • - ))} + {sidebar.items + .filter((item) => { + if (item.unlisted && !isSamePath(item.permalink, pathname)) { + return false; + } + + return true; + }) + .map((item) => ( +
  • + + {item.title} + +
  • + ))}
); } diff --git a/website/_dogfooding/_blog tests/2022-08-24-post-unlisted.md b/website/_dogfooding/_blog tests/2022-08-24-post-unlisted.md new file mode 100644 index 000000000000..5620b9c99da5 --- /dev/null +++ b/website/_dogfooding/_blog tests/2022-08-24-post-unlisted.md @@ -0,0 +1,7 @@ +--- +title: Unlisted blog post +unlisted: true +tags: [blog] +--- + +This unlisted blog post should always be directly accessible in any environment, but in production the sidebar link and pagination should only be visible when on the page itself diff --git a/website/_dogfooding/dogfooding.config.js b/website/_dogfooding/dogfooding.config.js index e961bca132a5..027447e97ef2 100644 --- a/website/_dogfooding/dogfooding.config.js +++ b/website/_dogfooding/dogfooding.config.js @@ -72,7 +72,8 @@ const dogfoodingPluginInstances = [ frontMatter.hide_reading_time ? undefined : defaultReadingTime({content, options: {wordsPerMinute: 5}}), - sortPosts: 'ascending', + // TODO: undo this + sortPosts: 'descending', }), ], diff --git a/website/docs/api/plugins/plugin-content-blog.md b/website/docs/api/plugins/plugin-content-blog.md index 696b4102858d..6e55adf6c242 100644 --- a/website/docs/api/plugins/plugin-content-blog.md +++ b/website/docs/api/plugins/plugin-content-blog.md @@ -193,6 +193,7 @@ Accepted fields: | `date` | `string` | File name or file creation time | The blog post creation date. If not specified, this can be extracted from the file or folder name, e.g, `2021-04-15-blog-post.mdx`, `2021-04-15-blog-post/index.mdx`, `2021/04/15/blog-post.mdx`. Otherwise, it is the Markdown file creation time. | | `tags` | `Tag[]` | `undefined` | A list of strings or objects of two string fields `label` and `permalink` to tag to your post. | | `draft` | `boolean` | `false` | A boolean flag to indicate that the blog post is work-in-progress. Draft blog posts will only be displayed during development. | +| `unlisted` | `boolean` | `false` | A boolean flag to indicate that the blog post should be accessible if linked to directly, but visibly hidden on the site otherwise and ignored by search engines. Unlisted blog posts are fully visible in development. | | `hide_table_of_contents` | `boolean` | `false` | Whether to hide the table of contents to the right. | | `toc_min_heading_level` | `number` | `2` | The minimum heading level shown in the table of contents. Must be between 2 and 6 and lower or equal to the max value. | | `toc_max_heading_level` | `number` | `3` | The max heading level shown in the table of contents. Must be between 2 and 6. |