This commit is contained in:
2024-03-22 03:47:51 +05:30
parent 8bcf3d211e
commit 89819f6fe2
28440 changed files with 3211033 additions and 2 deletions

21
node_modules/@docusaurus/plugin-content-blog/LICENSE generated vendored Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) Facebook, Inc. and its affiliates.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,7 @@
# `@docusaurus/plugin-content-blog`
Blog plugin for Docusaurus.
## Usage
See [plugin-content-blog documentation](https://docusaurus.io/docs/api/plugins/@docusaurus/plugin-content-blog).

View File

@@ -0,0 +1,23 @@
/**
* 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 type { BlogContentPaths } from './types';
import type { Author, BlogPostFrontMatter } from '@docusaurus/plugin-content-blog';
export type AuthorsMap = {
[authorKey: string]: Author;
};
export declare function validateAuthorsMap(content: unknown): AuthorsMap;
export declare function getAuthorsMap(params: {
authorsMapPath: string;
contentPaths: BlogContentPaths;
}): Promise<AuthorsMap | undefined>;
type AuthorsParam = {
frontMatter: BlogPostFrontMatter;
authorsMap: AuthorsMap | undefined;
baseUrl: string;
};
export declare function getBlogPostAuthors(params: AuthorsParam): Author[];
export {};

View File

@@ -0,0 +1,137 @@
"use strict";
/**
* 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.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.getBlogPostAuthors = exports.getAuthorsMap = exports.validateAuthorsMap = void 0;
const utils_1 = require("@docusaurus/utils");
const utils_validation_1 = require("@docusaurus/utils-validation");
const AuthorsMapSchema = utils_validation_1.Joi.object()
.pattern(utils_validation_1.Joi.string(), utils_validation_1.Joi.object({
name: utils_validation_1.Joi.string(),
url: utils_validation_1.URISchema,
imageURL: utils_validation_1.URISchema,
title: utils_validation_1.Joi.string(),
email: utils_validation_1.Joi.string(),
})
.rename('image_url', 'imageURL')
.or('name', 'imageURL')
.unknown()
.required()
.messages({
'object.base': '{#label} should be an author object containing properties like name, title, and imageURL.',
'any.required': '{#label} cannot be undefined. It should be an author object containing properties like name, title, and imageURL.',
}))
.messages({
'object.base': "The authors map file should contain an object where each entry contains an author key and the corresponding author's data.",
});
function validateAuthorsMap(content) {
const { error, value } = AuthorsMapSchema.validate(content);
if (error) {
throw error;
}
return value;
}
exports.validateAuthorsMap = validateAuthorsMap;
async function getAuthorsMap(params) {
return (0, utils_1.getDataFileData)({
filePath: params.authorsMapPath,
contentPaths: params.contentPaths,
fileType: 'authors map',
}, validateAuthorsMap);
}
exports.getAuthorsMap = getAuthorsMap;
function normalizeImageUrl({ imageURL, baseUrl, }) {
return imageURL?.startsWith('/')
? (0, utils_1.normalizeUrl)([baseUrl, imageURL])
: imageURL;
}
// Legacy v1/early-v2 front matter fields
// We may want to deprecate those in favor of using only frontMatter.authors
function getFrontMatterAuthorLegacy({ baseUrl, frontMatter, }) {
const name = frontMatter.author;
const title = frontMatter.author_title ?? frontMatter.authorTitle;
const url = frontMatter.author_url ?? frontMatter.authorURL;
const imageURL = normalizeImageUrl({
imageURL: frontMatter.author_image_url ?? frontMatter.authorImageURL,
baseUrl,
});
if (name || title || url || imageURL) {
return {
name,
title,
url,
imageURL,
};
}
return undefined;
}
function normalizeFrontMatterAuthors(frontMatterAuthors = []) {
function normalizeAuthor(authorInput) {
if (typeof authorInput === 'string') {
// Technically, we could allow users to provide an author's name here, but
// we only support keys, otherwise, a typo in a key would fallback to
// becoming a name and may end up unnoticed
return { key: authorInput };
}
return authorInput;
}
return Array.isArray(frontMatterAuthors)
? frontMatterAuthors.map(normalizeAuthor)
: [normalizeAuthor(frontMatterAuthors)];
}
function getFrontMatterAuthors(params) {
const { authorsMap } = params;
const frontMatterAuthors = normalizeFrontMatterAuthors(params.frontMatter.authors);
function getAuthorsMapAuthor(key) {
if (key) {
if (!authorsMap || Object.keys(authorsMap).length === 0) {
throw new Error(`Can't reference blog post authors by a key (such as '${key}') because no authors map file could be loaded.
Please double-check your blog plugin config (in particular 'authorsMapPath'), ensure the file exists at the configured path, is not empty, and is valid!`);
}
const author = authorsMap[key];
if (!author) {
throw Error(`Blog author with key "${key}" not found in the authors map file.
Valid author keys are:
${Object.keys(authorsMap)
.map((validKey) => `- ${validKey}`)
.join('\n')}`);
}
return author;
}
return undefined;
}
function toAuthor(frontMatterAuthor) {
return {
// Author def from authorsMap can be locally overridden by front matter
...getAuthorsMapAuthor(frontMatterAuthor.key),
...frontMatterAuthor,
};
}
return frontMatterAuthors.map(toAuthor);
}
function fixAuthorImageBaseURL(authors, { baseUrl }) {
return authors.map((author) => ({
...author,
imageURL: normalizeImageUrl({ imageURL: author.imageURL, baseUrl }),
}));
}
function getBlogPostAuthors(params) {
const authorLegacy = getFrontMatterAuthorLegacy(params);
const authors = getFrontMatterAuthors(params);
const updatedAuthors = fixAuthorImageBaseURL(authors, params);
if (authorLegacy) {
// Technically, we could allow mixing legacy/authors front matter, but do we
// really want to?
if (updatedAuthors.length > 0) {
throw new Error(`To declare blog post authors, use the 'authors' front matter in priority.
Don't mix 'authors' with other existing 'author_*' front matter. Choose one or the other, not both at the same time.`);
}
return [authorLegacy];
}
return updatedAuthors;
}
exports.getBlogPostAuthors = getBlogPostAuthors;

View File

@@ -0,0 +1,40 @@
/**
* 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 type { LoadContext } from '@docusaurus/types';
import type { PluginOptions, BlogPost, BlogTags, BlogPaginated } from '@docusaurus/plugin-content-blog';
import type { BlogContentPaths, BlogMarkdownLoaderOptions } from './types';
export declare function truncate(fileString: string, truncateMarker: RegExp): string;
export declare function getSourceToPermalink(blogPosts: BlogPost[]): {
[aliasedPath: string]: string;
};
export declare function paginateBlogPosts({ blogPosts, basePageUrl, blogTitle, blogDescription, postsPerPageOption, }: {
blogPosts: BlogPost[];
basePageUrl: string;
blogTitle: string;
blogDescription: string;
postsPerPageOption: number | 'ALL';
}): BlogPaginated[];
export declare function shouldBeListed(blogPost: BlogPost): boolean;
export declare function getBlogTags({ blogPosts, ...params }: {
blogPosts: BlogPost[];
blogTitle: string;
blogDescription: string;
postsPerPageOption: number | 'ALL';
}): BlogTags;
type ParsedBlogFileName = {
date: Date | undefined;
text: string;
slug: string;
};
export declare function parseBlogFileName(blogSourceRelative: string): ParsedBlogFileName;
export declare function generateBlogPosts(contentPaths: BlogContentPaths, context: LoadContext, options: PluginOptions): Promise<BlogPost[]>;
export type LinkifyParams = {
filePath: string;
fileString: string;
} & Pick<BlogMarkdownLoaderOptions, 'sourceToPermalink' | 'siteDir' | 'contentPaths' | 'onBrokenMarkdownLink'>;
export declare function linkify({ filePath, contentPaths, fileString, siteDir, sourceToPermalink, onBrokenMarkdownLink, }: LinkifyParams): string;
export {};

View File

@@ -0,0 +1,280 @@
"use strict";
/**
* 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.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.linkify = exports.generateBlogPosts = exports.parseBlogFileName = exports.getBlogTags = exports.shouldBeListed = exports.paginateBlogPosts = exports.getSourceToPermalink = exports.truncate = void 0;
const tslib_1 = require("tslib");
const fs_extra_1 = tslib_1.__importDefault(require("fs-extra"));
const path_1 = tslib_1.__importDefault(require("path"));
const lodash_1 = tslib_1.__importDefault(require("lodash"));
const logger_1 = tslib_1.__importDefault(require("@docusaurus/logger"));
const reading_time_1 = tslib_1.__importDefault(require("reading-time"));
const utils_1 = require("@docusaurus/utils");
const frontMatter_1 = require("./frontMatter");
const authors_1 = require("./authors");
function truncate(fileString, truncateMarker) {
return fileString.split(truncateMarker, 1).shift();
}
exports.truncate = truncate;
function getSourceToPermalink(blogPosts) {
return Object.fromEntries(blogPosts.map(({ metadata: { source, permalink } }) => [source, permalink]));
}
exports.getSourceToPermalink = getSourceToPermalink;
function paginateBlogPosts({ blogPosts, basePageUrl, blogTitle, blogDescription, postsPerPageOption, }) {
const totalCount = blogPosts.length;
const postsPerPage = postsPerPageOption === 'ALL' ? totalCount : postsPerPageOption;
const numberOfPages = Math.ceil(totalCount / postsPerPage);
const pages = [];
function permalink(page) {
return page > 0
? (0, utils_1.normalizeUrl)([basePageUrl, `page/${page + 1}`])
: basePageUrl;
}
for (let page = 0; page < numberOfPages; page += 1) {
pages.push({
items: blogPosts
.slice(page * postsPerPage, (page + 1) * postsPerPage)
.map((item) => item.id),
metadata: {
permalink: permalink(page),
page: page + 1,
postsPerPage,
totalPages: numberOfPages,
totalCount,
previousPage: page !== 0 ? permalink(page - 1) : undefined,
nextPage: page < numberOfPages - 1 ? permalink(page + 1) : undefined,
blogDescription,
blogTitle,
},
});
}
return pages;
}
exports.paginateBlogPosts = paginateBlogPosts;
function shouldBeListed(blogPost) {
return !blogPost.metadata.unlisted;
}
exports.shouldBeListed = shouldBeListed;
function getBlogTags({ blogPosts, ...params }) {
const groups = (0, utils_1.groupTaggedItems)(blogPosts, (blogPost) => blogPost.metadata.tags);
return lodash_1.default.mapValues(groups, ({ tag, items: tagBlogPosts }) => {
const tagVisibility = (0, utils_1.getTagVisibility)({
items: tagBlogPosts,
isUnlisted: (item) => item.metadata.unlisted,
});
return {
label: tag.label,
items: tagVisibility.listedItems.map((item) => item.id),
permalink: tag.permalink,
pages: paginateBlogPosts({
blogPosts: tagVisibility.listedItems,
basePageUrl: tag.permalink,
...params,
}),
unlisted: tagVisibility.unlisted,
};
});
}
exports.getBlogTags = getBlogTags;
const DATE_FILENAME_REGEX = /^(?<folder>.*)(?<date>\d{4}[-/]\d{1,2}[-/]\d{1,2})[-/]?(?<text>.*?)(?:\/index)?.mdx?$/;
function parseBlogFileName(blogSourceRelative) {
const dateFilenameMatch = blogSourceRelative.match(DATE_FILENAME_REGEX);
if (dateFilenameMatch) {
const { folder, text, date: dateString } = dateFilenameMatch.groups;
// Always treat dates as UTC by adding the `Z`
const date = new Date(`${dateString}Z`);
const slugDate = dateString.replace(/-/g, '/');
const slug = `/${slugDate}/${folder}${text}`;
return { date, text: text, slug };
}
const text = blogSourceRelative.replace(/(?:\/index)?\.mdx?$/, '');
const slug = `/${text}`;
return { date: undefined, text, slug };
}
exports.parseBlogFileName = parseBlogFileName;
function formatBlogPostDate(locale, date, calendar) {
try {
return new Intl.DateTimeFormat(locale, {
day: 'numeric',
month: 'long',
year: 'numeric',
timeZone: 'UTC',
calendar,
}).format(date);
}
catch (err) {
logger_1.default.error `Can't format blog post date "${String(date)}"`;
throw err;
}
}
async function parseBlogPostMarkdownFile({ filePath, parseFrontMatter, }) {
const fileContent = await fs_extra_1.default.readFile(filePath, 'utf-8');
try {
const result = await (0, utils_1.parseMarkdownFile)({
filePath,
fileContent,
parseFrontMatter,
removeContentTitle: true,
});
return {
...result,
frontMatter: (0, frontMatter_1.validateBlogPostFrontMatter)(result.frontMatter),
};
}
catch (err) {
logger_1.default.error `Error while parsing blog post file path=${filePath}.`;
throw err;
}
}
const defaultReadingTime = ({ content, options }) => (0, reading_time_1.default)(content, options).minutes;
async function processBlogSourceFile(blogSourceRelative, contentPaths, context, options, authorsMap) {
const { siteConfig: { baseUrl, markdown: { parseFrontMatter }, }, siteDir, i18n, } = context;
const { routeBasePath, tagsBasePath: tagsRouteBasePath, truncateMarker, showReadingTime, editUrl, } = options;
// Lookup in localized folder in priority
const blogDirPath = await (0, utils_1.getFolderContainingFile)((0, utils_1.getContentPathList)(contentPaths), blogSourceRelative);
const blogSourceAbsolute = path_1.default.join(blogDirPath, blogSourceRelative);
const { frontMatter, content, contentTitle, excerpt } = await parseBlogPostMarkdownFile({
filePath: blogSourceAbsolute,
parseFrontMatter,
});
const aliasedSource = (0, utils_1.aliasedSitePath)(blogSourceAbsolute, siteDir);
const draft = (0, utils_1.isDraft)({ frontMatter });
const unlisted = (0, utils_1.isUnlisted)({ frontMatter });
if (draft) {
return undefined;
}
if (frontMatter.id) {
logger_1.default.warn `name=${'id'} header option is deprecated in path=${blogSourceRelative} file. Please use name=${'slug'} option instead.`;
}
const parsedBlogFileName = parseBlogFileName(blogSourceRelative);
async function getDate() {
// Prefer user-defined date.
if (frontMatter.date) {
if (typeof frontMatter.date === 'string') {
// Always treat dates as UTC by adding the `Z`
return new Date(`${frontMatter.date}Z`);
}
// YAML only converts YYYY-MM-DD to dates and leaves others as strings.
return frontMatter.date;
}
else if (parsedBlogFileName.date) {
return parsedBlogFileName.date;
}
try {
const result = (0, utils_1.getFileCommitDate)(blogSourceAbsolute, {
age: 'oldest',
includeAuthor: false,
});
return result.date;
}
catch (err) {
logger_1.default.warn(err);
return (await fs_extra_1.default.stat(blogSourceAbsolute)).birthtime;
}
}
const date = await getDate();
const formattedDate = formatBlogPostDate(i18n.currentLocale, date, i18n.localeConfigs[i18n.currentLocale].calendar);
const title = frontMatter.title ?? contentTitle ?? parsedBlogFileName.text;
const description = frontMatter.description ?? excerpt ?? '';
const slug = frontMatter.slug ?? parsedBlogFileName.slug;
const permalink = (0, utils_1.normalizeUrl)([baseUrl, routeBasePath, slug]);
function getBlogEditUrl() {
const blogPathRelative = path_1.default.relative(blogDirPath, path_1.default.resolve(blogSourceAbsolute));
if (typeof editUrl === 'function') {
return editUrl({
blogDirPath: (0, utils_1.posixPath)(path_1.default.relative(siteDir, blogDirPath)),
blogPath: (0, utils_1.posixPath)(blogPathRelative),
permalink,
locale: i18n.currentLocale,
});
}
else if (typeof editUrl === 'string') {
const isLocalized = blogDirPath === contentPaths.contentPathLocalized;
const fileContentPath = isLocalized && options.editLocalizedFiles
? contentPaths.contentPathLocalized
: contentPaths.contentPath;
const contentPathEditUrl = (0, utils_1.normalizeUrl)([
editUrl,
(0, utils_1.posixPath)(path_1.default.relative(siteDir, fileContentPath)),
]);
return (0, utils_1.getEditUrl)(blogPathRelative, contentPathEditUrl);
}
return undefined;
}
const tagsBasePath = (0, utils_1.normalizeUrl)([
baseUrl,
routeBasePath,
tagsRouteBasePath,
]);
const authors = (0, authors_1.getBlogPostAuthors)({ authorsMap, frontMatter, baseUrl });
return {
id: slug,
metadata: {
permalink,
editUrl: getBlogEditUrl(),
source: aliasedSource,
title,
description,
date,
formattedDate,
tags: (0, utils_1.normalizeFrontMatterTags)(tagsBasePath, frontMatter.tags),
readingTime: showReadingTime
? options.readingTime({
content,
frontMatter,
defaultReadingTime,
})
: undefined,
hasTruncateMarker: truncateMarker.test(content),
authors,
frontMatter,
unlisted,
},
content,
};
}
async function generateBlogPosts(contentPaths, context, options) {
const { include, exclude } = options;
if (!(await fs_extra_1.default.pathExists(contentPaths.contentPath))) {
return [];
}
const blogSourceFiles = await (0, utils_1.Globby)(include, {
cwd: contentPaths.contentPath,
ignore: exclude,
});
const authorsMap = await (0, authors_1.getAuthorsMap)({
contentPaths,
authorsMapPath: options.authorsMapPath,
});
async function doProcessBlogSourceFile(blogSourceFile) {
try {
return await processBlogSourceFile(blogSourceFile, contentPaths, context, options, authorsMap);
}
catch (err) {
throw new Error(`Processing of blog source file path=${blogSourceFile} failed.`, { cause: err });
}
}
const blogPosts = (await Promise.all(blogSourceFiles.map(doProcessBlogSourceFile))).filter(Boolean);
blogPosts.sort((a, b) => b.metadata.date.getTime() - a.metadata.date.getTime());
if (options.sortPosts === 'ascending') {
return blogPosts.reverse();
}
return blogPosts;
}
exports.generateBlogPosts = generateBlogPosts;
function linkify({ filePath, contentPaths, fileString, siteDir, sourceToPermalink, onBrokenMarkdownLink, }) {
const { newContent, brokenMarkdownLinks } = (0, utils_1.replaceMarkdownLinks)({
siteDir,
fileString,
filePath,
contentPaths,
sourceToPermalink,
});
brokenMarkdownLinks.forEach((l) => onBrokenMarkdownLink(l));
return newContent;
}
exports.linkify = linkify;

View File

@@ -0,0 +1,15 @@
/**
* 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 type { DocusaurusConfig } from '@docusaurus/types';
import type { PluginOptions, BlogPost } from '@docusaurus/plugin-content-blog';
export declare function createBlogFeedFiles({ blogPosts: allBlogPosts, options, siteConfig, outDir, locale, }: {
blogPosts: BlogPost[];
options: PluginOptions;
siteConfig: DocusaurusConfig;
outDir: string;
locale: string;
}): Promise<void>;

View File

@@ -0,0 +1,147 @@
"use strict";
/**
* 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.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.createBlogFeedFiles = void 0;
const tslib_1 = require("tslib");
const path_1 = tslib_1.__importDefault(require("path"));
const fs_extra_1 = tslib_1.__importDefault(require("fs-extra"));
const logger_1 = tslib_1.__importDefault(require("@docusaurus/logger"));
const feed_1 = require("feed");
const srcset = tslib_1.__importStar(require("srcset"));
const utils_1 = require("@docusaurus/utils");
const utils_common_1 = require("@docusaurus/utils-common");
const cheerio_1 = require("cheerio");
async function generateBlogFeed({ blogPosts, options, siteConfig, outDir, locale, }) {
if (!blogPosts.length) {
return null;
}
const { feedOptions, routeBasePath } = options;
const { url: siteUrl, baseUrl, title, favicon } = siteConfig;
const blogBaseUrl = (0, utils_1.normalizeUrl)([siteUrl, baseUrl, routeBasePath]);
const blogPostsForFeed = feedOptions.limit === false || feedOptions.limit === null
? blogPosts
: blogPosts.slice(0, feedOptions.limit);
const updated = blogPostsForFeed[0]?.metadata.date;
const feed = new feed_1.Feed({
id: blogBaseUrl,
title: feedOptions.title ?? `${title} Blog`,
updated,
language: feedOptions.language ?? locale,
link: blogBaseUrl,
description: feedOptions.description ?? `${siteConfig.title} Blog`,
favicon: favicon ? (0, utils_1.normalizeUrl)([siteUrl, baseUrl, favicon]) : undefined,
copyright: feedOptions.copyright,
});
const createFeedItems = options.feedOptions.createFeedItems ?? defaultCreateFeedItems;
const feedItems = await createFeedItems({
blogPosts: blogPostsForFeed,
siteConfig,
outDir,
defaultCreateFeedItems,
});
feedItems.forEach(feed.addItem);
return feed;
}
async function defaultCreateFeedItems({ blogPosts, siteConfig, outDir, }) {
const { url: siteUrl } = siteConfig;
function toFeedAuthor(author) {
return { name: author.name, link: author.url, email: author.email };
}
return Promise.all(blogPosts.map(async (post) => {
const { metadata: { title: metadataTitle, permalink, date, description, authors, tags, }, } = post;
const content = await (0, utils_1.readOutputHTMLFile)(permalink.replace(siteConfig.baseUrl, ''), outDir, siteConfig.trailingSlash);
const $ = (0, cheerio_1.load)(content);
const blogPostAbsoluteUrl = (0, utils_1.normalizeUrl)([siteUrl, permalink]);
const toAbsoluteUrl = (src) => String(new URL(src, blogPostAbsoluteUrl));
// Make links and image urls absolute
// See https://github.com/facebook/docusaurus/issues/9136
$(`div#${utils_common_1.blogPostContainerID} a, div#${utils_common_1.blogPostContainerID} img`).each((_, elm) => {
if (elm.tagName === 'a') {
const { href } = elm.attribs;
if (href) {
elm.attribs.href = toAbsoluteUrl(href);
}
}
else if (elm.tagName === 'img') {
const { src, srcset: srcsetAttr } = elm.attribs;
if (src) {
elm.attribs.src = toAbsoluteUrl(src);
}
if (srcsetAttr) {
elm.attribs.srcset = srcset.stringify(srcset.parse(srcsetAttr).map((props) => ({
...props,
url: toAbsoluteUrl(props.url),
})));
}
}
});
const feedItem = {
title: metadataTitle,
id: blogPostAbsoluteUrl,
link: blogPostAbsoluteUrl,
date,
description,
// Atom feed demands the "term", while other feeds use "name"
category: tags.map((tag) => ({ name: tag.label, term: tag.label })),
content: $(`#${utils_common_1.blogPostContainerID}`).html(),
};
// json1() method takes the first item of authors array
// it causes an error when authors array is empty
const feedItemAuthors = authors.map(toFeedAuthor);
if (feedItemAuthors.length > 0) {
feedItem.author = feedItemAuthors;
}
return feedItem;
}));
}
async function createBlogFeedFile({ feed, feedType, generatePath, }) {
const [feedContent, feedPath] = (() => {
switch (feedType) {
case 'rss':
return [feed.rss2(), 'rss.xml'];
case 'json':
return [feed.json1(), 'feed.json'];
case 'atom':
return [feed.atom1(), 'atom.xml'];
default:
throw new Error(`Feed type ${feedType} not supported.`);
}
})();
try {
await fs_extra_1.default.outputFile(path_1.default.join(generatePath, feedPath), feedContent);
}
catch (err) {
logger_1.default.error(`Generating ${feedType} feed failed.`);
throw err;
}
}
function shouldBeInFeed(blogPost) {
const excluded = blogPost.metadata.frontMatter.draft ||
blogPost.metadata.frontMatter.unlisted;
return !excluded;
}
async function createBlogFeedFiles({ blogPosts: allBlogPosts, options, siteConfig, outDir, locale, }) {
const blogPosts = allBlogPosts.filter(shouldBeInFeed);
const feed = await generateBlogFeed({
blogPosts,
options,
siteConfig,
outDir,
locale,
});
const feedTypes = options.feedOptions.type;
if (!feed || !feedTypes) {
return;
}
await Promise.all(feedTypes.map((feedType) => createBlogFeedFile({
feed,
feedType,
generatePath: path_1.default.join(outDir, options.routeBasePath),
})));
}
exports.createBlogFeedFiles = createBlogFeedFiles;

View File

@@ -0,0 +1,10 @@
/**
* 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 type { BlogPostFrontMatter } from '@docusaurus/plugin-content-blog';
export declare function validateBlogPostFrontMatter(frontMatter: {
[key: string]: unknown;
}): BlogPostFrontMatter;

View File

@@ -0,0 +1,63 @@
"use strict";
/**
* 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.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.validateBlogPostFrontMatter = void 0;
const utils_validation_1 = require("@docusaurus/utils-validation");
const BlogPostFrontMatterAuthorSchema = utils_validation_1.JoiFrontMatter.object({
key: utils_validation_1.JoiFrontMatter.string(),
name: utils_validation_1.JoiFrontMatter.string(),
title: utils_validation_1.JoiFrontMatter.string(),
url: utils_validation_1.URISchema,
imageURL: utils_validation_1.JoiFrontMatter.string(),
})
.or('key', 'name', 'imageURL')
.rename('image_url', 'imageURL', { alias: true });
const FrontMatterAuthorErrorMessage = '{{#label}} does not look like a valid blog post author. Please use an author key or an author object (with a key and/or name).';
const BlogFrontMatterSchema = utils_validation_1.JoiFrontMatter.object({
id: utils_validation_1.JoiFrontMatter.string(),
title: utils_validation_1.JoiFrontMatter.string().allow(''),
description: utils_validation_1.JoiFrontMatter.string().allow(''),
tags: utils_validation_1.FrontMatterTagsSchema,
date: utils_validation_1.JoiFrontMatter.date().raw(),
// New multi-authors front matter:
authors: utils_validation_1.JoiFrontMatter.alternatives()
.try(utils_validation_1.JoiFrontMatter.string(), BlogPostFrontMatterAuthorSchema, utils_validation_1.JoiFrontMatter.array()
.items(utils_validation_1.JoiFrontMatter.string(), BlogPostFrontMatterAuthorSchema)
.messages({
'array.sparse': FrontMatterAuthorErrorMessage,
'array.includes': FrontMatterAuthorErrorMessage,
}))
.messages({
'alternatives.match': FrontMatterAuthorErrorMessage,
}),
// Legacy author front matter
author: utils_validation_1.JoiFrontMatter.string(),
author_title: utils_validation_1.JoiFrontMatter.string(),
author_url: utils_validation_1.URISchema,
author_image_url: utils_validation_1.URISchema,
// TODO enable deprecation warnings later
authorURL: utils_validation_1.URISchema,
// .warning('deprecate.error', { alternative: '"author_url"'}),
authorTitle: utils_validation_1.JoiFrontMatter.string(),
// .warning('deprecate.error', { alternative: '"author_title"'}),
authorImageURL: utils_validation_1.URISchema,
// .warning('deprecate.error', { alternative: '"author_image_url"'}),
slug: utils_validation_1.JoiFrontMatter.string(),
image: utils_validation_1.URISchema,
keywords: utils_validation_1.JoiFrontMatter.array().items(utils_validation_1.JoiFrontMatter.string().required()),
hide_table_of_contents: utils_validation_1.JoiFrontMatter.boolean(),
...utils_validation_1.FrontMatterTOCHeadingLevels,
})
.messages({
'deprecate.error': '{#label} blog frontMatter field is deprecated. Please use {#alternative} instead.',
})
.concat(utils_validation_1.ContentVisibilitySchema);
function validateBlogPostFrontMatter(frontMatter) {
return (0, utils_validation_1.validateFrontMatter)(frontMatter, BlogFrontMatterSchema);
}
exports.validateBlogPostFrontMatter = validateBlogPostFrontMatter;

View File

@@ -0,0 +1,10 @@
/**
* 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 type { LoadContext, Plugin } from '@docusaurus/types';
import type { PluginOptions, BlogContent } from '@docusaurus/plugin-content-blog';
export default function pluginContentBlog(context: LoadContext, options: PluginOptions): Promise<Plugin<BlogContent>>;
export { validateOptions } from './options';

View File

@@ -0,0 +1,366 @@
"use strict";
/**
* 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.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.validateOptions = void 0;
const tslib_1 = require("tslib");
const path_1 = tslib_1.__importDefault(require("path"));
const logger_1 = tslib_1.__importDefault(require("@docusaurus/logger"));
const utils_1 = require("@docusaurus/utils");
const blogUtils_1 = require("./blogUtils");
const footnoteIDFixer_1 = tslib_1.__importDefault(require("./remark/footnoteIDFixer"));
const translations_1 = require("./translations");
const feed_1 = require("./feed");
const props_1 = require("./props");
async function pluginContentBlog(context, options) {
const { siteDir, siteConfig, generatedFilesDir, localizationDir, i18n: { currentLocale }, } = context;
const { onBrokenMarkdownLinks, baseUrl } = siteConfig;
const contentPaths = {
contentPath: path_1.default.resolve(siteDir, options.path),
contentPathLocalized: (0, utils_1.getPluginI18nPath)({
localizationDir,
pluginName: 'docusaurus-plugin-content-blog',
pluginId: options.id,
}),
};
const pluginId = options.id ?? utils_1.DEFAULT_PLUGIN_ID;
const pluginDataDirRoot = path_1.default.join(generatedFilesDir, 'docusaurus-plugin-content-blog');
const dataDir = path_1.default.join(pluginDataDirRoot, pluginId);
const aliasedSource = (source) => `~blog/${(0, utils_1.posixPath)(path_1.default.relative(pluginDataDirRoot, source))}`;
const authorsMapFilePath = await (0, utils_1.getDataFilePath)({
filePath: options.authorsMapPath,
contentPaths,
});
return {
name: 'docusaurus-plugin-content-blog',
getPathsToWatch() {
const { include } = options;
const contentMarkdownGlobs = (0, utils_1.getContentPathList)(contentPaths).flatMap((contentPath) => include.map((pattern) => `${contentPath}/${pattern}`));
return [authorsMapFilePath, ...contentMarkdownGlobs].filter(Boolean);
},
getTranslationFiles() {
return (0, translations_1.getTranslationFiles)(options);
},
// Fetches blog contents and returns metadata for the necessary routes.
async loadContent() {
const { postsPerPage: postsPerPageOption, routeBasePath, tagsBasePath, blogDescription, blogTitle, blogSidebarTitle, } = options;
const baseBlogUrl = (0, utils_1.normalizeUrl)([baseUrl, routeBasePath]);
const blogTagsListPath = (0, utils_1.normalizeUrl)([baseBlogUrl, tagsBasePath]);
const blogPosts = await (0, blogUtils_1.generateBlogPosts)(contentPaths, context, options);
const listedBlogPosts = blogPosts.filter(blogUtils_1.shouldBeListed);
if (!blogPosts.length) {
return {
blogSidebarTitle,
blogPosts: [],
blogListPaginated: [],
blogTags: {},
blogTagsListPath,
blogTagsPaginated: [],
};
}
// Colocate next and prev metadata.
listedBlogPosts.forEach((blogPost, index) => {
const prevItem = index > 0 ? listedBlogPosts[index - 1] : null;
if (prevItem) {
blogPost.metadata.prevItem = {
title: prevItem.metadata.title,
permalink: prevItem.metadata.permalink,
};
}
const nextItem = index < listedBlogPosts.length - 1
? listedBlogPosts[index + 1]
: null;
if (nextItem) {
blogPost.metadata.nextItem = {
title: nextItem.metadata.title,
permalink: nextItem.metadata.permalink,
};
}
});
const blogListPaginated = (0, blogUtils_1.paginateBlogPosts)({
blogPosts: listedBlogPosts,
blogTitle,
blogDescription,
postsPerPageOption,
basePageUrl: baseBlogUrl,
});
const blogTags = (0, blogUtils_1.getBlogTags)({
blogPosts,
postsPerPageOption,
blogDescription,
blogTitle,
});
return {
blogSidebarTitle,
blogPosts,
blogListPaginated,
blogTags,
blogTagsListPath,
};
},
async contentLoaded({ content: blogContents, actions }) {
const { blogListComponent, blogPostComponent, blogTagsListComponent, blogTagsPostsComponent, blogArchiveComponent, routeBasePath, archiveBasePath, } = options;
const { addRoute, createData } = actions;
const { blogSidebarTitle, blogPosts, blogListPaginated, blogTags, blogTagsListPath, } = blogContents;
const listedBlogPosts = blogPosts.filter(blogUtils_1.shouldBeListed);
const blogItemsToMetadata = {};
const sidebarBlogPosts = options.blogSidebarCount === 'ALL'
? blogPosts
: blogPosts.slice(0, options.blogSidebarCount);
function blogPostItemsModule(items) {
return items.map((postId) => {
const blogPostMetadata = blogItemsToMetadata[postId];
return {
content: {
__import: true,
path: blogPostMetadata.source,
query: {
truncated: true,
},
},
};
});
}
if (archiveBasePath && listedBlogPosts.length) {
const archiveUrl = (0, utils_1.normalizeUrl)([
baseUrl,
routeBasePath,
archiveBasePath,
]);
// Create a blog archive route
const archiveProp = await createData(`${(0, utils_1.docuHash)(archiveUrl)}.json`, JSON.stringify({ blogPosts: listedBlogPosts }, null, 2));
addRoute({
path: archiveUrl,
component: blogArchiveComponent,
exact: true,
modules: {
archive: aliasedSource(archiveProp),
},
});
}
// This prop is useful to provide the blog list sidebar
const sidebarProp = await createData(
// Note that this created data path must be in sync with
// metadataPath provided to mdx-loader.
`blog-post-list-prop-${pluginId}.json`, JSON.stringify({
title: blogSidebarTitle,
items: sidebarBlogPosts.map((blogPost) => ({
title: blogPost.metadata.title,
permalink: blogPost.metadata.permalink,
unlisted: blogPost.metadata.unlisted,
})),
}, null, 2));
// Create routes for blog entries.
await Promise.all(blogPosts.map(async (blogPost) => {
const { id, metadata } = blogPost;
await createData(
// Note that this created data path must be in sync with
// metadataPath provided to mdx-loader.
`${(0, utils_1.docuHash)(metadata.source)}.json`, JSON.stringify(metadata, null, 2));
addRoute({
path: metadata.permalink,
component: blogPostComponent,
exact: true,
modules: {
sidebar: aliasedSource(sidebarProp),
content: metadata.source,
},
});
blogItemsToMetadata[id] = metadata;
}));
// Create routes for blog's paginated list entries.
await Promise.all(blogListPaginated.map(async (listPage) => {
const { metadata, items } = listPage;
const { permalink } = metadata;
const pageMetadataPath = await createData(`${(0, utils_1.docuHash)(permalink)}.json`, JSON.stringify(metadata, null, 2));
addRoute({
path: permalink,
component: blogListComponent,
exact: true,
modules: {
sidebar: aliasedSource(sidebarProp),
items: blogPostItemsModule(items),
metadata: aliasedSource(pageMetadataPath),
},
});
}));
// Tags. This is the last part so we early-return if there are no tags.
if (Object.keys(blogTags).length === 0) {
return;
}
async function createTagsListPage() {
const tagsPropPath = await createData(`${(0, utils_1.docuHash)(`${blogTagsListPath}-tags`)}.json`, JSON.stringify((0, props_1.toTagsProp)({ blogTags }), null, 2));
addRoute({
path: blogTagsListPath,
component: blogTagsListComponent,
exact: true,
modules: {
sidebar: aliasedSource(sidebarProp),
tags: aliasedSource(tagsPropPath),
},
});
}
async function createTagPostsListPage(tag) {
await Promise.all(tag.pages.map(async (blogPaginated) => {
const { metadata, items } = blogPaginated;
const tagPropPath = await createData(`${(0, utils_1.docuHash)(metadata.permalink)}.json`, JSON.stringify((0, props_1.toTagProp)({ tag, blogTagsListPath }), null, 2));
const listMetadataPath = await createData(`${(0, utils_1.docuHash)(metadata.permalink)}-list.json`, JSON.stringify(metadata, null, 2));
addRoute({
path: metadata.permalink,
component: blogTagsPostsComponent,
exact: true,
modules: {
sidebar: aliasedSource(sidebarProp),
items: blogPostItemsModule(items),
tag: aliasedSource(tagPropPath),
listMetadata: aliasedSource(listMetadataPath),
},
});
}));
}
await createTagsListPage();
await Promise.all(Object.values(blogTags).map(createTagPostsListPage));
},
translateContent({ content, translationFiles }) {
return (0, translations_1.translateContent)(content, translationFiles);
},
configureWebpack(_config, isServer, utils, content) {
const { admonitions, rehypePlugins, remarkPlugins, truncateMarker, beforeDefaultRemarkPlugins, beforeDefaultRehypePlugins, } = options;
const markdownLoaderOptions = {
siteDir,
contentPaths,
truncateMarker,
sourceToPermalink: (0, blogUtils_1.getSourceToPermalink)(content.blogPosts),
onBrokenMarkdownLink: (brokenMarkdownLink) => {
if (onBrokenMarkdownLinks === 'ignore') {
return;
}
logger_1.default.report(onBrokenMarkdownLinks) `Blog markdown link couldn't be resolved: (url=${brokenMarkdownLink.link}) in path=${brokenMarkdownLink.filePath}`;
},
};
const contentDirs = (0, utils_1.getContentPathList)(contentPaths);
return {
resolve: {
alias: {
'~blog': pluginDataDirRoot,
},
},
module: {
rules: [
{
test: /\.mdx?$/i,
include: contentDirs
// Trailing slash is important, see https://github.com/facebook/docusaurus/pull/3970
.map(utils_1.addTrailingPathSeparator),
use: [
{
loader: require.resolve('@docusaurus/mdx-loader'),
options: {
admonitions,
remarkPlugins,
rehypePlugins,
beforeDefaultRemarkPlugins: [
footnoteIDFixer_1.default,
...beforeDefaultRemarkPlugins,
],
beforeDefaultRehypePlugins,
staticDirs: siteConfig.staticDirectories.map((dir) => path_1.default.resolve(siteDir, dir)),
siteDir,
isMDXPartial: (0, utils_1.createAbsoluteFilePathMatcher)(options.exclude, contentDirs),
metadataPath: (mdxPath) => {
// Note that metadataPath must be the same/in-sync as
// the path from createData for each MDX.
const aliasedPath = (0, utils_1.aliasedSitePath)(mdxPath, siteDir);
return path_1.default.join(dataDir, `${(0, utils_1.docuHash)(aliasedPath)}.json`);
},
// For blog posts a title in markdown is always removed
// Blog posts title are rendered separately
removeContentTitle: true,
// Assets allow to convert some relative images paths to
// require() calls
createAssets: ({ frontMatter, metadata, }) => ({
image: frontMatter.image,
authorsImageUrls: metadata.authors.map((author) => author.imageURL),
}),
markdownConfig: siteConfig.markdown,
},
},
{
loader: path_1.default.resolve(__dirname, './markdownLoader.js'),
options: markdownLoaderOptions,
},
].filter(Boolean),
},
],
},
};
},
async postBuild({ outDir, content }) {
if (!options.feedOptions.type) {
return;
}
const { blogPosts } = content;
if (!blogPosts.length) {
return;
}
await (0, feed_1.createBlogFeedFiles)({
blogPosts,
options,
outDir,
siteConfig,
locale: currentLocale,
});
},
injectHtmlTags({ content }) {
if (!content.blogPosts.length || !options.feedOptions.type) {
return {};
}
const feedTypes = options.feedOptions.type;
const feedTitle = options.feedOptions.title ?? context.siteConfig.title;
const feedsConfig = {
rss: {
type: 'application/rss+xml',
path: 'rss.xml',
title: `${feedTitle} RSS Feed`,
},
atom: {
type: 'application/atom+xml',
path: 'atom.xml',
title: `${feedTitle} Atom Feed`,
},
json: {
type: 'application/json',
path: 'feed.json',
title: `${feedTitle} JSON Feed`,
},
};
const headTags = [];
feedTypes.forEach((feedType) => {
const { type, path: feedConfigPath, title: feedConfigTitle, } = feedsConfig[feedType];
headTags.push({
tagName: 'link',
attributes: {
rel: 'alternate',
type,
href: (0, utils_1.normalizeUrl)([
baseUrl,
options.routeBasePath,
feedConfigPath,
]),
title: feedConfigTitle,
},
});
});
return {
headTags,
};
},
};
}
exports.default = pluginContentBlog;
var options_1 = require("./options");
Object.defineProperty(exports, "validateOptions", { enumerable: true, get: function () { return options_1.validateOptions; } });

View File

@@ -0,0 +1,9 @@
/**
* 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 type { BlogMarkdownLoaderOptions } from './types';
import type { LoaderContext } from 'webpack';
export default function markdownLoader(this: LoaderContext<BlogMarkdownLoaderOptions>, source: string): void;

View File

@@ -0,0 +1,30 @@
"use strict";
/**
* 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.
*/
Object.defineProperty(exports, "__esModule", { value: true });
const blogUtils_1 = require("./blogUtils");
function markdownLoader(source) {
const filePath = this.resourcePath;
const fileString = source;
const callback = this.async();
const markdownLoaderOptions = this.getOptions();
// Linkify blog posts
let finalContent = (0, blogUtils_1.linkify)({
fileString,
filePath,
...markdownLoaderOptions,
});
// Truncate content if requested (e.g: file.md?truncated=true).
const truncated = this.resourceQuery
? !!new URLSearchParams(this.resourceQuery.slice(1)).get('truncated')
: undefined;
if (truncated) {
finalContent = (0, blogUtils_1.truncate)(finalContent, markdownLoaderOptions.truncateMarker);
}
return callback(null, finalContent);
}
exports.default = markdownLoader;

View File

@@ -0,0 +1,10 @@
/**
* 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 type { PluginOptions, Options } from '@docusaurus/plugin-content-blog';
import type { OptionValidationContext } from '@docusaurus/types';
export declare const DEFAULT_OPTIONS: PluginOptions;
export declare function validateOptions({ validate, options, }: OptionValidationContext<Options | undefined, PluginOptions>): PluginOptions;

View File

@@ -0,0 +1,109 @@
"use strict";
/**
* 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.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.validateOptions = exports.DEFAULT_OPTIONS = void 0;
const utils_validation_1 = require("@docusaurus/utils-validation");
const utils_1 = require("@docusaurus/utils");
exports.DEFAULT_OPTIONS = {
feedOptions: { type: ['rss', 'atom'], copyright: '', limit: 20 },
beforeDefaultRehypePlugins: [],
beforeDefaultRemarkPlugins: [],
admonitions: true,
truncateMarker: /<!--\s*truncate\s*-->|\{\/\*\s*truncate\s*\*\/\}/,
rehypePlugins: [],
remarkPlugins: [],
showReadingTime: true,
blogTagsPostsComponent: '@theme/BlogTagsPostsPage',
blogTagsListComponent: '@theme/BlogTagsListPage',
blogPostComponent: '@theme/BlogPostPage',
blogListComponent: '@theme/BlogListPage',
blogArchiveComponent: '@theme/BlogArchivePage',
blogDescription: 'Blog',
blogTitle: 'Blog',
blogSidebarCount: 5,
blogSidebarTitle: 'Recent posts',
postsPerPage: 10,
include: ['**/*.{md,mdx}'],
exclude: utils_1.GlobExcludeDefault,
routeBasePath: 'blog',
tagsBasePath: 'tags',
archiveBasePath: 'archive',
path: 'blog',
editLocalizedFiles: false,
authorsMapPath: 'authors.yml',
readingTime: ({ content, defaultReadingTime }) => defaultReadingTime({ content }),
sortPosts: 'descending',
};
const PluginOptionSchema = utils_validation_1.Joi.object({
path: utils_validation_1.Joi.string().default(exports.DEFAULT_OPTIONS.path),
archiveBasePath: utils_validation_1.Joi.string()
.default(exports.DEFAULT_OPTIONS.archiveBasePath)
.allow(null),
routeBasePath: utils_validation_1.RouteBasePathSchema.default(exports.DEFAULT_OPTIONS.routeBasePath),
tagsBasePath: utils_validation_1.Joi.string().default(exports.DEFAULT_OPTIONS.tagsBasePath),
include: utils_validation_1.Joi.array().items(utils_validation_1.Joi.string()).default(exports.DEFAULT_OPTIONS.include),
exclude: utils_validation_1.Joi.array().items(utils_validation_1.Joi.string()).default(exports.DEFAULT_OPTIONS.exclude),
postsPerPage: utils_validation_1.Joi.alternatives()
.try(utils_validation_1.Joi.equal('ALL').required(), utils_validation_1.Joi.number().integer().min(1).required())
.default(exports.DEFAULT_OPTIONS.postsPerPage),
blogListComponent: utils_validation_1.Joi.string().default(exports.DEFAULT_OPTIONS.blogListComponent),
blogPostComponent: utils_validation_1.Joi.string().default(exports.DEFAULT_OPTIONS.blogPostComponent),
blogTagsListComponent: utils_validation_1.Joi.string().default(exports.DEFAULT_OPTIONS.blogTagsListComponent),
blogTagsPostsComponent: utils_validation_1.Joi.string().default(exports.DEFAULT_OPTIONS.blogTagsPostsComponent),
blogArchiveComponent: utils_validation_1.Joi.string().default(exports.DEFAULT_OPTIONS.blogArchiveComponent),
blogTitle: utils_validation_1.Joi.string().allow('').default(exports.DEFAULT_OPTIONS.blogTitle),
blogDescription: utils_validation_1.Joi.string()
.allow('')
.default(exports.DEFAULT_OPTIONS.blogDescription),
blogSidebarCount: utils_validation_1.Joi.alternatives()
.try(utils_validation_1.Joi.equal('ALL').required(), utils_validation_1.Joi.number().integer().min(0).required())
.default(exports.DEFAULT_OPTIONS.blogSidebarCount),
blogSidebarTitle: utils_validation_1.Joi.string().default(exports.DEFAULT_OPTIONS.blogSidebarTitle),
showReadingTime: utils_validation_1.Joi.bool().default(exports.DEFAULT_OPTIONS.showReadingTime),
remarkPlugins: utils_validation_1.RemarkPluginsSchema.default(exports.DEFAULT_OPTIONS.remarkPlugins),
rehypePlugins: utils_validation_1.RehypePluginsSchema.default(exports.DEFAULT_OPTIONS.rehypePlugins),
admonitions: utils_validation_1.AdmonitionsSchema.default(exports.DEFAULT_OPTIONS.admonitions),
editUrl: utils_validation_1.Joi.alternatives().try(utils_validation_1.URISchema, utils_validation_1.Joi.function()),
editLocalizedFiles: utils_validation_1.Joi.boolean().default(exports.DEFAULT_OPTIONS.editLocalizedFiles),
truncateMarker: utils_validation_1.Joi.object().default(exports.DEFAULT_OPTIONS.truncateMarker),
beforeDefaultRemarkPlugins: utils_validation_1.RemarkPluginsSchema.default(exports.DEFAULT_OPTIONS.beforeDefaultRemarkPlugins),
beforeDefaultRehypePlugins: utils_validation_1.RehypePluginsSchema.default(exports.DEFAULT_OPTIONS.beforeDefaultRehypePlugins),
feedOptions: utils_validation_1.Joi.object({
type: utils_validation_1.Joi.alternatives()
.try(utils_validation_1.Joi.array().items(utils_validation_1.Joi.string().equal('rss', 'atom', 'json')), utils_validation_1.Joi.alternatives().conditional(utils_validation_1.Joi.string().equal('all', 'rss', 'atom', 'json'), {
then: utils_validation_1.Joi.custom((val) => val === 'all' ? ['rss', 'atom', 'json'] : [val]),
}))
.allow(null)
.default(exports.DEFAULT_OPTIONS.feedOptions.type),
title: utils_validation_1.Joi.string().allow(''),
description: utils_validation_1.Joi.string().allow(''),
// Only add default value when user actually wants a feed (type is not null)
copyright: utils_validation_1.Joi.when('type', {
is: utils_validation_1.Joi.any().valid(null),
then: utils_validation_1.Joi.string().optional(),
otherwise: utils_validation_1.Joi.string()
.allow('')
.default(exports.DEFAULT_OPTIONS.feedOptions.copyright),
}),
language: utils_validation_1.Joi.string(),
createFeedItems: utils_validation_1.Joi.function(),
limit: utils_validation_1.Joi.alternatives()
.try(utils_validation_1.Joi.number(), utils_validation_1.Joi.valid(null), utils_validation_1.Joi.valid(false))
.default(exports.DEFAULT_OPTIONS.feedOptions.limit),
}).default(exports.DEFAULT_OPTIONS.feedOptions),
authorsMapPath: utils_validation_1.Joi.string().default(exports.DEFAULT_OPTIONS.authorsMapPath),
readingTime: utils_validation_1.Joi.function().default(() => exports.DEFAULT_OPTIONS.readingTime),
sortPosts: utils_validation_1.Joi.string()
.valid('descending', 'ascending')
.default(exports.DEFAULT_OPTIONS.sortPosts),
}).default(exports.DEFAULT_OPTIONS);
function validateOptions({ validate, options, }) {
const validatedOptions = validate(PluginOptionSchema, options);
return validatedOptions;
}
exports.validateOptions = validateOptions;

