diff --git a/.eslintrc.js b/.eslintrc.js
index c76cc234dc9f..56851d0247c3 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -374,6 +374,7 @@ module.exports = {
// locals must be justified with a disable comment.
'@typescript-eslint/no-unused-vars': [ERROR, {ignoreRestSiblings: true}],
'@typescript-eslint/prefer-optional-chain': ERROR,
+ '@docusaurus/no-html-links': ERROR,
'@docusaurus/no-untranslated-text': [
WARNING,
{
diff --git a/packages/docusaurus-theme-classic/src/theme/EditThisPage/index.tsx b/packages/docusaurus-theme-classic/src/theme/EditThisPage/index.tsx
index ede0b5cbed62..1fbfb173ab5f 100644
--- a/packages/docusaurus-theme-classic/src/theme/EditThisPage/index.tsx
+++ b/packages/docusaurus-theme-classic/src/theme/EditThisPage/index.tsx
@@ -8,22 +8,19 @@
import React from 'react';
import Translate from '@docusaurus/Translate';
import {ThemeClassNames} from '@docusaurus/theme-common';
+import Link from '@docusaurus/Link';
import IconEdit from '@theme/Icon/Edit';
import type {Props} from '@theme/EditThisPage';
export default function EditThisPage({editUrl}: Props): JSX.Element {
return (
-
+
Edit this page
-
+
);
}
diff --git a/packages/docusaurus-theme-classic/src/theme/Heading/index.tsx b/packages/docusaurus-theme-classic/src/theme/Heading/index.tsx
index 3bb321285593..00ef3f52da13 100644
--- a/packages/docusaurus-theme-classic/src/theme/Heading/index.tsx
+++ b/packages/docusaurus-theme-classic/src/theme/Heading/index.tsx
@@ -9,6 +9,7 @@ import React from 'react';
import clsx from 'clsx';
import {translate} from '@docusaurus/Translate';
import {useThemeConfig} from '@docusaurus/theme-common';
+import Link from '@docusaurus/Link';
import type {Props} from '@theme/Heading';
import styles from './styles.module.css';
@@ -34,16 +35,16 @@ export default function Heading({as: As, id, ...props}: Props): JSX.Element {
)}
id={id}>
{props.children}
-
-
+
);
}
diff --git a/packages/docusaurus-theme-classic/src/theme/SkipToContent/index.tsx b/packages/docusaurus-theme-classic/src/theme/SkipToContent/index.tsx
index 0ae5c02a559e..3200aeec7277 100644
--- a/packages/docusaurus-theme-classic/src/theme/SkipToContent/index.tsx
+++ b/packages/docusaurus-theme-classic/src/theme/SkipToContent/index.tsx
@@ -7,7 +7,6 @@
import React from 'react';
import {SkipToContentLink} from '@docusaurus/theme-common';
-
import styles from './styles.module.css';
export default function SkipToContent(): JSX.Element {
diff --git a/packages/docusaurus-theme-classic/src/theme/TOCItems/Tree.tsx b/packages/docusaurus-theme-classic/src/theme/TOCItems/Tree.tsx
index 04ab3fc93bad..266feaba2c1b 100644
--- a/packages/docusaurus-theme-classic/src/theme/TOCItems/Tree.tsx
+++ b/packages/docusaurus-theme-classic/src/theme/TOCItems/Tree.tsx
@@ -6,6 +6,7 @@
*/
import React from 'react';
+import Link from '@docusaurus/Link';
import type {Props} from '@theme/TOCItems/Tree';
// Recursive component rendering the toc tree
@@ -22,12 +23,10 @@ function TOCItemTree({
{toc.map((heading) => (
-
- {/* eslint-disable-next-line jsx-a11y/control-has-associated-label */}
-
+ {/* eslint-disable-next-line @docusaurus/no-html-links */}
-
-
+
diff --git a/packages/docusaurus/src/client/exports/Link.tsx b/packages/docusaurus/src/client/exports/Link.tsx
index 10ee0ff4962e..4a7453dfef8f 100644
--- a/packages/docusaurus/src/client/exports/Link.tsx
+++ b/packages/docusaurus/src/client/exports/Link.tsx
@@ -148,7 +148,7 @@ function Link(
}
return isRegularHtmlLink ? (
- // eslint-disable-next-line jsx-a11y/anchor-has-content
+ // eslint-disable-next-line jsx-a11y/anchor-has-content, @docusaurus/no-html-links
test',
+ },
+ {
+ code: 'Twitter',
+ },
+ {
+ code: 'Twitter',
+ options: [{ignoreFullyResolved: true}],
+ },
+ {
+ code: 'Twitter',
+ options: [{ignoreFullyResolved: true}],
+ },
+ {
+ code: 'Contact ',
+ options: [{ignoreFullyResolved: true}],
+ },
+ {
+ code: 'Call',
+ options: [{ignoreFullyResolved: true}],
+ },
+ ],
+ invalid: [
+ {
+ code: 'test',
+ errors: errorsJSX,
+ },
+ {
+ code: 'test',
+ errors: errorsJSX,
+ },
+ {
+ code: 'test',
+ errors: errorsJSX,
+ },
+ {
+ code: 'Contact ',
+ errors: errorsJSX,
+ },
+ {
+ code: 'Call',
+ errors: errorsJSX,
+ },
+ {
+ code: 'Twitter',
+ errors: errorsJSX,
+ },
+ {
+ code: 'Twitter',
+ errors: errorsJSX,
+ },
+ {
+ code: 'Twitter',
+ options: [{ignoreFullyResolved: true}],
+ errors: errorsJSX,
+ },
+ {
+ // TODO we might want to make this test pass
+ // Can template literals be statically pre-evaluated? (Babel can do it)
+ // eslint-disable-next-line no-template-curly-in-string
+ code: 'Twitter',
+ options: [{ignoreFullyResolved: true}],
+ errors: errorsJSX,
+ },
+ ],
+});
diff --git a/packages/eslint-plugin/src/rules/index.ts b/packages/eslint-plugin/src/rules/index.ts
index 70a83f55a79b..85409ddfb473 100644
--- a/packages/eslint-plugin/src/rules/index.ts
+++ b/packages/eslint-plugin/src/rules/index.ts
@@ -5,10 +5,12 @@
* LICENSE file in the root directory of this source tree.
*/
+import noHtmlLinks from './no-html-links';
import noUntranslatedText from './no-untranslated-text';
import stringLiteralI18nMessages from './string-literal-i18n-messages';
export default {
'no-untranslated-text': noUntranslatedText,
'string-literal-i18n-messages': stringLiteralI18nMessages,
+ 'no-html-links': noHtmlLinks,
};
diff --git a/packages/eslint-plugin/src/rules/no-html-links.ts b/packages/eslint-plugin/src/rules/no-html-links.ts
new file mode 100644
index 000000000000..ca40a486ff16
--- /dev/null
+++ b/packages/eslint-plugin/src/rules/no-html-links.ts
@@ -0,0 +1,103 @@
+/**
+ * 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 {createRule} from '../util';
+import type {TSESTree} from '@typescript-eslint/types/dist/ts-estree';
+
+const docsUrl = 'https://docusaurus.io/docs/docusaurus-core#link';
+
+type Options = [
+ {
+ ignoreFullyResolved: boolean;
+ },
+];
+
+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 (e) {}
+ return false;
+}
+
+export default createRule({
+ name: 'no-html-links',
+ meta: {
+ type: 'problem',
+ docs: {
+ description: 'enforce using Docusaurus Link component instead of tag',
+ recommended: false,
+ },
+ schema: [
+ {
+ type: 'object',
+ properties: {
+ ignoreFullyResolved: {
+ type: 'boolean',
+ },
+ },
+ additionalProperties: false,
+ },
+ ],
+ messages: {
+ link: `Do not use an \`\` element to navigate. Use the \`\` component from \`@docusaurus/Link\` instead. See: ${docsUrl}`,
+ },
+ },
+ defaultOptions: [
+ {
+ ignoreFullyResolved: false,
+ },
+ ],
+
+ create(context, [options]) {
+ const {ignoreFullyResolved} = options;
+
+ return {
+ JSXOpeningElement(node) {
+ if ((node.name as TSESTree.JSXIdentifier).name !== 'a') {
+ return;
+ }
+
+ if (ignoreFullyResolved) {
+ const hrefAttr = node.attributes.find(
+ (attr): attr is TSESTree.JSXAttribute =>
+ attr.type === 'JSXAttribute' && attr.name.name === 'href',
+ );
+
+ if (hrefAttr?.value?.type === 'Literal') {
+ if (isFullyResolvedUrl(String(hrefAttr.value.value))) {
+ return;
+ }
+ }
+ if (hrefAttr?.value?.type === 'JSXExpressionContainer') {
+ const container: TSESTree.JSXExpressionContainer = hrefAttr.value;
+ const {expression} = container;
+ if (expression.type === 'TemplateLiteral') {
+ // Simple static string template literals
+ if (
+ expression.expressions.length === 0 &&
+ expression.quasis.length === 1 &&
+ expression.quasis[0]?.type === 'TemplateElement' &&
+ isFullyResolvedUrl(String(expression.quasis[0].value.raw))
+ ) {
+ return;
+ }
+ // TODO add more complex TemplateLiteral cases here
+ }
+ }
+ }
+
+ context.report({node, messageId: 'link'});
+ },
+ };
+ },
+});
diff --git a/website/_dogfooding/_pages tests/hydration-tests.tsx b/website/_dogfooding/_pages tests/hydration-tests.tsx
index 4a133497b65d..3bbefc07817d 100644
--- a/website/_dogfooding/_pages tests/hydration-tests.tsx
+++ b/website/_dogfooding/_pages tests/hydration-tests.tsx
@@ -6,28 +6,17 @@
*/
import React from 'react';
+import Link from '@docusaurus/Link';
import Layout from '@theme/Layout';
// Repro for hydration issue https://github.com/facebook/docusaurus/issues/5617
function BuggyText() {
return (
- Built using the{' '}
-
- Electron
- {' '}
- , based on{' '}
-
- Chromium
-
- , and written using{' '}
-
- TypeScript
- {' '}
- , Xplorer promises you an unprecedented experience.
+ Built using the Electron ,
+ based on Chromium, and written
+ using TypeScript ,
+ Xplorer promises you an unprecedented experience.
);
}
diff --git a/website/docs/api/misc/eslint-plugin/README.md b/website/docs/api/misc/eslint-plugin/README.md
index 436355a025e3..efaaea12765c 100644
--- a/website/docs/api/misc/eslint-plugin/README.md
+++ b/website/docs/api/misc/eslint-plugin/README.md
@@ -52,6 +52,7 @@ For more fine-grained control, you can also enable the plugin manually and confi
| --- | --- | --- |
| [`@docusaurus/no-untranslated-text`](./no-untranslated-text.md) | Enforce text labels in JSX to be wrapped by translate calls | |
| [`@docusaurus/string-literal-i18n-messages`](./string-literal-i18n-messages.md) | Enforce translate APIs to be called on plain text labels | ✅ |
+| [`@docusaurus/no-html-links`](./no-html-links.md) | Ensures @docusaurus/Link is used instead of `` tags | ✅ |
✅ = recommended
diff --git a/website/docs/api/misc/eslint-plugin/no-html-links.md b/website/docs/api/misc/eslint-plugin/no-html-links.md
new file mode 100644
index 000000000000..4b2a663b3b2a
--- /dev/null
+++ b/website/docs/api/misc/eslint-plugin/no-html-links.md
@@ -0,0 +1,45 @@
+---
+slug: /api/misc/@docusaurus/eslint-plugin/no-html-links
+---
+
+# no-html-links
+
+Ensure that the Docusaurus [``](../../../docusaurus-core.md#link) component is used instead of `` tags.
+
+The `` component has prefetching and preloading built-in. It also does build-time broken link detection, and helps Docusaurus understand your site's structure better.
+
+## Rule Details {#details}
+
+Examples of **incorrect** code for this rule:
+
+```html
+go to page!
+
+Twitter
+```
+
+Examples of **correct** code for this rule:
+
+```js
+import Link from '@docusaurus/Link'
+
+go to page!
+
+Twitter
+```
+
+## Rule Configuration {#configuration}
+
+Accepted fields:
+
+```mdx-code-block
+
+```
+
+| Option | Type | Default | Description |
+| --- | --- | --- | --- |
+| `ignoreFullyResolved` | `boolean` | `false` | Set to true will not report any `` tags with absolute URLs including a protocol. |
+
+```mdx-code-block
+
+```
diff --git a/website/src/components/HackerNewsIcon.tsx b/website/src/components/HackerNewsIcon.tsx
index 1535ae6e6fa8..4d27e76713f9 100644
--- a/website/src/components/HackerNewsIcon.tsx
+++ b/website/src/components/HackerNewsIcon.tsx
@@ -6,6 +6,7 @@
*/
import React from 'react';
+import Link from '@docusaurus/Link';
export default function HackerNewsIcon({
size = 54,
@@ -13,10 +14,8 @@ export default function HackerNewsIcon({
size?: number;
}): JSX.Element {
return (
-
-
+
);
}
diff --git a/website/src/components/ProductHuntCard.tsx b/website/src/components/ProductHuntCard.tsx
index abd4b1ed8363..c7ecbcc9735b 100644
--- a/website/src/components/ProductHuntCard.tsx
+++ b/website/src/components/ProductHuntCard.tsx
@@ -7,6 +7,7 @@
import type {ComponentProps} from 'react';
import React from 'react';
+import Link from '@docusaurus/Link';
export default function ProductHuntCard({
className,
@@ -16,10 +17,8 @@ export default function ProductHuntCard({
style?: ComponentProps<'a'>['style'];
}): JSX.Element {
return (
-
-
+
);
}
diff --git a/website/src/components/TeamProfileCards/index.tsx b/website/src/components/TeamProfileCards/index.tsx
index d1b3d506291b..9738cb42ccdb 100644
--- a/website/src/components/TeamProfileCards/index.tsx
+++ b/website/src/components/TeamProfileCards/index.tsx
@@ -53,14 +53,14 @@ function TeamProfileCard({
diff --git a/website/src/components/Tweet/index.tsx b/website/src/components/Tweet/index.tsx
index 415f1e21bffe..3be8a1307545 100644
--- a/website/src/components/Tweet/index.tsx
+++ b/website/src/components/Tweet/index.tsx
@@ -9,6 +9,7 @@ import React, {type ReactNode} from 'react';
import clsx from 'clsx';
+import Link from '@docusaurus/Link';
import styles from './styles.module.css';
export interface Props {
@@ -50,9 +51,9 @@ export default function Tweet({
{content}
);
diff --git a/website/src/components/TweetQuote/index.tsx b/website/src/components/TweetQuote/index.tsx
index bb96ac1a70dd..8be85be8d900 100644
--- a/website/src/components/TweetQuote/index.tsx
+++ b/website/src/components/TweetQuote/index.tsx
@@ -9,6 +9,7 @@ import React, {type ReactNode} from 'react';
import clsx from 'clsx';
+import Link from '@docusaurus/Link';
import styles from './styles.module.css';
export interface Props {
@@ -31,12 +32,10 @@ export default function TweetQuote({
return (
);
diff --git a/website/src/components/Versions.tsx b/website/src/components/Versions.tsx
index 1a32226cf3a4..0dba8c7e26b2 100644
--- a/website/src/components/Versions.tsx
+++ b/website/src/components/Versions.tsx
@@ -15,6 +15,7 @@ import React, {
import {useDocsPreferredVersion} from '@docusaurus/theme-common';
import {useVersions} from '@docusaurus/plugin-content-docs/client';
import Translate from '@docusaurus/Translate';
+import Link from '@docusaurus/Link';
import CodeBlock from '@theme/CodeBlock';
type ContextValue = {
@@ -113,9 +114,9 @@ export function StableMajorVersion(): JSX.Element {
function GitBranchLink({branch}: {branch: string}): JSX.Element {
return (
-
+
{branch}
-
+
);
}
diff --git a/website/src/pages/showcase/index.tsx b/website/src/pages/showcase/index.tsx
index a62ea419bf97..489027fad3cd 100644
--- a/website/src/pages/showcase/index.tsx
+++ b/website/src/pages/showcase/index.tsx
@@ -12,6 +12,7 @@ import Translate, {translate} from '@docusaurus/Translate';
import {useHistory, useLocation} from '@docusaurus/router';
import {usePluralForm} from '@docusaurus/theme-common';
+import Link from '@docusaurus/Link';
import Layout from '@theme/Layout';
import FavoriteIcon from '@site/src/components/svgIcons/FavoriteIcon';
import {
@@ -123,15 +124,11 @@ function ShowcaseHeader() {
{TITLE}
{DESCRIPTION}
-
+
-
+
);
}
diff --git a/website/src/pages/versions.tsx b/website/src/pages/versions.tsx
index 5e748d6571c4..8b3ba21a3486 100644
--- a/website/src/pages/versions.tsx
+++ b/website/src/pages/versions.tsx
@@ -81,9 +81,9 @@ export default function Version(): JSX.Element {
-
+
-
+
|