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

+ +
+ ); + } + }`, + errors: [ + { + message: ERROR_MSG, + type: 'JSXOpeningElement', + }, + ], + }, + ], +})