View File

@@ -0,0 +1,15 @@
/**
* 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 type { TagsListItem, TagModule } from '@docusaurus/utils';
import type { BlogTag, BlogTags } from '@docusaurus/plugin-content-blog';
export declare function toTagsProp({ blogTags }: {
blogTags: BlogTags;
}): TagsListItem[];
export declare function toTagProp({ blogTagsListPath, tag, }: {
blogTagsListPath: string;
tag: BlogTag;
}): TagModule;

View File

@@ -0,0 +1,23 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.toTagProp = exports.toTagsProp = void 0;
function toTagsProp({ blogTags }) {
return Object.values(blogTags)
.filter((tag) => !tag.unlisted)
.map((tag) => ({
label: tag.label,
permalink: tag.permalink,
count: tag.items.length,
}));
}
exports.toTagsProp = toTagsProp;
function toTagProp({ blogTagsListPath, tag, }) {
return {
label: tag.label,
permalink: tag.permalink,
allTagsPath: blogTagsListPath,
count: tag.items.length,
unlisted: tag.unlisted,
};
}
exports.toTagProp = toTagProp;

View File

@@ -0,0 +1,14 @@
/**
* 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 type { Transformer } from 'unified';
/**
* In the blog list view, each post will be compiled separately. However, they
* may use the same footnote IDs. This leads to duplicated DOM IDs and inability
* to navigate to footnote references. This plugin fixes it by appending a
* unique hash to each reference/definition.
*/
export default function plugin(): Transformer;

