diff --git a/errors/google-font-display.md b/errors/google-font-display.md
new file mode 100644
index 0000000000000..61328e8976282
--- /dev/null
+++ b/errors/google-font-display.md
@@ -0,0 +1,36 @@
+# Google Font Display
+
+### Why This Error Occurred
+
+For a Google Font, the `display` descriptor was either not assigned or set to `auto`, `fallback`, or `block`.
+
+### Possible Ways to Fix It
+
+For most cases, the best font display strategy for custom fonts is `optional`.
+
+```jsx
+import Head from 'next/head'
+
+export default function IndexPage() {
+ return (
+
+
+
+
+
+ )
+}
+```
+
+Specifying `display=optional` minimizes the risk of invisible text or layout shift. If swapping to the custom font after it has loaded is important to you, then use `display=swap` instead.
+
+### When Not To Use It
+
+If you want to specifically display a font using a `block` or `fallback` strategy, then you can disable this rule.
+
+### Useful Links
+
+- [Font-display](https://font-display.glitch.me/)
diff --git a/errors/google-font-preconnect.md b/errors/google-font-preconnect.md
new file mode 100644
index 0000000000000..de629ac4e1bf3
--- /dev/null
+++ b/errors/google-font-preconnect.md
@@ -0,0 +1,17 @@
+# Google Font Preconnect
+
+### Why This Error Occurred
+
+A preconnect resource hint was not used with a request to the Google Fonts domain. Adding `preconnect` is recommended to initiate an early connection to the origin.
+
+### Possible Ways to Fix It
+
+Add `rel="preconnect"` to the Google Font domain `` tag:
+
+```jsx
+
+```
+
+### Useful Links
+
+- [Preconnect to required origins](https://web.dev/uses-rel-preconnect/)
diff --git a/errors/manifest.json b/errors/manifest.json
index 70862c14293a0..593779eb6fc25 100644
--- a/errors/manifest.json
+++ b/errors/manifest.json
@@ -106,6 +106,14 @@
"title": "generatebuildid-not-a-string",
"path": "/errors/generatebuildid-not-a-string.md"
},
+ {
+ "title": "google-font-display",
+ "path": "/errors/google-font-display.md"
+ },
+ {
+ "title": "google-font-preconnect",
+ "path": "/errors/google-font-preconnect.md"
+ },
{
"title": "get-initial-props-as-an-instance-method",
"path": "/errors/get-initial-props-as-an-instance-method.md"
diff --git a/packages/eslint-plugin-next/lib/index.js b/packages/eslint-plugin-next/lib/index.js
index 4565a833fe11d..3212e7a21e8a3 100644
--- a/packages/eslint-plugin-next/lib/index.js
+++ b/packages/eslint-plugin-next/lib/index.js
@@ -5,6 +5,8 @@ module.exports = {
'no-html-link-for-pages': require('./rules/no-html-link-for-pages'),
'no-unwanted-polyfillio': require('./rules/no-unwanted-polyfillio'),
'no-title-in-document-head': require('./rules/no-title-in-document-head'),
+ 'google-font-display': require('./rules/google-font-display'),
+ 'google-font-preconnect': require('./rules/google-font-preconnect'),
},
configs: {
recommended: {
@@ -15,6 +17,8 @@ module.exports = {
'@next/next/no-html-link-for-pages': 1,
'@next/next/no-unwanted-polyfillio': 1,
'@next/next/no-title-in-document-head': 1,
+ '@next/next/google-font-display': 1,
+ '@next/next/google-font-preconnect': 1,
},
},
},
diff --git a/packages/eslint-plugin-next/lib/rules/google-font-display.js b/packages/eslint-plugin-next/lib/rules/google-font-display.js
new file mode 100644
index 0000000000000..af6529f407e12
--- /dev/null
+++ b/packages/eslint-plugin-next/lib/rules/google-font-display.js
@@ -0,0 +1,56 @@
+const NodeAttributes = require('../utils/nodeAttributes.js')
+
+module.exports = {
+ meta: {
+ docs: {
+ description:
+ 'Ensure correct font-display property is assigned for Google Fonts',
+ recommended: true,
+ },
+ },
+ create: function (context) {
+ return {
+ JSXOpeningElement(node) {
+ let message
+
+ if (node.name.name !== 'link') {
+ return
+ }
+
+ const attributes = new NodeAttributes(node)
+ if (!attributes.has('href') || !attributes.hasValue('href')) {
+ return
+ }
+
+ const hrefValue = attributes.value('href')
+ const isGoogleFont = hrefValue.includes(
+ 'https://fonts.googleapis.com/css'
+ )
+
+ if (isGoogleFont) {
+ const params = new URLSearchParams(hrefValue.split('?')[1])
+ const displayValue = params.get('display')
+
+ if (!params.has('display')) {
+ message = 'Display parameter is missing.'
+ } else if (
+ displayValue === 'block' ||
+ displayValue === 'fallback' ||
+ displayValue === 'auto'
+ ) {
+ message = `${
+ displayValue[0].toUpperCase() + displayValue.slice(1)
+ } behavior is not recommended.`
+ }
+ }
+
+ if (message) {
+ context.report({
+ node,
+ message: `${message} See https://nextjs.org/docs/messages/google-font-display.`,
+ })
+ }
+ },
+ }
+ },
+}
diff --git a/packages/eslint-plugin-next/lib/rules/google-font-preconnect.js b/packages/eslint-plugin-next/lib/rules/google-font-preconnect.js
new file mode 100644
index 0000000000000..acf8b6f1a50ee
--- /dev/null
+++ b/packages/eslint-plugin-next/lib/rules/google-font-preconnect.js
@@ -0,0 +1,40 @@
+const NodeAttributes = require('../utils/nodeAttributes.js')
+
+module.exports = {
+ meta: {
+ docs: {
+ description: 'Ensure preconnect is used with Google Fonts',
+ recommended: true,
+ },
+ },
+ create: function (context) {
+ return {
+ JSXOpeningElement(node) {
+ if (node.name.name !== 'link') {
+ return
+ }
+
+ const attributes = new NodeAttributes(node)
+ if (!attributes.has('href') || !attributes.hasValue('href')) {
+ return
+ }
+
+ const hrefValue = attributes.value('href')
+ const preconnectMissing =
+ !attributes.has('rel') ||
+ !attributes.hasValue('rel') ||
+ attributes.value('rel') !== 'preconnect'
+
+ if (
+ hrefValue.includes('https://fonts.gstatic.com') &&
+ preconnectMissing
+ ) {
+ context.report({
+ node,
+ message: `Preconnect is missing. See https://nextjs.org/docs/messages/google-font-preconnect.`,
+ })
+ }
+ },
+ }
+ },
+}
diff --git a/packages/eslint-plugin-next/lib/utils/nodeAttributes.js b/packages/eslint-plugin-next/lib/utils/nodeAttributes.js
new file mode 100644
index 0000000000000..c9eb583275c8d
--- /dev/null
+++ b/packages/eslint-plugin-next/lib/utils/nodeAttributes.js
@@ -0,0 +1,52 @@
+// Return attributes and values of a node in a convenient way:
+/* example:
+
+ { attr1: {
+ hasValue: true,
+ value: 15
+ },
+ attr2: {
+ hasValue: false
+ }
+Inclusion of hasValue is in case an eslint rule cares about boolean values
+explicitely assigned to attribute vs the attribute being used as a flag
+*/
+class NodeAttributes {
+ constructor(ASTnode) {
+ this.attributes = {}
+ ASTnode.attributes.forEach((attribute) => {
+ if (!attribute.type || attribute.type !== 'JSXAttribute') {
+ return
+ }
+ this.attributes[attribute.name.name] = {
+ hasValue: !!attribute.value,
+ }
+ if (attribute.value) {
+ if (attribute.value.value) {
+ this.attributes[attribute.name.name].value = attribute.value.value
+ } else if (attribute.value.expression) {
+ this.attributes[attribute.name.name].value =
+ attribute.value.expression.value
+ }
+ }
+ })
+ }
+ hasAny() {
+ return !!Object.keys(this.attributes).length
+ }
+ has(attrName) {
+ return !!this.attributes[attrName]
+ }
+ hasValue(attrName) {
+ return !!this.attributes[attrName].hasValue
+ }
+ value(attrName) {
+ if (!this.attributes[attrName]) {
+ return true
+ }
+
+ return this.attributes[attrName].value
+ }
+}
+
+module.exports = NodeAttributes
diff --git a/test/eslint-plugin-next/google-font-display.unit.test.js b/test/eslint-plugin-next/google-font-display.unit.test.js
new file mode 100644
index 0000000000000..7f1bca556b1cd
--- /dev/null
+++ b/test/eslint-plugin-next/google-font-display.unit.test.js
@@ -0,0 +1,143 @@
+const rule = require('@next/eslint-plugin-next/lib/rules/google-font-display')
+const RuleTester = require('eslint').RuleTester
+
+RuleTester.setDefaultConfig({
+ parserOptions: {
+ ecmaVersion: 2018,
+ sourceType: 'module',
+ ecmaFeatures: {
+ modules: true,
+ jsx: true,
+ },
+ },
+})
+
+var ruleTester = new RuleTester()
+ruleTester.run('google-font-display', rule, {
+ valid: [
+ `import Head from "next/head";
+
+ export default Test = () => {
+ return (
+
+
+
+ );
+ };
+ `,
+
+ `import Document, { Html, Head } from "next/document";
+
+ class MyDocument extends Document {
+ render() {
+ return (
+
+
+
+
+
+ );
+ }
+ }
+
+ export default MyDocument;
+ `,
+ ],
+
+ invalid: [
+ {
+ code: `import Head from "next/head";
+
+ export default Test = () => {
+ return (
+
+
+
+ );
+ };
+ `,
+ errors: [
+ {
+ message:
+ 'Display parameter is missing. See https://nextjs.org/docs/messages/google-font-display.',
+ type: 'JSXOpeningElement',
+ },
+ ],
+ },
+ {
+ code: `import Head from "next/head";
+
+ export default Test = () => {
+ return (
+
+
+
+ );
+ };
+ `,
+ errors: [
+ {
+ message:
+ 'Block behavior is not recommended. See https://nextjs.org/docs/messages/google-font-display.',
+ type: 'JSXOpeningElement',
+ },
+ ],
+ },
+ {
+ code: `import Head from "next/head";
+
+ export default Test = () => {
+ return (
+
+
+
+ );
+ };
+ `,
+ errors: [
+ {
+ message:
+ 'Auto behavior is not recommended. See https://nextjs.org/docs/messages/google-font-display.',
+ type: 'JSXOpeningElement',
+ },
+ ],
+ },
+ {
+ code: `import Head from "next/head";
+
+ export default Test = () => {
+ return (
+
+
+
+ );
+ };
+ `,
+ errors: [
+ {
+ message:
+ 'Fallback behavior is not recommended. See https://nextjs.org/docs/messages/google-font-display.',
+ type: 'JSXOpeningElement',
+ },
+ ],
+ },
+ ],
+})
diff --git a/test/eslint-plugin-next/google-font-preconnect.unit.test.js b/test/eslint-plugin-next/google-font-preconnect.unit.test.js
new file mode 100644
index 0000000000000..90b1a374db1ba
--- /dev/null
+++ b/test/eslint-plugin-next/google-font-preconnect.unit.test.js
@@ -0,0 +1,60 @@
+const rule = require('@next/eslint-plugin-next/lib/rules/google-font-preconnect')
+const RuleTester = require('eslint').RuleTester
+
+RuleTester.setDefaultConfig({
+ parserOptions: {
+ ecmaVersion: 2018,
+ sourceType: 'module',
+ ecmaFeatures: {
+ modules: true,
+ jsx: true,
+ },
+ },
+})
+
+var ruleTester = new RuleTester()
+ruleTester.run('google-font-preconnect', rule, {
+ valid: [
+ `export const Test = () => (
+