diff --git a/errors/next-script-for-ga.md b/errors/next-script-for-ga.md
new file mode 100644
index 000000000000000..6fad6f8531d4f1e
--- /dev/null
+++ b/errors/next-script-for-ga.md
@@ -0,0 +1,70 @@
+# Next Script for Google Analytics
+
+### Why This Error Occurred
+
+An inline script was used for Google analytics which might impact your webpage's performance.
+
+### Possible Ways to Fix It
+
+#### Script component (experimental)
+
+Use the Script component with the right loading strategy to defer loading of the script until necessary.
+
+```jsx
+import Script from 'next/experimental-script'
+
+const Home = () => {
+ return (
+
+
+
+ )
+}
+
+export default Home
+```
+
+If you are using the [alternative async variant](https://developers.google.com/analytics/devguides/collection/analyticsjs#alternative_async_tag):
+
+```jsx
+import Script from 'next/experimental-script'
+
+const Home = () => {
+ return (
+
+
+
+
+ )
+}
+
+export default Home
+```
+
+Note: This is still an experimental feature and needs to be enabled via the `experimental.scriptLoader` flag in `next.config.js`.
+
+### Useful Links
+
+- [Add analytics.js to Your Site](https://developers.google.com/analytics/devguides/collection/analyticsjs)
+- [Efficiently load third-party JavaScript](https://web.dev/efficiently-load-third-party-javascript/)
diff --git a/packages/eslint-plugin-next/lib/rules/next-script-for-ga.js b/packages/eslint-plugin-next/lib/rules/next-script-for-ga.js
new file mode 100644
index 000000000000000..f3333dfbcc94e44
--- /dev/null
+++ b/packages/eslint-plugin-next/lib/rules/next-script-for-ga.js
@@ -0,0 +1,60 @@
+const NodeAttributes = require('../utils/nodeAttributes.js')
+
+const GA_URL = 'www.google-analytics.com/analytics.js'
+const ERROR_MSG =
+ 'Use the Script component for loading third party scripts. See: https://nextjs.org/docs/messages/next-script-for-ga.'
+
+module.exports = {
+ meta: {
+ docs: {
+ description:
+ 'Prefer next script component when using the inline script for Google Analytics',
+ recommended: true,
+ },
+ },
+ create: function (context) {
+ return {
+ JSXOpeningElement(node) {
+ if (node.name.name !== 'script') {
+ return
+ }
+ if (node.attributes.length === 0) {
+ return
+ }
+ const attributes = new NodeAttributes(node)
+
+ // Check if the Alternative async tag is being used to add GA. https://developers.google.com/analytics/devguides/collection/analyticsjs#alternative_async_tag
+ if (
+ typeof attributes.value('src') === 'string' &&
+ attributes.value('src').includes(GA_URL)
+ ) {
+ return context.report({
+ node,
+ message: ERROR_MSG,
+ })
+ }
+
+ // Check if inline script is being used to add GA. https://developers.google.com/analytics/devguides/collection/analyticsjs#the_google_analytics_tag
+ if (
+ attributes.has('dangerouslySetInnerHTML') &&
+ attributes.value('dangerouslySetInnerHTML')[0]
+ ) {
+ const htmlContent = attributes.value('dangerouslySetInnerHTML')[0]
+ .value.quasis[0].value.raw
+
+ if (
+ htmlContent &&
+ htmlContent.includes('www.google-analytics.com/analytics.js')
+ ) {
+ context.report({
+ node,
+ message: ERROR_MSG,
+ })
+ }
+ }
+ },
+ }
+ },
+}
+
+module.exports.schema = []
diff --git a/packages/eslint-plugin-next/lib/utils/nodeAttributes.js b/packages/eslint-plugin-next/lib/utils/nodeAttributes.js
index c9eb583275c8d21..3367f31a264635a 100644
--- a/packages/eslint-plugin-next/lib/utils/nodeAttributes.js
+++ b/packages/eslint-plugin-next/lib/utils/nodeAttributes.js
@@ -26,7 +26,9 @@ class NodeAttributes {
this.attributes[attribute.name.name].value = attribute.value.value
} else if (attribute.value.expression) {
this.attributes[attribute.name.name].value =
- attribute.value.expression.value
+ typeof attribute.value.expression.value !== 'undefined'
+ ? attribute.value.expression.value
+ : attribute.value.expression.properties
}
}
})
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
new file mode 100644
index 000000000000000..36f8a7421c43017
--- /dev/null
+++ b/test/eslint-plugin-next/next-script-for-ga.unit.test.js
@@ -0,0 +1,129 @@
+const rule = require('@next/eslint-plugin-next/lib/rules/next-script-for-ga')
+
+const RuleTester = require('eslint').RuleTester
+
+const ERROR_MSG =
+ 'Use the Script component for loading third party scripts. See: https://nextjs.org/docs/messages/next-script-for-ga.'
+
+RuleTester.setDefaultConfig({
+ parserOptions: {
+ ecmaVersion: 2018,
+ sourceType: 'module',
+ ecmaFeatures: {
+ modules: true,
+ jsx: true,
+ },
+ },
+})
+
+var ruleTester = new RuleTester()
+ruleTester.run('sync-scripts', rule, {
+ valid: [
+ `import Script from 'next/experimental-script'
+
+ export class Blah extends Head {
+ render() {
+ return (
+
+
Hello title
+
+
+ );
+ }
+ }`,
+ `import Script from 'next/experimental-script'
+
+ export class Blah extends Head {
+ render() {
+ return (
+
+
Hello title
+
+
+ );
+ }
+ }`,
+ `export class Blah extends Head {
+ render() {
+ return (
+
+
Hello title
+
+
+ );
+ }
+ }`,
+ ],
+
+ invalid: [
+ {
+ code: `
+ export class Blah extends Head {
+ render() {
+ return (
+
+
Hello title
+
+
+ );
+ }
+ }`,
+ errors: [
+ {
+ message: ERROR_MSG,
+ type: 'JSXOpeningElement',
+ },
+ ],
+ },
+ {
+ code: `
+ export class Blah extends Head {
+ render() {
+ return (
+
+
Hello title
+
+
+
+ );
+ }
+ }`,
+ errors: [
+ {
+ message: ERROR_MSG,
+ type: 'JSXOpeningElement',
+ },
+ ],
+ },
+ ],
+})