View File

@@ -0,0 +1,28 @@
"use strict";
/**
* 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.
*/
Object.defineProperty(exports, "__esModule", { value: true });
const utils_1 = require("@docusaurus/utils");
/**
* In the blog list view, each post will be compiled separately. However, they
* may use the same footnote IDs. This leads to duplicated DOM IDs and inability
* to navigate to footnote references. This plugin fixes it by appending a
* unique hash to each reference/definition.
*/
function plugin() {
return async (root, vfile) => {
const { visit } = await import('unist-util-visit');
const suffix = `-${(0, utils_1.simpleHash)(vfile.path, 6)}`;
visit(root, 'footnoteReference', (node) => {
node.identifier += suffix;
});
visit(root, 'footnoteDefinition', (node) => {
node.identifier += suffix;
});
};
}
exports.default = plugin;

View File

@@ -0,0 +1,10 @@
/**
* 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 type { TranslationFile } from '@docusaurus/types';
import type { PluginOptions, BlogContent } from '@docusaurus/plugin-content-blog';
export declare function getTranslationFiles(options: PluginOptions): TranslationFile[];
export declare function translateContent(content: BlogContent, translationFiles: TranslationFile[]): BlogContent;

View File

@@ -0,0 +1,53 @@
"use strict";
/**
* 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.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.translateContent = exports.getTranslationFiles = void 0;
function translateListPage(blogListPaginated, translations) {
return blogListPaginated.map((page) => {
const { items, metadata } = page;
return {
items,
metadata: {
...metadata,
blogTitle: translations.title?.message ?? page.metadata.blogTitle,
blogDescription: translations.description?.message ?? page.metadata.blogDescription,
},
};
});
}
function getTranslationFiles(options) {
return [
{
path: 'options',
content: {
title: {
message: options.blogTitle,
description: 'The title for the blog used in SEO',
},
description: {
message: options.blogDescription,
description: 'The description for the blog used in SEO',
},
'sidebar.title': {
message: options.blogSidebarTitle,
description: 'The label for the left sidebar',
},
},
},
];
}
exports.getTranslationFiles = getTranslationFiles;
function translateContent(content, translationFiles) {
const { content: optionsTranslations } = translationFiles[0];
return {
...content,
blogSidebarTitle: optionsTranslations['sidebar.title']?.message ?? content.blogSidebarTitle,
blogListPaginated: translateListPage(content.blogListPaginated, optionsTranslations),
};
}
exports.translateContent = translateContent;

View File

@@ -0,0 +1,18 @@
/**
* 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 type { BrokenMarkdownLink, ContentPaths } from '@docusaurus/utils';
export type BlogContentPaths = ContentPaths;
export type BlogBrokenMarkdownLink = BrokenMarkdownLink<BlogContentPaths>;
export type BlogMarkdownLoaderOptions = {
siteDir: string;
contentPaths: BlogContentPaths;
truncateMarker: RegExp;
sourceToPermalink: {
[aliasedPath: string]: string;
};
onBrokenMarkdownLink: (brokenMarkdownLink: BlogBrokenMarkdownLink) => void;
};

View File

@@ -0,0 +1,8 @@
"use strict";
/**
* 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.
*/
Object.defineProperty(exports, "__esModule", { value: true });

