diff --git a/docs/basic-features/eslint.md b/docs/basic-features/eslint.md
index dc4c587de73c..736bbed3c7b2 100644
--- a/docs/basic-features/eslint.md
+++ b/docs/basic-features/eslint.md
@@ -94,6 +94,7 @@ Next.js provides an ESLint plugin, [`eslint-plugin-next`](https://www.npmjs.com/
| ✔️ | [next/no-sync-scripts](https://nextjs.org/docs/messages/no-sync-scripts) | Forbid synchronous scripts |
| ✔️ | [next/no-title-in-document-head](https://nextjs.org/docs/messages/no-title-in-document-head) | Disallow using <title> with Head from next/document |
| ✔️ | [next/no-unwanted-polyfillio](https://nextjs.org/docs/messages/no-unwanted-polyfillio) | Prevent duplicate polyfills from Polyfill.io |
+| ✔️ | [next/inline-script-id](https://nextjs.org/docs/messages/inline-script-id) | Enforce id attribute on next/script components with inline content |
| ✔️ | next/no-typos | Ensure no typos were made declaring [Next.js's data fetching function](https://nextjs.org/docs/basic-features/data-fetching) |
| ✔️ | [next/next-script-for-ga](https://nextjs.org/docs/messages/next-script-for-ga) | Use the Script component to defer loading of the script until necessary. |
diff --git a/docs/basic-features/script.md b/docs/basic-features/script.md
index 1d4a0b302e13..ef688170332e 100644
--- a/docs/basic-features/script.md
+++ b/docs/basic-features/script.md
@@ -131,13 +131,14 @@ export default function Home() {
```js
import Script from 'next/script'
-
// or
+
+ >
+ )
+}
+```
+
+## Useful links
+
+- [Docs for Next.js Script component](https://nextjs.org/docs/basic-features/script)
diff --git a/errors/manifest.json b/errors/manifest.json
index 5645b9cd13e8..d56d23abdcad 100644
--- a/errors/manifest.json
+++ b/errors/manifest.json
@@ -145,6 +145,10 @@
"title": "incompatible-href-as",
"path": "/errors/incompatible-href-as.md"
},
+ {
+ "title": "inline-script-id",
+ "path": "/errors/inline-script-id.md"
+ },
{ "title": "install-sass", "path": "/errors/install-sass.md" },
{ "title": "install-sharp", "path": "/errors/install-sharp.md" },
{
diff --git a/errors/next-script-for-ga.md b/errors/next-script-for-ga.md
index 15d400c098bd..2f367dff127c 100644
--- a/errors/next-script-for-ga.md
+++ b/errors/next-script-for-ga.md
@@ -20,8 +20,8 @@ const Home = () => {
-
+ />
)
}
diff --git a/packages/eslint-plugin-next/lib/index.js b/packages/eslint-plugin-next/lib/index.js
index ae474796056c..6098fa257017 100644
--- a/packages/eslint-plugin-next/lib/index.js
+++ b/packages/eslint-plugin-next/lib/index.js
@@ -16,6 +16,7 @@ module.exports = {
'no-script-in-head': require('./rules/no-script-in-head'),
'no-typos': require('./rules/no-typos'),
'no-duplicate-head': require('./rules/no-duplicate-head'),
+ 'inline-script-id': require('./rules/inline-script-id'),
'next-script-for-ga': require('./rules/next-script-for-ga'),
},
configs: {
@@ -39,6 +40,7 @@ module.exports = {
'@next/next/no-script-in-head': 2,
'@next/next/no-typos': 1,
'@next/next/no-duplicate-head': 2,
+ '@next/next/inline-script-id': 2,
},
},
'core-web-vitals': {
diff --git a/packages/eslint-plugin-next/lib/rules/inline-script-id.js b/packages/eslint-plugin-next/lib/rules/inline-script-id.js
new file mode 100644
index 000000000000..70843a2be1fb
--- /dev/null
+++ b/packages/eslint-plugin-next/lib/rules/inline-script-id.js
@@ -0,0 +1,48 @@
+module.exports = {
+ meta: {
+ docs: {
+ description:
+ 'next/script components with inline content must specify an `id` attribute.',
+ recommended: true,
+ },
+ },
+ create: function (context) {
+ let nextScriptImportName = null
+
+ return {
+ ImportDeclaration(node) {
+ if (node.source.value === 'next/script') {
+ nextScriptImportName = node.specifiers[0].local.name
+ }
+ },
+ JSXElement(node) {
+ if (nextScriptImportName == null) return
+
+ if (
+ node.openingElement &&
+ node.openingElement.name &&
+ node.openingElement.name.name !== nextScriptImportName
+ ) {
+ return
+ }
+
+ const attributes = node.openingElement.attributes
+
+ if (
+ node.children.length > 0 ||
+ attributes.some(
+ (attribute) => attribute.name.name === 'dangerouslySetInnerHTML'
+ )
+ ) {
+ if (!attributes.some((attribute) => attribute.name.name === 'id')) {
+ context.report({
+ node,
+ message:
+ 'next/script components with inline content must specify an `id` attribute. See: https://nextjs.org/docs/messages/inline-script-id',
+ })
+ }
+ }
+ },
+ }
+ },
+}
diff --git a/test/eslint-plugin-next/inline-script-id.unit.test.js b/test/eslint-plugin-next/inline-script-id.unit.test.js
new file mode 100644
index 000000000000..222b78b71cd1
--- /dev/null
+++ b/test/eslint-plugin-next/inline-script-id.unit.test.js
@@ -0,0 +1,156 @@
+const rule = require('@next/eslint-plugin-next/lib/rules/inline-script-id')
+
+const RuleTester = require('eslint').RuleTester
+
+RuleTester.setDefaultConfig({
+ parserOptions: {
+ ecmaVersion: 2018,
+ sourceType: 'module',
+ ecmaFeatures: {
+ modules: true,
+ jsx: true,
+ },
+ },
+})
+
+const errorMessage =
+ 'next/script components with inline content must specify an `id` attribute. See: https://nextjs.org/docs/messages/inline-script-id'
+
+const ruleTester = new RuleTester()
+ruleTester.run('inline-script-id', rule, {
+ valid: [
+ {
+ code: `import Script from 'next/script';
+
+ export default function TestPage() {
+ return (
+
+ )
+ }`,
+ },
+ {
+ code: `import Script from 'next/script';
+
+ export default function TestPage() {
+ return (
+
+ )
+ }`,
+ },
+ {
+ code: `import Script from 'next/script';
+
+ export default function TestPage() {
+ return (
+
+ )
+ }`,
+ },
+ {
+ code: `import MyScript from 'next/script';
+
+ export default function TestPage() {
+ return (
+
+ {\`console.log('Hello world');\`}
+
+ )
+ }`,
+ },
+ {
+ code: `import MyScript from 'next/script';
+
+ export default function TestPage() {
+ return (
+
+ )
+ }`,
+ },
+ ],
+ invalid: [
+ {
+ code: `import Script from 'next/script';
+
+ export default function TestPage() {
+ return (
+
+ )
+ }`,
+ errors: [
+ {
+ message: errorMessage,
+ type: 'JSXElement',
+ },
+ ],
+ },
+ {
+ code: `import Script from 'next/script';
+
+ export default function TestPage() {
+ return (
+
+ )
+ }`,
+ errors: [
+ {
+ message: errorMessage,
+ type: 'JSXElement',
+ },
+ ],
+ },
+ {
+ code: `import MyScript from 'next/script';
+
+ export default function TestPage() {
+ return (
+
+ {\`console.log('Hello world');\`}
+
+ )
+ }`,
+ errors: [
+ {
+ message: errorMessage,
+ type: 'JSXElement',
+ },
+ ],
+ },
+ {
+ code: `import MyScript from 'next/script';
+
+ export default function TestPage() {
+ return (
+
+ )
+ }`,
+ errors: [
+ {
+ message: errorMessage,
+ type: 'JSXElement',
+ },
+ ],
+ },
+ ],
+})
diff --git a/test/eslint-plugin-next/next-script-for-ga.unit.test.js b/test/eslint-plugin-next/next-script-for-ga.unit.test.js
index 18b156239c02..eb4c01fd9272 100644
--- a/test/eslint-plugin-next/next-script-for-ga.unit.test.js
+++ b/test/eslint-plugin-next/next-script-for-ga.unit.test.js
@@ -29,8 +29,8 @@ ruleTester.run('sync-scripts', rule, {
-