Skip to content

Commit 3dacdf3

Browse files
authored
feat(mdx): resolve @site/* markdown links, fix resolution priority bugs (#11397)
1 parent 72c48b5 commit 3dacdf3

File tree

4 files changed

+119
-18
lines changed

4 files changed

+119
-18
lines changed

packages/docusaurus-utils/src/__tests__/markdownLinks.test.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,4 +75,60 @@ describe('resolveMarkdownLinkPathname', () => {
7575
test('api/classes/divine_uri.URI.md', '/docs/api/classes/uri');
7676
test('another.md', '/docs/another');
7777
});
78+
79+
it('uses relative file paths in priority - Fix #11099', () => {
80+
// Unit test to fix bug https://github.com/facebook/docusaurus/issues/11099
81+
82+
const context: Context = {
83+
siteDir: '.',
84+
sourceFilePath: 'docs/test/file.mdx',
85+
contentPaths: {
86+
contentPath: 'docs',
87+
contentPathLocalized: 'i18n/docs-localized',
88+
},
89+
sourceToPermalink: new Map(
90+
Object.entries({
91+
'@site/docs/file.mdx': '/docs/file',
92+
'@site/docs/test/file.mdx': '/docs/test/file',
93+
}),
94+
),
95+
};
96+
97+
function test(linkPathname: string, expectedOutput: string) {
98+
const output = resolveMarkdownLinkPathname(linkPathname, context);
99+
expect(output).toEqual(expectedOutput);
100+
}
101+
102+
test('./file.mdx', '/docs/test/file');
103+
test('file.mdx', '/docs/test/file');
104+
});
105+
106+
it('can resolve @site/ links', () => {
107+
const context: Context = {
108+
siteDir: '.',
109+
sourceFilePath: 'docs/test/file.mdx',
110+
contentPaths: {
111+
contentPath: 'docs',
112+
contentPathLocalized: 'i18n/docs-localized',
113+
},
114+
sourceToPermalink: new Map(
115+
Object.entries({
116+
'@site/docs/file.mdx': '/docs/file',
117+
'@site/docs/dir with spaces/file.mdx': '/docs/dir-with-spaces/file',
118+
}),
119+
),
120+
};
121+
122+
function test(linkPathname: string, expectedOutput: string) {
123+
const output = resolveMarkdownLinkPathname(linkPathname, context);
124+
expect(output).toEqual(expectedOutput);
125+
}
126+
127+
test('@site/docs/file.mdx', '/docs/file');
128+
test('@site/docs/dir with spaces/file.mdx', '/docs/dir-with-spaces/file');
129+
test(
130+
'@site/docs/dir%20with%20spaces/file.mdx',
131+
'/docs/dir-with-spaces/file',
132+
);
133+
});
78134
});

packages/docusaurus-utils/src/markdownLinks.ts

Lines changed: 35 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,6 @@ export type SourceToPermalink = Map<
4747
string // Permalink: "/docs/content"
4848
>;
4949

50-
// Note this is historical logic extracted during a 2024 refactor
51-
// The algo has been kept exactly as before for retro compatibility
52-
// See also https://github.com/facebook/docusaurus/pull/10168
5350
export function resolveMarkdownLinkPathname(
5451
linkPathname: string,
5552
context: {
@@ -60,20 +57,45 @@ export function resolveMarkdownLinkPathname(
6057
},
6158
): string | null {
6259
const {sourceFilePath, sourceToPermalink, contentPaths, siteDir} = context;
63-
const sourceDirsToTry: string[] = [];
64-
// ./file.md and ../file.md are always relative to the current file
65-
if (!linkPathname.startsWith('./') && !linkPathname.startsWith('../')) {
66-
sourceDirsToTry.push(...getContentPathList(contentPaths), siteDir);
60+
61+
// If the link is already @site aliased, there's no need to resolve it
62+
if (linkPathname.startsWith('@site/')) {
63+
return sourceToPermalink.get(decodeURIComponent(linkPathname)) ?? null;
6764
}
68-
// /file.md is never relative to the source file path
69-
if (!linkPathname.startsWith('/')) {
70-
sourceDirsToTry.push(path.dirname(sourceFilePath));
65+
66+
// Get the dirs to "look into", ordered by priority, when resolving the link
67+
function getSourceDirsToTry() {
68+
// /file.md is always resolved from
69+
// - the plugin content paths,
70+
// - then siteDir
71+
if (linkPathname.startsWith('/')) {
72+
return [...getContentPathList(contentPaths), siteDir];
73+
}
74+
// ./file.md and ../file.md are always resolved from
75+
// - the current file dir
76+
else if (linkPathname.startsWith('./') || linkPathname.startsWith('../')) {
77+
return [path.dirname(sourceFilePath)];
78+
}
79+
// file.md is resolved from
80+
// - the current file dir,
81+
// - then from the plugin content paths,
82+
// - then siteDir
83+
else {
84+
return [
85+
path.dirname(sourceFilePath),
86+
...getContentPathList(contentPaths),
87+
siteDir,
88+
];
89+
}
7190
}
7291

73-
const aliasedSourceMatch = sourceDirsToTry
92+
const sourcesToTry = getSourceDirsToTry()
7493
.map((sourceDir) => path.join(sourceDir, decodeURIComponent(linkPathname)))
75-
.map((source) => aliasedSitePath(source, siteDir))
76-
.find((source) => sourceToPermalink.has(source));
94+
.map((source) => aliasedSitePath(source, siteDir));
95+
96+
const aliasedSourceMatch = sourcesToTry.find((source) =>
97+
sourceToPermalink.has(source),
98+
);
7799

78100
return aliasedSourceMatch
79101
? sourceToPermalink.get(aliasedSourceMatch) ?? null
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Link resolution tests
2+
3+
## Test for issue [#11099](https://github.com/facebook/docusaurus/issues/11099)
4+
5+
These links should target the root `index.mdx` file:
6+
7+
[`/index.mdx`](/index.mdx)
8+
9+
[`@site/_dogfooding/_docs tests/index.mdx`](@site/_dogfooding/_docs%20tests/index.mdx)
10+
11+
These links should target the current `index.mdx` file:
12+
13+
[`/tests/links/index.mdx`](/tests/links/resolution/index.mdx)
14+
15+
[`@site/_dogfooding/_docs tests/tests/links/resolution/index.mdx`](@site/_dogfooding/_docs%20tests/tests/links/resolution/index.mdx)
16+
17+
[`index.mdx`](index.mdx)
18+
19+
[`./index.mdx`](./index.mdx)

website/docs/guides/markdown-features/markdown-features-links.mdx

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,17 @@ Reference to another [document in a subfolder](subfolder/doc3.mdx).
2929

3030
Relative file paths are resolved against the current file's directory. Absolute file paths, on the other hand, are resolved relative to the **content root**, usually `docs/`, `blog/`, or [localized ones](../../i18n/i18n-tutorial.mdx) like `i18n/zh-Hans/plugin-content-docs/current`.
3131

32-
Absolute file paths can also be relative to the site directory. However, beware that links that begin with `/docs/` or `/blog/` are **not portable** as you would need to manually update them if you create new doc versions or localize them.
32+
Here are some examples of file path links and how they get resolved, assuming the current file is `website/docs/category/source.mdx`:
3333

34-
```md
35-
You can write [links](/otherFolder/doc4.mdx) relative to the content root (`/docs/`).
34+
- `[link](./target.mdx)` is resolved from the current file's directory `website/docs/category`.
35+
- `[link](../target.mdx)` is resolved from the parent file's directory `website/docs`.
36+
- `[link](/target.mdx)` is resolved from the docs content root `website/docs`, using in priority the localized docs.
37+
- `[link](target.mdx)` is resolved from the current directory `website/docs/category`, then from the docs content roots, then from the site root.
3638

37-
You can also write [links](/docs/otherFolder/doc4.mdx) relative to the site directory, but it's not recommended.
38-
```
39+
Absolute file paths can also be relative to the site directory. However, beware that links that begin with `/docs/`, `/blog/` or `@site/` are **not portable** as you would need to manually update them if you create new doc versions or localize them:
40+
41+
- `[link](/docs/target.mdx)` is resolved from the site root `website` (⚠️ less portable).
42+
- `[link](@site/docs/target.mdx)` is relative to the site root `website` (⚠️ less portable).
3943

4044
Using relative _file_ paths (with `.md` extensions) instead of relative _URL_ links provides the following benefits:
4145

0 commit comments

Comments
 (0)