View File

@@ -0,0 +1,48 @@
{
"name": "@docusaurus/plugin-content-blog",
"version": "3.1.1",
"description": "Blog plugin for Docusaurus.",
"main": "lib/index.js",
"types": "src/plugin-content-blog.d.ts",
"scripts": {
"build": "tsc",
"watch": "tsc --watch",
"test:generate-build-snap": "yarn docusaurus build src/__tests__/__fixtures__/website --out-dir build-snap && yarn rimraf src/__tests__/__fixtures__/website/.docusaurus && yarn rimraf src/__tests__/__fixtures__/website/build-snap/assets && git add src/__tests__/__fixtures__/website/build-snap"
},
"repository": {
"type": "git",
"url": "https://github.com/facebook/docusaurus.git",
"directory": "packages/docusaurus-plugin-content-blog"
},
"publishConfig": {
"access": "public"
},
"license": "MIT",
"dependencies": {
"@docusaurus/core": "3.1.1",
"@docusaurus/logger": "3.1.1",
"@docusaurus/mdx-loader": "3.1.1",
"@docusaurus/types": "3.1.1",
"@docusaurus/utils": "3.1.1",
"@docusaurus/utils-common": "3.1.1",
"@docusaurus/utils-validation": "3.1.1",
"cheerio": "^1.0.0-rc.12",
"feed": "^4.2.2",
"fs-extra": "^11.1.1",
"lodash": "^4.17.21",
"reading-time": "^1.5.0",
"srcset": "^4.0.0",
"tslib": "^2.6.0",
"unist-util-visit": "^5.0.0",
"utility-types": "^3.10.0",
"webpack": "^5.88.1"
},
"peerDependencies": {
"react": "^18.0.0",
"react-dom": "^18.0.0"
},
"engines": {
"node": ">=18.0"
},
"gitHead": "8017f6a6776ba1bd7065e630a52fe2c2654e2f1b"
}

View File

