Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,26 @@
# Docusaurus Changelog

## 3.10.2 (2026-07-10)

Backport and cherry-pick commits from main for v3.10.2 patch release:

- [fix(bundler): do not import `@swc/html`, fix StackBlitz playground \#12055](https://github.com/facebook/docusaurus/pull/12055)
- [fix(core): use locale url in site config \#12054](https://github.com/facebook/docusaurus/pull/12054)
- [fix(theme-classic): remove redundant sidebar label titles \#11966](https://github.com/facebook/docusaurus/pull/11966)
- [fix(mdx-loader): avoid transforming dotted directory links into asset… \#11944](https://github.com/facebook/docusaurus/pull/11944)
- [fix(dev-server): for HTTPS, support non-RSA TLS certs \#12065](https://github.com/facebook/docusaurus/pull/12065)
- [fix(utils): fix `extractLeadingEmoji()` edge cases \#12100](https://github.com/facebook/docusaurus/pull/12100)
- [fix(gtag.js, faster): Fix StackBlitz, vendor `@types/gtag.js`, upgrade `@swc/html` \#12080](https://github.com/facebook/docusaurus/pull/12080)
- [fix(create-docusaurus): init template README should use npm commands by default \#12138](https://github.com/facebook/docusaurus/pull/12138)
- [fix(cli): `docusaurus serve` should pass `--host` to `server.listen()` \#12127](https://github.com/facebook/docusaurus/pull/12127)
- [fix(core): bump detect-port to v2.1, fix pnpm `trustPolicy` downgrade issue \#12012](https://github.com/facebook/docusaurus/pull/12012)
- [fix(i18n): complete Spanish translations for theme-common and theme-l… \#12180](https://github.com/facebook/docusaurus/pull/12180)
- [chore(deps): migrate to `@11ty/gray-matter` \#12181](https://github.com/facebook/docusaurus/pull/12181)
- [fix(core): fix BaseUrlIssueBanner little security issue \#12260](https://github.com/facebook/docusaurus/pull/12260)
- [fix(core): accept boolean attributes in headTags config validation \#12238](https://github.com/facebook/docusaurus/pull/12238)
- [fix(sitemap): don't drop lastmod for an epoch (0) timestamp \#12212](https://github.com/facebook/docusaurus/pull/12212)
- [fix(utils): preserve author names containing commas in git log parsing \#12069](https://github.com/facebook/docusaurus/pull/12069)

## 3.10.1 (2026-04-30)

#### :bug: Bug Fix
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,9 +99,10 @@ function validateCollectedRedirects(
// See https://github.com/facebook/docusaurus/issues/6845
.map((to) => {
if (to.startsWith('/')) {
try {
return decodeURI(new URL(to, 'https://example.com').pathname);
} catch {}
const url = URL.parse(to, 'https://example.com');
if (url) {
return decodeURI(url.pathname);
}
}
return undefined;
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,12 @@ function renderRedirectPageTemplate(data: {
// if the target url does not include ?search#anchor,
// we forward search/anchor that the redirect page receives
function searchAnchorForwarding(toUrl: string): boolean {
try {
const url = new URL(toUrl, 'https://example.com');
const containsSearchOrAnchor = url.search || url.hash;
return !containsSearchOrAnchor;
} catch {
const url = URL.parse(toUrl, 'https://example.com');
if (url === null) {
return false;
}
const containsSearchOrAnchor = url.search || url.hash;
return !containsSearchOrAnchor;
}

export default function createRedirectPageContent({
Expand Down
7 changes: 3 additions & 4 deletions packages/docusaurus-utils-validation/src/validationSchemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,12 +72,11 @@ export const URISchema = Joi.alternatives(
if (typeof val !== 'string') {
return helpers.error('any.invalid');
}
try {
new URL(String(val));
return val;
} catch {
const url = URL.parse(String(val));
if (url === null) {
return helpers.error('any.invalid');
}
return val;
}),
).messages({
'alternatives.match':
Expand Down
32 changes: 14 additions & 18 deletions packages/docusaurus-utils/src/urlUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,22 +156,20 @@ export function isValidPathname(str: string): boolean {
if (!str.startsWith('/')) {
return false;
}
try {
const parsedPathname = new URL(str, 'https://domain.com').pathname;
return parsedPathname === str || parsedPathname === encodeURI(str);
} catch {
const url = URL.parse(str, 'https://domain.com');
if (url === null) {
return false;
}
const parsedPathname = url.pathname;
return parsedPathname === str || parsedPathname === encodeURI(str);
}

export function parseURLOrPath(url: string, base?: string | URL): URL {
const parsedURL = URL.parse(url, base ?? 'https://example.com');

if (parsedURL) {
return parsedURL;
export function parseURLOrPath(str: string, base?: string | URL): URL {
const url = URL.parse(str, base ?? 'https://example.com');
if (url) {
return url;
}

throw new Error(`Can't parse URL ${url}${base ? ` with base ${base}` : ''}`);
throw new Error(`Can't parse URL ${str}${base ? ` with base ${base}` : ''}`);
}

export type URLPath = {pathname: string; search?: string; hash?: string};
Expand Down Expand Up @@ -315,13 +313,11 @@ export function buildHttpsUrl(
* `git@github.com:facebook/docusaurus.git`.
*/
export function hasSSHProtocol(sourceRepoUrl: string): boolean {
try {
if (new URL(sourceRepoUrl).protocol === 'ssh:') {
return true;
}
return false;
} catch {
// Fails when there isn't a protocol
const url = URL.parse(sourceRepoUrl);
if (url === null) {
// Recognizes SCP-style addresses, implying SSH
// Example: git@github.com:facebook/docusaurus.git
return /^(?:[\w-]+@)?[\w.-]+:[\w./-]+/.test(sourceRepoUrl);
}
return url.protocol === 'ssh:';
}
18 changes: 18 additions & 0 deletions packages/docusaurus-utils/src/vcs/__tests__/gitUtils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 +396,24 @@ describe('commit info APIs', () => {
}
`);
});

it('preserves author names containing commas', async () => {
const {repoDir, git} = await createGitRepoEmpty();

await git.commitFile('comma-author.txt', {
fileContent: 'content',
commitMessage: 'Commit by author with comma in name',
commitDate: '2024-01-15',
commitAuthor: 'Doe, Jane <jane@example.com>',
});

const filesInfo = await getGitRepositoryFilesInfo(repoDir);
const fileInfo = filesInfo.get('comma-author.txt');

expect(fileInfo).toBeDefined();
expect(fileInfo!.creation.author).toBe('Doe, Jane');
expect(fileInfo!.lastUpdate.author).toBe('Doe, Jane');
});
});
});

Expand Down
12 changes: 7 additions & 5 deletions packages/docusaurus-utils/src/vcs/gitUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -487,11 +487,13 @@ The command exited with code ${result.exitCode}: ${result.stderr}`,
for (const logLine of logLines) {
if (logLine.startsWith('t:')) {
// t:<timestamp>,a:<author name>
const [timestampStr, authorStr] = logLine.split(',') as [string, string];
const timestamp = Number.parseInt(timestampStr.slice(2), 10) * 1000;
const author = authorStr.slice(2);

runningDate = timestamp;
// We can't use split(',') because author names may contain commas
// Example: "t:123456,a:John Doe, Jr."
const [timestampStr, author] = logLine.slice(2).split(',a:') as [
string,
string,
];
runningDate = Number.parseInt(timestampStr, 10) * 1000;
runningAuthor = author;
}

Expand Down
12 changes: 6 additions & 6 deletions packages/docusaurus/src/server/configValidation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,14 +38,14 @@ const DEFAULT_I18N_LOCALE = 'en';

const SiteUrlSchema = Joi.string()
.custom((value: string, helpers) => {
try {
const {pathname} = new URL(value);
if (pathname !== '/') {
return helpers.error('docusaurus.subPathError', {pathname});
}
} catch {
const url = URL.parse(value);
if (url === null) {
return helpers.error('any.invalid');
}
const {pathname} = url;
if (pathname !== '/') {
return helpers.error('docusaurus.subPathError', {pathname});
}
return removeTrailingSlash(value);
})
.messages({
Expand Down
10 changes: 2 additions & 8 deletions packages/eslint-plugin/src/rules/no-html-links.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,8 @@ type Options = [
type MessageIds = 'link';

function isFullyResolvedUrl(urlString: string): boolean {
try {
// href gets coerced to a string when it gets rendered anyway
const url = new URL(String(urlString));
if (url.protocol) {
return true;
}
} catch {}
return false;
const url = URL.parse(String(urlString));
return !!(url && url.protocol);
}

export default createRule<Options, MessageIds>({
Expand Down
2 changes: 1 addition & 1 deletion website/versions.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[
"3.10.1",
"3.10.2",
"3.9.2",
"3.8.1",
"3.7.0",
Expand Down
Loading