From be6c815366426a2036d0702615cd1b0852afa3bc Mon Sep 17 00:00:00 2001 From: chrisweb Date: Sun, 11 Aug 2024 18:31:22 +0200 Subject: [PATCH] remove the tutorials for now --- .../ci-cd-pipeline-setup/page.mdx | 146 --- .../code-highlighting-plugin/page.mdx | 903 ------------- .../content-security-policy/page.mdx | 572 --------- .../error-handling-and-logging/page.mdx | 218 ---- .../page.mdx | 120 -- .../first-typescript-page/page.mdx | 197 --- .../frontmatter-plugin/page.mdx | 241 ---- .../github-flawored-markdown-plugin/page.mdx | 647 ---------- .../github-like-alerts-plugin/page.mdx | 281 ---- .../headings-id-plugin/page.mdx | 158 --- .../linting-mdx-using-remark-lint/page.mdx | 289 ----- .../linting-setup-using-eslint/page.mdx | 423 ------ .../page.mdx | 346 ----- .../mdx-components-file/page.mdx | 130 -- .../mdx-plugins/page.mdx | 80 -- .../next-js-static-mdx-blog/metadata/page.mdx | 398 ------ .../navigation-and-next-link/page.mdx | 347 ----- .../navigation-styling-and-next-font/page.mdx | 246 ---- .../nextjs-configuration/page.mdx | 216 ---- .../nextjs-mdx-setup/page.mdx | 316 ----- .../optimizing-using-next-image/page.mdx | 1141 ----------------- .../optimizing-using-next-link/page.mdx | 256 ---- .../package-json-scripts/page.mdx | 226 ---- .../next-js-static-mdx-blog/page.mdx | 95 -- .../prerequisites/page.mdx | 102 -- .../project-setup-and-first-commit/page.mdx | 198 --- .../react-in-mdx-and-mdx-in-react/page.mdx | 218 ---- .../styling-and-css/page.mdx | 568 -------- .../table-of-contents-plugin/page.mdx | 503 -------- .../page.mdx | 126 -- .../next-static-export-github-pages/page.mdx | 19 - .../tutorials/node-js-app-aws-ec2/page.mdx | 596 --------- .../xcode-cloud-capacitor-webapp/page.mdx | 19 - 33 files changed, 10341 deletions(-) delete mode 100644 app/web_development/tutorials/next-js-static-mdx-blog/ci-cd-pipeline-setup/page.mdx delete mode 100644 app/web_development/tutorials/next-js-static-mdx-blog/code-highlighting-plugin/page.mdx delete mode 100644 app/web_development/tutorials/next-js-static-mdx-blog/content-security-policy/page.mdx delete mode 100644 app/web_development/tutorials/next-js-static-mdx-blog/error-handling-and-logging/page.mdx delete mode 100644 app/web_development/tutorials/next-js-static-mdx-blog/first-mdx-page-and-understanding-static-rendering/page.mdx delete mode 100644 app/web_development/tutorials/next-js-static-mdx-blog/first-typescript-page/page.mdx delete mode 100644 app/web_development/tutorials/next-js-static-mdx-blog/frontmatter-plugin/page.mdx delete mode 100644 app/web_development/tutorials/next-js-static-mdx-blog/github-flawored-markdown-plugin/page.mdx delete mode 100644 app/web_development/tutorials/next-js-static-mdx-blog/github-like-alerts-plugin/page.mdx delete mode 100644 app/web_development/tutorials/next-js-static-mdx-blog/headings-id-plugin/page.mdx delete mode 100644 app/web_development/tutorials/next-js-static-mdx-blog/linting-mdx-using-remark-lint/page.mdx delete mode 100644 app/web_development/tutorials/next-js-static-mdx-blog/linting-setup-using-eslint/page.mdx delete mode 100644 app/web_development/tutorials/next-js-static-mdx-blog/linting-using-vscode-and-extensions/page.mdx delete mode 100644 app/web_development/tutorials/next-js-static-mdx-blog/mdx-components-file/page.mdx delete mode 100644 app/web_development/tutorials/next-js-static-mdx-blog/mdx-plugins/page.mdx delete mode 100644 app/web_development/tutorials/next-js-static-mdx-blog/metadata/page.mdx delete mode 100644 app/web_development/tutorials/next-js-static-mdx-blog/navigation-and-next-link/page.mdx delete mode 100644 app/web_development/tutorials/next-js-static-mdx-blog/navigation-styling-and-next-font/page.mdx delete mode 100644 app/web_development/tutorials/next-js-static-mdx-blog/nextjs-configuration/page.mdx delete mode 100644 app/web_development/tutorials/next-js-static-mdx-blog/nextjs-mdx-setup/page.mdx delete mode 100644 app/web_development/tutorials/next-js-static-mdx-blog/optimizing-using-next-image/page.mdx delete mode 100644 app/web_development/tutorials/next-js-static-mdx-blog/optimizing-using-next-link/page.mdx delete mode 100644 app/web_development/tutorials/next-js-static-mdx-blog/package-json-scripts/page.mdx delete mode 100644 app/web_development/tutorials/next-js-static-mdx-blog/page.mdx delete mode 100644 app/web_development/tutorials/next-js-static-mdx-blog/prerequisites/page.mdx delete mode 100644 app/web_development/tutorials/next-js-static-mdx-blog/project-setup-and-first-commit/page.mdx delete mode 100644 app/web_development/tutorials/next-js-static-mdx-blog/react-in-mdx-and-mdx-in-react/page.mdx delete mode 100644 app/web_development/tutorials/next-js-static-mdx-blog/styling-and-css/page.mdx delete mode 100644 app/web_development/tutorials/next-js-static-mdx-blog/table-of-contents-plugin/page.mdx delete mode 100644 app/web_development/tutorials/next-js-static-mdx-blog/typescript-plugin-and-typed-routes/page.mdx delete mode 100644 app/web_development/tutorials/next-static-export-github-pages/page.mdx delete mode 100644 app/web_development/tutorials/node-js-app-aws-ec2/page.mdx delete mode 100644 app/web_development/tutorials/xcode-cloud-capacitor-webapp/page.mdx diff --git a/app/web_development/tutorials/next-js-static-mdx-blog/ci-cd-pipeline-setup/page.mdx b/app/web_development/tutorials/next-js-static-mdx-blog/ci-cd-pipeline-setup/page.mdx deleted file mode 100644 index d2f2d6ee..00000000 --- a/app/web_development/tutorials/next-js-static-mdx-blog/ci-cd-pipeline-setup/page.mdx +++ /dev/null @@ -1,146 +0,0 @@ ---- -title: CI/CD pipeline for automatic deployments - Tutorial -description: CI/CD pipeline for automatic deployments - Next.js static MDX blog | www.chris.lu Web development tutorials -keywords: ['CI/CD', 'Vercel', 'build', 'Production', 'preview'] -published: 2024-07-01T11:22:33.444Z -modified: 2024-07-01T11:22:33.444Z -permalink: https://chris.lu/web_development/tutorials/next-js-static-mdx-blog/ci-cd-pipeline-setup -section: Web development ---- - -import { sharedMetaDataArticle } from '@/shared/metadata-article' -import Breadcrumbs from '@/components/tutorial/Breadcrumbs' -import Pagination from '@/components/tutorial/Pagination' - -export const metadata = { - title: frontmatter.title, - description: frontmatter.description, - keywords: frontmatter.keywords, - alternates: { - canonical: frontmatter.permalink, - }, - openGraph: { - ...sharedMetaDataArticle.openGraph, - images: [{ - type: "image/png", - width: 1200, - height: 630, - url: '/web_development/og/tutorials_next-static-mdx-blog/opengraph-image' - }], - url: frontmatter.permalink, - section: frontmatter.section, - publishedTime: frontmatter.published, - modifiedTime: frontmatter.modified, - tags: frontmatter.keywords, - }, -} - -%toc% - -
- - - -# CI/CD pipeline for automatic deployments - -In this part of the tutorial, we will set up a CI/CD pipeline that will automatically deploy our code using [Vercel](https://vercel.com) - -> [!NOTE] -> You might still remember how, in the past, we would use FTP software and manually transfer code to a server, or you might have struggled setting up GitHub actions... When using Vercel, they will set up the workflow for us and start monitoring our repository. If they detect a new commit (or pull request), they will fetch our code and automatically deploy it (on their infrastructure) for us. -> -> This means we don't have to do anything else besides committing our code as we have already done before, but there will be no new additional step, meaning there is no further click on a button needed 😉 - -Of course, if you prefer to use GitHub actions to create your own CI/CD pipeline, feel free to do so - -Also, feel free to use another provider, but in this example, I show you how easy and quick it is to use [Vercel](https://vercel.com) - -The Hobby plan is free, so if you haven't tried Vercel yet, you can get an idea of how it performs compared to your current deployment process. - -## Vercel setup - -First, you need to have or create a hobby (free) account on [Vercel](https://vercel.com) (if you need help with that step, check out my chapter [Create a Vercel account (sign up)](/web_development/posts/vercel#create-an-account-sign-up) in the Vercel post) - -Now we need to create a new project on Vercel and allow them to access our repository (if you need help with that step, check out my chapter [Add a new project (repository)](/web_development/posts/vercel#add-a-new-project-repository) in the Vercel post) - -Now that we have added our GitHub repository to Vercel, every commit (or pull request) we make to the main branch will trigger a production deployment, and every commit we make to the preview branch will trigger a preview (staging) deployment. - -> [!MORE] -> [chris.lu "Vercel" post](/web_development/posts/vercel) - -## Testing preview deployments - -To see how this works, open a new tab in your browser and open the [Vercel dashboard](https://vercel.com/dashboard) page - -In the **Projects** list, click on the name of your project to access the project page (something like `https://vercel.com/TEAM_NAMEs-projects-PROJECT_HASH/PROJECT_NAME`) - -On top, you will have a section called **Production Deployment**, and below that, there is a section called **Active Branches**, which is still empty (No Preview Deployments) - -> [!NOTE] -> On the project page, you can also find your **production deployment domains** -> -> Those are useful if you don't have a custom domain yet, as they are short URLs to your production deployment that you can bookmark as they won't change over time - -Now open VSCode and make sure you are on the **preview** branch. - -Open the `README.md` file and, for example, add a small explanation that our project is now auto-deploying on Vercel, like so: - -```md showLineNumbers {10-12} -# 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 - -## 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 - -``` - -Then, save the file, commit, and sync the changes. - -Now open the browser tab in which you opened your Vercel project page. - -In the section **Active Branches**, you should now see an entry for the **preview** branch (if it does not show up, manually reload the page) - -Clicking on **View Deployment Status** will open a page with details about the current deployment. - -Back on the project page, click on the **3 dots** (...) at the end of your preview branch row and then click on **Copy Branch URL** - -Your branch URL will be something like `https://PROJECT_NAME-git-preview-TEAM_NAMEs-projects-PROJECT_HASH.vercel.app/` - -Paste the branch URL you just copied into your browser address bar and press `Enter` - -> [!NOTE] -> When you visit your preview URL, Vercel will ask you to log in (if you are not logged in yet); this is because only you are supposed to have access to the previews; if someone else wants access, they will first have to request access and wait for you to grant them access - -Because GitHub and Vercel are now connected, you will also have all the information about your deployments on your GitHub page. - -Open the repository page on GitHub and look at the right sidebar. - -You will now see a new section called **Deployments**: - -![GitHub sidebar deployments list](../../../../../public/assets/images/app/web_development/tutorials/next-js-static-mdx-blog/github_project_sidebar_deployments_list.png) - -If, for example, you click on **preview**, it will open the deployments page. - -On top of that page, you will have a link to the live preview on the vercel.app domain, and below, you will have a list of the recent deployments - -Congratulations 🎉 you are now viewing a preview version of your project hosted on Vercel - -If you liked this post, please consider making a [donation](https://buymeacoffee.com/chriswwweb) ❤️ as it will help me create more content and keep it free for everyone - - - -
diff --git a/app/web_development/tutorials/next-js-static-mdx-blog/code-highlighting-plugin/page.mdx b/app/web_development/tutorials/next-js-static-mdx-blog/code-highlighting-plugin/page.mdx deleted file mode 100644 index 9ff51419..00000000 --- a/app/web_development/tutorials/next-js-static-mdx-blog/code-highlighting-plugin/page.mdx +++ /dev/null @@ -1,903 +0,0 @@ ---- -title: Code highlighting plugin - Tutorial -description: Code highlighting plugin - Next.js static MDX blog | www.chris.lu Web development tutorials -keywords: ['highlighting', 'rehype', 'plugin', 'pretty', 'code', 'shiki'] -published: 2024-07-01T11:22:33.444Z -modified: 2024-07-01T11:22:33.444Z -permalink: https://chris.lu/web_development/tutorials/next-js-static-mdx-blog/code-highlighting-plugin -section: Web development ---- - -import { sharedMetaDataArticle } from '@/shared/metadata-article' -import Breadcrumbs from '@/components/tutorial/Breadcrumbs' -import Pagination from '@/components/tutorial/Pagination' - -export const metadata = { - title: frontmatter.title, - description: frontmatter.description, - keywords: frontmatter.keywords, - alternates: { - canonical: frontmatter.permalink, - }, - openGraph: { - ...sharedMetaDataArticle.openGraph, - images: [{ - type: "image/png", - width: 1200, - height: 630, - url: '/web_development/og/tutorials_next-static-mdx-blog/opengraph-image' - }], - url: frontmatter.permalink, - section: frontmatter.section, - publishedTime: frontmatter.published, - modifiedTime: frontmatter.modified, - tags: frontmatter.keywords, - }, -} - -%toc% - -
- - - -# Code highlighting plugin - -[rehype pretty code](https://rehype-pretty.pages.dev/) is a rehype plugin that we will use to convert our regular markdown code blocks into highlighted code blocks - -For me, the best of **rehype pretty code** is that it uses [shiki](https://shiki.matsu.io/) under the hood, **shiki** is fantastic at highlighting code, but it also comes with an impressive feature, which is that you can use your favorite VSCode theme to make your code blocks look the same as your code in VSCode - -## Adding a new playground page - -First, go into the `/app/tutorial_examples` folder and then create a new `code-highlighting_playground` folder - -Inside the `code-highlighting_playground` folder, create a new `page.mdx` MDX page and add the following content: - -````md title="/app/tutorial_examples/code-highlighting_playground/page.mdx" showLineNumbers -
- -```js -function helloWorld() { - // this is a comment - let greeting = 'Hello World!' - console.log(greeting) -} -``` - -
- -```` - -Lines 3 to 9: we add a code block example to our playground - -If the dev server is not already running, first start the dev server (using the `npm run dev` command) and then open the [http://localhost:3000/tutorial_examples/code-highlighting_playground](http://localhost:3000/tutorial_examples/code-highlighting_playground) playground page URL in your browser to have a look at the result: - -![MDX code block example with no code highlighting](../../../../../public/assets/images/app/web_development/tutorials/next-js-static-mdx-blog/MDX_code_block_no_code_highlighting.png) - -The markdown code block syntax got converted into \
 HTML element, and inside of that container, there is a \ HTML element, but there is no colorful syntax highlighting yet.
-
-This is why we will now add the *rehype pretty code* plugin.
-
-> [!MORE]  
-> ["rehype pretty code" website](https://rehype-pretty.pages.dev/)  
-> ["shiki" website](https://shiki.matsu.io/)  
-
-## Rehype pretty code installation
-
-To install the **rehype pretty code** as well as the **shiki** package, we use the following command command:
-
-```shell
-npm i rehype-pretty-code shiki --save-exact
-```
-
-Next, we edit our `next.config.mjs` file (in the root of our project) to add the plugin configuration like so:
-
-```js title="next.config.mjs" showLineNumbers {4} /rehypePrettyCode/2
-import { PHASE_DEVELOPMENT_SERVER } from 'next/constants.js'
-import createMdx from '@next/mdx'
-import rehypeMDXImportMedia from 'rehype-mdx-import-media'
-import rehypePrettyCode from 'rehype-pretty-code'
-
-const nextConfig = (phase) => {
-
-    const withMDX = createMdx({
-        extension: /\.mdx?$/,
-        options: {
-            // optional remark and rehype plugins
-            remarkPlugins: [],
-            rehypePlugins: [rehypeMDXImportMedia, rehypePrettyCode],
-        },
-    })
-
-    /** @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 Issue #62335 is fixed
-            // https://github.com/vercel/next.js/issues/62335
-            typedRoutes: phase === PHASE_DEVELOPMENT_SERVER ? true : false,
-        },
-        headers: async () => {
-            return [
-                {
-                    source: '/(.*)',
-                    headers: securityHeadersConfig(phase)
-                },
-            ];
-        },
-        // configure `pageExtensions` to include MDX files
-        pageExtensions: ['js', 'jsx', 'ts', 'tsx', 'mdx', 'md'],
-        // disable linting during builds using "next lint"
-        // we have manually added our lint script in package.json to the build command
-        eslint: {
-            ignoreDuringBuilds: true,
-        },
-        images: {
-            // file formats for next/image
-            formats: ['image/avif', 'image/webp'],
-            deviceSizes: [384, 640, 750, 828, 1080, 1200, 1920, 2176, 3840],
-        },
-    }
-
-    return nextConfigOptions
-
-}
-```
-
-Line 4: we import our new **rehypePrettyCode** plugin
-
-Line 13: we add the plugin to the rehype plugins configuration
-
-Now have another look at the playground page in your browser, and you will notice that this already looks a lot better when using the shiki default colors for highlighting:
-
-![rehype-pretty-code code highlighting example with default colors](../../../../../public/assets/images/app/web_development/tutorials/next-js-static-mdx-blog/rehype-pretty-code_plugin_default_colors.png)
-
-> [!MORE]  
-> [NPM "rehype-pretty-code" package](https://www.npmjs.com/package/rehype-pretty-code)  
-> [NPM "shiki" package](https://www.npmjs.com/package/shiki)  
-
-## Using VSCode themes for code highlighting
-
-Next, we will configure the rehype plugin to use a VSCode theme.
-
-> [!TIP]  
-> You can install any VSCode theme you like
->  
-> Have a look at the [VSCode marketplace](https://marketplace.visualstudio.com/vscode), and when you find one you like, I recommend you first check out if the [themes bundled with shiki](https://shiki.style/themes) page lists it, all themes included in shiki come from the [tm-themes package](https://www.npmjs.com/package/tm-themes), which is convenient as you don't need to install the theme yourself (but you could as we will see in the following example)
-
-To add a theme that **shiki** supports, all we need to do is edit the **rehype-pretty-code** plugin configuration in our `next.config.mjs` file, like so:
-
-```js title="next.config.mjs" showLineNumbers {8}#special {9-11} /[rehypePrettyCode, rehypePrettyCodeOptions]/#special
-import { PHASE_DEVELOPMENT_SERVER } from 'next/constants.js'
-import createMdx from '@next/mdx'
-import rehypeMDXImportMedia from 'rehype-mdx-import-media'
-import rehypePrettyCode from 'rehype-pretty-code'
-
-const nextConfig = (phase) => {
-
-    /** @type {import('rehype-pretty-code').Options} */
-    const rehypePrettyCodeOptions = {
-        theme: 'dracula',
-    }
-
-    const withMDX = createMdx({
-        extension: /\.(md|mdx)$/,
-        options: {
-            // optional remark and rehype plugins
-            remarkPlugins: [],
-            rehypePlugins: [rehypeMDXImportMedia, [rehypePrettyCode, rehypePrettyCodeOptions]],
-        },
-    })
-```
-
-Line 8: we import the types for the **rehype-pretty-code** configuration, which improves the DX as it shows us for every configuration option which values we can choose from
-
-Lines 9 to 11: we create a **rehype-pretty-code** options object and tell shiki that we want to use the bundled [Dracula Theme for Visual Studio Code](https://draculatheme.com/visual-studio-code), also because we added the types, we will get a nice autocomplete showing us what other values we could choose from:
-
-![rehype-pretty-code configuration options autocomplete because we added the types](../../../../../public/assets/images/app/web_development/tutorials/next-js-static-mdx-blog/vscode_rehype_pretty_code_configuration_options_autocomplete.png)
-
-Line 18: we add the options to our rehype plugins configuration
-
-Now have another look at the playground page in your browser, and you will notice that this time, the code is highlighted using the colors of the theme (using the same colors we have in VSCode using the Dracula Theme for Visual Studio Code):
-
-![rehype-pretty-code code highlighting example with Dracula theme for VSCode colors](../../../../../public/assets/images/app/web_development/tutorials/next-js-static-mdx-blog/rehype-pretty-code_plugin_dracula_theme.png)
-
-### Using a VSCode theme from a git(hub) repository
-
-If there is a theme you wish to use a theme that is NOT among the ones bundled in shiki, then you can do that, too.
-
-In this example, we will install a theme called [One Dark Pro (Atom's iconic One Dark theme for Visual Studio Code)](https://github.com/Binaryify/OneDark-Pro/)
-
-> [!NOTE]  
-> We are using the **One Dark Pro for VSCode** theme in this example even though you can use this theme without actually downloading it (as we did in the previous example) because it is one of the many themes bundled with shiki, but I use it here as an example to show you what to do when using any theme from a git repository  
-
-First, go to the git repository of the theme you want to use and copy the **https** URL, then install it using the npm install command, but instead of using a package name, you use the repository URL, for example, to install the **One Dark Pro Theme** you would use the following command:
-
-```shell
-npm i https://github.com/Binaryify/OneDark-Pro.git
-```
-
-The repository gets added to your node_modules like any other package, and the package.json then contains an entry like this:
-
-```json title="package.json"
-"dependencies": {
-    "material-theme": "github:Binaryify/OneDark-Pro",
-},
-```
-
-> [!NOTE]  
-> At first, I was surprised to see the name **material-theme** in my package.json, but this is normal
->  
-> It is the name the One Dark theme has in its package.json, so it is normal
-
-Then you go into your Next.js configuration file and change the code to this:
-
-```js title="next.config.mjs" showLineNumbers {5} {9} {10} /JSON.parse(themeFileContent)/
-import { PHASE_DEVELOPMENT_SERVER } from 'next/constants.js'
-import createMdx from '@next/mdx'
-import rehypeMDXImportMedia from 'rehype-mdx-import-media'
-import rehypePrettyCode from 'rehype-pretty-code'
-import { readFileSync } from 'fs'
-
-const nextConfig = (phase) => {
-
-    const themePath = new URL('./node_modules/material-theme/themes/OneDark-Pro.json', import.meta.url)
-    const themeFileContent = readFileSync(themePath, 'utf-8')
-
-    /** @type {import('rehype-pretty-code').Options} */
-    const rehypePrettyCodeOptions = {
-        theme: JSON.parse(themeFileContent),
-    }
-
-    const withMDX = createMdx({
-        extension: /\.(md|mdx)$/,
-        options: {
-            // optional remark and rehype plugins
-            remarkPlugins: [],
-            rehypePlugins: [rehypeMDXImportMedia, [rehypePrettyCode, rehypePrettyCodeOptions]],
-        },
-    })
-```
-
-Line 5: we import the **readFileSync** function from the Node.js [file system module (fs)](https://nodejs.org/api/fs.html)
-
-Line 9: we create a URL using the path to the location of our theme in the node_modules folder
-
-Line 10: we use the **readFileSync** to read the content of the theme file
-
-Line 14: we parse the themes json file content using the javascript **JSON parser**
-
-Now have another look at the playground, and you will notice that this time, the code is highlighted using the colors of the One Dark Pro theme:
-
-![rehype-pretty-code code highlighting example with Dracula theme for VSCode colors](../../../../../public/assets/images/app/web_development/tutorials/next-js-static-mdx-blog/rehype-pretty-code_plugin_one-dark-pro_theme.png)
-
-> [!NOTE]  
-> As themes are JSON files that you import in your configuration, and if you are into creating personalized color palettes, then you have the option to create or at least edit an existing theme  
->  
-> All you need to do is create a json file (or clone one of the themes you like), bring in your own colors, and then import it the same way we imported a theme that we downloaded from GitHub  
->  
-> If you do so, you then also have the option to use your customized theme in VSCode by [creating your own VSCode theme extension](https://code.visualstudio.com/api/get-started/your-first-extension)  
-
-> [!MORE]  
-> [VSCode marketplace](https://marketplace.visualstudio.com/vscode)  
-> [NPM "tm-themes" package](https://www.npmjs.com/package/tm-themes)  
-> [One Dark Pro theme for VSCode)](https://github.com/Binaryify/OneDark-Pro/)  
-> [Dracula theme for VSCode](https://draculatheme.com/visual-studio-code)  
-
-## Code block styling
-
-Next, we add the following CSS to our `global.css` file:
-
-```css title="/app/global.css" showLineNumbers{135} {1-5} {7-11}
-[data-rehype-pretty-code-figure],
-[data-rehype-pretty-code-figure]>pre {
-    margin: 0;
-    padding: 0;
-}
-
-[data-rehype-pretty-code-figure]>pre {
-    width: 100%;
-    overflow: auto;
-    padding: var(--spacing);
-}
-```
-
-Lines 135 to 139: we only reset the **margin** and **padding** on the `
` element that is the container of a rehype-pretty-code code block; it has a **data attribute** (**rehype-pretty-code** adds a container around code blocks, which consists of a `
` element width a **data** attribute that has the value **rehype-pretty-code-figure**) that we use to target it as well as the `
` element inside of it
-
-Lines 141 to 145: we set the **width** of the `
` element to **100%** to make sure our code block is as wide as our articles, and then we set the **overflow** property to **auto**, meaning that if the code inside of the code block is too large the code block is allowed to add (and display) scrollbars if the code fits the code block then the scrollbars will be hidden, finally use our spacing variable to add some padding all around our code 
-
-When styling and using themes, you can either keep the original background color or deactivate it.
-
-This is especially interesting if the theme background color does not match your website's color palette at all and you want to use a different background color for your code blocks.
-
-As we will see, it is easy to use a custom color.
-
-However, it can be tricky to find a background color that both looks good based on your project theme colors and also looks good and has good contrast for the colors used by the code highlight theme.
-
-To disable the default theme background color, we edit the rehype pretty code configuration in our `next.config.mjs` file:
-
-```js title="next.config.mjs" showLineNumbers{15}
-/** @type {import('rehype-pretty-code').Options} */
-const rehypePrettyCodeOptions = {
-    theme: JSON.parse(themeFileContent),
-    keepBackground: false,
-}
-```
-
-Line 15: we set the `keepBackground` option to false, which disables the default theme background color
-
-Then we add our background color to our rehype-pretty-code CSS in the `global.css` file:
-
-```css title="/app/global.css" showLineNumbers{135} {11}
-[data-rehype-pretty-code-figure],
-[data-rehype-pretty-code-figure]>pre {
-    margin: 0;
-    padding: 0;
-}
-
-[data-rehype-pretty-code-figure]>pre {
-    width: 100%;
-    overflow: auto;
-    padding: var(--spacing);
-    background-color: #27162b;
-}
-```
-
-Line 145: we add our `background-color`. I chose a very dark color to make we have a high contrast between the code colors and the background
-.
-## Line numbers
-
-On its website, the **rehype-pretty-code** plugin author recommends adding some more CSS if you want to display line numbers (in your code blocks on the left):
-
-```css title="/app/global.css" showLineNumbers{148}
-/* recommended by https://rehype-pretty-code.netlify.app/ */
-code[data-line-numbers] {
-    counter-reset: line;
-}
-
-code[data-line-numbers]>[data-line]::before {
-    counter-increment: line;
-    content: counter(line);
-
-    /* Other styling */
-    display: inline-block;
-    width: 1rem;
-    margin-right: 2rem;
-    text-align: right;
-    color: gray;
-}
-
-code[data-line-numbers-max-digits="2"]>[data-line]::before {
-    width: 2rem;
-}
-
-code[data-line-numbers-max-digits="3"]>[data-line]::before {
-    width: 3rem;
-}
-```
-
-We are using an interesting CSS feature here: the [CSS counters](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_counter_styles/Using_CSS_counters) feature is perfect for numbering code block lines but can be used for other things too, for example, if you want to add numbering to headings or lists
-
-Next, we need to go back to our playground and add a keyword on top of our code block to tell rehype pretty code to enable the **line numbers** for that code block, meaning only code blocks for which you use the **showLineNumbers** keywords will have line numbers.
-
-````md title="/app/tutorial_examples/code-highlighting_playground/page.mdx" showLineNumbers /showLineNumbers/#special
-```js showLineNumbers
-function helloWorld() {
-    // this is a comment
-    let greeting = 'Hello World!'
-    console.log(greeting)
-}
-```
-
-
- -```` - -Line 1: we add the `showLineNumbers` keyword to the code block markdown - -If your code block only contains a code fragment and you want to indicate that the first line is NOT 1 but another number, then you add the number you wish to use for the first line inside curly brackets to the showLineNumbers keyword, like so: - -````md title="/app/tutorial_examples/code-highlighting_playground/page.mdx" showLineNumbers /showLineNumbers{10}/#special -```js showLineNumbers{10} -function helloWorld() { - // this is a comment - let greeting = 'Hello World!' - console.log(greeting) -} -``` - - - -```` - -Line 1: we updated the `showLineNumbers` and attached `{10}` to it to tell the code block that the first line is number 10 - -> [!MORE] -> [MDN "CSS counters" documentation](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_counter_styles/Using_CSS_counters) - -## Line highlighting - -Another great feature of rehype pretty code is that you can highlight code lines, and it is effortless to do - -First, we update our global.css and add some styling for highlighted lines, like so: - -```css title="/app/global.css" showLineNumbers{173} {2-6} {8-11} -/* code blocks custom styling */ -[data-line] { - border-left-width: 2px; - border-left-style: solid; - border-left-color: transparent; -} - -[data-highlighted-line] { - background-color: #58404c; - border-left-color: #d6277f; -} -``` - -Lines 174 to 178: we use a selector to target all lines. Lines have a `data-line` attribute, and the CSS we add will border on the left, which will create a vertical line that we will then be able to colorize when the line is highlighted, but by default, we set it to `transparent`, to create this vertical line is of course not mandatory if you don't like it leave it away. - -Lines 180 to 183: we use a selector to target all lines that have a `data-highlighted-line` attribute; we change the background color of the line as well as the color of the new border (vertical line) we added on the left - -Then we need to add the numbers of the lines we want to highlight inside of curly brackets (`{}`), like so: - -````md title="/app/tutorial_examples/code-highlighting_playground/page.mdx" showLineNumbers {1}#special -```js showLineNumbers {1} {3-4} -function helloWorld() { - // this is a comment - let greeting = 'Hello World!' - console.log(greeting) -} -``` - - - -```` - -Line 1: we removed the `{10}` curly brackets to make our line numbers start with 1 again, then we added a space and then a number inside of curly brackets `{1}` to highlight the first line and also added `{3-4}` to highlight line 3 to 4 - -As you can see, you can highlight a single line by using the line number, or you can set a range using two values to highlight from line X to line Y - -If you want more than one highlighted line style, you can use IDs to create unlimited variations. - -Let's start the other way around this time by first adding an ID to our curly brackets in the playground code block example: - -````md title="/app/tutorial_examples/code-highlighting_playground/page.mdx" showLineNumbers /#errorLine/#special -```js showLineNumbers {1}#errorLine {3-4} -function helloWorld() { - // this is a comment - let greeting = 'Hello World!' - console.log(greeting) -} -``` - - - -```` - -Line 1: we add a new highlight style ID after the curly brackets defining which line(s) should get highlighted, and we set the highlight line ID to `errorLine` - -If you launch the dev server, then open the playground URL [http://localhost:3000/tutorial_examples/code-highlighting_playground](http://localhost:3000/tutorial_examples/code-highlighting_playground) in your browser, right-click on the code block and then select **Inspect**, you will notice that because we added the `#errorLine` ID we now have a `data-highlighted-line-id="errorLine"` attribute, meaning we can now target `data-highlighted-line-id` attributes that have the value `errorLine` in our CSS to change their background (and left line) color(s) - -![chrome dev tools element inspect showing the data-highlighted-line-id attribute with errorLine as value](../../../../../public/assets/images/app/web_development/tutorials/next-js-static-mdx-blog/chrome_dev_tools_inspect_element_highlight_id.png) - -Next, we update our global.css again and add another highlight style, but this one is only for highlighted lines that have the ID `errorLine`: - -```css title="/app/global.css" showLineNumbers{185} -[data-highlighted-line][data-highlighted-line-id="errorLine"] { - background-color: #6b2424; - border-left-color: #ff003d; -} -``` - -Lines 185 to 188: we add a selector to target the `data-highlighted-line-id` attribute if it has the value `errorLine`; we also set a reddish color for the background and vertical line to make this code block line look like a code line with an error - -If you launch the dev server and then open the playground URL in your browser, you will see that the first line is highlighted but uses the `errorLine` ID style. Lines 3 to 4 are highlighted, too, but as they have no ID, they use the default highlight line style. - -## Characters highlighting - -Not only can you highlight lines, but you can also highlight a series of characters. - -Go back to editing the playground markdown and change the code block to this: - -````md title="/app/tutorial_examples/code-highlighting_playground/page.mdx" showLineNumbers /helloWorld()/1#special -```js showLineNumbers /helloWorld()/ -function helloWorld() { - // this is a comment - let greeting = 'Hello World!' - console.log(greeting) -} -``` - - - -```` - -Line 1: we remove the curly brackets (used to highlight a line) from the previous example and instead used two slashes (`/`) to tell rehype pretty code that we want to highlight `helloWorld()` - -Next, we update our stylesheet and add some CSS to style highlighted characters: - -```css title="/app/global.css" showLineNumbers{192} -[data-highlighted-chars] { - background-color: #432936; -} -``` - -Lines 190 to 192: we add a custom background color for highlighted characters - -Same as for the highlighted lines, you can add an unlimited amount of highlighted character variations by using IDs like so: - -````md title="/app/tutorial_examples/code-highlighting_playground/page.mdx" showLineNumbers /#mySpecialHighlight/#special -```js showLineNumbers /greeting/#mySpecialHighlight -function helloWorld() { - // this is a comment - let greeting = 'Hello World!' - console.log(greeting) -} -``` - - - -```` - -And then you need to add the style for that new ID: - -```css title="/app/global.css" showLineNumbers{194} -[data-highlighted-chars][data-chars-id="mySpecialHighlight"] { - background-color: #874691; -} -``` - -Lines 194 to 196: we set the background color for highlighted characters when using the **mySpecialHighlight** ID - -If you look at the result in your browser, you will notice that both occurrences of the greeting variable were highlighted. - -To only highlight the second one, add the number 2 behind the second slash, like so: - -````md title="/app/tutorial_examples/code-highlighting_playground/page.mdx" showLineNumbers /2/#special -```js showLineNumbers /greeting/2#mySpecialHighlight -function helloWorld() { - // this is a comment - let greeting = 'Hello World!' - console.log(greeting) -} -``` - - - -```` - -## Code block language flag - -In the previous example(s), we already saw the first option, which consists of setting the programming language, for instance, to **javascript** by using the `js` language flag (placed after the 3 backticks of our fenced code block), it is essential to set the language flag as rehype pretty code (shiki) needs that information to know what colors to use when highlighting your code. - -So far, we have always used javascript by adding the language flag `js`, but if you want, for example, to change the programming language from **javascript** to **typescript**, then all you need to do is change the language flag (after the backticks) to `tsx`, like so: - -````md title="/app/tutorial_examplescode-highlighting_playground/page.mdx" showLineNumbers /tsx/#special -```tsx showLineNumbers /greeting/2#mySpecialHighlight -function helloWorld() { - // this is a comment - let greeting = 'Hello World!' - console.log(greeting) -} -``` -```` - -Line 1: we changed the language flag from js to **tsx** - -> [!TIP] -> You can use language flags like js and jsx, ts, and tsx, and also md and mdx, but for example, mjs or esm are not supported as language flags -> -> You can check out the complete list of language flags that are available by looking at the [shiki languages file](https://github.com/shikijs/shiki/blob/main/packages/shiki/src/assets/langs-bundle-full.ts#L1248) - -There is a second option to specify the programming language. This option is helpful if all your code blocks contain code written in the same language and you want to avoid having to set the language flag for every code block. - -In this case, you can use the rehype pretty code configuration to set a default language: - -```js title="next.config.mjs" showLineNumbers {16-18} -import { PHASE_DEVELOPMENT_SERVER } from 'next/constants.js' -import createMdx from '@next/mdx' -import rehypeMDXImportMedia from 'rehype-mdx-import-media' -import rehypePrettyCode from 'rehype-pretty-code' -import { readFileSync } from 'fs' - -const nextConfig = (phase) => { - - const themePath = new URL('./node_modules/material-theme/themes/OneDark-Pro.json', import.meta.url) - const themeFileContent = readFileSync(themePath, 'utf-8') - - /** @type {import('rehype-pretty-code').Options} */ - const rehypePrettyCodeOptions = { - theme: JSON.parse(themeFileContent), - keepBackground: false, - defaultLang: { - block: 'js', - }, - } - - const withMDX = createMdx({ - extension: /\.(md|mdx)$/, - options: { - // optional remark and rehype plugins - remarkPlugins: [], - rehypePlugins: [rehypeMDXImportMedia, [rehypePrettyCode, rehypePrettyCodeOptions]], - }, - }) -``` - -Lines 16 to 18: we set the rehype pretty code `defaultLang` configuration option for a code `block` to `js` - -Now that we have a default language set, we can remove the language flag we had after the 3 backticks of our fenced code block, like so: - -````md title="/app/tutorial_examples/code-highlighting_playground/page.mdx" showLineNumbers -``` -function helloWorld() { - // this is a comment - let greeting = 'Hello World!' - console.log(greeting) -} -``` - - - -```` - -Line 1: we remove the language flag (and any other options) - -If you did follow the "ESLint with remark lint setup" we did earlier in this tutorial, then you should now see that our code block is underlined, and if you hover over it, you will see it is an ESLint warning: - -![ESLint warning in VSCode: Unexpected missing fenced code language flag in info string](../../../../../public/assets/images/app/web_development/tutorials/next-js-static-mdx-blog/vscode_eslint_fenced_block_language_flag.png) - -We see this Remark lint [remark-lint-fenced-code-flag](https://github.com/remarkjs/remark-lint/blob/main/packages/remark-lint-fenced-code-flag/readme.md) warning because remark-lint is not aware of the default language we just added - -If you want to remove the linting warning, use a [comment to disable the remark-link rule](#remark-lint-disable-comments-in-mdx) for that code block or if you want to disable that remark-link rule altogether, then you need to edit your `.remarkrc.mjs` file and set the rule to false, as we previously did in the [configuring remark-lint](#adding-and-configuring-remark-lint) chapter, after editing the remark-lint configuration you might want to [restart the eslint server in VSCode](#restart-the-eslint-server-in-vscode) and [delete the eslint command cache](#clear-the-eslint-cache) - -> [!NOTE] -> If you don't set a default language and also don't set a language flag on a code block, then rehype pretty code will not get activated for that code block, as it needs to know the programming language to be able to choose the correct colors when highlighting - -## Highlighting inline code - -Rehype pretty code can also highlight inline code. - -To showcase this, let's edit our playground to add an inline code example like so: - -````md title="/app/tutorial_examples/code-highlighting_playground/page.mdx" showLineNumbers {9} -```js -function helloWorld() { - // this is a comment - let greeting = 'Hello World!' - console.log(greeting) -} -``` - -Some text `variable{:js}` some more text `helloWorld(){:js}` even more text - - - -```` - -As you might notice, if you run the dev server and then inspect the HTML in the browser, that rehype pretty code did not get applied, and instead, we still have a regular `` element - -As we saw in the previous chapter, rehype pretty code needs information on what colors to apply. - -In the previous example, we also saw that we have 2 options for specifying the programming language for a code block. - -For inline code, we have one more option: - -* add a language flag -* set a default language in the configuration -* add a token (this is an extra option only inline code can use) - -First, let's add a bit of CSS to our global.css to improve the styling of inline code: - -```css title="/app/global.css" showLineNumbers{198} -/* inline code custom styling */ -[data-rehype-pretty-code-figure]>code { - border-radius: 5px; - padding: 0 4px; - background-color: #27162b; -} - -[data-rehype-pretty-code-figure]>code [data-line] { - padding: 0px; -} -``` - -Both code blocks and inline code use similar HTML elements, but there is one significant difference, which is that for code blocks, there is a `
` element between the `
` and the `` element, but inline code has no `
` element, we are using this to our advantage here to be able to apply styling only to inline code, by using a [CSS selector](https://developer.mozilla.org/en-US/docs/Learn/CSS/Building_blocks/Selectors) that will only apply our styling if the `` element is a direct child of the `
` element by using a child combinator (`>`), the CSS we add is nothing special, we add a small border radius, some padding and the same background-color we previously used for the code blocks `
` element
-
-### Inline code language
-
-As we saw in the list above, our **first option** is to add a **language flag** to let rehype pretty code (shiki) know what colors to use.
-
-For inline code, the language flag needs to be inside curly brackets, and the brackets need to be at the end of our inline code before the "closing" backtick, like so:
-
-```md
-Some text `variable{:js}` some more text `function helloWorld(){:js}` even more text
-```
-
-Launch the dev server and look at your browser's playground page. You will see that the code is highlighted as intended. However, we will soon see a case where setting the language flag is not enough to get the correct colors. Our inline code has been highlighted, and the colors are correct. The `variable` now has the same color as our variables in the code block above, and so does the `hello-world()` function.
-
-Next, let's remove the language flags we added in the markdown for our inline code like so:
-
-```md
-Some text `variable` some more text `helloWorld()` even more text
-```
-
-Instead, we are going to use the **2nd option** which consists of setting a **default language** in our Next.js **configuration** file:
-
-```js title="next.config.mjs" showLineNumbers {18}
-import { PHASE_DEVELOPMENT_SERVER } from 'next/constants.js'
-import createMdx from '@next/mdx'
-import rehypeMDXImportMedia from 'rehype-mdx-import-media'
-import rehypePrettyCode from 'rehype-pretty-code'
-import { readFileSync } from 'fs'
-
-const nextConfig = (phase) => {
-
-    const themePath = new URL('./node_modules/material-theme/themes/OneDark-Pro.json', import.meta.url)
-    const themeFileContent = readFileSync(themePath, 'utf-8')
-
-    /** @type {import('rehype-pretty-code').Options} */
-    const rehypePrettyCodeOptions = {
-        theme: JSON.parse(themeFileContent),
-        keepBackground: false,
-        defaultLang: {
-            block: 'js',
-            inline: 'js',
-        },
-    }
-
-    const withMDX = createMdx({
-        extension: /\.(md|mdx)$/,
-        options: {
-            // optional remark and rehype plugins
-            remarkPlugins: [],
-            rehypePlugins: [rehypeMDXImportMedia, [rehypePrettyCode, rehypePrettyCodeOptions]],
-        },
-    })
-```
-
-Line 18: we add the `inline` option inside of the `defaultLang` configuration for rehype pretty code and set its value to `js`
-
-If we look at our playground in the browser, we see that the 2nd option works well, too.
-
-But look at what happens if we now remove the brackets of our `helloWorld` function:
-
-```md
-Some text `variable` some more text `helloWorld` even more text
-```
-
-This time, even though we have specified the language (in the configuration), the color is wrong.
-
-This is because, based on the little bit of code, the highlighter can NOT know that it is actually a function and instead assumes it is a variable.
-
-### Inline code tokens
-
-For the **3rd option** to fix the color, we will tell the highlighter explicitly that this is a function.
-
-To do that, we are going to use a **token** instead of the language flag, like so:
-
-```md
-Some text `variable` some more text `helloWorld{:.entity.name.function}` even more text
-```
-
-Now, the color is what we would expect it to be for a function.
-
-Tokens are similar to language flags, but the difference is that they are inside of curly brackets at the end of inline code, and **tokens start with a colon and a dot** (`:.`) while **language flags only begin with a colon** (`:`)
-
-Finally, you might wonder how you know what tokens are available.
-
-In a previous chapter, we used a VSCode theme and VSCode has a guide on their website about [what tokens to use when building themes](https://code.visualstudio.com/api/language-extensions/semantic-highlight-guide#standard-token-types-and-modifiers), on top of the page there is a list of **Standard token types** and on the bottom you will find a list of **Predefined TextMate scope mappings** 
-
-Another way to find out what tokens your theme uses is to look at the json file of the VSCode theme you use.
-
-In that json file, there is a section called **tokenColors**, which is the list of all the tokens and colors the theme uses
-
-For example, this is the [json file for the OneDark Pro theme](https://github.com/Binaryify/OneDark-Pro/blob/2d42e24be590925e686a477113723b7c28015a50/themes/OneDark-Pro.json) on GitHub, it is not always easy to find that json file, for example, the Dracula theme needs to get built first, on GitHub the Dracula theme uses a [yaml file to configure the tokens and colors](https://github.com/dracula/visual-studio-code/blob/e475d548db27773fa0004b252c0a4701f187fb7e/src/dracula.yml), if the YAML file is not enough you could check out the repository and build the Dracula theme yourself, or if you have it installed in VSCode, you could look at the json file in the theme folder (on Windows the path to themes folder is: `%USERPROFILE%\.vscode\extensions`, on macOS it is `~/.vscode/extensions` and on Linux, it is `~/.vscode/extensions`)
-
-There is one last feature I want to bring up, which is **token aliases**, if you don't want always to have to remember and type the full token name, which can be tedious as some tokens have rather complex names like `.meta.object-literal.key`
-
-To add those aliases to our setup, we need to edit our `rehypePrettyCodeOptions` configuration like so:
-
-```js title="next.config.mjs" showLineNumbers{12} {20-28}
-/** @type {import('rehype-pretty-code').Options} */
-const rehypePrettyCodeOptions = {
-    theme: JSON.parse(themeFileContent),
-    keepBackground: false,
-    defaultLang: {
-        block: 'tsx',
-        inline: 'js',
-    },
-    tokensMap: {
-        fn: 'entity.name.function',
-        cmt: 'comment',
-        str: 'string',
-        var: 'entity.name.variable',
-        obj: 'variable.other.object',
-        prop: 'meta.property.object',
-        int: 'constant.numeric',
-    },
-}
-```
-
-> [!NOTE]  
-> What tokens are available will vary from theme to theme  
->  
-> For example, in OneDark Pro, there is a token **constant.numeric** that I used to create the **int** alias, but in the Dracula theme, it does not exist  
->  
-> There is only a global **constant** but no specific definition for a numeric constant, which means you might need to make some adjustments to your tokens map depending on what theme you use  
-
-Lines 20 to 28: we add several custom token aliases to our rehype pretty code configuration; I tried to use abbreviations that are easy to remember without having to check out the configuration file constantly
-
-However, as I mentioned early in this tutorial, it doesn't hurt to also document them in the `README.md` file of the project:
-
-````md showLineNumbers{17} 
-### Inline code token aliases
-
-This section contains a list of token aliases for inline code in Markdown (MDX) files.
-
-Tokens get added at the end of inline code markup.
-
-They start with a curly bracket, then a colon followed by a dot, the token alias, and then a closing curly bracket:
-
-```md
-some text `myVariable{:.token}`
-```
-
-Available token aliases:
-
-* fn: function
-* cmt: comment
-* str: string (between quotes)
-* var: variable
-* obj: object
-* prop: object property
-* int: integer
-````
-
-Next, we add a code block and a list of inline code examples to our MDX playground page to test our tokens map:
-
-````md title="/app/tutorial_examples/code-highlighting_playground/page.mdx" showLineNumbers{13}
-```tsx
-function helloWorld() {
-    // read me
-    let foo = { bar: 'text', bar: 123 }
-    console.log(foo.bar, foo.baz)
-}
-```
-
-* I am a function `helloWorld{:.fn}`
-* I am a comment `read me{:.cmt}`
-* I am a string `'text'{:.str}`
-* I am a variable `foo{:.var}`
-* I am an object `foo{:.obj}`
-* I am an object property `bar{:.prop}`
-* I am an integer (numeric constant) `123{:.int}`
-
-
-
-````
-
-If we used the correct tokens to create our aliases map, the colors in the list of inline code examples (where we use the aliases) should match the colors in the code block above.
-
-![the result when using rehype pretty code for a code block and some inline code examples](../../../../../public/assets/images/app/web_development/tutorials/next-js-static-mdx-blog/rehype-pretty-code_code_block_and_inline_code_examples.png)
-
-Congratulations 🎉 you finished learning about how to highlight code using a remark plugin and learned how to use most of its features (including using VSCode themes)
-
-If you liked this post, please consider making a [donation](https://buymeacoffee.com/chriswwweb) ❤️ as it will help me create more content and keep it free for everyone
-
-> [!MORE]  
-> [VSCode "Semantic Highlight" guide](https://code.visualstudio.com/api/language-extensions/semantic-highlight-guide#standard-token-types-and-modifiers)  
-
-
-
-
diff --git a/app/web_development/tutorials/next-js-static-mdx-blog/content-security-policy/page.mdx b/app/web_development/tutorials/next-js-static-mdx-blog/content-security-policy/page.mdx
deleted file mode 100644
index 80cb6f19..00000000
--- a/app/web_development/tutorials/next-js-static-mdx-blog/content-security-policy/page.mdx
+++ /dev/null
@@ -1,572 +0,0 @@
----
-title: Content Security Policy (CSP) - Tutorial
-description: Content Security Policy (CSP) - Next.js static MDX blog | www.chris.lu Web development tutorials
-keywords: ['Content', 'Security', 'Policy', 'CSP', 'Headers', 'violation', 'report']
-published: 2024-07-01T11:22:33.444Z
-modified: 2024-07-01T11:22:33.444Z
-permalink: https://chris.lu/web_development/tutorials/next-js-static-mdx-blog/content-security-policy
-section: Web development
----
-
-import { sharedMetaDataArticle } from '@/shared/metadata-article'
-import Breadcrumbs from '@/components/tutorial/Breadcrumbs'
-import Pagination from '@/components/tutorial/Pagination'
-
-export const metadata = {
-    title: frontmatter.title,
-    description: frontmatter.description,
-    keywords: frontmatter.keywords,
-    alternates: {
-        canonical: frontmatter.permalink,
-    },
-    openGraph: {
-        ...sharedMetaDataArticle.openGraph,
-        images: [{
-          type: "image/png",
-          width: 1200,
-          height: 630,
-          url: '/web_development/og/tutorials_next-static-mdx-blog/opengraph-image'
-        }],
-        url: frontmatter.permalink,
-        section: frontmatter.section,
-        publishedTime: frontmatter.published,
-        modifiedTime: frontmatter.modified,
-        tags: frontmatter.keywords,
-    },
-}
-
-%toc%
-
-
- - - -# Content Security Policy (CSP) - -Using [Content Security Policy (CSP)](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP) headers is not required to make an app work, but it is highly recommended as it will make your project more secure - -> [!TIP] -> I like to set up the CSP headers as early as possible because if you wait until the last moment before going into production and then decide to add them, then you will probably have a bunch of **violations** that get reported, and it might take some time to adjust your CSP rules, this why I recommend starting as early as possible and fix the violations one by one as soon as they occur - -## Adding CSP Headers in Next.js configuration - -In this chapter, we are to add CSP rules to our `next.config.mjs` configuration file (which is in the root of the project) like so: - -```js title="next.config.mjs" showLineNumbers {16-23} {32} /cspReportOnly/#special /upgradeInsecure/#special {34-96} {98-103} -import { PHASE_DEVELOPMENT_SERVER } from 'next/constants.js' - -const nextConfig = (phase) => { - - /** @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 Issue #62335 is fixed - // https://github.com/vercel/next.js/issues/62335 - typedRoutes: phase === PHASE_DEVELOPMENT_SERVER ? true : false, - }, - headers: async () => { - return [ - { - source: '/(.*)', - headers: securityHeadersConfig(phase) - }, - ]; - }, - } - - return nextConfigOptions - -} - -const securityHeadersConfig = (phase) => { - - const cspReportOnly = true; - - const cspHeader = () => { - - const upgradeInsecure = (phase !== PHASE_DEVELOPMENT_SERVER) ? 'upgrade-insecure-requests;' : '' - - // worker-src is for sentry replay - // child-src is because safari <= 15.4 does not support worker-src - const defaultCSPDirectives = ` - default-src 'none'; - media-src 'self'; - object-src 'none'; - worker-src 'self' blob:; - child-src 'self' blob:; - manifest-src 'self'; - base-uri 'none'; - form-action 'none'; - require-trusted-types-for 'script'; - frame-ancestors 'none'; - ${upgradeInsecure} - ` - - // when environment is preview enable unsafe-inline scripts for vercel preview feedback/comments feature - // and whitelist vercel's domains based on: - // https://vercel.com/docs/workflow-collaboration/comments/specialized-usage#using-a-content-security-policy - // and white-list vitals.vercel-insights - // based on: https://vercel.com/docs/speed-insights#content-security-policy - if (process.env.VERCEL_ENV === 'preview') { - return ` - ${defaultCSPDirectives} - font-src 'self' https://vercel.live/ https://assets.vercel.com https://fonts.gstatic.com; - style-src 'self' 'unsafe-inline' https://vercel.live/fonts; - script-src 'self' 'unsafe-inline' https://vercel.live/; - connect-src 'self' https://vercel.live/ https://vitals.vercel-insights.com https://*.pusher.com/ wss://*.pusher.com/; - img-src 'self' data: https://vercel.com/ https://vercel.live/; - frame-src 'self' https://vercel.live/; - ` - } - - // for production environment white-list vitals.vercel-insights - // based on: https://vercel.com/docs/speed-insights#content-security-policy - if (process.env.VERCEL_ENV === 'production') { - return ` - ${defaultCSPDirectives} - font-src 'self'; - style-src 'self' 'unsafe-inline'; - script-src 'self'; - connect-src 'self' https://vitals.vercel-insights.com; - img-src 'self'; - frame-src 'none'; - ` - } - - // for dev environment enable unsafe-eval for hot-reload - return ` - ${defaultCSPDirectives} - font-src 'self'; - style-src 'self' 'unsafe-inline'; - script-src 'self' 'unsafe-inline' 'unsafe-eval'; - connect-src 'self'; - img-src 'self' data:; - frame-src 'none'; - ` - - } - - const headers = [ - { - key: cspReportOnly ? 'Content-Security-Policy-Report-Only' : 'Content-Security-Policy', - value: cspHeader().replace(/\n/g, ''), - }, - ] - - return headers - -} - -export default securityHeadersConfig -``` - -Lines 16 to 23: we add a `headers` configuration where we use the `source` property to tell Next.js that it should add those headers to every page, then we set a second `headers` property, and as the value, we make a call to our `securityHeadersConfig` function - -Line 32: we have added a `cspReportOnly` variable and have set it to **true**; we will use this variable to decide if we want to **only report** CSP violations or **enforce** CSP rules and report them; we start with true so that violations get reported but not enforced and later when we are sure that we have set out rules correctly and fixed potential violations then we will set this to false to start not only reporting but also enforcing CSP rules - -Lines 34 to 96: we have added a relatively long `cspHeader(){:fn}` function, which will create 4 sets of CSP rules: - -* The first set of CSP rules are the default rules that we will enable no matter the environment -* The second set of rules is for when the environment is **preview**, which is the case when you deploy the preview branch on Vercel; this is why this part contains a lot of URLs related to Vercel; those are sources for scripts that Vercel uses, for example, to add a comment system to your previews -* The next set contains the rules for the production environment; this part is essential as you need to ensure that you are NOT blocking any legitimate sources here, or it will create bugs in production that will impact your users -* The last set has the rules we use for our local development environment; for example, if you look at the `script-src` directive, you will see that we added `'unsafe-inline' 'unsafe-eval'`, now compare it with the `script-src` for the production rules, and you will see that those two values, this is because we need to be more permissive in development as Next.js uses tools like the Hot Reload package to do fast refreshes, which is a tool that is not being used in production, so in production we are more restrictive - -> [!TIP] -> I recommend you always start with the most restrictive rules possible. For example, if you look at the top of default CSP rules, I have set the **form-action** to none. This is because, in this tutorial, we will not have any forms, so there is no reason to allow them; however, if you add forms to your project in the future, then you will, of course, want to adjust the directive and for example, set it to 'self' instead of 'none' - -Line 36: we have added a variable that we will only populate with the [CSP: upgrade-insecure-requests](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/upgrade-insecure-requests) directive if we are not in development mode, this is because most often dev servers use HTTP requests as they have no SSL certificate installed, but for preview and production mode we add the directive, the directive tells the browser that it should assume that every request is a secure HTTPS request - -Lines 98 to 103: we create a header for our CSP rules, for the header **key** we use the **cspReportOnly** variable we added at the top, depending on the value of **cspReportOnly** we either set the CSP header to **Content-Security-Policy-Report-Only** (if cspReportOnly = true) or we set it to **Content-Security-Policy** (if cspReportOnly = false), this means that if **cspReportOnly** is true we will only report violations but not enforce them, so if for example you try to load a script from a source that is forbidden it will still get loaded but the browser will alert you about the violation, this mode is helpful for as long as we are unsure about our CSP setup and want to watch for potential violations but do NOT enforce them yet, when we are sure that our CSP rules have been fine tuned and will not block legit sources then we set **cspReportOnly** to false, meaning from now on we do NOT just report but actually also enforce the rules, finally as the header **value** we make a call to our `cspHeader(){:fn}` function which returns a string, we also use `replace{:fn}` to remove all line breaks - -> [!NOTE] -> When enforcing is enabled, it will still report the violations (besides enforcing them) - -For now, we set the CSP mode to only report violations. - -However, as soon as we are confident that there are no more violations, it is recommended to set our custom variable **cspReportOnly** to **false**, especially when you are done testing and decide to put everything into production. - -> [!NOTE] -> The CSP headers I added in the tutorial are based on Next.js recommendations that can be found in the [Next.js "Configuring CSP" documentation](https://nextjs.org/docs/app/building-your-application/configuring/content-security-policy) - -If you now start your development server (using `npm run dev`), open `http://localhost:3000` in your browser, open the browser developer tools, and then click on the [Console Tab](https://developer.chrome.com/docs/devtools/console), then you should see no CSP violations messages - -> [!MORE] -> [MDN "Content Security Policy (CSP)" documentation](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP) -> [MDN "CSP Headers" reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy) -> [Next.js "Configuring CSP" documentation](https://nextjs.org/docs/app/building-your-application/configuring/content-security-policy) -> [Vercel "Using a Content Security Policy" documentation](https://vercel.com/docs/workflow-collaboration/comments/specialized-usage#using-a-content-security-policy) - -### Example of a CSS violation - -> [!NOTE] -> To check for best practices, I used a tool by Google called [CSP Evaluator](https://csp-evaluator.withgoogle.com/), it showed a green checkmark for every directive except the **script-src** directive, where it mentioned that it would be better to remove **'unsafe-inline'**, however unsafe-inline is there because Next.js uses inline scripts a lot - -Let's edit the CSP rules we just added in our `next.config.mjs` file and make the **script-src** directive stricter by not using **unsafe-eval** as recommended by the CSP Evaluator service, like so: - -```js title="security.config.mjs" showLineNumbers{85} {6}#special - // for dev environment enable unsafe-eval for hot-reload - return ` - ${defaultCSPDirectives} - font-src 'self'; - style-src 'self' 'unsafe-inline'; - script-src 'self' 'unsafe-inline'; - connect-src 'self'; - img-src 'self' data:; - frame-src 'none'; - ` -``` - -Line 90 we remove `'unsafe-eval'` for the **script-src** directive - -Go back into the browser and check the [Console Tab](https://developer.chrome.com/docs/devtools/console) again. You should now be able to see a bunch of errors like these: - -{/* eslint-disable-next-line mdx/remark */} -> [Report Only] Refused to evaluate a string as JavaScript because 'unsafe-eval' is not an allowed source of script in the following Content Security Policy directive: "script-src 'self' 'unsafe-inline'". - -Those violations are there because Next.js attempts to inject javascript code used by development scripts like the Hot Module Reload (HMR) tool, which is a tool that reloads our page every time we save a file - -For HMR to work, we need to re-add the **'unsafe-eval** value to the **script-src** rule. Do that now, and then save the file to fix violations again. - -## Logging CSP violations - -You should log CSP violations the same way you log errors in your code to ensure they don't go unnoticed and to be able to fix them promptly. If the CSP rules get enforced, they will probably trigger violations. If unhandled, those violations will probably create bugs on your website, which is why you want to keep an eye on them. - -Several logging service providers offer to log CSP violations. I will use Sentry.io in this tutorial because it is already the tool we use for error logging. However, feel free to choose another provider if you find one you prefer or even create your CSP violations logging tool if you have the capacity to develop, host, and maintain such a project. - -### Why Sentry.io (is not yet) the ideal solution (and why we will still use it) - -In a lot of places (when reading about Sentry.io CSP violation logging), including in their documentation (as of now 01.04.2024), you will read that it is recommended to use both the **report-to** as well as **report-uri** as a fallback. - -The above works for Firefox, which does not yet support report-to but does support report-uri, so Firefox will fallback and use report-uri - -However this does not use when using chrome (or any chromium based browser like edge and brave), chrome (>96) will attempt to use the **report-to directive** (defined as fallback in the Sentry.io documentation example), but chrome will assume you are using the **Reporting-Endpoints header** from the Reporting API v1, however the Sentry.io example uses the **Report-To header** from the Reporting API v0 which chrome (>96) does not support (anymore), meaning chrome will queue the reports and then attempt to send them, but as it will not find a valid endpoint definition the requests will fail (chrome will put their status back to "Queued" for another attempt and after a while will set the status to "MarkedForRemoval") and after failing to send the reports chrome will never fall back to using the **report-uri directive**, you might be tempted to replace the **Report-To header** from the Sentry.io example with the new **Reporting-Endpoints header** however Sentry.io does not support the Reporting-Endpoints header yet, so that's also not an option. - -> [!TIP] -> For a more in-depth look at the evolution of CSP and violation logging, I recommend checking out my [CSP post](/web_development/posts/csp) - -In the next chapter, we will use [Sentry.io](https://sentry.io) that we have set up earlier for error logging purposes and add CSP violations logging - -However, we will only use the report-uri directive from the CSP v1 specification, as this solution works in Chrome, Firefox, and Safari. - -> [!NOTE] -> Keep an eye on CSP violation logging techniques as browsers and logging services will, one after the other, start supporting the Reporting API v1, and when they all do, I recommend replacing the report-uri directive with the report-to directive and the Reporting-Endpoints header - -The major drawback when using the report-uri directive is that it makes a request to your logging service for each violation it finds (the new reporting API v1 queues violations and then sends them all in one batch to the logging service), which is why I recommend only to enable logging periodically, to ensure that you are not using up your entire quota in just a few hours/days, if you look at big web platforms you will notice that, even though they have CSP rules, they also often remove the reporting when not needed. They only turn it on when there is a bug and suspect the CSP rules to be the cause. - -> [!MORE] -> [chris.lu "Content Security Policy (CSP)" post](/web_development/posts/csp) - -### Setting up CSP violations logging using Sentry.io - -First, you need to visit Sentry.io and copy the CSP reporting URL of your project: - -* visit Sentry.io and log in -* in the left navigation on the bottom, click on **Settings** -* Then, in the Settings navigation on the left, click on **Projects** -* Click on the project name -* Then in navigation on the left, under **SDK SETUP**, click on **Security Headers** -* On the **Security Header Reports** page, copy the URL under **REPORT URI** -* finally replace the URL for the **const reportingUrl = ''** in the following code by the CSP **REPORT URI** from your Sentry account - -Next, we need to ensure violations get sent to Sentry.io (logged like any other error) - -Therefore, we will edit our CSP setup in the next.config.mjs file, like so: - -```js title="next.config.mjs" showLineNumbers {34-35} {41-42} {71} {74} {86} {89} /${reportingDomainWildcard}/#special /${reportCSPViolations}/#special -import { PHASE_DEVELOPMENT_SERVER } from 'next/constants.js' - -const nextConfig = (phase) => { - - /** @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 Issue #62335 is fixed - // https://github.com/vercel/next.js/issues/62335 - typedRoutes: phase === PHASE_DEVELOPMENT_SERVER ? true : false, - }, - headers: async () => { - return [ - { - source: '/(.*)', - headers: securityHeadersConfig(phase) - }, - ]; - }, - } - - return nextConfigOptions - -} - -const securityHeadersConfig = (phase) => { - - const cspReportOnly = true; - - const reportingUrl = 'https://foo123.ingest.sentry.io/api/bar456/security/?sentry_key=baz789' - const reportingDomainWildcard = 'https://*.ingest.sentry.io' - - const cspHeader = () => { - - const upgradeInsecure = (phase !== PHASE_DEVELOPMENT_SERVER) ? 'upgrade-insecure-requests;' : '' - - // reporting uri (CSP v1) - const reportCSPViolations = `report-uri ${reportingUrl};` - - // worker-src is for sentry replay - // child-src is because safari <= 15.4 does not support worker-src - const defaultCSPDirectives = ` - default-src 'none'; - media-src 'self'; - object-src 'none'; - worker-src 'self' blob:; - child-src 'self' blob:; - manifest-src 'self'; - base-uri 'none'; - form-action 'none'; - require-trusted-types-for 'script'; - frame-ancestors 'none'; - ${upgradeInsecure} - ` - - // when environment is preview enable unsafe-inline scripts for vercel preview feedback/comments feature - // and whitelist vercel's domains based on: - // https://vercel.com/docs/workflow-collaboration/comments/specialized-usage#using-a-content-security-policy - // and white-list vitals.vercel-insights - // based on: https://vercel.com/docs/speed-insights#content-security-policy - if (process.env.VERCEL_ENV === 'preview') { - return ` - ${defaultCSPDirectives} - font-src 'self' https://vercel.live/ https://assets.vercel.com https://fonts.gstatic.com; - style-src 'self' 'unsafe-inline' https://vercel.live/fonts; - script-src 'self' 'unsafe-inline' https://vercel.live/; - connect-src 'self' https://vercel.live/ https://vitals.vercel-insights.com https://*.pusher.com/ wss://*.pusher.com/ ${reportingDomainWildcard}; - img-src 'self' data: https://vercel.com/ https://vercel.live/; - frame-src 'self' https://vercel.live/; - ${reportCSPViolations} - ` - } - - // for production environment white-list vitals.vercel-insights - // based on: https://vercel.com/docs/speed-insights#content-security-policy - if (process.env.VERCEL_ENV === 'production') { - return ` - ${defaultCSPDirectives} - font-src 'self'; - style-src 'self' 'unsafe-inline'; - script-src 'self'; - connect-src 'self' https://vitals.vercel-insights.com ${reportingDomainWildcard}; - img-src 'self'; - frame-src 'none'; - ${reportCSPViolations} - ` - } - - // for dev environment enable unsafe-eval for hot-reload - return ` - ${defaultCSPDirectives} - font-src 'self'; - style-src 'self' 'unsafe-inline'; - script-src 'self' 'unsafe-inline' 'unsafe-eval'; - connect-src 'self'; - img-src 'self' data:; - frame-src 'none'; - ` - - } - - const headers = [ - { - key: cspReportOnly ? 'Content-Security-Policy-Report-Only' : 'Content-Security-Policy', - value: cspHeader().replace(/\n/g, ''), - }, - ] - - return headers - -} - -export default securityHeadersConfig -``` - -Lines 34 to 35: we add two new variables to store the Sentry.io CSP logging URL and a wildcard for the Sentry.io ingest sub-domain - -* The first variable contains the CSP logging URL **https://foo123.ingest.sentry.io/api/bar456/security/?sentry_key=baz789** is the same for preview and prod; it is the endpoint URL where the reports will get sent to, which means we use that variable to tell the report-uri directive where to send CSP violations reports -* The second variable contains a wildcard for the sentry domain so that we can add the domain to our connect-src directive - -Lines 41 to 42: we use a template literal to create the reporting uri directive; this directive will tell the browser what URL it should use when sending the CSP reports - -Line 71 and 86: we add the **reportingDomainWildcard** to the connect-src directive - -Line 74 and 89: we add the **reportCSPViolations** variable, which contains a reporting directive, meaning we only report violations to sentry for preview and production deployments but NOT local development - -> [!NOTE] -> It is essential to add the **reportingDomainWildcard** to the connect-src directive, or CSP will block the reporting URL and not send reports to Sentry. We only add the **reportingDomainWildcard** to the connect-src for preview and production, but NOT development, as Sentry.io will filter out reports from localhost anyway. -> -> If you want to debug your code, you might want to also add the reporting for development. In that case, add the `${reportCSPViolations}` variable to the development directives too (same as for preview and production) and then check out the chapter about [disableng the "reports from localhost" filter](/web_development/posts/sentry-io/#disable-the-reports-from-locahost-filter) in my Sentry.io post as you will need to disable the filter for localhost reporting in your Sentry configuration on Sentry.io - -> [!MORE] -> [Sentry.io "CSP violations logging" documentation](https://docs.sentry.io/product/security-policy-reporting/) - -## Adding security headers - -There are also some useful security headers besides CSP headers. Let's add 3 of those security headers to our Next.js configuration. - -### Next.js configuration security headers - -We just added the CSP setup to every page header by altering the Next.js configuration file, and now we are going to add 4 more security headers: - -```js title="next.config.mjs" showLineNumbers{30} {15-25} {90} {95-106} -const securityHeadersConfig = (phase) => { - - const cspReportOnly = true; - - const reportingUrl = 'https://foo123.ingest.sentry.io/api/bar456/security/?sentry_key=baz789' - const reportingDomainWildcard = 'https://*.ingest.sentry.io' - - const cspHeader = () => { - - const upgradeInsecure = (phase !== PHASE_DEVELOPMENT_SERVER) ? 'upgrade-insecure-requests;' : '' - - // reporting uri (CSP v1) - const reportCSPViolations = `report-uri ${reportingUrl};` - - // security headers for preview & production - const extraSecurityHeaders = [] - - if (phase !== PHASE_DEVELOPMENT_SERVER) { - extraSecurityHeaders.push( - { - key: 'Strict-Transport-Security', - value: 'max-age=31536000', // 1 year - }, - ) - } - - // worker-src is for sentry replay - // child-src is because safari <= 15.4 does not support worker-src - const defaultCSPDirectives = ` - default-src 'none'; - media-src 'self'; - object-src 'none'; - worker-src 'self' blob:; - child-src 'self' blob:; - manifest-src 'self'; - base-uri 'none'; - form-action 'none'; - require-trusted-types-for 'script'; - frame-ancestors 'none'; - ${upgradeInsecure} - ` - - // when environment is preview enable unsafe-inline scripts for vercel preview feedback/comments feature - // and whitelist vercel's domains based on: - // https://vercel.com/docs/workflow-collaboration/comments/specialized-usage#using-a-content-security-policy - // and white-list vitals.vercel-insights - // based on: https://vercel.com/docs/speed-insights#content-security-policy - if (process.env.VERCEL_ENV === 'preview') { - return ` - ${defaultCSPDirectives} - font-src 'self' https://vercel.live/ https://assets.vercel.com https://fonts.gstatic.com; - style-src 'self' 'unsafe-inline' https://vercel.live/fonts; - script-src 'self' 'unsafe-inline' https://vercel.live/; - connect-src 'self' https://vercel.live/ https://vitals.vercel-insights.com https://*.pusher.com/ wss://*.pusher.com/ ${reportingDomainWildcard}; - img-src 'self' data: https://vercel.com/ https://vercel.live/; - frame-src 'self' https://vercel.live/; - ${reportCSPViolations} - ` - } - - // for production environment white-list vitals.vercel-insights - // based on: https://vercel.com/docs/speed-insights#content-security-policy - if (process.env.VERCEL_ENV === 'production') { - return ` - ${defaultCSPDirectives} - font-src 'self'; - style-src 'self' 'unsafe-inline'; - script-src 'self'; - connect-src 'self' https://vitals.vercel-insights.com ${reportingDomainWildcard}; - img-src 'self'; - frame-src 'none'; - ${reportCSPViolations} - ` - } - - // for dev environment enable unsafe-eval for hot-reload - return ` - ${defaultCSPDirectives} - font-src 'self'; - style-src 'self' 'unsafe-inline'; - script-src 'self' 'unsafe-inline' 'unsafe-eval'; - connect-src 'self'; - img-src 'self' data:; - frame-src 'none'; - ` - - } - - const headers = [ - ...extraSecurityHeaders, - { - key: cspReportOnly ? 'Content-Security-Policy-Report-Only' : 'Content-Security-Policy', - value: cspHeader().replace(/\n/g, ''), - }, - { - key: 'Referrer-Policy', - value: 'same-origin', - }, - { - key: 'X-Content-Type-Options', - value: 'nosniff', - }, - { - key: 'X-Frame-Options', - value: 'DENY' - }, - ] - - return headers - -} - -export default securityHeadersConfig -``` - -Lines 44 to 54: we added a new `extraSecurityHeaders` variable to store the HSTS header, but as we want to exclude it in development where we don't have an SSL certificate, we check if the phase is NOT development - -The HSTS header (`Strict-Transport-Security`) tells the browser that this app only supports HTTPS. We want the browser to always use HTTPS for every request, even if the scheme is HTTP in the page URL. - -Line 119: we use the `extraSecurityHeaders` to add the `Strict-Transport-Security` header to the list of headers - -Lines 124 to 135: we add the 3 security headers to the list of headers: - -* the first one is a `Referrer-Policy` header tells the browser when and when NOT to include information about the origin in referrer header, the [MDN "Referrer-Policy" documentation](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy) does a very good at explaining the different values, I like to only set the referrer for internal pages but not for external pages, that's why I use the value **same-origin** -* The second **X-Content-Type-Options** header tells the browser not to attempt to guess the MIME type of resource by itself -* The third one is the `X-Frame-Options` header, when set to **deny**, does the same thing as the **frame-ancestors** directive (we added earlier) when set to **none**, but it is for older browsers that did not have support for the directive - -Congratulations 🎉 you just made your project a lot more secure by setting up CSP headers and reporting potential violations - -If you liked this post, please consider making a [donation](https://buymeacoffee.com/chriswwweb) ❤️ as it will help me create more content and keep it free for everyone - -> [!MORE] -> [MDN "Strict-Transport-Security" documentation](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Strict-Transport-Security) -> [MDN "Referrer-Policy" documentation](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy) -> [MDN "X-Content-Type-Options" documentation]() -> [MDN "X-Frame-Options" documentation](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options) - - - -
diff --git a/app/web_development/tutorials/next-js-static-mdx-blog/error-handling-and-logging/page.mdx b/app/web_development/tutorials/next-js-static-mdx-blog/error-handling-and-logging/page.mdx deleted file mode 100644 index bb20ee79..00000000 --- a/app/web_development/tutorials/next-js-static-mdx-blog/error-handling-and-logging/page.mdx +++ /dev/null @@ -1,218 +0,0 @@ ---- -title: Error handling and logging - Tutorial -description: Error handling and logging - Next.js static MDX blog | www.chris.lu Web development tutorials -keywords: ['error', 'handling', 'logging', 'Boundary', 'react', 'sentry.io', 'nextjs'] -published: 2024-07-01T11:22:33.444Z -modified: 2024-07-01T11:22:33.444Z -permalink: https://chris.lu/web_development/tutorials/next-js-static-mdx-blog/error-handling-and-logging -section: Web development ---- - -import { sharedMetaDataArticle } from '@/shared/metadata-article' -import Breadcrumbs from '@/components/tutorial/Breadcrumbs' -import Pagination from '@/components/tutorial/Pagination' - -export const metadata = { - title: frontmatter.title, - description: frontmatter.description, - keywords: frontmatter.keywords, - alternates: { - canonical: frontmatter.permalink, - }, - openGraph: { - ...sharedMetaDataArticle.openGraph, - images: [{ - type: "image/png", - width: 1200, - height: 630, - url: '/web_development/og/tutorials_next-static-mdx-blog/opengraph-image' - }], - url: frontmatter.permalink, - section: frontmatter.section, - publishedTime: frontmatter.published, - modifiedTime: frontmatter.modified, - tags: frontmatter.keywords, - }, -} - -%toc% - -
- - - -# Error handling and logging - -As we saw earlier, each route segment is a directory, and each directory contains a page file, but unlike the page router, when using the app router, we can add more than just pages, for example, one of those files we are about to add is an error page - -How this works is that Next.js will automatically wrap the children of your page with a **React Error Boundary**, meaning that when an error gets thrown in a page, then the error boundary will contain it and then use the error file that is the closest (either an error file that is in the same directory as the page itself or a parent directory) as a fallback - -Let's create our first error file inside of our app directory, and let's use the example from the Next.js documentation like so: - -```ts title="app/error.tsx" showLineNumbers {13-16} -'use client' // Error components must be Client Components - -import { useEffect } from 'react' - -export default function Error({ - error, - reset, -}: { - error: Error & { digest?: string } - reset: () => void -}) { - - useEffect(() => { - // Log the error to an error reporting service - console.error(error) - }, [error]) - - return ( -
-

Sorry, something went wrong 😞

- -
- ) -} -``` - -Every time the component receives an error, it will use `console.log` to print the error in the console - -The second feature of this component is that there is a `button` which will trigger the `reset` function we got from the component props, this function provided by Next.js will attempt to rerender the segment that triggered an error, this is helpful if the error was caused by something that occurs sporadically and might allow the user to continue - -As you can see, the Next.js documentation example uses a `useEffect(){:.function}` function to log the error in our console, but what happens if the error is getting triggered on a user's computer, then we won't know about, so as Next.js suggests, in the second part of this chapter, we will use a third-party service called [Sentry.io](https://sentry.io) to do the logging for us (of course if you prefer you can also develop and run your logging service instead) - -> [!MORE] -> [Next.js "Handling Errors" documentation](https://nextjs.org/docs/app/building-your-application/routing/error-handling) - -## Sentry.io SDK for Next.js setup - -In this chapter, we will use [Sentry.io](https://sentry.io) (which has a free plan for side projects) to add error logging to the Next.js error file we just created - -First, you need to have or create an account on [Sentry.io](https://sentry.io) (if you need help with that step, check out my chapter [Create a Sentry account (sign up)](/web_development/posts/sentry-io#create-an-account-sign-up) in the Sentry.io post) - -Now we need to create a new project on Sentry.io (if you need help with that step, check out my chapter [Create a Sentry.io project](/web_development/posts/sentry-io#create-a-sentryio-project) in the Sentry.io post) - -Now that the project is created, we will use the Sentry.io Wizard tool to install the Sentry.io SDK for Next.js (if you need help with that step, check out my chapter [Sentry.io SDK for Next.js installation](/web_development/posts/sentry-io#sentryio-sdk-for-nextjs-installation) in the Sentry.io post) - -After creating a Sentry.io project and setting up the SDK, I recommend also using the [Sentry.io integration on Vercel](https://vercel.com/integrations/sentry) as this will automate the part **"Adding the Sentry authentication token as an environment variable to your CI setup"** that we just saw when using the Sentry.io wizard (if you need help with that step, check out my chapter [Sentry integration for Vercel](/web_development/posts/vercel#sentry-integration-for-vercel) in the Vercel post) - -Finally, now that you have installed the SDK, you might want to do some fine-tuning of the Sentry.io configuration (if you need help with that step, check out my chapter [Sentry.io for Next.js configuration](/web_development/posts/sentry-io#sentryio-for-nextjs-configuration) in the Sentry.io post) - -> [!MORE] -> [chris.lu "Sentry" post](/web_development/posts/sentry-io) - -## Error logging using Sentry.io - -Now that Sentry.io is set up, we can modify the error file we created earlier and add import the Sentry SDK, and add the Sentry.io logging function inside of the `useEffect(){:.function}` to replace the example console.log, like so: - -```tsx title="app/error.tsx" showLineNumbers {3} {14-17} -'use client' // Error components must be Client Components - -import * as Sentry from '@sentry/nextjs' -import { useEffect } from 'react' - -export default function Error({ - error, - reset, -}: { - error: Error & { digest?: string } - reset: () => void -}) { - - useEffect(() => { - // log the error to Sentry.io - Sentry.captureException(error) - }, [error]) - - return ( - <> -

Sorry, something went wrong 😞

- - - ) -} -``` - -> [!MORE] -> [Sentry.io "Next.js SDK" documentation](https://docs.sentry.io/platforms/javascript/guides/nextjs/) - -## Handling global errors - -The Sentry.io wizard we just used has created a Next.js `app/global-error.jsx` file for us - -The Next.js documentation explains well why this file is essential: - -> The root app/error.js boundary does not catch errors thrown in the root app/layout.js or app/template.js component. -> -> To specifically handle errors in these root components, use a variation of error.js called app/global-error.js located in the root app directory. -> -> global-error.js is the least granular error UI and can be considered "catch-all" error handling for the whole application. - -But because Sentry created a **javascript** file, but our project uses **typescript**, we will start by converting the file into a typescript file by renaming `app/global-error.jsx` to `app/global-error.tsx` - -After renaming the file, we edit it and replace its content with the code for a global error page from the Next.js documentation, which is a good start as it is strictly typed, the only difference is that we also modify the code to add the Sentry.io `captureException(){:.function}` function, that will capture exceptions and send them to Sentry.io, the final version looks like this: - -```ts title="app/global-error.tsx" showLineNumbers -'use client' // Error components must be Client Components - -import * as Sentry from '@sentry/nextjs' -import { useEffect } from 'react' - -export default function GlobalError({ - error, - reset, -}: { - error: Error & { digest?: string } - reset: () => void -}) { - - useEffect(() => { - // log the error to Sentry.io - Sentry.captureException(error) - }, [error]) - - return ( - - -

Sorry, something went wrong 😞

- - - - ) -} -``` - -The global error handling file will handle root layout errors and act as a catch-all for app errors - -Time to save, commit, and sync - -Congratulations 🎉 you now have error handling and logging for pages as well as global error handling in your project - -If you liked this post, please consider making a [donation](https://buymeacoffee.com/chriswwweb) ❤️ as it will help me create more content and keep it free for everyone - -> [!MORE] -> [Next.js "handling errors in root layouts" documentation](https://nextjs.org/docs/app/building-your-application/routing/error-handling#handling-errors-in-root-layouts) - - - -
diff --git a/app/web_development/tutorials/next-js-static-mdx-blog/first-mdx-page-and-understanding-static-rendering/page.mdx b/app/web_development/tutorials/next-js-static-mdx-blog/first-mdx-page-and-understanding-static-rendering/page.mdx deleted file mode 100644 index 6631a336..00000000 --- a/app/web_development/tutorials/next-js-static-mdx-blog/first-mdx-page-and-understanding-static-rendering/page.mdx +++ /dev/null @@ -1,120 +0,0 @@ ---- -title: First MDX page and understanding of static rendering - Tutorial -description: First MDX page and understanding of static rendering - Next.js static MDX blog | www.chris.lu Web development tutorials -keywords: ['static', 'rendering', 'SSG', 'first', 'MDX', 'page', 'next/mdx'] -published: 2024-07-01T11:22:33.444Z -modified: 2024-07-01T11:22:33.444Z -permalink: https://chris.lu/web_development/tutorials/next-js-static-mdx-blog/first-mdx-page-and-understanding-static-rendering -section: Web development ---- - -import { sharedMetaDataArticle } from '@/shared/metadata-article' -import Breadcrumbs from '@/components/tutorial/Breadcrumbs' -import Pagination from '@/components/tutorial/Pagination' - -export const metadata = { - title: frontmatter.title, - description: frontmatter.description, - keywords: frontmatter.keywords, - alternates: { - canonical: frontmatter.permalink, - }, - openGraph: { - ...sharedMetaDataArticle.openGraph, - images: [{ - type: "image/png", - width: 1200, - height: 630, - url: '/web_development/og/tutorials_next-static-mdx-blog/opengraph-image' - }], - url: frontmatter.permalink, - section: frontmatter.section, - publishedTime: frontmatter.published, - modifiedTime: frontmatter.modified, - tags: frontmatter.keywords, - }, -} - -%toc% - -
- - - -# First MDX page and understanding of static rendering - -We have reached yet another milestone, now that we did [set up MDX support for Next.js](/web_development/tutorials/next-js-static-mdx-blog/nextjs-mdx-setup), it is now time to create our very first MDX page, and after that, we will see how to know if pages got statically generated (or not) - -## Our first MDX page - -In the `app` folder, create a new `tutorial_examples` folder and then in it another `first_mdx_page` folder - -Then, inside of the `first_mdx_page` folder, add a `page.mdx` (note that we set **extension** to **mdx** and NOT tsx), and in it, paste the following content: - -```md title="/app/tutorial_examples/first_mdx_page/page.mdx" -# Hello 👋 with MDX! - -## headline 2nd level - -*italic* - -**bold** - -***bold and italic*** - -> a quote - -[link to Next.js](https://nextjs.org) - -* foo -* bar -* baz - -![This is an octocat image](https://myoctocat.com/assets/images/base-octocat.svg 'I\'m the title of the octocat image') -``` - -Make sure your dev server is running, if it is not, start it using `npm run dev` - -Then visit your newly created MDX page in the browser at `http://localhost:3000/tutorial_examples/first_mdx_page` - -Congratulations 🎉 you just added MDX support to your Next.js project and learned how to create MDX pages - -## Static rendering - -Putting our content into the first MDX page instead of fetching it from a database means we have just created our static site generator with MDX support - -To check if a page is fully **static**, you can do a build using the `npm run build` command: - -```shell -npm run build -``` - -After the build is done, look at the output in your terminal: - -![build terminal output showing that pages are static](../../../../../public/assets/images/app/web_development/tutorials/next-js-static-mdx-blog/nextjs_build_static_page.png) - -The empty circle (`○`) in front of our `/tutorial_examples/first_mdx_page` indicates that Next.js will statically generate routes (pages) at build time instead of on-demand at request time - -Because Next.js will automatically choose the [server rendering strategy](https://nextjs.org/docs/app/building-your-application/rendering/server-components#server-rendering-strategies) for each route based on the features you use, your page at some point might not be **static** anymore, for example when you use a **dynamic function** like [searchParams](https://nextjs.org/docs/app/api-reference/file-conventions/page#searchparams-optional) or when you fetch data and do NOT use [generateStaticParams](https://nextjs.org/docs/app/api-reference/functions/generate-static-params) then your page becomes **dynamic**, which is why it is recommended to launch the build locally from time to time and check if the pages you want to be static still are, if they are not static anymore you might want to find the cause and for example use **generateStaticParams** to make it fully static again - -If all of your pages are static, you can do a [static export](https://nextjs.org/docs/app/building-your-application/deploying/static-exports), meaning Next.js build will create an `out` folder and put all the HTML/CSS/JS static assets into it, then you can take that folder and for example deploy your app on [GitHub pages](https://pages.github.com/) - -Congratulations 🎉 you now know how to check if pages are static or dynamic, which is essential because the more static content you have, the less work the server will need to do during runtime, and this will result in pages that load blazingly fast - -If you liked this post, please consider making a [donation](https://buymeacoffee.com/chriswwweb) ❤️ as it will help me create more content and keep it free for everyone - -> [!MORE] -> [Next.js "Server Rendering Strategies" documentation](https://nextjs.org/docs/app/building-your-application/rendering/server-components#server-rendering-strategies) -> [Vercel.com "How to choose the best rendering strategy for your app" blog post](https://vercel.com/blog/how-to-choose-the-best-rendering-strategy-for-your-app) -> [Next.js "generateStaticParams" documentation](https://nextjs.org/docs/app/api-reference/functions/generate-static-params) -> [Next.js "Static Exports" documentation](https://nextjs.org/docs/app/building-your-application/deploying/static-exports) - - - -
diff --git a/app/web_development/tutorials/next-js-static-mdx-blog/first-typescript-page/page.mdx b/app/web_development/tutorials/next-js-static-mdx-blog/first-typescript-page/page.mdx deleted file mode 100644 index 3bc2dd42..00000000 --- a/app/web_development/tutorials/next-js-static-mdx-blog/first-typescript-page/page.mdx +++ /dev/null @@ -1,197 +0,0 @@ ---- -title: First Typescript page - Tutorial -description: First Typescript page - Next.js static MDX blog | www.chris.lu Web development tutorials -keywords: ['typescript', 'nextjs', 'app', 'directory', 'page', 'route', 'layout', 'HMR', 'hot', 'module', 'reload'] -published: 2024-07-01T11:22:33.444Z -modified: 2024-07-01T11:22:33.444Z -permalink: https://chris.lu/web_development/tutorials/next-js-static-mdx-blog/first-typescript-page -section: Web development ---- - -import { sharedMetaDataArticle } from '@/shared/metadata-article' -import Breadcrumbs from '@/components/tutorial/Breadcrumbs' -import Pagination from '@/components/tutorial/Pagination' - -export const metadata = { - title: frontmatter.title, - description: frontmatter.description, - keywords: frontmatter.keywords, - alternates: { - canonical: frontmatter.permalink, - }, - openGraph: { - ...sharedMetaDataArticle.openGraph, - images: [{ - type: "image/png", - width: 1200, - height: 630, - url: '/web_development/og/tutorials_next-static-mdx-blog/opengraph-image' - }], - url: frontmatter.permalink, - section: frontmatter.section, - publishedTime: frontmatter.published, - modifiedTime: frontmatter.modified, - tags: frontmatter.keywords, - }, -} - -%toc% - -
- - - -# First Typescript page - -I hope you are still there because it is finally time to start coding (a bit) 🙂, but first a bit of theory about routing 😉 (feel free to skip the first chapter if you know it already), and then we create our very first page - -## Next.js routing/pages - -If you used the Next.js **pages router** in the past but did not yet use the **app router**, then here is a short introduction - -With the **pages router**, if you wanted to have a page at `www.example.com/foo`, then you would create a file named foo.tsx inside of the `pages` folder - -With the **app router** if you want a page at that same `www.example.com/foo` path, then you create a folder named `foo` inside of the `app` folder - -Let's assume the path is now `www.example.com/foo/bar`, when using the **pages router** `foo` would be a folder inside of `pages`, but `bar` would be a file, however, when using the **app router**, every segment is a folder - -The other difference is the page files, with the **pages router**, the name of the file gets used for the last segment, in the **app router**, every page is always named **page**(.jsx/.tsx) - -The advantage of using a folder as the last segment and having a convention that says that every page needs to be named page is that with the introduction of the **app router** Next.js added a bunch of other [file conventions](https://nextjs.org/docs/app/building-your-application/routing#file-conventions), for example you can have a layout in every segment folder, you can have a not-found file in every folder and so on, meaning you can create different layouts for different parts of the website easily, or even create different error, loading, not-found, ... pages for various parts of your project (without having to add logic inside those files to check what the current path is and then show a different layout, UI or content based on it) - -A feature you will already know if you used the **pages router** is [dynamic routes](https://nextjs.org/docs/app/building-your-application/routing/dynamic-routes) - -To create a dynamic route segment, you wrap the folder's name in square brackets, for example, with a folder structure like this `/app/articles/[slug]`, the **slug** could be anything, so the URL `www.example.com/articlers/foo` the slug would be foo and for another URL `www.example.com/articlers/bar` the slug would be a bar, to retrieve the slug value you would use a page with the following code: - -```tsx title="/app/articles/[slug]/page.tsx" -export default function Page({ params }: { params: { slug: string } }) { - return
My article slug is: {params.slug}
-} -``` - -Other useful features were introduced with the new folder-based routing, like [route groups](https://nextjs.org/docs/app/building-your-application/routing/route-groups) or [parallel routes](https://nextjs.org/docs/app/building-your-application/routing/parallel-routes) but those I will not cover them in this tutorial, but it is good to know they exist as they might become useful sooner or later as your project grows - -> [!MORE] -> [Next.js "routing" documentation](https://nextjs.org/docs/app/building-your-application/routing) - -## Our 1st typescript page - -Start by opening the `app` folder, which create-next-app created for us during the initial setup of our project - -If you have a bit of time, have a look at what Next.js has put in there (it's always good to have a look at what the Next.js team recommends), but after that, delete all the files in the `/app` folder as I want to go step by step through the process of creating a Next.js blog, you can also delete the content in the `/public` folder as we won't need the assets of the demo project anymore - -Next, create a new file in the `app` folder and name it `page.tsx` (or `page.jsx` if you choose to use javascript) - -Then add the following content into the `page.tsx` file and finally save it - -```tsx title="/app/page.tsx" showLineNumbers -export default function Home() { - - return ( - <> -

Hello World?

- - ) - -} -``` - -Congratulations 🎉 you just coded your first Next.js page in typescript, if you liked this post, please consider making a [donation](https://buymeacoffee.com/chriswwweb) ❤️ as it will help me create more content and keep it free for everyone - -## Start the dev(elopment) server - -Now open the VSCode terminal if it isn't open yet (or use your favorite command line tool), and let's use one of the 4 commands create-next-app added to the package.json scripts (and which we documented in README.md earlier) to start the development server: - -```shell -npm run dev -``` - -Now, in the terminal, press `Ctrl` and then click on the Next.js local server URL or open your browser and put the following URL into the address bar: [http://localhost:3000/](http://localhost:3000/) - -As you can see, Next.js has compiled our typescript page, and the development server has responded to the browser request, which is why we can see our "Hello World?" message - -## The root layout is required - -Go back to VSCode and look at the list of files in the sidebar - -You will notice that Next.js re-added the `/app/layout.tsx` file (when we started the dev server) we just deleted earlier because this layout file is called the **root layout** and it is **required** (and that's because Next.js is a clever framework that in many places helps you do the right thing 😉), also if you look at your VSCode terminal you will see that Next.js printed the following line, informing us that it created the layout file for us: - -> Your page app/page.tsx did not have a root layout. We created app\layout.tsx for you. - -If you open the file, you will see that on top, it has added a metadata object, this is the Next.js metadata API that we will soon use in layouts and pages to set the tags in the `` element, like the **title** and **description** (I will go more in detail in a future chapter) but also open graph tags - -```tsx title="/app/layout.tsx" -export const metadata = { - title: 'Next.js', - description: 'Generated by Next.js', -} -``` - -The second part it has added is just a basic typed React / Next.js layout setup - -```tsx title="/app/layout.tsx" -export default function RootLayout({ - children, -}: { - children: React.ReactNode -}) { - return ( - - {children} - - ) -} -``` - -This layout is still very basic, it only contains the bare minimum of HTML elements to create a page - -In the props object, we have the children that we put into the body - -> [!MORE] -> [Next.js "layouts" documentation](https://nextjs.org/docs/app/building-your-application/routing/layouts-and-templates) - -## Edit the first page - -As you might have noticed, I added a question mark in the **Hello World?** heading text - -Let's replace the question mark with an exclamation mark and then save the file - -> [!NOTE] -> As soon as you save the file, you will see in the terminal that Next.js prints a message **Compiled in Xms (Y modules)**, which shows you that Next.js detected changes in your code base and did a new build for you - -Now go back into your browser to check the page ([https://localhost:3000/](https://localhost:3000/)), and even though you haven't reloaded the page, you will notice that your changes have been applied, which is because Next.js has a feature called fast refresh - -## Next.js fast refresh / Hot Module Reload package - -Let's go back to our project, make sure the dev server is running or use the `npm run dev` command to start it and then open [http://localhost:3000/](http://localhost:3000/) in your browser - -In your browser **right click** somewhere on the page and then select **Inspect** (or open the **browser dev tools** by pressing the `F12` and then open the `Elements` tab), you will see that Next.js injects a bunch of Javascript code into our page and some of those javascript files are heavy, this is because Next.js adds, for example, a tool called **Hot Module Reload** (HMR) (all the HMR code won't get loaded in production, Next.js only adds those files to our page when in development mode) - -HMR starts watching for file changes as soon as you start the development server - -If we edit and save (or add a new file), HMR will detect the change and tell Next.js to (re-)compile the files, then Next.js [fast refresh](https://nextjs.org/docs/architecture/fast-refresh) will update the output in the browser for us - -> [!MORE] -> [Next.js "fast refresh" documentation](https://nextjs.org/docs/architecture/fast-refresh) - -## Stop the dev(elopment) server - -We started the development server earlier, but how do we stop it? If you have never done it before, it might not be obvious how to do it - -The easiest way to stop the development is to press `Ctrl+S` (macOS: `⌘S`, Linux: `Ctrl+S`) - -Then you will get asked if you want to quit: - -> Terminate batch job (Y/N)? - -To confirm, either enter `Y` and then press `ENTER` or just press `Ctrl+S` (macOS: `⌘S`, Linux: `Ctrl+S`) again - - - -
diff --git a/app/web_development/tutorials/next-js-static-mdx-blog/frontmatter-plugin/page.mdx b/app/web_development/tutorials/next-js-static-mdx-blog/frontmatter-plugin/page.mdx deleted file mode 100644 index 55d7926e..00000000 --- a/app/web_development/tutorials/next-js-static-mdx-blog/frontmatter-plugin/page.mdx +++ /dev/null @@ -1,241 +0,0 @@ ---- -title: Frontmatter plugin - Tutorial -description: Frontmatter plugin - Next.js static MDX blog | www.chris.lu Web development tutorials -keywords: ['Frontmatter', 'YAML', 'plugin', 'remark', 'mdx', 'nextjs frontmatter', 'next/mdx', 'nextjs'] -published: 2024-07-01T11:22:33.444Z -modified: 2024-07-01T11:22:33.444Z -permalink: https://chris.lu/web_development/tutorials/next-js-static-mdx-blog/frontmatter-plugin -section: Web development ---- - -import { sharedMetaDataArticle } from '@/shared/metadata-article' -import Breadcrumbs from '@/components/tutorial/Breadcrumbs' -import Pagination from '@/components/tutorial/Pagination' - -export const metadata = { - title: frontmatter.title, - description: frontmatter.description, - keywords: frontmatter.keywords, - alternates: { - canonical: frontmatter.permalink, - }, - openGraph: { - ...sharedMetaDataArticle.openGraph, - images: [{ - type: "image/png", - width: 1200, - height: 630, - url: '/web_development/og/tutorials_next-static-mdx-blog/opengraph-image' - }], - url: frontmatter.permalink, - section: frontmatter.section, - publishedTime: frontmatter.published, - modifiedTime: frontmatter.modified, - tags: frontmatter.keywords, - }, -} - -%toc% - -
- - - -# Frontmatter introduction - -As stated in the ["GitHub docs" frontmatter documentation](https://docs.github.com/en/contributing/writing-for-github-docs/using-yaml-frontmatter), the [Jekyll](https://jekyllrb.com/docs/front-matter/) static site generator was the first to popularize frontmatter, but today a lot of frameworks and libraries add support for frontmatter, so maybe your markdown files already have frontmatter and you want to be able to use that data, or as I did when I started with this tutorial, you just learned about frontmatter now and think it is a good way to store metadata in your MDX files - -You might have noticed that frontmatter is sometimes called "yaml frontmatter" or "frontmatter yaml", this is because frontmatter uses the [YAML data language](https://yaml.org/spec/1.2.2/) - -To add frontmatter to a document, you start by adding 3 dashes (`---`), then add your frontmatter yaml, and finally close the frontmatter block with another 3 dashes (`---`) - -I already mentioned the GitHub and Jekyll documentation about frontmatter, they both specify predefined frontmatter variables, but because we will add our own frontmatter support, we are free to use whatever variables we think are useful for our project, there is, however one convention that you should follow, and that is to always put the frontmatter part on top of your MDX page or markdown document - -> [!MORE] -> ["YAML data language" specification](https://yaml.org/spec/1.2.2/) - -## Frontmatter plugins installation - -What we will do in this chapter is add two plugins to our next/mdx setup that will read the frontmatter part of our MDX pages and then automatically populate the Next.js metadata object (using the frontmatter metadata) for us - -First, we install the 2 remark plugins using the following command: - -```shell -npm i remark-frontmatter remark-mdx-frontmatter --save-exact -``` - -[remark-frontmatter](https://github.com/remarkjs/remark-frontmatter) is a plugin that will parse the frontmatter, without this plugin, an MDX page with frontmatter would just display the frontmatter as text (when getting rendered), after enabling this plugin, the frontmatter part will not show up in your MDX pages anymore but will get parsed as frontmatter yaml - -[remark-mdx-frontmatter](https://github.com/remcohaszing/remark-mdx-frontmatter) is a plugin that is important as it will put the parsed frontmatter values into a variable inside of our MDX documents, the variable is called frontmatter by default but you can change the name using the options of the plugin - -Next, we add the frontmatter plugins to our next/mdx configuration: - -```js title="next.config.mjs" showLineNumbers {10-11} /remarkFrontmatter, remarkMdxFrontmatter/#special -import { withSentryConfig } from '@sentry/nextjs' -import createMdx from '@next/mdx' -import rehypeMDXImportMedia from 'rehype-mdx-import-media' -import rehypePrettyCode from 'rehype-pretty-code' -import { readFileSync } from 'fs' -import rehypeSlug from 'rehype-slug' -import { remarkTableOfContents } from 'remark-table-of-contents' -import remarkGfm from 'remark-gfm' -import { rehypeGithubAlerts } from 'rehype-github-alerts' -import remarkFrontmatter from 'remark-frontmatter' -import remarkMdxFrontmatter from 'remark-mdx-frontmatter' - -const nextConfig = (phase) => { - - const themePath = new URL('./node_modules/material-theme/themes/OneDark-Pro.json', import.meta.url) - const themeFileContent = readFileSync(themePath, 'utf-8') - - /** @type {import('rehype-pretty-code').Options} */ - const rehypePrettyCodeOptions = { - theme: JSON.parse(themeFileContent), - keepBackground: false, - defaultLang: { - block: 'tsx', - inline: 'js', - }, - tokensMap: { - fn: 'entity.name.function', - cmt: 'comment', - str: 'string', - var: 'entity.name.variable', - obj: 'variable.other.object', - prop: 'meta.property.object', - int: 'constant.numeric', - }, - } - - /** @type {import('remark-table-of-contents').IRemarkTableOfContentsOptions} */ - const remarkTableOfContentsOptions = { - containerAttributes: { - id: 'articleToc', - }, - navAttributes: { - 'aria-label': 'table of contents' - }, - maxDepth: 3, - } - - /** @type {import('remark-gfm').Options} */ - const remarkGfmOptions = { - singleTilde: false, - } - - const withMDX = createMdx({ - extension: /\.(md|mdx)$/, - options: { - // optional remark and rehype plugins - remarkPlugins: [remarkFrontmatter, remarkMdxFrontmatter, [remarkTableOfContents, remarkTableOfContentsOptions], [remarkGfm, remarkGfmOptions]], - rehypePlugins: [rehypeGithubAlerts, rehypeSlug, rehypeMDXImportMedia, [rehypePrettyCode, rehypePrettyCodeOptions]], - remarkRehypeOptions: { - footnoteLabel: 'Notes', - footnoteLabelTagName: 'span', - }, - }, - }) -``` - -Lines 10 to 11: we import the 2 frontmatter plugins - -Line 57: we add both to our remark plugins list - -> [!NOTE] -> Something I will not cover here, but if you want to go a step further and are interested in adding linting for the frontmatter part, then have a look at [remark-lint-frontmatter-schema](https://github.com/JulianCataldo/remark-lint-frontmatter-schema) - -> [!MORE] -> [mdx.js "Frontmatter" documentation](https://mdxjs.com/guides/frontmatter/) -> [Next.js "Frontmatter" documentation](https://nextjs.org/docs/app/building-your-application/configuring/mdx#frontmatter) -> [npmjs.com "remark-frontmatter" page](https://www.npmjs.com/package/remark-frontmatter) -> [npmjs.com "remark-mdx-frontmatter" page](https://www.npmjs.com/package/remark-mdx-frontmatter) - -Now it is time to create an example where we define some frontmatter, let both plugins do their magic, and then use the frontmatter variable to populate the Next.js metadata object - -## Frontmatter for metadata (and more) - -Let's reuse our gfm_plaground page one more time - -First, remove the current metadata and then add this instead: - -```md title="/app/tutorial_examples/gfm_playground/page.mdx" showLineNumbers ---- -title: GFM playground page -keywords: ['gfm', 'playground', 'frontmatter', 'mdx'] -published: 2024-05-24T19:14:23.792Z -modified: 2024-05-24T19:14:23.792Z -permalink: http://localhost:3000/tutorial_examples/gfm_playground -siteName: My website name ---- - -export const metadata = { - title: frontmatter.title, - keywords: frontmatter.keywords, - openGraph: { - url: frontmatter.permalink, - siteName: frontmatter.siteName, - type: 'article', - publishedTime: frontmatter.published, - modifiedTime: frontmatter.modified, - tags: frontmatter.keywords, - } -} -``` - -Lines 1 to 8: we first added our frontmatter on top of the page with some variables we will use on the MDX page itself - -Lines 10 to 21: we created a Next.js metadata object and used the frontmatter variable (that holds all the key/value pairs from our frontmatter above) to populate it - -Finally, make sure the dev server is running, then open the playground page [http://localhost:3000/tutorial_examples/gfm_playground](http://localhost:3000/tutorial_examples/gfm_playground) in your browser and then right-click in the page to have a look at the meta tags of the `` element - -You should be getting the following result: - -```html showLineNumbers - - -GFM playground page | example.com - - - - - - - - - - - - - -``` - -Lines 1 to 2: are the viewport and charset Next.js adds by default - -Line 3: is the default [HTML title element](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/title) that has the `frontmatter.title` as value and uses the template we have set in the layout file - -Line 4: is similar but holds the **description** - -Line 5: we have a **keywords** meta tag, which contains some keywords we added to our frontmatter, it is an example of how an array gets transformed into a string, but search engines like [google apparently don't use it](https://developers.google.com/search/docs/crawling-indexing/special-tags) - -Lines 6 and 7: we have **open graph title and description**, which Next.js sets based on the default title and description - -Lines 8 and 9: we have the **open graph URL and sitename**, which are two values we have set in our frontmatter (and they overwrite the open graph values set in the layout) - -Line 10: we add a static **article** value as **open graph type** just to demonstrate the following two meta tags at lines 11 and 12 - -Line 11 and 12: we have two new meta tags, which have a property that is NOT prefixed with `og:` but as can be seen on [https://ogp.me/#type_article](https://ogp.me/#type_article) they are part of the open graph protocol, the open graph **article namespace** needs to have the **open graph type** set to **article** and then you get tags that are **prefixed** with `article:` if however the type is for example set to website those two metatags will not show up - -Lines 13 to 16: we have the keywords that get used as tags for our open graph article, unlike the **keywords** meta tag the **tags** get split into multiple tags - -Congratulations 🎉 you just learned how to set metadata for MDX pages and how to add frontmatter to MDX documents - -If you liked this post, please consider making a [donation](https://buymeacoffee.com/chriswwweb) ❤️ as it will help me create more content and keep it free for everyone - - - -
diff --git a/app/web_development/tutorials/next-js-static-mdx-blog/github-flawored-markdown-plugin/page.mdx b/app/web_development/tutorials/next-js-static-mdx-blog/github-flawored-markdown-plugin/page.mdx deleted file mode 100644 index 8454bfc4..00000000 --- a/app/web_development/tutorials/next-js-static-mdx-blog/github-flawored-markdown-plugin/page.mdx +++ /dev/null @@ -1,647 +0,0 @@ ---- -title: GitHub flawored markdown plugin - Tutorial -description: GitHub flawored markdown plugin - Next.js static MDX blog | www.chris.lu Web development tutorials -keywords: ['GitHub', 'flawored', 'plugin', 'gfm', 'markdown', 'mdx', 'remark-gfm', 'next/mdx', 'remark'] -published: 2024-07-01T11:22:33.444Z -modified: 2024-07-01T11:22:33.444Z -permalink: https://chris.lu/web_development/tutorials/next-js-static-mdx-blog/github-flawored-markdown-plugin -section: Web development ---- - -import { sharedMetaDataArticle } from '@/shared/metadata-article' -import Breadcrumbs from '@/components/tutorial/Breadcrumbs' -import Pagination from '@/components/tutorial/Pagination' - -export const metadata = { - title: frontmatter.title, - description: frontmatter.description, - keywords: frontmatter.keywords, - alternates: { - canonical: frontmatter.permalink, - }, - openGraph: { - ...sharedMetaDataArticle.openGraph, - images: [{ - type: "image/png", - width: 1200, - height: 630, - url: '/web_development/og/tutorials_next-static-mdx-blog/opengraph-image' - }], - url: frontmatter.permalink, - section: frontmatter.section, - publishedTime: frontmatter.published, - modifiedTime: frontmatter.modified, - tags: frontmatter.keywords, - }, -} - -%toc% - -
- - - -# GitHub flawored markdown plugin(s) - -There is more than just one plugin related to GitHub markdown: - -* [the "remark-gfm" plugin](https://www.npmjs.com/package/remark-gfm) -* ["rehype-github" plugins list README](https://github.com/rehypejs/rehype-github) - -The **remark-gfm plugin** transforms GitHub Flavored Markdown (GFM) into HTML, the remark-gfm plugin will add the same features to your MDX pages that GitHub has introduced in their own GitHub Flavored Markdown (GFM), for example, it has support for **autolink literals**, **footnotes**, **strikethrough**, **markdown tables**, **tasklists**, ... - -**rehype "GitHub" plugins repository** is a collection of plugins, some are remark plugins, but most are rehype plugins, this repository for example, it contains **rehype-github-color**, which will add a color preview rectangle to your hex color codes, check out the repository for a complete list of plugins it has to offer - -> [!NOTE] -> If you are curious to know what GitHub uses on its website, have a look at the GitHub [cmark-gfm repository](https://github.com/github/cmark-gfm) repository, which is a fork of the CommonMark reference implementation, this does not contain everything GitHub is doing -> -> For example, they transform quotes that start with a special alert type section (`[!ALERT_TYPE]`) into alerts ([Docusaurus](https://docusaurus.io/docs/markdown-features/admonitions) and [Gatsby](https://www.gatsbyjs.com/plugins/gatsby-remark-admonitions/) call them **admonitions**, I have also seen some remark plugins on [npmjs](https://www.npmjs.com/search?q=callouts) calling them **callouts**) and those alerts are not something the **remark-gfm** plugin supports, for that reason I created a rehype plugin called [rehype-github-alerts](https://www.npmjs.com/package/rehype-github-alerts) and I will show you how to use it in a bit - -> [!MORE] -> [npmjs.com "remark-gfm plugin" page](https://www.npmjs.com/package/remark-gfm) -> ["rehype-github plugins list" repository](https://github.com/rehypejs/rehype-github) - -## GitHub flavored markdown (gfm) plugin - -By adding the [remark "GitHub Flavored Markdown" (GFM) plugin](https://www.npmjs.com/package/remark-gfm), we extend the syntax features provided by the original markdown with extensions for autolink literals, footnotes, strikethrough, tables, tasklists and some more, you may already know most of them from writing markdown in GitHub READMEs, Issues and comments and might have thought they were part of the base markdown syntax - -Installing and setting up the plugin is easy, as we will see in a bit, but in case you want to know more or have a look at their releases list, then I recommend checking out their [remark-gfm](https://github.com/remarkjs/remark-gfm#install) README that has a well-written chapter about "what it is" and "what it does" as well as some examples - -We first need to install the **remark-gfm** package by using the following command: - -```shell -npm i remark-gfm --save-exact -``` - -Next, we edit our `next.config.mjs` configuration file to add the plugin to the `next/mdx` setup, like so: - -```js title="next.config.mjs" showLineNumbers {8} {44-47} /[remarkGfm, remarkGfmOptions]/#special -import { withSentryConfig } from '@sentry/nextjs' -import createMdx from '@next/mdx' -import rehypeMDXImportMedia from 'rehype-mdx-import-media' -import rehypePrettyCode from 'rehype-pretty-code' -import { readFileSync } from 'fs' -import rehypeSlug from 'rehype-slug' -import { remarkTableOfContents } from 'remark-table-of-contents' -import remarkGfm from 'remark-gfm' - -const nextConfig = (phase) => { - - const themePath = new URL('./node_modules/material-theme/themes/OneDark-Pro.json', import.meta.url) - const themeFileContent = readFileSync(themePath, 'utf-8') - - /** @type {import('rehype-pretty-code').Options} */ - const rehypePrettyCodeOptions = { - theme: JSON.parse(themeFileContent), - keepBackground: false, - defaultLang: { - block: 'tsx', - inline: 'js', - }, - tokensMap: { - fn: 'entity.name.function', - cmt: 'comment', - str: 'string', - var: 'entity.name.variable', - obj: 'variable.other.object', - prop: 'meta.property.object', - int: 'constant.numeric', - }, - } - - /** @type {import('remark-table-of-contents').IRemarkTableOfContentsOptions} */ - const remarkTableOfContentsOptions = { - containerAttributes: { - id: 'articleToc', - }, - navAttributes: { - 'aria-label': 'table of contents' - }, - maxDepth: 3, - } - - /** @type {import('remark-gfm').Options} */ - const remarkGfmOptions = { - singleTilde: false, - } - - const withMDX = createMdx({ - extension: /\.(md|mdx)$/, - options: { - // optional remark and rehype plugins - remarkPlugins: [[remarkTableOfContents, remarkTableOfContentsOptions], [remarkGfm, remarkGfmOptions]], - rehypePlugins: [rehypeSlug, rehypeMDXImportMedia, [rehypePrettyCode, rehypePrettyCodeOptions]], - }, - }) -``` - -Line 8: we import the **remark-gfm** plugin - -Lines 45 to 48: we add a configuration object for the plugin, we first add the options type from the package to have strictly typed options, but there are few things we can or NEED to configure, there is just one option I personally like to disable, and that is the **singleTilde** which we set to false, why it is true by default is well explained in their [README](https://github.com/remarkjs/remark-gfm?tab=readme-ov-file#options) so I will just quote the official explanation here: - -> whether to support strikethrough with a single tilde; single tildes work on github.com but are technically prohibited by GFM; you can always use 2 or more tildes for strikethrough - -Line 53: we add an array containing the **remarkGfm** plugin as well as the **remarkGfmOptions** options object we just created to the **rehypePlugins** array - -## GFM playground page - -Now that the plugin is installed, let's create some "GitHub Flavored Markdown" (GFM) examples using a new playground page - -First, go into the `/app/tutorial_examples` folder and then create a new `gfm_playground` folder - -Inside the `gfm_playground` folder, create a new `page.mdx` MDX page and add the following content: - -```md title="/app/tutorial_examples/gfm_playground/page.mdx" showLineNumbers -
- -~~strikethrough~~ - -Table: -| Left | Right | -| -------- | ------- | -| Foo | Bar | -| ~~strikethrough~~ | 😃 | -| `code` | [external link](https://google.com) | - -Autolink: https://www.example.com - -Tasklist: -* [x] foo -* [ ] bar - -
- -``` - -We added some examples of new features that are now available: - -* like ~~strikethrough~~ text -* a table, where the 1st row has text in both cells, the 2nd row has a strikethrough text and an emoji, and the 3rd row has inline code and a link -* A link that automatically gets converted to an anchor element (automatic here means you don't need to use the regular markdown **link** syntax, it is enough to add a URL, and it gets automatically transformed into a link) -* A tasklist consisting of 2 tasks, the 1st one is checked the 2nd is unchecked - -> [!WARN] -> The remark-gfm tasklist feature is called tasklist for a reason -> -> What I mean by that is that the following syntax `[ ]` and `[x]` with the list syntax is NOT going to generate a checkbox: -> -> ```md -> [ ] a checkbox -> ``` -> -> So when using tasklists, you need to use the exact syntax that remark-gfm expects, which is a **list** of tasks (checkboxes): -> -> ```md -> * [ ] a task -> ``` - -If you open the playground in the browser and inspect the HTML source, you will notice that all checkboxes are marked as disabled (this is a remark-gfm feature, NOT a bug) - -### Making GFM tasklists interactive - -It is, however, possible to customize the **checkboxes** of a **tasklist** using the `mdx-components.tsx` file that is in the root of our project: - -```tsx title="mdx-components.tsx" showLineNumbers {51-53} -import type { MDXComponents } from 'mdx/types' -import BaseLink from '@/components/base/Link' -import type { Route } from 'next' -import BaseImage from '@/components/base/Image' -import type { ImageProps } from 'next/image' - -// This file allows you to provide custom React components -// to be used in MDX files. You can import and use any -// React component you want, including components from -// other libraries. - -// This file is required to use MDX in `app` directory. -export function useMDXComponents(components: MDXComponents): MDXComponents { - return { - // Allows customizing built-in components, e.g. to add styling. - ul: ({ children, ...props }) => ( -
    - {children} -
- ), - a: ({ children, href, ...props }) => ( - - {children} - - ), - img: (props) => (), - aside: ({ children, ...props }) => { - const tocHighlightProps = { - headingsToObserve: 'h1, h2, h3', - rootMargin: '-5% 0px -50% 0px', - threshold: 1, - ...props - } - return ( - <> - {props.id === 'articleToc' ? ( - - {children} - - ) : ( - - ) - } - - ) - }, - input: (props) => { - console.log(props) - }, - ...components, - } -} -``` - -Lines 51 to 53: for any input element, we do a `console.log` to get an idea of what the props of an input element are - -In this case, our `console.log` will print the following in our terminal: - -```shell -{ type: 'checkbox', checked: true, disabled: true } -{ type: 'checkbox', disabled: true } -``` - -As you can see, all checkboxes are **disabled**, this is the default behavior for remark-gfm tasklists - -### Removing the tasklist checkbox disabled attribute - -You might want to remove the `disabled` attribute, we can do that easily by using a destructuring assignment to remove the `disabled` attribute and put the remaining props into a new object that we then pass to a custom input element, like so: - -```tsx title="mdx-components.tsx" showLineNumbers {52-55} -import type { MDXComponents } from 'mdx/types' -import BaseLink from '@/components/base/Link' -import type { Route } from 'next' -import BaseImage from '@/components/base/Image' -import type { ImageProps } from 'next/image' - -// This file allows you to provide custom React components -// to be used in MDX files. You can import and use any -// React component you want, including components from -// other libraries. - -// This file is required to use MDX in `app` directory. -export function useMDXComponents(components: MDXComponents): MDXComponents { - return { - // Allows customizing built-in components, e.g. to add styling. - ul: ({ children, ...props }) => ( -
    - {children} -
- ), - a: ({ children, href, ...props }) => ( - - {children} - - ), - img: (props) => (), - aside: ({ children, ...props }) => { - const tocHighlightProps = { - headingsToObserve: 'h1, h2, h3', - rootMargin: '-5% 0px -50% 0px', - threshold: 1, - ...props - } - return ( - <> - {props.id === 'articleToc' ? ( - - {children} - - ) : ( - - ) - } - - ) - }, - input: (props) => { - console.log(props) - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { disabled, ...newProps } = props - return () - }, - ...components, - } -} -``` - -Lines 52 to 55: we use a destructuring assignment to split the original props into the disabled attribute and the remaining props, we need to disable the eslint `@typescript-eslint/no-unused-vars` for that line as we won't use the disabled variable, then we create a new input element and pass the remaining props - -> [!TIP] -> When you make changes to the `mdx-components.tsx` file, Next.js will not always instantly detect those changes and reload the project, the easiest trick I have found to make sure Next.js notices the changes is also to open the `next.config.mjs` configuration file and make a small change like adding a line break at the end, then save the Next.js configuration file, this will cause a reload of the project, which ensures your mdx-components changes get taken into account - -However, if we remove the `disabled` attribute and start our dev server, we see that we get an error in our terminal: - -> Warning: You provided a `checked` prop to a form field without an `onChange` handler. This will render a read-only field. If the field should be mutable, use `defaultChecked`. Otherwise, set either `onChange` or `readOnly` - -### Checkbox react component - -This error tells us that checkboxes that have the `checked` prop also need to have an `onChange` handler, and for that reason, we will create a custom checkbox React component, go into the `/components/base` folder, and then create a new `Checkbox.tsx` file with the following content: - -```tsx title="/components/base/Checkbox.tsx" showLineNumbers -'use client' - -import { useState } from 'react' - -const BaseCheckbox: React.FC> = (props): JSX.Element => { - - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { disabled, checked, ...newProps } = props - - const [isChecked, setIsChecked] = useState(checked ? true : false) - - const changeHandler = (event: React.ChangeEvent) => { - console.log(event.target.value) - setIsChecked((previous) => { - return !previous - }) - } - - return () - -} - -export default BaseCheckbox -``` - -Line 1: we first add the `'use client'` as our component will have an **onChange** handler and also because we will use React **state** - -Line 5: we create a component and use the types for an Input Element to make it strictly typed - -Line 8: we do the same thing we did in the mdx-components file, but we also extract the **checked** prop as we will need for the initial value of our state - -Line 10: we create our **is checked** state, which will turn the component into a controlled checkbox component - -Lines 12 to 17: we create a basic **onChange** handler that will log the current value in the console and will update the state, every time the user interacts with the checkbox, we read the current checked value, and if true, we set it to false, and if false we set it true - -Line 19: we create an input element of type checkbox and add all our attributes - -### mdx-components custom tasklist checkbox - -Now that we have our custom checkbox component, we can start using it in the `mdx-components.tsx` file, like so: - -```tsx title="mdx-components.tsx" showLineNumbers {6} {52} -import type { MDXComponents } from 'mdx/types' -import BaseLink from '@/components/base/Link' -import type { Route } from 'next' -import BaseImage from '@/components/base/Image' -import type { ImageProps } from 'next/image' -import BaseCheckbox from '@/components/base/Checkbox' - -// This file allows you to provide custom React components -// to be used in MDX files. You can import and use any -// React component you want, including components from -// other libraries. - -// This file is required to use MDX in `app` directory. -export function useMDXComponents(components: MDXComponents): MDXComponents { - return { - // Allows customizing built-in components, e.g. to add styling. - ul: ({ children, ...props }) => ( -
    - {children} -
- ), - a: ({ children, href, ...props }) => ( - - {children} - - ), - img: (props) => (), - aside: ({ children, ...props }) => { - const tocHighlightProps = { - headingsToObserve: 'h1, h2, h3', - rootMargin: '-5% 0px -50% 0px', - threshold: 1, - ...props - } - return ( - <> - {props.id === 'articleToc' ? ( - - {children} - - ) : ( - - ) - } - - ) - }, - input: (props) => { - return() - }, - ...components, - } -} -``` - -Line 6: we import our checkbox component - -Line 52: we remove the previous code and use our checkbox component instead - -Of course, depending on your needs, what you would do now is update the **BaseCheckbox** onChange handler with some code to do something useful based on your needs, like for example, add code to send a POST request to the server or add code that updates a value in the localstorage of the user's browser - -> [!MORE] -> [GitHub "remark-gfm" repository](https://github.com/remarkjs/remark-gfm) -> [npmjs.com "remark-gfm" page](https://www.npmjs.com/package/remark-gfm) -> [GitHub "GFM markdown formatting" documentation](https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax) - -## remark-gfm Footnotes - -The footnotes are a bit more complex to use than the other remark-gfm features, which is why I decided to create a separate chapter just for them - -I also recommend you have a look at the [footnotes issues list](https://github.com/micromark/micromark-extension-gfm-footnote?tab=readme-ov-file#bugs) that got added to the footnotes README, as those answer some of the questions you might have when you start using the footnotes - -First, let's go back into our playground file and add a simple notes example: - -```md title="/app/tutorial_examples/gfm_playground/page.mdx" showLineNumbers {20-21} -
- -~~strikethrough~~ - -Table: -| Left | Right | -| -------- | ------- | -| Foo | Bar | -| ~~strikethrough~~ | 😃 | -| `code` | [external link](https://google.com) | - -Autolink: https://www.example.com - -Tasklist: -* [x] foo -* [ ] bar - -Example text with a note.[^1] -[^1]: This is the text of the note, [it can be a link too](https://www.example.com) - -
- -``` - -Lines 20 to 21: we add an example for footnotes - -If you launch the dev server and then open the playground URL [http://localhost:3000/tutorial_examples/gfm_playground](http://localhost:3000/tutorial_examples/gfm_playground) in your browser, you will notice that there are a few things that are not great - -First, our footnotes appear on the right, which is because our **main** element (in our layout) uses `display: flex` and the default **flex direction** is **row**, which was great for our table of contents but NOT for the footnotes, which we want to have on the bottom and not the right side - -The footnotes don't use a placeholder as does the TOC to place them anywhere in the document, this is because they are supposed always to be placed at the end of the page, there is even a linting rule [remark-lint-final-definition](https://github.com/remarkjs/remark-lint/tree/main/packages/remark-lint-final-definition) to make sure definitions are at the end - -So, to change the footnotes from being on the right side to being on the bottom, we need to add a bit of HTML and CSS to our project - -Let's start by adding a new HTML container element to our playground: - -```md title="/app/tutorial_examples/gfm_playground/page.mdx" showLineNumbers {1} {25} -
- -
- -~~strikethrough~~ - -Table: -| Left | Right | -| -------- | ------- | -| Foo | Bar | -| ~~strikethrough~~ | 😃 | -| `code` | [external link](https://google.com) | - -Autolink: https://www.example.com - -Tasklist: -* [x] foo -* [ ] bar - -Example text with a note.[^1] -[^1]: This is the text of the note, [it can be a link too](https://www.example.com) - -
- -
- -``` - -Line 1: we add our **core** container div - -Line 25: we close the div - -Next, we edit our `global.css` stylesheet to add the [flex-direction](https://developer.mozilla.org/en-US/docs/Web/CSS/flex-direction) CSS property: - -```css title="/app/global.css" showLineNumbers{49} {3} -main { - display: flex; - flex-direction: column; - max-width: var(--maxWidth); - margin-left: auto; - margin-right: auto; - margin-bottom: calc(var(--spacing) * 4); -} -``` - -Line 51: we add `flex-direction` and set it to `column` - -If you launch the dev server and then open the playground URL [http://localhost:3000/tutorial_examples/gfm_playground](http://localhost:3000/tutorial_examples/gfm_playground) in your browser, you will notice that the footnotes are now at the bottom where they should be - -> [!NOTE] -> Footnotes can be further customized, but the options to do that are not part of the [gfm options](https://github.com/remarkjs/remark-gfm?tab=readme-ov-file#options), instead you need to edit the [remark-rehype options](https://github.com/remarkjs/remark-rehype?tab=readme-ov-file#options), which is because **remark-rehype** is where the logic for the footnotes resides - -### Footnotes label(s) - -In the following example, we are going to change the label that is being used in the footnotes at the bottom, and we will change the element used for the footnotes label - -```js title="next.config.mjs" showLineNumbers {56-59} -import { withSentryConfig } from '@sentry/nextjs' -import createMdx from '@next/mdx' -import rehypeMDXImportMedia from 'rehype-mdx-import-media' -import rehypePrettyCode from 'rehype-pretty-code' -import { readFileSync } from 'fs' -import rehypeSlug from 'rehype-slug' -import { remarkTableOfContents } from 'remark-table-of-contents' -import remarkGfm from 'remark-gfm' - -const nextConfig = (phase) => { - - const themePath = new URL('./node_modules/material-theme/themes/OneDark-Pro.json', import.meta.url) - const themeFileContent = readFileSync(themePath, 'utf-8') - - /** @type {import('rehype-pretty-code').Options} */ - const rehypePrettyCodeOptions = { - theme: JSON.parse(themeFileContent), - keepBackground: false, - defaultLang: { - block: 'tsx', - inline: 'js', - }, - tokensMap: { - fn: 'entity.name.function', - cmt: 'comment', - str: 'string', - var: 'entity.name.variable', - obj: 'variable.other.object', - prop: 'meta.property.object', - int: 'constant.numeric', - }, - } - - /** @type {import('remark-table-of-contents').IRemarkTableOfContentsOptions} */ - const remarkTableOfContentsOptions = { - containerAttributes: { - id: 'articleToc', - }, - navAttributes: { - 'aria-label': 'table of contents' - }, - maxDepth: 3, - } - - /** @type {import('remark-gfm').Options} */ - const remarkGfmOptions = { - singleTilde: false, - } - - const withMDX = createMdx({ - extension: /\.(md|mdx)$/, - options: { - // optional remark and rehype plugins - remarkPlugins: [[remarkTableOfContents, remarkTableOfContentsOptions], [remarkGfm, remarkGfmOptions]], - rehypePlugins: [rehypeSlug, rehypeMDXImportMedia, [rehypePrettyCode, rehypePrettyCodeOptions]], - remarkRehypeOptions: { - footnoteLabel: 'Notes', - footnoteLabelTagName: 'span', - }, - }, - }) -``` - -Lines 56 to 59: we add the options for the footnotes to the **remarkRehypeOptions** object and NOT (as one might assume) to the **remarkGfmOptions** object (at lines 46 to 48) - -If you launch the dev server then open the playground URL [http://localhost:3000/tutorial_examples/gfm_playground](http://localhost:3000/tutorial_examples/gfm_playground) in your browser, you will notice that the footnotes label changed from the default "Footnotes" to "Notes" (this can be useful if you have a website that has content in multiple languages and you want to translate the label), then we also changed the element of the label (which by default is a `

`) to a `` - -Congratulations 🎉 you just added GitHub-flavored markdown support to your project and learned how to use the mdx-components file to make tasklists dynamic using your custom checkbox component - -If you liked this post, please consider making a [donation](https://buymeacoffee.com/chriswwweb) ❤️ as it will help me create more content and keep it free for everyone - -> [!MORE] -> ["footnotes bug list" in the micromark-extension-gfm-footnote readme](https://github.com/micromark/micromark-extension-gfm-footnote?tab=readme-ov-file#bugs) -> ["remark-gfm options" in the remark-gfm readme](https://github.com/remarkjs/remark-gfm?tab=readme-ov-file#options) -> ["remark-rehype options" in the remark-rehype readme](https://github.com/remarkjs/remark-rehype?tab=readme-ov-file#options) - - - -

diff --git a/app/web_development/tutorials/next-js-static-mdx-blog/github-like-alerts-plugin/page.mdx b/app/web_development/tutorials/next-js-static-mdx-blog/github-like-alerts-plugin/page.mdx deleted file mode 100644 index fdd19cd6..00000000 --- a/app/web_development/tutorials/next-js-static-mdx-blog/github-like-alerts-plugin/page.mdx +++ /dev/null @@ -1,281 +0,0 @@ ---- -title: GitHub-like alerts (admonitions/callouts) using the rehype-github-alerts plugin - Tutorial -description: GitHub-like alerts (admonitions/callouts) using the rehype-github-alerts plugin - Next.js static MDX blog | www.chris.lu Web development tutorials -keywords: ['GitHub', 'alerts', 'rehype', 'plugin', 'mdx', 'markdown', 'admonitions', 'callouts'] -published: 2024-07-01T11:22:33.444Z -modified: 2024-07-01T11:22:33.444Z -permalink: https://chris.lu/web_development/tutorials/next-js-static-mdx-blog/github-like-alerts-plugin -section: Web development ---- - -import { sharedMetaDataArticle } from '@/shared/metadata-article' -import Breadcrumbs from '@/components/tutorial/Breadcrumbs' -import Pagination from '@/components/tutorial/Pagination' - -export const metadata = { - title: frontmatter.title, - description: frontmatter.description, - keywords: frontmatter.keywords, - alternates: { - canonical: frontmatter.permalink, - }, - openGraph: { - ...sharedMetaDataArticle.openGraph, - images: [{ - type: "image/png", - width: 1200, - height: 630, - url: '/web_development/og/tutorials_next-static-mdx-blog/opengraph-image' - }], - url: frontmatter.permalink, - section: frontmatter.section, - publishedTime: frontmatter.published, - modifiedTime: frontmatter.modified, - tags: frontmatter.keywords, - }, -} - -%toc% - -
- - - -# GitHub-like alerts using the rehype-github-alerts plugin - -The [rehype-github-alerts](https://github.com/chrisweb/rehype-github-alerts) plugin to render alerts (**admonitions**/**callouts**) in a similar way to how GitHub does it, **rehype-github-alerts** is a plugin I did a while back after I first saw the [GitHub alerts RFC](https://github.com/orgs/community/discussions/16925) in which GitHub suggests adding a new alerts syntax to their GitHub markdown - -The **rehype github alerts** plugin is not a copy of the exact GitHub source code as the code used for their implementation is NOT open source, but the **rehype github alerts** attempts to mimic the [GitHub alerts](https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax#alerts) appearance and features, meaning that when you add alerts into your markdown and publish it on GitHub then the rendered alert output should be very similar to what you get when using this plugin, of course, the alerts style might be different for your project depending on what CSS you apply to them - -This plugin attempts to mimic the GitHub alerts by implementing the same features it is, however, possible to use the configuration to, for example, add new custom types of alerts (the default GitHub alert types are Note, Tip, Important, Warning, and Caution), also using custom CSS it is possible to change how they get displayed (check out the [**rehype github alerts** README](https://github.com/chrisweb/rehype-github-alerts?tab=readme-ov-file#rehype-github-alerts) for some documentation and more examples), as you probably noticed I use the plugin a lot on this website and my alerts look pretty different from what they look like on GitHub - -## rehype-github-alerts installation - -To add the **rehype github alerts** plugin, we first need to install the package: - -```shell -npm i rehype-github-alerts --save-exact -``` - -Next, we need to edit our Next.js configuration file to add the plugin to our MDX setup: - -```js title="next.config.mjs" showLineNumbers {9} /rehypeGithubAlerts/2#special -import { withSentryConfig } from '@sentry/nextjs' -import createMdx from '@next/mdx' -import rehypeMDXImportMedia from 'rehype-mdx-import-media' -import rehypePrettyCode from 'rehype-pretty-code' -import { readFileSync } from 'fs' -import rehypeSlug from 'rehype-slug' -import { remarkTableOfContents } from 'remark-table-of-contents' -import remarkGfm from 'remark-gfm' -import { rehypeGithubAlerts } from 'rehype-github-alerts' - -const nextConfig = (phase) => { - - const themePath = new URL('./node_modules/material-theme/themes/OneDark-Pro.json', import.meta.url) - const themeFileContent = readFileSync(themePath, 'utf-8') - - /** @type {import('rehype-pretty-code').Options} */ - const rehypePrettyCodeOptions = { - theme: JSON.parse(themeFileContent), - keepBackground: false, - defaultLang: { - block: 'tsx', - inline: 'js', - }, - tokensMap: { - fn: 'entity.name.function', - cmt: 'comment', - str: 'string', - var: 'entity.name.variable', - obj: 'variable.other.object', - prop: 'meta.property.object', - int: 'constant.numeric', - }, - } - - /** @type {import('remark-table-of-contents').IRemarkTableOfContentsOptions} */ - const remarkTableOfContentsOptions = { - containerAttributes: { - id: 'articleToc', - }, - navAttributes: { - 'aria-label': 'table of contents' - }, - maxDepth: 3, - } - - /** @type {import('remark-gfm').Options} */ - const remarkGfmOptions = { - singleTilde: false, - } - - const withMDX = createMdx({ - extension: /\.(md|mdx)$/, - options: { - // optional remark and rehype plugins - remarkPlugins: [[remarkTableOfContents, remarkTableOfContentsOptions], [remarkGfm, remarkGfmOptions]], - rehypePlugins: [rehypeGithubAlerts, rehypeSlug, rehypeMDXImportMedia, [rehypePrettyCode, rehypePrettyCodeOptions]], - remarkRehypeOptions: { - footnoteLabel: 'Notes', - footnoteLabelTagName: 'span', - }, - }, - }) -``` - -Line 9: we import the `rehype-github-alerts` plugin - -Line 56: we add the **rehypeGithubAlerts** plugin to our rehype plugins configuration array - -## Rehype github alerts in action - -The plugin is now ready to be used, so we can now add some examples to our playground: - -```md title="/app/tutorial_examples/gfm_playground/page.mdx" showLineNumbers {24-36} -
- -
- -~~strikethrough~~ - -Table: -| Left | Right | -| -------- | ------- | -| Foo | Bar | -| ~~strikethrough~~ | 😃 | -| `code` | [external link](https://google.com) | - -Autolink: https://www.example.com - -Tasklist: -* [x] foo -* [ ] bar - -Example text with a note.[^1] -[^1]: This is the text of the note, [it can be a link too](https://www.example.com) - -> [!NOTE] -> Highlights information that users should take into account, even when skimming. - -> [!TIP] -> Optional information to help a user be more successful. - -> [!IMPORTANT] -> Crucial information necessary for users to succeed. - -> [!WARNING] -> Critical content demanding immediate user attention due to potential risks. - -> [!CAUTION] -> Negative potential consequences of an action. - -
- -
- -``` - -> [!NOTE] -> If you get the following linting error for what is between the square brackets (`[]`): -> -> > Unexpected reference to undefined definition, expected corresponding definition (`!warning`) for a link or escaped opening bracket (`\[`) for regular text eslint(remark-lint-no-undefined-references) -> -> Then have a look at the previous chapter ["adding and configuring remark-lint"](#adding-and-configuring-remark-lint) to learn how to get rid of those - -If you now launch the dev server and then open the playground URL [http://localhost:3000/tutorial_examples/gfm_playground](http://localhost:3000/tutorial_examples/gfm_playground) in your browser, you will see that alerts get rendered - -### Styling rehype github alerts - -Now, to mimic the style alerts we have on GitHub, we will add some custom CSS to our `global.css` CSS file: - -```css title="/app/global.css" showLineNumbers{228} -.markdown-alert { - --github-alert-default-color: rgb(48, 54, 61); - --github-alert-note-color: rgb(31, 111, 235); - --github-alert-tip-color: rgb(35, 134, 54); - --github-alert-important-color: rgb(137, 87, 229); - --github-alert-warning-color: rgb(158, 106, 3); - --github-alert-caution-color: rgb(248, 81, 73); - - padding: 0.5rem 1rem; - margin-bottom: 16px; - border-left: 0.25em solid var(--github-alert-default-color); -} - -.markdown-alert>:first-child { - margin-top: 0; -} - -.markdown-alert>:last-child { - margin-bottom: 0; -} - -.markdown-alert-note { - border-left-color: var(--github-alert-note-color); -} - -.markdown-alert-tip { - border-left-color: var(--github-alert-tip-color); -} - -.markdown-alert-important { - border-left-color: var(--github-alert-important-color); -} - -.markdown-alert-warning { - border-left-color: var(--github-alert-warning-color); -} - -.markdown-alert-caution { - border-left-color: var(--github-alert-caution-color); -} - -.markdown-alert-title { - display: flex; - margin-bottom: 4px; - align-items: center; -} - -.markdown-alert-title>svg { - margin-right: 8px; -} - -.markdown-alert-note .markdown-alert-title { - color: var(--github-alert-note-color); -} - -.markdown-alert-tip .markdown-alert-title { - color: var(--github-alert-tip-color); -} - -.markdown-alert-important .markdown-alert-title { - color: var(--github-alert-important-color); -} - -.markdown-alert-warning .markdown-alert-title { - color: var(--github-alert-warning-color); -} - -.markdown-alert-caution .markdown-alert-title { - color: var(--github-alert-caution-color); -} -``` - -Congratulations 🎉 you have now added support for markdown alerts that are compatible with GitHub (flavored markdown) - -If you liked this post, please consider making a [donation](https://buymeacoffee.com/chriswwweb) ❤️ as it will help me create more content and keep it free for everyone - -> [!MORE] -> [GitHub "rehype-github-alerts" repository](https://github.com/chrisweb/rehype-github-alerts) -> [npmjs.com "rehype-github-alerts" page](https://www.npmjs.com/package/rehype-github-alerts) - - - -
diff --git a/app/web_development/tutorials/next-js-static-mdx-blog/headings-id-plugin/page.mdx b/app/web_development/tutorials/next-js-static-mdx-blog/headings-id-plugin/page.mdx deleted file mode 100644 index a9d189b6..00000000 --- a/app/web_development/tutorials/next-js-static-mdx-blog/headings-id-plugin/page.mdx +++ /dev/null @@ -1,158 +0,0 @@ ---- -title: Rehype slug plugin to add IDs to headings - Tutorial -description: Rehype slug plugin to add IDs to headings - Next.js static MDX blog | www.chris.lu Web development tutorials -keywords: ['CI/CD', 'Vercel', 'build', 'Production', 'preview'] -published: 2024-07-01T11:22:33.444Z -modified: 2024-07-01T11:22:33.444Z -permalink: https://chris.lu/web_development/tutorials/next-js-static-mdx-blog/headings-id-plugin -section: Web development ---- - -import { sharedMetaDataArticle } from '@/shared/metadata-article' -import Breadcrumbs from '@/components/tutorial/Breadcrumbs' -import Pagination from '@/components/tutorial/Pagination' - -export const metadata = { - title: frontmatter.title, - description: frontmatter.description, - keywords: frontmatter.keywords, - alternates: { - canonical: frontmatter.permalink, - }, - openGraph: { - ...sharedMetaDataArticle.openGraph, - images: [{ - type: "image/png", - width: 1200, - height: 630, - url: '/web_development/og/tutorials_next-static-mdx-blog/opengraph-image' - }], - url: frontmatter.permalink, - section: frontmatter.section, - publishedTime: frontmatter.published, - modifiedTime: frontmatter.modified, - tags: frontmatter.keywords, - }, -} - -%toc% - -
- - - -# Rehype slug plugin to add IDs to headings - -The [rehype-slug](https://github.com/rehypejs/rehype-slug) does not do much on its own, but it does something useful for other plugins, which is adding an ID to headings, this ID can then be used by plugins like [rehype-autolink-headings](https://github.com/rehypejs/rehype-autolink-headings) or a table of contents (TOC) plugin like [remark-table-of-contents](https://github.com/chrisweb/remark-table-of-contents) - -The **rehype-slug** plugin will transform the text of a heading into an ID, if there are two headings with the same text, then the plugin will add a number to the ID to make sure every heading has a unique ID - -## rehype-slug installation - -To install the **rehype-slug** plugin package, use the following command: - -```shell -npm i rehype-slug --save-exact -``` - -Now that the plugin is installed, we need to edit our Next.js configuration file and add it to our MDX setup: - -```js title="next.config.mjs" showLineNumbers {6} /rehypeSlug/2#special -import { withSentryConfig } from '@sentry/nextjs' -import createMdx from '@next/mdx' -import rehypeMDXImportMedia from 'rehype-mdx-import-media' -import rehypePrettyCode from 'rehype-pretty-code' -import { readFileSync } from 'fs' -import rehypeSlug from 'rehype-slug' - -const nextConfig = (phase) => { - - const themePath = new URL('./node_modules/material-theme/themes/OneDark-Pro.json', import.meta.url) - const themeFileContent = readFileSync(themePath, 'utf-8') - - /** @type {import('rehype-pretty-code').Options} */ - const rehypePrettyCodeOptions = { - theme: JSON.parse(themeFileContent), - keepBackground: false, - defaultLang: { - block: 'tsx', - inline: 'js', - }, - tokensMap: { - fn: 'entity.name.function', - cmt: 'comment', - str: 'string', - var: 'entity.name.variable', - obj: 'variable.other.object', - prop: 'meta.property.object', - int: 'constant.numeric', - }, - } - - const withMDX = createMdx({ - extension: /\.(md|mdx)$/, - options: { - // optional remark and rehype plugins - remarkPlugins: [[remarkTableOfContents, remarkTableOfContentsOptions]], - rehypePlugins: [rehypeSlug, rehypeMDXImportMedia, [rehypePrettyCode, rehypePrettyCodeOptions]], - }, - }) -``` - -Line 6: we import the **rehypeSlug** plugin - -Line 37: we add the plugin to our array of rehype plugins (in our MDX configuration) - -And that's already it, the plugin is now operational - -## Playground page to experiment with markdown headings - -To see this plugin in action and also in preparation for the table of contents (TOC) plugin experiments we will do in the next chapter, we are going to create a new playground - -First, go into the `/app/tutorial_examples` folder and then create a new `toc_playground` folder - -Inside the `toc_playground` folder, create a new `page.mdx` MDX page and add the following content: - -```md title="/app/tutorial_examples/toc_playground/page.mdx" -
- -# headline level 1 - -Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin aliquet lacus in magna congue, sed vestibulum lorem luctus. Proin efficitur libero ut nisi tincidunt sodales. Ut maximus, ex ac suscipit consequat, ligula nibh blandit quam, a vestibulum elit turpis vel ante. Sed fringilla mi ac odio varius pharetra. Mauris et ex in mi vulputate sagittis. Aenean imperdiet neque at diam consequat eleifend. Donec sit amet metus odio. Aliquam commodo mollis purus, at euismod odio fermentum vitae. Pellentesque pretium ipsum porta, gravida arcu vel, aliquam elit. Vivamus vehicula semper risus, feugiat euismod nisi feugiat nec. Ut pulvinar id purus pulvinar efficitur. Nulla ac libero sed felis posuere tristique sit amet ac nisl. Cras a purus ligula. Fusce convallis suscipit varius. - -## headline level 2 - -Aenean tristique, elit nec ultrices auctor, felis mauris interdum diam, et sagittis magna magna molestie erat. Donec sed purus aliquam, iaculis felis sed, laoreet nunc. Phasellus ultrices iaculis nisl, ac convallis lectus dignissim non. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Donec in enim posuere, maximus diam in, pharetra mauris. Nullam erat orci, posuere id lorem sed, aliquam tincidunt ex. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse auctor sem nunc, eget aliquet sem malesuada sit amet. Vivamus ornare mauris nisi, nec sodales justo elementum fermentum. Integer vel blandit neque. Pellentesque blandit, ligula a pulvinar vulputate, tortor nisi euismod tortor, ultrices eleifend nisl lectus sit amet nunc. Donec fermentum lacinia lectus. Maecenas feugiat blandit arcu, sed tincidunt odio sodales vel. - -### headline level 3 - -Maecenas urna risus, aliquet non est in, tristique venenatis tellus. Ut vulputate consectetur pharetra. Aliquam erat volutpat. Phasellus imperdiet blandit nisl, sit amet consequat libero luctus ac. Proin sodales, urna nec varius egestas, leo eros semper purus, in posuere risus libero tempus justo. Nunc a elit ullamcorper, lacinia ipsum sit amet, imperdiet ipsum. Nunc varius condimentum congue. Pellentesque tortor libero, vulputate eget dapibus sit amet, faucibus in nibh. Maecenas hendrerit augue velit, eu pretium sem consequat pellentesque. Duis elementum semper dolor ac posuere. Aenean vitae gravida elit. Maecenas mattis lorem mauris, ut tincidunt orci efficitur nec. - -#### headline level 4 - -Sed sodales mi id odio finibus, eu scelerisque dui convallis. Aliquam ornare urna luctus convallis faucibus. Cras quis vulputate urna. Aenean et ante vitae turpis mattis pretium in ut orci. Sed vitae porttitor mauris. Duis dignissim aliquam ante, imperdiet consequat magna iaculis vel. Nam sit amet ipsum a justo tincidunt pretium sed ut nisl. Mauris lacus dui, aliquam id placerat a, egestas ac lorem. Praesent pulvinar diam nec elit sollicitudin dignissim. Etiam condimentum mi et tellus ornare iaculis in eu lorem. Nam elit odio, venenatis ut dictum ac, elementum ac diam. Aenean at aliquam augue, vel sagittis eros. Praesent eget turpis at mi ornare fermentum in non magna. - -
- -``` - -We have added some markdown headings of different levels to our MDX page as well as some fake text using an online [lorem ipsum generator](https://www.lipsum.com/), the content is wrapped inside of an `
` element (which is optional but I like to use semantic elements whenever it makes sense) - -Now launch the dev server, then open the [http://localhost:3000/tutorial_examples/toc_playground](http://localhost:3000/tutorial_examples/toc_playground) TOC playground URL and then right-click on a heading and select **Inspect**, if you now look at the HTML code of the heading you will notice that **rehype slug** has added an ID - -Congratulations 🎉 all of your headings now automatically get a unique ID - -If you liked this post, please consider making a [donation](https://buymeacoffee.com/chriswwweb) ❤️ as it will help me create more content and keep it free for everyone - -> [!MORE] -> [npmjs.com "rehype-slug" page](https://www.npmjs.com/package/rehype-slug) - - - -
diff --git a/app/web_development/tutorials/next-js-static-mdx-blog/linting-mdx-using-remark-lint/page.mdx b/app/web_development/tutorials/next-js-static-mdx-blog/linting-mdx-using-remark-lint/page.mdx deleted file mode 100644 index b6f2052d..00000000 --- a/app/web_development/tutorials/next-js-static-mdx-blog/linting-mdx-using-remark-lint/page.mdx +++ /dev/null @@ -1,289 +0,0 @@ ---- -title: Linting MDX using remark-lint - Tutorial -description: Linting MDX using remark-lint - Next.js static MDX blog | www.chris.lu Web development tutorials -keywords: ['Linting', 'MDX', 'markdown', 'remark', 'remark-lint', 'plugin', 'rules', 'eslint', 'nextjs'] -published: 2024-07-01T11:22:33.444Z -modified: 2024-07-01T11:22:33.444Z -permalink: https://chris.lu/web_development/tutorials/next-js-static-mdx-blog/linting-mdx-using-remark-lint -section: Web development ---- - -import { sharedMetaDataArticle } from '@/shared/metadata-article' -import Breadcrumbs from '@/components/tutorial/Breadcrumbs' -import Pagination from '@/components/tutorial/Pagination' - -export const metadata = { - title: frontmatter.title, - description: frontmatter.description, - keywords: frontmatter.keywords, - alternates: { - canonical: frontmatter.permalink, - }, - openGraph: { - ...sharedMetaDataArticle.openGraph, - images: [{ - type: "image/png", - width: 1200, - height: 630, - url: '/web_development/og/tutorials_next-static-mdx-blog/opengraph-image' - }], - url: frontmatter.permalink, - section: frontmatter.section, - publishedTime: frontmatter.published, - modifiedTime: frontmatter.modified, - tags: frontmatter.keywords, - }, -} - -%toc% - -
- - - -# Linting MDX using remark-lint - -In the previous part of the tutorial, we added linting for our code - -We made heavy changes to the linting setup, which we needed to be able to start the second major linting setup, which is setting up linting for the markdown content in our MDX pages - -## Adding and configuring remark-lint - -We have now created a `.eslintrc.js` ESLint configuration file, we have added the MDX plugin, which has installed [remark-lint](https://www.npmjs.com/package/remark-lint) for us as it is a dependency of the **eslint-plugin-mdx** package - -The next step is to configure **remark-lint**, but first, we need to install remark-lint as well as 3 packages that contain presets for remark-lint, those presets will install a bunch of rules for us and will add a default configuration for those rules use the following command to install all 4: - -```shell -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: - -```js title=".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, below in the plugins config object, we use the 3 presets - -The other imports are single rules we want to configure, below in the config object, we use arrays where the first value is the rule and the second value is the configuration we want to apply: - -* for the **maximum headings length** rule, we use **1,100**, which tells remark-lint that headings should not have a length lower than 1 character and not greater than 100 -* for the **unordered lists marker style**, we set it to **consistent**, when set to **consistent**, the rule will check which marker is the most used and then enforce it everywhere, this is nice because it means it is flexible and will adapt to what you use, do you often define lists by using an asterisk (*), then this is what the rule will enforce or do you prefer using a hyphen-minus (-) then that is what will get enforced, consistent is nice setting to ensure that your styling is consistent based on what you use the most -* the **no undefined references** rule adds a list of references that should get excluded (hence not be considered undefined), we add `!NOTE` and some others as those will be used at some point when we start to use GitHub, like alerts for markdown -* images and links can have a title, the title needs to be enclosed by two symbols, here we define that we want to use a single quote (') for titles, so an image with a title would look like this `![IMAGE_ALT_TEXT](IMAGE_PATH 'IMAGE_TITLE')`, if you prefer, you can enforce double quotes or even set it to `consistent` to let it choose the one you use most -* the **maximum line length** rule I disabled it, some people like to stick to 80 characters, I try to keep my lines short, but if a line is bigger, that's ok (for me), too -* finally, the **list item spacing** I disabled it because I had a lot of false positives when using that rule (I might re-enable it in the future and check if the problems I encountered got fixed) - -> [!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](https://github.com/remarkjs/remark-lint/tree/main/packages) 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 - -> [!MORE] -> [remark lint "rules and presets" repository](https://github.com/remarkjs/remark-lint/tree/main/packages) - -## 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](#why-are-we-changing-the-nextjs-linting-setup)) is to change the lint script in our `package.json` - -Instead of using the **next lint** command line interface (CLI) command for linting, we will create our 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 option `--cache`, and then we tell ESLint to use the same cache directory that Next.js would use, using the `--cache-location` flag, like so: - -```shell -npx eslint ./ --cache --cache-location .next/cache/eslint -``` - -To specify what files (extensions) get linted, we need to use the ext flag like so: - -```shell -npx eslint ./ --ext .js,.jsx,.ts,.tsx,.mdx,.md -``` - -Now we need to put those commands into our `package.json`, but because we are in the `package.json`, we don't need to use `npx` - -We change the **lint script** in the `package.json` to this: - -```js title="package.json" {3} -{ - "scripts": { - "lint": "eslint ./ --ext .js,.jsx,.ts,.tsx,.mdx,.md --cache --cache-location .next/cache/eslint" - }, -} -``` - -Now, when we use the `npm run lint` command in our terminal, it will use our new script, which will use the **ESLint CLI**, which will read the ESLint configuration from our `.eslintrc.js` configuration file - -I have two more scripts that can be useful that you might add, too: - -```js title="package.json" {4-5}#special -{ - "scripts": { - "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](/web_development/posts/vscode#vscode-eslint-output-channel) 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: - -```md showLineNumbers {9-11} -# 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 - -> [!MORE] -> [ESLint "command line interface (CLI)" reference](https://eslint.org/docs/v8.x/use/command-line-interface) - -## 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 - -## 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](#why-are-we-changing-the-nextjs-linting-setup)) - -We need to ensure the **build** script will **NOT** use the **Next lint** CLI by default by editing our `next.config.mjs`, like so: - -```js title="next.config.mjs" showLineNumbers {38-40} -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 Issue #62335 is fixed - // https://github.com/vercel/next.js/issues/62335 - typedRoutes: phase === PHASE_DEVELOPMENT_SERVER ? true : false, - }, - headers: async () => { - return [ - { - source: '/(.*)', - headers: securityHeadersConfig(phase) - }, - ]; - }, - // configure `pageExtensions` to include MDX files - pageExtensions: ['js', 'jsx', 'ts', 'tsx', 'mdx', 'md'], - // disable linting during builds using "next lint" - // we have manually added our lint script in package.json to the build command - eslint: { - ignoreDuringBuilds: true, - }, - } - - return nextConfigOptions - -} -``` - -Lines 38 to 40: 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: - -```json title="package.json" {10-12}#special -{ - "scripts": { - "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 - -Congratulations 🎉 you just added linting for all your markdown content in your project's MDX pages - -If you liked this post, please consider making a [donation](https://buymeacoffee.com/chriswwweb) ❤️ as it will help me create more content and keep it free for everyone - - - -
diff --git a/app/web_development/tutorials/next-js-static-mdx-blog/linting-setup-using-eslint/page.mdx b/app/web_development/tutorials/next-js-static-mdx-blog/linting-setup-using-eslint/page.mdx deleted file mode 100644 index 6b298450..00000000 --- a/app/web_development/tutorials/next-js-static-mdx-blog/linting-setup-using-eslint/page.mdx +++ /dev/null @@ -1,423 +0,0 @@ ---- -title: Linting setup using ESLint - Tutorial -description: Linting setup using ESLint - Next.js static MDX blog | www.chris.lu Web development tutorials -keywords: ['Linting', 'ESLint', 'flat config', 'nextjs', 'mdx', 'plugin', 'parser'] -published: 2024-07-01T11:22:33.444Z -modified: 2024-07-01T11:22:33.444Z -permalink: https://chris.lu/web_development/tutorials/next-js-static-mdx-blog/linting-setup-using-eslint -section: Web development ---- - -import { sharedMetaDataArticle } from '@/shared/metadata-article' -import Breadcrumbs from '@/components/tutorial/Breadcrumbs' -import Pagination from '@/components/tutorial/Pagination' - -export const metadata = { - title: frontmatter.title, - description: frontmatter.description, - keywords: frontmatter.keywords, - alternates: { - canonical: frontmatter.permalink, - }, - openGraph: { - ...sharedMetaDataArticle.openGraph, - images: [{ - type: "image/png", - width: 1200, - height: 630, - url: '/web_development/og/tutorials_next-static-mdx-blog/opengraph-image' - }], - url: frontmatter.permalink, - section: frontmatter.section, - publishedTime: frontmatter.published, - modifiedTime: frontmatter.modified, - tags: frontmatter.keywords, - }, -} - -%toc% - -
- - - -# Linting setup using ESLint - -Adding linting to a project is something I recommend doing as early as possible, similar to adding CSP to a project - -Those are things that, if you postpone them, then you will have a lot more work later, which is why it is best to add linting as early as possible and then fix linting-related problems one by one as soon as they come up - -## The state of ESLint flat config files - -The following ESLint setup uses the [ESLint "Classic" configuration files](https://eslint.org/docs/latest/use/configure/configuration-files-deprecated), which is the default for all ESLint versions below 9 - -In ESLint 9, those configuration files are now **deprecated*,* and it is recommended to use the new [flat config files](https://eslint.org/docs/latest/use/configure/configuration-files), which are the new default since the release of ESLint 9 - -ESLint mentions in their documentation: - -> We are transitioning to a new config system in ESLint v9.0.0. The config system shared on this page is currently the default but will be deprecated in v9.0.0. You can opt-in to the new config system by following the instructions in the documentation. - -Also, support for [eslintrc configuration files will be removed in version 10.0.0](https://eslint.org/blog/2023/10/flat-config-rollout-plans/#eslintrc-removed-in-eslint-v10.0.0) - -> [!WARN] -> I, however, will use the classic configuration files with **overrides** in this tutorial, as this is what is currently supported by Next.js - -A lot of plugins like typescript-eslint have started working on support for **ESLint 9 / flat config files**, as you can see in [typescript-eslint Issue #8211](https://github.com/typescript-eslint/typescript-eslint/issues/8211) but [typescript-eslint 8](https://github.com/typescript-eslint/typescript-eslint/pull/9002) has not been released yet (as of today April 30, 2024), there are also a lot of other plugins that have not completed the transition to eslint 9 / flat config yet, if you are interested in the progress of those plugins then have a look at the Issue in the ESLint repository that keeps track of the [flat config rollout](https://github.com/eslint/eslint/issues/18093) for many packages - -Even though we won't use them yet (I will update the ESLint chapter when Next.js adds support for flat config), expect the new flat config files to become the new default in the foreseeable future - -> [!MORE] -> [ESLint "flat config rollout" issue](https://github.com/eslint/eslint/issues/18093) -> [ESLint "flag config part 1" blog post](https://eslint.org/blog/2022/08/new-config-system-part-1/) -> [ESLint "flag config part 2" blog post](https://eslint.org/blog/2022/08/new-config-system-part-2/) -> [ESLint "flag config part 3" blog post](https://eslint.org/blog/2022/08/new-config-system-part-3/) -> [ESLint "flat config files" RFC](https://github.com/eslint/rfcs/tree/main/designs/2019-config-simplification) - -## Answering some questions regarding the linting setup - -> [!NOTE] -> The next 3 chapters contain a lot of theory -> -> The 1st chapter explains what **create-next-app** did in regards to linting, the 2nd chapter explains what packages related to linting Next.js has, and the 3rd explains why we do want to modify the current ESLint setup, so if you prefer to get straight to the solution (code) then skip ahead to the ["Installing the MDX ESLint plugin and parser" chapter](#installing-the-mdx-eslint-plugin-and-parser) if you have the time and are interested in understanding the *"Why"** then I recommend reading on - -### But didn't Next.js already set up linting? - -Yes, Next.js has built-in linting support, this chapter is a recap of what Next.js has done so far - -Earlier in this tutorial, we used **create-next-app**, which has installed [ESLint](https://github.com/eslint/eslint) as well as the **eslint-config-next** package for us (both packages got added to the devDependencies in the `package.json`) - -**create-next-app** has also added a `.eslintrc.json` file in the root of the project, in that file, Next.js has added a default configuration that works best for most projects, Next.js has added that `.eslintrc.json` file so that the linting setup that gets used by the lint command can also be used by your IDE (VSCode) in the code editor itself - -When you [install ESLint for Next.js manually](https://nextjs.org/docs/app/building-your-application/configuring/eslint) by adding the lint command `"lint": "next lint"` to your package.json scripts and then executing it for the first time it will detect that there is no `.eslintrc.json` it will ask you if you want to use the **Base** mode or the **Strict** mode, we, however, used **create-next-app** and it did not let us chose if we prefer the **Base** mode or the **Strict** mode, that's because when using **create-next-app** it chooses the strict mode by default, which is why currently in your `.eslintrc.json` it extends **next/core-web-vitals** (which is the strict mode) and not just **next** (which is the base mode) - -**next/core-web-vitals** is a set of extra rules that will check your code and inform you about potential optimizations you can do that are related to core web vitals metrics, like rules to improve page loading speed, but **next/core-web-vitals** will also extend the base **next** rules - -Finally, **create-next-app** will also add the line `"lint": "next lint"{:json}` to your package.json `scripts`, which means that you now can use the command `npm run lint`, which will execute `next lint`, next lint is the Next.js CLI command for linting - -> [!MORE] -> [web.dev "Core Web Vitals" page](https://web.dev/articles/vitals) - -### Why does Next.js have two packages related to ESLINT? - -Next.js has **2 packages** that are related to ESLint, one is called eslint-**config**-next (ESLint Config), and the other one is called eslint-**plugin**-next (ESLint Plugin) - -* [eslint-config-next](https://www.npmjs.com/package/eslint-config-next) -* [eslint-plugin-next](https://www.npmjs.com/package/@next/eslint-plugin-next) - -Package 1: **eslint-config-next** (ESLint Config) intends to make it easier to get started with ESLint by installing and configuring several plugins for us, some of these plugins are: - -* [eslint-plugin-react](https://www.npmjs.com/package/eslint-plugin-react) -* [eslint-plugin-react-hooks](https://www.npmjs.com/package/eslint-plugin-react-hooks) -* [eslint-plugin-next](https://www.npmjs.com/package/@next/eslint-plugin-next) -* and some more, if you want the full list of plugins that eslint-config-next installs, check out the [eslint-config-next package.json dependencies](https://github.com/vercel/next.js/blob/b2625477c002343e7fe083204c45af1fdd7cd407/packages/eslint-config-next/package.json) - -Package 2: **eslint-plugin-next** is the actual ESLint plugin for Nextjs (called **@next/eslint-plugin-next** on npmjs), it aims to catch common problems in a Next.js application - -For a complete list of rules that the Next.js ESLint plugin adds check out the [Nextjs "ESLint rules" documentation](https://nextjs.org/docs/app/building-your-application/configuring/eslint#eslint-plugin) or have a look at the [eslint-plugin-next rules directory on GitHub](https://github.com/vercel/next.js/tree/b2625477c002343e7fe083204c45af1fdd7cd407/packages/eslint-plugin-next/src/rules) - -> [!MORE] -> [Next.js "ESLint" documentation](https://nextjs.org/docs/app/building-your-application/configuring/eslint) - -### Why are we changing the Next.js linting setup? - -The Next.js linting setup lints code in `.ts` and `.tsx` files using the typescript-eslint parser, however, it does not lint markdown syntax and code in MDX files for which you need to have an MDX parser installed - -This is why we are going to add 3 packages to do the linting of markdown content in MDX pages: - -* the first one is a **remark plugin** called [remark lint](https://github.com/remarkjs/remark-lint) that will lint the markdown style we use to format our content in MDX pages -* the second one is an **ESLint plugin** called [eslint-plugin-mdx](https://github.com/mdx-js/eslint-mdx/tree/master/packages/eslint-plugin-mdx) which will lint MDX -* the third one is a **parser** called [eslint-mdx](https://www.npmjs.com/package/eslint-mdx) which will parse the content of MDX files - -**The recommended way** to add [eslint-plugin-mdx as described in their README](https://github.com/mdx-js/eslint-mdx#notice) is to use the **overrides** feature of ESLint (if you want to know more about the parsing issues you might have if not using **overrides** to check out the [eslint-plugin-mdx GitHub issue #251](https://github.com/mdx-js/eslint-mdx/issues/251#issuecomment-736139224)) - -However, even though Next.js has created a `.eslintrc.json` for us that lets us do some fine-tuning of rules, adding a new **overrides** for markdown will not work due to a limitation how the **next lint** CLI works (there is open discussion [next lint command doesn't support overrides #35228](https://github.com/vercel/next.js/issues/35228) where the limitation gets discussed), **next lint** doesn't use the `.eslintrc.json` that **create-next-app** added to the root of project, it just added that file so that our IDE (VSCode) can do linting in files using the same setup as Next.js - -So because **next lint** does ignore custom **overrides** that are in your `.eslintrc.json`, this, unfortunately, means that we will not be able to use **next lint** CLI anymore, and instead, we will create a custom **lint** command in our `package.json` scripts, by using our command we ensure that the eslint configuration file in the root of our project gets used for linting, both in the IDE while coding and then also when using the `npm run lint` command - -Finally, there is yet another problem, the Next.js CLI **build** command does not use the `package.json` **lint** script but uses the Next.js **lint** CLI directly, this means that we will need to tell the **build** CLI not to do linting during builds and then we will manually re-add linting by changing the `package.json` **build** script so that it uses our `package.json` **lint** script before doing an actual build - -> [!MORE] -> [ESLint "how eslint overrides work" documentation](https://eslint.org/docs/v8.x/use/configure/configuration-files#how-do-overrides-work) - -## Installing the MDX ESLint plugin and parser - -First, we need to make sure the [MDX eslint plugin](https://www.npmjs.com/package/eslint-plugin-mdx) (and parser) are installed by using the following command: - -```shell -npm i eslint-plugin-mdx@latest --save-exact --save-dev -``` - -The ESLint MDX plugin has the [ESLint MDX parser (called eslint-mdx)](https://www.npmjs.com/package/eslint-mdx) listed as a dependency so it will get installed too, alongside other packages like the [eslint-plugin-markdown](https://www.npmjs.com/package/eslint-plugin-markdown) and a few others - -## ESLint configuration step 1: Basic ESLint configuration file - -**create next app** has added a `.eslintrc.json` in the root of our project, as we will add our own custom eslint configuration file, the first thing we need to do is delete the current `.eslintrc.json` - -**create next app** should have installed all dependencies needed, so we do NOT need to install any packages; if you didn't use **create-next-app** as we did in this tutorial, then I recommend using this command to ensure all packages are installed: - -```shell -npm i eslint@latest eslint-config-next@latest --save-exact --save-dev -``` - -> [!WARN] -> As the new ESLint 9 did get released, you might encounter backward compatibility problems when using the latest version in combination with the next config eslint or other plugins, in that case, I recommend using the latest version of the 8.x branch until the flat config is more widely supported, as of now this is [ESLint 8.57.0](https://www.npmjs.com/package/eslint/v/8.57.0) - -Then create a new `.eslintrc.js` and add the following content: - -```js title=".eslintrc.js" -module.exports = { - root: true, - parserOptions: { - ecmaVersion: 'latest', - sourceType: 'module', - }, - 'env': { - browser: true, - es2021: true, - node: true, - }, - ignorePatterns: [ - 'node_modules/', - '.next/', - '.vscode/', - 'public/', - ], - reportUnusedDisableDirectives: true, - overrides: [ - ], -} -``` - -> [!NOTE] -> I chose javascript for my eslint configuration file, I usually use javascript over json as it allows me to add comments -> -> However, if you prefer json, feel free to create a `.eslintrc.json` instead of a `.eslintrc.js` or use one of the many other [ESLint configuration file formats](https://eslint.org/docs/latest/use/configure/configuration-files-deprecated#configuration-file-formats) that are supported - -The above basic setup is inspired by what you get if you use the [eslint init](https://eslint.org/docs/v8.x/use/getting-started) script to setup ESLint in a new project, using **eslint init** in a sandbox folder is a good way to see what kind of basic setup the ESLint team recommends - -I added root true to make sure eslint [stops at the root of my project](https://eslint.org/docs/v8.x/use/configure/configuration-files#cascading-and-hierarchy) and does not attempt to check for other eslint configuration files in parent directories - -I also added some entries in the **ignorePatterns** to make sure ESLint is not going to lint anything in those folders, you might want to add other folders to this list over time if you want ESLint to exclude those folders from linting - -I then enabled the option **reportUnusedDisableDirectives** to make sure ESLint will trigger a [warning if it finds unused disable eslint comments](https://eslint.org/docs/v8.x/use/configure/rules#report-unused-eslint-disable-comments), which can happen when code gets deleted or moved around and suddenly an `// eslint-disable-next-line` comment becomes useless - -## ESLint configuration step 2: ESLint ts(x) and md(x) files override - -The 1st override we add to the array is fairly short, as its only purpose is to tell ESLint which **rule sets** we want to use, no matter if it is mdx / markdown content in md(x) files or typescript code in ts(x) files - -We do this because of a rule from the Next.js ESLint plugin that recommends using [next/image](https://nextjs.org/docs/app/api-reference/components/image) instead of a regular `` element can be useful in both MDX files as well as Typescript code in one of our React components or Next.js pages - -Add the following object to the **overrides** array: - -```js title=".eslintrc.js" -overrides: [ - { - files: ['**/*.ts?(x)', '**/*.md?(x)'], - extends: [ - 'next/core-web-vitals', - ], - }, -], -``` - -What this override does: - -* **files** is set so that it will lint any file that is a **ts** or **tsx** file and also any file that is a **md** or **mdx** file, meaning this override is for both code in our typescript files as well as content in our MDX files -* **extends** is set so that we will include the **recommended** rules from the **eslint** plugin, it will also use the rules from the **core-web-vitals**, **core-web-vitals** will add a few rules related to core web vitals, but also extends the next base rules (so there is no need to add 'next' to the extends too) - -## ESLint configuration step 3: ESLint ts(x) files only override - -The 2nd override is specifically for **typescript** code, its primary purpose is to tell ESLint to use the **@typescript-eslint/parser** parser to parse ts(x) files - -I have 2 options here: - -* the 1st one is what Next.js uses in **eslint-config-next** WITHOUT any of the **@typescript-eslint** rule sets enabled -* the 2nd option is more **strict** version that - * by using the **@typescript-eslint/recommended** rule set, we will add a bunch of typescript-related rules, like the [no-unnecessary-type-assertion](https://typescript-eslint.io/rules/no-unnecessary-type-assertion/) that checks if you have type assertions that are not needed - * you can even enable a stricter version if you also enable the **stylistic-type-checked** rule set, which will, for example, use [prefer-nullish-coalescing](https://typescript-eslint.io/rules/prefer-nullish-coalescing/) in cases where you could have but did NOT use the [Nullish coalescing operator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Nullish_coalescing) - -### Option 1: typescript only parser + Next.js config (which includes react, react-hooks, ...) - -If you chose option 1 (the less strict version), then add the following override into your `.eslintrc.js`: - -```js title=".eslintrc.js" -overrides: [ - { - files: ['**/*.ts?(x)'], - parser: '@typescript-eslint/parser', - parserOptions: { - sourceType: 'module', - ecmaFeatures: { - jsx: true, - }, - warnOnUnsupportedTypeScriptVersion: true, - }, - rules: { - quotes: [ - 'error', - 'single', - { "allowTemplateLiterals": true }, - ], - semi: [ - 'error', - 'never', - ], - }, - }, -], -``` - -What this override does: - -* **files** is set to **ts(x)**, meaning this override is only for **ts** and **tsx** files -* there is no **extends** defined, as it will already use the **extends** of the previous override in which we extended **next/core-web-vitals** (next/core-web-vitals will then extend the base **next** rule set) -* because this **overrides** is specifically for **typescript** files, we set the parser to use the **@typescript-eslint** parser (instead of the default eslint parser, which only supports parsing javascript) - -### Option 2: everything that is in option 1 + typescript code rules - -I like option 2 best as it adds a lot of good rules that will check your typescript code and give you feedback if needed, if it finds too many problems in your code, maybe set the rules to warn instead of error until you have time to fix them all and then enforce the rules by using error again (or don't use them at all, it is up to you) - -Suppose you chose option 2 (the stricter version with more typescript-related rules) - -In that case, we first need to install the additional **@typescript-eslint/eslint-plugin** package, but to ensure that this package uses the same version as the **@typescript-eslint/parser** package, I recommend installing both, like so: - -```shell -npm i @typescript-eslint/parser@latest @typescript-eslint/eslint-plugin@latest --save-exact --save-dev -``` - -> [!WARN] -> If you get an NPM error because the version of the installed typescript-eslint/parser and the typescript-eslint/plugin version doesn't match: -> ->> npm ERR! code ERESOLVE ->> npm ERR! ERESOLVE could not resolve ->> Conflicting peer dependency: @typescript-eslint/parser -> -> Then use the following command to remove the parser: -> ->```shell -> npm remove @typescript-eslint/parser -> ``` -> -> Then delete your `package-lock.json` (NOT the `package.json` file) file in the root in the repository -> -> Finally, try out the installation command (above) again - -Next, we put the following override into our **overrides** array (instead of the one in option 1): - -```js title=".eslintrc.js" -overrides: [ - { - files: ['**/*.ts?(x)'], - extends: [ - 'plugin:@typescript-eslint/recommended-type-checked', - 'plugin:@typescript-eslint/stylistic-type-checked', - ], - parser: '@typescript-eslint/parser', - parserOptions: { - sourceType: 'module', - ecmaFeatures: { - jsx: true, - }, - warnOnUnsupportedTypeScriptVersion: true, - project: './tsconfig.json', - }, - plugins: [ - '@typescript-eslint', - ], - rules: { - quotes: [ - 'error', - 'single', - { "allowTemplateLiterals": true }, - ], - semi: [ - 'error', - 'never', - ], - '@typescript-eslint/consistent-indexed-object-style': 'off', - '@typescript-eslint/ban-ts-comment': [ - 'error', - { - 'ts-expect-error': 'allow-with-description', - 'ts-ignore': 'allow-with-description', - 'ts-nocheck': false, - 'ts-check': false, - minimumDescriptionLength: 3, - }, - ], - }, - }, -], -``` - -> [!WARN] -> This overrides is not additional to the overrides in option 1, it is a full replacement, you chose to either add option 1 or option 2 but NOT BOTH - -What this override does: - -* **files** is set to **ts(x)**, meaning this override is only for **ts** and **tsx** files -* **extends** is set to extend the **recommended** rules of the **@typescript-eslint** plugin as well as the **stylistic** rules, if you only want the recommended rules but NOT the stylistic, then comment the `'plugin:@typescript-eslint/stylistic-type-checked',` line out -* because this **overrides** is specifically for **typescript** files, we set the parser to use the **@typescript-eslint** parser (instead of the default eslint parser, which only supports parsing javascript) -* **plugins** is set to typescript-eslint, but this is not the only plugin that will be used for typescript files, in the previous overrides, we already added the Next.js eslint plugin, so there is no need to add it again here (the Next.js eslint config will include a bunch of other plugins like react, react-hooks and some more) -* finally, we have the **rules** option, here you can add whatever rules you need in your project - * I like to single quotes and not double quotes in my javascript code this is why I set **quotes** to **single** (feel free to set it to **double** if you prefer) - * then I set the rule for semicolons at the end of a javascript line to **never** because I don't use semicolons at the end of lines in my code (as long as they are optional, in the few cases where they are required, I of course use them, but those cases are very rare) - * next, I also disable the [consistent-indexed-object-style](https://typescript-eslint.io/rules/consistent-indexed-object-style/) rule, I usually like consistency but in this case, I think the rule goes too far, different variants are supported by typescript and as long as there their syntax is right I feel like letting the dev chose which one he prefers, feel free to remove this line if you prefer enforcing the consistency - * finally, I added a configuration for rule [ban-ts-comment](https://typescript-eslint.io/rules/ban-ts-comment/) to allow the **ts-expect-error** and **ts-ignore** comments but only when there is a description as to why those comments got added to the code (by default no comments are allowed) - -> [!MORE] -> [typescript-eslint "rules" overview](https://typescript-eslint.io/rules/) - -## ESLint configuration step 4: ESLint md(x) files only overrides - -The 3rd and final overrides is specifically for **markdown / mdx** content, its primary purpose is to tell ESLint to use the **eslint-mdx** parser to parse md(x) files: - -```js title=".eslintrc.js" -overrides: [ - { - files: ['**/*.md?(x)'], - extends: [ - 'plugin:mdx/recommended', - ], - parser: 'eslint-mdx', - parserOptions: { - markdownExtensions: ['*.md, *.mdx'], - }, - settings: { - 'mdx/code-blocks': false, - 'mdx/remark': true, - }, - rules: { - 'react/no-unescaped-entities': 0, - } - // markdown rules get configured in remarkrc.mjs - }, -], -``` - -* **files** is set to **md(x)**, meaning this **overrides** is only for **md** and **mdx** files -* **extends** is set to extend the recommended recommended **MDX plugin** rules -* because this **overrides** is specifically for **MDX (and markdown)** files, we set the parser to use the **MDX parser** (instead of the default eslint parser, which only supports parsing javascript) -* **settings** is for to do 2 things: - * **mdx/code-blocks** is set to **false** to disable the linting of code blocks, you may want to enable this instead, it can be a very nice feature to have **eslint MDX** use ESLint to lint the code inside of code blocks, I, however, shorten code in code blocks, and this creates a case where rules get triggered just because not all the code is in every code block, like using a variable that has not been created or using a package but the import is missing, this is why for myself I disabled this (I sometimes enable it periodically to lint a new files but then disable it before committing the code for the reasons I just described) - * **mdx/remark** is set to **true** because this enables [eslint-mdx to use the remark-lint plugin](https://github.com/mdx-js/eslint-mdx?tab=readme-ov-file#mdxremark), it will read `.remarkrc.mjs` configuration file and use all the remark-line rules that are listed in that file, this means that when ESLint will now be capable to also lint our markdown content (both in the IDE and when using the `npm run lint` command) -* finally, in the **rules**, I disable the react eslint plugin rule [react/no-unescaped-entities](https://github.com/jsx-eslint/eslint-plugin-react/blob/master/docs/rules/no-unescaped-entities.md) because I don't want to have to escape entities in my markdown files, I understand that the **react/no-unescaped-entities** is useful in react components but not in markdown content, all other react rules we keep them as is (those rules are enabled because in the very first overrides we added **next/core-web-vitals**, which extends the **next** config, which then adds a bunch of eslint packages, each comes with their own set comes, to see which ones it adds look at the [Why does Next.js have two packages related to ESLINT?](#why-does-nextjs-have-two-packages-related-to-eslint) chapter and have a look at **Package 1: eslint-config-next**) - -This was the last **overrides**, which means our configuration file is now complete, you can now save the `.eslintrc.js` file - -Congratulations 🎉 if you made it this far, we had to make a lot of changes, but you now know a lot more about how ESLint works, and we are now ready to start adding linting for MDX content in the next part of this tutorial - -If you liked this post, please consider making a [donation](https://buymeacoffee.com/chriswwweb) ❤️ as it will help me create more content and keep it free for everyone - -> [!MORE] -> [eslint-plugin-react "rules" repository](https://github.com/jsx-eslint/eslint-plugin-react/tree/master/docs/rules) - - - -
diff --git a/app/web_development/tutorials/next-js-static-mdx-blog/linting-using-vscode-and-extensions/page.mdx b/app/web_development/tutorials/next-js-static-mdx-blog/linting-using-vscode-and-extensions/page.mdx deleted file mode 100644 index 445d905b..00000000 --- a/app/web_development/tutorials/next-js-static-mdx-blog/linting-using-vscode-and-extensions/page.mdx +++ /dev/null @@ -1,346 +0,0 @@ ---- -title: Linting in VSCode using ESLint and MDX extensions - Tutorial -description: Linting in VSCode using ESLint and MDX extensions - Next.js static MDX blog | www.chris.lu Web development tutorials -keywords: ['CI/CD', 'Vercel', 'build', 'Production', 'preview'] -published: 2024-07-01T11:22:33.444Z -modified: 2024-07-01T11:22:33.444Z -permalink: https://chris.lu/web_development/tutorials/next-js-static-mdx-blog/linting-using-vscode-and-extensions -section: Web development ---- - -import { sharedMetaDataArticle } from '@/shared/metadata-article' -import Breadcrumbs from '@/components/tutorial/Breadcrumbs' -import Pagination from '@/components/tutorial/Pagination' - -export const metadata = { - title: frontmatter.title, - description: frontmatter.description, - keywords: frontmatter.keywords, - alternates: { - canonical: frontmatter.permalink, - }, - openGraph: { - ...sharedMetaDataArticle.openGraph, - images: [{ - type: "image/png", - width: 1200, - height: 630, - url: '/web_development/og/tutorials_next-static-mdx-blog/opengraph-image' - }], - url: frontmatter.permalink, - section: frontmatter.section, - publishedTime: frontmatter.published, - modifiedTime: frontmatter.modified, - tags: frontmatter.keywords, - }, -} - -%toc% - -
- - - -# Linting in VSCode using ESLint and MDX extensions - -We now have [linting set up and a custom lint command](/web_development/tutorials/next-js-static-mdx-blog/linting-setup-using-eslint) 🎉, but we have no linting in VSCode itself (yet) - -For that reason we are now going to install 2 **VSCode extensions** - -The first one will add linting messages directly into our code, and the second one will add MDX language support to the VSCode IntelliSense - -Having the ESLint extension is great because it allows us to see linting warnings and errors as we code, instead of having to wait for the linting command to run, meaning we can fix the problems one by one as they occur instead of waiting until the last moment and potentially having to fix a lot of issues all at once - -## ESLint extension - -The first extension I recommend installing is the [VSCode "ESLint" extension](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) - -After installing the ESLint extension, you need to edit the settings of that extension to add things like the **.mdx** extension to the list of file extensions that it will use - -You have several options when editing VSCode settings (spoiler alert: I will use option 2) - -Option 1: Open the extensions view (extensions list), if you don't know (yet) how to do this, then I recommend checking out my ["VSCode extensions view" chapter](/web_development/posts/vscode#vscode-extensions-view) in the VSCode post - -Then click on the gear icon (⚙️) of the ESLint extension, and then in the menu, select **Extension Settings** - -Option 2: If you have already set custom settings for your VSCode workspace, then you will have a `.vscode` folder in your project root, if not, create that folder, then inside of that folder, you will have a `settings.json` file, if that file is not there create it - -> [!NOTE] -> When you edit settings, you can do it on a **User** level or **Workspace** level -> -> In this case, we will do it on a **Workspace** level by using the `settings.json` file inside of the `.vscode` folder (that is in the root of our project), to learn more about how to use the VSCode settings I recommend checking out the [VSCode settings](/web_development/posts/vscode#vscode-settings) in the VSCode post - -Now open the `.vscode/settings.json` file and add the following settings for our ESLint extension: - -```json -{ - "eslint.debug": true, - "eslint.options": { - "extensions": [ - ".js", - ".jsx", - ".md", - ".mdx", - ".ts", - ".tsx" - ] - }, - "eslint.validate": [ - "markdown", - "mdx", - "javascript", - "javascriptreact", - "typescript", - "typescriptreact" - ] -} -``` - -This will tell the ESLint extension to include files with the ts(x) as well as md(x) extension, and it will make sure that it validates markdown, mdx, javascript, typescript, and also react code - -> [!MORE] -> [VSCode "ESLint" extension](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) - -## MDX extension - -The second VSCode extension I recommend installing is the VSCode [MDX (Language support for MDX)](https://marketplace.visualstudio.com/items?itemName=unifiedjs.vscode-mdx), when working with MDX content, as this extension will add IntelliSense support for MDX files to VSCode - -> [!Note] -> This extension is still experimental, but I had no problems when using it - -Unlike the ESLint extension, the MDX extension does not require any custom settings configuration, you are all set - -> [!MORE] -> [VSCode "MDX (Language support for MDX)" extension](https://marketplace.visualstudio.com/items?itemName=unifiedjs.vscode-mdx) - -## Restarting the ESLint server in VSCode - -Every time you make changes to your ESLint setup, for example, after editing either the `.eslintrc.js` ESLint configuration file or the `.remarkrc.mjs` remark-lint configuration file, I recommend restarting the ESLint server in VSCode to make sure the changes get applied immediately - -To restart the eslint server in VSCode, press `ctrl` + `shift` + `p` to open the command palette, then type `ESLint` and choose the command called **ESLint: Restart ESLint Server** - -Congratulations 🎉 you just completed the ESLint setup of your project, you now have a linting command for your code as well as your MDX content, you installed 2 extensions in VSCode so that you now also have full ESLint and MDX (markdown) linting inside of our VSCode IDE - -## Ensure linting in VSCode works as intended - -Now, the final question is: "But does it work?" - -To test if everything works as intended, we will create some errors in both the tsx and mdx files, and if we did the setup correctly, we should get linting errors both in VSCode as well as in the command line output - -### Testing the MDX file(s) linting process (in VSCode) - -We are going to create a test MDX file with some content to see if VSCode displays linting warnings for 3 problems we are going to add to our content - -First, let's create a `tests` folder in the root of our project, and inside that folder, add another folder called `eslint`, then in that folder, create a file called `content.mdx` with the following content: - -```md title="/tests/eslint/content.mdx" -# title - -# Another level 1 headline (it should trigger a linting error) - - - -The linting errors that should be shown in this file: -- On line 3: Unexpected duplicate toplevel heading (remark-lint-no-multiple-toplevel-headings) -- On line 5: error no image element use next/image -- On line 10 (this line): should trigger: Unexpected missing final newline character, the last character of this line should have a wavy underline (remark-lint-final-newline) -``` - -If your ESLint setup is working as intended, you should now see that several lines are underlined with a green wave, if you hover over the part that is underlined, a modal box should show you details about the warning - -For example, the 2nd level 1 heading will display a warning text like this: - -> Unexpected duplicate toplevel heading, exected a single heading with rank `1` eslint(remark-lint-no-multiple-toplevel-headings) - -The first part is the warning message followed by the name of the extension that added the warning (in this case **eslint**) and then in parenthesis the name of the rule, in this case it is the rule [remark-lint-no-multiple-toplevel-headings](https://github.com/remarkjs/remark-lint/tree/main/packages/remark-lint-no-multiple-toplevel-headings) that got added by **remark-lint** - -This warning alone shows us that **ESLint** and **remark-lint** are both working (together) - -You should be able to see 2 more warnings in that file: - -* one is for the image, which will show a modal containing two warnings - * the first one is from **eslint-plugin-next** (the Next.js ESLint plugin), which triggered a warning for the [@next/next/no-img-element](https://nextjs.org/docs/messages/no-img-element), telling you to prefer **next/image** instead of the `` element - * the second warning for the image is from a plugin called [eslint-plugin-jsx-a11y](https://www.npmjs.com/package/eslint-plugin-jsx-a11y) that got added by **eslint-config-next** (the Next.js ESLint configuration) and which triggered a warning because of the rule [jsx-a11y/alt-text](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/0d5321a5457c5f0da0ca216053cc5b4f571b53ae/docs/rules/alt-text.md) which is a rule that warns you if your image has no **alt** attribute -* the 3rd and last underline is on the last character of the text block at the very end of the file (it is just one character wide, so it is easy to miss it) and is a warning from the **remark-lint** for the [remark-lint-final-newline](https://github.com/remarkjs/remark-lint/tree/main/packages/remark-lint-final-newline), which recommends adding a newline at the end of every file - -### Testing the React component(s) linting (in VSCode) - -Now we are going to test if VSCode displays error inside of typescript code by creating a Button component with 3 errors in it - -Next, inside the folder `/tests/eslint` that we created previously, add a new folder called `components`, and inside that folder, create a new file called `Button.tsx` with the following content: - -```tsx title="/tests/eslint/components/Button.tsx" -'use client' - -import { forwardRef } from 'react' - -type ButtonRefType = HTMLButtonElement - -interface PropsInterface { - clickCallback?: () => void -} - -const TestButton = forwardRef((props, buttonRef) => { - - const { clickCallback, ...rest } = props - - const buttonClickHandler = (/*event: React.MouseEvent*/) => { - - if (typeof clickCallback === 'function') { - clickCallback() - } - - } - - return ( - <> - - - ) -}) - -TestButton.displayName = 'TestButton' - -export default TestButton - -/* linting errors I should see in this test component -- line 11: buttonRef is declared but its value is never read (@typescript-eslint/no-unused-vars) -- line 29: the "'" should be escaped (react/no-unescaped-entities) -- line 35: first you need to comment that line out by adding `//` at the beginning and then you should get: Component definition is missing display name (react/display-name) -*/ -``` - -This test only shows an error if you chose option 2 when adding the overrides to the eslint configuration: The first thing ESLint should have spotted in this test is the **buttonRef** variable, it should have a wavy red underline because it is an error if you hover over it you will see in the modal that the [@typescript-eslint/no-unused-vars](https://typescript-eslint.io/rules/no-unused-vars/) rule got triggered, this rule is recommend by the **@typescript-eslint** plugin, it extends the base rule [eslint/no-unused-vars](https://eslint.org/docs/latest/rules/no-unused-vars) from the **eslint** plugin and tells you that you have a variable in your code is unused - -Then we have another error, this time, it is the [react/no-unescaped-entities](https://github.com/jsx-eslint/eslint-plugin-react/blob/master/docs/rules/no-unescaped-entities.md) rule from the **eslint-plugin-react** (the React ESLint plugin) that tells us to escape entities - -Finally line 37, there is the following line: `TestButton.displayName = 'TestButton'` that you need to comment out to see the actual error, like this `//TestButton.displayName = 'TestButton'` - -If you comment out line 37 (I left it uncommented as the error adds an underline wave to add the code, making it hard to see the other errors), then there will be another error triggered by the [react/display-name](https://github.com/jsx-eslint/eslint-plugin-react/blob/master/docs/rules/display-name.md) rule from the **eslint-plugin-react**, telling you to set a displayName, because we used **forwardRef** and because by using a displayName debugging will be easier - -So, as you can see, the linting of typescript code is working, too, we got an error from the **@typescript-eslint** and two from the **eslint-plugin-react** - -## Testing the lint command - -There is only one final test left, which is testing if the linting command works, too - -To test the linting command, open the VSCode terminal and then use the following command: - -```shell -npm run lint -``` - -This should display all the warnings and errors we found in the two previous chapters, similar to this (I removed the plugin names in the output below to ensure it fits inside of the code box): - -```shell -PATH_TO_PROJECT\tests\eslint\components\Button.tsx - 11:20 error Component definition is missing display name - 11:70 error 'buttonRef' is defined but never used - 31:18 error `'` can be escaped with `'`, `‘`, `'`, `’` - -PATH_TO_PROJECT\tests\eslint\content.mdx - 3:1 warning Unexpected duplicate toplevel heading, exected a single heading with rank `1` - 5:1 warning Using `` could result in slower LCP and higher bandwidth. Consider using `` from `next/image` to automatically optimize images. This may incur additional usage or cost from your provider. See: https://nextjs.org/docs/messages/no-img-element - 5:1 warning img elements must have an alt prop, either with meaningful text, or an empty string for decorative images - 10:99 warning Unexpected missing final newline character, expected line feed (`\n`) at end of file -``` - -## Excluding our test files from linting - -Before you commit, there is one last thing we need to do, which is to exclude our tests folder from linting by adding it to the exclude block in our `.eslintrc.js` file, like this: - -```js title=".eslintrc.js" - ignorePatterns: [ - 'node_modules/', - '.next/', - '.vscode/', - 'public/', - // by default we always ignore our tests folder - // to ensure the tests do NOT trigger errors in - //staging/production deployments - // comment out the next line to have eslint check - // the test files (in development) - 'tests/eslint/', - ], -``` - -By adding 'tests/eslint/' to the **ignorePatterns**, we ensure that the linting process will not lint those files when we do a deployment and hence prevent our build process - -When you want to run the tests again (in development), just comment this line out - -## Disabling rules using comments - -Now that the linting setup is done, you will start seeing a warning in your code, and at some point, you might wonder how you can **disable/ignore** certain **warnings** within files - -Sometimes, you might encounter warnings that you want to suppress because it is an exception to the rule (but you don't want to disable the rule entirely in your configuration), then you can use comments to disable the rule, for example, for an entire file or just the next line - -### ESLint "disable" comment - -Disabling an eslint rule for the **next line** (works for plugins too, except remark, see below): - -```ts -// eslint-disable-next-line @typescript-eslint/no-unused-vars -const foo = 'bar' -``` - -To disable a rule for an entire file (and NOT just the next line), you can use: - -```ts -/* eslint-disable no-console */ -console.log('foo') - -console.log('bar') -``` - -To disable more than one rule in a single comment, you need to separate the rule names with a comma - -For example, if you use an `` element with no `alt` attribute, you will get two warnings - -To disable them both, you do it like this: - -```ts -// eslint-disable-next-line jsx-a11y/alt-text, @next/next/no-img-element - -``` - -> [!MORE] -> [ESLint "disabling rules using comments" documentation](https://eslint.org/docs/latest/use/configure/rules#disabling-rules) - -### remark-lint disable comments (in MDX) - -To **disable remark-lint rules** we added by using **MDX plugin** you do **NOT** specify the **rule name**: - -```mdx title="THIS EXAMPLE IS WRONG" -{/* eslint-disable-next-line remark-lint-no-undefined-references) */} -> [Info - 8:41:03 PM] ESLint server is running. -``` - -If you do, you will get an error like this: - -> Error: Definition for rule **remark-lint-no-undefined-references** was not found - -Instead, you need to use **mdx/remark** for **ANY** rule you want to disable - -In **MDX** files to add an **eslint-disable** comment, you need to use **JSX comments** - -So if we do both things, we get something like this: - -```mdx -{/* eslint-disable-next-line mdx/remark */} -> [Info - 8:41:03 PM] ESLint server is running. -``` - - - -
diff --git a/app/web_development/tutorials/next-js-static-mdx-blog/mdx-components-file/page.mdx b/app/web_development/tutorials/next-js-static-mdx-blog/mdx-components-file/page.mdx deleted file mode 100644 index 0ecf5578..00000000 --- a/app/web_development/tutorials/next-js-static-mdx-blog/mdx-components-file/page.mdx +++ /dev/null @@ -1,130 +0,0 @@ ---- -title: The mdx-components file - Tutorial -description: The mdx-components file - Next.js static MDX blog | www.chris.lu Web development tutorials -keywords: ['CI/CD', 'Vercel', 'build', 'Production', 'preview'] -published: 2024-07-01T11:22:33.444Z -modified: 2024-07-01T11:22:33.444Z -permalink: https://chris.lu/web_development/tutorials/next-js-static-mdx-blog/mdx-components-file -section: Web development ---- - -import { sharedMetaDataArticle } from '@/shared/metadata-article' -import Breadcrumbs from '@/components/tutorial/Breadcrumbs' -import Pagination from '@/components/tutorial/Pagination' - -export const metadata = { - title: frontmatter.title, - description: frontmatter.description, - keywords: frontmatter.keywords, - alternates: { - canonical: frontmatter.permalink, - }, - openGraph: { - ...sharedMetaDataArticle.openGraph, - images: [{ - type: "image/png", - width: 1200, - height: 630, - url: '/web_development/og/tutorials_next-static-mdx-blog/opengraph-image' - }], - url: frontmatter.permalink, - section: frontmatter.section, - publishedTime: frontmatter.published, - modifiedTime: frontmatter.modified, - tags: frontmatter.keywords, - }, -} - -%toc% - -
- - - -# The mdx-components file - -Before we experiment with plugins in the next chapter, I wanted to first come back to the `mdx-components.tsx` file, which we briefly saw when setting up MDX support (at the very beginning), as the file is required to make MDX work - -`mdx-components.tsx` is great for quickly and easily replacing an HTML Element with a React component - -For example, how can we quickly add a CSS class to all our `
    ` [unordered list elements](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/ul) because the markdown syntax does NOT let us add a class in a similar way to how we add a class to an HTML element the ul is not even part of the markdown syntax: - -```md -* foo -* bar -``` - -To solve this, you could add a remark plugin, which would extend the base markdown syntax, but if you want to move your content to another platform someday, then that platform might not understand your custom markdown syntax as the plugin you used to add classes to lists is not installed - -You could also write a custom react component, but then you would need to manually import and use that react component in every single MDX page. The advantage of using the **mdx-components** file is that you only need to import a component once. For example, in an upcoming chapter, we will create a custom link component. We will only need to import it once into the **mdx-components** file, but it will still transform every link in every page of our project . - -This is why the solution we will use is the next/mdx `mdx-components.tsx` file, and as we will see in the next chapter, it will allow us to add a class to all of our lists without using an extra plugin, and without having to introduce any new markdown syntax - -> [!MORE] -> [mdxjs.com "components" documentation](https://mdxjs.com/table-of-components/) -> [Next.js "MDX custom elements" documentation](https://nextjs.org/docs/app/building-your-application/configuring/mdx#custom-elements) - -## Adding a CSS class to lists using mdx-components - -First, open the `mdx-components.tsx` (which is in the root of your project) and add an entry for each headline, like so: - -```tsx title="mdx-components.tsx" showLineNumbers {12-16} -import type { MDXComponents } from 'mdx/types' - -// This file allows you to provide custom React components -// to be used in MDX files. You can import and use any -// React component you want, including components from -// other libraries. - -// This file is required to use MDX in `app` directory. -export function useMDXComponents(components: MDXComponents): MDXComponents { - return { - // Allows customizing built-in components, e.g. to add styling. - ul: ({ children, ...props }) => ( -
      - {children} -
    - ), - ...components, - } -} -``` - -Lines 12 to 16: we have created a simple custom component for **ul lists**, what happens is that first, the markdown parser turns the markdown list into HTML, and then mdx-components will be used to add a CSS class called `listContainer` to each `
      ` element - -## CSS class example using a MDX playground page - -Next, let's create a new playground to experiment with the **mdx-components** file - -Inside the `/app/tutorial_examples` folder, create a `mdx-components_playground` folder - -Then, in the `mdx-components_playground` folder, create a `page.mdx` file and paste the following content into it: - -```mdx title="/app/tutorial_examples/mdx-components_playground/page.mdx" -
      - -* foo -* bar - -
      - -``` - -> [!NOTE] -> We wrap our content with the `article` HTML element so that the content is not placed directly into the `main` HTML element (of our layout), which has a **flex-direction** set to **row**, the `article` element has a flex-direction of **column** (we defined all this in our `global.css` file which is in the `app` folder when we created it) - -Now ensure the dev server is running (or launch it using the `npm run dev` command), and then in the browser, navigate to [http://localhost:3000/tutorial_examples/mdx-components_playground](http://localhost:3000/tutorial_examples/mdx-components_playground), then use your browser developer tools inspect tool and you will see that the `
        ` element now has a **class** attribute containing the value `listContainer` - -Congratulations 🎉 you just learned how to use the **mdx-components** file, in other parts of this tutorial, we will edit this file again and add several more features to it - -If you liked this post, please consider making a [donation](https://buymeacoffee.com/chriswwweb) ❤️ as it will help me create more content and keep it free for everyone - - - -
diff --git a/app/web_development/tutorials/next-js-static-mdx-blog/mdx-plugins/page.mdx b/app/web_development/tutorials/next-js-static-mdx-blog/mdx-plugins/page.mdx deleted file mode 100644 index a49303bb..00000000 --- a/app/web_development/tutorials/next-js-static-mdx-blog/mdx-plugins/page.mdx +++ /dev/null @@ -1,80 +0,0 @@ ---- -title: MDX (remark/rehype) plugins - Tutorial -description: MDX (remark/rehype) plugins - Next.js static MDX blog | www.chris.lu Web development tutorials -keywords: ['MDX', 'remark', 'rehype', 'plugin', 'markdown', 'nextjs'] -published: 2024-07-01T11:22:33.444Z -modified: 2024-07-01T11:22:33.444Z -permalink: https://chris.lu/web_development/tutorials/next-js-static-mdx-blog/mdx-plugins -section: Web development ---- - -import { sharedMetaDataArticle } from '@/shared/metadata-article' -import Breadcrumbs from '@/components/tutorial/Breadcrumbs' -import Pagination from '@/components/tutorial/Pagination' - -export const metadata = { - title: frontmatter.title, - description: frontmatter.description, - keywords: frontmatter.keywords, - alternates: { - canonical: frontmatter.permalink, - }, - openGraph: { - ...sharedMetaDataArticle.openGraph, - images: [{ - type: "image/png", - width: 1200, - height: 630, - url: '/web_development/og/tutorials_next-static-mdx-blog/opengraph-image' - }], - url: frontmatter.permalink, - section: frontmatter.section, - publishedTime: frontmatter.published, - modifiedTime: frontmatter.modified, - tags: frontmatter.keywords, - }, -} - -%toc% - -
- - - -# MDX plugins - -What I like the most about MDX is its ecosystem of plugins, plugins for MDX are either Remark plugins or Rehype plugins - -## Difference between remark and rehype - -Quote from the remark readme: - -> Remark is a tool that transforms markdown with plugins. These plugins can inspect and change your markup - -Quote from the rehype readme: - -> rehype is a tool that transforms HTML with plugins. These plugins can inspect and change the HTML - -This means that **remark** plugins do their work by processing your markdown before it gets transformed into HTML, while **rehype** plugins will process HTML, when the plugin is done, that HTML gets transformed into JSX, and then other JSX, like a React component you imported into your MDX page gets added, you experiment with this workflow using the [MDX playground](https://mdxjs.com/playground/) (modify the input and then use the select field to switch between different modes, to see the corresponding output on the right) - -You will sometimes find a plugin for **remark** and then another plugin for **rehype**, but both do the same thing, for example, a plugin that would make a table of contents by listing all headings in your content, if it is a **remark** plugin it would search for headings like `# foo`, `## bar`, `### baz` in your markdown, while a similar **rehype** plugin would look for headings `

foo

`, `

bar

`, `

baz

` in the the HTML (after markdown got converted to HTML), in such a case it is up to you which one you want to use, there is no right or wrong here, just take the one that has the features you need, the one with more detailed documentation, the most stars on GitHub or the one with the least open Issues (it is up to you to define what criteria you want to use to judge which one is better) - -> [!NOTE] -> If you are interested in learning the difference between remark and rehype, I recommend checking out my [MDX post](/web_development/posts/mdx) - -In the previous chapter we already installed a **rehype-mdx-import-media** a Rehype plugin to convert all image paths to static imports and in the following chapters we will install and configure several [remark plugins](https://github.com/remarkjs/remark/blob/main/doc/plugins.md) and [rehype plugins](https://github.com/rehypejs/rehype/blob/main/doc/plugins.md) - -> [!MORE] -> [chris.lu "MDX" post](/web_development/posts/mdx) -> [remark "plugins" README](https://github.com/remarkjs/remark/blob/main/doc/plugins.md) -> [rehype "plugins" README](https://github.com/rehypejs/rehype/blob/main/doc/plugins.md) - - - -
diff --git a/app/web_development/tutorials/next-js-static-mdx-blog/metadata/page.mdx b/app/web_development/tutorials/next-js-static-mdx-blog/metadata/page.mdx deleted file mode 100644 index 48de5f24..00000000 --- a/app/web_development/tutorials/next-js-static-mdx-blog/metadata/page.mdx +++ /dev/null @@ -1,398 +0,0 @@ ---- -title: Metadata (for tsx and mdx pages) - Tutorial -description: Metadata (for tsx and mdx pages) - Next.js static MDX blog | www.chris.lu Web development tutorials -keywords: ['Metadata', 'tsx', 'mdx', 'pages', 'OpenGraph', 'SEO', 'nextjs'] -published: 2024-07-01T11:22:33.444Z -modified: 2024-07-01T11:22:33.444Z -permalink: https://chris.lu/web_development/tutorials/next-js-static-mdx-blog/metadata -section: Web development ---- - -import { sharedMetaDataArticle } from '@/shared/metadata-article' -import Breadcrumbs from '@/components/tutorial/Breadcrumbs' -import Pagination from '@/components/tutorial/Pagination' - -export const metadata = { - title: frontmatter.title, - description: frontmatter.description, - keywords: frontmatter.keywords, - alternates: { - canonical: frontmatter.permalink, - }, - openGraph: { - ...sharedMetaDataArticle.openGraph, - images: [{ - type: "image/png", - width: 1200, - height: 630, - url: '/web_development/og/tutorials_next-static-mdx-blog/opengraph-image' - }], - url: frontmatter.permalink, - section: frontmatter.section, - publishedTime: frontmatter.published, - modifiedTime: frontmatter.modified, - tags: frontmatter.keywords, - }, -} - -%toc% - -
- - - -# Metadata (for tsx and mdx pages) - -In this chapter, we will add **metadata** to our pages (and layout) by using the [Next.js Metadata API](https://nextjs.org/docs/app/building-your-application/optimizing/metadata), this will add meta tags inside of the `` element of our HTML documents, which are essential to help crawlers from search engines and social networks better understand the content of our pages and should result in good SEO scores - -As we saw early on in this tutorial, Next.js has created a `layout.tsx` file in the `/app` folder and already added a basic **metadata** object - -The good thing is that Next.js imported the `Metadata` type (line 2) and used it to strictly type the metadata object (line 6), meaning we will benefit from **autocomplete** in VSCode while editing our metadata object - -## Metadata in layouts - -We start by editing the `layout.tsx` file to add some entries to the metadata object: - -```tsx title="/app/layout.tsx" showLineNumbers -import './global.css' -import { Metadata } from 'next' -import HeaderNavigation from '@/components/header/Navigation' -import { Kablammo } from 'next/font/google' - -export const metadata: Metadata = { - title: { - template: '%s | example.com', - default: 'Home | example.com', - }, - description: 'My description', -} - -const kablammo = Kablammo({ - subsets: ['latin'], - variable: '--font-kablammo', - weight: ['400'], - display: 'swap', -}) - -export default function RootLayout({ - children, -}: { - children: React.ReactNode -}) { - return ( - - -
- -
-
{children}
-
-

My Footer

-
- - - ) -} -``` - -Line 7: we edit the title meta tag, the default title value was previously a string, but we turned it into an object with two properties, [title.template](https://nextjs.org/docs/app/api-reference/functions/generate-metadata#template) is a great way to ensure titles have the same structure on every page and it helps reducing repetition (DRY), the second property is the default title for the homepage (which is required) - -The template will work for any pages that are in the same route segment as the layout, as this is the root layout of our project, it means the template will work on all our pages - -How the title template works is that on every page, it will take the page title and then replace the `%s` placeholder of the template with the page title - -Line 8: we only changed the description default value to something else - -If you now launch the dev server, then open the "home" page [http://localhost:3000/](http://localhost:3000/) in your browser, then right-click and select **inspect** - -Look at what is inside your page's `` element. There are, for example, some Next.js `