@@ -0,0 +1,200 @@
/**
* 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 {getDataFileData, normalizeUrl} from '@docusaurus/utils';
import {Joi, URISchema} from '@docusaurus/utils-validation';
import type {BlogContentPaths} from './types';
import type {
Author,
BlogPostFrontMatter,
BlogPostFrontMatterAuthor,
BlogPostFrontMatterAuthors,
} from '@docusaurus/plugin-content-blog';
export type AuthorsMap = {[authorKey: string]: Author};
const AuthorsMapSchema = Joi.object<AuthorsMap>()
.pattern(
Joi.string(),
Joi.object({
name: Joi.string(),
url: URISchema,
imageURL: URISchema,
title: Joi.string(),
email: Joi.string(),
})
.rename('image_url', 'imageURL')
.or('name', 'imageURL')
.unknown()
.required()
.messages({
'object.base':
'{#label} should be an author object containing properties like name, title, and imageURL.',
'any.required':
'{#label} cannot be undefined. It should be an author object containing properties like name, title, and imageURL.',
}),
)
.messages({
'object.base':
"The authors map file should contain an object where each entry contains an author key and the corresponding author's data.",
});
export function validateAuthorsMap(content: unknown): AuthorsMap {
const {error, value} = AuthorsMapSchema.validate(content);
if (error) {
throw error;
}
return value;
}
export async function getAuthorsMap(params: {
authorsMapPath: string;
contentPaths: BlogContentPaths;
}): Promise<AuthorsMap | undefined> {
return getDataFileData(
{
filePath: params.authorsMapPath,
contentPaths: params.contentPaths,
fileType: 'authors map',
},
validateAuthorsMap,
);
}
type AuthorsParam = {
frontMatter: BlogPostFrontMatter;
authorsMap: AuthorsMap | undefined;
baseUrl: string;
};
function normalizeImageUrl({
imageURL,
baseUrl,
}: {
imageURL: string | undefined;
baseUrl: string;
}) {
return imageURL?.startsWith('/')
? normalizeUrl([baseUrl, imageURL])
: imageURL;
}
// Legacy v1/early-v2 front matter fields
// We may want to deprecate those in favor of using only frontMatter.authors
function getFrontMatterAuthorLegacy({
baseUrl,
frontMatter,
}: {
baseUrl: string;
frontMatter: BlogPostFrontMatter;
}): Author | undefined {
const name = frontMatter.author;
const title = frontMatter.author_title ?? frontMatter.authorTitle;
const url = frontMatter.author_url ?? frontMatter.authorURL;
const imageURL = normalizeImageUrl({
imageURL: frontMatter.author_image_url ?? frontMatter.authorImageURL,
baseUrl,
});
if (name || title || url || imageURL) {
return {
name,
title,
url,
imageURL,
};
}
return undefined;
}
function normalizeFrontMatterAuthors(
frontMatterAuthors: BlogPostFrontMatterAuthors = [],
): BlogPostFrontMatterAuthor[] {
function normalizeAuthor(
authorInput: string | Author,
): BlogPostFrontMatterAuthor {
if (typeof authorInput === 'string') {
// Technically, we could allow users to provide an author's name here, but
// we only support keys, otherwise, a typo in a key would fallback to
// becoming a name and may end up unnoticed
return {key: authorInput};
}
return authorInput;
}
return Array.isArray(frontMatterAuthors)
? frontMatterAuthors.map(normalizeAuthor)
: [normalizeAuthor(frontMatterAuthors)];
}
function getFrontMatterAuthors(params: AuthorsParam): Author[] {
const {authorsMap} = params;
const frontMatterAuthors = normalizeFrontMatterAuthors(
params.frontMatter.authors,
);
function getAuthorsMapAuthor(key: string | undefined): Author | undefined {
if (key) {
if (!authorsMap || Object.keys(authorsMap).length === 0) {
throw new Error(`Can't reference blog post authors by a key (such as '${key}') because no authors map file could be loaded.
Please double-check your blog plugin config (in particular 'authorsMapPath'), ensure the file exists at the configured path, is not empty, and is valid!`);
}
const author = authorsMap[key];
if (!author) {
throw Error(`Blog author with key "${key}" not found in the authors map file.
Valid author keys are:
${Object.keys(authorsMap)
.map((validKey) => `- ${validKey}`)
.join('\n')}`);
}
return author;
}
return undefined;
}
function toAuthor(frontMatterAuthor: BlogPostFrontMatterAuthor): Author {
return {
// Author def from authorsMap can be locally overridden by front matter
...getAuthorsMapAuthor(frontMatterAuthor.key),
...frontMatterAuthor,
};
}
return frontMatterAuthors.map(toAuthor);
}
function fixAuthorImageBaseURL(
authors: Author[],
{baseUrl}: {baseUrl: string},
) {
return authors.map((author) => ({
...author,
imageURL: normalizeImageUrl({imageURL: author.imageURL, baseUrl}),
}));
}
export function getBlogPostAuthors(params: AuthorsParam): Author[] {
const authorLegacy = getFrontMatterAuthorLegacy(params);
const authors = getFrontMatterAuthors(params);
const updatedAuthors = fixAuthorImageBaseURL(authors, params);
if (authorLegacy) {
// Technically, we could allow mixing legacy/authors front matter, but do we
// really want to?
if (updatedAuthors.length > 0) {
throw new Error(
`To declare blog post authors, use the 'authors' front matter in priority.
Don't mix 'authors' with other existing 'author_*' front matter. Choose one or the other, not both at the same time.`,
);
}
return [authorLegacy];
}
return updatedAuthors;
}

View File

@@ -0,0 +1,445 @@
/**
* 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 fs from 'fs-extra';
import path from 'path';
import _ from 'lodash';
import logger from '@docusaurus/logger';
import readingTime from 'reading-time';
import {
parseMarkdownFile,
normalizeUrl,
aliasedSitePath,
getEditUrl,
getFolderContainingFile,
posixPath,
replaceMarkdownLinks,
Globby,
normalizeFrontMatterTags,
groupTaggedItems,
getTagVisibility,
getFileCommitDate,
getContentPathList,
isUnlisted,
isDraft,
} from '@docusaurus/utils';
import {validateBlogPostFrontMatter} from './frontMatter';
import {type AuthorsMap, getAuthorsMap, getBlogPostAuthors} from './authors';
import type {LoadContext, ParseFrontMatter} from '@docusaurus/types';
import type {
PluginOptions,
ReadingTimeFunction,
BlogPost,
BlogTags,
BlogPaginated,
} from '@docusaurus/plugin-content-blog';
import type {BlogContentPaths, BlogMarkdownLoaderOptions} from './types';
export function truncate(fileString: string, truncateMarker: RegExp): string {
return fileString.split(truncateMarker, 1).shift()!;
}
export function getSourceToPermalink(blogPosts: BlogPost[]): {
[aliasedPath: string]: string;
} {
return Object.fromEntries(
blogPosts.map(({metadata: {source, permalink}}) => [source, permalink]),
);
}
export function paginateBlogPosts({
blogPosts,
basePageUrl,
blogTitle,
blogDescription,
postsPerPageOption,
}: {
blogPosts: BlogPost[];
basePageUrl: string;
blogTitle: string;
blogDescription: string;
postsPerPageOption: number | 'ALL';
}): BlogPaginated[] {
const totalCount = blogPosts.length;
const postsPerPage =
postsPerPageOption === 'ALL' ? totalCount : postsPerPageOption;
const numberOfPages = Math.ceil(totalCount / postsPerPage);
const pages: BlogPaginated[] = [];
function permalink(page: number) {
return page > 0
? normalizeUrl([basePageUrl, `page/${page + 1}`])
: basePageUrl;
}
for (let page = 0; page < numberOfPages; page += 1) {
pages.push({
items: blogPosts
.slice(page * postsPerPage, (page + 1) * postsPerPage)
.map((item) => item.id),
metadata: {
permalink: permalink(page),
page: page + 1,
postsPerPage,
totalPages: numberOfPages,
totalCount,
previousPage: page !== 0 ? permalink(page - 1) : undefined,
nextPage: page < numberOfPages - 1 ? permalink(page + 1) : undefined,
blogDescription,
blogTitle,
},
});
}
return pages;
}
export function shouldBeListed(blogPost: BlogPost): boolean {
return !blogPost.metadata.unlisted;
}
export function getBlogTags({
blogPosts,
...params
}: {
blogPosts: BlogPost[];
blogTitle: string;
blogDescription: string;
postsPerPageOption: number | 'ALL';
}): BlogTags {
const groups = groupTaggedItems(
blogPosts,
(blogPost) => blogPost.metadata.tags,
);
return _.mapValues(groups, ({tag, items: tagBlogPosts}) => {
const tagVisibility = getTagVisibility({
items: tagBlogPosts,
isUnlisted: (item) => item.metadata.unlisted,
});
return {
label: tag.label,
items: tagVisibility.listedItems.map((item) => item.id),
permalink: tag.permalink,
pages: paginateBlogPosts({
blogPosts: tagVisibility.listedItems,
basePageUrl: tag.permalink,
...params,
}),
unlisted: tagVisibility.unlisted,
};
});
}
const DATE_FILENAME_REGEX =
/^(?<folder>.*)(?<date>\d{4}[-/]\d{1,2}[-/]\d{1,2})[-/]?(?<text>.*?)(?:\/index)?.mdx?$/;
type ParsedBlogFileName = {
date: Date | undefined;
text: string;
slug: string;
};
export function parseBlogFileName(
blogSourceRelative: string,
): ParsedBlogFileName {
const dateFilenameMatch = blogSourceRelative.match(DATE_FILENAME_REGEX);
if (dateFilenameMatch) {
const {folder, text, date: dateString} = dateFilenameMatch.groups!;
// Always treat dates as UTC by adding the `Z`
const date = new Date(`${dateString!}Z`);
const slugDate = dateString!.replace(/-/g, '/');
const slug = `/${slugDate}/${folder!}${text!}`;
return {date, text: text!, slug};
}
const text = blogSourceRelative.replace(/(?:\/index)?\.mdx?$/, '');
const slug = `/${text}`;
return {date: undefined, text, slug};
}
function formatBlogPostDate(
locale: string,
date: Date,
calendar: string,
): string {
try {
return new Intl.DateTimeFormat(locale, {
day: 'numeric',
month: 'long',
year: 'numeric',
timeZone: 'UTC',
calendar,
}).format(date);
} catch (err) {
logger.error`Can't format blog post date "${String(date)}"`;
throw err;
}
}
async function parseBlogPostMarkdownFile({
filePath,
parseFrontMatter,
}: {
filePath: string;
parseFrontMatter: ParseFrontMatter;
}) {
const fileContent = await fs.readFile(filePath, 'utf-8');
try {
const result = await parseMarkdownFile({
filePath,
fileContent,
parseFrontMatter,
removeContentTitle: true,
});
return {
...result,
frontMatter: validateBlogPostFrontMatter(result.frontMatter),
};
} catch (err) {
logger.error`Error while parsing blog post file path=${filePath}.`;
throw err;
}
}
const defaultReadingTime: ReadingTimeFunction = ({content, options}) =>
readingTime(content, options).minutes;
async function processBlogSourceFile(
blogSourceRelative: string,
contentPaths: BlogContentPaths,
context: LoadContext,
options: PluginOptions,
authorsMap?: AuthorsMap,
): Promise<BlogPost | undefined> {
const {
siteConfig: {
baseUrl,
markdown: {parseFrontMatter},
},
siteDir,
i18n,
} = context;
const {
routeBasePath,
tagsBasePath: tagsRouteBasePath,
truncateMarker,
showReadingTime,
editUrl,
} = options;
// Lookup in localized folder in priority
const blogDirPath = await getFolderContainingFile(
getContentPathList(contentPaths),
blogSourceRelative,
);
const blogSourceAbsolute = path.join(blogDirPath, blogSourceRelative);
const {frontMatter, content, contentTitle, excerpt} =
await parseBlogPostMarkdownFile({
filePath: blogSourceAbsolute,
parseFrontMatter,
});
const aliasedSource = aliasedSitePath(blogSourceAbsolute, siteDir);
const draft = isDraft({frontMatter});
const unlisted = isUnlisted({frontMatter});
if (draft) {
return undefined;
}
if (frontMatter.id) {
logger.warn`name=${'id'} header option is deprecated in path=${blogSourceRelative} file. Please use name=${'slug'} option instead.`;
}
const parsedBlogFileName = parseBlogFileName(blogSourceRelative);
async function getDate(): Promise<Date> {
// Prefer user-defined date.
if (frontMatter.date) {
if (typeof frontMatter.date === 'string') {
// Always treat dates as UTC by adding the `Z`
return new Date(`${frontMatter.date}Z`);
}
// YAML only converts YYYY-MM-DD to dates and leaves others as strings.
return frontMatter.date;
} else if (parsedBlogFileName.date) {
return parsedBlogFileName.date;
}
try {
const result = getFileCommitDate(blogSourceAbsolute, {
age: 'oldest',
includeAuthor: false,
});
return result.date;
} catch (err) {
logger.warn(err);
return (await fs.stat(blogSourceAbsolute)).birthtime;
}
}
const date = await getDate();
const formattedDate = formatBlogPostDate(
i18n.currentLocale,
date,
i18n.localeConfigs[i18n.currentLocale]!.calendar,
);
const title = frontMatter.title ?? contentTitle ?? parsedBlogFileName.text;
const description = frontMatter.description ?? excerpt ?? '';
const slug = frontMatter.slug ?? parsedBlogFileName.slug;
const permalink = normalizeUrl([baseUrl, routeBasePath, slug]);
function getBlogEditUrl() {
const blogPathRelative = path.relative(
blogDirPath,
path.resolve(blogSourceAbsolute),
);
if (typeof editUrl === 'function') {
return editUrl({
blogDirPath: posixPath(path.relative(siteDir, blogDirPath)),
blogPath: posixPath(blogPathRelative),
permalink,
locale: i18n.currentLocale,
});
} else if (typeof editUrl === 'string') {
const isLocalized = blogDirPath === contentPaths.contentPathLocalized;
const fileContentPath =
isLocalized && options.editLocalizedFiles
? contentPaths.contentPathLocalized
: contentPaths.contentPath;
const contentPathEditUrl = normalizeUrl([
editUrl,
posixPath(path.relative(siteDir, fileContentPath)),
]);
return getEditUrl(blogPathRelative, contentPathEditUrl);
}
return undefined;
}
const tagsBasePath = normalizeUrl([
baseUrl,
routeBasePath,
tagsRouteBasePath,
]);
const authors = getBlogPostAuthors({authorsMap, frontMatter, baseUrl});
return {
id: slug,
metadata: {
permalink,
editUrl: getBlogEditUrl(),
source: aliasedSource,
title,
description,
date,
formattedDate,
tags: normalizeFrontMatterTags(tagsBasePath, frontMatter.tags),
readingTime: showReadingTime
? options.readingTime({
content,
frontMatter,
defaultReadingTime,
})
: undefined,
hasTruncateMarker: truncateMarker.test(content),
authors,
frontMatter,
unlisted,
},
content,
};
}
export async function generateBlogPosts(
contentPaths: BlogContentPaths,
context: LoadContext,
options: PluginOptions,
): Promise<BlogPost[]> {
const {include, exclude} = options;
if (!(await fs.pathExists(contentPaths.contentPath))) {
return [];
}
const blogSourceFiles = await Globby(include, {
cwd: contentPaths.contentPath,
ignore: exclude,
});
const authorsMap = await getAuthorsMap({
contentPaths,
authorsMapPath: options.authorsMapPath,
});
async function doProcessBlogSourceFile(blogSourceFile: string) {
try {
return await processBlogSourceFile(
blogSourceFile,
contentPaths,
context,
options,
authorsMap,
);
} catch (err) {
throw new Error(
`Processing of blog source file path=${blogSourceFile} failed.`,
{cause: err as Error},
);
}
}
const blogPosts = (
await Promise.all(blogSourceFiles.map(doProcessBlogSourceFile))
).filter(Boolean) as BlogPost[];
blogPosts.sort(
(a, b) => b.metadata.date.getTime() - a.metadata.date.getTime(),
);
if (options.sortPosts === 'ascending') {
return blogPosts.reverse();
}
return blogPosts;
}
export type LinkifyParams = {
filePath: string;
fileString: string;
} & Pick<
BlogMarkdownLoaderOptions,
'sourceToPermalink' | 'siteDir' | 'contentPaths' | 'onBrokenMarkdownLink'
>;
export function linkify({
filePath,
contentPaths,
fileString,
siteDir,
sourceToPermalink,
onBrokenMarkdownLink,
}: LinkifyParams): string {
const {newContent, brokenMarkdownLinks} = replaceMarkdownLinks({
siteDir,
fileString,
filePath,
contentPaths,
sourceToPermalink,
});
brokenMarkdownLinks.forEach((l) => onBrokenMarkdownLink(l));
return newContent;
}

View File

@@ -0,0 +1,241 @@
/**
* 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 path from 'path';
import fs from 'fs-extra';
import logger from '@docusaurus/logger';
import {Feed, type Author as FeedAuthor} from 'feed';
import * as srcset from 'srcset';
import {normalizeUrl, readOutputHTMLFile} from '@docusaurus/utils';
import {blogPostContainerID} from '@docusaurus/utils-common';
import {load as cheerioLoad} from 'cheerio';
import type {DocusaurusConfig} from '@docusaurus/types';
import type {
FeedType,
PluginOptions,
Author,
BlogPost,
BlogFeedItem,
} from '@docusaurus/plugin-content-blog';
async function generateBlogFeed({
blogPosts,
options,
siteConfig,
outDir,
locale,
}: {
blogPosts: BlogPost[];
options: PluginOptions;
siteConfig: DocusaurusConfig;
outDir: string;
locale: string;
}): Promise<Feed | null> {
if (!blogPosts.length) {
return null;
}
const {feedOptions, routeBasePath} = options;
const {url: siteUrl, baseUrl, title, favicon} = siteConfig;
const blogBaseUrl = normalizeUrl([siteUrl, baseUrl, routeBasePath]);
const blogPostsForFeed =
feedOptions.limit === false || feedOptions.limit === null
? blogPosts
: blogPosts.slice(0, feedOptions.limit);
const updated = blogPostsForFeed[0]?.metadata.date;
const feed = new Feed({
id: blogBaseUrl,
title: feedOptions.title ?? `${title} Blog`,
updated,
language: feedOptions.language ?? locale,
link: blogBaseUrl,
description: feedOptions.description ?? `${siteConfig.title} Blog`,
favicon: favicon ? normalizeUrl([siteUrl, baseUrl, favicon]) : undefined,
copyright: feedOptions.copyright,
});
const createFeedItems =
options.feedOptions.createFeedItems ?? defaultCreateFeedItems;
const feedItems = await createFeedItems({
blogPosts: blogPostsForFeed,
siteConfig,
outDir,
defaultCreateFeedItems,
});
feedItems.forEach(feed.addItem);
return feed;
}
async function defaultCreateFeedItems({
blogPosts,
siteConfig,
outDir,
}: {
blogPosts: BlogPost[];
siteConfig: DocusaurusConfig;
outDir: string;
}): Promise<BlogFeedItem[]> {
const {url: siteUrl} = siteConfig;
function toFeedAuthor(author: Author): FeedAuthor {
return {name: author.name, link: author.url, email: author.email};
}
return Promise.all(
blogPosts.map(async (post) => {
const {
metadata: {
title: metadataTitle,
permalink,
date,
description,
authors,
tags,
},
} = post;
const content = await readOutputHTMLFile(
permalink.replace(siteConfig.baseUrl, ''),
outDir,
siteConfig.trailingSlash,
);
const $ = cheerioLoad(content);
const blogPostAbsoluteUrl = normalizeUrl([siteUrl, permalink]);
const toAbsoluteUrl = (src: string) =>
String(new URL(src, blogPostAbsoluteUrl));
// Make links and image urls absolute
// See https://github.com/facebook/docusaurus/issues/9136
$(`div#${blogPostContainerID} a, div#${blogPostContainerID} img`).each(
(_, elm) => {
if (elm.tagName === 'a') {
const {href} = elm.attribs;
if (href) {
elm.attribs.href = toAbsoluteUrl(href);
}
} else if (elm.tagName === 'img') {
const {src, srcset: srcsetAttr} = elm.attribs;
if (src) {
elm.attribs.src = toAbsoluteUrl(src);
}
if (srcsetAttr) {
elm.attribs.srcset = srcset.stringify(
srcset.parse(srcsetAttr).map((props) => ({
...props,
url: toAbsoluteUrl(props.url),
})),
);
}
}
},
);
const feedItem: BlogFeedItem = {
title: metadataTitle,
id: blogPostAbsoluteUrl,
link: blogPostAbsoluteUrl,
date,
description,
// Atom feed demands the "term", while other feeds use "name"
category: tags.map((tag) => ({name: tag.label, term: tag.label})),
content: $(`#${blogPostContainerID}`).html()!,
};
// json1() method takes the first item of authors array
// it causes an error when authors array is empty
const feedItemAuthors = authors.map(toFeedAuthor);
if (feedItemAuthors.length > 0) {
feedItem.author = feedItemAuthors;
}
return feedItem;
}),
);
}
async function createBlogFeedFile({
feed,
feedType,
generatePath,
}: {
feed: Feed;
feedType: FeedType;
generatePath: string;
}) {
const [feedContent, feedPath] = (() => {
switch (feedType) {
case 'rss':
return [feed.rss2(), 'rss.xml'];
case 'json':
return [feed.json1(), 'feed.json'];
case 'atom':
return [feed.atom1(), 'atom.xml'];
default:
throw new Error(`Feed type ${feedType} not supported.`);
}
})();
try {
await fs.outputFile(path.join(generatePath, feedPath), feedContent);
} catch (err) {
logger.error(`Generating ${feedType} feed failed.`);
throw err;
}
}
function shouldBeInFeed(blogPost: BlogPost): boolean {
const excluded =
blogPost.metadata.frontMatter.draft ||
blogPost.metadata.frontMatter.unlisted;
return !excluded;
}
export async function createBlogFeedFiles({
blogPosts: allBlogPosts,
options,
siteConfig,
outDir,
locale,
}: {
blogPosts: BlogPost[];
options: PluginOptions;
siteConfig: DocusaurusConfig;
outDir: string;
locale: string;
}): Promise<void> {
const blogPosts = allBlogPosts.filter(shouldBeInFeed);
const feed = await generateBlogFeed({
blogPosts,
options,
siteConfig,
outDir,
locale,
});
const feedTypes = options.feedOptions.type;
if (!feed || !feedTypes) {
return;
}
await Promise.all(
feedTypes.map((feedType) =>
createBlogFeedFile({
feed,
feedType,
generatePath: path.join(outDir, options.routeBasePath),
}),
),
);
}

View File

@@ -0,0 +1,83 @@
/**
* 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 {
JoiFrontMatter as Joi, // Custom instance for front matter
URISchema,
validateFrontMatter,
FrontMatterTagsSchema,
FrontMatterTOCHeadingLevels,
ContentVisibilitySchema,
} from '@docusaurus/utils-validation';
import type {BlogPostFrontMatter} from '@docusaurus/plugin-content-blog';
const BlogPostFrontMatterAuthorSchema = Joi.object({
key: Joi.string(),
name: Joi.string(),
title: Joi.string(),
url: URISchema,
imageURL: Joi.string(),
})
.or('key', 'name', 'imageURL')
.rename('image_url', 'imageURL', {alias: true});
const FrontMatterAuthorErrorMessage =
'{{#label}} does not look like a valid blog post author. Please use an author key or an author object (with a key and/or name).';
const BlogFrontMatterSchema = Joi.object<BlogPostFrontMatter>({
id: Joi.string(),
title: Joi.string().allow(''),
description: Joi.string().allow(''),
tags: FrontMatterTagsSchema,
date: Joi.date().raw(),
// New multi-authors front matter:
authors: Joi.alternatives()
.try(
Joi.string(),
BlogPostFrontMatterAuthorSchema,
Joi.array()
.items(Joi.string(), BlogPostFrontMatterAuthorSchema)
.messages({
'array.sparse': FrontMatterAuthorErrorMessage,
'array.includes': FrontMatterAuthorErrorMessage,
}),
)
.messages({
'alternatives.match': FrontMatterAuthorErrorMessage,
}),
// Legacy author front matter
author: Joi.string(),
author_title: Joi.string(),
author_url: URISchema,
author_image_url: URISchema,
// TODO enable deprecation warnings later
authorURL: URISchema,
// .warning('deprecate.error', { alternative: '"author_url"'}),
authorTitle: Joi.string(),
// .warning('deprecate.error', { alternative: '"author_title"'}),
authorImageURL: URISchema,
// .warning('deprecate.error', { alternative: '"author_image_url"'}),
slug: Joi.string(),
image: URISchema,
keywords: Joi.array().items(Joi.string().required()),
hide_table_of_contents: Joi.boolean(),
...FrontMatterTOCHeadingLevels,
})
.messages({
'deprecate.error':
'{#label} blog frontMatter field is deprecated. Please use {#alternative} instead.',
})
.concat(ContentVisibilitySchema);
export function validateBlogPostFrontMatter(frontMatter: {
[key: string]: unknown;
}): BlogPostFrontMatter {
return validateFrontMatter(frontMatter, BlogFrontMatterSchema);
}

View File

@@ -0,0 +1,536 @@
/**
* 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 path from 'path';
import logger from '@docusaurus/logger';
import {
normalizeUrl,
docuHash,
aliasedSitePath,
getPluginI18nPath,
posixPath,
addTrailingPathSeparator,
createAbsoluteFilePathMatcher,
getContentPathList,
getDataFilePath,
DEFAULT_PLUGIN_ID,
} from '@docusaurus/utils';
import {
generateBlogPosts,
getSourceToPermalink,
getBlogTags,
paginateBlogPosts,
shouldBeListed,
} from './blogUtils';
import footnoteIDFixer from './remark/footnoteIDFixer';
import {translateContent, getTranslationFiles} from './translations';
import {createBlogFeedFiles} from './feed';
import {toTagProp, toTagsProp} from './props';
import type {BlogContentPaths, BlogMarkdownLoaderOptions} from './types';
import type {LoadContext, Plugin, HtmlTags} from '@docusaurus/types';
import type {
PluginOptions,
BlogPostFrontMatter,
BlogPostMetadata,
Assets,
BlogTag,
BlogTags,
BlogContent,
BlogPaginated,
} from '@docusaurus/plugin-content-blog';
export default async function pluginContentBlog(
context: LoadContext,
options: PluginOptions,
): Promise<Plugin<BlogContent>> {
const {
siteDir,
siteConfig,
generatedFilesDir,
localizationDir,
i18n: {currentLocale},
} = context;
const {onBrokenMarkdownLinks, baseUrl} = siteConfig;
const contentPaths: BlogContentPaths = {
contentPath: path.resolve(siteDir, options.path),
contentPathLocalized: getPluginI18nPath({
localizationDir,
pluginName: 'docusaurus-plugin-content-blog',
pluginId: options.id,
}),
};
const pluginId = options.id ?? DEFAULT_PLUGIN_ID;
const pluginDataDirRoot = path.join(
generatedFilesDir,
'docusaurus-plugin-content-blog',
);
const dataDir = path.join(pluginDataDirRoot, pluginId);
const aliasedSource = (source: string) =>
`~blog/${posixPath(path.relative(pluginDataDirRoot, source))}`;
const authorsMapFilePath = await getDataFilePath({
filePath: options.authorsMapPath,
contentPaths,
});
return {
name: 'docusaurus-plugin-content-blog',
getPathsToWatch() {
const {include} = options;
const contentMarkdownGlobs = getContentPathList(contentPaths).flatMap(
(contentPath) => include.map((pattern) => `${contentPath}/${pattern}`),
);
return [authorsMapFilePath, ...contentMarkdownGlobs].filter(
Boolean,
) as string[];
},
getTranslationFiles() {
return getTranslationFiles(options);
},
// Fetches blog contents and returns metadata for the necessary routes.
async loadContent() {
const {
postsPerPage: postsPerPageOption,
routeBasePath,
tagsBasePath,
blogDescription,
blogTitle,
blogSidebarTitle,
} = options;
const baseBlogUrl = normalizeUrl([baseUrl, routeBasePath]);
const blogTagsListPath = normalizeUrl([baseBlogUrl, tagsBasePath]);
const blogPosts = await generateBlogPosts(contentPaths, context, options);
const listedBlogPosts = blogPosts.filter(shouldBeListed);
if (!blogPosts.length) {
return {
blogSidebarTitle,
blogPosts: [],
blogListPaginated: [],
blogTags: {},
blogTagsListPath,
blogTagsPaginated: [],
};
}
// Colocate next and prev metadata.
listedBlogPosts.forEach((blogPost, index) => {
const prevItem = index > 0 ? listedBlogPosts[index - 1] : null;
if (prevItem) {
blogPost.metadata.prevItem = {
title: prevItem.metadata.title,
permalink: prevItem.metadata.permalink,
};
}
const nextItem =
index < listedBlogPosts.length - 1
? listedBlogPosts[index + 1]
: null;
if (nextItem) {
blogPost.metadata.nextItem = {
title: nextItem.metadata.title,
permalink: nextItem.metadata.permalink,
};
}
});
const blogListPaginated: BlogPaginated[] = paginateBlogPosts({
blogPosts: listedBlogPosts,
blogTitle,
blogDescription,
postsPerPageOption,
basePageUrl: baseBlogUrl,
});
const blogTags: BlogTags = getBlogTags({
blogPosts,
postsPerPageOption,
blogDescription,
blogTitle,
});
return {
blogSidebarTitle,
blogPosts,
blogListPaginated,
blogTags,
blogTagsListPath,
};
},
async contentLoaded({content: blogContents, actions}) {
const {
blogListComponent,
blogPostComponent,
blogTagsListComponent,
blogTagsPostsComponent,
blogArchiveComponent,
routeBasePath,
archiveBasePath,
} = options;
const {addRoute, createData} = actions;
const {
blogSidebarTitle,
blogPosts,
blogListPaginated,
blogTags,
blogTagsListPath,
} = blogContents;
const listedBlogPosts = blogPosts.filter(shouldBeListed);
const blogItemsToMetadata: {[postId: string]: BlogPostMetadata} = {};
const sidebarBlogPosts =
options.blogSidebarCount === 'ALL'
? blogPosts
: blogPosts.slice(0, options.blogSidebarCount);
function blogPostItemsModule(items: string[]) {
return items.map((postId) => {
const blogPostMetadata = blogItemsToMetadata[postId]!;
return {
content: {
__import: true,
path: blogPostMetadata.source,
query: {
truncated: true,
},
},
};
});
}
if (archiveBasePath && listedBlogPosts.length) {
const archiveUrl = normalizeUrl([
baseUrl,
routeBasePath,
archiveBasePath,
]);
// Create a blog archive route
const archiveProp = await createData(
`${docuHash(archiveUrl)}.json`,
JSON.stringify({blogPosts: listedBlogPosts}, null, 2),
);
addRoute({
path: archiveUrl,
component: blogArchiveComponent,
exact: true,
modules: {
archive: aliasedSource(archiveProp),
},
});
}
// This prop is useful to provide the blog list sidebar
const sidebarProp = await createData(
// Note that this created data path must be in sync with
// metadataPath provided to mdx-loader.
`blog-post-list-prop-${pluginId}.json`,
JSON.stringify(
{
title: blogSidebarTitle,
items: sidebarBlogPosts.map((blogPost) => ({
title: blogPost.metadata.title,
permalink: blogPost.metadata.permalink,
unlisted: blogPost.metadata.unlisted,
})),
},
null,
2,
),
);
// Create routes for blog entries.
await Promise.all(
blogPosts.map(async (blogPost) => {
const {id, metadata} = blogPost;
await createData(
// Note that this created data path must be in sync with
// metadataPath provided to mdx-loader.
`${docuHash(metadata.source)}.json`,
JSON.stringify(metadata, null, 2),
);
addRoute({
path: metadata.permalink,
component: blogPostComponent,
exact: true,
modules: {
sidebar: aliasedSource(sidebarProp),
content: metadata.source,
},
});
blogItemsToMetadata[id] = metadata;
}),
);
// Create routes for blog's paginated list entries.
await Promise.all(
blogListPaginated.map(async (listPage) => {
const {metadata, items} = listPage;
const {permalink} = metadata;
const pageMetadataPath = await createData(
`${docuHash(permalink)}.json`,
JSON.stringify(metadata, null, 2),
);
addRoute({
path: permalink,
component: blogListComponent,
exact: true,
modules: {
sidebar: aliasedSource(sidebarProp),
items: blogPostItemsModule(items),
metadata: aliasedSource(pageMetadataPath),
},
});
}),
);
// Tags. This is the last part so we early-return if there are no tags.
if (Object.keys(blogTags).length === 0) {
return;
}
async function createTagsListPage() {
const tagsPropPath = await createData(
`${docuHash(`${blogTagsListPath}-tags`)}.json`,
JSON.stringify(toTagsProp({blogTags}), null, 2),
);
addRoute({
path: blogTagsListPath,
component: blogTagsListComponent,
exact: true,
modules: {
sidebar: aliasedSource(sidebarProp),
tags: aliasedSource(tagsPropPath),
},
});
}
async function createTagPostsListPage(tag: BlogTag): Promise<void> {
await Promise.all(
tag.pages.map(async (blogPaginated) => {
const {metadata, items} = blogPaginated;
const tagPropPath = await createData(
`${docuHash(metadata.permalink)}.json`,
JSON.stringify(toTagProp({tag, blogTagsListPath}), null, 2),
);
const listMetadataPath = await createData(
`${docuHash(metadata.permalink)}-list.json`,
JSON.stringify(metadata, null, 2),
);
addRoute({
path: metadata.permalink,
component: blogTagsPostsComponent,
exact: true,
modules: {
sidebar: aliasedSource(sidebarProp),
items: blogPostItemsModule(items),
tag: aliasedSource(tagPropPath),
listMetadata: aliasedSource(listMetadataPath),
},
});
}),
);
}
await createTagsListPage();
await Promise.all(Object.values(blogTags).map(createTagPostsListPage));
},
translateContent({content, translationFiles}) {
return translateContent(content, translationFiles);
},
configureWebpack(_config, isServer, utils, content) {
const {
admonitions,
rehypePlugins,
remarkPlugins,
truncateMarker,
beforeDefaultRemarkPlugins,
beforeDefaultRehypePlugins,
} = options;
const markdownLoaderOptions: BlogMarkdownLoaderOptions = {
siteDir,
contentPaths,
truncateMarker,
sourceToPermalink: getSourceToPermalink(content.blogPosts),
onBrokenMarkdownLink: (brokenMarkdownLink) => {
if (onBrokenMarkdownLinks === 'ignore') {
return;
}
logger.report(
onBrokenMarkdownLinks,
)`Blog markdown link couldn't be resolved: (url=${brokenMarkdownLink.link}) in path=${brokenMarkdownLink.filePath}`;
},
};
const contentDirs = getContentPathList(contentPaths);
return {
resolve: {
alias: {
'~blog': pluginDataDirRoot,
},
},
module: {
rules: [
{
test: /\.mdx?$/i,
include: contentDirs
// Trailing slash is important, see https://github.com/facebook/docusaurus/pull/3970
.map(addTrailingPathSeparator),
use: [
{
loader: require.resolve('@docusaurus/mdx-loader'),
options: {
admonitions,
remarkPlugins,
rehypePlugins,
beforeDefaultRemarkPlugins: [
footnoteIDFixer,
...beforeDefaultRemarkPlugins,
],
beforeDefaultRehypePlugins,
staticDirs: siteConfig.staticDirectories.map((dir) =>
path.resolve(siteDir, dir),
),
siteDir,
isMDXPartial: createAbsoluteFilePathMatcher(
options.exclude,
contentDirs,
),
metadataPath: (mdxPath: string) => {
// Note that metadataPath must be the same/in-sync as
// the path from createData for each MDX.
const aliasedPath = aliasedSitePath(mdxPath, siteDir);
return path.join(
dataDir,
`${docuHash(aliasedPath)}.json`,
);
},
// For blog posts a title in markdown is always removed
// Blog posts title are rendered separately
removeContentTitle: true,
// Assets allow to convert some relative images paths to
// require() calls
createAssets: ({
frontMatter,
metadata,
}: {
frontMatter: BlogPostFrontMatter;
metadata: BlogPostMetadata;
}): Assets => ({
image: frontMatter.image,
authorsImageUrls: metadata.authors.map(
(author) => author.imageURL,
),
}),
markdownConfig: siteConfig.markdown,
},
},
{
loader: path.resolve(__dirname, './markdownLoader.js'),
options: markdownLoaderOptions,
},
].filter(Boolean),
},
],
},
};
},
async postBuild({outDir, content}) {
if (!options.feedOptions.type) {
return;
}
const {blogPosts} = content;
if (!blogPosts.length) {
return;
}
await createBlogFeedFiles({
blogPosts,
options,
outDir,
siteConfig,
locale: currentLocale,
});
},
injectHtmlTags({content}) {
if (!content.blogPosts.length || !options.feedOptions.type) {
return {};
}
const feedTypes = options.feedOptions.type;
const feedTitle = options.feedOptions.title ?? context.siteConfig.title;
const feedsConfig = {
rss: {
type: 'application/rss+xml',
path: 'rss.xml',
title: `${feedTitle} RSS Feed`,
},
atom: {
type: 'application/atom+xml',
path: 'atom.xml',
title: `${feedTitle} Atom Feed`,
},
json: {
type: 'application/json',
path: 'feed.json',
title: `${feedTitle} JSON Feed`,
},
};
const headTags: HtmlTags = [];
feedTypes.forEach((feedType) => {
const {
type,
path: feedConfigPath,
title: feedConfigTitle,
} = feedsConfig[feedType];
headTags.push({
tagName: 'link',
attributes: {
rel: 'alternate',
type,
href: normalizeUrl([
baseUrl,
options.routeBasePath,
feedConfigPath,
]),
title: feedConfigTitle,
},
});
});
return {
headTags,
};
},
};
}
export {validateOptions} from './options';

View File

@@ -0,0 +1,38 @@
/**
* 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 {truncate, linkify} from './blogUtils';
import type {BlogMarkdownLoaderOptions} from './types';
import type {LoaderContext} from 'webpack';
export default function markdownLoader(
this: LoaderContext<BlogMarkdownLoaderOptions>,
source: string,
): void {
const filePath = this.resourcePath;
const fileString = source;
const callback = this.async();
const markdownLoaderOptions = this.getOptions();
// Linkify blog posts
let finalContent = linkify({
fileString,
filePath,
...markdownLoaderOptions,
});
// Truncate content if requested (e.g: file.md?truncated=true).
const truncated: boolean | undefined = this.resourceQuery
? !!new URLSearchParams(this.resourceQuery.slice(1)).get('truncated')
: undefined;
if (truncated) {
finalContent = truncate(finalContent, markdownLoaderOptions.truncateMarker);
}
return callback(null, finalContent);
}

View File

@@ -0,0 +1,143 @@
/**
* 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 {
Joi,
RemarkPluginsSchema,
RehypePluginsSchema,
AdmonitionsSchema,
RouteBasePathSchema,
URISchema,
} from '@docusaurus/utils-validation';
import {GlobExcludeDefault} from '@docusaurus/utils';
import type {
PluginOptions,
Options,
FeedType,
} from '@docusaurus/plugin-content-blog';
import type {OptionValidationContext} from '@docusaurus/types';
export const DEFAULT_OPTIONS: PluginOptions = {
feedOptions: {type: ['rss', 'atom'], copyright: '', limit: 20},
beforeDefaultRehypePlugins: [],
beforeDefaultRemarkPlugins: [],
admonitions: true,
truncateMarker: /<!--\s*truncate\s*-->|\{\/\*\s*truncate\s*\*\/\}/,
rehypePlugins: [],
remarkPlugins: [],
showReadingTime: true,
blogTagsPostsComponent: '@theme/BlogTagsPostsPage',
blogTagsListComponent: '@theme/BlogTagsListPage',
blogPostComponent: '@theme/BlogPostPage',
blogListComponent: '@theme/BlogListPage',
blogArchiveComponent: '@theme/BlogArchivePage',
blogDescription: 'Blog',
blogTitle: 'Blog',
blogSidebarCount: 5,
blogSidebarTitle: 'Recent posts',
postsPerPage: 10,
include: ['**/*.{md,mdx}'],
exclude: GlobExcludeDefault,
routeBasePath: 'blog',
tagsBasePath: 'tags',
archiveBasePath: 'archive',
path: 'blog',
editLocalizedFiles: false,
authorsMapPath: 'authors.yml',
readingTime: ({content, defaultReadingTime}) => defaultReadingTime({content}),
sortPosts: 'descending',
};
const PluginOptionSchema = Joi.object<PluginOptions>({
path: Joi.string().default(DEFAULT_OPTIONS.path),
archiveBasePath: Joi.string()
.default(DEFAULT_OPTIONS.archiveBasePath)
.allow(null),
routeBasePath: RouteBasePathSchema.default(DEFAULT_OPTIONS.routeBasePath),
tagsBasePath: Joi.string().default(DEFAULT_OPTIONS.tagsBasePath),
include: Joi.array().items(Joi.string()).default(DEFAULT_OPTIONS.include),
exclude: Joi.array().items(Joi.string()).default(DEFAULT_OPTIONS.exclude),
postsPerPage: Joi.alternatives()
.try(Joi.equal('ALL').required(), Joi.number().integer().min(1).required())
.default(DEFAULT_OPTIONS.postsPerPage),
blogListComponent: Joi.string().default(DEFAULT_OPTIONS.blogListComponent),
blogPostComponent: Joi.string().default(DEFAULT_OPTIONS.blogPostComponent),
blogTagsListComponent: Joi.string().default(
DEFAULT_OPTIONS.blogTagsListComponent,
),
blogTagsPostsComponent: Joi.string().default(
DEFAULT_OPTIONS.blogTagsPostsComponent,
),
blogArchiveComponent: Joi.string().default(
DEFAULT_OPTIONS.blogArchiveComponent,
),
blogTitle: Joi.string().allow('').default(DEFAULT_OPTIONS.blogTitle),
blogDescription: Joi.string()
.allow('')
.default(DEFAULT_OPTIONS.blogDescription),
blogSidebarCount: Joi.alternatives()
.try(Joi.equal('ALL').required(), Joi.number().integer().min(0).required())
.default(DEFAULT_OPTIONS.blogSidebarCount),
blogSidebarTitle: Joi.string().default(DEFAULT_OPTIONS.blogSidebarTitle),
showReadingTime: Joi.bool().default(DEFAULT_OPTIONS.showReadingTime),
remarkPlugins: RemarkPluginsSchema.default(DEFAULT_OPTIONS.remarkPlugins),
rehypePlugins: RehypePluginsSchema.default(DEFAULT_OPTIONS.rehypePlugins),
admonitions: AdmonitionsSchema.default(DEFAULT_OPTIONS.admonitions),
editUrl: Joi.alternatives().try(URISchema, Joi.function()),
editLocalizedFiles: Joi.boolean().default(DEFAULT_OPTIONS.editLocalizedFiles),
truncateMarker: Joi.object().default(DEFAULT_OPTIONS.truncateMarker),
beforeDefaultRemarkPlugins: RemarkPluginsSchema.default(
DEFAULT_OPTIONS.beforeDefaultRemarkPlugins,
),
beforeDefaultRehypePlugins: RehypePluginsSchema.default(
DEFAULT_OPTIONS.beforeDefaultRehypePlugins,
),
feedOptions: Joi.object({
type: Joi.alternatives()
.try(
Joi.array().items(Joi.string().equal('rss', 'atom', 'json')),
Joi.alternatives().conditional(
Joi.string().equal('all', 'rss', 'atom', 'json'),
{
then: Joi.custom((val: FeedType | 'all') =>
val === 'all' ? ['rss', 'atom', 'json'] : [val],
),
},
),
)
.allow(null)
.default(DEFAULT_OPTIONS.feedOptions.type),
title: Joi.string().allow(''),
description: Joi.string().allow(''),
// Only add default value when user actually wants a feed (type is not null)
copyright: Joi.when('type', {
is: Joi.any().valid(null),
then: Joi.string().optional(),
otherwise: Joi.string()
.allow('')
.default(DEFAULT_OPTIONS.feedOptions.copyright),
}),
language: Joi.string(),
createFeedItems: Joi.function(),
limit: Joi.alternatives()
.try(Joi.number(), Joi.valid(null), Joi.valid(false))
.default(DEFAULT_OPTIONS.feedOptions.limit),
}).default(DEFAULT_OPTIONS.feedOptions),
authorsMapPath: Joi.string().default(DEFAULT_OPTIONS.authorsMapPath),
readingTime: Joi.function().default(() => DEFAULT_OPTIONS.readingTime),
sortPosts: Joi.string()
.valid('descending', 'ascending')
.default(DEFAULT_OPTIONS.sortPosts),
}).default(DEFAULT_OPTIONS);
export function validateOptions({
validate,
options,
}: OptionValidationContext<Options | undefined, PluginOptions>): PluginOptions {
const validatedOptions = validate(PluginOptionSchema, options);
return validatedOptions;
}

