diff --git a/packages/docusaurus-plugin-content-blog/src/__tests__/__snapshots__/blogUtils.test.ts.snap b/packages/docusaurus-plugin-content-blog/src/__tests__/__snapshots__/blogUtils.test.ts.snap index 11e9d9ff1cbf..6137538dad52 100644 --- a/packages/docusaurus-plugin-content-blog/src/__tests__/__snapshots__/blogUtils.test.ts.snap +++ b/packages/docusaurus-plugin-content-blog/src/__tests__/__snapshots__/blogUtils.test.ts.snap @@ -37,7 +37,7 @@ exports[`paginateBlogPosts generates right pages 1`] = ` "page": 1, "permalink": "/blog", "postsPerPage": 2, - "previousPage": null, + "previousPage": undefined, "totalCount": 5, "totalPages": 3, }, @@ -66,7 +66,7 @@ exports[`paginateBlogPosts generates right pages 1`] = ` "metadata": { "blogDescription": "Blog Description", "blogTitle": "Blog Title", - "nextPage": null, + "nextPage": undefined, "page": 3, "permalink": "/blog/page/3", "postsPerPage": 2, @@ -92,7 +92,7 @@ exports[`paginateBlogPosts generates right pages 2`] = ` "page": 1, "permalink": "/", "postsPerPage": 2, - "previousPage": null, + "previousPage": undefined, "totalCount": 5, "totalPages": 3, }, @@ -121,7 +121,7 @@ exports[`paginateBlogPosts generates right pages 2`] = ` "metadata": { "blogDescription": "Blog Description", "blogTitle": "Blog Title", - "nextPage": null, + "nextPage": undefined, "page": 3, "permalink": "/page/3", "postsPerPage": 2, @@ -146,11 +146,11 @@ exports[`paginateBlogPosts generates right pages 3`] = ` "metadata": { "blogDescription": "Blog Description", "blogTitle": "Blog Title", - "nextPage": null, + "nextPage": undefined, "page": 1, "permalink": "/", "postsPerPage": 10, - "previousPage": null, + "previousPage": undefined, "totalCount": 5, "totalPages": 1, }, 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 ec932459f47a..5fa35968ed25 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 @@ -182,6 +182,7 @@ exports[`rss has feed item for each post 1`] = ` Sat, 06 Mar 2021 00:00:00 GMT https://validator.w3.org/feed/docs/rss2.html https://github.com/jpmonette/feed + en Copyright <![CDATA[MDX Blog Sample with require calls]]> diff --git a/packages/docusaurus-plugin-content-blog/src/__tests__/__snapshots__/index.test.ts.snap b/packages/docusaurus-plugin-content-blog/src/__tests__/__snapshots__/index.test.ts.snap index 473ed4d5bdc3..1eab791d4450 100644 --- a/packages/docusaurus-plugin-content-blog/src/__tests__/__snapshots__/index.test.ts.snap +++ b/packages/docusaurus-plugin-content-blog/src/__tests__/__snapshots__/index.test.ts.snap @@ -19,11 +19,11 @@ exports[`blog plugin works on blog tags without pagination 1`] = ` "metadata": { "blogDescription": "Blog", "blogTitle": "Blog", - "nextPage": null, + "nextPage": undefined, "page": 1, "permalink": "/blog/tags/tag-1", "postsPerPage": 3, - "previousPage": null, + "previousPage": undefined, "totalCount": 3, "totalPages": 1, }, @@ -46,11 +46,11 @@ exports[`blog plugin works on blog tags without pagination 1`] = ` "metadata": { "blogDescription": "Blog", "blogTitle": "Blog", - "nextPage": null, + "nextPage": undefined, "page": 1, "permalink": "/blog/tags/tag-2", "postsPerPage": 2, - "previousPage": null, + "previousPage": undefined, "totalCount": 2, "totalPages": 1, }, @@ -83,7 +83,7 @@ exports[`blog plugin works with blog tags 1`] = ` "page": 1, "permalink": "/blog/tags/tag-1", "postsPerPage": 2, - "previousPage": null, + "previousPage": undefined, "totalCount": 3, "totalPages": 2, }, @@ -95,7 +95,7 @@ exports[`blog plugin works with blog tags 1`] = ` "metadata": { "blogDescription": "Blog", "blogTitle": "Blog", - "nextPage": null, + "nextPage": undefined, "page": 2, "permalink": "/blog/tags/tag-1/page/2", "postsPerPage": 2, @@ -122,11 +122,11 @@ exports[`blog plugin works with blog tags 1`] = ` "metadata": { "blogDescription": "Blog", "blogTitle": "Blog", - "nextPage": null, + "nextPage": undefined, "page": 1, "permalink": "/blog/tags/tag-2", "postsPerPage": 2, - "previousPage": null, + "previousPage": undefined, "totalCount": 2, "totalPages": 1, }, diff --git a/packages/docusaurus-plugin-content-blog/src/__tests__/__snapshots__/translations.test.ts.snap b/packages/docusaurus-plugin-content-blog/src/__tests__/__snapshots__/translations.test.ts.snap index e07e327f88f7..beb31a882566 100644 --- a/packages/docusaurus-plugin-content-blog/src/__tests__/__snapshots__/translations.test.ts.snap +++ b/packages/docusaurus-plugin-content-blog/src/__tests__/__snapshots__/translations.test.ts.snap @@ -32,11 +32,11 @@ exports[`translateContent falls back when translation is incomplete 1`] = ` "metadata": { "blogDescription": "Someone's random blog", "blogTitle": "My blog", - "nextPage": null, + "nextPage": undefined, "page": 1, "permalink": "/", "postsPerPage": 10, - "previousPage": null, + "previousPage": undefined, "totalCount": 1, "totalPages": 1, }, @@ -73,11 +73,11 @@ exports[`translateContent returns translated loaded 1`] = ` "metadata": { "blogDescription": "Someone's random blog (translated)", "blogTitle": "My blog (translated)", - "nextPage": null, + "nextPage": undefined, "page": 1, "permalink": "/", "postsPerPage": 10, - "previousPage": null, + "previousPage": undefined, "totalCount": 1, "totalPages": 1, }, diff --git a/packages/docusaurus-plugin-content-blog/src/__tests__/feed.test.ts b/packages/docusaurus-plugin-content-blog/src/__tests__/feed.test.ts index 99ac95ca49ed..03b4abb14d78 100644 --- a/packages/docusaurus-plugin-content-blog/src/__tests__/feed.test.ts +++ b/packages/docusaurus-plugin-content-blog/src/__tests__/feed.test.ts @@ -49,6 +49,7 @@ async function testGenerateFeeds( options, siteConfig: context.siteConfig, outDir: context.outDir, + locale: 'en', }); } diff --git a/packages/docusaurus-plugin-content-blog/src/__tests__/translations.test.ts b/packages/docusaurus-plugin-content-blog/src/__tests__/translations.test.ts index c6cfd5b5ae04..b6f950a10e53 100644 --- a/packages/docusaurus-plugin-content-blog/src/__tests__/translations.test.ts +++ b/packages/docusaurus-plugin-content-blog/src/__tests__/translations.test.ts @@ -45,8 +45,8 @@ const sampleBlogContent: BlogContent = { postsPerPage: 10, totalPages: 1, totalCount: 1, - previousPage: null, - nextPage: null, + previousPage: undefined, + nextPage: undefined, blogTitle: sampleBlogOptions.blogTitle, blogDescription: sampleBlogOptions.blogDescription, }, diff --git a/packages/docusaurus-plugin-content-blog/src/authors.ts b/packages/docusaurus-plugin-content-blog/src/authors.ts index 9e234d86d9a7..1de62ec0c8c6 100644 --- a/packages/docusaurus-plugin-content-blog/src/authors.ts +++ b/packages/docusaurus-plugin-content-blog/src/authors.ts @@ -70,7 +70,7 @@ type AuthorsParam = { // We may want to deprecate those in favor of using only frontMatter.authors function getFrontMatterAuthorLegacy( frontMatter: BlogPostFrontMatter, -): BlogPostFrontMatterAuthor | undefined { +): Author | undefined { const name = frontMatter.author; const title = frontMatter.author_title ?? frontMatter.authorTitle; const url = frontMatter.author_url ?? frontMatter.authorURL; @@ -92,7 +92,7 @@ function normalizeFrontMatterAuthors( frontMatterAuthors: BlogPostFrontMatterAuthors = [], ): BlogPostFrontMatterAuthor[] { function normalizeAuthor( - authorInput: string | BlogPostFrontMatterAuthor, + authorInput: string | Author, ): BlogPostFrontMatterAuthor { if (typeof authorInput === 'string') { // Technically, we could allow users to provide an author's name here, but diff --git a/packages/docusaurus-plugin-content-blog/src/blogUtils.ts b/packages/docusaurus-plugin-content-blog/src/blogUtils.ts index eefbf31cba85..ab24aa34ab92 100644 --- a/packages/docusaurus-plugin-content-blog/src/blogUtils.ts +++ b/packages/docusaurus-plugin-content-blog/src/blogUtils.ts @@ -88,8 +88,8 @@ export function paginateBlogPosts({ postsPerPage, totalPages: numberOfPages, totalCount, - previousPage: page !== 0 ? permalink(page - 1) : null, - nextPage: page < numberOfPages - 1 ? permalink(page + 1) : null, + previousPage: page !== 0 ? permalink(page - 1) : undefined, + nextPage: page < numberOfPages - 1 ? permalink(page + 1) : undefined, blogDescription, blogTitle, }, diff --git a/packages/docusaurus-plugin-content-blog/src/feed.ts b/packages/docusaurus-plugin-content-blog/src/feed.ts index 58a59b4302e4..426f19df8db5 100644 --- a/packages/docusaurus-plugin-content-blog/src/feed.ts +++ b/packages/docusaurus-plugin-content-blog/src/feed.ts @@ -29,11 +29,13 @@ async function generateBlogFeed({ options, siteConfig, outDir, + locale, }: { blogPosts: BlogPost[]; options: PluginOptions; siteConfig: DocusaurusConfig; outDir: string; + locale: string; }): Promise { if (!blogPosts.length) { return null; @@ -47,11 +49,11 @@ async function generateBlogFeed({ const feed = new Feed({ id: blogBaseUrl, - title: feedOptions.title || `${title} Blog`, + title: feedOptions.title ?? `${title} Blog`, updated, - language: feedOptions.language, + language: feedOptions.language ?? locale, link: blogBaseUrl, - description: feedOptions.description || `${siteConfig.title} Blog`, + description: feedOptions.description ?? `${siteConfig.title} Blog`, favicon: favicon ? normalizeUrl([siteUrl, baseUrl, favicon]) : undefined, copyright: feedOptions.copyright, }); @@ -140,17 +142,20 @@ export async function createBlogFeedFiles({ options, siteConfig, outDir, + locale, }: { blogPosts: BlogPost[]; options: PluginOptions; siteConfig: DocusaurusConfig; outDir: string; + locale: string; }): Promise { const feed = await generateBlogFeed({ blogPosts, options, siteConfig, outDir, + locale, }); const feedTypes = options.feedOptions.type; diff --git a/packages/docusaurus-plugin-content-blog/src/index.ts b/packages/docusaurus-plugin-content-blog/src/index.ts index 2027f068ffb4..fca3a7d81b30 100644 --- a/packages/docusaurus-plugin-content-blog/src/index.ts +++ b/packages/docusaurus-plugin-content-blog/src/index.ts @@ -26,13 +26,9 @@ import type { BlogTag, BlogTags, BlogContent, - BlogItemsToMetadata, - TagsModule, BlogPaginated, BlogContentPaths, BlogMarkdownLoaderOptions, - MetaData, - TagModule, } from './types'; import {PluginOptionSchema} from './pluginOptionSchema'; import type { @@ -52,7 +48,9 @@ import {createBlogFeedFiles} from './feed'; import type { PluginOptions, BlogPostFrontMatter, + BlogPostMetadata, Assets, + TagModule, } from '@docusaurus/plugin-content-blog'; export default async function pluginContentBlog( @@ -214,7 +212,7 @@ export default async function pluginContentBlog( blogTagsListPath, } = blogContents; - const blogItemsToMetadata: BlogItemsToMetadata = {}; + const blogItemsToMetadata: Record = {}; const sidebarBlogPosts = options.blogSidebarCount === 'ALL' @@ -325,11 +323,10 @@ export default async function pluginContentBlog( return; } - const tagsModule: TagsModule = Object.fromEntries( - Object.entries(blogTags).map(([tagKey, tag]) => { + const tagsModule: Record = Object.fromEntries( + Object.entries(blogTags).map(([, tag]) => { const tagModule: TagModule = { allTagsPath: blogTagsListPath, - slug: tagKey, name: tag.name, count: tag.items.length, permalink: tag.permalink, @@ -479,7 +476,7 @@ export default async function pluginContentBlog( metadata, }: { frontMatter: BlogPostFrontMatter; - metadata: MetaData; + metadata: BlogPostMetadata; }): Assets => ({ image: frontMatter.image, authorsImageUrls: metadata.authors.map( @@ -512,6 +509,7 @@ export default async function pluginContentBlog( options, outDir, siteConfig, + locale: currentLocale, }); }, 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 411f95ced3e3..b369cd978644 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 @@ -11,184 +11,487 @@ declare module '@docusaurus/plugin-content-blog' { import type {Overwrite} from 'utility-types'; export interface Assets { + /** + * If `metadata.image` is a collocated image path, this entry will be the + * bundler-generated image path. Otherwise, it's empty, and the image URL + * should be accessed through `frontMatter.image`. + */ image?: string; - authorsImageUrls: (string | undefined)[]; // Array of same size as the original MetaData.authors array + /** + * Array where each item is 1-1 correlated with the `metadata.authors` array + * so that client can render the correct author images. If the author's + * image is a local file path, the slot will be filled with the bundler- + * generated image path; otherwise, it's empty, and the author's image URL + * should be accessed through `authors.imageURL`. + */ + authorsImageUrls: (string | undefined)[]; } - // We allow passing custom fields to authors, e.g., twitter + /** + * Unknown keys are allowed, so that we can pass custom fields to authors, + * e.g., `twitter`. + */ export interface Author extends Record { + /** + * If `name` doesn't exist, an `imageURL` is expected. + */ name?: string; + /** + * The image path could be collocated, in which case + * `metadata.assets.authorsImageUrls` should be used instead. If `imageURL` + * doesn't exist, a `name` is expected. + */ imageURL?: string; + /** + * Used to generate the author's link. + */ url?: string; + /** + * Used as a subtitle for the author, e.g. "maintainer of Docusaurus" + */ title?: string; + /** + * Mainly used for RSS feeds; if `url` doesn't exist, `email` can be used + * to generate a fallback `mailto:` URL. + */ email?: string; } + /** + * Everything is partial/unnormalized, because front matter is always + * preserved as-is. Default values will be applied when generating metadata + */ export type BlogPostFrontMatter = { + /** + * @deprecated Use `slug` instead. + */ id?: string; + /** + * Will override the default title collected from h1 heading. + * @see {@link BlogPostMetadata.title} + */ title?: string; + /** + * Will override the default excerpt. + * @see {@link BlogPostMetadata.description} + */ description?: string; + /** + * Front matter tags, unnormalized. + * @see {@link BlogPostMetadata.tags} + */ tags?: FrontMatterTag[]; + /** + * Custom slug appended after /// + * @see {@link BlogPostMetadata.slug} + */ slug?: string; + /** + * Marks the post as draft and excludes it from the production build. + */ draft?: boolean; - date?: Date | string; // Yaml automatically convert some string patterns as Date, but not all - + /** + * 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 + * plain string. + * @see {@link BlogPostMetadata.date} + */ + date?: Date | string; + /** + * Authors, unnormalized. + * @see {@link BlogPostMetadata.authors} + */ authors?: BlogPostFrontMatterAuthors; - - // We may want to deprecate those older author front matter fields later: + /** + * To be deprecated + * @see {@link BlogPostFrontMatterAuthor.name} + */ author?: string; + /** + * To be deprecated + * @see {@link BlogPostFrontMatterAuthor.title} + */ author_title?: string; + /** + * To be deprecated + * @see {@link BlogPostFrontMatterAuthor.url} + */ author_url?: string; + /** + * To be deprecated + * @see {@link BlogPostFrontMatterAuthor.imageURL} + */ author_image_url?: string; - /** @deprecated */ + /** @deprecated v1 legacy */ authorTitle?: string; - /** @deprecated */ + /** @deprecated v1 legacy */ authorURL?: string; - /** @deprecated */ + /** @deprecated v1 legacy */ authorImageURL?: string; + /** + * @see {@link BlogPostMetadata.image} + */ image?: string; + /** + * Used in the head meta + */ keywords?: string[]; + /** + * Hide the right TOC + */ hide_table_of_contents?: boolean; + /** + * Minimum TOC heading level + */ toc_min_heading_level?: number; + /** + * Maximum TOC heading level + */ toc_max_heading_level?: number; }; - export type BlogPostFrontMatterAuthor = Record & { + export type BlogPostFrontMatterAuthor = Author & { + /** + * Will be normalized into the `imageURL` prop. + */ + image_url?: string; + /** + * References an existing author in the authors map. + */ key?: string; - name?: string; - imageURL?: string; - url?: string; - title?: string; }; - // All the possible variants that the user can use for convenience + /** + * Blog post authors can be declared in front matter as a string key + * (referencing an author in authors map), an object (partially overriding the + * data in authors map, or a completely new author), or an array of a mix of + * both. + */ export type BlogPostFrontMatterAuthors = | string | BlogPostFrontMatterAuthor | (string | BlogPostFrontMatterAuthor)[]; + export type BlogPostMetadata = { + /** + * Path to the Markdown source, with `@site` alias. + */ + readonly source: string; + /** + * Used to generate the page h1 heading, tab title, and pagination title. + */ + readonly title: string; + /** + * The publish date of the post. On client side, this will be serialized + * into a string. + */ + readonly date: Date; + /** + * Publish date formatted according to the locale, so that the client can + * render the date regardless of the existence of `Intl.DateTimeFormat`. + */ + readonly formattedDate: string; + /** + * Full link including base URL. + */ + readonly permalink: string; + /** + * Description used in the meta. Could be an empty string (empty content) + */ + readonly description: string; + /** + * Absolute URL to the editing page of the post. Undefined if the post + * shouldn't be edited. + */ + readonly editUrl?: string; + /** + * Reading time in minutes calculated based on word count. + */ + readonly readingTime?: number; + /** + * Whether the truncate marker exists in the post's content. + */ + readonly truncated?: boolean; + /** + * Used in pagination. Generated after the other metadata, so not readonly. + * Content is just a subset of another post's metadata. + */ + nextItem?: {readonly title: string; readonly permalink: string}; + /** + * Used in pagination. Generated after the other metadata, so not readonly. + * Content is just a subset of another post's metadata. + */ + prevItem?: {readonly title: string; readonly permalink: string}; + /** + * Author metadata, normalized. Should be used in joint with + * `assets.authorsImageUrls` on client side. + */ + readonly authors: Author[]; + /** + * Front matter, as-is. + */ + readonly frontMatter: BlogPostFrontMatter & Record; + /** + * Tags, normalized. + */ + readonly tags: readonly { + readonly label: string; + readonly permalink: string; + }[]; + }; + /** + * @returns The edit URL that's directly plugged into metadata. + */ export type EditUrlFunction = (editUrlParams: { + /** + * The root content directory containing this post file, relative to the + * site path. Usually the same as `options.path` but can be localized + */ blogDirPath: string; + /** + * Path to this post file, relative to `blogDirPath` + */ blogPath: string; + /** + * @see {@link BlogPostMetadata.permalink} + */ permalink: string; + /** + * Locale name. + */ locale: string; }) => string | undefined; export type FeedType = 'rss' | 'atom' | 'json'; + /** + * Normalized feed options used within code. + */ export type FeedOptions = { + /** If `null`, no feed is generated. */ type?: FeedType[] | null; + /** Title of generated feed. */ title?: string; + /** Description of generated feed. */ description?: string; + /** Copyright notice. Required because the feed library marked it that. */ copyright: string; + /** Language of the feed. */ language?: string; }; - // Feed options, as provided by user config - export type UserFeedOptions = Overwrite< - Partial, - {type?: FeedOptions['type'] | 'all'} // Handle the type: "all" shortcut - >; - // Duplicate from ngryman/reading-time to keep stability of API + /** + * Duplicate from ngryman/reading-time to keep stability of API. + */ type ReadingTimeOptions = { wordsPerMinute?: number; + /** + * @param char The character to be matched. + * @returns `true` if this character is a word bound. + */ wordBound?: (char: string) => boolean; }; + /** + * Represents the default reading time implementation. + * @returns The reading time directly plugged into metadata. + */ export type ReadingTimeFunction = (params: { + /** Markdown content. */ content: string; + /** Front matter. */ frontMatter?: BlogPostFrontMatter & Record; + /** Options accepted by ngryman/reading-time. */ options?: ReadingTimeOptions; }) => number; + /** + * @returns The reading time directly plugged into metadata. `undefined` to + * hide reading time for a specific post. + */ export type ReadingTimeFunctionOption = ( + /** + * The `options` is not provided by the caller; the user can inject their + * own option values into `defaultReadingTime` + */ params: Required[0], 'options'>> & { + /** + * The default reading time implementation from ngryman/reading-time. + */ defaultReadingTime: ReadingTimeFunction; }, ) => number | undefined; - + /** + * Plugin options after normalization. + */ export type PluginOptions = RemarkAndRehypePluginOptions & { + /** Plugin ID. */ id?: string; + /** + * Path to the blog content directory on the file system, relative to site + * directory. + */ path: string; + /** + * URL route for the blog section of your site. **DO NOT** include a + * trailing slash. Use `/` to put the blog at root path. + */ routeBasePath: string; + /** + * URL route for the tags section of your blog. Will be appended to + * `routeBasePath`. **DO NOT** include a trailing slash. + */ tagsBasePath: string; + /** + * URL route for the archive section of your blog. Will be appended to + * `routeBasePath`. **DO NOT** include a trailing slash. Use `null` to + * disable generation of archive. + */ archiveBasePath: string | null; + /** + * Array of glob patterns matching Markdown files to be built, relative to + * the content path. + */ include: string[]; + /** + * Array of glob patterns matching Markdown files to be excluded. Serves as + * refinement based on the `include` option. + */ exclude: string[]; + /** + * Number of posts to show per page in the listing page. Use `'ALL'` to + * display all posts on one listing page. + */ postsPerPage: number | 'ALL'; + /** Root component of the blog listing page. */ blogListComponent: string; + /** Root component of each blog post page. */ blogPostComponent: string; + /** Root component of the tags list page. */ blogTagsListComponent: string; + /** Root component of the "posts containing tag" page. */ blogTagsPostsComponent: string; + /** Root component of the blog archive page. */ blogArchiveComponent: string; + /** Blog page title for better SEO. */ blogTitle: string; + /** Blog page meta description for better SEO. */ blogDescription: string; + /** + * Number of blog post elements to show in the blog sidebar. `'ALL'` to show + * all blog posts; `0` to disable. + */ blogSidebarCount: number | 'ALL'; + /** Title of the blog sidebar. */ blogSidebarTitle: string; + /** Truncate marker marking where the summary ends. */ truncateMarker: RegExp; + /** Show estimated reading time for the blog post. */ showReadingTime: boolean; - feedOptions: { - type?: FeedType[] | null; - title?: string; - description?: string; - copyright: string; - language?: string; - }; + /** Blog feed. */ + feedOptions: FeedOptions; + /** + * Base URL to edit your site. The final URL is computed by `editUrl + + * relativePostPath`. Using a function allows more nuanced control for each + * file. Omitting this variable entirely will disable edit links. + */ editUrl?: string | EditUrlFunction; + /** + * The edit URL will target the localized file, instead of the original + * unlocalized file. Ignored when `editUrl` is a function. + */ editLocalizedFiles?: boolean; admonitions: Record; + /** Path to the authors map file, relative to the blog content directory. */ authorsMapPath: string; + /** A callback to customize the reading time number displayed. */ readingTime: ReadingTimeFunctionOption; + /** Governs the direction of blog post sorting. */ sortPosts: 'ascending' | 'descending'; }; - // Options, as provided in the user config (before normalization) + + /** + * Feed options, as provided by user config. `type` accepts `all` as shortcut + */ + export type UserFeedOptions = Overwrite< + Partial, + { + /** Type of feed to be generated. Use `null` to disable generation. */ + type?: FeedOptions['type'] | 'all' | FeedType; + } + >; + /** + * Options as provided in the user config (before normalization) + */ export type Options = Overwrite< Partial, - {feedOptions?: UserFeedOptions} + { + /** Blog feed. */ + feedOptions?: UserFeedOptions; + } >; + + export type TagModule = { + /** Permalink of the tag's own page. */ + permalink: string; + /** Name of the tag. */ + name: string; + /** Number of posts with this tag. */ + count: number; + /** The tags list page. */ + allTagsPath: string; + }; + + export type BlogSidebar = { + title: string; + items: {title: string; permalink: string}[]; + }; } declare module '@theme/BlogPostPage' { - import type {BlogSidebar} from '@theme/BlogSidebar'; import type {TOCItem} from '@docusaurus/types'; import type { BlogPostFrontMatter, - Author, + BlogPostMetadata, Assets, + BlogSidebar, } from '@docusaurus/plugin-content-blog'; + import type {Overwrite} from 'utility-types'; export type FrontMatter = BlogPostFrontMatter; - export type Metadata = { - readonly title: string; - readonly date: string; - readonly formattedDate: string; - readonly permalink: string; - readonly description?: string; - readonly editUrl?: string; - readonly readingTime?: number; - readonly truncated?: string; - readonly nextItem?: {readonly title: string; readonly permalink: string}; - readonly prevItem?: {readonly title: string; readonly permalink: string}; - readonly authors: Author[]; - readonly frontMatter: FrontMatter & Record; - readonly tags: readonly { - readonly label: string; - readonly permalink: string; - }[]; - }; + export type Metadata = Overwrite< + BlogPostMetadata, + { + /** The publish date of the post. Serialized from the `Date` object. */ + date: string; + } + >; export type Content = { + // TODO remove this. `metadata.frontMatter` is preferred because it can be + // accessed in enhanced plugins + /** Same as `metadata.frontMatter` */ readonly frontMatter: FrontMatter; + /** + * Usually image assets that may be collocated like `./img/thumbnail.png`. + * The loader would also bundle these assets and the client should use these + * in priority. + */ readonly assets: Assets; + /** Metadata of the post. */ readonly metadata: Metadata; + /** A list of TOC items (headings). */ readonly toc: readonly TOCItem[]; + /** Renders the actual MDX content. */ (): JSX.Element; }; export interface Props { + /** Blog sidebar. */ readonly sidebar: BlogSidebar; + /** Content of this post as an MDX component, with useful metadata. */ readonly content: Content; } @@ -197,23 +500,38 @@ declare module '@theme/BlogPostPage' { declare module '@theme/BlogListPage' { import type {Content} from '@theme/BlogPostPage'; - import type {BlogSidebar} from '@theme/BlogSidebar'; + import type {BlogSidebar} from '@docusaurus/plugin-content-blog'; export type Metadata = { + /** Title of the entire blog. */ readonly blogTitle: string; + /** Blog description. */ readonly blogDescription: string; + /** Permalink to the next list page. */ readonly nextPage?: string; - readonly page: number; + /** Permalink of the current page. */ readonly permalink: string; - readonly postsPerPage: number; + /** Permalink to the previous list page. */ readonly previousPage?: string; + /** Index of the current page, 1-based. */ + readonly page: number; + /** Posts displayed on each list page. */ + readonly postsPerPage: number; + /** Total number of posts in the entire blog. */ readonly totalCount: number; + /** Total number of list pages. */ readonly totalPages: number; }; export interface Props { + /** Blog sidebar. */ readonly sidebar: BlogSidebar; + /** Metadata of the current listing page. */ readonly metadata: Metadata; + /** + * Array of blog posts included on this page. Every post's metadata is also + * available. + */ readonly items: readonly {readonly content: Content}[]; } @@ -221,34 +539,34 @@ declare module '@theme/BlogListPage' { } declare module '@theme/BlogTagsListPage' { - import type {BlogSidebar} from '@theme/BlogSidebar'; - - export type Tag = { - permalink: string; - name: string; - count: number; - allTagsPath: string; - slug: string; - }; + import type {BlogSidebar, TagModule} from '@docusaurus/plugin-content-blog'; export interface Props { + /** Blog sidebar. */ readonly sidebar: BlogSidebar; - readonly tags: Readonly>; + /** A map from tag names to the full tag module. */ + readonly tags: Readonly>; } export default function BlogTagsListPage(props: Props): JSX.Element; } declare module '@theme/BlogTagsPostsPage' { - import type {BlogSidebar} from '@theme/BlogSidebar'; - import type {Tag} from '@theme/BlogTagsListPage'; + import type {BlogSidebar, TagModule} from '@docusaurus/plugin-content-blog'; import type {Content} from '@theme/BlogPostPage'; import type {Metadata} from '@theme/BlogListPage'; export interface Props { + /** Blog sidebar. */ readonly sidebar: BlogSidebar; - readonly metadata: Tag; + /** Metadata of this tag. */ + readonly metadata: TagModule; + /** Looks exactly the same as the posts list page */ readonly listMetadata: Metadata; + /** + * Array of blog posts included on this page. Every post's metadata is also + * available. + */ readonly items: readonly {readonly content: Content}[]; } @@ -258,10 +576,13 @@ declare module '@theme/BlogTagsPostsPage' { declare module '@theme/BlogArchivePage' { import type {Content} from '@theme/BlogPostPage'; + /** We may add extra metadata or prune some metadata from here */ export type ArchiveBlogPost = Content; export interface Props { + /** The entirety of the blog's data. */ readonly archive: { + /** All posts. Can select any useful data/metadata to render. */ readonly blogPosts: readonly ArchiveBlogPost[]; }; } diff --git a/packages/docusaurus-plugin-content-blog/src/types.ts b/packages/docusaurus-plugin-content-blog/src/types.ts index 54dc73c501b0..451f571d9342 100644 --- a/packages/docusaurus-plugin-content-blog/src/types.ts +++ b/packages/docusaurus-plugin-content-blog/src/types.ts @@ -5,15 +5,12 @@ * LICENSE file in the root directory of this source tree. */ -import type {Tag} from '@docusaurus/utils'; import type { BrokenMarkdownLink, ContentPaths, } from '@docusaurus/utils/lib/markdownLinks'; -import type { - BlogPostFrontMatter, - Author, -} from '@docusaurus/plugin-content-blog'; +import type {BlogPostMetadata} from '@docusaurus/plugin-content-blog'; +import type {Metadata as BlogPaginatedMetadata} from '@theme/BlogListPage'; export type BlogContentPaths = ContentPaths; @@ -42,65 +39,15 @@ export interface BlogTag { export interface BlogPost { id: string; - metadata: MetaData; + metadata: BlogPostMetadata; content: string; } -export interface BlogPaginatedMetadata { - permalink: string; - page: number; - postsPerPage: number; - totalPages: number; - totalCount: number; - previousPage: string | null; - nextPage: string | null; - blogTitle: string; - blogDescription: string; -} - export interface BlogPaginated { metadata: BlogPaginatedMetadata; items: string[]; // blog post permalinks } -export interface MetaData { - permalink: string; - source: string; - description: string; - date: Date; - formattedDate: string; - tags: Tag[]; - title: string; - readingTime?: number; - prevItem?: Paginator; - nextItem?: Paginator; - truncated: boolean; - editUrl?: string; - authors: Author[]; - frontMatter: BlogPostFrontMatter & Record; -} - -export interface Paginator { - title: string; - permalink: string; -} - -export interface BlogItemsToMetadata { - [key: string]: MetaData; -} - -export interface TagsModule { - [key: string]: TagModule; -} - -export interface TagModule { - allTagsPath: string; - slug: string; - name: string; - count: number; - permalink: string; -} - export type BlogBrokenMarkdownLink = BrokenMarkdownLink; export type BlogMarkdownLoaderOptions = { siteDir: string; diff --git a/packages/docusaurus-theme-classic/src/theme-classic.d.ts b/packages/docusaurus-theme-classic/src/theme-classic.d.ts index bd926b51379d..f6973773c9b3 100644 --- a/packages/docusaurus-theme-classic/src/theme-classic.d.ts +++ b/packages/docusaurus-theme-classic/src/theme-classic.d.ts @@ -41,11 +41,7 @@ declare module '@theme/BlogListPaginator' { } declare module '@theme/BlogSidebar' { - export type BlogSidebarItem = {title: string; permalink: string}; - export type BlogSidebar = { - title: string; - items: BlogSidebarItem[]; - }; + import type {BlogSidebar} from '@docusaurus/plugin-content-blog'; export interface Props { readonly sidebar: BlogSidebar; @@ -106,7 +102,7 @@ declare module '@theme/BlogPostPaginator' { declare module '@theme/BlogLayout' { import type {ReactNode} from 'react'; import type {Props as LayoutProps} from '@theme/Layout'; - import type {BlogSidebar} from '@theme/BlogSidebar'; + import type {BlogSidebar} from '@docusaurus/plugin-content-blog'; export interface Props extends LayoutProps { readonly sidebar?: BlogSidebar; diff --git a/packages/docusaurus-theme-classic/src/theme/BlogLayout/index.tsx b/packages/docusaurus-theme-classic/src/theme/BlogLayout/index.tsx index 08c183746090..009eef93276d 100644 --- a/packages/docusaurus-theme-classic/src/theme/BlogLayout/index.tsx +++ b/packages/docusaurus-theme-classic/src/theme/BlogLayout/index.tsx @@ -22,7 +22,7 @@ export default function BlogLayout(props: Props): JSX.Element {
{hasSidebar && ( )}
= { * override the other */ export function groupTaggedItems( - items: Item[], - getItemTags: (item: Item) => Tag[], + items: readonly Item[], + getItemTags: (item: Item) => readonly Tag[], ): Record> { const result: Record> = {}; diff --git a/website/docs/api/plugins/plugin-content-blog.md b/website/docs/api/plugins/plugin-content-blog.md index 7eda77338a4c..dbd658b6812e 100644 --- a/website/docs/api/plugins/plugin-content-blog.md +++ b/website/docs/api/plugins/plugin-content-blog.md @@ -37,32 +37,32 @@ Accepted fields: | Name | Type | Default | Description | | --- | --- | --- | --- | -| `path` | `string` | `'blog'` | Path to the blog content directory on the filesystem, relative to site dir. | -| `editUrl` | string \| EditUrlFunction | `undefined` | Base URL to edit your site. The final URL is computed by `editUrl + relativeDocPath`. Using a function allows more nuanced control for each file. Omitting this variable entirely will disable edit links. | +| `path` | `string` | `'blog'` | Path to the blog content directory on the file system, relative to site dir. | +| `editUrl` | string \| EditUrlFunction | `undefined` | Base URL to edit your site. The final URL is computed by `editUrl + relativePostPath`. Using a function allows more nuanced control for each file. Omitting this variable entirely will disable edit links. | | `editLocalizedFiles` | `boolean` | `false` | The edit URL will target the localized file, instead of the original unlocalized file. Ignored when `editUrl` is a function. | | `blogTitle` | `string` | `'Blog'` | Blog page title for better SEO. | | `blogDescription` | `string` | `'Blog'` | Blog page meta description for better SEO. | -| `blogSidebarCount` | number \| 'ALL' | `5` | Number of blog post elements to show in the blog sidebar. `'ALL'` to show all blog posts; `0` to disable | +| `blogSidebarCount` | number \| 'ALL' | `5` | Number of blog post elements to show in the blog sidebar. `'ALL'` to show all blog posts; `0` to disable. | | `blogSidebarTitle` | `string` | `'Recent posts'` | Title of the blog sidebar. | | `routeBasePath` | `string` | `'blog'` | URL route for the blog section of your site. **DO NOT** include a trailing slash. Use `/` to put the blog at root path. | -| `tagsBasePath` | `string` | `'tags'` | URL route for the tags list page of your site. It is prepended to the `routeBasePath`. | -| `archiveBasePath` | string \| null | `'/archive'` | URL route for the archive blog section of your site. It is prepended to the `routeBasePath`. **DO NOT** include a trailing slash. Use `null` to disable generation of archive. | -| `include` | `string[]` | `['**/*.{md,mdx}']` | Matching files will be included and processed. | -| `exclude` | `string[]` | _See example configuration_ | No route will be created for matching files. | +| `tagsBasePath` | `string` | `'tags'` | URL route for the tags section of your blog. Will be appended to `routeBasePath`. **DO NOT** include a trailing slash. | +| `archiveBasePath` | string \| null | `'archive'` | URL route for the archive section of your blog. Will be appended to `routeBasePath`. **DO NOT** include a trailing slash. Use `null` to disable generation of archive. | +| `include` | `string[]` | `['**/*.{md,mdx}']` | Array of glob patterns matching Markdown files to be built, relative to the content path. | +| `exclude` | `string[]` | _See example configuration_ | Array of glob patterns matching Markdown files to be excluded. Serves as refinement based on the `include` option. | | `postsPerPage` | number \| 'ALL' | `10` | Number of posts to show per page in the listing page. Use `'ALL'` to display all posts on one listing page. | | `blogListComponent` | `string` | `'@theme/BlogListPage'` | Root component of the blog listing page. | | `blogPostComponent` | `string` | `'@theme/BlogPostPage'` | Root component of each blog post page. | -| `blogTagsListComponent` | `string` | `'@theme/BlogTagsListPage'` | Root component of the tags list page | +| `blogTagsListComponent` | `string` | `'@theme/BlogTagsListPage'` | Root component of the tags list page. | | `blogTagsPostsComponent` | `string` | `'@theme/BlogTagsPostsPage'` | Root component of the "posts containing tag" page. | | `blogArchiveComponent` | `string` | `'@theme/BlogArchivePage'` | Root component of the blog archive page. | | `remarkPlugins` | `any[]` | `[]` | Remark plugins passed to MDX. | | `rehypePlugins` | `any[]` | `[]` | Rehype plugins passed to MDX. | | `beforeDefaultRemarkPlugins` | `any[]` | `[]` | Custom Remark plugins passed to MDX before the default Docusaurus Remark plugins. | | `beforeDefaultRehypePlugins` | `any[]` | `[]` | Custom Rehype plugins passed to MDX before the default Docusaurus Rehype plugins. | -| `truncateMarker` | `string` | `//` | Truncate marker, can be a regex or string. | +| `truncateMarker` | `RegExp` | `//` | Truncate marker marking where the summary ends. | | `showReadingTime` | `boolean` | `true` | Show estimated reading time for the blog post. | | `readingTime` | `ReadingTimeFunctionOption` | The default reading time | A callback to customize the reading time number displayed. | -| `authorsMapPath` | `string` | `'authors.yml'` | Path to the authors map file, relative to the blog content directory specified with `path`. Can also be a `json` file. | +| `authorsMapPath` | `string` | `'authors.yml'` | Path to the authors map file, relative to the blog content directory. | | `feedOptions` | _See below_ | `{type: ['rss', 'atom']}` | Blog feed. | | `feedOptions.type` | FeedType \| FeedType[] \| 'all' \| null | **Required** | Type of feed to be generated. Use `null` to disable generation. | | `feedOptions.title` | `string` | `siteConfig.title` | Title of the feed. |