diff --git a/packages/docusaurus-plugin-content-blog/src/__tests__/blogUtils.test.ts b/packages/docusaurus-plugin-content-blog/src/__tests__/blogUtils.test.ts index a340ec61e79e..040240641127 100644 --- a/packages/docusaurus-plugin-content-blog/src/__tests__/blogUtils.test.ts +++ b/packages/docusaurus-plugin-content-blog/src/__tests__/blogUtils.test.ts @@ -5,12 +5,14 @@ * LICENSE file in the root directory of this source tree. */ +import {jest} from '@jest/globals'; import {fromPartial} from '@total-typescript/shoehorn'; import { truncate, parseBlogFileName, paginateBlogPosts, applyProcessBlogPosts, + reportUntruncatedBlogPosts, } from '../blogUtils'; import type {BlogPost} from '@docusaurus/plugin-content-blog'; @@ -32,6 +34,109 @@ describe('truncate', () => { }); }); +describe('reportUntruncatedBlogPosts', () => { + function testPost({ + source, + hasTruncateMarker, + }: { + source: string; + hasTruncateMarker: boolean; + }): BlogPost { + return fromPartial({ + metadata: { + source, + hasTruncateMarker, + }, + }); + } + + it('throw for untruncated blog posts', () => { + const blogPosts = [ + testPost({source: '@site/blog/post1.md', hasTruncateMarker: false}), + testPost({source: '@site/blog/post2.md', hasTruncateMarker: true}), + testPost({ + source: '@site/blog/subDir/post3.md', + hasTruncateMarker: false, + }), + ]; + expect(() => + reportUntruncatedBlogPosts({blogPosts, onUntruncatedBlogPosts: 'throw'}), + ).toThrowErrorMatchingInlineSnapshot(` + "Docusaurus found blog posts without truncation markers: + - "blog/post1.md" + - "blog/subDir/post3.md" + + We recommend using truncation markers (\`\` or \`{/* truncate */}\`) in blog posts to create shorter previews on blog paginated lists. + Tip: turn this security off with the \`onUntruncatedBlogPosts: 'ignore'\` blog plugin option." + `); + }); + + it('warn for untruncated blog posts', () => { + const consoleMock = jest.spyOn(console, 'warn'); + + const blogPosts = [ + testPost({source: '@site/blog/post1.md', hasTruncateMarker: false}), + testPost({source: '@site/blog/post2.md', hasTruncateMarker: true}), + testPost({ + source: '@site/blog/subDir/post3.md', + hasTruncateMarker: false, + }), + ]; + expect(() => + reportUntruncatedBlogPosts({blogPosts, onUntruncatedBlogPosts: 'warn'}), + ).not.toThrow(); + + expect(consoleMock.mock.calls).toMatchInlineSnapshot(` + [ + [ + "[WARNING] Docusaurus found blog posts without truncation markers: + - "blog/post1.md" + - "blog/subDir/post3.md" + + We recommend using truncation markers (\`\` or \`{/* truncate */}\`) in blog posts to create shorter previews on blog paginated lists. + Tip: turn this security off with the \`onUntruncatedBlogPosts: 'ignore'\` blog plugin option.", + ], + ] + `); + consoleMock.mockRestore(); + }); + + it('ignore untruncated blog posts', () => { + const logMock = jest.spyOn(console, 'log'); + const warnMock = jest.spyOn(console, 'warn'); + const errorMock = jest.spyOn(console, 'error'); + + const blogPosts = [ + testPost({source: '@site/blog/post1.md', hasTruncateMarker: false}), + testPost({source: '@site/blog/post2.md', hasTruncateMarker: true}), + testPost({ + source: '@site/blog/subDir/post3.md', + hasTruncateMarker: false, + }), + ]; + expect(() => + reportUntruncatedBlogPosts({blogPosts, onUntruncatedBlogPosts: 'ignore'}), + ).not.toThrow(); + + expect(logMock).not.toHaveBeenCalled(); + expect(warnMock).not.toHaveBeenCalled(); + expect(errorMock).not.toHaveBeenCalled(); + logMock.mockRestore(); + warnMock.mockRestore(); + errorMock.mockRestore(); + }); + + it('does not throw for truncated posts', () => { + const blogPosts = [ + testPost({source: '@site/blog/post1.md', hasTruncateMarker: true}), + testPost({source: '@site/blog/post2.md', hasTruncateMarker: true}), + ]; + expect(() => + reportUntruncatedBlogPosts({blogPosts, onUntruncatedBlogPosts: 'throw'}), + ).not.toThrow(); + }); +}); + describe('paginateBlogPosts', () => { const blogPosts = [ {id: 'post1', metadata: {}, content: 'Foo 1'}, diff --git a/packages/docusaurus-plugin-content-blog/src/__tests__/options.test.ts b/packages/docusaurus-plugin-content-blog/src/__tests__/options.test.ts index 254d56b96b2a..b2de2306f6f9 100644 --- a/packages/docusaurus-plugin-content-blog/src/__tests__/options.test.ts +++ b/packages/docusaurus-plugin-content-blog/src/__tests__/options.test.ts @@ -374,4 +374,46 @@ describe('validateOptions', () => { ); }); }); + + describe('onUntruncatedBlogPosts', () => { + it('accepts onUntruncatedBlogPosts - undefined', () => { + expect( + testValidate({onUntruncatedBlogPosts: undefined}) + .onUntruncatedBlogPosts, + ).toBe('warn'); + }); + + it('accepts onUntruncatedBlogPosts - "throw"', () => { + expect( + testValidate({onUntruncatedBlogPosts: 'throw'}).onUntruncatedBlogPosts, + ).toBe('throw'); + }); + + it('rejects onUntruncatedBlogPosts - "trace"', () => { + expect(() => + // @ts-expect-error: test + testValidate({onUntruncatedBlogPosts: 'trace'}), + ).toThrowErrorMatchingInlineSnapshot( + `""onUntruncatedBlogPosts" must be one of [ignore, log, warn, throw]"`, + ); + }); + + it('rejects onUntruncatedBlogPosts - null', () => { + expect(() => + // @ts-expect-error: test + testValidate({onUntruncatedBlogPosts: 42}), + ).toThrowErrorMatchingInlineSnapshot( + `""onUntruncatedBlogPosts" must be one of [ignore, log, warn, throw]"`, + ); + }); + + it('rejects onUntruncatedBlogPosts - 42', () => { + expect(() => + // @ts-expect-error: test + testValidate({onUntruncatedBlogPosts: 42}), + ).toThrowErrorMatchingInlineSnapshot( + `""onUntruncatedBlogPosts" must be one of [ignore, log, warn, throw]"`, + ); + }); + }); }); diff --git a/packages/docusaurus-plugin-content-blog/src/blogUtils.ts b/packages/docusaurus-plugin-content-blog/src/blogUtils.ts index d26d319c0f1b..ab7426eac589 100644 --- a/packages/docusaurus-plugin-content-blog/src/blogUtils.ts +++ b/packages/docusaurus-plugin-content-blog/src/blogUtils.ts @@ -26,6 +26,7 @@ import { isDraft, readLastUpdateData, normalizeTags, + aliasedSitePathToRelativePath, } from '@docusaurus/utils'; import {getTagsFile} from '@docusaurus/utils-validation'; import {validateBlogPostFrontMatter} from './frontMatter'; @@ -47,6 +48,28 @@ export function truncate(fileString: string, truncateMarker: RegExp): string { return fileString.split(truncateMarker, 1).shift()!; } +export function reportUntruncatedBlogPosts({ + blogPosts, + onUntruncatedBlogPosts, +}: { + blogPosts: BlogPost[]; + onUntruncatedBlogPosts: PluginOptions['onUntruncatedBlogPosts']; +}): void { + const untruncatedBlogPosts = blogPosts.filter( + (p) => !p.metadata.hasTruncateMarker, + ); + if (onUntruncatedBlogPosts !== 'ignore' && untruncatedBlogPosts.length > 0) { + const message = logger.interpolate`Docusaurus found blog posts without truncation markers: +- ${untruncatedBlogPosts + .map((p) => logger.path(aliasedSitePathToRelativePath(p.metadata.source))) + .join('\n- ')} + +We recommend using truncation markers (code=${``} or code=${`{/* truncate */}`}) in blog posts to create shorter previews on blog paginated lists. +Tip: turn this security off with the code=${`onUntruncatedBlogPosts: 'ignore'`} blog plugin option.`; + logger.report(onUntruncatedBlogPosts)(message); + } +} + export function paginateBlogPosts({ blogPosts, basePageUrl, diff --git a/packages/docusaurus-plugin-content-blog/src/index.ts b/packages/docusaurus-plugin-content-blog/src/index.ts index ea2652c57fe8..7ae3b5a54c79 100644 --- a/packages/docusaurus-plugin-content-blog/src/index.ts +++ b/packages/docusaurus-plugin-content-blog/src/index.ts @@ -28,6 +28,7 @@ import { shouldBeListed, applyProcessBlogPosts, generateBlogPosts, + reportUntruncatedBlogPosts, } from './blogUtils'; import footnoteIDFixer from './remark/footnoteIDFixer'; import {translateContent, getTranslationFiles} from './translations'; @@ -189,6 +190,10 @@ export default async function pluginContentBlog( blogPosts, processBlogPosts: options.processBlogPosts, }); + reportUntruncatedBlogPosts({ + blogPosts, + onUntruncatedBlogPosts: options.onUntruncatedBlogPosts, + }); const listedBlogPosts = blogPosts.filter(shouldBeListed); if (!blogPosts.length) { diff --git a/packages/docusaurus-plugin-content-blog/src/options.ts b/packages/docusaurus-plugin-content-blog/src/options.ts index e9d91d3bf449..11e912b68170 100644 --- a/packages/docusaurus-plugin-content-blog/src/options.ts +++ b/packages/docusaurus-plugin-content-blog/src/options.ts @@ -72,6 +72,7 @@ export const DEFAULT_OPTIONS: PluginOptions = { tags: undefined, authorsBasePath: 'authors', onInlineAuthors: 'warn', + onUntruncatedBlogPosts: 'warn', }; export const XSLTBuiltInPaths = { @@ -240,6 +241,9 @@ const PluginOptionSchema = Joi.object({ onInlineAuthors: Joi.string() .equal('ignore', 'log', 'warn', 'throw') .default(DEFAULT_OPTIONS.onInlineAuthors), + onUntruncatedBlogPosts: Joi.string() + .equal('ignore', 'log', 'warn', 'throw') + .default(DEFAULT_OPTIONS.onUntruncatedBlogPosts), }).default(DEFAULT_OPTIONS); export function validateOptions({ 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 02e98f0b1e3b..578688a4595b 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 @@ -521,6 +521,8 @@ declare module '@docusaurus/plugin-content-blog' { authorsBasePath: string; /** The behavior of Docusaurus when it finds inline authors. */ onInlineAuthors: 'ignore' | 'log' | 'warn' | 'throw'; + /** The behavior of Docusaurus when it finds untruncated blog posts. */ + onUntruncatedBlogPosts: 'ignore' | 'log' | 'warn' | 'throw'; }; export type UserFeedXSLTOptions = diff --git a/project-words.txt b/project-words.txt index 528d635318ee..d8bda0b3bef1 100644 --- a/project-words.txt +++ b/project-words.txt @@ -391,6 +391,8 @@ unlocalized Unlocalized unnormalized unswizzle +untruncated +Untruncated upvotes urlset Vannicatte diff --git a/website/_dogfooding/dogfooding.config.ts b/website/_dogfooding/dogfooding.config.ts index d31bce0776d7..94b142cae275 100644 --- a/website/_dogfooding/dogfooding.config.ts +++ b/website/_dogfooding/dogfooding.config.ts @@ -99,6 +99,7 @@ export const dogfoodingPluginInstances: PluginConfig[] = [ : defaultReadingTime({content, options: {wordsPerMinute: 5}}), onInlineTags: 'warn', onInlineAuthors: 'ignore', + onUntruncatedBlogPosts: 'ignore', tags: 'tags.yml', } satisfies BlogOptions, ], diff --git a/website/docs/api/plugins/plugin-content-blog.mdx b/website/docs/api/plugins/plugin-content-blog.mdx index 9b2d50d96d2e..50a268c2fda9 100644 --- a/website/docs/api/plugins/plugin-content-blog.mdx +++ b/website/docs/api/plugins/plugin-content-blog.mdx @@ -85,6 +85,7 @@ Accepted fields: | `showLastUpdateTime` | `boolean` | `false` | Whether to display the last date the blog post was updated. This requires access to git history during the build, so will not work correctly with shallow clones (a common default for CI systems). With GitHub `actions/checkout`, use`fetch-depth: 0`. | | `tags` | `string \| false \| null \| undefined` | `tags.yml` | Path to the YAML tags file listing pre-defined tags. Relative to the blog content directory. | | `onInlineTags` | `'ignore' \| 'log' \| 'warn' \| 'throw'` | `warn` | The plugin behavior when blog posts contain inline tags (not appearing in the list of pre-defined tags, usually `tags.yml`). | +| `onUntruncatedBlogPosts` | `'ignore' \| 'log' \| 'warn' \| 'throw'` | `warn` | The plugin behavior when blog posts do not contain a truncate marker. | ```mdx-code-block diff --git a/website/docusaurus.config.ts b/website/docusaurus.config.ts index 26a3e265f0af..8668adef3a26 100644 --- a/website/docusaurus.config.ts +++ b/website/docusaurus.config.ts @@ -496,6 +496,10 @@ export default async function createConfigAsync() { blogDescription: 'Read blog posts about Docusaurus from the team', blogSidebarCount: 'ALL', blogSidebarTitle: 'All our posts', + onUntruncatedBlogPosts: + process.env.DOCUSAURUS_CURRENT_LOCALE !== defaultLocale + ? 'warn' + : 'throw', onInlineTags: process.env.DOCUSAURUS_CURRENT_LOCALE !== defaultLocale ? 'warn'