View File

@@ -0,0 +1,631 @@
/**
* 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.
*/
declare module '@docusaurus/plugin-content-blog' {
import type {LoadedMDXContent} from '@docusaurus/mdx-loader';
import type {MDXOptions} from '@docusaurus/mdx-loader';
import type {FrontMatterTag, Tag} from '@docusaurus/utils';
import type {DocusaurusConfig, Plugin, LoadContext} from '@docusaurus/types';
import type {Item as FeedItem} from 'feed';
import type {Overwrite} from 'utility-types';
export type Assets = {
/**
* If `metadata.yarn workspace website typecheck
4
yarn workspace v1.22.19yarn workspace website typecheck
4
yarn workspace v1.22.19yarn workspace website typecheck
4
yarn workspace v1.22.19image` 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;
/**
* 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)[];
};
export type Author = {
/**
* 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;
/**
* Unknown keys are allowed, so that we can pass custom fields to authors,
* e.g., `twitter`.
*/
[key: string]: unknown;
};
/**
* 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 `/<baseUrl>/<routeBasePath>/` */
slug?: string;
/**
* 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
* plain string.
* @see {@link BlogPostMetadata.date}
*/
date?: Date | string;
/**
* Authors, unnormalized.
* @see {@link BlogPostMetadata.authors}
*/
authors?: BlogPostFrontMatterAuthors;
/**
* 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 v1 legacy */
authorTitle?: string;
/** @deprecated v1 legacy */
authorURL?: string;
/** @deprecated v1 legacy */
authorImageURL?: string;
/** Used in the head meta. Should use `assets.image` in priority. */
image?: string;
/** Used in the head meta. */
keywords?: string[];
/** Hide the right TOC. */
hide_table_of_contents?: boolean;
/**
* Minimum TOC heading level. Must be between 2 and 6 and lower or equal to
* the max value.
*/
toc_min_heading_level?: number;
/** Maximum TOC heading level. Must be between 2 and 6. */
toc_max_heading_level?: number;
};
export type BlogPostFrontMatterAuthor = Author & {
/**
* Will be normalized into the `imageURL` prop.
*/
image_url?: string;
/**
* References an existing author in the authors map.
*/
key?: string;
};
/**
* 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 hasTruncateMarker: 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 & {[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.
*/
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;
/** Allow control over the construction of BlogFeedItems */
createFeedItems?: CreateFeedItemsFn;
/** Limits the feed to the specified number of posts, false|null for all */
limit?: number | false | null;
};
type DefaultCreateFeedItemsParams = {
blogPosts: BlogPost[];
siteConfig: DocusaurusConfig;
outDir: string;
};
type CreateFeedItemsFn = (
params: CreateFeedItemsParams,
) => Promise<BlogFeedItem[]>;
type CreateFeedItemsParams = DefaultCreateFeedItemsParams & {
defaultCreateFeedItems: (
params: DefaultCreateFeedItemsParams,
) => Promise<BlogFeedItem[]>;
};
/**
* 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 & {[key: string]: unknown};
/** 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<Omit<Parameters<ReadingTimeFunction>[0], 'options'>> & {
/**
* The default reading time implementation from ngryman/reading-time.
*/
defaultReadingTime: ReadingTimeFunction;
},
) => number | undefined;
/**
* Plugin options after normalization.
*/
export type PluginOptions = MDXOptions & {
/** 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;
/** 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;
/** 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';
};
/**
* Feed options, as provided by user config. `type` accepts `all` as shortcut
*/
export type UserFeedOptions = Overwrite<
Partial<FeedOptions>,
{
/** 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<PluginOptions>,
{
/** Blog feed. */
feedOptions?: UserFeedOptions;
}
>;
export type BlogSidebarItem = {
title: string;
permalink: string;
unlisted: boolean;
};
export type BlogSidebar = {
title: string;
items: BlogSidebarItem[];
};
export type BlogContent = {
blogSidebarTitle: string;
blogPosts: BlogPost[];
blogListPaginated: BlogPaginated[];
blogTags: BlogTags;
blogTagsListPath: string;
};
export type BlogTags = {
[permalink: string]: BlogTag;
};
export type BlogTag = Tag & {
/** Blog post permalinks. */
items: string[];
pages: BlogPaginated[];
unlisted: boolean;
};
export type BlogPost = {
id: string;
metadata: BlogPostMetadata;
content: string;
};
export type BlogFeedItem = FeedItem;
export type BlogPaginatedMetadata = {
/** Title of the entire blog. */
readonly blogTitle: string;
/** Blog description. */
readonly blogDescription: string;
/** Permalink to the next list page. */
readonly nextPage?: string;
/** Permalink of the current page. */
readonly permalink: string;
/** 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 type BlogPaginated = {
metadata: BlogPaginatedMetadata;
/** Blog post permalinks. */
items: string[];
};
type PropBlogPostMetadata = Overwrite<
BlogPostMetadata,
{
/** The publish date of the post. Serialized from the `Date` object. */
date: string;
}
>;
export type PropBlogPostContent = LoadedMDXContent<
BlogPostFrontMatter,
PropBlogPostMetadata,
Assets
>;
export default function pluginContentBlog(
context: LoadContext,
options: PluginOptions,
): Promise<Plugin<BlogContent>>;
}
declare module '@theme/BlogPostPage' {
import type {
BlogPostFrontMatter,
BlogSidebar,
PropBlogPostContent,
} from '@docusaurus/plugin-content-blog';
export type FrontMatter = BlogPostFrontMatter;
export type Content = PropBlogPostContent;
export interface Props {
/** Blog sidebar. */
readonly sidebar: BlogSidebar;
/** Content of this post as an MDX component, with useful metadata. */
readonly content: Content;
}
export default function BlogPostPage(props: Props): JSX.Element;
}
declare module '@theme/BlogPostPage/Metadata' {
export default function BlogPostPageMetadata(): JSX.Element;
}
declare module '@theme/BlogListPage' {
import type {Content} from '@theme/BlogPostPage';
import type {
BlogSidebar,
BlogPaginatedMetadata,
} from '@docusaurus/plugin-content-blog';
export interface Props {
/** Blog sidebar. */
readonly sidebar: BlogSidebar;
/** Metadata of the current listing page. */
readonly metadata: BlogPaginatedMetadata;
/**
* Array of blog posts included on this page. Every post's metadata is also
* available.
*/
readonly items: readonly {readonly content: Content}[];
}
export default function BlogListPage(props: Props): JSX.Element;
}
declare module '@theme/BlogTagsListPage' {
import type {BlogSidebar} from '@docusaurus/plugin-content-blog';
import type {TagsListItem} from '@docusaurus/utils';
export interface Props {
/** Blog sidebar. */
readonly sidebar: BlogSidebar;
/** All tags declared in this blog. */
readonly tags: TagsListItem[];
}
export default function BlogTagsListPage(props: Props): JSX.Element;
}
declare module '@theme/BlogTagsPostsPage' {
import type {Content} from '@theme/BlogPostPage';
import type {
BlogSidebar,
BlogPaginatedMetadata,
} from '@docusaurus/plugin-content-blog';
import type {TagModule} from '@docusaurus/utils';
export interface Props {
/** Blog sidebar. */
readonly sidebar: BlogSidebar;
/** Metadata of this tag. */
readonly tag: TagModule;
/** Looks exactly the same as the posts list page */
readonly listMetadata: BlogPaginatedMetadata;
/**
* Array of blog posts included on this page. Every post's metadata is also
* available.
*/
readonly items: readonly {readonly content: Content}[];
}
export default function BlogTagsPostsPage(props: Props): JSX.Element;
}
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[];
};
}
export default function BlogArchivePage(props: Props): JSX.Element;
}

