feat(v2): editUrl function for advanced use-cases (#4121)

* EditUrl function

* normalize blog/docs regarding the editUrl feature + editUrl function

* editUrl fn => always inject posix style relative paths, make tests more reliable
(see also https://github.com/facebook/docusaurus/issues/4124)

* fix editUrl on windows
This commit is contained in:
Sébastien Lorber 2021-01-29 15:35:25 +01:00 committed by GitHub
parent 15c50e2ecb
commit be7b5dca78
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 368 additions and 69 deletions

View file

@ -10,14 +10,20 @@ import path from 'path';
import pluginContentBlog from '../index'; import pluginContentBlog from '../index';
import {DocusaurusConfig, LoadContext, I18n} from '@docusaurus/types'; import {DocusaurusConfig, LoadContext, I18n} from '@docusaurus/types';
import {PluginOptionSchema} from '../pluginOptionSchema'; import {PluginOptionSchema} from '../pluginOptionSchema';
import {PluginOptions, EditUrlFunction} from '../types';
import Joi from 'joi';
const DefaultI18N: I18n = { const DefaultI18N: I18n = {
currentLocale: 'en', currentLocale: 'en',
locales: ['en'], locales: ['en'],
defaultLocale: 'en', defaultLocale: 'en',
localeConfigs: {},
}; };
function validateAndNormalize(schema, options) { function validateAndNormalize(
schema: Joi.ObjectSchema,
options: Partial<PluginOptions>,
) {
const {value, error} = schema.validate(options); const {value, error} = schema.validate(options);
if (error) { if (error) {
throw error; throw error;
@ -27,8 +33,14 @@ function validateAndNormalize(schema, options) {
} }
describe('loadBlog', () => { describe('loadBlog', () => {
const pluginPath = 'blog'; const PluginPath = 'blog';
const getBlogPosts = async (siteDir) => {
const BaseEditUrl = 'https://baseEditUrl.com/edit';
const getBlogPosts = async (
siteDir: string,
pluginOptions: Partial<PluginOptions> = {},
) => {
const generatedFilesDir: string = path.resolve(siteDir, '.docusaurus'); const generatedFilesDir: string = path.resolve(siteDir, '.docusaurus');
const siteConfig = { const siteConfig = {
title: 'Hello', title: 'Hello',
@ -43,12 +55,12 @@ describe('loadBlog', () => {
i18n: DefaultI18N, i18n: DefaultI18N,
} as LoadContext, } as LoadContext,
validateAndNormalize(PluginOptionSchema, { validateAndNormalize(PluginOptionSchema, {
path: pluginPath, path: PluginPath,
editUrl: editUrl: BaseEditUrl,
'https://github.com/facebook/docusaurus/edit/master/website-1x', ...pluginOptions,
}), }),
); );
const {blogPosts} = await plugin.loadContent(); const {blogPosts} = (await plugin.loadContent!())!;
return blogPosts; return blogPosts;
}; };
@ -58,14 +70,13 @@ describe('loadBlog', () => {
const blogPosts = await getBlogPosts(siteDir); const blogPosts = await getBlogPosts(siteDir);
expect({ expect({
...blogPosts.find((v) => v.metadata.title === 'date-matter').metadata, ...blogPosts.find((v) => v.metadata.title === 'date-matter')!.metadata,
...{prevItem: undefined}, ...{prevItem: undefined},
}).toEqual({ }).toEqual({
editUrl: editUrl: `${BaseEditUrl}/blog/date-matter.md`,
'https://github.com/facebook/docusaurus/edit/master/website-1x/blog/date-matter.md',
permalink: '/blog/date-matter', permalink: '/blog/date-matter',
readingTime: 0.02, readingTime: 0.02,
source: path.posix.join('@site', pluginPath, 'date-matter.md'), source: path.posix.join('@site', PluginPath, 'date-matter.md'),
title: 'date-matter', title: 'date-matter',
description: `date inside front matter`, description: `date inside front matter`,
date: new Date('2019-01-01'), date: new Date('2019-01-01'),
@ -81,10 +92,9 @@ describe('loadBlog', () => {
expect( expect(
blogPosts.find( blogPosts.find(
(v) => v.metadata.title === 'Happy 1st Birthday Slash! (translated)', (v) => v.metadata.title === 'Happy 1st Birthday Slash! (translated)',
).metadata, )!.metadata,
).toEqual({ ).toEqual({
editUrl: editUrl: `${BaseEditUrl}/blog/2018-12-14-Happy-First-Birthday-Slash.md`,
'https://github.com/facebook/docusaurus/edit/master/website-1x/i18n/en/docusaurus-plugin-content-blog/2018-12-14-Happy-First-Birthday-Slash.md',
permalink: '/blog/2018/12/14/Happy-First-Birthday-Slash', permalink: '/blog/2018/12/14/Happy-First-Birthday-Slash',
readingTime: 0.015, readingTime: 0.015,
source: path.posix.join( source: path.posix.join(
@ -105,14 +115,13 @@ describe('loadBlog', () => {
}); });
expect({ expect({
...blogPosts.find((v) => v.metadata.title === 'Complex Slug').metadata, ...blogPosts.find((v) => v.metadata.title === 'Complex Slug')!.metadata,
...{prevItem: undefined}, ...{prevItem: undefined},
}).toEqual({ }).toEqual({
editUrl: editUrl: `${BaseEditUrl}/blog/complex-slug.md`,
'https://github.com/facebook/docusaurus/edit/master/website-1x/blog/complex-slug.md',
permalink: '/blog/hey/my super path/héllô', permalink: '/blog/hey/my super path/héllô',
readingTime: 0.015, readingTime: 0.015,
source: path.posix.join('@site', pluginPath, 'complex-slug.md'), source: path.posix.join('@site', PluginPath, 'complex-slug.md'),
title: 'Complex Slug', title: 'Complex Slug',
description: `complex url slug`, description: `complex url slug`,
prevItem: undefined, prevItem: undefined,
@ -126,14 +135,13 @@ describe('loadBlog', () => {
}); });
expect({ expect({
...blogPosts.find((v) => v.metadata.title === 'Simple Slug').metadata, ...blogPosts.find((v) => v.metadata.title === 'Simple Slug')!.metadata,
...{prevItem: undefined}, ...{prevItem: undefined},
}).toEqual({ }).toEqual({
editUrl: editUrl: `${BaseEditUrl}/blog/simple-slug.md`,
'https://github.com/facebook/docusaurus/edit/master/website-1x/blog/simple-slug.md',
permalink: '/blog/simple/slug', permalink: '/blog/simple/slug',
readingTime: 0.015, readingTime: 0.015,
source: path.posix.join('@site', pluginPath, 'simple-slug.md'), source: path.posix.join('@site', PluginPath, 'simple-slug.md'),
title: 'Simple Slug', title: 'Simple Slug',
description: `simple url slug`, description: `simple url slug`,
prevItem: undefined, prevItem: undefined,
@ -147,6 +155,59 @@ describe('loadBlog', () => {
}); });
}); });
test('edit url with editLocalizedBlogs true', async () => {
const siteDir = path.join(__dirname, '__fixtures__', 'website');
const blogPosts = await getBlogPosts(siteDir, {editLocalizedFiles: true});
const localizedBlogPost = blogPosts.find(
(v) => v.metadata.title === 'Happy 1st Birthday Slash! (translated)',
)!;
expect(localizedBlogPost.metadata.editUrl).toEqual(
`${BaseEditUrl}/i18n/en/docusaurus-plugin-content-blog/2018-12-14-Happy-First-Birthday-Slash.md`,
);
});
test('edit url with editUrl function', async () => {
const siteDir = path.join(__dirname, '__fixtures__', 'website');
const hardcodedEditUrl = 'hardcoded-edit-url';
const editUrlFunction: EditUrlFunction = jest.fn(() => hardcodedEditUrl);
const blogPosts = await getBlogPosts(siteDir, {editUrl: editUrlFunction});
blogPosts.forEach((blogPost) => {
expect(blogPost.metadata.editUrl).toEqual(hardcodedEditUrl);
});
expect(editUrlFunction).toHaveBeenCalledTimes(5);
expect(editUrlFunction).toHaveBeenCalledWith({
blogDirPath: 'blog',
blogPath: 'date-matter.md',
locale: 'en',
});
expect(editUrlFunction).toHaveBeenCalledWith({
blogDirPath: 'blog',
blogPath: 'draft.md',
locale: 'en',
});
expect(editUrlFunction).toHaveBeenCalledWith({
blogDirPath: 'blog',
blogPath: 'complex-slug.md',
locale: 'en',
});
expect(editUrlFunction).toHaveBeenCalledWith({
blogDirPath: 'blog',
blogPath: 'simple-slug.md',
locale: 'en',
});
expect(editUrlFunction).toHaveBeenCalledWith({
blogDirPath: 'i18n/en/docusaurus-plugin-content-blog',
blogPath: '2018-12-14-Happy-First-Birthday-Slash.md',
locale: 'en',
});
});
test('draft blog post not exists in production build', async () => { test('draft blog post not exists in production build', async () => {
process.env.NODE_ENV = 'production'; process.env.NODE_ENV = 'production';
const siteDir = path.join(__dirname, '__fixtures__', 'website'); const siteDir = path.join(__dirname, '__fixtures__', 'website');
@ -162,17 +223,16 @@ describe('loadBlog', () => {
'website-blog-without-date', 'website-blog-without-date',
); );
const blogPosts = await getBlogPosts(siteDir); const blogPosts = await getBlogPosts(siteDir);
const noDateSource = path.posix.join('@site', pluginPath, 'no date.md'); const noDateSource = path.posix.join('@site', PluginPath, 'no date.md');
const noDateSourceBirthTime = ( const noDateSourceBirthTime = (
await fs.stat(noDateSource.replace('@site', siteDir)) await fs.stat(noDateSource.replace('@site', siteDir))
).birthtime; ).birthtime;
expect({ expect({
...blogPosts.find((v) => v.metadata.title === 'no date').metadata, ...blogPosts.find((v) => v.metadata.title === 'no date')!.metadata,
...{prevItem: undefined}, ...{prevItem: undefined},
}).toEqual({ }).toEqual({
editUrl: editUrl: `${BaseEditUrl}/blog/no date.md`,
'https://github.com/facebook/docusaurus/edit/master/website-1x/blog/no date.md',
permalink: '/blog/no date', permalink: '/blog/no date',
readingTime: 0.01, readingTime: 0.01,
source: noDateSource, source: noDateSource,

View file

@ -26,6 +26,7 @@ import {
aliasedSitePath, aliasedSitePath,
getEditUrl, getEditUrl,
getFolderContainingFile, getFolderContainingFile,
posixPath,
} from '@docusaurus/utils'; } from '@docusaurus/utils';
import {LoadContext} from '@docusaurus/types'; import {LoadContext} from '@docusaurus/types';
import {keyBy} from 'lodash'; import {keyBy} from 'lodash';
@ -99,7 +100,7 @@ export async function generateBlogFeed(
export async function generateBlogPosts( export async function generateBlogPosts(
contentPaths: BlogContentPaths, contentPaths: BlogContentPaths,
{siteConfig, siteDir}: LoadContext, {siteConfig, siteDir, i18n}: LoadContext,
options: PluginOptions, options: PluginOptions,
): Promise<BlogPost[]> { ): Promise<BlogPost[]> {
const { const {
@ -107,7 +108,7 @@ export async function generateBlogPosts(
routeBasePath, routeBasePath,
truncateMarker, truncateMarker,
showReadingTime, showReadingTime,
editUrl, editUrl: siteEditUrl,
} = options; } = options;
if (!fs.existsSync(contentPaths.contentPath)) { if (!fs.existsSync(contentPaths.contentPath)) {
@ -124,18 +125,47 @@ export async function generateBlogPosts(
await Promise.all( await Promise.all(
blogSourceFiles.map(async (blogSourceFile: string) => { blogSourceFiles.map(async (blogSourceFile: string) => {
// Lookup in localized folder in priority // Lookup in localized folder in priority
const contentPath = await getFolderContainingFile( const blogDirPath = await getFolderContainingFile(
getContentPathList(contentPaths), getContentPathList(contentPaths),
blogSourceFile, blogSourceFile,
); );
const source = path.join(contentPath, blogSourceFile); const source = path.join(blogDirPath, blogSourceFile);
const aliasedSource = aliasedSitePath(source, siteDir); const aliasedSource = aliasedSitePath(source, siteDir);
const relativePath = path.relative(siteDir, source);
const blogFileName = path.basename(blogSourceFile); const blogFileName = path.basename(blogSourceFile);
const editBlogUrl = getEditUrl(relativePath, editUrl); function getBlogEditUrl() {
const blogPathRelative = path.relative(
blogDirPath,
path.resolve(source),
);
if (typeof siteEditUrl === 'function') {
return siteEditUrl({
blogDirPath: posixPath(path.relative(siteDir, blogDirPath)),
blogPath: posixPath(blogPathRelative),
locale: i18n.currentLocale,
});
} else if (typeof siteEditUrl === 'string') {
const isLocalized = blogDirPath === contentPaths.contentPathLocalized;
const fileContentPath =
isLocalized && options.editLocalizedFiles
? contentPaths.contentPathLocalized
: contentPaths.contentPath;
const contentPathEditUrl = normalizeUrl([
siteEditUrl,
posixPath(path.relative(siteDir, fileContentPath)),
]);
return getEditUrl(blogPathRelative, contentPathEditUrl);
} else {
return undefined;
}
}
const editBlogUrl = getBlogEditUrl();
const {frontMatter, content, excerpt} = await parseMarkdownFile(source); const {frontMatter, content, excerpt} = await parseMarkdownFile(source);

View file

@ -34,6 +34,7 @@ export const DEFAULT_OPTIONS = {
include: ['*.md', '*.mdx'], include: ['*.md', '*.mdx'],
routeBasePath: 'blog', routeBasePath: 'blog',
path: 'blog', path: 'blog',
editLocalizedFiles: false,
}; };
export const PluginOptionSchema = Joi.object({ export const PluginOptionSchema = Joi.object({
@ -67,7 +68,8 @@ export const PluginOptionSchema = Joi.object({
remarkPlugins: RemarkPluginsSchema.default(DEFAULT_OPTIONS.remarkPlugins), remarkPlugins: RemarkPluginsSchema.default(DEFAULT_OPTIONS.remarkPlugins),
rehypePlugins: RehypePluginsSchema.default(DEFAULT_OPTIONS.rehypePlugins), rehypePlugins: RehypePluginsSchema.default(DEFAULT_OPTIONS.rehypePlugins),
admonitions: AdmonitionsSchema.default(DEFAULT_OPTIONS.admonitions), admonitions: AdmonitionsSchema.default(DEFAULT_OPTIONS.admonitions),
editUrl: URISchema, editUrl: Joi.alternatives().try(URISchema, Joi.function()),
editLocalizedFiles: Joi.boolean().default(DEFAULT_OPTIONS.editLocalizedFiles),
truncateMarker: Joi.object().default(DEFAULT_OPTIONS.truncateMarker), truncateMarker: Joi.object().default(DEFAULT_OPTIONS.truncateMarker),
beforeDefaultRemarkPlugins: RemarkPluginsSchema.default( beforeDefaultRemarkPlugins: RemarkPluginsSchema.default(
DEFAULT_OPTIONS.beforeDefaultRemarkPlugins, DEFAULT_OPTIONS.beforeDefaultRemarkPlugins,

View file

@ -24,6 +24,12 @@ export interface DateLink {
export type FeedType = 'rss' | 'atom'; export type FeedType = 'rss' | 'atom';
export type EditUrlFunction = (editUrlParams: {
blogDirPath: string;
blogPath: string;
locale: string;
}) => string | undefined;
export interface PluginOptions { export interface PluginOptions {
id?: string; id?: string;
path: string; path: string;
@ -57,7 +63,8 @@ export interface PluginOptions {
copyright: string; copyright: string;
language?: string; language?: string;
}; };
editUrl?: string; editUrl?: string | EditUrlFunction;
editLocalizedFiles?: boolean;
admonitions: Record<string, unknown>; admonitions: Record<string, unknown>;
} }

View file

@ -15,6 +15,7 @@ import {
MetadataOptions, MetadataOptions,
VersionMetadata, VersionMetadata,
PluginOptions, PluginOptions,
EditUrlFunction,
} from '../types'; } from '../types';
import {LoadContext} from '@docusaurus/types'; import {LoadContext} from '@docusaurus/types';
import {DEFAULT_PLUGIN_ID} from '@docusaurus/core/lib/constants'; import {DEFAULT_PLUGIN_ID} from '@docusaurus/core/lib/constants';
@ -285,6 +286,45 @@ describe('simple site', () => {
}); });
}); });
test('docs with function editUrl', async () => {
const hardcodedEditUrl = 'hardcoded-edit-url';
const editUrlFunction: EditUrlFunction = jest.fn(() => hardcodedEditUrl);
const {siteDir, context, options, currentVersion} = await loadSite({
options: {
editUrl: editUrlFunction,
},
});
const testUtilsLocal = createTestUtils({
siteDir,
context,
options,
versionMetadata: currentVersion,
});
await testUtilsLocal.testMeta(path.join('foo', 'baz.md'), {
version: 'current',
id: 'foo/baz',
unversionedId: 'foo/baz',
isDocsHomePage: false,
permalink: '/docs/foo/bazSlug.html',
slug: '/foo/bazSlug.html',
title: 'baz',
editUrl: hardcodedEditUrl,
description: 'Images',
});
expect(editUrlFunction).toHaveBeenCalledTimes(1);
expect(editUrlFunction).toHaveBeenCalledWith({
version: 'current',
versionDocsDirPath: 'docs',
docPath: path.posix.join('foo', 'baz.md'),
locale: 'en',
});
});
test('docs with last update time and author', async () => { test('docs with last update time and author', async () => {
const {siteDir, context, options, currentVersion} = await loadSite({ const {siteDir, context, options, currentVersion} = await loadSite({
options: { options: {
@ -595,6 +635,47 @@ describe('versioned site', () => {
); );
}); });
test('doc with editUrl function', async () => {
const hardcodedEditUrl = 'hardcoded-edit-url';
const editUrlFunction: EditUrlFunction = jest.fn(() => hardcodedEditUrl);
const {siteDir, context, options, version100} = await loadSite({
options: {
editUrl: editUrlFunction,
},
});
const testUtilsLocal = createTestUtils({
siteDir,
context,
options,
versionMetadata: version100,
});
await testUtilsLocal.testMeta(path.join('hello.md'), {
id: 'version-1.0.0/hello',
unversionedId: 'hello',
isDocsHomePage: false,
permalink: '/docs/1.0.0/hello',
slug: '/hello',
title: 'hello',
description: 'Hello 1.0.0 ! (translated en)',
version: '1.0.0',
source:
'@site/i18n/en/docusaurus-plugin-content-docs/version-1.0.0/hello.md',
editUrl: hardcodedEditUrl,
});
expect(editUrlFunction).toHaveBeenCalledTimes(1);
expect(editUrlFunction).toHaveBeenCalledWith({
version: '1.0.0',
versionDocsDirPath: 'versioned_docs/version-1.0.0',
docPath: path.join('hello.md'),
locale: 'en',
});
});
test('translated doc with editUrl', async () => { test('translated doc with editUrl', async () => {
const {siteDir, context, options, version100} = await loadSite({ const {siteDir, context, options, version100} = await loadSite({
options: { options: {
@ -657,11 +738,11 @@ describe('versioned site', () => {
}); });
}); });
test('translated fr doc with editUrl and editLocalizedDocs=true', async () => { test('translated fr doc with editUrl and editLocalizedFiles=true', async () => {
const {siteDir, context, options, version100} = await loadSite({ const {siteDir, context, options, version100} = await loadSite({
options: { options: {
editUrl: 'https://github.com/facebook/docusaurus/edit/master/website', editUrl: 'https://github.com/facebook/docusaurus/edit/master/website',
editLocalizedDocs: true, editLocalizedFiles: true,
}, },
locale: 'fr', locale: 'fr',
}); });
@ -689,12 +770,12 @@ describe('versioned site', () => {
}); });
}); });
test('translated fr doc with editUrl and editLocalizedDocs=true + editCurrentVersion=true', async () => { test('translated fr doc with editUrl and editLocalizedFiles=true + editCurrentVersion=true', async () => {
const {siteDir, context, options, version100} = await loadSite({ const {siteDir, context, options, version100} = await loadSite({
options: { options: {
editUrl: 'https://github.com/facebook/docusaurus/edit/master/website', editUrl: 'https://github.com/facebook/docusaurus/edit/master/website',
editCurrentVersion: true, editCurrentVersion: true,
editLocalizedDocs: true, editLocalizedFiles: true,
}, },
locale: 'fr', locale: 'fr',
}); });

View file

@ -39,7 +39,7 @@ describe('normalizeDocsPluginOptions', () => {
includeCurrentVersion: false, includeCurrentVersion: false,
disableVersioning: true, disableVersioning: true,
editCurrentVersion: true, editCurrentVersion: true,
editLocalizedDocs: true, editLocalizedFiles: true,
versions: { versions: {
current: { current: {
path: 'next', path: 'next',

View file

@ -13,6 +13,7 @@ import {
getFolderContainingFile, getFolderContainingFile,
normalizeUrl, normalizeUrl,
parseMarkdownString, parseMarkdownString,
posixPath,
} from '@docusaurus/utils'; } from '@docusaurus/utils';
import {LoadContext} from '@docusaurus/types'; import {LoadContext} from '@docusaurus/types';
@ -120,14 +121,29 @@ export function processDocMetadata({
const relativeFilePath = path.relative(docsDirPath, filePath); const relativeFilePath = path.relative(docsDirPath, filePath);
const isLocalized = docsDirPath === versionMetadata.docsDirPathLocalized; function getDocEditUrl() {
if (typeof options.editUrl === 'function') {
return options.editUrl({
version: versionMetadata.versionName,
versionDocsDirPath: posixPath(
path.relative(siteDir, versionMetadata.docsDirPath),
),
docPath: posixPath(relativeFilePath),
locale: context.i18n.currentLocale,
});
} else if (typeof options.editUrl === 'string') {
const isLocalized = docsDirPath === versionMetadata.docsDirPathLocalized;
const baseVersionEditUrl =
isLocalized && options.editLocalizedFiles
? versionMetadata.versionEditUrlLocalized
: versionMetadata.versionEditUrl;
return getEditUrl(relativeFilePath, baseVersionEditUrl);
} else {
return undefined;
}
}
const versionEditUrl = const docsEditUrl = getDocEditUrl();
isLocalized && options.editLocalizedDocs
? versionMetadata.versionEditUrlLocalized
: versionMetadata.versionEditUrl;
const docsEditUrl = getEditUrl(relativeFilePath, versionEditUrl);
const {frontMatter = {}, excerpt} = parseMarkdownString(content); const {frontMatter = {}, excerpt} = parseMarkdownString(content);
const {sidebar_label, custom_edit_url} = frontMatter; const {sidebar_label, custom_edit_url} = frontMatter;

View file

@ -38,7 +38,7 @@ export const DEFAULT_OPTIONS: Omit<PluginOptions, 'id'> = {
lastVersion: undefined, lastVersion: undefined,
versions: {}, versions: {},
editCurrentVersion: false, editCurrentVersion: false,
editLocalizedDocs: false, editLocalizedFiles: false,
}; };
const VersionOptionsSchema = Joi.object({ const VersionOptionsSchema = Joi.object({
@ -52,9 +52,9 @@ const VersionsOptionsSchema = Joi.object()
export const OptionsSchema = Joi.object({ export const OptionsSchema = Joi.object({
path: Joi.string().default(DEFAULT_OPTIONS.path), path: Joi.string().default(DEFAULT_OPTIONS.path),
editUrl: URISchema, editUrl: Joi.alternatives().try(URISchema, Joi.function()),
editCurrentVersion: Joi.boolean().default(DEFAULT_OPTIONS.editCurrentVersion), editCurrentVersion: Joi.boolean().default(DEFAULT_OPTIONS.editCurrentVersion),
editLocalizedDocs: Joi.boolean().default(DEFAULT_OPTIONS.editLocalizedDocs), editLocalizedFiles: Joi.boolean().default(DEFAULT_OPTIONS.editLocalizedFiles),
routeBasePath: Joi.string() routeBasePath: Joi.string()
// '' not allowed, see https://github.com/facebook/docusaurus/issues/3374 // '' not allowed, see https://github.com/facebook/docusaurus/issues/3374
// .allow('') "" // .allow('') ""

View file

@ -31,12 +31,19 @@ export type VersionMetadata = {
routePriority: number | undefined; // -1 for the latest docs routePriority: number | undefined; // -1 for the latest docs
}; };
export type EditUrlFunction = (editUrlParams: {
version: string;
versionDocsDirPath: string;
docPath: string;
locale: string;
}) => string | undefined;
export type MetadataOptions = { export type MetadataOptions = {
routeBasePath: string; routeBasePath: string;
homePageId?: string; homePageId?: string;
editUrl?: string; editUrl?: string | EditUrlFunction;
editCurrentVersion: boolean; editCurrentVersion: boolean;
editLocalizedDocs: boolean; editLocalizedFiles: boolean;
showLastUpdateTime?: boolean; showLastUpdateTime?: boolean;
showLastUpdateAuthor?: boolean; showLastUpdateAuthor?: boolean;
}; };

View file

@ -211,6 +211,12 @@ function getVersionEditUrls({
return undefined; return undefined;
} }
// if the user is using the functional form of editUrl,
// he has total freedom and we can't compute a "version edit url"
if (typeof editUrl === 'function') {
return undefined;
}
const editDirPath = editCurrentVersion ? currentVersionPath : docsDirPath; const editDirPath = editCurrentVersion ? currentVersionPath : docsDirPath;
const editDirPathLocalized = editCurrentVersion const editDirPathLocalized = editCurrentVersion
? getDocsDirPathLocalized({ ? getDocsDirPathLocalized({

View file

@ -31,11 +31,23 @@ module.exports = {
*/ */
path: 'blog', path: 'blog',
/** /**
* URL for editing a blog post. * Base url to edit your site.
* Example: 'https://github.com/facebook/docusaurus/edit/master/website/blog/' * Docusaurus will compute the final editUrl with "editUrl + relativeDocPath"
*/ */
editUrl: editUrl: 'https://github.com/facebook/docusaurus/edit/master/website/',
'https://github.com/facebook/docusaurus/edit/master/website/blog/', /**
* For advanced cases, compute the edit url for each markdown file yourself.
*/
editUrl: ({locale, blogDirPath, blogPath}) => {
return `https://github.com/facebook/docusaurus/edit/master/website/${blogDirPath}/${blogPath}`;
},
/**
* Useful if you commit localized files to git.
* When markdown files are localized, the edit url will target the localized file,
* instead of the original unlocalized file.
* Note: this option is ignored when editUrl is a function
*/
editLocalizedFiles: false,
/** /**
* Blog page title for better SEO * Blog page title for better SEO
*/ */

View file

@ -31,22 +31,30 @@ module.exports = {
*/ */
path: 'docs', path: 'docs',
/** /**
* URL for editing a doc in the website repo. * Base url to edit your site.
* Example: 'https://github.com/facebook/docusaurus/edit/master/website/' * Docusaurus will compute the final editUrl with "editUrl + relativeDocPath"
*/ */
editUrl: 'https://github.com/facebook/docusaurus/edit/master/website/', editUrl: 'https://github.com/facebook/docusaurus/edit/master/website/',
/** /**
* For advanced cases, compute the edit url for each markdown file yourself.
*/
editUrl: function ({locale, version, versionDocsDirPath, docPath}) {
return `https://github.com/facebook/docusaurus/edit/master/website/${versionDocsDirPath}/${docPath}`;
},
/**
* Useful if you commit localized files to git.
* When markdown files are localized, the edit url will target the localized file,
* instead of the original unlocalized file.
* Note: this option is ignored when editUrl is a function
*/
editLocalizedFiles: false,
/**
* Useful if you don't want users to submit doc pull-requests to older versions.
* When docs are versioned, the edit url will link to the doc * When docs are versioned, the edit url will link to the doc
* in current version, instead of the versioned doc. * in current version, instead of the versioned doc.
* Useful if you don't want users to submit doc pull-requests to older versions. * Note: this option is ignored when editUrl is a function
*/ */
editCurrentVersion: false, editCurrentVersion: false,
/**
* When docs are localized, the edit url will target the localized doc,
* instead of the original unlocalized doc.
* Useful if you commit localized docs to git, instead of using a translation service.
*/
editLocalizedDocs: false,
/** /**
* URL route for the docs section of your site. * URL route for the docs section of your site.
* *DO NOT* include a trailing slash. * *DO NOT* include a trailing slash.

View file

@ -409,6 +409,54 @@ Crowdin replaces markdown strings with technical ids such as `crowdin:id12345`,
::: :::
### Localize edit urls
When the user is browsing a page at `/fr/doc1`, the edit button will link by default to the unlocalized doc at `website/docs/doc1.md`.
You may prefer the edit button to link to the Crowdin interface instead, and can use the `editUrl` function to customize the edit urls on a per-locale basis.
```js title="docusaurus.config.js"
const DefaultLocale = 'en';
module.exports = {
presets: [
[
'@docusaurus/preset-classic',
{
docs: {
// highlight-start
editUrl: ({locale, versionDocsDirPath, docPath}) => {
// Link to Crowdin for French docs
if (locale !== DefaultLocale) {
return `https://crowdin.com/project/docusaurus-v2/${locale}`;
}
// Link to Github for English docs
return `https://github.com/facebook/docusaurus/edit/master/website/${versionDocsDirPath}/${docPath}`;
},
// highlight-end
},
blog: {
// highlight-start
editUrl: ({locale, blogDirPath, blogPath}) => {
if (locale !== DefaultLocale) {
return `https://crowdin.com/project/docusaurus-v2/${locale}`;
}
return `https://github.com/facebook/docusaurus/edit/master/website/${blogDirPath}/${blogPath}`;
},
// highlight-start
},
},
],
],
};
```
:::note
It is currently **not possible to link to a specific file** in Crowdin.
:::
### Example configuration ### Example configuration
The **Docusaurus v2 configuration file** is a good example of using versioning and multi-instance: The **Docusaurus v2 configuration file** is a good example of using versioning and multi-instance:

View file

@ -171,3 +171,11 @@ New translation will be appended, and existing ones will not be overridden.
Reset your translations with the `--override` option. Reset your translations with the `--override` option.
::: :::
### Localize edit urls
When the user is browsing a page at `/fr/doc1`, the edit button will link by default to the unlocalized doc at `website/docs/doc1.md`.
Your translations are on Git, and you can use the `editLocalizedFiles: true` option of the docs and blog plugins.
The edit button will link to the localized doc at `i18n/fr/docusaurus-plugin-content-docs/current/doc1.md`.

View file

@ -41,6 +41,7 @@ const isBootstrapPreset = process.env.DOCUSAURUS_PRESET === 'bootstrap';
const isVersioningDisabled = !!process.env.DISABLE_VERSIONING; const isVersioningDisabled = !!process.env.DISABLE_VERSIONING;
/** @type {import('@docusaurus/types').DocusaurusConfig} */
module.exports = { module.exports = {
title: 'Docusaurus', title: 'Docusaurus',
tagline: 'Build optimized websites quickly, focus on your content', tagline: 'Build optimized websites quickly, focus on your content',
@ -76,7 +77,12 @@ module.exports = {
{ {
id: 'community', id: 'community',
path: 'community', path: 'community',
editUrl: 'https://github.com/facebook/docusaurus/edit/master/website/', editUrl: ({locale, versionDocsDirPath, docPath}) => {
if (locale !== 'en') {
return `https://crowdin.com/project/docusaurus-v2/${locale}`;
}
return `https://github.com/facebook/docusaurus/edit/master/website/${versionDocsDirPath}/${docPath}`;
},
editCurrentVersion: true, editCurrentVersion: true,
routeBasePath: 'community', routeBasePath: 'community',
sidebarPath: require.resolve('./sidebarsCommunity.js'), sidebarPath: require.resolve('./sidebarsCommunity.js'),
@ -204,8 +210,12 @@ module.exports = {
// routeBasePath: '/', // routeBasePath: '/',
path: 'docs', path: 'docs',
sidebarPath: require.resolve('./sidebars.js'), sidebarPath: require.resolve('./sidebars.js'),
editUrl: editUrl: ({locale, versionDocsDirPath, docPath}) => {
'https://github.com/facebook/docusaurus/edit/master/website/', if (locale !== 'en') {
return `https://crowdin.com/project/docusaurus-v2/${locale}`;
}
return `https://github.com/facebook/docusaurus/edit/master/website/${versionDocsDirPath}/${docPath}`;
},
editCurrentVersion: true, editCurrentVersion: true,
showLastUpdateAuthor: true, showLastUpdateAuthor: true,
showLastUpdateTime: true, showLastUpdateTime: true,
@ -227,8 +237,12 @@ module.exports = {
blog: { blog: {
// routeBasePath: '/', // routeBasePath: '/',
path: '../website-1.x/blog', path: '../website-1.x/blog',
editUrl: editUrl: ({locale, blogDirPath, blogPath}) => {
'https://github.com/facebook/docusaurus/edit/master/website-1.x/', if (locale !== 'en') {
return `https://crowdin.com/project/docusaurus-v2/${locale}`;
}
return `https://github.com/facebook/docusaurus/edit/master/website/${blogDirPath}/${blogPath}`;
},
postsPerPage: 3, postsPerPage: 3,
feedOptions: { feedOptions: {
type: 'all', type: 'all',