Chris.luChris.lu header image, displaying an 80s style landscape and sunset

Linting MDX using remark-lint

In the previous part of the tutorial, we added linting for our code, in this part we will add linting for our (MDX / markdown) content

Adding and configuring remark-lint

In the previous part we added the .eslintrc.js ESLint configuration file, in which we have added the MDX plugin

The next step is to install and then configure remark-lint, but first, we need to install remark-lint as well as 3 other packages that contain presets for remark-lint, those presets will install a bunch of rules

Use the following command to install all 4 packages:

npm i remark-lint@latest remark-preset-lint-consistent@latest remark-preset-lint-recommended@latest remark-preset-lint-markdown-style-guide@latest --save-exact --save-dev

Remark-lint rules don't get added to .eslintrc.js, but instead, we create a new file called .remarkrc.mjs (in the root of our project) and add the following content:

.remarkrc.mjs
// presets imports
import remarkPresetLintRecommended from 'remark-preset-lint-consistent'
import remarkPresetLintConsistent from 'remark-preset-lint-recommended'
import remarkPresetLintMarkdownStyleGuide from 'remark-preset-lint-markdown-style-guide'
 
// rules imports
import remarkLintMaximumHeadingLength from 'remark-lint-maximum-heading-length'
import remarkLintUnorderedListMarkerStyle from 'remark-lint-unordered-list-marker-style'
import remarkLintNoUndefinedReferences from 'remark-lint-no-undefined-references'
import remarkLintLinkTitleStyle from 'remark-lint-link-title-style'
import remarkLintMaximumLineLength from 'remark-lint-maximum-line-length'
import remarkLintListItemSpacing from 'remark-lint-list-item-spacing'
 
const config = {
    plugins: [
        // presets
        remarkPresetLintRecommended,
        remarkPresetLintConsistent,
        remarkPresetLintMarkdownStyleGuide,
        // rules
        // https://www.npmjs.com/package/remark-lint-maximum-heading-length
        [remarkLintMaximumHeadingLength, [1, 100]],
        // https://www.npmjs.com/package/remark-lint-unordered-list-marker-style
        [remarkLintUnorderedListMarkerStyle, 'consistent'],
        // https://www.npmjs.com/package/remark-lint-no-undefined-references
        [remarkLintNoUndefinedReferences, { allow: ['!NOTE', '!TIP', '!IMPORTANT', '!WARNING', '!CAUTION', ' ', 'x'] }],
        // https://www.npmjs.com/package/remark-lint-link-title-style
        [remarkLintLinkTitleStyle, '\''],
        // https://www.npmjs.com/package/remark-lint-maximum-line-length
        [remarkLintMaximumLineLength, false],
        // https://www.npmjs.com/package/remark-lint-list-item-spacing
        [remarkLintListItemSpacing, false],
    ]
}
 
export default config

The first 3 imports are presets with recommended rules, we then use them in the plugins config

The other imports are single rules we want to configure, we then use them in the plugins config, the configuration for each rule consists of an array where the first value is the rule and the second value is the configuration we want to apply, here is an explanation for the ones I suggest adding (but feel free to configure them differently, to match your use case):

Tip

To get more information about what each rule does as well as more information about the 3 presets I use here, I recommend visiting the rules and presets GitHub repository and then check out the README of each package

There are a lot more rules than the ones listed here that get used, but for all other rules, we keep the default configuration, so we don't need to install and import them individually

package.json lint script update

The next thing we need to do (as I explained in the "Why are we changing the Next.js linting setup?" chapter) is to change the lint script in our package.json

Instead of using the next lint (the Next.js CLI linting command), we will create our own command using the ESLint CLI

When using the next lint CLI command, several things were done for us in the background, however, as we want to use the ESLint CLI, we need to set those flags ourselves

For example, to enable ESLint cache, we need to use the --cache flag, and then we tell ESLint to use the same .next/cache/eslint cache folder that Next.js would use, using the --cache-location flag

To test our command in the terminal (or your preferred command line tool) we are going to use npx:

npx eslint ./ --cache --cache-location .next/cache/eslint

To specify what files (extensions) get linted, we need to use the --ext flag followed by a list of extensions like so:

npx eslint ./ --ext .js,.jsx,.ts,.tsx,.mdx,.md

Now that we have tested the command and know how the flags work, we need to put our command into our package.json (when using the command inside of the package.json, we don't need to use npx, as npm does this for us):

We change the lint script in the package.json to this:

package.json
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "next-lint": "next lint",
    "lint": "eslint ./ --ext .js,.jsx,.ts,.tsx,.mdx,.md --cache --cache-location .next/cache/eslint"
  },

Line 5: I replaced the original alias of the lint command that Next.js had created by next-lint, this way we keep the original command in the scripts, just in case we need it in the future

Line 6: I added our new custom linting command

Now, when we use the npm run lint command in our terminal, it will use our new script, it will read the ESLint configuration from our .eslintrc.js configuration file and then do the linting based on what we set in the flags

I have 3 more scripts that can be useful that you might want to add, too:

package.json
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "next-lint": "next lint",
    "lint": "eslint ./ --ext .js,.jsx,.ts,.tsx,.mdx,.md --cache --cache-location .next/cache/eslint",
    "lint-nocache": "eslint ./ --ext .js,.jsx,.ts,.tsx,.mdx,.md",
    "lint-debug": "eslint ./ --ext .js,.jsx,.ts,.tsx,.mdx,.md --debug",
    "lint-fix": "eslint ./ --ext .js,.jsx,.ts,.tsx,.mdx,.md --fix"
  },

The lint-nocache script is the same linting command, but it does NOT use the ESLint cache, this is useful when tweaking the ESLint configuration and not wanting ESLint to use the cache but instead always do a fresh start