View File

@@ -0,0 +1,34 @@
/**
* 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 type {TagsListItem, TagModule} from '@docusaurus/utils';
import type {BlogTag, BlogTags} from '@docusaurus/plugin-content-blog';
export function toTagsProp({blogTags}: {blogTags: BlogTags}): TagsListItem[] {
return Object.values(blogTags)
.filter((tag) => !tag.unlisted)
.map((tag) => ({
label: tag.label,
permalink: tag.permalink,
count: tag.items.length,
}));
}
export function toTagProp({
blogTagsListPath,
tag,
}: {
blogTagsListPath: string;
tag: BlogTag;
}): TagModule {
return {
label: tag.label,
permalink: tag.permalink,
allTagsPath: blogTagsListPath,
count: tag.items.length,
unlisted: tag.unlisted,
};
}

View File

@@ -0,0 +1,31 @@
/**
* 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 {simpleHash} from '@docusaurus/utils';
// @ts-expect-error: TODO see https://github.com/microsoft/TypeScript/issues/49721
import type {Transformer} from 'unified';
import type {FootnoteReference, FootnoteDefinition} from 'mdast';
/**
* In the blog list view, each post will be compiled separately. However, they
* may use the same footnote IDs. This leads to duplicated DOM IDs and inability
* to navigate to footnote references. This plugin fixes it by appending a
* unique hash to each reference/definition.
*/
export default function plugin(): Transformer {
return async (root, vfile) => {
const {visit} = await import('unist-util-visit');
const suffix = `-${simpleHash(vfile.path!, 6)}`;
visit(root, 'footnoteReference', (node: FootnoteReference) => {
node.identifier += suffix;
});
visit(root, 'footnoteDefinition', (node: FootnoteDefinition) => {
node.identifier += suffix;
});
};
}

View File

@@ -0,0 +1,69 @@
/**
* 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 type {TranslationFileContent, TranslationFile} from '@docusaurus/types';
import type {
PluginOptions,
BlogContent,
BlogPaginated,
} from '@docusaurus/plugin-content-blog';
function translateListPage(
blogListPaginated: BlogPaginated[],
translations: TranslationFileContent,
) {
return blogListPaginated.map((page) => {
const {items, metadata} = page;
return {
items,
metadata: {
...metadata,
blogTitle: translations.title?.message ?? page.metadata.blogTitle,
blogDescription:
translations.description?.message ?? page.metadata.blogDescription,
},
};
});
}
export function getTranslationFiles(options: PluginOptions): TranslationFile[] {
return [
{
path: 'options',
content: {
title: {
message: options.blogTitle,
description: 'The title for the blog used in SEO',
},
description: {
message: options.blogDescription,
description: 'The description for the blog used in SEO',
},
'sidebar.title': {
message: options.blogSidebarTitle,
description: 'The label for the left sidebar',
},
},
},
];
}
export function translateContent(
content: BlogContent,
translationFiles: TranslationFile[],
): BlogContent {
const {content: optionsTranslations} = translationFiles[0]!;
return {
...content,
blogSidebarTitle:
optionsTranslations['sidebar.title']?.message ?? content.blogSidebarTitle,
blogListPaginated: translateListPage(
content.blogListPaginated,
optionsTranslations,
),
};
}

View File

@@ -0,0 +1,19 @@
/**
* 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 type {BrokenMarkdownLink, ContentPaths} from '@docusaurus/utils';
export type BlogContentPaths = ContentPaths;
export type BlogBrokenMarkdownLink = BrokenMarkdownLink<BlogContentPaths>;
export type BlogMarkdownLoaderOptions = {
siteDir: string;
contentPaths: BlogContentPaths;
truncateMarker: RegExp;
sourceToPermalink: {[aliasedPath: string]: string};
onBrokenMarkdownLink: (brokenMarkdownLink: BlogBrokenMarkdownLink) => void;
};