The lint-debug script is again the same linting command, but this time, we add the --debug flag (which you can't use when using the next lint CLI, but as we now use the ESLint CLI, we can), this will print a lot of helpful information about what ESLint does in the OUTPUT tab in VSCode, which is again beneficial if you tweak your configuration and want to verify the things ESLint is doing or to debug a problem with your setup, like checking if your plugins get loaded correctly and much more

If you don't know (yet) how to open the output channel, check out the "VSCode (ESLint) output channel" chapter in the VSCode post

The lint-fix script will attempt to fix linting problems for you automatically, however this can be tricky on big repositories with a lot of code, so if you run this later and have potentially a lot of fixes that get applied, then I recommend first to create a new branch, run the command and then use the commit list to check the changes manually for each file, then do some testing to ensure nothing is broken and then finally merge the changes into your main branch

Btw the --fix flag works when using the ESLint CLI, but it can also be used when using the next lint CLI

One more thing that will help us NOT forget about those 3 new commands is to document them in our README.md file:

# MY_PROJECT
 
## npm commands (package.json scripts)
 
`npm run dev`: to start the development server  
`npm run build`: to make a production build  
`npm run start`: to start the server on a production server using the build we made with the previous command  
`npm run lint`: to run a linting script that will scan our code and help us find problems in our code  
`npm run lint-nocache`:  same as the **lint** command but it does **NOT** use the ESLint **cache**, useful to testing changes to the linting configuration  
`npm run lint-debug`: a more verbose version of the lint command that adds more information to the **output** tab, useful to verify the things ESLint is doing and to debug potential problems  
`npm run lint-fix`: ESLint will attempt to automatically fix linting problems (use with caution as ESLint might make a lot of changes, so you might want to create a new branch before running this command)  
 
## CI/CD pipeline for automatic deployments
 
Every time code gets pushed into the main branch, it will trigger a production deployment, when code gets pushed into the preview branch, it will trigger a preview deployment
 

Finally, save the README.md file and commit/sync to changes

Clearing the ESLint cache

Because by default, we use a cache to speed up the linting process when using the npm run lint command, we need to delete the cache manually after making changes to the .eslintrc.js ESLint configuration file or the .remarkrc.mjs remark-lint configuration file

To delete the cache manually, open the .next folder in the root of your project, then go into the cache folder and finally delete the eslint file

Tip

If you do a lot of tests and don't want to delete the cache manually after every change, use the npm run lint-nocache instead, until you are done testing, then delete the cache once and use the regular npm run lint command one last time to do final test

package.json build script update

Another script we need to change is the build script (as I explained in the "Why are we changing the Next.js linting setup?" chapter)

We need to ensure the build script will NOT use the Next lint CLI by default by editing our next.config.mjs, like so:

next.config.mjs
import { withSentryConfig } from '@sentry/nextjs';
import { PHASE_DEVELOPMENT_SERVER } from 'next/constants.js'
import createMdx from '@next/mdx'
 
const nextConfig = (phase) => {
 
    const withMDX = createMdx({
        extension: /\.mdx$/,
        options: {
            // optional remark and rehype plugins
            remarkPlugins: [],
            rehypePlugins: [],
        },
    })
 
    /** @type {import('next').NextConfig} */
    const nextConfigOptions = {
        reactStrictMode: true,
        poweredByHeader: false,
        experimental: {
            // experimental typescript "statically typed links"
            // https://nextjs.org/docs/app/api-reference/next-config-js/typedRoutes
            // currently false in prod until PR #67824 lands in a stable release
            // https://github.com/vercel/next.js/pull/67824
            typedRoutes: phase === PHASE_DEVELOPMENT_SERVER ? true : false,
            // use experimental rust compiler for MDX
            // as of now (07.10.2023) there is no support for rehype & remark plugins
            // this is why it is currently disabled
            mdxRs: false,
        },
        headers: async () => {
            return [
                {
                    source: '/(.*)',
                    headers: securityHeadersConfig(phase)
                },
            ];
        },
        // configure `pageExtensions` to include MDX files
        pageExtensions: ['js', 'jsx', 'ts', 'tsx', 'mdx'],
        eslint: {
            ignoreDuringBuilds: true,
        },
    }
 
    return withMDX(nextConfigOptions)
 
}

Lines 41 to 43: we add the ignoreDuringBuilds option and set it to true to ensure Next.js does not automatically run the lint command during builds

Now, as we still want linting to happen before a build, we need to add the lint script we did in the previous chapter to the build script in our package.json, like so:

package.json
"build": "npm run lint && next build",

When we now use our npm run build command (or when our deployment script uses it), it will first execute npm run lint, and if there are NO linting errors, it will then execute next build. This means that if you a deployment tool like Vercel and linting fails, then the build process will abort, so the same behavior as we had when using the default next lint and next build commands

Testing our new lint command

After all the coding let's finally make a linting test, by using the following command in our terminal:

npm run lint

You probably notice that the linting command will output some errors and warnings in the terminal, those are because of files from packages we inslled earlier like Sentry, but this is a good as it we can now also test our lint-fix command, that should automatically fix those errors for us

To fix the linting errors, use the following command:

npm run lint-fix

And then run the lint command one more time and you should have no more warnings and errors as they got all fixed:

npm run lint

We had to do a lot of changes to get this new linting setup up and running, but I hope you agree with me that it was worth it, we have greatly improved our DX by automating both the linting of code and content, which will ensure we write cleaner code and ensure our markdown content is well formatted

Congratulations 🎉 you just added linting for all your MDX (markdown) content in your project's MDX pages

If you liked this post, please consider making a donation ❤️ as it will help me create more content and keep it free for everyone