diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..66e2009c --- /dev/null +++ b/.editorconfig @@ -0,0 +1,15 @@ +root = true + +[*] +indent_style = space +indent_size = 4 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true + +[*.{md,mdx}] +indent_size = 2 +insert_final_newline = true + +[*.{js,jsx,mjs,ts,tsx,mts,json}] +indent_size = 4 \ No newline at end of file diff --git a/.eslintrc.js b/.eslintrc.js deleted file mode 100644 index a56b5862..00000000 --- a/.eslintrc.js +++ /dev/null @@ -1,108 +0,0 @@ -module.exports = { - root: true, - parserOptions: { - ecmaVersion: 'latest', - sourceType: 'module', - }, - 'env': { - browser: true, - es2021: true, - node: true, - }, - ignorePatterns: [ - 'node_modules/', - '.next/', - '.vscode/', - 'public/', - 'LICENSE', - // by default we always ignore our tests folder - // to ensure the tests dont trigger errors in - // staging / production deployments - // comment out the next line to have eslint check - // the test files (in development) - 'tests/eslint/', - ], - reportUnusedDisableDirectives: true, - overrides: [ - { - files: ['**/*.ts?(x)', '**/*.md?(x)'], - extends: [ - 'next/core-web-vitals', - ], - }, - { - files: ['**/*.ts?(x)'], - extends: [ - 'plugin:@react-three/recommended', - // https://typescript-eslint.io/users/configs#recommended-configurations - '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/naming-convention': [ - 'error', - { - 'selector': 'interface', - 'format': [ - 'PascalCase', - ], - 'custom': { - 'regex': '^I[A-Z]', - 'match': true, - }, - } - ], - '@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, - }, - ], - }, - }, - { - 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 - }, - ], -} \ No newline at end of file diff --git a/.no-dead-urls.remarkrc.mjs b/.no-dead-urls.remarkrc.mjs index 4f9e073e..2c63f088 100644 --- a/.no-dead-urls.remarkrc.mjs +++ b/.no-dead-urls.remarkrc.mjs @@ -1,38 +1,52 @@ import remarkLintNoDeadUrls from 'remark-lint-no-dead-urls' +/** @type {import('remark-lint-no-dead-urls').Options} */ +const remarkLintNoDeadUrlsOptions = { + from: 'http://localhost:3000', + skipUrlPatterns: [ + // can't find the anchor element for the hash + // document is very big and loads slowly + // might be due to a timeout + '/*w3c.github.io*/', + // pexels blocks all requests with a 403 (forbidden) response + '/*pexels.com*/', + // github urls with a hash for the line numbers + // produce the error that the hash can not be found as anchor element + // I do NOT want to disable all of github.com + // disabling all the urls with a line number hash would be ideal but not sure how to do that + // for now I search for /blob/ or /blame/ in the URL and exclude those + '/*/blob/*/', + '/*/blame/*/', + // this page wants to redirect to a url with the language set + // I prefer to use the URL with no language + // so that it adapts to the user's language that will visit + '/*azure.microsoft.com*/', + // npmjs has low rate limit, often returns 429 (too many requests) + '/*npmjs.com*/', + // these domains often produce fetch errors + '/*https://developer.apple.com/*/', + '/*archive.org*/', + // images have /public in their path + // next.js however uses a cached version /_next/image?url= + // for now I exclude them all, future we need something to convert URLs + '/*/public/assets/images*/', + // discord always returns 403 (forbidden) + '/*discord.com*/', + ], + deadOrAliveOptions: { + // I will wait 60 seconds for the request to complete + timeout: 60000, + // I will retry 1 times + retries: 1, + // I will wait 60 seconds between retries + retryDelay: 60000, + }, +} + const config = { plugins: [ //[remarkLintNoDeadUrls, { from: 'https://example.com' }] - [remarkLintNoDeadUrls, { - from: 'http://localhost:3000', - skipUrlPatterns: [ - // can't find the anchor element for the hash - // document is very big and loads slowly - // might be due to a timeout - '/*w3c.github.io*/', - // pexels blocks all requests with a 403 (forbidden) response - '/*pexels.com*/', - // github urls with a hash for the line numbers - // produce the error that the hash can not be found as anchor element - // I do NOT want to disable all of github.com - // disabling all the urls with a line number hash would be ideal but not sure how to do that - // for now I search for /blob/ in the URL and exclude those - '/*/blob/*/', - // this page wants to redirect to a url with the language set - // I prefer to use the URL with no language - // so that it adapts to the user's language that will visit - '/*azure.microsoft.com*/', - // npmjs has low rate limit, often returns 429 (too many requests) - '/*npmjs.com*/', - // these domains often produce fetch errors - '/*https://developer.apple.com/*/', - '/*archive.org*/', - // images have /public in their path - // next.js however uses a cached version /_next/image?url= - // for now I eclude them all, future we need something to convert URLs - '/*/public/assets/images*/', - ] - }] + [remarkLintNoDeadUrls, remarkLintNoDeadUrlsOptions] ] } diff --git a/.remarkrc.mjs b/.remarkrc.mjs index 464c05a7..7d734eda 100644 --- a/.remarkrc.mjs +++ b/.remarkrc.mjs @@ -1,6 +1,6 @@ // presets imports -import remarkPresetLintRecommended from 'remark-preset-lint-consistent' -import remarkPresetLintConsistent from 'remark-preset-lint-recommended' +import remarkPresetLintRecommended from 'remark-preset-lint-recommended' +import remarkPresetLintConsistent from 'remark-preset-lint-consistent' import remarkPresetLintMarkdownStyleGuide from 'remark-preset-lint-markdown-style-guide' // rules imports @@ -10,15 +10,21 @@ 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' + +// remark plugins +import remarkGfm from 'remark-gfm' import remarkFrontmatter from 'remark-frontmatter' const config = { plugins: [ - // presets + // first the plugins + remarkGfm, + remarkFrontmatter, + // then the presets remarkPresetLintRecommended, remarkPresetLintConsistent, remarkPresetLintMarkdownStyleGuide, - // rules + // and finally the rules customizations // https://www.npmjs.com/package/remark-lint-maximum-heading-length [remarkLintMaximumHeadingLength, [1, 100]], // https://www.npmjs.com/package/remark-lint-unordered-list-marker-style diff --git a/.vscode/settings.json b/.vscode/settings.json index 4b541bcb..b2f405a3 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -6,14 +6,7 @@ }, "eslint.debug": true, "eslint.options": { - "extensions": [ - ".js", - ".jsx", - ".md", - ".mdx", - ".ts", - ".tsx" - ] + "flags": ["unstable_ts_config"] }, "eslint.validate": [ "markdown", diff --git a/README.md b/README.md index ce548428..86581677 100644 --- a/README.md +++ b/README.md @@ -18,17 +18,15 @@ On [chris.lu](https://chris.lu), you will find my tutorials and can learn more a ## Technologies used -During the development of the blog, I wrote a ["Next.js static MDX blog" tutorial](https://chris.lu/web_development/tutorials/next-js-static-mdx-blog) that showcases most of the technologies that I used +During the development of the blog, I wrote a ["Next.js static MDX blog" tutorial](https://chris.lu/web-development/tutorials/next-js-static-mdx-blog) that showcases most of the technologies that I used -The framework I used is [Next.js 14](https://github.com/vercel/next.js) with [React 18](https://github.com/facebook/react) (I plan on upgrading to Next.js 15 and React 19 as soon as the first stable versions get released and will update my tutorial accordingly) +The framework I used is [Next.js 15.x](https://github.com/vercel/next.js) with [React 19.x](https://github.com/facebook/react) -I added [MDX](https://mdxjs.com/) support to be able to create content using next/mdx. I then also used several remark and rehype plugins and even built two myself, [rehype-github-alerts -](https://github.com/chrisweb/rehype-github-alerts) and [remark-table-of-contents -](https://github.com/chrisweb/remark-table-of-contents) +I added [MDX](https://mdxjs.com/) support to be able to create content using **@next/mdx**. I then also used several MDX (remark and rehype) plugins and even built two myself, [rehype-github-alerts](https://github.com/chrisweb/rehype-github-alerts) and [remark-table-of-contents](https://github.com/chrisweb/remark-table-of-contents) I had a lot of fun doing my [WebGL](https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API) header animation using [react-three-fiber](https://github.com/pmndrs/react-three-fiber) (a React renderer for [three.js](https://github.com/mrdoob/three.js)) -I also added a jukebox on top using my [web-audio-api-player](https://github.com/chrisweb/web-audio-api-player) and added a dynamic waveform using my [waveform-visualizer](https://github.com/chrisweb/waveform-visualizer) and [waveform-data-generator](https://github.com/chrisweb/waveform-data-generator) packages +I also added a jukebox like music player (on the top right) using my [web-audio-api-player](https://github.com/chrisweb/web-audio-api-player) and added a dynamic waveform using my [waveform-visualizer](https://github.com/chrisweb/waveform-visualizer) and [waveform-data-generator](https://github.com/chrisweb/waveform-data-generator) packages ## Feedback & bug reports @@ -47,7 +45,11 @@ If you have feedback or want to discuss something, please use the [chris.lu gith `npm run lint-debug`: linting command but more verbose output `npm run lint-fix`: linting command that also attempts to automatically fix problems `npm run info`: the default next.js script to get some info about the project -`npm run check-urls`: check if URLs in documents are alive or not, this linting is seperate from the main linting script so that it can be used sporadically, as it makes lots of calls to 3rd party URLs to check if they are alive, it does not run during the build process so that a unreachable URL of a third party won't break the build +`npm run check-urls`: check if URLs in documents are alive or not, this linting is separate from the main linting script so that it can be used sporadically, as it makes lots of calls to 3rd party URLs to check if they are alive, it does not run during the build process so that a unreachable URL of a third party won't break the build, it is separate from eslint process and uses remark-cli + +## Node.js version + +Next.js [requires >=18.18.0](https://github.com/vercel/next.js/commit/ecd2be6d3b74d7af2513a8b355408a8f88ec6b25) (same as ESLint v9), Typescript ESLint [requires Node.js >=20.11.0](https://typescript-eslint.io/getting-started/typed-linting) (for import.meta.dirname in ESM files), this projects [package.json](./package.json) has the engines node set to 20.11.0, the latest Node.js LTS is 22.11.0 (Nov. 2024) ## License diff --git a/app/about_me/opengraph-image.tsx b/app/about_me/opengraph-image.tsx index 215d9585..71cc1a05 100644 --- a/app/about_me/opengraph-image.tsx +++ b/app/about_me/opengraph-image.tsx @@ -15,26 +15,16 @@ const title = 'About me' export const alt = `Chris.lu ${title} banner` -/*interface IImageProps { - params: { - slug: string - } - id: number -}*/ - // Image generation -export default async function OGImage(/*props: IImageProps*/) { - - //console.log(props) +export default async function Image() { - // Font const permanentMarkerRegular = fetch( new URL('/public/assets/fonts/PermanentMarker-Regular.ttf', import.meta.url) - ).then((res) => res.arrayBuffer()) + ).then(res => res.arrayBuffer()) const imageData = await fetch( new URL('/public/assets/images/og_image_background_1200x630.jpg', import.meta.url) - ).then((res) => res.arrayBuffer()) + ).then(res => res.arrayBuffer()) return new ImageResponse( // ImageResponse JSX element @@ -48,17 +38,15 @@ export default async function OGImage(/*props: IImageProps*/) { justifyContent: 'center', }} > - { - // eslint-disable-next-line jsx-a11y/alt-text, @next/next/no-img-element - - } + {/* eslint-disable-next-line jsx-a11y/alt-text, @next/next/no-img-element */} + {title} - + ), // ImageResponse options { diff --git a/app/about_me/page.mdx b/app/about_me/page.mdx index d7828c15..f19626c9 100644 --- a/app/about_me/page.mdx +++ b/app/about_me/page.mdx @@ -1,4 +1,4 @@ -import webpMindBlowingStaticImport from '/public/assets/images/animated/mind_blowing.webp' +import webpMindBlowingStaticImport from '@/public/assets/images/animated/mind_blowing.webp' import ImageAnimatedPicture from '@/components/base/image/AnimatedPicture' import { sharedMetaData } from '@/shared/metadata' @@ -8,7 +8,7 @@ export const metadata = { canonical: 'https://chris.lu/about_me', }, openGraph: { - ...sharedMetaData, + ...sharedMetaData.openGraph, url: 'https://chris.lu/about_me', }, } @@ -51,6 +51,7 @@ On tombraider.net, I had a "music box" (similar to the jukebox I now have on thi
Tombraider 1 inspired midi song:
@@ -61,6 +62,7 @@ On tombraider.net, I had a "music box" (similar to the jukebox I now have on thi
Tombraider 2 inspired midi song:
@@ -76,6 +78,7 @@ I was also able to retrieve a third song. It is only now that I realize how mela
Evening Falls (by Etherea):
@@ -135,9 +138,9 @@ When the COVID-19 pandemic started to impact us all, it created new challenges w ## Sabbatical and current projects -I have taken a [sabbatical](https://en.wikipedia.org/wiki/Sabbatical) to update my skills once again. I also developed an Android and iOS app called [Beavo](https://beavoapp.com/), it is a web app for Beach Volleyball players using [React](https://react.dev/) and [Capacitor.js](https://capacitorjs.com/), that I did for a friend. I used Xcode Cloud to create a CI/CD pipeline to automatically build the app and distribute it to testers whenever a new pull request is entered into the GitHub repository. +I have taken a [sabbatical](https://en.wikipedia.org/wiki/Sabbatical) to update my skills once again. I also developed an Android and iOS app called [Beavo](https://beavo.com), it is a web app for Beach Volleyball players using [React](https://react.dev/) and [Capacitor.js](https://capacitorjs.com/), that I did for a friend. I used Xcode Cloud to create a CI/CD pipeline to automatically build the app and distribute it to testers whenever a new pull request is entered into the GitHub repository. -I rebuilt my personal blog using [Next.js](https://nextjs.org/) and focused on [MDX](https://mdxjs.com/) (markdown) content formatting to create a static blog, which led to the creation of 2 new open source plugins [remark-table-of-contents](https://github.com/chrisweb/remark-table-of-contents) and [rehype-github-alerts](https://github.com/chrisweb/rehype-github-alerts). I also updated my [web-audio-api-player](https://github.com/chrisweb/web-audio-api-player) project which is powering the jukebox on top of this blog and had a lot of fun working on the header ("Press Start" in the top header to see the animation) using [React-three-fiber](https://r3f.docs.pmnd.rs/getting-started/introduction) a React renderer for three.js (WebGL). +I rebuilt my personal blog using [Next.js](https://nextjs.org/) and focused on [MDX](https://mdxjs.com/) (markdown) content formatting to create a static blog, which led to the creation of 2 new open source plugins [remark-table-of-contents](https://github.com/chrisweb/remark-table-of-contents) and [rehype-github-alerts](https://github.com/chrisweb/rehype-github-alerts). I also updated my [web-audio-api-player](https://github.com/chrisweb/web-audio-api-player) project which is powering the jukebox on top of this blog and had a lot of fun working on the header ("Press Start" in the top header to see the animation) using [React-three-fiber](https://docs.pmnd.rs/react-three-fiber/getting-started/introduction) a React renderer for three.js (WebGL). I read countless articles and watched educational videos on YouTube about web development and game development (using the [Godot Engine](https://godotengine.org/)) and contributed to open-source projects on GitHub, fixing bugs and helping improve documentation, for example, my latest PR for Next.js [Next.js PR #61412](https://github.com/vercel/next.js/pull/61412) just got accepted recently diff --git a/app/error.tsx b/app/error.tsx index 58b221d5..1a594946 100644 --- a/app/error.tsx +++ b/app/error.tsx @@ -19,9 +19,16 @@ export default function Error({ return (
-

Sorry, something went wrong 😞

+

+ Sorry, something went wrong +   + 😞 +

reset()} // attempt to recover by trying to re-render the segment + clickCallback={() => { + // attempt to recover by trying to re-render the segment + reset() + }} > Try again diff --git a/app/games/opengraph-image.tsx b/app/games/opengraph-image.tsx index c4b5e3ff..a0c9f1e7 100644 --- a/app/games/opengraph-image.tsx +++ b/app/games/opengraph-image.tsx @@ -15,26 +15,16 @@ const title = 'Games' export const alt = `Chris.lu ${title} banner` -/*interface IImageProps { - params: { - slug: string - } - id: number -}*/ - // Image generation -export default async function OGImage(/*props: IImageProps*/) { - - //console.log(props) +export default async function Image() { - // Font const permanentMarkerRegular = fetch( new URL('/public/assets/fonts/PermanentMarker-Regular.ttf', import.meta.url) - ).then((res) => res.arrayBuffer()) + ).then(res => res.arrayBuffer()) const imageData = await fetch( new URL('/public/assets/images/og_image_background_1200x630.jpg', import.meta.url) - ).then((res) => res.arrayBuffer()) + ).then(res => res.arrayBuffer()) return new ImageResponse( // ImageResponse JSX element @@ -48,17 +38,15 @@ export default async function OGImage(/*props: IImageProps*/) { justifyContent: 'center', }} > - { - // eslint-disable-next-line jsx-a11y/alt-text, @next/next/no-img-element - - } + {/* eslint-disable-next-line jsx-a11y/alt-text, @next/next/no-img-element */} + {title} - + ), // ImageResponse options { diff --git a/app/games/page.mdx b/app/games/page.mdx index d6c3c94f..b71ffb65 100644 --- a/app/games/page.mdx +++ b/app/games/page.mdx @@ -6,7 +6,7 @@ export const metadata = { canonical: 'https://chris.lu/games', }, openGraph: { - ...sharedMetaData, + ...sharedMetaData.openGraph, url: 'https://chris.lu/games', }, } @@ -128,7 +128,7 @@ My first PC ever had an [Intel 486DX2-66](https://en.wikipedia.org/wiki/I486) pr
![MS-DOS The Clou! game](../../public/assets/images/app/games/msdos_the_clou.png '{ screenshot }') - [The Clue!](https://en.wikipedia.org/wiki/The_Clue!) (German: Der Clou!) The mix of genres in this game was what I liked most. The first part was like a classical adventure game in which you were wandering through the city of London trying to recruit members for your crew, but there was also a second part that was a top-down view of the interior of the buildings, in which you had to execute the actual burglaries + [The Clou!](https://en.wikipedia.org/wiki/The_Clou!) (German: Der Clou!) The mix of genres in this game was what I liked most. The first part was like a classical adventure game in which you were wandering through the city of London trying to recruit members for your crew, but there was also a second part that was a top-down view of the interior of the buildings, in which you had to execute the actual burglaries
![MS-DOS The Settlers II: Veni, Vidi, Vici game](../../public/assets/images/app/games/msdos_the_settlers_ii.png '{ screenshot }') @@ -147,9 +147,10 @@ My first PC ever had an [Intel 486DX2-66](https://en.wikipedia.org/wiki/I486) pr Video (with sound) of the intro of X-COM: UFO Defense: ## Nintendo 64 diff --git a/app/global-error.tsx b/app/global-error.tsx index d1ae5dd5..44d06cb6 100644 --- a/app/global-error.tsx +++ b/app/global-error.tsx @@ -18,13 +18,20 @@ export default function GlobalError({ }, [error]) return ( - +
-

Sorry, something went wrong 😞

+

+ Sorry, something went wrong +   + 😞 +

reset()} // attempt to recover by trying to re-render the segment + clickCallback={() => { + // attempt to recover by trying to re-render the segment + reset() + }} > Try again diff --git a/app/global.css b/app/global.css index a12648c9..e66d5a5b 100644 --- a/app/global.css +++ b/app/global.css @@ -49,7 +49,7 @@ --tertiary-light-color: rgb(var(--tertiary-light-value)); --quaternary-light-value: 215 110 255; --quaternary-light-color: rgb(var(--quaternary-light-value)); - --codebloc-font-family: var(--font-sourceCodePro), ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; + --codeblock-font-family: var(--font-sourceCodePro), ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; --header-navbar-height: 50px; --cursor-default: url(/assets/cursors/secondary_mouse_default.cur), default; --cursor-pointer: url(/assets/cursors/secondary_mouse_pointer.cur), pointer; @@ -91,7 +91,7 @@ html { scroll-behavior: smooth; /* https://developer.mozilla.org/en-US/docs/Web/CSS/font-smooth */ -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; /* only macos */ + -moz-osx-font-smoothing: grayscale; /* https://developer.mozilla.org/en-US/docs/Web/CSS/text-rendering */ /*text-rendering: optimizeLegibility;*/ /* https://developer.mozilla.org/en-US/docs/Web/CSS/text-size-adjust */ @@ -575,7 +575,7 @@ progress { } code { - font-family: var(--codebloc-font-family); + font-family: var(--codeblock-font-family); font-size: var(--main-fontSize-small); counter-reset: line; } @@ -606,6 +606,16 @@ code[data-line-numbers-max-digits="3"]>[data-line]::before { border-left-color: transparent; } +[data-line].remove { + background-color: #34151b; + border-left-color: #ff0035; +} + +[data-line].add { + background-color: #2a4827; + border-left-color: #28ff00; +} + [data-highlighted-line] { background-color: #3f2046; border-left-color: var(--primary-light-color); @@ -682,7 +692,7 @@ article, padding-left: var(--main-spacing); padding-right: var(--main-spacing); margin-bottom: calc(var(--main-spacing) * 2); - /* for mobile when a chain of characters is extremly long */ + /* for mobile, when a chain of characters is extremely long */ overflow-x: clip; } @@ -795,10 +805,15 @@ aside { text-decoration: none; } -/* "external link" icon styling */ -.externalLinkIcon { +/* svg icons (font awesome) */ +.inlineIcon { color: var(--primary-light-color); - margin-left: calc(var(--main-spacing) / 5); + margin: 0 calc(var(--main-spacing) / 5); +} + +.startInlineIcon { + color: var(--primary-light-color); + margin-right: calc(var(--main-spacing) / 5); } /* github like heading anchor */ @@ -939,7 +954,7 @@ make the text color a bit darker */ 0 100%); } -.makrdown-alert-fake-border { +.markdown-alert-fake-border { width: 100%; height: 100%; position: relative; @@ -1041,3 +1056,21 @@ make the text color a bit darker */ height: 30px; padding-left: calc(var(--main-spacing) / 2); } + +.neonGreenHighlightedText { + font-weight: var(--main-fontWeight-bolder); + color: rgb(57, 255, 20); + text-shadow: 0 0 5px rgba(57, 255, 20, 0.5); +} + +.neonRedHighlightedText { + font-weight: var(--main-fontWeight-bolder); + color: rgb(255, 0, 0); + text-shadow: 0 0 5px rgba(255, 20, 20, 0.5); +} + +.neonBlueHighlightedText { + font-weight: var(--main-fontWeight-bolder); + color: rgb(0, 157, 255); + text-shadow: 0 0 5px rgba(20, 149, 255, 0.5); +} \ No newline at end of file diff --git a/app/layout.tsx b/app/layout.tsx index 73369d0c..dbe352ca 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -3,8 +3,8 @@ import './global.css' import styles from './layout.module.css' import { Permanent_Marker, VT323, Architects_Daughter, Source_Code_Pro, Anta } from 'next/font/google' import HeaderNavigation from '@/components/header/Navigation' -import BaseLink from '@/components/base/Link' -import type { Metadata } from 'next' +import Disclaimer from '@/components/footer/Disclaimer' +import type { Metadata, Viewport } from 'next' import { sharedMetaData } from '@/shared/metadata' import { Analytics } from '@vercel/analytics/react' import { SpeedInsights } from '@vercel/speed-insights/next' @@ -12,9 +12,9 @@ import { SpeedInsights } from '@vercel/speed-insights/next' export const metadata: Metadata = { // default next.js value // added this just to make the console message go away - metadataBase: process.env.VERCEL_URL - ? new URL(`https://${process.env.VERCEL_URL}`) - : new URL(`http://localhost:${process.env.PORT ?? 3000}`), + metadataBase: process.env.VERCEL_URL ? + new URL(`https://${process.env.VERCEL_URL}`) : + new URL(`http://localhost:${process.env.PORT ?? '3000'}`), title: { template: '%s | chris.lu', default: 'Home | chris.lu', @@ -30,8 +30,6 @@ export const metadata: Metadata = { }, } -import type { Viewport } from 'next' - export const viewport: Viewport = { /* on older safari this is used as background color for the top safari UI, use dark color instead of primary */ @@ -95,8 +93,7 @@ export default function RootLayout({ children }: { {children}
-
-

All content on this site is licensed under a CC BY-NC-SA 4.0 license. The source code of this project is licensed under MIT and a copy of the source code can be found in the chris.lu public GitHub repository. A list of all open source packages used to build this project can be found in the package.json file. This website uses music licensed under different creative commons licenses, the music tracks credits file can be found in the repository of this project or by clicking on the "eject" button of the player on the top right of the screen. This website uses Free Icons by Font Awesome.

+
diff --git a/app/lego/opengraph-image.tsx b/app/lego/opengraph-image.tsx index a5a8fe77..2e788afd 100644 --- a/app/lego/opengraph-image.tsx +++ b/app/lego/opengraph-image.tsx @@ -15,26 +15,16 @@ const title = 'Lego' export const alt = `Chris.lu ${title} banner` -/*interface IImageProps { - params: { - slug: string - } - id: number -}*/ - // Image generation -export default async function OGImage(/*props: IImageProps*/) { - - //console.log(props) +export default async function Image() { - // Font const permanentMarkerRegular = fetch( new URL('/public/assets/fonts/PermanentMarker-Regular.ttf', import.meta.url) - ).then((res) => res.arrayBuffer()) + ).then(res => res.arrayBuffer()) const imageData = await fetch( new URL('/public/assets/images/og_image_background_1200x630.jpg', import.meta.url) - ).then((res) => res.arrayBuffer()) + ).then(res => res.arrayBuffer()) return new ImageResponse( // ImageResponse JSX element @@ -48,17 +38,15 @@ export default async function OGImage(/*props: IImageProps*/) { justifyContent: 'center', }} > - { - // eslint-disable-next-line jsx-a11y/alt-text, @next/next/no-img-element - - } + {/* eslint-disable-next-line jsx-a11y/alt-text, @next/next/no-img-element */} + {title} -
+ ), // ImageResponse options { diff --git a/app/lego/page.mdx b/app/lego/page.mdx index e942bca3..6cdda542 100644 --- a/app/lego/page.mdx +++ b/app/lego/page.mdx @@ -6,7 +6,7 @@ export const metadata = { canonical: 'https://chris.lu/lego', }, openGraph: { - ...sharedMetaData, + ...sharedMetaData.openGraph, url: 'https://chris.lu/lego', }, } diff --git a/app/loading.tsx b/app/loading.tsx index 73b2b35e..d1ef36c0 100644 --- a/app/loading.tsx +++ b/app/loading.tsx @@ -1,4 +1,3 @@ - export default function Loading() { return ( diff --git a/app/manifest.ts b/app/manifest.ts index 957b7c41..59505844 100644 --- a/app/manifest.ts +++ b/app/manifest.ts @@ -1,84 +1,84 @@ -import { MetadataRoute } from 'next' +import type { MetadataRoute } from 'next' export default function manifest(): MetadataRoute.Manifest { return { - 'name': 'chrisweb\'s blog | chris.lu', - 'short_name': 'chris.lu', - 'theme_color': '#ff00aa', - 'background_color': '#0f0019', - 'start_url': '/', - 'orientation': 'any', - 'display': 'minimal-ui', - 'dir': 'auto', - 'lang': 'en-US', - 'description': 'chrisweb\'s blog about web development, games, Lego, music, memes, ... | chris.lu', - 'icons': [ + name: 'chrisweb\'s blog | chris.lu', + short_name: 'chris.lu', + theme_color: '#ff00aa', + background_color: '#0f0019', + start_url: '/', + orientation: 'any', + display: 'minimal-ui', + dir: 'auto', + lang: 'en-US', + description: 'chrisweb\'s blog about web development, games, Lego, music, memes, ... | chris.lu', + icons: [ { - 'purpose': 'any', - 'src': '/apple-icon1.png', - 'sizes': '152x152', - 'type': 'image/png' + purpose: 'any', + src: '/apple-icon1.png', + sizes: '152x152', + type: 'image/png' }, { - 'purpose': 'any', - 'src': '/apple-icon2.png', - 'sizes': '167x167', - 'type': 'image/png' + purpose: 'any', + src: '/apple-icon2.png', + sizes: '167x167', + type: 'image/png' }, { - 'purpose': 'any', - 'src': '/apple-icon3.png', - 'sizes': '180x180', - 'type': 'image/png' + purpose: 'any', + src: '/apple-icon3.png', + sizes: '180x180', + type: 'image/png' }, { - 'purpose': 'any', - 'src': '/apple-icon4.png', - 'sizes': '192x192', - 'type': 'image/png' + purpose: 'any', + src: '/apple-icon4.png', + sizes: '192x192', + type: 'image/png' }, { - 'purpose': 'any', - 'src': '/apple-icon5.png', - 'sizes': '512x512', - 'type': 'image/png' + purpose: 'any', + src: '/apple-icon5.png', + sizes: '512x512', + type: 'image/png' } ], - 'shortcuts': [ + shortcuts: [ { - 'name': 'Home', - 'url': '/' + name: 'Home', + url: '/' }, { - 'name': 'Web development', - 'url': '/web_development' + name: 'Web development', + url: '/web_development' }, { - 'name': 'Lego', - 'url': '/lego' + name: 'Lego', + url: '/lego' }, { - 'name': 'Games', - 'url': '/games' + name: 'Games', + url: '/games' }, { - 'name': 'Music', - 'url': '/music' + name: 'Music', + url: '/music' }, { - 'name': 'Memes', - 'url': '/memes' + name: 'Memes', + url: '/memes' }, { - 'name': 'About Me', - 'url': '/about_me' + name: 'About Me', + url: '/about_me' } ], - 'screenshots': [ + screenshots: [ { - 'src': '/assets/images/chris-lu_banner.png', - 'type': 'image/png', - 'sizes': '2560x512', + src: '/assets/images/chris-lu_banner.png', + type: 'image/png', + sizes: '2560x512', } ] } diff --git a/app/memes/opengraph-image.tsx b/app/memes/opengraph-image.tsx index cd51d466..a8e473b8 100644 --- a/app/memes/opengraph-image.tsx +++ b/app/memes/opengraph-image.tsx @@ -15,26 +15,16 @@ const title = 'Memes' export const alt = `Chris.lu ${title} banner` -/*interface IImageProps { - params: { - slug: string - } - id: number -}*/ - // Image generation -export default async function OGImage(/*props: IImageProps*/) { - - //console.log(props) +export default async function Image() { - // Font const permanentMarkerRegular = fetch( new URL('/public/assets/fonts/PermanentMarker-Regular.ttf', import.meta.url) - ).then((res) => res.arrayBuffer()) + ).then(res => res.arrayBuffer()) const imageData = await fetch( new URL('/public/assets/images/og_image_background_1200x630.jpg', import.meta.url) - ).then((res) => res.arrayBuffer()) + ).then(res => res.arrayBuffer()) return new ImageResponse( // ImageResponse JSX element @@ -48,17 +38,15 @@ export default async function OGImage(/*props: IImageProps*/) { justifyContent: 'center', }} > - { - // eslint-disable-next-line jsx-a11y/alt-text, @next/next/no-img-element - - } + {/* eslint-disable-next-line jsx-a11y/alt-text, @next/next/no-img-element */} + {title} - + ), // ImageResponse options { diff --git a/app/memes/page.mdx b/app/memes/page.mdx index 5f44d69a..49cd6d2c 100644 --- a/app/memes/page.mdx +++ b/app/memes/page.mdx @@ -6,7 +6,7 @@ export const metadata = { canonical: 'https://chris.lu/memes', }, openGraph: { - ...sharedMetaData, + ...sharedMetaData.openGraph, url: 'https://chris.lu/memes', }, } diff --git a/app/music/opengraph-image.tsx b/app/music/opengraph-image.tsx index 0b90ddc2..dc9ee9e1 100644 --- a/app/music/opengraph-image.tsx +++ b/app/music/opengraph-image.tsx @@ -15,26 +15,16 @@ const title = 'Music' export const alt = `Chris.lu ${title} banner` -/*interface IImageProps { - params: { - slug: string - } - id: number -}*/ - // Image generation -export default async function OGImage(/*props: IImageProps*/) { - - //console.log(props) +export default async function Image() { - // Font const permanentMarkerRegular = fetch( new URL('/public/assets/fonts/PermanentMarker-Regular.ttf', import.meta.url) - ).then((res) => res.arrayBuffer()) + ).then(res => res.arrayBuffer()) const imageData = await fetch( new URL('/public/assets/images/og_image_background_1200x630.jpg', import.meta.url) - ).then((res) => res.arrayBuffer()) + ).then(res => res.arrayBuffer()) return new ImageResponse( // ImageResponse JSX element @@ -48,17 +38,15 @@ export default async function OGImage(/*props: IImageProps*/) { justifyContent: 'center', }} > - { - // eslint-disable-next-line jsx-a11y/alt-text, @next/next/no-img-element - - } + {/* eslint-disable-next-line jsx-a11y/alt-text, @next/next/no-img-element */} + {title} - + ), // ImageResponse options { diff --git a/app/music/page.mdx b/app/music/page.mdx index 3add7ccb..740ae286 100644 --- a/app/music/page.mdx +++ b/app/music/page.mdx @@ -13,7 +13,7 @@ export const metadata = { canonical: 'https://chris.lu/music', }, openGraph: { - ...sharedMetaData, + ...sharedMetaData.openGraph, url: 'https://chris.lu/music', }, } @@ -30,7 +30,7 @@ export const metadata = {
# Music - ![a band of alian musicians and a human singer](../../public/assets/images/app/music/banner.png '{ banner }') + ![a band of alien musicians and a human singer](../../public/assets/images/app/music/banner.png '{ banner }') ## Spotify Playlists
@@ -58,7 +58,7 @@ export const metadata = { Indie pop 🎉 - ![indie chill sunset playlist showing a sun gowing down in front of a red and purple sky](../../public/assets/images/app/music/indie_chill_sunset.jpg '{ card }') + ![indie chill sunset playlist showing a sun going down in front of a red and purple sky](../../public/assets/images/app/music/indie_chill_sunset.jpg '{ card }') Indie chill sunset 🌇 diff --git a/app/not-found.tsx b/app/not-found.tsx index 012ccc2d..06b74333 100644 --- a/app/not-found.tsx +++ b/app/not-found.tsx @@ -1,5 +1,5 @@ import BaseLink from '@/components/base/Link' -import webpNotFoundStaticImport from '/public/assets/images/animated/404.webp' +import webpNotFoundStaticImport from '@/public/assets/images/animated/404.webp' import ImageAnimatedPicture from '@/components/base/image/AnimatedPicture' export default function NotFound() { @@ -8,7 +8,7 @@ export default function NotFound() {

404 Page not found

Sorry, I looked everywhere but somehow I can't find this page.

- +

Return Home diff --git a/app/opengraph-image.tsx b/app/opengraph-image.tsx index 0ef70e73..bab74ed7 100644 --- a/app/opengraph-image.tsx +++ b/app/opengraph-image.tsx @@ -13,26 +13,16 @@ export const contentType = 'image/png' export const alt = 'Chris.lu banner' -/*interface IImageProps { - params: { - slug: string - } - id: number -}*/ - // Image generation -export default async function OGImage(/*props: IImageProps*/) { - - //console.log(props) +export default async function Image() { - // Font const permanentMarkerRegular = fetch( new URL('/public/assets/fonts/PermanentMarker-Regular.ttf', import.meta.url) - ).then((res) => res.arrayBuffer()) + ).then(res => res.arrayBuffer()) const imageData = await fetch( new URL('/public/assets/images/og_image_background_1200x630.jpg', import.meta.url) - ).then((res) => res.arrayBuffer()) + ).then(res => res.arrayBuffer()) return new ImageResponse( // ImageResponse JSX element @@ -46,18 +36,15 @@ export default async function OGImage(/*props: IImageProps*/) { justifyContent: 'center', }} > - { - // eslint-disable-next-line @next/next/no-img-element - - } + {/* eslint-disable-next-line jsx-a11y/alt-text, @next/next/no-img-element */} + Chris.lu -

+ ), // ImageResponse options { diff --git a/app/page.tsx b/app/page.tsx index 3358d78d..0a0faf4f 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -2,25 +2,35 @@ import Typing from '@/components/animated/Typing' import styles from './page.module.css' import Link from 'next/link' import ImageAnimatedEmoji from '@/components/base/image/AnimatedEmoji' -import gifWaveStaticImport from '/public/assets/images/noto_emoji_animated/48/waving.gif' -import webpWaveStaticImport from '/public/assets/images/noto_emoji_animated/48/waving.webp' +import gifWaveStaticImport from '@/public/assets/images/noto_emoji_animated/48/waving.gif' +import webpWaveStaticImport from '@/public/assets/images/noto_emoji_animated/48/waving.webp' export default function Homepage() { return ( <>
-

Hello, World!  +

+ Hello, World! +  

Welcome to my blog, my name is Chris Weber (aka chrisweb)

-

I like Web development, Lego bricks, Music, Games, Cooking, Movies & TV shows, Memes

+

+ I like +   + Web development, Lego bricks, Music, Games, Cooking, Movies & TV shows, Memes +

-

Web Development: In this portal you will find my articles about all things web development, so mostly about Javascript (Typescript), React, Next.js, APIs, CI/CD deployment, capacitor (web apps), WebGL, but probably also some posts about Cloud (serverless, edge, CDNs, ...), AI, IoT and maybe some more

+

+ Web Development: +   + In this portal you will find my articles about all things web development, so mostly about Javascript (Typescript), React, Next.js, APIs, CI/CD deployment, capacitor (web apps), WebGL, but probably also some posts about Cloud (serverless, edge, CDNs, ...), AI, IoT and maybe some more +

@@ -29,7 +39,11 @@ export default function Homepage() {
-

Games: I have always liked playing video games, be it on consoles, PC and yes even mobile (I know shame on me 😉). Playing games is my oldest hobby I still enjoy these days. I like playing games and also like watching people play on Twitch and sometimes I even do both at the same time.

+

+ Games: +   + I have always liked playing video games, be it on consoles, PC and yes even mobile (I know shame on me 😉). Playing games is my oldest hobby I still enjoy these days. I like playing games and also like watching people play on Twitch and sometimes I even do both at the same time. +

@@ -38,7 +52,11 @@ export default function Homepage() {
-

Lego: If there is one hobby that helps me chill after a busy day it for sure is building things using bricks. I like building a lot as I can do it in the evening while also bing watching my favorite TV series or just listening to music. I like being creative and like watching Videos or Streams from other AFOLs.

+

+ Lego: +   + If there is one hobby that helps me chill after a busy day it for sure is building things using bricks. I like building a lot as I can do it in the evening while also bing watching my favorite TV series or just listening to music. I like being creative and like watching Videos or Streams from other AFOLs. +

@@ -47,7 +65,11 @@ export default function Homepage() {
-

Music: There are a lot of different activities during which I like listening to music, hence the music genres I listen to vary depending on what I do. I have my road trip playlists for when I'm in my car, my work playlist I listen to while coding, my chill playlists when building with bricks, ...

+

+ Music: +   + There are a lot of different activities during which I like listening to music, hence the music genres I listen to vary depending on what I do. I have my road trip playlists for when I'm in my car, my work playlist I listen to while coding, my chill playlists when building with bricks, ... +

@@ -56,7 +78,11 @@ export default function Homepage() {
-

Memes: A growing collection of memes that make me laugh. The most important ingredient for a good meme is humor, without it a meme is just a quote on a picture. A good meme can be used as a source of light relief during a tense situation.

+

+ Memes: +   + A growing collection of memes that make me laugh. The most important ingredient for a good meme is humor, without it a meme is just a quote on a picture. A good meme can be used as a source of light relief during a tense situation. +

@@ -65,7 +91,11 @@ export default function Homepage() {
-

About me: Everything on this blog is already about me, but I also wanted to have a more personal area where I can write about things that are not related to web development, playing games and building with bricks.

+

+ About me: +   + Everything on this blog is already about me, but I also wanted to have a more personal area where I can write about things that are not related to web development, playing games and building with bricks. +

diff --git a/app/sitemap.ts b/app/sitemap.ts index fa356cfc..22e00f8c 100644 --- a/app/sitemap.ts +++ b/app/sitemap.ts @@ -1,4 +1,4 @@ -import { MetadataRoute } from 'next' +import type { MetadataRoute } from 'next' import path from 'node:path' import fs from 'node:fs' import { glob } from 'glob' diff --git a/app/web_development/og/[key]/opengraph-image.tsx b/app/web_development/og/[key]/opengraph-image.tsx index c6d51a66..7978340e 100644 --- a/app/web_development/og/[key]/opengraph-image.tsx +++ b/app/web_development/og/[key]/opengraph-image.tsx @@ -12,7 +12,7 @@ export const size = { export const contentType = 'image/png' -export const alt = `Chris.lu article banner` +export const alt = 'Chris.lu article banner' interface IImageProps { params: { @@ -21,32 +21,31 @@ interface IImageProps { } // Image generation -export default async function OGImage(props: IImageProps) { +export default async function Image(props: IImageProps) { if (!props.params.key) { return } - if (!imageInfo[props.params.key]) { - return - } - const imageTitle = imageInfo[props.params.key][0] const imagePath = imageInfo[props.params.key][1] - const baseUrl = process.env.VERCEL_URL - ? `https://${process.env.VERCEL_URL}` - : `http://localhost:${process.env.PORT ?? 3000}` + const baseUrl = process.env.VERCEL_URL ? + `https://${process.env.VERCEL_URL}` : + `http://localhost:${process.env.PORT ?? '3000'}` - // Font - const permanentMarkerRegular = fetch( - new URL('/public/assets/fonts/PermanentMarker-Regular.ttf', import.meta.url) - ).then((res) => res.arrayBuffer()) + const antaRegular = await fetch( + new URL('/public/assets/fonts/Anta-Regular.ttf', import.meta.url) + ).then(res => res.arrayBuffer()) - // using new URL(myPath, import.meta.url) did not work const imageData = await fetch( + // relative does NOT work (for me) + //new URL('../../../../public/assets/images/app/web_development/' + imagePath + '/opengraph.jpg', import.meta.url) + // this works for font but not images + //new URL('/public/assets/images/app/web_development/' + imagePath + '/opengraph.jpg', import.meta.url) + // using this instead baseUrl + '/assets/images/app/web_development/' + imagePath + '/opengraph.jpg' - ).then((res) => res.arrayBuffer()) + ).then(res => res.arrayBuffer()) return new ImageResponse( // ImageResponse JSX element @@ -59,7 +58,7 @@ export default async function OGImage(props: IImageProps) { }} > { - // eslint-disable-next-line jsx-a11y/alt-text, @next/next/no-img-element + // eslint-disable-next-line jsx-a11y/alt-text, @next/next/no-img-element {imageTitle} | Chris.lu
-
+ ), // ImageResponse options { @@ -98,8 +97,8 @@ export default async function OGImage(props: IImageProps) { ...size, fonts: [ { - name: 'PermanentMarkerRegular', - data: await permanentMarkerRegular, + name: 'AntaRegular', + data: antaRegular, style: 'normal', weight: 400, }, diff --git a/app/web_development/opengraph-image.tsx b/app/web_development/opengraph-image.tsx index e68fa213..8a059a77 100644 --- a/app/web_development/opengraph-image.tsx +++ b/app/web_development/opengraph-image.tsx @@ -15,26 +15,16 @@ const title = 'Web development' export const alt = `Chris.lu ${title} banner` -/*interface IImageProps { - params: { - slug: string - } - id: number -}*/ - // Image generation -export default async function OGImage(/*props: IImageProps*/) { - - //console.log(props) +export default async function Image() { - // Font const permanentMarkerRegular = fetch( new URL('/public/assets/fonts/PermanentMarker-Regular.ttf', import.meta.url) - ).then((res) => res.arrayBuffer()) + ).then(res => res.arrayBuffer()) const imageData = await fetch( new URL('/public/assets/images/og_image_background_1200x630.jpg', import.meta.url) - ).then((res) => res.arrayBuffer()) + ).then(res => res.arrayBuffer()) return new ImageResponse( // ImageResponse JSX element @@ -49,7 +39,7 @@ export default async function OGImage(/*props: IImageProps*/) { }} > { - // eslint-disable-next-line jsx-a11y/alt-text, @next/next/no-img-element + // eslint-disable-next-line jsx-a11y/alt-text, @next/next/no-img-element {title} - + ), // ImageResponse options { diff --git a/app/web_development/page.mdx b/app/web_development/page.mdx index ece0ad51..3166280e 100644 --- a/app/web_development/page.mdx +++ b/app/web_development/page.mdx @@ -5,8 +5,8 @@ import styles from './page.module.css' import Link from 'next/link' import AsideContent from '@/components/aside/Content' import ImageAnimatedEmoji from '@/components/base/image/AnimatedEmoji' -import gifRocketStaticImport from '/public/assets/images/noto_emoji_animated/48/rocket.gif' -import webpRocketStaticImport from '/public/assets/images/noto_emoji_animated/48/rocket.webp' +import gifRocketStaticImport from '@/public/assets/images/noto_emoji_animated/48/rocket.gif' +import webpRocketStaticImport from '@/public/assets/images/noto_emoji_animated/48/rocket.webp' import { sharedMetaData } from '@/shared/metadata' export const metadata = { @@ -15,7 +15,7 @@ export const metadata = { canonical: 'https://chris.lu/web_development', }, openGraph: { - ...sharedMetaData, + ...sharedMetaData.openGraph, url: 'https://chris.lu/web_development', }, } @@ -34,16 +34,16 @@ export const metadata = {

Web development 

## Tutorials
- - ![voodoo lady mixing potions in a big cauldron, it represents a dev using different packages to build a project using an IDE](../../public/assets/images/app/web_development/tutorials/next-js-static-mdx-blog/banner.png '{ card }') - Next.js / React static MDX Blog + + ![voodoo lady mixing potions in a big cauldron, it represents a dev using different packages like Next.js 15 and React 19 to build an MDX static first starterkit](../../public/assets/images/app/web_development/tutorials/next-js-static-first-mdx-starterkit/banner.png '{ card }') + Next.js 15 + React 19 (ESLint 9) static first MDX starterkit
## Posts
![a futuristic city with two signs "react" and "next.js"](../../public/assets/images/app/web_development/posts/road-to-react-19-next-js-15/banner.png '{ card }') - The road to React 19 and Next.js 15 + The road to React 19 and Next.js 14 ![a female robocop, in front of a police car with the text CSP on the side door, in a futuristic city](../../public/assets/images/app/web_development/posts/csp/banner.png '{ card }') @@ -82,4 +82,11 @@ export const metadata = { MDX introduction
+ ### Archived tutorials +
+ + ![voodoo lady mixing potions in a big cauldron, it represents a dev using different packages to build a project using an IDE](../../public/assets/images/app/web_development/tutorials/next-js-static-mdx-blog/banner.png '{ card }') + Next.js 14 / React 18 static MDX Blog + +
diff --git a/app/web_development/posts/csp/page.mdx b/app/web_development/posts/csp/page.mdx index d41a4d93..6ac3fc5c 100644 --- a/app/web_development/posts/csp/page.mdx +++ b/app/web_development/posts/csp/page.mdx @@ -105,12 +105,30 @@ Content-Security-Policy: default-src 'none'; img-src 'self'; script-src 'self'; Not all policy directives are fetch directives. Check out this [MDN "Content-Security-Policy Directives" documentation](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy#directives) for a complete list of directives +### require-trusted-types-for directive + For example, adding the [require-trusted-types-for](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/require-trusted-types-for) directive is recommended to reduce the DOM XSS attack surface. This ensures that if you have a script that attempts to put a string into, for example, Element.innerHTML, but the string has not been sanitized, then CSP will consider this a violation: ```shell Content-Security-Policy: default-src 'none'; img-src 'self'; script-src 'self'; style-src 'self'; connect-src https://api.example.com; require-trusted-types-for 'script' ``` +The directive is still **experimental** but [Chrome has supported since version 83](https://developer.chrome.com/blog/new-in-chrome-83) (released in May 2020) + +If you use a packages (or have written code yourself) that violate the directive then you will get errors (in browser console) like this one: + +```shell +This document requires 'TrustedScript' assignment. +``` + +I recommend to give it a try and to enable the **require-trusted-types-for** directive, maybe start with your preview environment and then if there are no violations add it to your production builds, if you are lucky your stack won't use packages that require you to disable it in production. + +It is unlikely that this directive will work in development mode, some packages have development tools that are eventually going to violate the directive, but if you add it to directives under the production mode check (`process.env.VERCEL_ENV === 'production'`) and see no violations in your browser console when in production (or preview / staging) then you can keep it enabled + +> [!MORE] +> [MDN "require-trusted-types-for directive" documentation](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/require-trusted-types-for) +> [caniuse "trusted-types"](https://caniuse.com/trusted-types) + ### nonce, hash, and strict-dynamic An alternative to whitelisting an entire domain for external scripts, is to use **hashes** or **nonces** to whitelist only certain scripts from that source. @@ -124,7 +142,7 @@ Those nonces and hashes can also be used for inline scripts. When used for inlin Finally, I recommend checking out the strict-dynamic source expression, which can be used when you want your trust in a script to be propagated to all the scripts getting loaded by the that "root" script. But use with caution as allowing a script to load other scripts should only happen if you have unconditional trust in the source itself, as well as the sources your source gets other scripts from > [!MORE] -> [MDN "CSP hashes" documentation](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/script-src#allowlisting_external_scripts_using_hashes) +> [MDN "CSP hashes" documentation](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/script-src#whitelisting_external_scripts_using_hashes) > [MDN "CSP nonce" documentation](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/nonce) > [MDN "CSP strict-dynamic expression" documentation](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/script-src#strict-dynamic) @@ -261,7 +279,7 @@ Reporting-Endpoints: default="https://csp-logging.example.com", second-endpoint= ## Logging CSP violations -A lot of the paid SAAS Error Monitoring services can also be used to log CSP violations, [Sentry.io CSP logging documentation](https://docs.sentry.io/security-legal-pii/security/security-policy-reporting/#content-security-policy), [raygun.com](https://raygun.com/documentation/language-guides/browser-reporting/crash-reporting/csp/) or [datadoghq.com](https://www.datadoghq.com/blog/content-security-policy-reporting-with-datadog/) +A lot of the paid SAAS Error Monitoring services can also be used to log CSP violations, [Sentry.io CSP logging documentation](https://docs.sentry.io/product/security-policy-reporting/), [raygun.com](https://raygun.com/documentation/language-guides/browser-reporting/crash-reporting/csp/) or [datadoghq.com](https://www.datadoghq.com/blog/content-security-policy-reporting-with-datadog/) An alternative is to host a logging tool on your own infrastructure. For example, Mozilla published an opensource [CSP logging service called "CSP Logger"](https://github.com/mozilla/csp-logger) on GitHub that is written in Javascript, but it only supports the report-uri directive and has not been updated in years @@ -277,7 +295,7 @@ Sentry.io can be used to log CSP violations and will add those to your project i Sentry.io also supports [Certificate Transparency](https://developer.mozilla.org/en-US/docs/Web/Security/Certificate_Transparency) reports logging and [HTTP Public Key Pinning (HPKP)](https://developer.mozilla.org/en-US/docs/Glossary/HPKP). However, both those features are now obsolete. > [!MORE] -> [Sentry.io "Security Policy Reporting" documentation](https://docs.sentry.io/security-legal-pii/security/security-policy-reporting/#content-security-policy) +> [Sentry.io "Security Policy Reporting" documentation](https://docs.sentry.io/product/security-policy-reporting/) #### Sentry.io does not have the Reporting-Endpoints header (yet) @@ -287,7 +305,7 @@ The reporting API v1 **Reporting-Endpoints** header is not yet supported by sent Your localhost requests might get filtered by Sentry.io if you or a team member have enabled that feature; if you use **report-uri**, you will see the successful requests in the Network tab, but they won't show up in Sentry.io. -For a guide about how to disable the filter, check out the chapter about [disabling the "reports from localhost" filter](/web_development/posts/sentry-io#disable--enable-reports-from-localhost-filter) in my Sentry.io post +For a guide about how to disable the filter, check out the chapter about [disabling the "reports from localhost" filter](/web_development/posts/sentry-io/#disable--enable-reports-from-localhost-filter) in my Sentry.io post > [!TIP] > You probably don't want to keep the localhost reports filter disabled for too long as developing locally will generate all sorts of error logs that are not very useful, so just remember to turn the filter back on when you don't need the localhost reports anymore @@ -309,7 +327,7 @@ The problem is that Chrome (>96) will attempt to use the new **report-to directi I suggest using the report-uri directive for the time being and adding the report-to directive only when browser support for that feature has significantly improved. > [!MORE] -> [Sentry.io "Content-Security-Policy reporting" documentation](https://docs.sentry.io/security-legal-pii/security/security-policy-reporting/#content-security-policy) +> [Sentry.io "Content-Security-Policy reporting" documentation](https://docs.sentry.io/product/security-policy-reporting/) ## CSP debugging tips @@ -337,11 +355,9 @@ Start-Process -FilePath 'C:\Program Files (x86)\Google\Chrome\Application\chrome ### report-uri works in localhost, but report-to does NOT -When using **report-uri**, reporting violations work locally (localhost) as well as in preview/production (secure context / https URL) - -When using **report-to**, reporting violations will NOT work on localhost (in Chrome) without a valid SSL certificate, which is surprising as localhost usually is considered a secure context (if you know why, please let me know using the [chris.lu github discussions](https://github.com/chrisweb/chris.lu/discussions)) +When using **report-uri**, reporting violations work locally (localhost) as well as in preview/production (as those usually use a https URL and hence are considered being a secure context) -In staging (preview) / production environments, however, the **report-to** will work if those environments have an SSL certificate. +When using **report-to**, reporting violations will **NOT work on localhost** (in Chrome) without a valid **SSL certificate**, which is surprising as localhost usually is considered a secure context (if you know why, please let me know using the [chris.lu github discussions](https://github.com/chrisweb/chris.lu/discussions)) #### report-uri requests show up in the Network tab @@ -462,13 +478,15 @@ To start the server, use the following command: node csp-logging.mjs ``` +{/*If you need to set up a server to host the Node.js server, you might want to have a look at my [Tutorial: Node.js server on an AWS EC2 instance with an NGINX reverse proxy](/web_development/tutorials/node-js-app-aws-ec2/)*/} + ### Debugging Sentry.io dropping reports While testing the CSP reporting feature on Sentry.io, I had trouble understanding why some reports would not go through. In the **Stats** page on Sentry.io, I would only see that a certain number of requests had been dropped, but I would not know why. -If you want to get a little bit more information as to why requests get dropped, you can use the Sentry.io API. I recommend having a look at the [Sentry.io API chapter in my Sentry.io Post](/web_development/posts/sentry-io#investigating-sentryio-dropped-requests) +If you want to get a little bit more information as to why requests get dropped, you can use the Sentry.io API. I recommend having a look at the [Sentry.io API chapter in my Sentry.io Post](/web_development/posts/sentry-io/#investigating-sentryio-dropped-requests) ### Debugging Chrome requests using Netlog diff --git a/app/web_development/posts/git/page.mdx b/app/web_development/posts/git/page.mdx index e822c71a..cec18e02 100644 --- a/app/web_development/posts/git/page.mdx +++ b/app/web_development/posts/git/page.mdx @@ -2,7 +2,7 @@ title: git keywords: ['git', 'commit', 'repository', 'branch'] published: 2024-08-03T11:22:33.444Z -modified: 2024-09-11T14:22:33.444Z +modified: 2024-09-04T14:22:33.444Z permalink: https://chris.lu/web_development/posts/git section: Web development --- @@ -37,7 +37,7 @@ export const metadata = { # git -![two super heros pointing at each other, it represents two versions of a file getting compared in a git diff](../../../../public/assets/images/app/web_development/posts/git/banner.png 'when two files meet in git diff { banner }') +![two super heroes pointing at each other, it represents two versions of a file getting compared in a git diff](../../../../public/assets/images/app/web_development/posts/git/banner.png 'when two files meet in git diff { banner }') **git** is an open-source version control system used for tracking changes in source code during software development. Git gives every developer a local copy of a project. After a developer has made his changes, they create a new commit and then push it to a central repository. Other developers can then pull that commit from the central repository into their local copy. Git makes branching (creating a separate line of development) and merging (combining changes from different branches) easy. @@ -97,16 +97,11 @@ To initialize a git project locally, you first need to use the following command git init ``` -> [!MORE] -> [git documentation](https://git-scm.com/doc) - -## link the remote repository to your local project - -If you already have a GitHub repository (I will use GitHub, but you could use any other Git cloud service, like GitLab or Bitbucket if you prefer those services), and you have a local repository, but both are not linked, then this is what you need to do +I will use GitHub as the origin, but you could use any other Git cloud service, like GitLab. -Go to [github.com](https://github.com) and create a new repository (if you haven't already created one), by clicking on the "+" icon on the left of your user avatar (in the top right section of the page), then select "new repository". +Next, go to GitHub and create a new repository by clicking on the "+" icon on the left of your user avatar (in the top right section of the page), then select "new repository". -Then use the following command to link the remote origin to your local git setup (if you have no local repository yet, check out the previous [initialze git](#initialize-git) chapter): +Then use the following command to add the origin to your local git setup: ```shell git remote add origin https://github.com/GITHUB_USER_NAME/GITHUB_REPOSITORY_NAME.git @@ -118,20 +113,29 @@ If you are using the git credential manager and have multiple users, then you ma git remote add origin https://DEFAULT_GIT_USER_NAME@github.com/GITHUB_USER_NAME/GITHUB_REPOSITORY_NAME.git ``` -Finally you can verify that your local copy and the remote origin are linked by using the following command: +> [!MORE] +> [git documentation](https://git-scm.com/doc) +> [GitHub "Adding locally hosted code to GitHub" documentation](https://docs.github.com/github/importing-your-projects-to-github/adding-an-existing-project-to-github-using-the-command-line) -```shell -git remote -v -``` +## Choosing a Git Branching Strategy + +There are a lot of different approaches when it comes to managing code using branches, some popular Git branching strategies are for example [Git Flow](https://nvie.com/posts/a-successful-git-branching-model/), [GitHub Flow](https://docs.github.com/en/get-started/using-github/github-flow), [GitLab Flow](https://about.gitlab.com/topics/version-control/what-is-gitlab-flow/), [Trunk-based development](https://www.atlassian.com/continuous-delivery/continuous-integration/trunk-based-development) and many more. -This should display two links to the origin, one for **fetch** and another one for **push** commands +There is no right or wrong when choosing a Git branching strategy, the goal is NOT to use the most popular Git flow, the goal is to find and use the one that best suits your workflow. I recommend checking out the above links and familiarizing yourself with the different workflows, then try the one out that you (your team) think is best suited, and if it does not fit perfectly you can still adjust the rules and make your own Git flow 😉. + +Also don't overthink it, you can still switch to another git flow later. Why not just start with a rather simple Git flow with only two branches. The **main** branch a second branch that you could name **preview** (some prefer to call it **staging** or **testing**). When you develop locally you do in the preview branch and then commit your code updates into the remote preview (on GitHub, Gitlab). If you have a CI/CD pipeline in place should trigger an automatic deployment. After testing the preview deployment you are ready to make a pull request (PR) to merge the code into the main branch, the main branch will then get deployed in production. + +If you work with others on your blog project, to avoid having to deal with merge conflicts regularly, you might want to create a branch per feature (hence called feature branches) and use that branch for development, before merging your code into preview (and later into main) > [!MORE] -> [GitHub "Adding locally hosted code to GitHub" documentation](https://docs.github.com/github/importing-your-projects-to-github/adding-an-existing-project-to-github-using-the-command-line) +> [Git Flow](https://nvie.com/posts/a-successful-git-branching-model/) +> [GitHub Flow](https://docs.github.com/en/get-started/using-github/github-flow) +> [GitLab Flow](https://about.gitlab.com/topics/version-control/what-is-gitlab-flow/) +> [Trunk-based development](https://www.atlassian.com/continuous-delivery/continuous-integration/trunk-based-development) ## Cloning a repository -To clone one of your repositories (hosted by remote git service like GitHub), first get the HTTPS web URL of the repository, and then use the following command: +To clone a repository first get the HTTPS web URL of the repository, and then use the following command: ```shell git clone https://github.com/GITHUB_USER_NAME/GITHUB_REPOSITORY_NAME.git @@ -154,70 +158,6 @@ git remote set-url origin https://DEFAULT_GIT_USER_NAME@github.com/GITHUB_USER_N > [GitHub "git-credential-manager" repository](https://github.com/git-ecosystem/git-credential-manager) > [git-credential-manager documentation](https://github.com/git-ecosystem/git-credential-manager/blob/release/docs/README.md) -## git fetch vs pull - -A fetch will only **fetch** the changes from the remote repository into your local copy and then stops. - -A **pull** will also fetch the remote changes but then additionally (attempt) to merge, so **pull = fetch + merge** (a pull will abort if there are conflicts). - -So usually after a git fetch you would use the `git merge` command to also merge the changes. The advantage of NOT doing a pull but use a **fetch** instead, is that you can still save and commit your local changes before doing using the `git merge` command. - -A **pull** on the other hand is two commands in one and recommended for when you have no unsaved and uncommited changes in your local copy and just quickly want to fetch + merge the remote changes into your local files. - -## remote vs upstream - -When you create a repository on GitHub (or another git service), then this repository will be your **remote** repository (also called the **origin**) - -When you do a fork of a repository, then the original repository will be the **updtream** repository and your fork will be the **remote** repository - -If you have cloned your fork (remote repository) locally and use the following command: - -```shell -git remote -v -``` - -Then will display two links from your local, the first origin is for **fetch** and the second one is for **push** commands - -So when you use the following command (or if you want to immediatly merge the remote changes with your uncommited changes use `git pull` instead): - -```shell -git fetch -``` - -You will get the remote changes (if there any) - -If you now want to also create a link to the upstream repository (the original one you forked earlier), then use the following command: - -```shell -git remote add upstream https://github.com/ORIGINAL_REPOSITORY_USER/ORIGINAL_REPOSITORY_NAME.git -``` - -If you now use the **remote** command again, you will see that there are now 4 entries: - -```shell -git remote -v -``` - -You now also have a link to the repository you forked, for fetch and push commands - -If there are changes in the repository you forked and you want to fetch those to update your local repository, then you use the fetch command like this: - -```shell -git fetch upstream -``` - -Finally to merge the fetched changes with the code in your local repository, use this command: - -```shell -git merge upstream/master master -``` - -Or if you prefer to do both at once (which is preferred if you have no uncommited changes), use the pull command instead, like this: - -```shell -git pull upstream -``` - ## git status To list the local files that differ from the ones in the remote repository and list files that are not tracked, use the following command: @@ -408,6 +348,6 @@ Edit the git config in your repository, change **ignorecase** from true to false ``` > [!MORE] -> [git "ignoreCase" documentation](https://git-scm.com/docs/git-config/2.14.6#Documentation/git-config.txt-coreignoreCase) +> [git "ignoreCase" documentation](https://www.git-scm.com/docs/git-config/2.14.6#Documentation/git-config.txt-coreignoreCase) diff --git a/app/web_development/posts/github/page.mdx b/app/web_development/posts/github/page.mdx index 9a97186f..fb8b40a3 100644 --- a/app/web_development/posts/github/page.mdx +++ b/app/web_development/posts/github/page.mdx @@ -117,9 +117,9 @@ You are finally done. Now click on **Create repository** to create your new repo GitHub will now have created a repository for you with a .gitignore, a LICENSE, and, if you chose that option, a README.md file. > [!MORE] -> [github "Creating an account on GitHub" documentation](https://docs.github.com/en/get-started/start-your-journey/creating-an-account-on-github) +> [github "Creating an account on GitHub" documentation](https://docs.github.com/en/get-started/quickstart/creating-an-account-on-github) > [github "Creating a new repository" documentation](https://docs.github.com/en/repositories/creating-and-managing-repositories/creating-a-new-repository) -> [open source initiative MIT license page](https://opensource.org/license/mit) +> [open source initiative MIT license page](https://opensource.org/license/mit/) > [creative commons zero (CC0) page](https://creativecommons.org/public-domain/cc0/) ## Switch branches diff --git a/app/web_development/posts/mdx/page.mdx b/app/web_development/posts/mdx/page.mdx index 80188956..f19c2843 100644 --- a/app/web_development/posts/mdx/page.mdx +++ b/app/web_development/posts/mdx/page.mdx @@ -2,7 +2,7 @@ title: MDX keywords: ['MDX', 'markdown', 'remark', 'rehype', 'MDAST', 'HAST'] published: 2024-08-10T11:22:33.444Z -modified: 2024-08-10T11:22:33.444Z +modified: 2024-10-25T10:10:10.444Z permalink: https://chris.lu/web_development/posts/mdx section: Web development --- @@ -41,7 +41,7 @@ export const metadata = { [MDX](https://mdxjs.com/) is an extension of **markdown**, MDX means [markdown](https://en.wikipedia.org/wiki/Markdown) + [JSX](https://en.wikipedia.org/wiki/JSX_(JavaScript)), [markdown](https://daringfireball.net/projects/markdown/syntax) is a markup language that can be used to format raw text, it was developed in 2004 by John Gruber in collaboration with Aaron Swartz and [JSX](https://react.dev/learn/writing-markup-with-jsx) is a syntax extension for JavaScript that lets you write HTML-like markup inside a JavaScript file -For an in-depth tutorial about how to use MDX with Next.js, check out my [Tutorial: Next.js static MDX blog](/web_development/tutorials/next-js-static-mdx-blog) +For an in-depth tutorial about how to use MDX with Next.js 15, check out my [Tutorial: Next.js 15 MDX starterkit](/web_development/tutorials/next-js-static-first-mdx-starterkit) ## What is MDX (behind the scenes) @@ -55,15 +55,21 @@ All transformations happen thanks to utilities built on top of [unified](https:/ > Unified is an interface for parsing, inspecting, transforming, and serializing content through syntax trees -The specifications are all managed by the [syntax-tree organisation on github](https://github.com/syntax-tree) which is part of the **unified** collective, this is the home for several **syntax tree** specifications, the base syntax tree is called [unist](https://github.com/syntax-tree/unist), **unist** is a universal syntax tree specification, it is part of the family of syntax trees called **Abstract Syntax Trees**s hence the abbreviation **AST**s, on top of unist you have the other syntax trees that we are interested in, the first one is called [MDAST](https://github.com/syntax-tree/mdast) this is the specification that represents markdown in a syntax tree and the second one is called [HAST](https://github.com/syntax-tree/hast) which is the specification that represents HTML in a syntax tree +The specifications are all managed by the [syntax-tree organization on github](https://github.com/syntax-tree) which is part of the **unified** collective, this is the home for several **syntax tree** specifications, the base syntax tree is called [unist](https://github.com/syntax-tree/unist), **unist** is a universal syntax tree specification, it is part of the family of syntax trees called **Abstract Syntax Trees**s hence the abbreviation **AST**s, on top of unist you have the other syntax trees that we are interested in, the first one is called [MDAST](https://github.com/syntax-tree/mdast) this is the specification that represents markdown in a syntax tree and the second one is called [HAST](https://github.com/syntax-tree/hast) which is the specification that represents HTML in a syntax tree, but because we use MDX and not pure markdown, the two syntax trees **MDAST** and **HAST** have supersets, the superset of **MDAST** is called [MDXAST](https://github.com/mdx-js/specification#mdxast) and the superset of **HAST** is called [MDXHAST](https://github.com/mdx-js/specification#mdxhast) -**MDAST** and **HAST** are the syntax tree specifications, but the actual tools are [remnark](https://github.com/remarkjs/remark), which is a tool that transforms markdown with plugins, and [rehype](https://github.com/rehypejs/rehype) which is a tool that transforms HTML with plugins +**MDAST** and **HAST** are the syntax tree specifications, but the actual tools are Remark and Rehype. + +[remnark](https://github.com/remarkjs/remark) is a tool that transforms markdown with plugins, which they explain well in their README: + +> remark supports CommonMark by default. Non-standard markdown extensions can be enabled with plugins + +[rehype](https://github.com/rehypejs/rehype) is similar to remark but has another purpose, Rehype is a tool that transforms HTML with plugins MDX supports both **remark** and **rehype** plugins. More about plugins in the ["using plugins to extend MDX" chapter](#using-plugins-to-extend-mdx) Note: if you want to experiment with MDX content, there is a great tool called [MDX playground](https://mdxjs.com/playground/). You can experiment with MDX content, and it will help you visualize how MDX content gets transformed from MDAST (markdown AST) to HAST (HTML AST) to ESAST (javascript / JSX) -## Collection of informative souces +## Collection of informative sources * [markdown website by its author John Gruber (aka daringfireball)](https://daringfireball.net/projects/markdown/syntax) * [MDX website](https://mdxjs.com/) @@ -72,15 +78,62 @@ Note: if you want to experiment with MDX content, there is a great tool called [ * [GFM (GitHub Flavored Markdown) specification](https://github.github.com/gfm/) * [unified (js) website](https://unifiedjs.com/) * [unified github repository](https://github.com/unifiedjs/unified) -* [syntax-tree organisation on github](https://github.com/syntax-tree) +* [syntax-tree organization on github](https://github.com/syntax-tree) * [unist specification github repository](https://github.com/syntax-tree/unist) * [MDAST specification github repository](https://github.com/syntax-tree/mdast) * [HAST specification github repository](https://github.com/syntax-tree/hast) -* [MDX (remark-mdx)](https://mdxjs.com/packages/remark-mdx/) +* [MDX specification (mdx-js) github repository](https://github.com/mdx-js/specification) +* [MDXAST chapter in the mdx-js specification](https://github.com/mdx-js/specification#mdxast) +* [MDXHAST chapter in the mdx-js specification](https://github.com/mdx-js/specification#mdxhast) * [remnark github repository](https://github.com/remarkjs/remark) * [rehype github repository](https://github.com/rehypejs/rehype) * [MDX playground](https://mdxjs.com/playground/) +## VSCode extensions + +There are a lot of VSCode extensions available for both MDX and markdown, open your [extensions view](/web_development/posts/vscode#vscode-extensions-view) in VSCode and search for MDX or markdown, or visit the online [marketplace](https://marketplace.visualstudio.com/) + +In the following chapters I will introduce two extensions, one for MDX and one for markdown + +### Markdownlint extension for VSCode + +[Markdownlint](https://marketplace.visualstudio.com/items?itemName=DavidAnson.vscode-markdownlint) is another popular extension, that is great when you quickly want to lint pure markdown files, like your projects README.md + +> [!WARN] +> it is not recommend to install the plugin if working with MDX as it will for example use markdown linting rules like the [no-inline-html](https://github.com/DavidAnson/markdownlint/blob/v0.36.1/doc/md033.md) rule, but that rule is invalid when linting MDX (which allows using HTML elements inside of MDX content) + +### MDX extension for VSCode + +There is a very useful [MDX extension for VSCode](https://marketplace.visualstudio.com/items?itemName=unifiedjs.vscode-mdx) that I recommend you install (if you work with MDX in VSCode) + +The extension will read your .remarkrc(.js|.mjs) file (if you have one in the root of your workspace (project)) and apply the same rules you use on the command line inside of VSCode, meaning when there is a violation (based on your rules) it will underline the code that is responsible for the violation (green for warnings and red for errors) + +The extension will also add MDX language support to VSCode. The support is experimental and there is a problem when setting the .mdx file association to be MDX instead of markdown, which is that VSCode will remove features like the markdown preview, I like to have the preview (even if it does NOT understand MDX and is only useful to get an idea of how the markdown parts will get rendered), the second disadvantage is that some themes have no support for MDX code highlighting colors, meaning when using some themes the MDX content will get displayed as simple text with no colors (or only a few) + +#### install MDX for VSCode + +Open the [extensions view](/web_development/posts/vscode#vscode-extensions-view), then search for an extension named **MDX** (published my unified) and then click on the **install** button + +> [!Note] +> This extensions MDX language support is still experimental, but I had no major problems when using it + +Unlike other extension, the **MDX extension** does not require any custom settings configuration, install it and you are done + +#### file associations + +To change the file association (matching a **file extension** with a **language mode**), first open an MDX file, then in the bottom right corner of VSCode you will either have "Markdown" or "MDX" set as default, if it is set to MDX then I recommend setting it to "Markdown" + +Another option, if you have a `.vscode/settings.json` file in your workspace, is to edit that file and add the file association for MDX manually: + +```json title=".vscode/settings.json" + "files.associations": { + "*.mdx": "markdown" + }, +``` + +> [!NOTE] +> by changing the file association to markdown, you are NOT disabling the MDX extension, it will continue to work and read your remark-link configuration file (if you have one in your workspace) + ## Frameworks with MDX support All major frameworks support MDX either out of the box or via packages that you can install separately, Next.js has [@next/mdx](https://www.npmjs.com/package/@next/mdx), but there are alternatives that I listed in the [@next/mdx alternatives](#nextmdx-alternatives) chapter, Astro has [@astrojs/mdx](https://github.com/withastro/astro/tree/main/packages/integrations/mdx/), Remix devs often use [mdx-bundler](https://www.npmjs.com/package/mdx-bundler), and Gatsby has [gatsby-plugin-mdx](https://www.npmjs.com/package/gatsby-plugin-mdx), there is also a popular project called [Docusaurus](https://docusaurus.io/) which uses MDX, but it is more opinionated than the other frameworks as it focuses on building a documentation website diff --git a/app/web_development/posts/node-js/page.mdx b/app/web_development/posts/node-js/page.mdx index b71c2dd4..751badbd 100644 --- a/app/web_development/posts/node-js/page.mdx +++ b/app/web_development/posts/node-js/page.mdx @@ -1,8 +1,8 @@ --- title: Node.js -keywords: ['Node.js', 'Nodejs'] +keywords: ['Node.js', 'Nodejs', 'runtime', 'javascript', 'js'] published: 2024-08-09T11:22:33.444Z -modified: 2024-08-15T14:02:33.444Z +modified: 2024-11-17T14:02:33.444Z permalink: https://chris.lu/web_development/posts/node-js section: Web development --- @@ -39,7 +39,9 @@ export const metadata = { ![a waterfall of code with the text node.js](../../../../public/assets/images/app/web_development/posts/node-js/banner.png '{ banner }') -**Node.js** is an open-source, cross-platform JavaScript runtime environment that executes JavaScript code outside a web browser, allowing developers to use JavaScript for server-side scripting. +When [Node.js](https://nodejs.org/) first showed up, most people were probably using PHP, Java or Ruby as backend language. **Node.js** gave us the possibility to use Javascript outside a web browser, meaning that for the first time we could use the same language for backend as well as frontend code. If you add other tools like [Electron](https://www.electronjs.org/), [Cordova](https://cordova.apache.org/) or [Capacitor](https://capacitorjs.com/) to your stack and you can use Javascript for your desktop and mobile apps too. It will never be possible to have a fully Isomorphic cross platform codebase, but by using Node.js you can reuse some of your Javascript code. + +> Node.js is an open-source, cross-platform JavaScript runtime environment that executes JavaScript Under the hood, Node.js uses the Chrome (chromium) [V8 engine](https://nodejs.org/en/learn/getting-started/the-v8-javascript-engine), meaning it is based on the same asynchronous, event-driven model that Chrome uses. Still, there are differences. If you would like to learn more about those differences, check out the Node.js article [Differences between Node.js and the Browser](https://nodejs.org/en/learn/getting-started/differences-between-nodejs-and-the-browser). @@ -47,13 +49,14 @@ The significant advantage when using Node.js is that web applications can be wri ## Installation -There are several options when it comes to installing Node.js. The first option is good for beginners experimenting with Node.js and wanting a quick and easy installation. The second option uses a Node version manager to install Node.js. This has the advantage that you can install several versions of Node.js in parallel on your computer, and then, depending on which project you work on, you can switch between versions (for example, you might want to use the latest version of Node.js for a personal project and have another LTS version of Node.js installed in parallel for a work-related project) +There are several options when it comes to installing Node.js. The first option is to use an installer for a specific version of Node.js, which is a good solution if you install Node.js on a server. The second option uses a Node version manager to install Node.js, which is usually a better solution for your local machine as it has the advantage that you can install several versions of Node.js in parallel on your computer. Using a Node version manager, depending on which project you work on, you can switch between versions (for example, you might want to use the latest version of Node.js for a personal project and have another LTS version of Node.js installed in parallel for a work-related project) * Head over to the [Node.js download page](https://nodejs.org/en/download) and download the latest LTS version for your operating system (OS). On Mac OS X, you can also use brew to install Node.js using this command `brew install node` -* Alternatively, use a Node version manager to install Node.js. If you are on Windows have a look at [nvm-windows](https://github.com/coreybutler/nvm-windows). For Mac OS X, check out [nvm](https://github.com/nvm-sh/nvm). For Linux distributions, check out [NodeSource](https://github.com/nodesource/distributions). For a more complete list of options, check out the [npm.js "download and install Node.js" documentation](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm/) page +* Alternatively, use a Node version manager to install Node.js. If you are on Windows have a look at [nvm-windows](https://github.com/coreybutler/nvm-windows). For Mac OS X, check out [nvm](https://github.com/nvm-sh/nvm). For Linux distributions, check out [NodeSource](https://github.com/nodesource/distributions). For a more complete list of options, check out the [npm.js "download and install Node.js" documentation](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm) page > [!MORE] -> [npm.js "download and install Node.js" documentation](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm/) +> [Node.js "Download Node.js the way you want" page](https://nodejs.org/en/download/package-manager) +> [npm.js "download and install Node.js" documentation](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm) ### Should I install the LTS or the current version @@ -75,6 +78,26 @@ You can use the following command in the VSCode terminal (or your favorite comma node -v ``` +## Checking if your current Node.js is still secure + +Sometimes you wonder if you need to update Node.js on your server(s) to fix a security flaw or if you can continue to use the current version (for another ten years 😉) + +The Node.js team member has released a little tool called [is-my-node-vulnerable](https://github.com/RafaelGSS/is-my-node-vulnerable), all you need to do is run this command: + +```shell +npx is-my-node-vulnerable +``` + +To learn more about what it does and especially how it does it, I recommend checking out the projects [README](https://github.com/RafaelGSS/is-my-node-vulnerable), to give you an idea of what it does I will quote a few lines from their README: + +> **is-my-node-vulnerable** helps ensure the security of your Node.js installation by checking for known vulnerabilities. +> +> It compares the version of Node.js you have installed (process.version) to the [Node.js Security Database](https://github.com/nodejs/security-wg/tree/main/vuln) and alerts you if a vulnerability is found. + +> [!MORE] +> [GitHub "is-my-node-vulnerable" readme](https://github.com/RafaelGSS/is-my-node-vulnerable) +> [GitHub "Node.js Core and Ecosystem vulnerabilities" readme](https://github.com/nodejs/security-wg/tree/main/vuln) + ## Node.js alternatives If, for some reason, you are unhappy with Node.js or are just curious and want to know more about alternatives, then I recommend checking out the following two JavaScript runtimes: @@ -88,6 +111,7 @@ If, for some reason, you are unhappy with Node.js or are just curious and want t > Bun is an all-in-one JavaScript toolkit and runtime designed for speed. It includes a bundler, a test runner, and a Node.js-compatible package manager. > [!MORE] +> [Announcing Deno 2 video on YouTube](https://www.youtube.com/watch?v=d35SlRgVxT8) > [Bun 1.0 introduction video on YouTube](https://www.youtube.com/watch?v=BsnCpESUEqM) ## Did you know? diff --git a/app/web_development/posts/npm/page.mdx b/app/web_development/posts/npm/page.mdx index 224bb459..c0b130fd 100644 --- a/app/web_development/posts/npm/page.mdx +++ b/app/web_development/posts/npm/page.mdx @@ -2,7 +2,7 @@ title: npm & package.json keywords: ['npm', 'package.json', 'publishing', 'package'] published: 2024-08-08T11:22:33.444Z -modified: 2024-08-08T11:22:33.444Z +modified: 2025-01-01T01:01:01.001Z permalink: https://chris.lu/web_development/posts/npm section: Web development --- @@ -70,10 +70,10 @@ npm install -g npm@latest **package.json** is a file you will find in most Javascript (Typescript) projects. Package.json is a JSON file that contains metadata about the project, like the project name, a description and the current version, a list of dependencies as well as devDependencies, scripts (custom scripts that can be run from the command line using `npm run SCRIPT_NAME`), ... -**package-lock.json** is a file that gets created when you install packages using a package manager. It lists the exact version of each installed package and its dependencies (unlike the package.json itself, which often allows a range of versions to be installed). This file is important as it can be used to create reproducible builds. When you test your project on the staging server and then deploy it to production, you don't want the package versions to be different, as this could introduce bugs after you are done testing if a package uses a newer version in production than previously on the staging server. +**package-lock.json** is a file that gets created when you install packages using a package manager. It lists the exact version of each installed package and its dependencies (unlike the package.json itself, which often allows a range of versions to be installed). This file is important as it can be used to create reproducible builds. When you test your project on the staging (pre-production) server and then deploy it to production, you don't want the package versions to be different, as this could introduce bugs after you are done testing if a package uses a newer version in production than previously on the staging server. > [!MORE] -> [npmjs.com "package.json" documentation](https://docs.npmjs.com/cli/configuring-npm/package-json/) +> [npmjs.com "package.json" documentation](https://docs.npmjs.com/cli/configuring-npm/package-json) > [nodejs.org "Modules: Packages" documentation](https://nodejs.org/api/packages.html) ### creating a package.json @@ -122,11 +122,11 @@ Packages use semantic versioning (semver) for their version numbers, with semver * the major version indicates that existing features got modified in a way that potentially will introduce breaking changes, so when updating a package and there is a change in the major version, it is highly recommended that you check out the package's changelog. If you find breaking changes, you need to verify if those will impact your code that uses the package. If there is an impact, then you need to update your code accordingly after installing the update and before committing the update > [!NOTE] -> when you install a dependency, the verion will be prefixed with a **^** (carret), when using **caret ranges** npm will install the version you specified as long as there is no newer version available, if however a newer PATCH or even MINOR version is available then npm will install the newer version instead, if we take the example above where we have "^18.2.0", this means npm will install react 18.2.0 as long as there is no update, however if react releases a fix for an existing feature and increment their PATCH number then the newest version would be 18.2.1 and if you now use the npm install command then npm will install 18.2.1 (and not 18.2.0 anymore), this is also true if the react team adds a new feature and bumps their MINOR number to 18.3.0 then npm will install that version, however if the react team adds breaking changes and the new version is 19.0.0 then npm will not update your package because this version might introduce breaking changes, instead npm will use the latest 18.x.x available version +> when you install a dependency, the version will be prefixed with a **^** (carret), when using **caret ranges** npm will install the version you specified as long as there is no newer version available, if however a newer PATCH or even MINOR version is available then npm will install the newer version instead, if we take the example above where we have "^18.2.0", this means npm will install react 18.2.0 as long as there is no update, however if react releases a fix for an existing feature and increment their PATCH number then the newest version would be 18.2.1 and if you now use the npm install command then npm will install 18.2.1 (and not 18.2.0 anymore), this is also true if the react team adds a new feature and bumps their MINOR number to 18.3.0 then npm will install that version, however if the react team adds breaking changes and the new version is 19.0.0 then npm will not update your package because this version might introduce breaking changes, instead npm will use the latest 18.x.x available version > [!MORE] > [Semantic Versioning Specification](https://semver.org/) -> [npmjs-com "About semantic versioning" documentation](https://docs.npmjs.com/about-semantic-versioning/) +> [npmjs-com "About semantic versioning" documentation](https://docs.npmjs.com/about-semantic-versioning) #### dependency version pinning (optional) @@ -150,11 +150,44 @@ This will add the dependencies to your package.json as before, but the differenc #### VSCode extension for manual version updates -To make manual updates of my dependencies easier, I use a VSCode extension called [versionlens](https://marketplace.visualstudio.com/items?itemName=pflannery.vscode-versionlens). I like this extension because when I open my package.json, it will fetch all the versions of the packages I have installed and tell me if a new version is available. If there is a new version and I want to use it, all I need to do is click on **↑ latest x.x.x** (where x.x.x stands for the new version number), and the version lens will insert the new version for me. When I'm done with updating the versions, I save the package.json and then install the new versions with the command `npm i` +To make manual updates of my dependencies easier, I use a VSCode extension called [versionlens](https://marketplace.visualstudio.com/items?itemName=pflannery.vscode-versionlens). I like this extension because when I open my package.json, it will fetch all the versions of the packages I have installed and tell me if a new version is available. If there is a new version and I want to use it, all I need to do is click on **↑ latest x.x.x** (where x.x.x stands for the new version number), and the version lens will insert the new version for me. When I'm done with updating the versions, I save the package.json and then update everything using the `npm update` command > [!MORE] > [VSCode "versionlens" extension](https://marketplace.visualstudio.com/items?itemName=pflannery.vscode-versionlens) +#### Npm cli to automatically update semver versions + +To not just update your dependencies but also their semver version in the package.json file, use the update command with the --save flag, like this: `npm update --save`. Without the save flag npm would only update versions in your package-lock.json + +> [!WARN] +> if you are using tags in your package.json instead of fixed versions, using the npm update with the save flag will replace the tags in your package.json with fixed versions, so if you use tags use the update command without the flag to only update the package-lock.json (according to the tags you have set in the package.json) + +> [!MORE] +> [npmjs.com "npm update" documentation](https://docs.npmjs.com/cli/v11/commands/npm-update) + +#### missing binaries due to corrupted lock file + +I encountered a problem I wanted to highlight as it took me some time to figure out what was wrong (thank you vercel support for helping me out) where installing my dependencies in my CI/CD kept failing with weird error messages about packages failing to install: + +> unhandledRejection Error: binary for this platform/architecture not found! + +Usually if there is an integrity problem, you get a clear message like this one: + +> npm error code EINTEGRITY + +What caused the problem was a replace all I had done to change the version of a package. Instead of just replacing the version in the package.json I had also replaced matching versions in the package-lock.json. This meant that I now had a lock file where some packages had an updated version that was not matching the integrity hash anymore. This led to the unhandled rejection. + +So this is a reminder to myself, to make sure you never manually edit a package-lock.json. Instead if something is not right, you should delete the lock file (keep the package.json) and then use `npm i` to create a fresh package-lock.json. + +The package-lock.json file contains a list of all packages that got installed in your node_modules. Each module can have an integrity value: + +> integrity: A sha512 or sha1 Standard [Subresource Integrity](https://w3c.github.io/webappsec-subresource-integrity/) string for the artifact that was unpacked in this location. For git dependencies, this is the commit sha. + +The hash value gets used to verify that between the time it got added in your package-lock.json and the time you install the package in production (using the package-lock.json), the package has not been changed. Every change to content of the package would result in the verification hash to be different, which means the hash in your lock file would not match the current hash of the package and hence the suspicious package would not get installed + +> [!MORE] +> [npmjs.com "package-lock.json" documentation](https://docs.npmjs.com/cli/v11/configuring-npm/package-lock-json) + ### Git(Hub) URLs as Dependencies If a package/repository has not been published on npm, for example, one of your own projects on GitHub, and you want to add it as a dependency to your project, then you can do this, too. @@ -165,7 +198,7 @@ If a package/repository has not been published on npm, for example, one of your }, ``` -This will add the package located at https://github.com/USER_NAME/REPOSITORY_NAME as a dependency. +This will add the package located at `https://github.com/USER_NAME/REPOSITORY_NAME` as a dependency. If you want to pin a more specific version of the repository, you can do so by adding a hash symbol at the end, followed by the commit ID or the tag (if the repository author has published a tag) or a release version. @@ -187,7 +220,7 @@ For example, if adding a repository with the latest commit ID being **d29f7d9**, I will list a few methods here and add some links to the relevant documentation, but I will not add a complete walkthrough as those differ quite a lot depending on what platform your code is hosted on and based on what tools are in your deployment pipeline (if, however, I see that there is interest in such a tutorial in the [GitHub discussions for chris.lu](https://github.com/chrisweb/chris.lu/discussions) then I might write one in the future) -An alternative to using private GitHub repositories is to use npm to host [private packages](https://docs.npmjs.com/creating-and-publishing-private-packages/) but be aware that this is a paid service +An alternative to using private GitHub repositories is to use npm to host [private packages](https://docs.npmjs.com/creating-and-publishing-private-packages) but be aware that this is a paid service ##### using a GitHub personal access token diff --git a/app/web_development/posts/road-to-react-19-next-js-15/page.mdx b/app/web_development/posts/road-to-react-19-next-js-15/page.mdx index 0bb66fe2..09e953f4 100644 --- a/app/web_development/posts/road-to-react-19-next-js-15/page.mdx +++ b/app/web_development/posts/road-to-react-19-next-js-15/page.mdx @@ -1,8 +1,8 @@ --- title: The road to React 19 and Next.js 15 -keywords: ['Next.js', 'nextjs', 'React', 'react 19', 'Next.js 15', 'Server Components', 'react compiler', 'Server Actions'] +keywords: ['Next.js', 'nextjs', 'React', 'react 19', 'Next.js 15', 'Server Components', 'react compiler', 'Server Actions', 'server functions', 'upgrade', 'update', 'migrate', 'codemods'] published: 2024-08-12T11:22:33.444Z -modified: 2024-10-10T10:10:10.444Z +modified: 2024-12-17T03:12:12.333Z permalink: https://chris.lu/web_development/posts/road-to-react-19-next-js-15 section: Web development --- @@ -47,11 +47,11 @@ I will try to keep this document up to date as things evolve continuously. If yo React updates recap: -* On December 21, 2020, the react team announced in a [blog post on react.org](https://legacy.reactjs.org/blog/2020/12/21/data-fetching-with-react-server-components.html) that they had just published the [RFC: React Server Components](https://github.com/reactjs/rfcs/blob/main/text/0188-server-components.md) on GitHub, as well as an experimental demo [video of React Server Components (RSC) on YouTube](https://www.youtube.com/watch?v=TQQPAU21ZUw) -* In March 2022, they released a blog post announcing the release of [React v18.0](https://react.dev/blog/2022/03/29/react-v18) and in that post they also mentioned that the **Server Components** were still in development and would get released in a future update, they also posted a new guide on React.dev to help developers [migrate to React 18](https://react.dev/blog/2022/03/08/react-18-upgrade-guide), the [React 18 changelog](https://github.com/facebook/react/releases/tag/v18.0.0) lists a lot improvements, the first one is the introduction of **Concurrent React**, some new hooks mostly useful for CSS-in-JS libraries and external data stores got added to make those libraries work well during concurrent rendering. The second big feature was [Suspense](https://react.dev/reference/react/Suspense), **Suspense** is a new feature related to concurrent rendering and the one that makes other new features like **Streaming Server Rendering** and **Selective Hydration** even possible (features that would get used mainly by frameworks in upcoming releases) +* On December 21, 2020, the react team announced in a [blog post on react.org](https://reactjs.org/blog/2020/12/21/data-fetching-with-react-server-components.html) that they had just published the [RFC: React Server Components](https://github.com/reactjs/rfcs/blob/main/text/0188-server-components.md) on GitHub, as well as an experimental demo [video of React Server Components (RSC) on YouTube](https://www.youtube.com/watch?v=TQQPAU21ZUw) +* In March 2022, they released a blog post announcing the release of [React v18.0](https://reactjs.org/blog/2022/03/29/react-v18.html) and in that post they also mentioned that the **Server Components** were still in development and would get released in a future update, they also posted a new guide on React.dev to help developers [migrate to React 18](https://react.dev/blog/2022/03/08/react-18-upgrade-guide), the [React 18 changelog](https://github.com/facebook/react/releases/tag/v18.0.0) lists a lot improvements, the first one is the introduction of **Concurrent React**, some new hooks mostly useful for CSS-in-JS libraries and external data stores got added to make those libraries work well during concurrent rendering. The second big feature was [Suspense](https://react.dev/reference/react/Suspense), **Suspense** is a new feature related to concurrent rendering and the one that makes other new features like **Streaming Server Rendering** and **Selective Hydration** even possible (features that would get used mainly by frameworks in upcoming releases) * In April 2022, react released the first major update [React 18.1](https://github.com/facebook/react/releases/tag/v18.1.0), a maintenance version with many fixes. Most were for React DOM * That same year, in June 2022, the React team released [React 18.2](https://github.com/facebook/react/releases/tag/v18.2.0), another maintenance release with again lots of improvements for React DOM -* One year after the first release of React 18 in March 2023, we got another update from the React Team in a blog post [What We've Been Working On](https://react.dev/blog/2023/03/22/react-labs-what-we-have-been-working-on-march-2023), one of the new features they mentioned to be working on was Server Actions (which then first appeared in Next.js 14 in October 2023) +* One year after the first release of React 18 in March 2023, we got another update from the React Team in a blog post [What We've Been Working On](https://react.dev/blog/2023/03/22/react-labs-what-we-have-been-working-on-march-2023), well it seems like they have been working on **Server Actions** (which then first appeared in Next.js 14 in October 2023) * In May 2023, the React team announced the [Canary release channel](https://react.dev/blog/2023/05/03/react-canaries). The canary releases are officially supported, meaning that if any regressions land, they will treat them with a similar urgency to bugs in stable releases. This meant for us, that from now on, we could use canary releases in production, a lot of frameworks like Next.js, Remix, Astro, ... for example started using those canary releases, which is why frameworks had support for **Server Components** and **Server Actions** even though those features were NOT yet in a stable React release * In February 2024 the React team released another [post](https://react.dev/blog/2024/02/15/react-labs-what-we-have-been-working-on-february-2024) in their series called **What We've Been Working On** (2024 edition), one big announcement was that they were preparing the first open source release of the **React compiler** in the upcoming months, they also announced that they were working on a client only version of **Server Actions** called **Actions** * In April 2024, the React team released [React 18.3.0](https://github.com/facebook/react/releases/tag/v18.3.0), which was identical to React 18.2.0 but included new warnings for APIs that should now be considered deprecated and other changes needed for the transition to React 19.x. A day later they released [React 18.3.1](https://github.com/facebook/react/releases/tag/v18.3.1) that included a minor update to what gets exported @@ -65,20 +65,37 @@ React updates recap: * On April 25, 2024 the react team released the **first beta** 🚀 of **React 19** on [npmjs.com](https://www.npmjs.com/package/react/v/19.0.0-beta-94eed63c49-20240425), they announced the [release of React 19 (beta)](https://react.dev/blog/2024/04/25/react-19) in a blog post that same day, the post contains a recap of the most interesting features that are included in React 19, the new **client Actions** that are the counterpart to Server Actions but the client ones are just called Actions (not Client Actions), the new **use** API, **Server Components** and **Server Actions** that you might already be using as frameworks like Next.js and Remix (to just name those two) already had access to those features via the canary releases, support for document metadata and a lot more that I won't list here as I recommend you check out their [release of React 19 (beta)](https://react.dev/blog/2024/04/25/react-19) a blog post for a more complete list and details * In April 2024, the React team published the first React 19 migration guide, which lists the upcoming changes in React 19. There is also a valuable [chapter about codemods](https://react.dev/blog/2024/04/25/react-19-upgrade-guide#codemods); codemods are a great help, especially when you intend to migrate the types and already have a big codebase * During [React Conf 2024](https://react.dev/blog/2024/05/22/react-conf-2024-recap) in April 2024, the React team announced the release of the first [React 19 RC](https://react.dev/blog/2024/04/25/react-19) (release candidate), another big announcement was the experimental release of the [React Compiler](https://react.dev/learn/react-compiler) +* In oct. 2024 the React team announced the release of the [first React Compiler beta](https://react.dev/blog/2024/10/21/react-compiler-beta-release) +* After the announcement of the first RC, a React Suspense (fallback rendering) optimization (that had been in the canary version for some time) started to get "a lot of attention". If you like to know more details, then I recommend reading through this comment in [issue #29898](https://github.com/facebook/react/issues/29898#issuecomment-2477449973) as it is a very good summary of the problem and how it got addressed, here is a small quote: + +> In React 19 RC0, we stopped rendering the siblings so that the fallback could be committed immediately. (...) +> +> This change meant lazy requests initiated at render time would be forced to waterfall. (...) + +Meaning that prior to the release of the first RC for React 19, if you had several components making requests inside of one Suspense then those requests were running in parallel (asynchronously), after the change their were running one after the other (synchronously). + +* In nov. 2024 the React team announced the release of the second RC of React ([React RC1](https://www.npmjs.com/package/react/v/19.0.0-rc.1)), as they explain in the comment on GitHub regarding the fix for the **Suspense problem**, what changes is this: + +> This means we're able to get the benefits of committing the fallback immediately, without the downsides of creating waterfalls for lazy requests + +* In early december 2024 the react team announced the release of [React 19 stable](https://react.dev/blog/2024/12/05/react-19) 🎉, the release included the Suspense fix from the previous RC as well as all the features I highlighted in the previous announcements. If you want to try out React 19 you could use the [Next.js 15](https://nextjs.org/) framework, which already used some of the React 19 features (under the hood) for some time, but now you too can set React to v19 in your package.json (React 19 with Next.js 15 works in both the pages and the app router). Or if you prefer you could use new [vite v6](https://github.com/vitejs/vite) to quickly spin up a dev server and try out the new React 19 client features like actions + +I really liked that the React maintainers paused their release to address the concerns around Suspense and come up with a good solution. I can wait to experiment more with React 19 actions in combination with the 2 new hooks useActionState and useOptimistic to see for myself if I want to start using them in "real" projects > [!MORE] > [react.dev "React 19 (beta)" post](https://react.dev/blog/2024/04/25/react-19) > [react.dev "React 19 migration" guide](https://react.dev/blog/2024/04/25/react-19-upgrade-guide) > [react.dev "React 19 RC" post](https://react.dev/blog/2024/04/25/react-19) +> [react.dev "React 19 stable" post](https://react.dev/blog/2024/12/05/react-19) > [react.dev "React Compiler" documentation](https://react.dev/learn/react-compiler) ## New features in Next.js before v13 -Since version v10.2.1 Next.js enabled [Incremental type checking](https://www.typescriptlang.org/tsconfig/#incremental), making builds much faster and then in version 12. Next.js typescript compilation got another speed boost, because Next.js started using ["SWC"](https://swc.rs/) a compiler written in Rust. For a bit of history and more in-depth information, check out their [Next.js "compiler using SWC" documentation](https://nextjs.org/docs/architecture/nextjs-compiler). Also in [version 12 Next.js](https://nextjs.org/blog/next-12#react-server-components) they started experimenting with React Server Components (alpha version). This version of Next.js did NOT include the app directory yet. +Since version v10.2.1 Next.js enabled [Incremental type checking](https://www.typescriptlang.org/tsconfig#incremental), making builds much faster and then in version 12. Next.js typescript compilation got another speed boost, because Next.js started using ["SWC"](https://swc.rs/) a compiler written in Rust. For a bit of history and more in-depth information, check out their [Next.js "SWC compiler" documentation](https://nextjs.org/docs/advanced-features/compiler). Also in [version 12 Next.js](https://nextjs.org/blog/next-12#react-server-components) they started experimenting with React Server Components (alpha version). This version of Next.js did NOT include the app directory yet. > [!MORE] > [Next.js "TypeScript" documentation](https://nextjs.org/docs/app/building-your-application/configuring/typescript) -> [Next.js "compiler using SWC" documentation](https://nextjs.org/docs/architecture/nextjs-compiler) +> [Next.js "SWC compiler" documentation](https://nextjs.org/docs/advanced-features/compiler) ## Next.js v13 @@ -86,7 +103,7 @@ In May 2022, the next.js team released the [Layouts RFC](https://nextjs.org/blog Next.js 13 releases: -* In october 2022, the next.js team released [Next.js 13](https://nextjs.org/blog/next-13) which included the first beta version of the new `app` directory (**App Router**), with the new app router and thanks to React 18 Next.js another new feature they added were the **React Server Components** (beta version), in this release we also got the first alpha of [Turbopack](https://turbo.build/pack/docs) a new bundler written in Rust that someday should replace webpack in Next.js projects, next/image got a complete overhaul in that version and the previous version got renamed to [next/legacy/image](https://nextjs.org/docs/pages/api-reference/components/image-legacy) (if you migrate your code to a newer Next.js version but don't want to migrate your code related to images, then you can use the legacy image component but for new projects (using Next.js 13.x) it is highly recommended that you use the new version of next/image). Another new component they introduced in that version was next/font (in beta), a component which will optimize your font usage by making them self hosted (compared to fetching them from a remote CDN) which reduces layout shifts when using fonts that are not installed on the users device. Yet another new component they updated is next/link which now does NOT require you to add `{:html}` element as child anymore +* In october 2022, the next.js team released [Next.js 13](https://nextjs.org/blog/next-13) which included the first beta version of the new `app` directory (**App Router**), with the new app router and thanks to React 18 Next.js another new feature they added were the **React Server Components** (beta version), in this release we also got the first alpha of [Turbopack](https://turbo.build/pack) a new bundler written in Rust that someday should replace webpack in Next.js projects, next/image got a complete overhaul in that version and the previous version got renamed to [next/legacy/image](https://nextjs.org/docs/pages/api-reference/components/image-legacy) (if you migrate your code to a newer Next.js version but don't want to migrate your code related to images, then you can use the legacy image component but for new projects (using Next.js 13.x) it is highly recommended that you use the new version of next/image). Another new component they introduced in that version was next/font (in beta), a component which will optimize your font usage by making them self hosted (compared to fetching them from a remote CDN) which reduces layout shifts when using fonts that are not installed on the users device. Yet another new component they updated is next/link which now does NOT require you to add `{:html}` element as child anymore * Two months later, in December 2022, [Next.js 13.1](https://nextjs.org/blog/next-13-1) got released, it contained a lot of improvements for the new app directory but also for middlewares, Turbopack as well as [SWC](https://swc.rs/) a tool written in Rust to replace babel * In february 2023, we got [Next.js 13.2](https://nextjs.org/blog/next-13-2) which introduced the new [Metadata Files API](https://nextjs.org/docs/app/api-reference/file-conventions/metadata) for built SEO support and [MDX](https://mdxjs.com/) for Server Components ([MDX app directory support](https://nextjs.org/docs/app/building-your-application/configuring/mdx)) * In April 2023, we got [Next.js 13.3](https://nextjs.org/blog/next-13-3), which brought improvements for SEO tools like the [Metadata Files API](https://nextjs.org/docs/app/api-reference/file-conventions/metadata) and automatically generated [OpenGraph Images](https://nextjs.org/docs/app/api-reference/file-conventions/metadata/opengraph-image), as well as a new routes feature that looks very promising called [Parallel Routes](https://nextjs.org/docs/app/building-your-application/routing/parallel-routes) which allows you to render the content of two (or more) routes in a single layout, which is probably going to be very helpful when building things like dashboards or complex admin panels @@ -100,13 +117,34 @@ Next.js 13 releases: * In April 2024, the Next.js team released [Next.js 14.2](https://nextjs.org/blog/next-14-2) and added a new experimental option to called [staleTimes](https://nextjs.org/docs/app/api-reference/next-config-js/staleTimes) to let developers set custom revalidation times for the [Router Cache](https://nextjs.org/docs/app/building-your-application/caching#router-cache), this version also came with some changes intended to improve Tree-shaking and builds should now use less memory > [!MORE] -> [Next.js 13 to 14 upgrade guide](https://nextjs.org/docs/app/building-your-application/upgrading/version-14) +> [Next.js 13 to 14 upgrade guide](https://rc.nextjs.org/docs/app/building-your-application/upgrading/version-14) > [Next.js "partial prerendering" documentation](https://nextjs.org/docs/app/api-reference/next-config-js/partial-prerendering) > [Turbopack "are we turbo yet" (percentage of tests Turbopack passes)](https://areweturboyet.com/) ## Next.js 15 * In may 2024, the Next.js team released the first [Next.js 15 RC](https://nextjs.org/blog/next-15-rc) (release candidate) which included a big change for [caching](https://nextjs.org/blog/next-15-rc#caching-updates), the Next.js team decided that from now on all caching would be opt-in instead of being enabled by default. The problem with caching being enabled by default was that in development even though caching was enabled the content (fetch requests, routes, client navigation) would not get cached, caching would only happen during the build process, this made it hard to detect problems linked to caching before publishing the app. Another change is that Next.js now includes React 19 RC (see my [React 19 chapter](#react-19) for details about what's new in React 19), **Partial Prerendering (PPR)** which previously was only available when using the release candidates is now included in this release and there is a new **experimental_ppr** feature that allows you to enable PPR only on certain pages. Also included is a new feature called **next/after** (which is an experimental API to execute code after your content got streamed to the user), a new layout for **create-next-app** as well as new configuration options to opt out of bundling for certain packages +* On Oct. 2024 the Next.js team released a new [blog post about the second RC for Next.js 15](https://nextjs.org/blog/next-15-rc2) +* Still oct. 2024 (a few days later) Vercel announced the first stable version of [Next.js 15 on their blog](https://nextjs.org/blog/next-15), it included the features already mentioned for the first RC, but they also added support for **ESLint 9** (no flat config support yet) as well as the possibility to use Typescript for "next.config**.ts**" configuration files, and on the same day they announced (in a separate post) that [Turbopack Dev is Now Stable](https://nextjs.org/blog/turbopack-for-development-stable) +* In Dec. 2024 the Next.js team announced the release of [Next.js 15.1](https://nextjs.org/blog/next-15-1), which added **forbidden / unauthorized** 2 new **experimental** functions, which you can integrate into your authentication workflow to show a page similar to the notFound (404) page but for authentication errors ([forbidden](https://nextjs.org/docs/app/api-reference/functions/forbidden) (403) / [unauthorized](https://nextjs.org/docs/app/api-reference/functions/unauthorized) (401)), they improved the display of error messages and stack traces, **after** a feature (they first released "after" as experimental feature RC of Next.js 15, back in May 2024) is now stable + +Since the release of the first stable version the Next.js team has released several updates (they also ported fixes back and released new Next.js 14 versions), to get an overview I recommend checking out their [releases page](https://github.com/vercel/next.js/releases) + +Next.js also continues to get canary channel updates on a regular basis (if you want to get an overview of what channels are available and what the latest versions for each of them is, then I recommend checking out the [Next.js versions page on npmjs](https://www.npmjs.com/package/next?activeTab=versions)) + +> [!TIP] +> When upgrading to Next.js 15 and React 19 it is recommended to first read the documentation, but it is also important that you have a look at the [Next.js codemods](https://nextjs.org/docs/canary/app/building-your-application/upgrading/codemods) as those could potentially save you a lot of time +> +> To get started I recommend you create a new branch (or at least make sure you have no uncommitted changes) and then use the following command to let the Next.js update your dependencies for you: +> +> ```shell +> npx @next/codemod@canary upgrade latest +> ``` + +> [!MORE] +> [Next.js 15 "upgrading" documentation](https://nextjs.org/docs/app/building-your-application/upgrading/version-15) +> [Next.js codemods](https://nextjs.org/docs/canary/app/building-your-application/upgrading/codemods) +> [Next.js 15 and React 19 upgrade guide (with optional types tips)](https://nextjs.org/docs/app/building-your-application/upgrading/version-15#react-19) ## do I need to migrate all my code today? @@ -119,7 +157,7 @@ The app directory and server-side React are features you can opt into when ready This means you can still start a new project today and use the pages directory (but I don't recommend it as I think projects can be much more powerful if they use the app router). If you have an existing codebase, you don't need to migrate everything at once from pages to the app directory. You can do it bit by bit. If, however, you start a new project from scratch, then I recommend that you consider using the app directory as well as Server Components and, eventually, Server Actions because those technologies are more future-proof and also include a lot of great features the pages router or React client side only projects do not have -If you are already using a previous version of Next.js and are migrating to one of the latest versions then I highly recommend you first check out the [Next.js "codemods" documentation](https://nextjs.org/docs/pages/building-your-application/upgrading/codemods) as these might save you a lot of time, another good read is the [Next.js app router migration guide](https://nextjs.org/docs/app/building-your-application/upgrading/app-router-migration) +If you are already using a previous version of Next.js and are migrating to one of the latest versions then I highly recommend you first check out the [Next.js "codemods" documentation](https://nextjs.org/docs/app/building-your-application/upgrading/codemods) as these might save you a lot of time, another good read is the [Next.js app router migration guide](https://nextjs.org/docs/app/building-your-application/upgrading/app-router-migration) > [!MORE] > [Next.js "App Router Incremental Adoption" documentation](https://nextjs.org/docs/app/building-your-application/upgrading/app-router-migration) diff --git a/app/web_development/posts/sentry-io/page.mdx b/app/web_development/posts/sentry-io/page.mdx index 37258411..69b3bbaa 100644 --- a/app/web_development/posts/sentry-io/page.mdx +++ b/app/web_development/posts/sentry-io/page.mdx @@ -41,7 +41,7 @@ export const metadata = { Sentry.io is a cloud-based error monitoring service that helps developers capture and log errors (including stack traces and request information) in real-time (when they occur in production). They support a wide range of languages and frameworks through their ["sentry-*" opensource SDKs](https://github.com/getsentry) that are hosted on GitHub (the error logging SDKs for javascript, React, Capacitor, Next.js, and many more have an MIT license, but not all sentry packages do, for example, the [self-hosted version](https://develop.sentry.dev/self-hosted/) of sentry uses a license called [FSL](https://github.com/getsentry/fsl.software/tree/main) that is NOT an OSI-approved license). -Beyond error tracking, Sentry also offers [performance monitoring](https://docs.sentry.io/product/performance/) features as well as a feature called [Sessions Replays](https://sentry.io/for/session-replay/), which lets you capture and then replay the user interactions that happened before the error occurs, making it easier to reproduce an error and more. +Beyond error tracking, Sentry also offers [performance monitoring](https://docs.sentry.io/product/performance) features as well as a feature called [Sessions Replays](https://sentry.io/for/session-replay/), which lets you capture and then replay the user interactions that happened before the error occurs, making it easier to reproduce an error and more. > [!WARN] > Know that Sentry has open source SDKs but the SaaS service is a paid service. Sentry.io has a (free) [Developer](https://sentry.io/pricing/) plan for developers who want to start a side project or just experiment with the service. The plan includes logs for up to 5000 errors, 10,000 performance metrics, and more. To learn more about the quotas for the free plan or check out the pricing of other plans, I recommend checking out their [pricing page](https://sentry.io/pricing/). @@ -50,6 +50,13 @@ Beyond error tracking, Sentry also offers [performance monitoring](https://docs. > [Sentry.io documentation](https://docs.sentry.io/) > [Sentry.io pricing](https://sentry.io/pricing/) +## Next.js 14.x and 15.x tutorials + +I have two separate tutorials about Next.js that each have a page dedicated to setting up and configuring Sentry for Next.js by using the Sentry wizard: + +* [Error pages and logging using Sentry Next.js 15.x](/web_development/tutorials/next-js-static-first-mdx-starterkit/error-handling-and-logging) +* [Error pages and logging using Sentry Next.js 14.x](/web_development/tutorials/next-js-static-mdx-blog/error-handling-and-logging) + ## Create an account (sign up) * Go to [sentry.io](https://sentry.io) and then click on **Get Started** @@ -90,231 +97,9 @@ Then you can **Name your project and assign a team to it**. I again kept the def Finally, click on **Create Project** -Now click on **Configure SDK** - -## Sentry.io SDK for Next.js installation - -You can use the Sentry Wizard or follow their manual [setup toturial](https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/). I chose to use the Wizard as it guides you through setting up Sentry. I will also install some test files to ensure everything is working. If you chose manual, you may want to copy the **DSN** they give you at the end of the page (a Sentry DSN is an API key you will use when setting up your SDK) - -> [!TIP] -> Before using the wizard, I recommend committing your latest changes (if you haven't already) and doing a last sync before launching the sentry wizard, this way you will be able to see what stuff Sentry.io will add to your project, and you will see what got changed in existing files like the next.config.mjs file - -In your VSCode terminal (or your preferred command line tool), type the following command: - -```shell -npx @sentry/wizard@latest -i nextjs -``` - -Or, if you prefer to use the wizard without sending telemetry data to sentry.io (usage statics and crash reports), then add the `--disable-telemetry` option to the command, like so: - -```shell -npx @sentry/wizard@latest -i nextjs --disable-telemetry -``` - -First, you will get asked if you accept installing the sentry wizard npm package. Press `y` to accept and press `Enter` to move on - -After the installation, the wizard will automatically get launched, and it will start asking you questions about your setup preferences: - -**Are you using Sentry SaaS or self-hosted Sentry?** You probably want to choose **Sentry SaaS (sentry.io)** like I did (but if you are a company and need a custom solution, then you might want to look at the hosted version), then press `Enter`. - -**Do you already have an account?** chose **Yes** (if you did follow the previous chapter or already had an account before), then press `Enter` (or choose **No** if you have no account yet and follow the [account creation](#create-an-account-sign-up) process) - -Then, the sentry wizard will ask you to log in, which will open the sentry login page in your default browser. Log into your account, then go back to your terminal. - -**Select your Sentry project**, choose your Sentry Project from the list (when using the wizard, Sentry will have automatically created a project for the SDK you chose earlier for you; if, however, you don't see a Project listed here, you can check out the [Create a Sentry.io project](#create-a-sentryio-project) chapter to create a project first) - -Now, Sentry will install the latest Sentry SDK for Next.js. - -**Do you want to route Sentry requests in the browser through your NextJS server to avoid ad blockers?** Sentry wants to know if it should route its requests through your Next.js server. By doing so, Sentry attempts to bypass the block lists of adblocker addons that are installed in some browsers. This means Sentry will first send the client-side requests to a URL on your server, and then your server will forward the request to the Sentry API. I personally chose **Yes** as I want to increase the chance of getting bug reports but feel free to answer **No** (if for example, you don't want to have the extra traffic, on your server backend, that this redirect will cause) - -**Do you want to enable React component annotations to make breadcrumbs and session replays more readable?** Next Sentry is asking if we want to use the feature called [React component annotations](https://docs.sentry.io/platforms/javascript/guides/react/features/component-names/) which attempts to use component names in reports instead of more cryptic selectors, I think this is a nice feature, so I selected **Yes**, if you already use Sentry.io and don't want to change how bug reports work, then leave it on **No**, you can always turn it on/off via the configuration later if you want - -> [!WARN] -> I turned **React component annotations** on and then noticed that my [react-three-fiber](https://github.com/pmndrs/react-three-fiber) animation had stopped working, this is because **React component annotations** adds data attrobites to components which React Three Fiber does not like, and which then creates bugs which print the following in your console: -> -> > TypeError: Cannot read properties of undefined (reading 'sentry') -> -> So if you plan on using **React Three Fiber** then you should answer to this question with **NO**, to learn more about this problem and how to disable **React component annotations** manually in the configuration have a look at my warning box in the [Sentry.io for Next.js configuration chapter](#sentryio-for-nextjs-configuration) - -**Do you want to create an example page** chose **YES** (we will later use it to test the Sentry setup, and then we will delete it) - -**Are you using a CI/CD tool to build and deploy your application?** chose **YES** (if you are using Vercel, GitHub actions, or any other CI/CD deployment tool); If you do NOT use one, choose **NO**) - -The Sentry.io Wizard will give you a **SENTRY_AUTH_TOKEN** string if you choose yes. If you use a CI/CD for your deployments, copy the token, and save it in a secure location, you will need this token later if, for example, you set up a custom GitHub action. You will want to add that token environment variable to your GitHub secrets. If you use another CI/CD service, check out their documentation to learn how to use that token to upload source maps to Sentry automatically. If using Vercel, you can use the [Sentry integration for Vercel](https://vercel.com/integrations/sentry), which will set the Vercel environment variables for you, or if you prefer you can add the token manually to your environment variables using the [Vercel environment variables interface](https://vercel.com/docs/projects/environment-variables). - -CI/CD tools can authenticate themselves to Sentry.io using the **SENTRY_AUTH_TOKEN** environment variable and then use the Sentry.io API to automatically upload the source maps of your build to Sentry.io. Later, if there is a bug report on Sentry.io, it will be able to use the source maps instead of the minified build files to show you where the error occurred. - -**Did you configure CI as shown above?** chose **YES** - -That was the last question: - -> Successfully installed the Sentry Next.js SDK! - -After answering all questions, the Sentry SDK will edit your next.config.mjs to add the **withSentryConfig** Sentry configuration, and it will have added several sentry.*.config files (to the root of your project) that contain environment-specific configurations, it will also create some other files depending on what answers you gave, like a page.tsx that we can now use to test the setup - -> [!NOTE] -> If you use Vercel for your deployments, then you don't need to set the **SENTRY_AUTH_TOKEN** yourself; you can use the [Sentry integration for Vercel](https://vercel.com/integrations/sentry), which will set the Vercel environment variables for you, I recommend you do that now, as all you need to do is click on the **Add integration** button, and then continue with the tutorial - -### Sentry.io Next.js example page - -Next, as suggested by the wizard at the end of the installation process, it is recommended to start the development server using the `npm run dev` command, and then we visit the Sentry example page in our browser at `http://localhost:3000/sentry-example-page` - -On that page, hit the **Throw Error!** button and then click on the link just below to **visit your Sentry projects issues page** - -Now wait for the backend and frontend errors to appear (this can take a few minutes, which is a good time to refresh your cup of coffee ☕ (or whatever beverage you prefer)) - -As soon as the two errors appear, feel free to click on them and have a look at what error logging on Sentry.io looks like - -Before we commit/sync all the changes Sentry.io did in our project, **delete** the `app\sentry-example-page\` folder, including the `page.jsx` **example page**, and then also delete the `app\api\sentry-example-api\` folder including the `route.js` **API example route** file that Sentry.io created to test the error logging, you will not need them anymore. - -## Sentry.io for Next.js configuration - -Sentry.io can be customized a lot and has several places to change the default configuration. - -Sentry wizard has edited our `next.config.mjs`, and it should now look like this: - -```js title="next.config.mjs" -import { withSentryConfig } from '@sentry/nextjs'; -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 PR #67824 lands in a stable release - // https://github.com/vercel/next.js/pull/67824 - typedRoutes: phase === PHASE_DEVELOPMENT_SERVER ? true : false, - }, - } - - return nextConfigOptions - -} - -export default withSentryConfig(nextConfig, { - // For all available options, see: - // https://github.com/getsentry/sentry-webpack-plugin#options - - org: "YOUR_ORG_NAME", - project: "YOUR_PROJECT_NAME", - - // Only print logs for uploading source maps in CI - silent: !process.env.CI, - - // For all available options, see: - // https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/ - - // Upload a larger set of source maps for prettier stack traces (increases build time) - widenClientFileUpload: true, - - // Automatically annotate React components to show their full name in breadcrumbs and session replay - reactComponentAnnotation: { - enabled: true, - }, - - // Route browser requests to Sentry through a Next.js rewrite to circumvent ad-blockers. - // This can increase your server load as well as your hosting bill. - // Note: Check that the configured route will not match with your Next.js middleware, otherwise reporting of client- - // side errors will fail. - tunnelRoute: "/monitoring", - - // Hides source maps from generated client bundles - hideSourceMaps: true, - - // Automatically tree-shake Sentry logger statements to reduce bundle size - disableLogger: true, - - // Enables automatic instrumentation of Vercel Cron Monitors. (Does not yet work with App Router route handlers.) - // See the following for more information: - // https://docs.sentry.io/product/crons/ - // https://vercel.com/docs/cron-jobs - automaticVercelMonitors: true, -}); -``` - -> [!NOTE] -> By default, the options that the wizard set for us are good enough. As soon as you have the time, I recommend checking out the [Extend your Next.js Configuration](https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/#extend-your-nextjs-configuration) Sentry.io documentation, which explains what each option does - -> [!WARN] -> To enable **reactComponentAnnotation** is usually a good idea as it makes reports more readable by using component names instead of long selectors, but to make this feature happen Sentry needs to add a data attributes to components, this does usually not pose a problem except for [react-three-fiber](https://github.com/pmndrs/react-three-fiber) which does not like those extra attributes at all, which means that **React component annotations** are great unless you use **React Three Fiber** -> -> For now if you use **React three fiber** the only workaround is to turn the Sentry **React component annotations** option off, by setting the `reactComponentAnnotation` variable to false -> -> It is only after I had opened an [Issue #13413](https://github.com/getsentry/sentry-javascript/issues/13413) in the **sentry-javascript** repository that I found the [Issue #530](https://github.com/getsentry/sentry-javascript-bundler-plugins/issues/530) in the **sentry-javascript-bundler-plugins** repository, which has a comment by one of the Sentry SDK mainteners, they mentioned that they consider adding more options in the future to let you exclude components, however as of now those options are not available yet, so for now we can NOT enable **React component annotations** and exclude **React three fiber**, we MUST completly disable the feature - -Another file that got added to the root of our project is `sentry.client.config.ts`, which is used to configure Sentry.io for **client components**. Check out the [Next.js SDK Configuration Options](https://docs.sentry.io/platforms/javascript/guides/nextjs/configuration/options/) documentation for more details about each option. I slightly modified my configuration file to be like this: - -```ts title="sentry.client.config.ts" showLineNumbers {3-12} {18} {23} {27} {38} -import * as Sentry from '@sentry/nextjs' - -let replaysOnErrorSampleRate = 0 -let tracesSampleRate = 0.1 - -if (process.env.NODE_ENV === 'production') { - replaysOnErrorSampleRate = 1 -} - -if (process.env.NODE_ENV === 'development') { - tracesSampleRate = 0 -} - -Sentry.init({ - dsn: 'YOUR_SENTRY_DSN_URL', - - // Adjust this value in production, or use tracesSampler for greater control - tracesSampleRate: tracesSampleRate, - - // Setting this option to true will print useful information to the console while setting up Sentry. - debug: false, - - replaysOnErrorSampleRate: replaysOnErrorSampleRate, - - // This sets the sample rate to be 10%. You may want this to be 100% while - // in development and sample at a lower rate in production - replaysSessionSampleRate: 0, - - // You can remove this option if you're not planning to use the Sentry Session Replay feature: - integrations: [ - Sentry.replayIntegration({ - // Additional Replay configuration goes in here, for example: - maskAllText: true, - blockAllMedia: true, - }), - ], - - environment: process.env.NODE_ENV ? process.env.NODE_ENV : '', -}) -``` - -Lines 3-12: I added two variables for the **replaysOnErrorSampleRate** and the **tracesSampleRate**, replaysOnErrorSampleRate I set to zero by default to not produce replays when not in production, then line 6-8 I added a check to verify if the current environment is **production** and if it is I tell Sentry to always make a replay if there is an error (be careful with this option, in the free plan you only have 50 replays per month, which is why I only turn it on in production, also in development it is usually the developer themself that triggers the error so there is not really a need for a replay); the tracesSampleRate I set it to 0.1, meaning 10% of the traces will get sent to Sentry but then line 10-12 I disable them in development (this is to ensure no performance metrics are getting calculated when the app is running on a local computer, to check local performance it is preferred to use the developer tools), you may want a lower or higher value depending on what plan you are on and then check if you reach your limits or not and then adjust over time - -Line 27: I completely disabled **replaysSessionSampleRate**, meaning there will be no replays being made when there is no error; the free plan has 50 replays per month, and I prefer to keep all the replays for cases where there is a bug - -Line 38: I pass the environment to Sentry, meaning Sentry will know if the environment is preview or production (that value is based on the Vercel environment on which Next.js got deployed) - -> [!WARN] -> If you copy paste the `sentry.client.config.ts` above into your project make sure you update the **YOUR_SENTRY_DSN_URL** placeholder with your own Sentry DSN - -`sentry.edge.config.ts` are the options for when Next.js uses the [Vercel Edge Network](https://vercel.com/docs/edge-network/overview). I kept that file, but feel free to adjust any values to fit your use case. - -`sentry.server.config.ts` is again similar to the previous two, just this one is specifically for Next.js server-side options. I also kept this file as is - -There is, however, one option in the server configuration that is commented out (by default) that you might want to consider. The option lets you use the [Spotlight js](https://spotlightjs.com/) package by Sentry. If you're going to use it, I recommend checking out the documentation to [install and setup Spotlight in a Next.js project](https://spotlightjs.com/setup/nextjs/) - -> [!MORE] -> [Sentry.io "Extend your Next.js Configuration" documentation](https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/#extend-your-nextjs-configuration) -> [Sentry.io "Next.js SDK Configuration" documentation](https://docs.sentry.io/platforms/javascript/guides/nextjs/configuration/options/) -> [Sentry.io "Configure Tunneling to avoid Ad-Blockers" documentation](https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/#configure-tunneling-to-avoid-ad-blockers) -> [Spotlight js "Using Spotlight with Next.js" documentation](https://spotlightjs.com/setup/nextjs/) - ## Allowed domains filter -By default Sentry.io will accept reports from whatever domain they originate as long as the DSN is yours, you can however explicitly whitelist **domains** that are **allowed** to send in reports, in which case Sentry will check the [Origin](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Origin) and [Referer](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referer) headers and exclude reports from domains that you did not whitelist +By default Sentry.io will accept reports from whatever domain they originate as long as the DSN is yours, you can however explicitly add **domains** that are **allowed** to send in reports, in which case Sentry will check the [Origin](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Origin) and [Referer](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referer) headers and exclude reports from domains that you did not add to your allowlist To specific domains instead of all, use the following steps: @@ -327,7 +112,7 @@ To specific domains instead of all, use the following steps: * in that field enter all the domains you will use for your project > [!TIP] -> The field for allowed domains is a textarea, meaning you don't add a comma seperated list but instead you add one domain per row, let's assume you want to whitelist your `example.com` domain as well as two subdomains `foo.example.com` and `bar.excample.com`, then this would be what your whitelist looks like: +> The field for allowed domains is a textarea, meaning you don't add a comma separated list but instead you add one domain per row, let's assume you want to add the `example.com` domain to your allowlist as well as two subdomains `foo.example.com` and `bar.example.com`, then this would be what your allowlist looks like: > > ```txt > example.com @@ -371,9 +156,9 @@ If you had Sentry v7 already installed in your project and now want to upgrade t If you have a feeling that something is wrong, you either get 403 responses from Sentry (or your tunnel URL), or you trigger an error but it won't show up, I recommend starting with the following steps: -* if you are using the sentry domain whitelist feature, meaning you don't just have an asterisk (`*`) set as default value, then you might want to have a look at the [Allowed domains filter](#allowed-domains-filter) chapter and make sure the domain making the request is on the whitelist, maybe you are using a regex and the submain you are on is not covered by the regex +* if you are using the sentry domain allowlist feature, meaning you don't just have an asterisk (`*`) set as default value, then you might want to have a look at the [Allowed domains filter](#allowed-domains-filter) chapter and make sure the domain making the request is in the allowlist, maybe you are using a regex and the subdomain you are on is not covered by the regex * If you are on localhost, then maybe localhost request are blocked, in this case have a look at the [Disable the "reports from localhost" filter](#disable--enable-reports-from-localhost-filter) chapter, and make sure that localhost is set to **enabled** -* Have a look at your [inbound filters](https://docs.sentry.io/concepts/data-management/filtering/), mayby one of those is excluding the Issues +* Have a look at your [inbound filters](https://docs.sentry.io/concepts/data-management/filtering/), maybe one of those is excluding the Issues * Make sure the DSN you are using is correct, make sure the DSN (Sentry client key) that your project is using is correct, eventually it is wrong key from another project, or someone has disabled or even deleted a key that is still in use * Have a look at the following chapter [Using the Sentry.io API to debug issues](#using-the-sentryio-api-to-debug-issues) which will show you how to make quick requests to the Sentry.io API to help you narrow down the cause of the missing Issues @@ -412,4 +197,37 @@ This will hopefully help you understand if your requests got filtered, were inva > [Sentry.io "Sentry API organization events" documentation](https://docs.sentry.io/api/organizations/retrieve-event-counts-for-an-organization-v2/) > [Sentry.io "troubleshooting" documentation](https://docs.sentry.io/platforms/javascript/guides/nextjs/troubleshooting/) +## Sentry React Component Annotation(s) can be problematic + +To enable Sentry **reactComponentAnnotation** configuration option is usually a good idea as it makes reports more readable by using component names instead of long selectors + +> [!MORE] +> [Sentry "React Component Names" option](https://docs.sentry.io/platforms/javascript/guides/react/features/component-names/) + +### Issues with Sentry Component Annotations + +To make this feature happen Sentry needs to add a data attributes to components, this does usually not pose a problem except sometimes the Sentry Annotations on third party components will cause an error in those third party tools, like [react-three-fiber](https://github.com/pmndrs/react-three-fiber) which do NOT like those extra attributes at all + +This means that **React component annotations** are great unless you use a package like **React Three Fiber** or setup your project using **Vite**, then you need to disable the feature + +#### React three fiber (R3F) issue + +For now if you use **React three fiber (R3F)** the only workaround is to turn the Sentry **React component annotations** option off, by setting the `reactComponentAnnotation` variable to false + +It is only after I had opened an [Issue #13413](https://github.com/getsentry/sentry-javascript/issues/13413) in the **sentry-javascript** repository that I found the [Issue #530 (Cannot read properties of undefined (reading 'sentry') when using reactComponentAnnotation)](https://github.com/getsentry/sentry-javascript-bundler-plugins/issues/530) in the **sentry-javascript-bundler-plugins** repository, which has a comment by one of the Sentry SDK maintainers, they mentioned that they consider adding more options in the future to let you exclude components + +However as of now those options are not available yet (we can NOT enable React component annotations and exclude React three fiber), meaning the only option left is to disable the reactComponentAnnotation feature (if you chose to continue using R3F) + +> [!MORE] +> [Sentry "Issue #530" (open as of dec. 2024)](https://github.com/getsentry/sentry-javascript-bundler-plugins/issues/530) + +#### Vite issue + +There is a similar issue when using Vite and the @sentry/vite-plugin as described in the [Issue #492](https://github.com/getsentry/sentry-javascript-bundler-plugins/issues/492) which (as of dec. 2024) is also still open + +Same as R3F, the only solution here is to NOT enable the annotations feature until the problem is fixed + +> [!MORE] +> [Sentry "Issue #492" (open as of dec. 2024)](https://github.com/getsentry/sentry-javascript-bundler-plugins/issues/530) + diff --git a/app/web_development/posts/vercel/page.mdx b/app/web_development/posts/vercel/page.mdx index d43b3b78..9ea69974 100644 --- a/app/web_development/posts/vercel/page.mdx +++ b/app/web_development/posts/vercel/page.mdx @@ -2,7 +2,7 @@ title: Vercel keywords: ['Vercel', 'ci/cd', 'deployment', 'hosting', 'SaaS'] published: 2024-08-06T11:22:33.444Z -modified: 2024-09-17T11:22:33.444Z +modified: 2024-08-06T11:22:33.444Z permalink: https://chris.lu/web_development/posts/vercel section: Web development --- @@ -51,11 +51,8 @@ Vercel has a (free) [Hobby](https://vercel.com/pricing) plan for developers who * visit [vercel.com](https://vercel.com/) * click on **Sign Up** in the top right -* select the [Hobby](https://vercel.com/pricing) plan -* then a field pops up, asking you for your name. Enter a name (you can use your full name, or as I did, use your GitHub username or any display name you like). If you already or later plan to work in a team, then I recommend you choose a name they will recognize) - -### Select a Git provider to import an existing project from a Git Repository - +* Select the [Hobby](https://vercel.com/pricing) plan +* then a field pops up asking you for your name. Enter a name (you can use your full name, or as I did, use your GitHub username or any display name you like). If you already or later plan to work in a team, then I recommend you choose a name they will recognize) * next, they ask you to connect your Git provider (or use your email address by clicking on "Continue with Email") to create a new account; I intended to connect to a GitHub account, so I clicked on the GitHub button (or click on [GitLab](https://about.gitlab.com/) or [Bitbucket](https://bitbucket.org/) if that is the git provider hosting your code), I usually try not to connect too many accounts which each other as a breach into one of them could potentially give the attacker access to the services that are linked to it too, but in this case, I chose to because in the next step, we need to connect our GitHub account anyway and allow Vercel to access repositories * for a guide about creating a GitHub account, check out my chapter [Create a GitHub account (sign up)](/web_development/posts/github#create-an-account-sign-up) in the GitHub post * next, a pop-up window will open, asking you to authorize Vercel; there is one request they do, which is called [Act on your behalf](https://docs.github.com/en/apps/using-github-apps/authorizing-github-apps#about-github-apps-acting-on-your-behalf), this means that Vercel will be able to do tasks on your repositories, this is needed so that Vercel can fetch your code after you made a new commit or pull request and deploy the code on their servers (you will later get asked to give access to one or more repositories, meaning you can define which repositories Vercel can access and which not) @@ -68,82 +65,42 @@ Vercel has a (free) [Hobby](https://vercel.com/pricing) plan for developers who > [vercel.com "Create an Account" documentation](https://vercel.com/docs/accounts/create-an-account) > [GitHub "About GitHub Apps acting on your behalf" documentation](https://docs.github.com/en/apps/using-github-apps/authorizing-github-apps#about-github-apps-acting-on-your-behalf) -## Add a new project - -If you are NOT on the **new project** page yet, in the top left navigation, click on **Overview** - -On the overview page, on the top right, click on **Add New...** and select **Project** - -### Manage git repository permission to allow Vercel to access it - -The first step when creating a new Vercel project is **Import Git Repository**, but only repositories for which you have granted Vercel access will be listed - -If you created a Vercel account using an email address and do NOT have a git provider app connected yet, then this is the first thing you need to do is follow the steps in the previous chapter to [select a git provider (to import an existing repository)](#select-a-git-provider-to-import-an-existing-project-from-a-git-repository) - -If you have already connected to a git provider in the past (I will use GitHub as a provider in this example, you might have connected using another provider like Bitbucket or GitLab), then that provider will be selected by default, click on the down arrow if you want to add or switch to another provider - -Now that the git provider is selected, you will see a list of repositories - -If your repository is in the list, then you are done for this step and can jump to the next chapter to [import a git repository](#import-git-repository) +## Add a new project (repository) -If your repository is NOT in the list of repositories because you have not granted Vercel permissions to access it, then you have 2 options: +If you are not yet on the **new project** page, in the top left navigation, click on **Overview** and then below on the right, click on **Add New...** and select **Project** -> [!WARN] -> I had trouble using **option 1** recently, every time I clicked on the **Adjust App Permissions** Link, I would get a red modal box telling me that: -> -> > The popup to install the GitHub App could not be opened. -> -> The workaround here is to use **option 2** +### Install GitHub on Vercel -**Option 1**: +On this page, you will have a Box with the title **Import Git Repository**. If you have already installed GitHub in the past, then you will see a list of repositories, and in that case, you can jump to the next chapter [Import a Repository](#import-a-repository) (if you already installed GitHub but the repository is missing in the list then click on the **Adjust GitHub App Permissions** link to add access to that repository) -First, click on the **Adjust GitHub App Permissions** link to add access permissions for that repository +Because we have already linked our GitHub account when signing up, we also have a **GitHub** button. Click on the button to start the process. -Next, click on the git provider button (in my case, the **GitHub** button) to start the process +Next, Vercel will open a modal asking you to give access to one or more repositories. I recommend only providing access to the repository where you store the code you want to deploy. To do so, click on the radio button next to **Only select repositories** -Next, Vercel will open a modal asking you to give access to one or more repositories. I recommend only providing access to the repository where you store the code you want to deploy. To do so, click on the radio button next to **Only select repositories**, but if you prefer, you can also just allow access to all repositories - -Now click on the **Select repositories** select box and select your git repository +Now click on the **Select repositories** select box and select your git repository. ![](../../../../public/assets/images/app/web_development/posts/vercel/vercel_only_select_repositories.png) -Next, click on **Install** at the bottom of the modal - -**Option 2**: Open the GitHub apps settings page at [github.com/settings/installations](https://github.com/settings/installations) - -Then click on the **Configure** button (on the Vercel row) - -Next, scroll down to the **Repository access** section, **Only select repositories** is selected, and below that is a **Select repositories** select box. Click the down arrow and then search for the new repository you want to add - -Then click on the repository in the results +Finally, click on **Install** at the bottom of the modal. -Now that it got added to the list of repositories Vercel will have access to, finally click on the **save** button to finalize the changes +### Import a Repository -You are now done on GitHub. Go back to [Vercel.com](https://vercel.com). You should now be on the overview page (of your Vercel account), on the top right, click on **Add New...** and select **Project** - -### Import a git repository - -Now that your repository is in the **Git Repository** list, the next step is to click on the Import button that is on the right of your repository name +On the new project page, you will see that your repository has been added to the Import Git Repository list. Click on the Import button to the right of your repository name. ![](../../../../public/assets/images/app/web_development/posts/vercel/vercel_import_repository.png) -### Configure a Vercel Project - -Now you will be on a new **Configure Project** page, you can change the **Project Name** of the project (I left it as is, as I like it to match the name of my repository) +Now you will be on a new page, **Configure Project**, you can change the **Project Name** of the project (I left it as is, as I like it to match the name of my repository) -Next, you can choose a **Framework Preset**, I selected Next.js, but of course, you select the framework (Astro, Remix, ...) that you are using in your project +Next you can choose a **Framework Preset**, select **Next.js** (or the framework you chose for your project) -The next option, **Root Directory**, you can leave it as is if your project is located in the root of your repository (even if your code is an `./src` folder, the actual root of the project would still be `./`), if you use a monorepo that hosts multiple projects, then you may want to set the **Root Directory** to the path of your project, for example `./packages/PROJECT_NAME` +The next option, **Root Directory**, you can leave it as is (if you use a monorepo that hosts multiple projects, you may want to set the root to the directory in which you placed your project) -You can click on the **Build and Output Settings** to see what options are included, in this example, we don't need to touch the **Build and Output Settings** because we chose a **Framework Preset** earlier, meaning these values will already be set to defaults, if you know what you are doing and depending on what Framework you chose, you may want to adjust those values by using the **override** switch on the right and then entering your custom values +We also don't need to touch the **Build and Output Settings**, as we chose a **Framework Preset** earlier. These values are already set (if you know what you are doing and depending on what Framework you chose, you may want to adapt those values) -We have no use for the **Environment Variables** just yet. It is, however, an interesting feature that you will probably use at some time, as it lets you add environment variables to your project. Instead of having a .env file in your project, you set the key/value pairs using the Vercel interface, and Vercel will then pass those env variables to the javascript runtime. You can later download a copy of those env variables into your development environment if you want to have an actual file in your codebase +The **Environment Variables** is something we have no use for just yet; it is, however, an interesting feature that lets you add environment variables to your project (instead of having a .env file in your project, you set the key-value pair here and can later download a copy of those variables into your development environment) Finally, click on **Deploy** (unfortunately, the only way to **save** the imported project is to click Deploy, which will start a deployment. You may need more time to be ready to make your first deployment, and your main branch may still be empty. This doesn't matter; still, proceed and click Deploy. If the main branch is empty, the deployment will just fail, but your project will be saved. You can also click on **Cancel Deployment** if you want) -> [!MORE] -> [vercel.com "Environment Variables" documentation](https://vercel.com/docs/projects/environment-variables) - ## Sentry integration for Vercel First, open the [Sentry integration page on Vercel](https://vercel.com/integrations/sentry) diff --git a/app/web_development/posts/vscode/page.mdx b/app/web_development/posts/vscode/page.mdx index 4e667531..6263b4d9 100644 --- a/app/web_development/posts/vscode/page.mdx +++ b/app/web_development/posts/vscode/page.mdx @@ -2,7 +2,7 @@ title: VSCode keywords: ['VSCode', 'ide', 'programming', 'extension', 'theme'] published: 2024-08-05T11:22:33.444Z -modified: 2024-08-20T19:22:55.444Z +modified: 2024-11-23T23:11:00.444Z permalink: https://chris.lu/web_development/posts/vscode section: Web development --- @@ -41,11 +41,11 @@ export const metadata = { **VSCode** (Visual Studio Code) is a versatile Integrated Development Environment (IDE). VSCode is amazing to develop projects using Javascript / Typescript (and HTML, CSS) but it supports a wide range of other languages. I like VSCode because it has a lot of great features like smart code completion called [Intellisense](https://code.visualstudio.com/docs/editor/intellisense), code [refactoring](https://code.visualstudio.com/docs/editor/refactoring)... -VSCode also comes with great [debugging](https://code.visualstudio.com/docs/editor/debugging) tools, meaning you can debug your frontend but also backend code within VSCode. For example, managing breakpoints is similar to debugging using the Chrome developer tools debugging tools. The [VSCode debugger](https://code.visualstudio.com/docs/editor/debugging) launch configuration json file (launch.json) makes it very easy to adapt the **debugger** to any project, and the debugger **launch.json** can then be shared as part of your git repository, to make it very easy for other developers that will work on your project, and that also use VSCode, to use the debugger without having to configure it themself +VSCode also comes with great [debugging](https://code.visualstudio.com/docs/editor/debugging) tools, meaning you can debug your frontend but also backend code within VSCode. For example, managing breakpoints is similar to debugging using the Chrome developer tools debugging tools. The [VSCode debugger](https://code.visualstudio.com/docs/editor/debugging) launch configuration json file (launch.json) makes it very easy to adapt the **debugger** to any project, and the debugger **launch.json** can then be shared as part of your git repository, to make it very easy for other developers that will work on your project, and that also use VSCode, to use the debugger without having to configure it themselves What is also great is that VSCode has [integrated source control (git) support](https://code.visualstudio.com/docs/sourcecontrol/overview). To just name a few, **Staging**, **switching** between branches, and **pushing** are all supported by VSCode, meaning we will be able to push our commits directly to GitHub from within VSCode with just one click. It also makes resolving conflicts and merging very easy using its visual diff (as opposed to using git in the terminal or any other command line tool, which is another option if you prefer). -VSCode also has an [extensions (plugins) marketplace](https://code.visualstudio.com/docs/editor/extension-marketplace), which means that if one feature is not in VSCode core, you will probably find an extension that suits your needs. +VSCode also has an [extensions marketplace](https://code.visualstudio.com/docs/editor/extension-marketplace), which means that if one feature is not in VSCode core, you will probably find an extension that suits your needs. > [!NOTE] > VSCode (the project itself) is open source, and everyone can access and contribute to its source code in the [VSCode GitHub repository](https://github.com/microsoft/vscode). The source code is licensed using the MIT license. VSCode, the build you download from Microsoft, uses the [Microsoft Software License](https://code.visualstudio.com/license). @@ -177,137 +177,45 @@ The easiest way to save a file in VSCode is to use the keyboard shortcut `Ctrl+S > [!MORE] > [VSCode "Editing basics" documentation](https://code.visualstudio.com/docs/editor/codebasics) -## Commit your changes to GitHub using the VSCode version control tool - -Click on the **Source Control** icon on the left in the **Activity bar** (if you have already customized VSCode, your Activity bar might be in another location) or use the keyboard shortcut `Ctrl+Shift+G` (macOS: `⇧⌘G`, Linux: `Ctrl+Shift+G`) - -![](../../../../public/assets/images/app/web_development/posts/vscode/vscode_activity_bar_source_control_icon.png) - -* you will now see that all the files in which you made changes are listed in the category **Changes** -* if you double click on a filename, it will open a **diff** view in VSCode that allows you to see what has changed between the current version and the previous one; the previous version of the file is on the left, the current one on the right, on the right side the lines that will change are highlighted with a green color, on the left highlighted in red are the lines that will get modified, if you want to undo a change and revert to the previous version you can click on the arrow in the middle of the page next to the line you want to undo -* you don't need to commit all files that have been changed all at once; if you do, then just enter a commit message on top and hit the **Commit** button; else, to choose which files to commit, click on the + icon next to the file, this will add the file to a category called **Staged changes**, now enter a commit message for the files you selected and click on commit, this will create a commit with only those files -* next to each file, you also have an icon with an arrow that points to the left; if you want to discard all of the changes you made to a file, click on that icon, and it will reset the file (be careful this will delete all changes you did and you will not be able to recover them, if you are unsure, first create a new branch and commit the changes to that branch to have a backup just in case) - -Committing creates a local commit, but it does not yet push that commit to the remote repository. To sync the changes with the remote repository, click on the icon with two circular arrows next to the branch name at the bottom of the sidebar. - -![](../../../../public/assets/images/app/web_development/posts/vscode/vscode_sync_changes.png) - -> [!TIP] -> Synching means that the GitHub version control tool will first pull in changes from the remote repository and then right after that also do a push of your local changes to the remote repository - -## git credential manager - -### VSCode asks me to choose a GitHub account every time I sync - -If you have multiple users (GitHub / GitLab, … accounts) or, for example, a GitHub account but also a personal token on your machine, then the [git credential manager](https://github.com/git-ecosystem/git-credential-manager) will not know which one you want to use. Hence, it will ask you, meaning that every time you hit the sync button, it will open a modal that looks like this: - -![](../../../../public/assets/images/app/web_development/posts/vscode/vscode_git_credential_manager_modal.png) - -> [!TIP] -> To avoid getting asked every time, you can set a default user or token for a repository, which is what the next chapter is about - -> [!MORE] -> [GitHub "git-credential-manager" repository](https://github.com/git-ecosystem/git-credential-manager) -> [git-credential-manager documentation](https://github.com/git-ecosystem/git-credential-manager/blob/release/docs/README.md) - -### Setting a default user/token per repository - -To set a default user for a repository, you need to add the **USER_NAME** of the account you want to use to the remote URL of the repository. +## Open a VSCode terminal -To do so, open your VSCode terminal (or use your preferred command line tool) and then use the following command: +VSCode has a terminal (command line tool) that, by default, is closed. To open the terminal, click on **View** in the top navigation bar and then **Terminal** -```shell -git remote set-url origin https://USER_NAME@github.com/USER_NAME/REPOSITORY_NAME.git -``` +Or use the keyboard shortcut `` Ctrl+` `` (macOS: `` ⌃` ``, Linux: `` Ctrl+` ``) -Then press `ENTER`, and you are done. +The third option is to use the command palette by pressing using the keyboard shortcut `Ctrl+Shift+p` (macOS: `⇧⌘P`, Linux: `Ctrl+Shift+P`) and then typing `view: toggle terminal`. > [!MORE] -> [git-credential-manager "Multiple users" documentation](https://github.com/git-ecosystem/git-credential-manager/blob/main/docs/multiple-users.md) - -### getting a list of git credentials (on Windows) - -If you want to list all your git credentials that exist on your machine, you can use the following command in your terminal: - -```shell -cmdkey /list:git* -``` +> [VSCode "terminal" documentation](https://code.visualstudio.com/docs/terminal/basics) -### Manage git credentials (on Windows) +## VSCode command palette -To manage the accounts on your machine, for example if you decide to delete one of them, then you can use the [credential manager](https://support.microsoft.com/en-us/windows/accessing-credential-manager-1b5c916a-6a16-889f-8581-fc16e8165ac0) to do so, click on the windows start icon and then type **credential manager** to find it (optionally you can also open a File Explorer window and enter the following path: **Control Panel\All Control Panel Items\Credential Manager** to access it) +To open the VSCode command palette, use the keyboard shortcut `Ctrl+Shift+p` (macOS: `⇧⌘P`, Linux: `Ctrl+Shift+P`) -Once the **credential manager** is open, select the second tab called **windows credentials**, then click on the down arrow next to the credentials you want to edit or remove, verify those are the correct credentials, and then click on either **Edit** or **Remove** depending on what you wish to do. +Or you can use the VSCode navigation on top, click on **View**, and then click on **Command Palette...** > [!MORE] -> [accessing the Windows (10) "Credential Manager"](https://support.microsoft.com/en-us/windows/accessing-credential-manager-1b5c916a-6a16-889f-8581-fc16e8165ac0) - -## switching between branches - -Click on the **Source Control** icon on the left in the **Activity bar** (if you have already customized VSCode, your Activity bar might be in another location) or use the keyboard shortcut `Ctrl+Shift+G` (macOS: `⇧⌘G`, Linux: `Ctrl+Shift+G`) - -![](../../../../public/assets/images/app/web_development/posts/vscode/vscode_activity_bar_source_control_icon.png) - -* on the bottom left, you will have your current branch that you are currently on; if you have not yet created a new branch, it will probably be called "main" - -![](../../../../public/assets/images/app/web_development/posts/vscode/vscode_main_branch.png) - -* click on the branch name to open the branch options on top of VSCode; here, you can now choose a branch that you want to switch to (local branches have a branch icon in front of their name, branches that are on GitHub (remote branches that are in the cloud) have a cloud icon in front of their name and are called **origin**/branch_name) - * why do I see some branch names twice? This is because a branch that, for example, got created by another developer will be listed as origin/BRANCH_NAME; when you check out the origin/BRANCH_NAME, it will now exist both locally as **BRANCH_NAME** and also remotely (on GitHub) as **origin/BRANCH_NAME**, hence it will be twice in the list - * a branch that only exists in the remote repository (on GitHub) or a branch you created locally but did not yet push to the remote repository will only have one entry in the list - -![](../../../../public/assets/images/app/web_development/posts/vscode/vscode_local_and_remote_branch.png) - -## Creating a new branch - -On the bottom left, you will have your current branch that you are currently on. If you have not yet created a new branch, it will probably be called "main" - -Click on the branch name to open the options (on top of the IDE). - -![](../../../../public/assets/images/app/web_development/posts/vscode/vscode_main_branch.png) - -Now you have two options when it comes to creating branches: creating a new empty branch or creating a new branch based on what is in another branch (making a copy) - -### Create a new (empty) branch - -This will open the branch options on top of VSCode. Then click on **+ Create new branch** to create a new (empty) branch. - -![alt text](../../../../public/assets/images/app/web_development/posts/vscode/vscode_create_new_branch.png) - -Then, enter a name for that new branch, and you are done. - -### Create a new branch from (based on) another branch - -This will open the branch options on top of VSCode. Then click on **+ Create new branch from...** to create a new branch by copying an existing branch. - -![](../../../../public/assets/images/app/web_development/posts/vscode/vscode_create_new_branch_from.png) - -* First, select which branch you want to use to create the new branch -* then enter a name for that new branch +> [VSCode "User Interface > Command Palette" documentation](https://code.visualstudio.com/docs/getstarted/userinterface#_command-palette) -Now that the new branch has been created, I recommend you publish it immediately (push it to the remote repository) by clicking on the cloud icon next to its name at the bottom left of VSCode. +## VSCode diff (compare) two files -![](../../../../public/assets/images/app/web_development/posts/vscode/vscode_publish_branch.png) +It is easy to compare two files using a diff in VSCode. -## Open a VSCode terminal +On the right make sure the file explorer is open -VSCode has a terminal (command line tool) that, by default, is closed. To open the terminal, click on **View** in the top navigation bar and then **Terminal** +Then right click on a file and select **Select for Compare** -Or use the keyboard shortcut `` Ctrl+` `` (macOS: `` ⌃` ``, Linux: `` Ctrl+` ``) +Then right click on another file and choose **Compare with Selected** -The third option is to use the command palette by pressing using the keyboard shortcut `Ctrl+Shift+p` (macOS: `⇧⌘P`, Linux: `Ctrl+Shift+P`) and then typing `view: toggle terminal`. +This will open a nice diff panel with both files next to each other, the changes in both files will be highlighted and you have action buttons to revert changes -> [!MORE] -> [VSCode "terminal" documentation](https://code.visualstudio.com/docs/terminal/basics) +### files are in the wrong order -## VSCode command palette +You can easily swap both files by using the **Swap Left and Right Side** button on the top right of the diff panel (it is a button with an icon that has a top arrow pointing to the right and and below an arrow pointing to the left): -To open the VSCode command palette, use the keyboard shortcut `Ctrl+Shift+p` (macOS: `⇧⌘P`, Linux: `Ctrl+Shift+P`) +![](../../../../public/assets/images/app/web_development/tutorials/next-js-static-first-mdx-starterkit/vscode_diff_swap_sides_button.png) -Or you can use the VSCode navigation on top, click on **View**, and then click on **Command Palette...** - -> [!MORE] -> [VSCode "User Interface > Command Palette" documentation](https://code.visualstudio.com/docs/getstarted/userinterface#_command-palette) +You can also achieve this by using the [VSCode command palette](#vscode-command-palette), when open start typing **Compare** and then select the **Compare: Swap Left and Right Editor Side** command (command is only available when a diff panel is open) ## VSCode typescript version @@ -477,9 +385,13 @@ Then VSCode will list all extensions related to your keyword Next on the bottom right each extension (that is not yet installed) will have an **Install** button, click on the button to install the extension -## VSCode markdown (and MDX) related tips +## VSCode MDX and markdown extensions + +The chapters about the [VSCode MDX extension](/web_development/posts/mdx#vscode-extensions) and the VSCode markdownlint extension are in the [MDX post](/web_development/posts/mdx) -Here are a few tips that can make your life easier when editing markdown (like README.md files) or MDX content +## VSCode markdown (and MDX) features + +The following chapter contain tips about **VSCode features** that will make working with markdown (like your README.md) files in VSCode easier and faster ### easy way to add images to MDX content (using VSCODE) @@ -567,11 +479,63 @@ To change a heading and update all the links pointing to it, just click inside t > [!MORE] > [VSCode "Rename headers and links" documentation](https://code.visualstudio.com/docs/languages/markdown#_rename-headers-and-links) -## Git commands using the VSCode command palette +## VSCode git support + +If you like to use git commands then you can do so using the VSCode built in **terminal** support (you might want to read the [open a VSCode terminal](#open-a-vscode-terminal) chapter first) + +The first alternative to using the terminal (or any other command line tool) is to use the VSCode **command palette** (you might want to read the [VSCode command palette](#vscode-command-palette) chapter first) + +The third option is to the VSCode **Source Control** tools. I find the [source control graph](https://code.visualstudio.com/docs/sourcecontrol/overview#_source-control-graph) very useful to quickly get an overview of the incoming (and outgoing) commits. I also like to be able to just do a quick commit by clicking a button. + +> [!MORE] +> [VSCode "source control" documentation](https://code.visualstudio.com/docs/sourcecontrol/overview) -You can use the terminal and type any git command you want, but you can also do many things by using the VSCode command palette. +### Switching between branches -Press `Ctrl` + `Shift` + `p` to open the VSCode command palette and then type **Git**, then chose the Git command you wish to execute +Click on the **Source Control** icon on the left in the **Activity bar** (if you have already customized VSCode, your Activity bar might be in another location) or use the keyboard shortcut `Ctrl+Shift+G` (macOS: `⇧⌘G`, Linux: `Ctrl+Shift+G`) + +![](../../../../public/assets/images/app/web_development/posts/vscode/vscode_activity_bar_source_control_icon.png) + +* on the bottom left, you will have your current branch that you are currently on; if you have not yet created a new branch, it will probably be called "main" + +![](../../../../public/assets/images/app/web_development/posts/vscode/vscode_main_branch.png) + +* click on the branch name to open the branch options on top of VSCode; here, you can now choose a branch that you want to switch to (local branches have a branch icon in front of their name, branches that are on GitHub (remote branches that are in the cloud) have a cloud icon in front of their name and are called **origin**/branch_name) + * why do I see some branch names twice? This is because a branch that, for example, got created by another developer will be listed as origin/BRANCH_NAME; when you check out the origin/BRANCH_NAME, it will now exist both locally as **BRANCH_NAME** and also remotely (on GitHub) as **origin/BRANCH_NAME**, hence it will be twice in the list + * a branch that only exists in the remote repository (on GitHub) or a branch you created locally but did not yet push to the remote repository will only have one entry in the list + +![](../../../../public/assets/images/app/web_development/posts/vscode/vscode_local_and_remote_branch.png) + +### Creating a new branch + +On the bottom left, you will have your current branch that you are currently on. If you have not yet created a new branch, it will probably be called "main" + +Click on the branch name to open the options (on top of the IDE). + +![](../../../../public/assets/images/app/web_development/posts/vscode/vscode_main_branch.png) + +Now you have two options when it comes to creating branches: creating a new empty branch or creating a new branch based on what is in another branch (making a copy) + +#### Create a new (empty) branch + +This will open the branch options on top of VSCode. Then click on **+ Create new branch** to create a new (empty) branch. + +![alt text](../../../../public/assets/images/app/web_development/posts/vscode/vscode_create_new_branch.png) + +Then, enter a name for that new branch, and you are done. + +#### Create a new branch from (based on) another branch + +This will open the branch options on top of VSCode. Then click on **+ Create new branch from...** to create a new branch by copying an existing branch. + +![](../../../../public/assets/images/app/web_development/posts/vscode/vscode_create_new_branch_from.png) + +* First, select which branch you want to use to create the new branch +* then enter a name for that new branch + +Now that the new branch has been created, I recommend you publish it immediately (push it to the remote repository) by clicking on the cloud icon next to its name at the bottom left of VSCode. + +![](../../../../public/assets/images/app/web_development/posts/vscode/vscode_publish_branch.png) ### Rename the current git branch @@ -580,4 +544,69 @@ To rename the current **local** branch (the one currently being displayed in the > [!TIP] > To rename another branch (other than the current one), first switch to the branch you want to rename and then follow the steps listed above +### Commit your changes to GitHub using the VSCode version control tool + +Click on the **Source Control** icon on the left in the **Activity bar** (if you have already customized VSCode, your Activity bar might be in another location) or use the keyboard shortcut `Ctrl+Shift+G` (macOS: `⇧⌘G`, Linux: `Ctrl+Shift+G`) + +![](../../../../public/assets/images/app/web_development/posts/vscode/vscode_activity_bar_source_control_icon.png) + +* you will now see that all the files in which you made changes are listed in the category **Changes** +* if you double click on a filename, it will open a **diff** view in VSCode that allows you to see what has changed between the current version and the previous one; the previous version of the file is on the left, the current one on the right, on the right side the lines that will change are highlighted with a green color, on the left highlighted in red are the lines that will get modified, if you want to undo a change and revert to the previous version you can click on the arrow in the middle of the page next to the line you want to undo +* you don't need to commit all files that have been changed all at once; if you do, then just enter a commit message on top and hit the **Commit** button; else, to choose which files to commit, click on the + icon next to the file, this will add the file to a category called **Staged changes**, now enter a commit message for the files you selected and click on commit, this will create a commit with only those files +* next to each file, you also have an icon with an arrow that points to the left; if you want to discard all of the changes you made to a file, click on that icon, and it will reset the file (be careful this will delete all changes you did and you will not be able to recover them, if you are unsure, first create a new branch and commit the changes to that branch to have a backup just in case) + +Committing creates a local commit, but it does not yet push that commit to the remote repository. To sync the changes with the remote repository, click on the icon with two circular arrows next to the branch name at the bottom of the sidebar. + +![](../../../../public/assets/images/app/web_development/posts/vscode/vscode_sync_changes.png) + +> [!TIP] +> Synching means that the GitHub version control tool will first pull in changes from the remote repository and then right after that also do a push of your local changes to the remote repository + +## git credential manager + +### VSCode asks me to choose a GitHub account every time I sync + +If you have multiple users (GitHub / GitLab, … accounts) or, for example, a GitHub account but also a personal token on your machine, then the [git credential manager](https://github.com/git-ecosystem/git-credential-manager) will not know which one you want to use. Hence, it will ask you, meaning that every time you hit the sync button, it will open a modal that looks like this: + +![](../../../../public/assets/images/app/web_development/posts/vscode/vscode_git_credential_manager_modal.png) + +> [!TIP] +> To avoid getting asked every time, you can set a default user or token for a repository, which is what the next chapter is about + +> [!MORE] +> [GitHub "git-credential-manager" repository](https://github.com/git-ecosystem/git-credential-manager) +> [git-credential-manager documentation](https://github.com/git-ecosystem/git-credential-manager/blob/release/docs/README.md) + +### Setting a default user/token per repository + +To set a default user for a repository, you need to add the **USER_NAME** of the account you want to use to the remote URL of the repository. + +To do so, open your VSCode terminal (or use your preferred command line tool) and then use the following command: + +```shell +git remote set-url origin https://USER_NAME@github.com/USER_NAME/REPOSITORY_NAME.git +``` + +Then press `ENTER`, and you are done. + +> [!MORE] +> [git-credential-manager "Multiple users" documentation](https://github.com/git-ecosystem/git-credential-manager/blob/main/docs/multiple-users.md) + +### getting a list of git credentials (on Windows) + +If you want to list all your git credentials that exist on your machine, you can use the following command in your terminal: + +```shell +cmdkey /list:git* +``` + +### Manage git credentials (on Windows) + +To manage the accounts on your machine, for example if you decide to delete one of them, then you can use the [credential manager](https://support.microsoft.com/en-us/windows/accessing-credential-manager-1b5c916a-6a16-889f-8581-fc16e8165ac0) to do so, click on the windows start icon and then type **credential manager** to find it (optionally you can also open a File Explorer window and enter the following path: **Control Panel\All Control Panel Items\Credential Manager** to access it) + +Once the **credential manager** is open, select the second tab called **windows credentials**, then click on the down arrow next to the credentials you want to edit or remove, verify those are the correct credentials, and then click on either **Edit** or **Remove** depending on what you wish to do. + +> [!MORE] +> [accessing the Windows (10) "Credential Manager"](https://support.microsoft.com/en-us/windows/accessing-credential-manager-1b5c916a-6a16-889f-8581-fc16e8165ac0) + diff --git a/app/web_development/tutorials/next-js-static-first-mdx-starterkit/analytics_and_speed_insights/page.mdx b/app/web_development/tutorials/next-js-static-first-mdx-starterkit/analytics_and_speed_insights/page.mdx new file mode 100644 index 00000000..8164a75a --- /dev/null +++ b/app/web_development/tutorials/next-js-static-first-mdx-starterkit/analytics_and_speed_insights/page.mdx @@ -0,0 +1,282 @@ +--- +title: Vercel Analytics and Speed Insights - Next.js 15 Tutorial +description: Vercel Analytics and Speed Insights - Next.js 15 static first MDX starterkit | Web development tutorials | www.chris.lu +keywords: ['vercel', 'Analytics', 'Speed Insights', 'packages', 'gdpr'] +published: 2024-12-31T23:00:00.000Z +modified: 2024-12-31T23:00:00.000Z +permalink: https://chris.lu/web_development/tutorials/next-js-static-first-mdx-starterkit/analytics_and_speed_insights +section: Web development +--- + +import { sharedMetaDataArticle } from '@/shared/metadata-article' +import Breadcrumbs from '@/components/tutorial/Breadcrumbs' +import Pagination from '@/components/tutorial/Pagination' +import DonationsMessage from '@/shared/donations-message.mdx' + +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-js-static-first-mdx-starterkit/opengraph-image' + }], + url: frontmatter.permalink, + section: frontmatter.section, + publishedTime: frontmatter.published, + modifiedTime: frontmatter.modified, + tags: frontmatter.keywords, + }, +} + +%toc% + +
+ + + +# Vercel Analytics and Speed Insights + +We are now going to add Vercel **Analytics** and **Speed Insights** to our Next.js 15 project using 2 packages by Vercel so that later we can go to our Vercel Dashboard and check out how well our project performs + +The 2 chapters are quite similar because both packages work similarly. The real difference is only the name. This means that if you are done with the Analytics chapter, you just need to repeat the same steps for the Speed Insights package. + +## Add Vercel Analytics to your project + +> [!WARN] +> If you want to use this service, extra fees might apply, so make sure you check out the [vercel analytics pricing page](https://vercel.com/docs/analytics/limits-and-pricing) first; on a hobby plan the first 2500 events are free, and data will be kept for up to one month + +As we are about to release our project, now should be a good time to add [vercel analytics](https://vercel.com/docs/analytics/quickstart) so that we can have an overview of who visits our project + +First, go to [vercel.com](https://vercel.com/) and log in + +You should now be on the overview page, select the project for which you want to add analytics + +On the project page, in the top navigation click on **Analytics** + +Next, click on the **Enable** button + +Chose your plan, if you haven't already done so you may want to check out the [Vercel analytics pricing page](https://vercel.com/docs/analytics/limits-and-pricing) + +Now we need to add Vercel Analytics to our Next.js project, so we go back into VSCode and open our project + +Then we open the VSCode terminal and run the following command to install the Vercel Analytics package: + +```shell title="terminal" showLineNumbers +npm i @vercel/analytics --save-exact +``` + +Now we open the **root layout** that is the `/app` folder, and add the following code: + +```tsx title="/app/layout.tsx" showLineNumbers {5} {43} +import './global.css' +import { Metadata } from 'next' +import HeaderNavigation from '@/components/header/Navigation' +import { Kablammo } from 'next/font/google' +import { Analytics } from '@vercel/analytics/react' + +export const metadata: Metadata = { + title: { + template: '%s | example.com', + default: 'Home | example.com', + }, + description: 'My description', + openGraph: { + url: 'https://example.com/', + siteName: 'My website name', + locale: 'en_US', + type: 'website', + }, +} + +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 5: we import the react component from the Vercel [Analytics](https://www.npmjs.com/package/@vercel/analytics) package + +Line 43: we add the **Analytics react component** right before the body closing tag + +There are a few things you can configure, like for example, the debug mode is enabled by default and will print messages in your console when the environment is development, to disable it change the code to this: + +```tsx title="/app/layout.tsx" showLineNumbers{43} + +``` + +Line 43 set the **debug** prop of the analytics component to **false** to disable the debug mode + +There is also a **beforeSend** option that lets you modify the events before they get sent to Vercel, if you want to use this [advanced option](https://vercel.com/docs/analytics/package), I recommend you have a look at their [official example](https://vercel.com/docs/analytics/package) in the Vercel Analytics documentation + +To see your **Analytics** in action, log in to your Vercel account, click on your project name, and then click on **Analytics** + +> [!MORE] +> [Vercel "Web Analytics" quickstart](https://vercel.com/docs/analytics/quickstart) +> [Vercel "Advanced Web Analytics Configuration" documentation](https://vercel.com/docs/analytics/package) + +## Add Vercel Speed Insights to your project + +> [!WARN] +> If you want to use this service, extra fees might apply, so make sure you check out the [Vercel Speed Insights pricing page](https://vercel.com/docs/speed-insights/limits-and-pricing) first; on a hobby plan, the first project is free, and you get up 10k free analytics events + +First, go to [vercel.com](https://vercel.com/) and log in + +You should now be on the overview page, select the project for which you want to add analytics + +On the project page, in the top navigation click on **Speed Insights** + +Next, click on the **Enable** button + +Now we need to add Vercel Speed Insights to our Next.js project, so we go back into VSCode and open our project + +Then we open the VSCode terminal and run the following command to install the vercel Speed Insights package: + +```shell title="terminal" +npm i @vercel/speed-insights --save-exact +``` + +Now we open the root layout that is the `/app` folder, and add the following code: + +```tsx title="/app/layout.tsx" showLineNumbers {6} {45} +import './global.css' +import { Metadata } from 'next' +import HeaderNavigation from '@/components/header/Navigation' +import { Kablammo } from 'next/font/google' +import { Analytics } from '@vercel/analytics/react' +import { SpeedInsights } from '@vercel/speed-insights/next' + +export const metadata: Metadata = { + title: { + template: '%s | example.com', + default: 'Home | example.com', + }, + description: 'My description', + openGraph: { + url: 'https://example.com/', + siteName: 'My website name', + locale: 'en_US', + type: 'website', + }, +} + +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 6: we import the react component from the Vercel [Speed Insights](https://www.npmjs.com/package/@vercel/speed-insights) package + +Line 45: we add the **Speed Insights react component** right before the body closing tag + +There are a few things you can configure, like for example, the debug mode is enabled by default and will print messages in your console when the environment is development, if you want to disable you change the code to this: + +```tsx title="/app/layout.tsx" showLineNumbers{45} + +``` + +Line 45: set the **debug** prop of the Speed Insights component to **false** + +There is also a **beforeSend** option that lets you modify the data before it gets sent to Vercel, if you want to use this [advanced option](https://vercel.com/docs/speed-insights/package), I recommend you have a look at their [official example](https://vercel.com/docs/speed-insights/package) in the Vercel **Speed Insights** documentation + +To see your **Speed Insights** in action, login to your Vercel account, click on your projects name and then click on **Speed Insights** + +> [!MORE] +> [Vercel "Speed Insights" quickstart](https://vercel.com/docs/speed-insights/quickstart) +> [Vercel "Speed Insights Configuration" documentation](https://vercel.com/docs/speed-insights/package) + +## How to fix TrustedScript errors + +If the CSP **trusted-types** directive is in use, then you will get the following errors in your browser console: + +```shell +This document requires 'TrustedScriptURL' assignment. +``` + +Which is why you need to remove the directive from your CSP header, to do so edit your `next.config.mjs` file and then search for `require-trusted-types-for` and delete that entry from your CSP directives list + +You might want to add a comment instead (above the defaultCSPDirectives) to let other developers that might wonder why this directive is missing, or why you haven't included it: + +```js +// we commented out the trusted types directive: +// require-trusted-types-for 'script'; +// because of the following error in the browser console: +// This document requires 'TrustedScript' assignment +``` + +I have more details about this directive in the [require-trusted-types-for](/web_development/posts/csp#require-trusted-types-for-directive) chapter in the CSP post + +A second change you need to do (when using Vercel analytics), is to add the `https://va.vercel-scripts.com` domain for the **development** environment to the **script-src** directive: + +```shell +script-src 'self' 'unsafe-inline' 'unsafe-eval' https://va.vercel-scripts.com; +``` + +This is only needed in development, in development Vercel Analytics does NOT track visits, but it is still loading a script from that domain, which is why we add it to the script-src directive + + + + + +
diff --git a/app/web_development/tutorials/next-js-static-first-mdx-starterkit/ci-cd-pipeline-setup/page.mdx b/app/web_development/tutorials/next-js-static-first-mdx-starterkit/ci-cd-pipeline-setup/page.mdx new file mode 100644 index 00000000..4554e88e --- /dev/null +++ b/app/web_development/tutorials/next-js-static-first-mdx-starterkit/ci-cd-pipeline-setup/page.mdx @@ -0,0 +1,154 @@ +--- +title: CI/CD pipeline for automatic deployments - Next.js 15 Tutorial +description: CI/CD pipeline for automatic deployments - Next.js 15 static first MDX starterkit | Web development tutorials | www.chris.lu +keywords: ['CI/CD', 'Vercel', 'build', 'Production', 'preview'] +published: 2024-12-31T23:00:00.000Z +modified: 2024-12-31T23:00:00.000Z +permalink: https://chris.lu/web_development/tutorials/next-js-static-first-mdx-starterkit/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' +import DonationsMessage from '@/shared/donations-message.mdx' + +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-js-static-first-mdx-starterkit/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) + +> [!WARN] +> Vercel is a paid service but their 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. For personal blog the hobby plan is enough, but if you plan on deploying a high traffic website then costs will occur, to get a better idea about what is included in the free plan and what the potential costs are, I recommend checking out their [pricing page](https://vercel.com/pricing) first + +> [!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 like [AWS](https://aws.amazon.com/), [Google cloud (GCP)](https://cloud.google.com/), [Azure](https://azure.microsoft.com/), [Netlify](https://www.netlify.com/), [Heroku](https://www.heroku.com/), to just name a few, but in this example, I show you how easy and quick it is to use [Vercel](https://vercel.com) + +I'm not sponsored and don't endorse any of the providers listed here, I just wanted to give Vercel a try for some time and thought it would fit well into this tutorial. So far I really like it, my blog [chris.lu](https://chris.lu) currently uses the hobby plan. I will try to make more tutorials in the future that use other deployment tools and hosting providers, maybe the next tutorial will be about deploying a static website to [GitHub pages](https://pages.github.com/) using [GitHub actions](https://docs.github.com/actions) + +> [!MORE] +> [vercel.com "pricing" page](https://vercel.com/pricing) + +## 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** branch deployment yet) + +> [!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 {11-15} +# MY_PROJECT + +## npm commands (package.json scripts) + +`npm run dev`: to start the development server (NOT using turbopack) +`npm run dev-turbo`: to start the development server (using turbopack) +`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) + +At the end of the deployment row there is a button with **3 dots**, click on the **3 dots** (`...`) at the end of your preview branch row and then click on **Copy URL** to copy the URL of the deployment + +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` + +On top of project page, in the navigation, there is also a tab called **Deployments**, which leads to a page that list all current and past deployments for your project. + +> [!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-first-mdx-starterkit/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 + + + + + +
diff --git a/app/web_development/tutorials/next-js-static-first-mdx-starterkit/code-highlighting-plugin/page.mdx b/app/web_development/tutorials/next-js-static-first-mdx-starterkit/code-highlighting-plugin/page.mdx new file mode 100644 index 00000000..1edf7c2a --- /dev/null +++ b/app/web_development/tutorials/next-js-static-first-mdx-starterkit/code-highlighting-plugin/page.mdx @@ -0,0 +1,948 @@ +--- +title: Code highlighting plugin - Next.js 15 Tutorial - Next.js 15 Tutorial +description: Code highlighting plugin - Next.js 15 static first MDX starterkit | Web development tutorials | www.chris.lu +keywords: ['highlighting', 'rehype', 'plugin', 'pretty', 'code', 'shiki'] +published: 2024-12-31T23:00:00.000Z +modified: 2024-12-31T23:00:00.000Z +permalink: https://chris.lu/web_development/tutorials/next-js-static-first-mdx-starterkit/code-highlighting-plugin +section: Web development +--- + +import { sharedMetaDataArticle } from '@/shared/metadata-article' +import Breadcrumbs from '@/components/tutorial/Breadcrumbs' +import Pagination from '@/components/tutorial/Pagination' +import DonationsMessage from '@/shared/donations-message.mdx' + +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-js-static-first-mdx-starterkit/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 about **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 + +## No next.config.ts + +If you use Typescript for your Next.js 15 config file and import this plugin, then you will get a node.js error telling you that it can NOT import the "vscode-textmate" package (which is a shiki dependency, shiki is used by this plugin). + +As described on a previous page, the reason for this is that next.config.ts files have [NO support for ESM only packages](/web_development/tutorials/next-js-static-first-mdx-starterkit/next-config#nextconfigts-does-not-support-esm-only-packages) (yet) + +As of now the only workaround for this problem (besides not using this plugin, as there are many alternatives like [rehype-starry-night](https://github.com/rehypejs/rehype-starry-night) and [rehype-highlight)](https://github.com/rehypejs/rehype-highlight)), is to go back to using a **next.config.mjs** file. You could rename your current next.config.ts to _next.config.ts (with an underscore) to have a backup and then make a copy that you name it next.config.mjs. Then you need to edit the next.config.mjs to comment out type imports and comment out code that creates or uses types. After that all you can do is keep using a Javascript file until the Typescript configuration files get support for ESM only packages + +## 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) +} +``` + +
+ +```` + +We create a new playground MDX page, where 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/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-first-mdx-starterkit/MDX_code_block_no_code_highlighting.png) + +The markdown code block syntax got converted into `
{:html}` HTML element, and inside of that container, there is a `{:html}` 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 {2} {6} {15} /rehypePrettyCode/
+import { withSentryConfig } from '@sentry/nextjs';
+//import type { NextConfig } from 'next'
+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/*: string*/) => {
+
+    const withMDX = createMdx({
+        extension: /\.mdx$/,
+        options: {
+            // optional remark and rehype plugins
+            remarkPlugins: [],
+            rehypePlugins: [rehypePrettyCode, rehypeMDXImportMedia],
+        },
+    })
+```
+
+Line 2: as we went back to using Javascript for our next.config file, we need to comment out type imports (we keep them as we will transition back to Typescript as soon as the [next.config.ts has no support for ESM only packages](#no-nextconfigts) problem we discussed earlier is fixed)
+
+Line 3: As this is not Typescript code anymore, and we are importing a file (have a look at the Node.js documentation explaining why [file extensions are mandatory](https://nodejs.org/api/esm.html#mandatory-file-extensions) for more details), which is why we added the extension to the constants file path (to become `next/constants.js`)
+
+Without the file extension you would get this error in your terminal as soon as you try to launch the development server:
+
+```shell
+Error [ERR_MODULE_NOT_FOUND]: Cannot find module 'next\constants' imported from next.config.mjs
+
+Did you mean to import "next/constants.js"
+```
+
+Line 6: we import our new **rehypePrettyCode** plugin
+
+Line 15: 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-first-mdx-starterkit/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. If you haven't a favorite VSCode theme yet, have a look at the [VSCode marketplace](https://marketplace.visualstudio.com/vscode)
+>  
+> 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), if a theme is in that package, then it is convenient as you don't need to install the theme yourself
+
+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 {10}#special {11-13} {20} /rehypePrettyCodeOptions/#special
+import { withSentryConfig } from '@sentry/nextjs';
+//import type { NextConfig } from 'next'
+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/*: string*/) => {
+
+    /** @type {import('rehype-pretty-code').Options} */
+    const rehypePrettyCodeOptions = {
+        theme: 'dracula',
+    }
+
+    const withMDX = createMdx({
+        extension: /\.mdx$/,
+        options: {
+            // optional remark and rehype plugins
+            remarkPlugins: [],
+            rehypePlugins: [[rehypePrettyCode, rehypePrettyCodeOptions], rehypeMDXImportMedia],
+        },
+    })
+```
+
+Line 9: 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 10 to 12: 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)
+
+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-first-mdx-starterkit/vscode_rehype_pretty_code_configuration_options_autocomplete.png)
+
+Line 19: 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-first-mdx-starterkit/rehype-pretty-code_plugin_dracula_theme.png)
+
+### Using a VSCode theme from a git repository
+
+If there is a theme you wish to use, but that theme is NOT among the ones bundled in shiki, then you can get the theme using its git repository as source.
+
+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 defined as name in its package.json
+
+Then you go into your Next.js configuration file and change the code to this:
+
+```js title="next.config.mjs" showLineNumbers {7} {11-12} {16} /JSON.parse(themeFileContent)/
+import { withSentryConfig } from '@sentry/nextjs';
+//import type { NextConfig } from 'next'
+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/*: string*/) => {
+
+    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: /\.mdx$/,
+        options: {
+            // optional remark and rehype plugins
+            remarkPlugins: [],
+            rehypePlugins: [[rehypePrettyCode, rehypePrettyCodeOptions], rehypeMDXImportMedia],
+        },
+    })
+```
+
+Line 7: we import the **readFileSync** function from the Node.js [file system module (fs)](https://nodejs.org/api/fs.html)
+
+Line 11: we create a URL using the path to the location of our theme in the node_modules folder
+
+Line 12: we use the **readFileSync** to read the content of the theme file
+
+Line 16: we parse the themes json file content using the javascript **JSON parser** before passing it to the plugin theme configuration option
+
+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-first-mdx-starterkit/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 have created your own theme you could then host it on GitHub to share it with others, and even go a step further by [creating your own VSCode theme extension](https://code.visualstudio.com/api/get-started/your-first-extension) and then [publish it on the VSCode marketplace](https://code.visualstudio.com/api/working-with-extensions/publishing-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 `
{:html}` element that is the container of a rehype-pretty-code code block; **rehype-pretty-code** use a `
{:html}` element as container around code blocks, that `
{:html}` element has a **data-rehype-pretty-code-figure** attribute, we use that attribute to target the element using a selector in our CSS, as well as the `
{:html}` element that is inside of the `
{:html}` element + +Lines 141 to 145: we set the **width** of the `
{:html}` 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, then the code block is allowed to add (and display) a vertical scrollbar. If the code fits the code block then the scrollbar will be hidden. Finally we use our spacing variable to add some padding all around our code. 
+
+### Customizing the code block background color
+
+When styling and using themes, you can either keep the original background color defined in the themes json 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 configure rehype pretty code to use a custom background color instead of the one from the VSCode theme. However, it can be tricky to find a background color that both, looks good when placed in a page that uses your websites color palette, and still has a high enough contrast when used as background for code that gets highlighted using the VSCode theme colors.
+
+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{13} {4}
+/** @type {import('rehype-pretty-code').Options} */
+const rehypePrettyCodeOptions = {
+    theme: JSON.parse(themeFileContent),
+    keepBackground: false,
+}
+```
+
+Line 16: 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
+
+Make sure your dev server is running and then have a look at the result of what we have achieved so far: `http://localhost:3000/code-highlighting_playground`
+
+(Also if you didn't do a commit recently, now is a good time to do so)
+
+## 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 side):
+
+```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** keyword will have line numbers. This is because when using the keyword, the rehype pretty code plugin will add `data-line-numbers` attribute to the `{:html}` elements, our CSS selector which checks if that attribute exists will then apply the numbering
+
+````md title="/app/(tutorial_examples)/code-highlighting_playground/page.mdx" showLineNumbers {3} /showLineNumbers/#special
+
+ +```js showLineNumbers +function helloWorld() { + // this is a comment + let greeting = 'Hello World!' + console.log(greeting) +} +``` + +
+ +```` + +Line 3: 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 {3} /showLineNumbers{10}/1#special +
+ +```js showLineNumbers{10} +function helloWorld() { + // this is a comment + let greeting = 'Hello World!' + console.log(greeting) +} +``` + +
+ +```` + +Line 3: we updated the `showLineNumbers` and attached `{10}` to it, to tell the code block that the first line is number 10 (and NOT the default 1) + +> [!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 so + +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 uses a border to create a vertical line on the left, we will then colorize that border when the code 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 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 {3} +
+ +```js showLineNumbers {1} {3-4} +function helloWorld() { + // this is a comment + let greeting = 'Hello World!' + console.log(greeting) +} +``` + +
+ +```` + +Line 3: we removed the `{10}{:md}` 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}{:md}` to highlight the first line and also added `{3-4}{:md}` to highlight lines 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 we first add an ID to our curly brackets in the playground code block example: + +````md title="/app/(tutorial_examples)/code-highlighting_playground/page.mdx" showLineNumbers {3} /#errorLine/#special +
+ +```js showLineNumbers {1}#errorLine {3-4} +function helloWorld() { + // this is a comment + let greeting = 'Hello World!' + console.log(greeting) +} +``` + +
+ +```` + +Line 3: we add a new highlight style ID after the curly brackets using a hashtag (`#`), and then we set the highlight line ID to `errorLine` + +If you launch the dev server, then open the playground URL `http://localhost:3000/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-first-mdx-starterkit/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. + +![highlighted line using a special ID to make it look like there was an error at that line](../../../../../public/assets/images/app/web_development/tutorials/next-js-static-first-mdx-starterkit/rehype-pretty-code_plugin_highlight_lines_special_id.png) + +## 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 {3} /helloWorld()/1#special +
+ +```js showLineNumbers /helloWorld()/ +function helloWorld() { + // this is a comment + let greeting = 'Hello World!' + console.log(greeting) +} +``` + +
+ +```` + +Line 3: 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{190} +[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 {3} /#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 {3} /2/#special +
+ +```js showLineNumbers /greeting/2#mySpecialHighlight +function helloWorld() { + // this is a comment + let greeting = 'Hello World!' + console.log(greeting) +} +``` + +
+ +```` + +Gives you this (only the second occurrence of "greeting" is highlighted): + +![tell the code block to highlight the "greeting" characters but only the second occurrence](../../../../../public/assets/images/app/web_development/tutorials/next-js-static-first-mdx-starterkit/rehype-pretty-code_plugin_highlight_characters_but_only_second_occurence.png) + +## 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 **JSX**, then all you need to do is change the language flag (after the backticks) to `jsx`, like so: + +````md title="/app/(tutorial_examples)code-highlighting_playground/page.mdx" showLineNumbers {3} /jsx/#special +
+ +```jsx showLineNumbers /greeting/2#mySpecialHighlight +function helloWorld() { + // this is a comment + let greeting = 'Hello World!' + console.log(greeting) +} +``` + +
+ +```` + +Line 3: we changed the language flag from js to **jsx** + +> [!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) + +### Configuring the code block default language + +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 {18-20} +import { withSentryConfig } from '@sentry/nextjs'; +//import type { NextConfig } from 'next' +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/*: string*/) => { + + 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', + }, + } +``` + +Lines 18 to 20: 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 3: 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-first-mdx-starterkit/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 rule](/web_development/tutorials/next-js-static-first-mdx-starterkit/linting-in-vscode-using-extensions#disabling-rules-using-comments) 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](/web_development/tutorials/next-js-static-first-mdx-starterkit/eslint-mdx-plugin-and-remark-lint#markdown-linting-using-remark-lint) chapter, after editing the remark-lint configuration you might want to [restart the ESLint server in VSCode](/web_development/tutorials/next-js-static-first-mdx-starterkit/linting-in-vscode-using-extensions#restarting-the-eslint-server-in-vscode) and [clear the ESLint cache](/web_development/tutorials/next-js-static-first-mdx-starterkit/eslint-mdx-plugin-and-remark-lint#clearing-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 + +### diff (+/- lines) language flag + +I mention the **diff** language flag separately as it has an extra feature, by adding a **+** or **-** sign to the start of a line you can tell the code highlighter to display the rows starting with a plus sign as if they had been added and the rows starting with a minus sign as if they had been removed (meaning lines that got added are green and lines that got removed are red) + +````md title="/app/(tutorial_examples)/code-highlighting_playground/page.mdx" showLineNumbers +
+ +```diff +function helloWorld() { +- // this is a comment ++ let greeting = 'Hello World!' + console.log(greeting) +} +``` + +
+ +```` + +With this language you can use the **+** and **-** signs to tell the code highlighter to display the rows starting with a plus sign as if they had been added and the rows starting with a minus sign as if they had been removed + +## 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 (below the code block example) like so: + +````md title="/app/(tutorial_examples)/code-highlighting_playground/page.mdx" showLineNumbers {11} +
+ +```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 `{:html}` 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 `
{:html}` element between the `[data-rehype-pretty-code-figure]` (the `
{:html}` element) and the `{:html}` element, but inline code has no `
{:html}` element between the two. We can use that to our advantage here, by using a child combinator (`>`) between `[data-rehype-pretty-code-figure]` and `code`. This ensures our styling will only get applied if the `{:html}` element is a direct child of the `[data-rehype-pretty-code-figure]` (the `
{:html}` element). + +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 `
{:html}` 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 preceded by a colon (`:`). The brackets need to be at the end of our inline code before the "closing" backtick, like so:
+
+```md /{:js}/#special
+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 {20}
+import { withSentryConfig } from '@sentry/nextjs';
+//import type { NextConfig } from 'next'
+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/*: string*/) => {
+
+    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',
+        },
+    }
+```
+
+Line 20: 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** (`:`)
+
+### How do I know which 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`)
+
+> [!MORE]  
+> [VSCode "Semantic Highlight" guide](https://code.visualstudio.com/api/language-extensions/semantic-highlight-guide#standard-token-types-and-modifiers)  
+
+### token aliases
+
+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{13} {9-17}
+/** @type {import('rehype-pretty-code').Options} */
+const rehypePrettyCodeOptions = {
+    theme: JSON.parse(themeFileContent),
+    keepBackground: false,
+    defaultLang: {
+        block: 'js',
+        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',
+    },
+}
+```
+
+Lines 21 to 29: 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
+
+> [!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  
+
+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{19} 
+## 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 code-highlighting playground page to test our tokens map, this time using the tsx language flag:
+
+````md title="/app/(tutorial_examples)/code-highlighting_playground/page.mdx" showLineNumbers
+
+ +```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-first-mdx-starterkit/rehype-pretty-code_code_block_and_inline_code_examples.png) + +> [!TIP] +> There are even more features like **shiki transformers**, **visitor hooks** and **custom highlighters** (which we will not see in this tutorial), but you can find more info about them in the [rehype pretty code](https://rehype-pretty.pages.dev/) and [shiki](https://shiki.style/) docs websites + +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) + + + + + +
diff --git a/app/web_development/tutorials/next-js-static-first-mdx-starterkit/content-security-policy/page.mdx b/app/web_development/tutorials/next-js-static-first-mdx-starterkit/content-security-policy/page.mdx new file mode 100644 index 00000000..c5208b80 --- /dev/null +++ b/app/web_development/tutorials/next-js-static-first-mdx-starterkit/content-security-policy/page.mdx @@ -0,0 +1,501 @@ +--- +title: Content Security Policy (CSP) - Next.js 15 Tutorial +description: Content Security Policy (CSP) - Next.js 15 static first MDX starterkit | Web development tutorials | www.chris.lu +keywords: ['Content', 'Security', 'Policy', 'CSP', 'Headers', 'violation', 'report'] +published: 2024-12-31T23:00:00.000Z +modified: 2024-12-31T23:00:00.000Z +permalink: https://chris.lu/web_development/tutorials/next-js-static-first-mdx-starterkit/content-security-policy +section: Web development +--- + +import { sharedMetaDataArticle } from '@/shared/metadata-article' +import Breadcrumbs from '@/components/tutorial/Breadcrumbs' +import Pagination from '@/components/tutorial/Pagination' +import DonationsMessage from '@/shared/donations-message.mdx' + +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-js-static-first-mdx-starterkit/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 Next.js 15 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.ts` configuration file (which is in the root of the project) like so: + +```js title="next.config.ts" showLineNumbers {19-26} {33} /cspReportOnly/#special /upgradeInsecure/#special {35-98} {100-105} +import type { NextConfig } from 'next' +import { PHASE_DEVELOPMENT_SERVER } from 'next/constants' +import { withSentryConfig } from '@sentry/nextjs' + +const nextConfig = (phase: string) => { + + if (phase === PHASE_DEVELOPMENT_SERVER) { + console.log('happy development session ;)') + } + + const nextConfigOptions: NextConfig = { + reactStrictMode: true, + poweredByHeader: false, + experimental: { + // experimental typescript "statically typed links" + // https://nextjs.org/docs/app/api-reference/next-config-js/typedRoutes + typedRoutes: true, + }, + headers: async () => { + return [ + { + source: '/(.*)', + headers: securityHeadersConfig(phase) + }, + ]; + }, + }; + + return nextConfigOptions + +} + +const securityHeadersConfig = (phase: string) => { + + const cspReportOnly = true + + const cspHeader = () => { + + const upgradeInsecure = (phase !== PHASE_DEVELOPMENT_SERVER && !cspReportOnly) ? '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'; + frame-ancestors 'none'; + ${upgradeInsecure} + ` + + // when environment is preview enable unsafe-inline scripts for vercel preview feedback/comments feature + // and allow vercel's domains based on: + // https://vercel.com/docs/workflow-collaboration/comments/specialized-usage#using-a-content-security-policy + // and allow also 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 allowing vitals.vercel-insights.com + // 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' 'unsafe-inline'; + connect-src 'self' https://vitals.vercel-insights.com; + img-src 'self' data:; + 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 + +} +``` + +Lines 19 to 26: 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 33: 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 35 to 97: 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 want to adjust the directive to 'self' instead of 'none' + +Line 37: we have added a `upgradeInsecure` variable that we will only contain 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 and only if **cspReportOnly** is false, 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, however if **cspReportOnly** is false, then this directive does not apply, if you still enable it while in "report only" mode than you will get these errors in the console: + +> The Content Security Policy directive 'upgrade-insecure-requests' is ignored when delivered in a report-only policy. + +Lines 99 to 104: 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 + +> [!TIP] +> 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/vercel-toolbar/managing-toolbar#using-a-content-security-policy) + +### require-trusted-types-for (script) + +I tried to add CSP **require-trusted-types-for** directive for this Next.js 15 tutorial, but I kept on getting errors in the console related to this rule, which is why I did not add in the above CSP configuration + +It is a powerful CSP directive (which is still experimental), to better understand what it does I will quote the [caniuse](https://caniuse.com/trusted-types) page: + +> An API that forces developers to be very explicit about their use of powerful DOM-injection APIs. Can greatly improve security against XSS attacks. + +If you are interested in using that directive then have a look at my [require-trusted-types-for](/web_development/posts/csp#require-trusted-types-for-directive) chapter in the CSP post + +## 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 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{87} {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 uses the JavaScript [eval()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/eval) function, eval is required for development scripts like the Hot Module Reload (HMR) tool, which is a tool that reloads our page every time we save a file. + +> [!WARN] +> JavaScript [eval()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/eval#never_use_direct_eval!) function can be dangerous when you display content that got through user inputs (like a textarea or an input field), as a malicious hacker could inject Javascript as a sting and then your eval function would turn that string into executable Javascript that could do things like stealing user credentials to a server owned by the malicious actor + +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. + +> [!NOTE] +> we only add 'unsafe-eval' when in development mode, we do not add 'unsafe-eval' to preview or production, HMR is only active in development so there is no need to add 'unsafe-eval' in preview or production + +## 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 might trigger violations. If unhandled, those violations will probably create bugs on your website, which is why you want to keep an eye on those violations by logging 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 + +> [!WARN] +> However this does NOT work 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](https://docs.sentry.io/security-legal-pii/security/security-policy-reporting/)), chrome will then also 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"). +> +> 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. + +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 + +> [!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) + +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 also start using the Reporting-Endpoints header as soon as Sentry supports it + +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, so that they can use the reporting for debugging, because they 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 = 'INSET_YOUR_SENTRY_REPORT_URI_HERE'** in the following code by the CSP **REPORT URI** from your Sentry account +* For the **reportingDomainWildcard** variable, I used `https://*.ingest.us.sentry.io` as this is the domain name used in the Sentry **REPORT URI**, if your Sentry account is in the EU your **REPORT URI** might have `https://*.ingest.eu.sentry.io` domain, in which case you also need to change the **reportingDomainWildcard** variable value to be `https://*.ingest.eu.sentry.io` + +Next, we need to ensure violations get sent to Sentry.io (logged like any other error), to do that we will edit our CSP setup in the `next.config.mjs` file, like so: + +```js title="next.config.mjs" showLineNumbers{31} {5-6} {14-15} {48} {51} {63} {66} /${reportingDomainWildcard}/#special /${reportCSPViolations}/#special +const securityHeadersConfig = (phase: string) => { + + const cspReportOnly = true + + const reportingUrl = 'INSET_YOUR_SENTRY_REPORT_URI_HERE' + const reportingDomainWildcard = 'https://*.ingest.us.sentry.io' + // if in the EU, uncomment next line, and comment out previous one + //const reportingDomainWildcard = 'https://*.ingest.eu.sentry.io' + + const cspHeader = () => { + + const upgradeInsecure = (phase !== PHASE_DEVELOPMENT_SERVER && !cspReportOnly) ? 'upgrade-insecure-requests;' : '' + + // reporting uri (CSP v1) + const reportCSPViolations = `report-uri ${reportingUrl};` + + // we commented out the trusted types directive: + // require-trusted-types-for 'script'; + // because of the following error in the browser console: + // This document requires 'TrustedScript' assignment + + // 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'; + frame-ancestors 'none'; + ${upgradeInsecure} + ` + + // when environment is preview enable unsafe-inline scripts for vercel preview feedback/comments feature + // and allow vercel's domains based on: + // https://vercel.com/docs/workflow-collaboration/comments/specialized-usage#using-a-content-security-policy + // and allow 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 allow vitals.vercel-insights.com + // 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' 'unsafe-inline'; + 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 + +} +``` + +Lines 35 to 36: 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 **INSET_YOUR_SENTRY_REPORT_URI_HERE**, we use that variable to tell the report-uri directive where to send CSP violations reports (to what endpoint), make sure you replace the **INSET_YOUR_SENTRY_REPORT_URI_HERE** placeholder with your own DSN from Sentry.io as described at the top of this chapter +* The second variable contains a wildcard for the reporting domain, so that we can add the domain to our connect-src directive + +> [!NOTE] +> As most devs that use Sentry are using the service that is located in the US, the reporting domain wildcard should be ok as it is, but if your Sentry account is in the EU, then you need to change it to this value to `https://*.ingest.eu.sentry.io` +> +> The difference is that we replace **us** with **eu** before the last part which is **sentry.io** +> +> If using a wildcard does not work for you, you could also just put the full URL into the **reportingDomainWildcard** variable, that works too, the wildcard is just more flexible in case you generate a new DSN it will still be valid + +Lines 44 to 45: 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 + +Lines 78 and 93: we add the **reportingDomainWildcard** to the connect-src directive + +Lines 81 and 96: 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. + +> [!TIP] +> If you want to debug your code, you might want to also add the reporting for development. In that case, add the `${reportCSPViolations}` and `${reportingDomainWildcard}` variables to the development directives too (same as for preview and production) and then check out the chapter about [disabling the "reports from localhost" filter](/web_development/posts/sentry-io#disable--enable-reports-from-localhost-filter) in my Sentry.io post as you will need to disable the filter for localhost reporting in your Sentry configuration on Sentry.io +> +> If you want to only allow certain domains to be able to send in reports, then you can add those domains to an allow list, meaning Sentry.io will filter out reports that come from other domains (and use your Sentry DSN), I have a chapter in my Sentry.io post that goes explains [how to set up an allowed domains list](/web_development/posts/sentry-io#allowed-domains-filter) using the Sentry UI + +> [!MORE] +> [Sentry.io "CSP violations logging" documentation](https://docs.sentry.io/security-legal-pii/security/security-policy-reporting/) +> [chris.lu "Sentry.io" post](/web_development/posts/sentry-io) + +## 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 + +For now we only added the CSP setup to every page header by altering the Next.js configuration file, next we are going to add 4 more security headers: + +```js title="next.config.ts" showLineNumbers{113} {1-11} {14} {19-30} + // security headers for preview & production + const extraSecurityHeaders = [] + + if (phase !== PHASE_DEVELOPMENT_SERVER) { + extraSecurityHeaders.push( + { + key: 'Strict-Transport-Security', + value: 'max-age=31536000', // 1 year + }, + ) + } + + 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 +``` + +Lines 113 to 123: we edit the content of our **securityHeadersConfig** function and add 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 the code of the page will make, even if the URL scheme of the content it needs to fetch is HTTP. + +Line 126: we use the `extraSecurityHeaders` variable to add the `Strict-Transport-Security` header to the list of headers + +Lines 131 to 142: we add 3 more security headers to the list of headers: + +* the first one is a `Referrer-Policy` header, which 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 job at explaining the different values, I like to only set the referrer for internal pages but not for external pages, that's why I set 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 (which is a technique called MIME type sniffing) and instead take the value that we (our CDN) puts into the header, the problem with MIME type sniffing is that a malicious actor could hide code in the MIME type string of a file to then perform an attack on the tool doing the MIME type sniffing +* The third one is the `X-Frame-Options` header, when set to **deny** it does the same thing as the **frame-ancestors** directive (we added earlier) when it is set to **none**, but it is for older browsers that did not have support for the directive, If you want to decide for yourself, then have a look at the [caniuse "frame-ancestors" page](https://caniuse.com/?search=frame-ancestors) and if you think support for the directive is high enough then you can drop [X-Frame-Options](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options) + +Congratulations 🎉 you just made your project a lot more secure by setting up CSP headers and reporting potential violations + + + +> [!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-first-mdx-starterkit/error-handling-and-logging/page.mdx b/app/web_development/tutorials/next-js-static-first-mdx-starterkit/error-handling-and-logging/page.mdx new file mode 100644 index 00000000..661c44b3 --- /dev/null +++ b/app/web_development/tutorials/next-js-static-first-mdx-starterkit/error-handling-and-logging/page.mdx @@ -0,0 +1,597 @@ +--- +title: Error handling and logging - Next.js 15 Tutorial +description: Error handling and logging - Next.js 15 static first MDX starterkit | Web development tutorials | www.chris.lu +keywords: ['error', 'handling', 'logging', 'Boundary', 'react', 'sentry.io', 'nextjs'] +published: 2024-12-31T23:00:00.000Z +modified: 2024-12-31T23:00:00.000Z +permalink: https://chris.lu/web_development/tutorials/next-js-static-first-mdx-starterkit/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' +import DonationsMessage from '@/shared/donations-message.mdx' + +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-js-static-first-mdx-starterkit/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 into we can put different files. We already created the page(.tsx|.jsx|.mdx) file, then Next.js 15 created a default layout file for us, the third file we will now create 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), that error page can do different things with the error, obviously it can display an error message to the user but you can also use it to do other things like save the error in your database or send a request to your logging service + +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: + +```tsx title="app/error.tsx" showLineNumbers {5-8} {10-13} {23-28} +'use client' // Error components must be Client Components + +import { useEffect } from 'react' + +interface ErrorBoundaryProps { + error: Error & { digest?: string } + reset: () => void +} + +export default function Error({ + error, + reset, +}: ErrorBoundaryProps) { + + useEffect(() => { + // simulate logging the error + console.error(error) + }, [error]) + + return ( + <> +

Sorry, something went wrong 😞

+ + + ) +} +``` + +Lines 5 to 8: we create a Typescript interface for the props our error boundary + +Lines 10 to 13: a Next.js error boundary will give you two useful things, the error itself and a function that can rerender the segment in which the error occurred + +Lines 23 to 28 there is the second feature of this component, a `button` which will trigger the `reset` function we got from the component props (line 10), 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 (lines 13 to 16) and inside of it we use a `console.log` to print an error in the console of the browser, but what happens if the error is getting triggered on a user's computer, then we won't know about it. This is why in the next 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) + +## Using Sentry.io to log errors + +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 + +We will use the Sentry.io **Wizard** tool to **install** the **Sentry.io SDK for Next.js** or you can follow their manual [setup tutorial](https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/) (if you prefer a manual installation). + +> [!MORE] +> [Sentry "Next.js" documentation](https://docs.sentry.io/platforms/javascript/guides/nextjs/) + +### Sentry.io account & first project + +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) + +> [!TIP] +> Before using the wizard, I recommend committing your latest changes (if you haven't already) and doing a last sync before launching the sentry wizard, as the Sentry Wizard will add and edit some existing files. This way you will be able to see (using a git diff after the wizard is done) which new files the Sentry Wizard has added, and also see what got changed in existing files like the next.config.(mjs|ts) file + +> [!MORE] +> [chris.lu "Sentry" post](/web_development/posts/sentry-io) + +### Next.js 15.x canary (optional) + +If you don't use the latest stable version of Next.js 15.x but prefer to use a **canary** version, then attempting to add the **latest** version of **Sentry for Next.js** will likely result in a dependency error: + +```shell +npm error ERESOLVE unable to resolve dependency tree +│ npm error +│ npm error While resolving: nextjs15_sentry_wizard_reproduction@0.1.0 +│ npm error Found: next@15.x.x-canary.x +``` + +You can bypass this error by using the overrides in your package.json, here is an example of what your package would look like with an overrides for Next.js 15 canary: + +```json title="package.json" showLineNumbers {22-24} +{ + "name": "MY_PROJECT", + "version": "0.0.1", + "scripts": { + "dev": "next dev", + "dev-turbo": "next dev --turbopack", + "build": "next build", + "start": "next start", + "lint": "next lint" + }, + "dependencies": { + "next": "canary", + "react": "canary", + "react-dom": "canary" + }, + "devDependencies": { + "@types/node": "22.10.2", + "@types/react": "19.0.2", + "@types/react-dom": "19.0.2", + "typescript-eslint": "8.18.1" + }, + "overrides": { + "next": "canary" + } +} +``` + +Lines 22 to 24 we add an **overrides** that will tell npm that it should use this Next.js version instead of the one it finds in the peer dependencies for Sentry and potentially other packages as well + +> [!NOTE] +> In the package.json I used the canary tag, if you want to use an exact version you can use the [npmjs next versions page](https://www.npmjs.com/package/next?activeTab=versions) or you have a look inside of their [next package.json](https://github.com/vercel/next.js/blob/canary/packages/next/package.json). +> +> When you update your exact canary version, make sure that you update both Next.js versions, the one in your dependencies and the one you use in the overrides + +### Sentry Installation Wizard + +```shell +npx @sentry/wizard@latest -i nextjs +``` + +Or, if you prefer to use the wizard without sending telemetry data to sentry.io (usage statics and crash reports), then add the `--disable-telemetry` option to the command, like so: + +```shell +npx @sentry/wizard@latest -i nextjs --disable-telemetry +``` + +### Wizard Q&A + +First, you will get asked if you accept installing the sentry wizard npm package. Press `y` to accept and press `Enter` to move on + +After the installation, the wizard will automatically get launched, and it will start asking you questions about your setup preferences: + +**Are you using Sentry SaaS or self-hosted Sentry?** You probably want to choose **Sentry SaaS (sentry.io)** like I did (but if you are a company and need a custom solution, then you might want to look at the hosted version), then press `Enter`. + +**Do you already have an account?** chose **Yes** (if you did follow the previous chapter or already had an account before), then press `Enter` (or choose **No** if you have no account yet and follow the [account creation](/web_development/posts/sentry-io#create-an-account-sign-up) process) + +Then, the sentry wizard will ask you to log in, which will open the sentry login page in your default browser. Log into your account, then go back to your terminal. + +**Select your Sentry project**, choose your Sentry Project from the list (when using the wizard, Sentry will have automatically created a project for the SDK you chose earlier for you; if, however, you don't see a Project listed here, you can check out the [Create a Sentry.io project](/web_development/posts/sentry-io#create-a-sentryio-project) chapter to create a project first) + +Now, Sentry will install the latest Sentry SDK for Next.js. + +**Do you want to route Sentry requests in the browser through your NextJS server to avoid ad blockers?** Sentry wants to know if it should route its requests through your Next.js server. By doing so, Sentry attempts to bypass the block lists of adblocker addons that are installed in some browsers. This means Sentry will first send the client-side requests to a URL on your server, and then your server will forward the request to the Sentry API. I personally chose **Yes** as I want to increase the chance of getting bug reports but feel free to answer **No** (if for example, you don't want to have the extra traffic, on your server backend, that this redirect will cause) + +**Do you want to enable React component annotations to make breadcrumbs and session replays more readable?** Next Sentry is asking if we want to use the feature called [React component annotations](https://docs.sentry.io/platforms/javascript/guides/react/features/component-names/) which attempts to use component names in reports instead of more cryptic selectors, I think this is a nice feature, so I selected **Yes**, if you already use Sentry.io and don't want to change how bug reports work, then leave it on **No**, you can always turn it on/off via the configuration later if you want + +> [!WARN] +> I turned **React component annotations** on and then noticed that my [react-three-fiber](https://github.com/pmndrs/react-three-fiber) animation had stopped working, this is because **React component annotations** adds data attributes to components which React Three Fiber does not like, and which then creates bugs which print the following in your console: +> +> > TypeError: Cannot read properties of undefined (reading 'sentry') +> +> So if you plan on using **React Three Fiber** then you should answer to this question with **NO**, to learn more about this problem and how to disable **React component annotations** manually in the configuration have a look at the [Sentry React Component Annotation(s) can be problematic](#sentry-react-component-annotations-can-be-problematic) chapter + +**Do you want to create an example page** chose **YES** (we will later use it to test the Sentry setup, and then we will delete it) + +**Are you using a CI/CD tool to build and deploy your application?** chose **YES** (if you are using Vercel, GitHub actions, or any other CI/CD deployment tool); If you do NOT use one, choose **NO**) + +The Sentry.io Wizard will give you a **SENTRY_AUTH_TOKEN** string if you choose yes. If you use a CI/CD for your deployments, copy the token, and save it in a secure location, you will need this token later if, for example, you set up a custom GitHub action. You will want to add that token environment variable to your GitHub secrets. If you use another CI/CD service, check out their documentation to learn how to use that token to upload source maps to Sentry automatically. If using Vercel, you can use the [Sentry integration for Vercel](https://vercel.com/integrations/sentry), which will set the Vercel environment variables for you, or if you prefer you can add the token manually to your environment variables using the [Vercel environment variables interface](https://vercel.com/docs/projects/environment-variables). + +CI/CD tools can authenticate themselves to Sentry.io using the **SENTRY_AUTH_TOKEN** environment variable and then use the Sentry.io API to automatically upload the source maps of your build to Sentry.io. Later, if there is a bug report on Sentry.io, it will be able to use the source maps instead of the minified build files to show you where the error occurred. + +**Did you configure CI as shown above?** chose **YES** + +That was the last question: + +> Successfully installed the Sentry Next.js SDK! + +After answering all questions, the Sentry SDK will edit your next.config.mjs to add the **withSentryConfig** Sentry configuration, and it will have added several sentry.*.config files (to the root of your project) that contain environment-specific configurations, it will also create some other files depending on what answers you gave, like a page.tsx that we can now use to test the setup + +> [!NOTE] +> If you use Vercel for your deployments, then you don't need to set the **SENTRY_AUTH_TOKEN** yourself; you can use the [Sentry integration for Vercel](https://vercel.com/integrations/sentry), which will set the Vercel environment variables for you, I recommend you do that now, as all you need to do is click on the **Add integration** button, and then continue with the tutorial + +## Update Sentry SDK version (to use alpha or beta) (optional) + +If you use the wizard it will always use the **latest** version of Sentry for Next.js + +If you are feeling adventurous, you could try out the latest alpha or beta versions, to know what versions are currently available I recommend having a look at the [Sentry for Next.js version](https://www.npmjs.com/package/@sentry/nextjs?activeTab=versions) page and check out what the latest versions are + +Sentry for Next.js has a **next** branch that is similar to a canary in Next.js, if you want to install that version (instead of the latest that the wizard installed), then use the following command: + +```shell +npm i @sentry/nextjs@next --save-exact +``` + +This will replace the current version of Sentry for Next.js with a more up to date but also more experimental version + +> [!WARN] +> be careful when you chose what version you install as there is not always an alpha or beta available, sometimes the newest beta or alpha that is tagged with **next** (on npmjs) will actually be older than the version tagged as **latest** + +## Sentry configuration + +Sentry.io can be customized a lot and has several places to change the default configuration. + +The main part of the Sentry for Next.js configuration is in your `next.config.ts` file (which is in the root of your project): + +```ts title="next.config.ts" showLineNumbers {25-66} +import { withSentryConfig } from '@sentry/nextjs' +import type { NextConfig } from 'next' +import { PHASE_DEVELOPMENT_SERVER } from 'next/constants' + +const nextConfig = (phase: string) => { + + if (phase === PHASE_DEVELOPMENT_SERVER) { + console.log('happy development session ;)') + } + + const nextConfigOptions: NextConfig = { + reactStrictMode: true, + poweredByHeader: false, + experimental: { + // experimental typescript "statically typed links" + // https://nextjs.org/docs/app/api-reference/next-config-js/typedRoutes + typedRoutes: true, + } + } + + return nextConfigOptions + +} + +export default withSentryConfig( + nextConfig, + { + // For all available options, see: + // https://github.com/getsentry/sentry-webpack-plugin#options + + org: "MY_ORGANIZATION_NAME", + project: "MY_PROJECT_NAME", + + // Only print logs for uploading source maps in CI + silent: !process.env.CI, + + // For all available options, see: + // https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/ + + // Upload a larger set of source maps for prettier stack traces (increases build time) + widenClientFileUpload: true, + + // Automatically annotate React components to show their full name in breadcrumbs and session replay + reactComponentAnnotation: { + enabled: true, + }, + + // Route browser requests to Sentry through a Next.js rewrite to circumvent ad-blockers. + // This can increase your server load as well as your hosting bill. + // Note: Check that the configured route will not match with your Next.js middleware, otherwise reporting of client- + // side errors will fail. + tunnelRoute: "/monitoring", + + // Hides source maps from generated client bundles + hideSourceMaps: true, + + // Automatically tree-shake Sentry logger statements to reduce bundle size + disableLogger: true, + + // Enables automatic instrumentation of Vercel Cron Monitors. (Does not yet work with App Router route handlers.) + // See the following for more information: + // https://docs.sentry.io/product/crons/ + // https://vercel.com/docs/cron-jobs + automaticVercelMonitors: true, + } +) +``` + +Lines 25 to 66: the Sentry wizard has updated your `next.config.ts` based on the answers you gave to the wizard questions (I formatted mine (in VSCode, you can right click and then select **Format Document**) + +Now is a good time to commit the updated `next.config.ts` (and other changes the wizard did) + +> [!NOTE] +> By default, the options that the wizard set for us are good enough. As soon as you have the time, I recommend checking out the [Extend your Next.js Configuration](https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/#extend-your-nextjs-configuration) Sentry.io documentation, which explains what each option does + +### Sentry React Component Annotation(s) can be problematic + +To enable Sentry **reactComponentAnnotation** configuration option is usually a good idea as it makes reports more readable by using component names instead of long selectors + +To make this feature happen Sentry needs to add a data attributes to components, this does usually not pose a problem except sometimes the Sentry Annotations on third party components will cause an error in those third party tools + +I have a more detailed explanation about annotations and the problems that can occur in my [Sentry Post](/web_development/posts/sentry-io) + +The solution to these problems is to disable the feature and wait for the Sentry team to fine tune the feature (which is quite young) and fix annotations bugs (I have linked to some tickets in my Sentry post, you may want to subscribe to them if you want to keep track of fixes) + +> [!MORE] +> [chris.lu "Sentry.io" post](/web_development/posts/sentry-io) + +### Sentry does not (yet) support Turbopack + +If you try to use Turbopack with Sentry you will get the following error: + +```shell +[@sentry/nextjs] WARNING: You are using the Sentry SDK with `next dev --turbo`. The Sentry SDK doesn't yet fully support Turbopack. The SDK will not be loaded in the browser, and serverside instrumentation will be inaccurate or incomplete. Production builds without `--turbo` will still fully work. If +you are just trying out Sentry or attempting to configure the SDK, we recommend temporarily removing the `--turbo` flag while you are developing locally. Follow this issue for progress on Sentry + Turbopack: https://github.com/getsentry/sentry-javascript/issues/8105. (You can suppress this warning by +setting SENTRY_SUPPRESS_TURBOPACK_WARNING=1 as environment variable) +``` + +As of now (dec. 2024) there is no workaround available for this, you need to make a choice and either not use Turbopack or use Turbopack but then Sentry will not get loaded in development (production builds are not impacted as Next.js does NOT (yet) use Turbopack for production builds) + +In the meantime if you are interested in seeing how the Sentry JS SDK support for Turbopack progresses, then I recommend subscribing to their ["Turbopack Support" issue #8105](https://github.com/getsentry/sentry-javascript/issues/8105) + +> [!NOTE] +> I assume the first Sentry SDK release that will be fully compatible with Turbopack will be v9 (this is an assumption)!? But before that happens there is a quite long list of [Sentry javascript SDK v9 tasks](https://github.com/getsentry/sentry-javascript/issues/14225), the Turbopack changes are related to **withSentryConfig** + +> [!MORE] +> [Sentry "javascript SDK v9 tasks" issue](https://github.com/getsentry/sentry-javascript/issues/14225) + +## The instrumentation file + +Next.js has added support for a new [instrumentation](https://nextjs.org/docs/app/building-your-application/optimizing/instrumentation) which has the goal to improve the integration of monitoring and logging tools into Next.js + +Sentry wizard will have created such a `instrumentation.ts` file for us, with the following content: + +```ts title="instrumentation.ts" showLineNumbers +export async function register() { + if (process.env.NEXT_RUNTIME === 'nodejs') { + await import('./sentry.server.config') + } + + if (process.env.NEXT_RUNTIME === 'edge') { + await import('./sentry.edge.config') + } +} +``` + +> [!MORE] +> [Next.js "instrumentation" documentation](https://nextjs.org/docs/app/building-your-application/optimizing/instrumentation) + +## Sentry Client Configuration file + +Another file that got added to the root of our project is `sentry.client.config.ts`, which is used to configure Sentry.io for **client components**. Check out the [Next.js SDK Configuration Options](https://docs.sentry.io/platforms/javascript/guides/nextjs/configuration/options/) documentation for more details about each option. We will slightly modified the Sentry configuration to this: + +```ts title="sentry.client.config.ts" showLineNumbers {7-8} {10-12} {14-16} {22} {27} {31} {42} +// This file configures the initialization of Sentry on the client. +// The config you add here will be used whenever a users loads a page in their browser. +// https://docs.sentry.io/platforms/javascript/guides/nextjs/ + +import * as Sentry from '@sentry/nextjs' + +let replaysOnErrorSampleRate = 0 +let tracesSampleRate = 0.1 + +if (process.env.NODE_ENV === 'production') { + replaysOnErrorSampleRate = 1 +} + +if (process.env.NODE_ENV === 'development') { + tracesSampleRate = 0 +} + +Sentry.init({ + dsn: 'YOUR_SENTRY_DSN_URL', + + // Adjust this value in production, or use tracesSampler for greater control + tracesSampleRate: tracesSampleRate, + + // Setting this option to true will print useful information to the console while setting up Sentry. + debug: false, + + replaysOnErrorSampleRate: replaysOnErrorSampleRate, + + // on free plan lower (as limited to 50 per month) + // if you have a paid plan set it higher + replaysSessionSampleRate: 0, + + // You can remove this option if you're not planning to use the Sentry Session Replay feature: + integrations: [ + Sentry.replayIntegration({ + // Additional Replay configuration goes in here, for example: + maskAllText: true, + blockAllMedia: true, + }), + ], + + environment: process.env.NODE_ENV ? process.env.NODE_ENV : '', +}) +``` + +Lines 7 to 8: we added two variables for the **replaysOnErrorSampleRate** we set it to zero by default to not produce replays when not in production and we set the **tracesSampleRate** to 0.1, meaning we want to limit the amount of traces that get recorded (and sent to sentry) to 10% + +Lines 10 to 12: we added a check to verify if the current environment is **production** and if it is we tell Sentry to always make a replay if there is an error (be careful with this option, in the free plan you only have 50 replays per month, which is why I only turn it on in production, also in development it is usually the developer that triggers the error so there is not really a need for a replay) + +Lines 14 to 16: we disable the traces in development (this is to ensure no performance metrics are getting calculated when the app is running on a local computer, to check local performance it is preferred to use the developer tools), you may want a lower or higher value depending on what plan you are on and then check if you reach your limits or not and then adjust over time + +Lines 22 and 27: we replace the default sentry values with our replaysOnErrorSampleRate and tracesSampleRate variables + +Line 31: we set the **replaysSessionSampleRate** to zero (related to the changes we did line 10 to 12) + +Line 42: we pass the environment to Sentry, meaning Sentry will know if the environment is preview or production (that value is based on the Vercel environment on which Next.js got deployed) + +> [!WARN] +> If you copy paste the `sentry.client.config.ts` above into your project make sure you update the **YOUR_SENTRY_DSN_URL** placeholder with your own Sentry DSN + +`sentry.edge.config.ts` are the options for when Next.js uses the [Vercel Edge Network](https://vercel.com/docs/edge-network/overview). I kept that file, but feel free to adjust any values to fit your use case. + +`sentry.server.config.ts` is again similar to the previous two, just this one is specifically for Next.js server-side options. I also kept this file as is + +There is, however, one option in the server configuration that is commented out (by default) that you might want to consider. The option lets you use the [Spotlight js](https://spotlightjs.com/) package by Sentry. If you're going to use it, I recommend checking out the documentation to [install and setup Spotlight in a Next.js project](https://spotlightjs.com/setup/nextjs/) + +> [!MORE] +> [Sentry.io "Extend your Next.js Configuration" documentation](https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/#extend-your-nextjs-configuration) +> [Sentry.io "Next.js SDK Configuration" documentation](https://docs.sentry.io/platforms/javascript/guides/nextjs/configuration/options/) +> [Sentry.io "Configure Tunneling to avoid Ad-Blockers" documentation](https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/#configure-tunneling-to-avoid-ad-blockers) +> [Spotlight js "Using Spotlight with Next.js" documentation](https://spotlightjs.com/setup/nextjs/) + +## Sentry Next.js example page + +Next, as suggested by the wizard at the end of the installation process, it is recommended to start the development server using the `npm run dev` command, and then we visit the Sentry example page in our browser at `http://localhost:3000/sentry-example-page` + +On that page, hit the **Throw Error!** button and then click on the link just below to **visit your Sentry projects issues page** + +Now wait for the backend and frontend errors to appear (this can take a few minutes, which is a good time to refresh your cup of coffee ☕ (or whatever beverage you prefer)) + +As soon as the two errors appear, feel free to click on them and have a look at what error logging on Sentry.io looks like + +Before we commit/sync all the changes Sentry.io did in our project, **delete** the `app\sentry-example-page\` folder, including the `page.jsx` **example page**, and then also delete the `app\api\sentry-example-api\` folder including the `route.js` **API example route** file that Sentry.io created to test the error logging, you will not need them anymore. + +## Error page with Sentry error logging + +Now that Sentry.io is set up, we can modify the error file we created earlier, like so: + +```tsx title="app/error.tsx" showLineNumbers {3} {17-18} +'use client' // Error components must be Client Components + +import * as Sentry from '@sentry/nextjs' +import { useEffect } from 'react' + +interface ErrorBoundaryProps { + error: Error & { digest?: string } + reset: () => void +} + +export default function Error({ + error, + reset, +}: ErrorBoundaryProps) { + + useEffect(() => { + // log the error to Sentry.io + Sentry.captureException(error) + }, [error]) + + return ( + <> +

Sorry, something went wrong 😞

+ + + ) +} +``` + +Line 3: we import the Sentry SDK + +Lines 17 to 18: we replace our **console.error** (inside of the `useEffect(){:.function}`) with the Sentry.io **captureException** logging function + +> [!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 boundary does not catch errors thrown in the root app/layout or app/template component. +> +> To specifically handle errors in these root components, use a variation of error.js called global-error.js located in the root `/app` directory. +> +> global-error is the least granular error UI and can be considered "catch-all" error handling for the whole application. + +> [!WARN] +> same as for the warnings above, if you used the current wizard with Next.js and a typescript configuration file then the wizard will have created a `app/global-error.jsx` file, if this is your case then delete the `app/global-error.jsx` and create a new `app/global-error.tsx` file, then insert the code as shown below + +We will update the global-error file Sentry just created, and add a slightly different UI using the same reset button we added to the regular error page instead of using the Next.js built in **NextError** component, the final version looks like this: + +```ts title="app/global-error.tsx" showLineNumbers {3} {17-18} +'use client' // Error components must be Client Components + +import * as Sentry from '@sentry/nextjs' +import { useEffect } from 'react' + +interface GlobalErrorProps { + error: Error & { digest?: string } + reset: () => void +} + +export default function GlobalError({ + error, + reset, +}: GlobalErrorProps) { + + useEffect(() => { + // log the error to Sentry.io + Sentry.captureException(error) + }, [error]) + + return ( + + +

Sorry, something went wrong 😞

+ + + + ) +} +``` + +Line 3: we import the Sentry SDK + +Lines 17 to 18: we replace our **console.error** (inside of the `useEffect(){:.function}`) with the Sentry.io **captureException** logging function + +> [!TIP] +> 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 + + + +> [!MORE] +> [Next.js "handling global errors (in root layouts)" documentation](https://nextjs.org/docs/app/building-your-application/routing/error-handling#handling-global-errors) + + + +
diff --git a/app/web_development/tutorials/next-js-static-first-mdx-starterkit/eslint-mdx-plugin-and-remark-lint/page.mdx b/app/web_development/tutorials/next-js-static-first-mdx-starterkit/eslint-mdx-plugin-and-remark-lint/page.mdx new file mode 100644 index 00000000..16c26d6a --- /dev/null +++ b/app/web_development/tutorials/next-js-static-first-mdx-starterkit/eslint-mdx-plugin-and-remark-lint/page.mdx @@ -0,0 +1,383 @@ +--- +title: ESLint MDX plugin and remark-lint - Next.js 15 Tutorial +description: ESLint MDX plugin and remark-lint - Next.js 15 static first MDX starterkit | Web development tutorials | www.chris.lu +keywords: ['Linting', 'MDX', 'markdown', 'remark', 'remark-lint', 'plugin', 'rules', 'eslint', 'nextjs'] +published: 2024-12-31T23:00:00.000Z +modified: 2024-12-31T23:00:00.000Z +permalink: https://chris.lu/web_development/tutorials/next-js-static-first-mdx-starterkit/eslint-mdx-plugin-and-remark-lint +section: Web development +--- + +import { sharedMetaDataArticle } from '@/shared/metadata-article' +import Breadcrumbs from '@/components/tutorial/Breadcrumbs' +import Pagination from '@/components/tutorial/Pagination' +import DonationsMessage from '@/shared/donations-message.mdx' + +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-js-static-first-mdx-starterkit/opengraph-image' + }], + url: frontmatter.permalink, + section: frontmatter.section, + publishedTime: frontmatter.published, + modifiedTime: frontmatter.modified, + tags: frontmatter.keywords, + }, +} + +%toc% + +
+ + + +# ESLint MDX plugin and remark-lint + +We have added linting for our code by creating a custom ESLint 9 flat config, next we will add linting for our MDX (markdown) content + +## Adding and configuring the MDX ESLint plugin + +First we install the [ESLint MDX plugin](https://github.com/mdx-js/eslint-mdx/tree/master/packages/eslint-plugin-mdx): + +```shell +npm i eslint-plugin-mdx@latest --save-dev --save-exact +``` + +Then we can update our ESLint configuration and to add the MDX plugin: + +```ts title="eslint.config.ts" showLineNumbers {12} {130-153} /lintCodeBlocks/#special {160} +import eslintPlugin from '@eslint/js' +import tseslint, { configs as tseslintConfigs } from 'typescript-eslint' +import type { FlatConfig } from '@typescript-eslint/utils/ts-eslint' +// @ts-expect-error this package has no types +import importPlugin from 'eslint-plugin-import' +import reactPlugin from 'eslint-plugin-react' +// @ts-expect-error this package has no types +import reactHooksPlugin from 'eslint-plugin-react-hooks' +import jsxA11yPlugin from 'eslint-plugin-jsx-a11y' +// @ts-expect-error this package has no types +import nextPlugin from '@next/eslint-plugin-next' +import * as mdxPlugin from 'eslint-plugin-mdx' + +const eslintConfig = [ + { + name: 'custom/eslint/recommended', + files: ['**/*.ts?(x)'], + ...eslintPlugin.configs.recommended, + }, +] + +const ignoresConfig = [ + { + name: 'custom/eslint/ignores', + // the ignores option needs to be in a separate configuration object + // replaces the .eslintignore file + ignores: [ + '.next/', + '.vscode/', + 'public/', + ] + }, +] as FlatConfig.Config[] + +const tseslintConfig = tseslint.config( + { + name: 'custom/typescript-eslint/recommended', + files: ['**/*.ts?(x)'], + extends: [ + ...tseslintConfigs.recommended, + // OR more type checked rules + //...tseslintConfigs.recommendedTypeChecked, + // OR more strict rules + //...tseslintConfigs.strict, + // OR more strict and type checked rules + //...tseslintConfigs.strictTypeChecked, + // optional stylistic rules + ...tseslintConfigs.stylistic, + // OR the type checked version + //...tseslintConfigs.stylisticTypeChecked, + ] as FlatConfig.ConfigArray, + // only needed if you use TypeChecked rules + languageOptions: { + parserOptions: { + // https://typescript-eslint.io/getting-started/typed-linting + projectService: true, + tsconfigRootDir: import.meta.dirname, + // react recommended is already adding the ecmaFeatures + /*ecmaFeatures: { + jsx: true, + },*/ + // better keep it turned on, if needed uncomment + //warnOnUnsupportedTypeScriptVersion: false, + }, + }, + }, + { + // disable type-aware linting on JS files + // only needed if you use TypeChecked rules + // (and you have javascript files in your project) + files: ['**/*.mjs'], + ...tseslintConfigs.disableTypeChecked, + name: 'custom/typescript-eslint/disable-type-checked', + }, +) + +const nextConfig = [ + { + name: 'custom/next/config', + // no files for this config as we want to apply it to all files + plugins: { + 'react': reactPlugin, + 'jsx-a11y': jsxA11yPlugin, + /* eslint-disable @typescript-eslint/no-unsafe-assignment */ + 'react-hooks': reactHooksPlugin, + '@next/next': nextPlugin, + 'import': importPlugin, + /* eslint-enable @typescript-eslint/no-unsafe-assignment */ + }, + rules: { + ...reactPlugin.configs.recommended.rules, + ...reactPlugin.configs['jsx-runtime'].rules, + /* eslint-disable @typescript-eslint/no-unsafe-member-access */ + ...reactHooksPlugin.configs.recommended.rules, + ...nextPlugin.configs.recommended.rules, + // this is the nextjs strict mode + ...nextPlugin.configs['core-web-vitals'].rules, + ...importPlugin.configs.recommended.rules, + /* eslint-enable @typescript-eslint/no-unsafe-member-access */ + //...jsxA11yPlugin.configs.recommended.rules, + // OR more strict a11y rules + ...jsxA11yPlugin.configs.strict.rules, + // rules from eslint-config-next + 'import/no-anonymous-default-export': 'warn', + 'react/no-unknown-property': 'off', + 'react/react-in-jsx-scope': 'off', + 'react/prop-types': 'off', + 'react/jsx-no-target-blank': 'off', + 'jsx-a11y/alt-text': ['warn', { elements: ['img'], img: ['Image'], },], + 'jsx-a11y/aria-props': 'warn', + 'jsx-a11y/aria-proptypes': 'warn', + 'jsx-a11y/aria-unsupported-elements': 'warn', + 'jsx-a11y/role-has-required-aria-props': 'warn', + 'jsx-a11y/role-supports-aria-props': 'warn', + } as FlatConfig.Rules, + settings: { + 'react': { + version: 'detect', + }, + // only needed if you use (eslint-import-resolver-)typescript + 'import/resolver': { + typescript: { + alwaysTryTypes: true + } + } + }, + } +] as FlatConfig.Config[] + +const mdxConfig = [ + // https://github.com/mdx-js/eslint-mdx/blob/d6fc093fb32ab58fb226e8cf42ac77399b8a4758/README.md#flat-config + { + name: 'custom/mdx/recommended', + files: ['**/*.mdx'], + ...mdxPlugin.flat, + processor: mdxPlugin.createRemarkProcessor({ + // I disabled linting code blocks + // as I was having performance issues + lintCodeBlocks: false, + languageMapper: {}, + }), + }, + { + name: 'custom/mdx/code-blocks', + files: ['**/*.mdx'], + ...mdxPlugin.flatCodeBlocks, + rules: { + ...mdxPlugin.flatCodeBlocks.rules, + 'no-var': 'error', + 'prefer-const': 'error', + }, + }, +] + +export default [ + ...eslintConfig, + ...ignoresConfig, + ...tseslintConfig, + ...nextConfig, + ...mdxConfig, +] satisfies FlatConfig.Config[] +``` + +Line 12: we import the eslint plugin mdx + +Lines 130 to 153: we create a mdxConfig (config array) with two configurations one for mdx content and a second config for the content of markdown codeblocks (which you might have in your mdx documents) + +Line 139: Because off performance problems (linting process was very long) I decided to disable linting of codeblocks content by setting the **lintCodeBlocks** option in the mdx config to **false**. I left the second config MDX config (for codeblocks content) unchanged as we might use it in the future with a faster parser. If you want to lint the content of codeblocks, make sure to set the **lintCodeBlocks** option to **true** + +Line 160 we add the **mdxConfig** to the config export + +Side note: eslint-mdx already had a [support flat config PR #468](https://github.com/mdx-js/eslint-mdx/pull/468) merged in Aug. 2023 😮 + +### Markdown linting using remark-lint + +Now that we have added MDX ESLint plugin to our Next.js 15 project, we can easily add the [remark-lint](https://github.com/remarkjs/remark-lint/) plugin to our setup, all we need to do is create a configuration file and import a few recommend rule sets for markdown linting + +First we need to add few more **remark-lint** dependencies, these **remark-lint** presets will each add different rules (plugins) to our markdown linting setup: + +* [consistent](https://www.npmjs.com/package/remark-preset-lint-consistent) +* [recommended](https://www.npmjs.com/package/remark-preset-lint-recommended) +* [markdown style guide](https://www.npmjs.com/package/remark-preset-lint-markdown-style-guide) + +By installing them using the following command: + +```shell +npm i remark-preset-lint-recommended@latest remark-preset-lint-consistent@latest remark-preset-lint-markdown-style-guide@latest --save-dev --save-exact +``` + +Then we create a remark linting configuration file, with the following content: + +```js title=".remarkrc.mjs" +// presets imports +import remarkPresetLintRecommended from 'remark-preset-lint-recommended' +import remarkPresetLintConsistent from 'remark-preset-lint-consistent' +import remarkPresetLintMarkdownStyleGuide from 'remark-preset-lint-markdown-style-guide' + +// rules imports +import remarkLintMaximumHeadingLength from 'remark-lint-maximum-heading-length' +import remarkLintUnorderedListMarkerStyle from 'remark-lint-unordered-list-marker-style' +import remarkLintNoUndefinedReferences from 'remark-lint-no-undefined-references' +import remarkLintLinkTitleStyle from 'remark-lint-link-title-style' +import remarkLintMaximumLineLength from 'remark-lint-maximum-line-length' +import remarkLintListItemSpacing from 'remark-lint-list-item-spacing' + +const config = { + plugins: [ + // presets + remarkPresetLintRecommended, + remarkPresetLintConsistent, + remarkPresetLintMarkdownStyleGuide, + // rules + // https://www.npmjs.com/package/remark-lint-maximum-heading-length + [remarkLintMaximumHeadingLength, [1, 100]], + // https://www.npmjs.com/package/remark-lint-unordered-list-marker-style + [remarkLintUnorderedListMarkerStyle, 'consistent'], + // https://www.npmjs.com/package/remark-lint-no-undefined-references + [remarkLintNoUndefinedReferences, { allow: ['!NOTE', '!TIP', '!IMPORTANT', '!WARNING', '!CAUTION', ' ', 'x'] }], + // https://www.npmjs.com/package/remark-lint-link-title-style + [remarkLintLinkTitleStyle, '\''], + // https://www.npmjs.com/package/remark-lint-maximum-line-length + [remarkLintMaximumLineLength, false], + // https://www.npmjs.com/package/remark-lint-list-item-spacing + [remarkLintListItemSpacing, false], + ] +} + +export default config +``` + +The first 3 imports are presets with recommended rules, we then use them in the **plugins** config + +The other imports are single rules we want to configure, we then use them in the **plugins** config, the configuration for each rule consists of an array where the first value is the rule and the second value is the configuration we want to apply, here is an explanation for the ones I suggest adding (but feel free to configure them differently, to match your use case): + +* 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** will check if you are using [undefined references](https://github.com/remarkjs/remark-lint/tree/main/packages/remark-lint-no-undefined-references), we add `!NOTE` and a few more to the allow list as those are exceptions to the rule (those get used by the plugin that we will add in the [rehype-github-alerts](/web_development/tutorials/next-js-static-first-mdx-starterkit/github-like-alerts-plugin) page, and the last two are for the [remark-gfm task lists](/web_development/tutorials/next-js-static-first-mdx-starterkit/github-flavored-markdown-plugin#remark-gfm-tasklists)) +* 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) + +> [!NOTE] +> Each remark lint presets package we just installed has installed a bunch of rules (packages) for us; each preset has a readme that list the rules they support: +> +> * ["recommended preset" readme](https://github.com/remarkjs/remark-lint/tree/main/packages/remark-preset-lint-recommended#plugins) +> * ["consistent preset" readme](https://github.com/remarkjs/remark-lint/tree/main/packages/remark-preset-lint-consistent#plugins) +> * ["markdown style guide preset" readme](https://github.com/remarkjs/remark-lint/tree/main/packages/remark-preset-lint-markdown-style-guide#plugins) + +And then we are already done, because the MDX plugin in our ESLint setup will automatically detect that we have a remark-lint configuration file. The potential warnings and errors from remark-lint will now get displayed along other ESLint messages every time you use the linting command (`npm run lint`) + +> [!MORE] +> ["remark-lint" repository](https://github.com/remarkjs/remark-lint/) +> [list of official remark link rules and presets](https://github.com/remarkjs/remark-lint/tree/main/packages) + +## Testing our new lint command + +After all the coding let's finally make a linting test, by using the following command in our terminal: + +```shell +npm run lint +``` + +You probably notice that the linting command will output some errors and warnings in the terminal, those are files that got automatically created by the CNA or the sentry wizard (which we used earlier), but this is a good as it we can now also test our lint-fix command, that should automatically fix those errors for us + +To fix the linting errors, use the following command: + +```shell +npm run lint-fix +``` + +And then run the lint command one more time and you should have no more warnings and errors as they got all fixed: + +```shell +npm run lint +``` + +We had to do a lot of changes to get this new linting setup up and running, but I hope you agree with me that it was worth it, we have greatly improved our DX by automating both the linting of code and content, which will ensure we write cleaner code and ensure our markdown content is well formatted + +## 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 comment won't work:" +{/* 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 title="eslint disable for remark lint rules:" +{/* eslint-disable-next-line mdx/remark */} +> [Info - 8:41:03 PM] ESLint server is running. +``` + +## Clearing the ESLint cache + +Because by default, we use a cache to speed up the linting process when using the `npm run lint` command, we need to delete the cache manually after making changes to the `.eslintrc.js` ESLint configuration file or the `.remarkrc.mjs` remark-lint configuration file + +To delete the cache manually, open the `.next` folder in the root of your project, then go into the `cache` folder and finally delete the `eslint` file + +> [!TIP] +> If you do a lot of tests and don't want to delete the cache manually after every change, use the `npm run lint-nocache` instead, until you are done testing, then delete the cache once and use the regular `npm run lint command` one last time to do final test + +Congratulations 🎉 you just added linting for all your MDX (markdown) content in your project's MDX pages + + + + + +
diff --git a/app/web_development/tutorials/next-js-static-first-mdx-starterkit/first-mdx-page-and-understanding-static-rendering/page.mdx b/app/web_development/tutorials/next-js-static-first-mdx-starterkit/first-mdx-page-and-understanding-static-rendering/page.mdx new file mode 100644 index 00000000..db4bc6f9 --- /dev/null +++ b/app/web_development/tutorials/next-js-static-first-mdx-starterkit/first-mdx-page-and-understanding-static-rendering/page.mdx @@ -0,0 +1,148 @@ +--- +title: First MDX page and understanding of static rendering - Next.js 15 Tutorial +description: First MDX page and understanding of static rendering - Next.js 15 static first MDX starterkit | Web development tutorials | www.chris.lu +keywords: ['static', 'rendering', 'SSG', 'first', 'MDX', 'page', 'next/mdx'] +published: 2024-12-31T23:00:00.000Z +modified: 2024-12-31T23:00:00.000Z +permalink: https://chris.lu/web_development/tutorials/next-js-static-first-mdx-starterkit/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' +import DonationsMessage from '@/shared/donations-message.mdx' + +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-js-static-first-mdx-starterkit/opengraph-image' + }], + url: frontmatter.permalink, + section: frontmatter.section, + publishedTime: frontmatter.published, + modifiedTime: frontmatter.modified, + tags: frontmatter.keywords, + }, +} + +%toc% + +
+ + + +# First MDX page and understanding static rendering + +We have reached yet another milestone, now that we did [set up MDX support for Next.js 15](/web_development/tutorials/next-js-static-first-mdx-starterkit/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 (note that the name is in parenthesis (`()`), this is important, more about this in the next chapter) and then in it another `first_mdx_page` folder (this time no parenthesis) + +Then, inside of the `first_mdx_page` folder, add a `page.mdx` (note that we set **extension** to **mdx** and NOT tsx), and then paste the following content into it: + +```md title="/app/(tutorial_examples)/first_mdx_page/page.mdx" +# Hello 👋 with MDX! + +## headline 2nd level + +text in *italic* + +text in **bold** + +text in ***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/first_mdx_page` + +Congratulations 🎉 you added MDX support to your Next.js project and learned how to create MDX pages + +### Next.js route groups + +By adding parenthesis (`()`) around the `tutorial_examples` folder name we have created our first **route group**, the page in `/app/(tutorial_examples)/first_mdx_page/page.mdx` will be at the `http://localhost:3000/first_mdx_page` URL, as you can see **tutorial_examples** did NOT become a segment of the URL, that is because folders which use parenthesis (in their name) are called **route groups** + +The purpose of route groups is to allow us to better organize our files without impacting the structure of the URL + +By putting files into different folders the amount of files per folder can become more manageable without impacting the resulting URL + +> [!MORE] +> [Next.js "route groups" documentation](https://nextjs.org/docs/app/building-your-application/routing/route-groups) + +## Static rendering + +Putting our content into the first MDX page instead of fetching it from a database means we just used [Static Rendering](https://nextjs.org/docs/app/building-your-application/rendering/server-components#static-rendering-default) we made sure that our page can get generated at build time + +There are two ways to verify if our pages are static: + +The **first** option to check if a page is fully **static**, is a new feature that got added to Next.js 15. If your page is static then a round toast (badge) will appear at the bottom right of your page and if you hover it, it says "Static route" + +![Next.js "Static route" toast](../../../../../public/assets/images/app/web_development/tutorials/next-js-static-first-mdx-starterkit/nextjs_static_route_icon.png) + +The **second** option to check if a page is fully **static**, is to make a local build: + +```shell +npm run build +``` + +After the build is done, you will see that Next.js has printed the following info in our terminal: + +```shell +Route (app) Size First Load JS +┌ ○ / 307 B 187 kB +├ ○ /_not-found 1.02 kB 188 kB +└ ○ /first_mdx_page 307 B 187 kB + +○ (Static) prerendered as static content +``` + +The empty circle (`○`) in front of our `/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 what Next.js calls a [Dynamic API](https://nextjs.org/docs/app/building-your-application/rendering/server-components#dynamic-apis), 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 `npm run build` will produce 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/) or use a CDN to deliver your static content. + +Another feature you might be interested in is called [assetPrefix](https://nextjs.org/docs/app/api-reference/config/next-config-js/assetPrefix), the **assetPrefix** is a `next.config.mjs` configuration option that is useful when the images are NOT stored on the same domain (sub-domain) as the content itself, for example if your static project is at `www.example.com` but the images are at `cdn.example.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 + + + +> [!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-first-mdx-starterkit/first-typescript-page/page.mdx b/app/web_development/tutorials/next-js-static-first-mdx-starterkit/first-typescript-page/page.mdx new file mode 100644 index 00000000..8acd168b --- /dev/null +++ b/app/web_development/tutorials/next-js-static-first-mdx-starterkit/first-typescript-page/page.mdx @@ -0,0 +1,237 @@ +--- +title: First Typescript page - Next.js 15 Tutorial +description: First Typescript page - Next.js 15 static first MDX starterkit | Web development tutorials | www.chris.lu +keywords: ['typescript', 'nextjs', 'app', 'directory', 'page', 'route', 'layout', 'HMR', 'hot', 'module', 'reload'] +published: 2024-12-31T23:00:00.000Z +modified: 2024-12-31T23:00:00.000Z +permalink: https://chris.lu/web_development/tutorials/next-js-static-first-mdx-starterkit/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-js-static-first-mdx-starterkit/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 app vs pages router (optional) + +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 (as in this tutorial we will exclusively use the app router) + +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 (and add a page.tsx file inside of it) + +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 are the page files, with the **pages router**, the name of the file gets used as the last segment of a URL, in the **app router**, every page is always named **page**(.jsx|.tsx) and all directories will be a segment of the URL + +The advantage of using a folder as the last segment and having a convention that says that every page needs to be named page(.jsx|.tsx) is that with the introduction of the **app router** Next.js added a bunch of other [file conventions](https://nextjs.org/docs/app/getting-started/project-structure), 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 what the path is) + +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 if the URL is `www.example.com/articles/foo` then the slug have "foo" as value and for another URL `www.example.com/articles/bar` the slug would be a "bar", to retrieve the slug value you would create a page with the following code: + +```tsx title="/app/articles/[slug]/page.tsx" +export default async function Page({ + params, +}: { + params: Promise<{ slug: string }> +}) { + const slug = (await params).slug + return
My article slug is: {slug}
+} +``` + +Side note: dynamic pages now need to be async functions, due to params being promises starting with Next.js 15 ([Async Request APIs, Next.js 15 breaking change](https://nextjs.org/blog/next-15#async-request-apis-breaking-change)) + +Other useful features were introduced with the new router, like [route groups](https://nextjs.org/docs/app/building-your-application/routing/route-groups) which we will see in a future part of this tutorial. Another new feature are [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 + +If you haven't read my [The road to React 19 and Next.js 15](/web_development/posts/road-to-react-19-next-js-15) post yet but want to know more about the Next.js pages VS app router, then you might want to check out the ["pages VS app router"](/web_development/posts/road-to-react-19-next-js-15#pages-vs-app-router) chapter + +> [!MORE] +> [Next.js "routing" documentation](https://nextjs.org/docs/app/building-your-application/routing) +> [chris.lu "pages VS app router" post](/web_development/posts/road-to-react-19-next-js-15#pages-vs-app-router) + +## 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 15 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, next we will make sure the page actually works + +## Start the development 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/` + +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. This is because this layout file is called the **root layout** and it is **required** (and 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. + +### No root layout replacement for you Turbo users + +We just saw that when using the `npm run dev` command, Next.js will automatically create a root layout file, this is however only the case when you have removed the `--turbopack` flag from the dev script command (in our package.json) as we did in a previous [Typescript plugin and typed routes](/web_development/tutorials/next-js-static-first-mdx-starterkit/typescript-plugin-and-typed-routes) chapter + +If you use the `npm run dev-turbo` command (or if you prefer, use `npm run dev -- --turbopack`, note that use two hyphen (`--`) before the `--turbopack` flag, this is because we do NOT want to apply the flag to our dev script but instead want to pass the flag the to the next dev command inside of our dev script) and you do NOT have root Layout, then Next.js will **NOT** automatically create one for you 😔. + +After starting the development server **with turbopack**, if you then visit your localhost (`http://localhost:3000/`) in the browser, instead of getting a root layout file you will see the following error message: + +```md +Missing required html tags +The following tags are missing in the Root Layout: , . +Read more at https://nextjs.org/docs/messages/missing-root-layout-tags +``` + +The fix for Turbo users is to create the layout file you will see in the next chapter manually 😏. + +## Root layout content + +If you open the `/app/layout.tsx` file, you will see that on top, there is the following code: + +```tsx title="/app/layout.tsx (part 1)" +export const metadata = { + title: 'Next.js', + description: 'Generated by Next.js', +} +``` + +As you can see, Next.js 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 `{:html}` element, like the **title** and **description** (I will go more in detail in a future chapter) but also open graph tags + +The second part it has added is just a basic Next.js **RootLayout** function that adds basic HTML elements and the children prop that will contain the content of the pages that get wrapped by the layout: + +```tsx title="/app/layout.tsx (part 2)" +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 valid HTML document, but in future chapters we will add more code to make this layout file more useful + +> [!MORE] +> [Next.js "layouts and pages" documentation](https://nextjs.org/docs/app/getting-started/layouts-and-pages) + +## 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: + +```tsx title="/app/page.tsx" showLineNumbers /!/#special +export default function Home() { + + return ( + <> +

Hello World!

+ + ) + +} +``` + +> [!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 have another look at the rendered page using the `https://localhost:3000/` URL + +Even though you haven't reloaded the page manually, you will notice that your changes have been applied, which is because Next.js has a feature called fast refresh, this feature will watch your code base and each time a page gets rebuilt it will also refresh the page in the browser + +## Next.js Hot Module Reload (HMR) + +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/` 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 development 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-first-mdx-starterkit/frontmatter-plugin/page.mdx b/app/web_development/tutorials/next-js-static-first-mdx-starterkit/frontmatter-plugin/page.mdx new file mode 100644 index 00000000..c4233172 --- /dev/null +++ b/app/web_development/tutorials/next-js-static-first-mdx-starterkit/frontmatter-plugin/page.mdx @@ -0,0 +1,311 @@ +--- +title: Frontmatter plugin - Next.js 15 Tutorial +description: Frontmatter plugin - Next.js 15 static first MDX starterkit | Web development tutorials | www.chris.lu +keywords: ['Frontmatter', 'YAML', 'plugin', 'remark', 'mdx', 'nextjs frontmatter', 'next/mdx', 'nextjs'] +published: 2024-12-31T23:00:00.000Z +modified: 2024-12-31T23:00:00.000Z +permalink: https://chris.lu/web_development/tutorials/next-js-static-first-mdx-starterkit/frontmatter-plugin +section: Web development +--- + +import { sharedMetaDataArticle } from '@/shared/metadata-article' +import Breadcrumbs from '@/components/tutorial/Breadcrumbs' +import Pagination from '@/components/tutorial/Pagination' +import DonationsMessage from '@/shared/donations-message.mdx' + +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-js-static-first-mdx-starterkit/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 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 you are like me and 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. One convention that I follow, is to always put the frontmatter part on top of the MDX page (or top of the markdown document) + +> [!MORE] +> ["YAML data language" specification](https://yaml.org/spec/1.2.2/) +> [GitHub "About YAML frontmatter" documentation](https://docs.github.com/en/contributing/writing-for-github-docs/using-yaml-frontmatter) +> [Jekyll "Front Matter" documentation](https://jekyllrb.com/docs/front-matter/) + +## Frontmatter plugins installation + +What we will do in this chapter is **add 2 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 + +Use the following command to install the 2 remark frontmatter plugins: + +```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 {12-13} {54} /remarkFrontmatter, remarkMdxFrontmatter/#special +import { withSentryConfig } from '@sentry/nextjs'; +//import type { NextConfig } from 'next' +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' +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/*: string*/) => { + + 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', + }, + 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, + } + + const withMDX = createMdx({ + extension: /\.mdx$/, + options: { + // optional remark and rehype plugins + remarkPlugins: [remarkFrontmatter, remarkMdxFrontmatter, remarkGfm, [remarkTableOfContents, remarkTableOfContentsOptions]], + rehypePlugins: [rehypeGithubAlerts, rehypeSlug, [rehypePrettyCode, rehypePrettyCodeOptions], rehypeMDXImportMedia], + remarkRehypeOptions: { + footnoteLabel: 'Notes', + footnoteLabelTagName: 'span', + }, + }, + }) +``` + +Lines 12 to 13: we import the 2 frontmatter plugins + +Line 54: 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) + +## Frontmatter for metadata (and more) + +Now it is time to create an example where we define some frontmatter on top of our MDX pages. Then we let both plugins do their magic, and finally we can use the frontmatter variable to fill our Next.js 15 metadata object + +Let's reuse our **gfm playground** 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/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 block on top of the page with some custom variables that suit our needs + +Lines 10 to 21: we created a Next.js metadata object and used the frontmatter object (that holds all the key/value pairs from our frontmatter above) to populate the metadata object + +Finally, make sure the dev server is running, then open the playground page `http://localhost:3000/gfm_playground` in your browser and then right-click in the page to have a look at the meta tags inside of the `{:html}` 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: we have the **description** meta tag + +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 + +Line 10: we have the **open graph type** to **article**, 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:`, opengraph has a documentation page for the [https://ogp.me/#type_article](https://ogp.me/#type_article) about the open graph **article namespace**, it 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, then those two meta tags will disappear from the head element (even if you define them in your page source) + +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 + +## Frontmatter linting errors + +If you followed the remark lint tutorial part, you might have noticed that as soon as you add the frontmatter block to your MDX documents, remark-lint will start complaining about non valid content, if you hover with your mouse over the part that is underlined with a green wave, it will open a modal that shows you one of those linting problems: + +> Unexpected setext heading, expected ATX (remark-lint-heading-style) + +If you launch use the `npm run lint` linting command you will see even more: + +> Unexpected setext heading, expected ATX (remark-lint-heading-style) +> Unexpected `xxx` characters in heading, expected at most `100` characters (remark-lint-maximum-heading-length) +> Unexpected reference to undefined definition, expected corresponding definition (`'foo'`) for a link or escaped opening bracket (`\[`) for regular text (remark-lint-no-undefined-references) + +To solve this problem need to add frontmatter as a plugin to our **remark lint configuration** file (that is in root of the project): + +```js title=".remarkrc.mjs" showLineNumbers {16} {22} +// 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' + +// remark plugins +import remarkGfm from 'remark-gfm' +import remarkFrontmatter from 'remark-frontmatter' + +const config = { + plugins: [ + // first the plugins + remarkGfm, + remarkFrontmatter, + // then the presets + remarkPresetLintRecommended, + remarkPresetLintConsistent, + remarkPresetLintMarkdownStyleGuide, + // and finally the rules customizations + // 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], + // https://www.npmjs.com/package/remark-lint-no-literal-urls + // disable rule as we have gfm autolink support + [remarkLintNoLiteralUrls, false], + ] +} + +export default config +``` + +Line 16: we import the frontmatter plugin + +Line 22: we add the frontmatter plugin to the remark lint configuration + +If you now run the linting process again the errors should be gone (if they are not gone yet, then you might want to [clear the ESLint cache](/web_development/tutorials/next-js-static-first-mdx-starterkit/eslint-mdx-plugin-and-remark-lint#clearing-the-eslint-cache)) and they should also disappear in VSCode (you might have to [restart the ESLint server](/web_development/tutorials/next-js-static-first-mdx-starterkit/linting-in-vscode-using-extensions#restarting-the-eslint-server-in-vscode) in VSCode to update the linting process) + +Congratulations 🎉 you just learned how to set metadata for MDX pages and how to add frontmatter to MDX documents + + + + + +
diff --git a/app/web_development/tutorials/next-js-static-first-mdx-starterkit/github-flavored-markdown-plugin/page.mdx b/app/web_development/tutorials/next-js-static-first-mdx-starterkit/github-flavored-markdown-plugin/page.mdx new file mode 100644 index 00000000..f228340b --- /dev/null +++ b/app/web_development/tutorials/next-js-static-first-mdx-starterkit/github-flavored-markdown-plugin/page.mdx @@ -0,0 +1,706 @@ +--- +title: GitHub flavored markdown plugin - Next.js 15 Tutorial +description: GitHub flavored markdown plugin - Next.js 15 static first MDX starterkit | Web development tutorials | www.chris.lu +keywords: ['GitHub', 'flavored', 'plugin', 'gfm', 'markdown', 'mdx', 'remark-gfm', 'next/mdx', 'remark'] +published: 2024-12-31T23:00:00.000Z +modified: 2024-12-31T23:00:00.000Z +permalink: https://chris.lu/web_development/tutorials/next-js-static-first-mdx-starterkit/github-flavored-markdown-plugin +section: Web development +--- + +import { sharedMetaDataArticle } from '@/shared/metadata-article' +import Breadcrumbs from '@/components/tutorial/Breadcrumbs' +import Pagination from '@/components/tutorial/Pagination' +import DonationsMessage from '@/shared/donations-message.mdx' + +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-js-static-first-mdx-starterkit/opengraph-image' + }], + url: frontmatter.permalink, + section: frontmatter.section, + publishedTime: frontmatter.published, + modifiedTime: frontmatter.modified, + tags: frontmatter.keywords, + }, +} + +%toc% + +
+ + + +# GitHub flavored markdown + +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](https://github.com/rehypejs/rehype-github) + +There is the **remark-gfm plugin** which 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**, ... + +There is also the **rehype "GitHub" plugins repository**, which lists a lot of plugins you can use to match how GitHub transforms markdown to HTML. Some are remark plugins like [remark-github-yaml-metadata](https://github.com/rehypejs/rehype-github/tree/main/packages/yaml-metadata), which you can use to display the frontmatter of every MDX page as table, but most are rehype plugins, like [rehype-github-color](https://github.com/rehypejs/rehype-github/tree/main/packages/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](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**, some remark plugins on [npmjs](https://www.npmjs.com/search?q=callouts) call 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 Specification](https://github.github.com/gfm/) + +## Next.js 15 remark-gfm Rust alternative + +This is a reminder that earlier in this tutorial when we did the initial MDX setup, we saw that instead of using the default MDX compiler written in Javascript, we can use the [Next.js 15 MDX Rust compiler](/web_development/tutorials/next-js-static-first-mdx-starterkit/nextjs-mdx-setup#nextjs-15-mdx-rust-compiler) (experimental) + +If this is the only plugin you plan to use, then you could use the next.config **mdxRs** option and set the **mdxType** to **gfm** instead of installing and adding the **remark-gfm** (javascript) plugin to your configuration. This would enable the Rust Compiler for MDX and also add gfm flavored markdown support. Then you could enable the turbopack flag in the dev server command (by changing the `npm run dev` script in the **package.json** back to: `"dev": "next dev --turbopack"`) + +## GitHub flavored markdown (gfm) plugin + +By adding the [remark "GitHub Flavored Markdown" (GFM) plugin](https://www.npmjs.com/package/remark-gfm) to our Next.js 15 project, 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 if you want to know more about this plugin, then I recommend checking out their [remark-gfm](https://github.com/remarkjs/remark-gfm#install) repository, it has a README that has a well-written chapter about "what it is" and "what it does" as well as some examples and there you can also have a look at the previous releases list + +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 {10} {47-50} {56} /[remarkGfm, remarkGfmOptions]/#special +import { withSentryConfig } from '@sentry/nextjs'; +//import type { NextConfig } from 'next' +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' +import rehypeSlug from 'rehype-slug' +import { remarkTableOfContents } from 'remark-table-of-contents' +import remarkGfm from 'remark-gfm' + +const nextConfig = (phase/*: string*/) => { + + 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', + }, + 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: /\.mdx$/, + options: { + // optional remark and rehype plugins + remarkPlugins: [[remarkGfm, remarkGfmOptions], [remarkTableOfContents, remarkTableOfContentsOptions]], + rehypePlugins: [rehypeSlug, [rehypePrettyCode, rehypePrettyCodeOptions], rehypeMDXImportMedia], + }, + }) +``` + +Line 9: we import the **remark-gfm** plugin + +Lines 46 to 49: we add a configuration object for the plugin, we first add the options type information; then there are few things we can configure, as an example we will set the **singleTilde** option to false, what this option does is well explained in their [README](https://github.com/remarkjs/remark-gfm?tab=readme-ov-file#options): + +> 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 + +I personally always use two, so I don't need this feature (but if you prefer using one then don't set this option) + +Line 55: we add an array containing the **remarkGfm** plugin as well as the **remarkGfmOptions** options object (we just created) to the **rehypePlugins** array + +## GitHub flavored markdown (gfm) linting + +Now that we have added GitHub flavored markdown (gfm) support to our project we also want to make sure our **linting** supports gfm + +We will do this by adding the remark-gfm plugin to the remark-lint configuration: + +```js title=".remarkrc.mjs" showLineNumbers {13} {16} {21} {41} +// presets imports +import remarkPresetLintRecommended from 'remark-preset-lint-recommended' +import remarkPresetLintConsistent from 'remark-preset-lint-consistent' +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' +import remarkLintNoLiteralUrls from 'remark-lint-no-literal-urls' + +// remark plugins +import remarkGfm from 'remark-gfm' + +const config = { + plugins: [ + // first the plugins + remarkGfm, + // then the presets + remarkPresetLintRecommended, + remarkPresetLintConsistent, + remarkPresetLintMarkdownStyleGuide, + // and finally the rules customizations + // 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], + // https://www.npmjs.com/package/remark-lint-no-literal-urls + // disable rule as we have gfm autolink support + [remarkLintNoLiteralUrls, false], + ] +} + +export default config +``` + +Line 13: we import a rule which we will disable on line 41 + +Line 16: we import the gfm plugin + +Line 21: we add the gfm plugin to the linting config + +Line 41: we disable the [remark-lint-no-literal-urls](https://www.npmjs.com/package/remark-lint-no-literal-urls) rule as we now have **gfm autolink** support enabled + +This update will make sure that rules like [remark-lint-table-pipe-alignment](https://github.com/remarkjs/remark-lint/tree/main/packages/remark-lint-table-pipe-alignment) (which checks if your table cell dividers are aligned) get applied + +Another example is footnotes, as adding the remark-gfm plugin makes sure that footnotes get linted correctly, without it you would get warnings from the [remark-lint-no-undefined-references](https://github.com/remarkjs/remark-lint/tree/main/packages/remark-lint-no-undefined-references) rule + +If can now run the linting process (`npm run lint`) and should not get any warnings related to gfm + +> [!TIP] +> you might want to [clear the ESLint cache](/web_development/tutorials/next-js-static-first-mdx-starterkit/eslint-mdx-plugin-and-remark-lint#clearing-the-eslint-cache) to make sure the previous setup is not cached +> +> you should also NOT have any warnings in VSCode (if you do have gfm related warnings then you might have to [restart the ESLint server](/web_development/tutorials/next-js-static-first-mdx-starterkit/linting-in-vscode-using-extensions#restarting-the-eslint-server-in-vscode) in VSCode to update the linting process) + +## 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 + +## remark-gfm tasklists + +The remark-gfm tasklist feature is called tasklist for a reason + +What I mean by that is that the following syntax `[ ]` and `[x]` is NOT going to generate a checkbox: + +```md +[ ] a checkbox +``` + +What will get you a gfm tasklist, is if you use the exact syntax that remark-gfm expects, which is a **list** of tasks: + +```md +* [ ] a task +``` + +If you open the playground in the browser and inspect the HTML source, you will notice that by default 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 {18} {56-59} /InputPropsType/#special +import type { ComponentPropsWithoutRef } from 'react' +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 TocHighlight from '@/components/toc/Highlight' + +// 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. + +type ListPropsType = ComponentPropsWithoutRef<'ul'> +type AnchorPropsType = ComponentPropsWithoutRef<'a'> +// Note: ImageProps get imported from 'next/image' +type AsidePropsType = ComponentPropsWithoutRef<'aside'> +type InputPropsType = ComponentPropsWithoutRef<'input'> + +// 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 }: ListPropsType) => ( +
    + {children} +
+ ), + a: ({ children, href, ...props }: AnchorPropsType) => ( + + {children} + + ), + img: (props) => (), + aside: ({ children, ...props }: AsidePropsType) => { + const tocHighlightProps = { + headingsToObserve: 'h1, h2, h3', + rootMargin: '-5% 0px -50% 0px', + threshold: 1, + ...props + } + return ( + <> + {props.id === 'articleToc' ? ( + + {children} + + ) : ( + + )} + + ) + }, + input: (props: InputPropsType) => { + console.log(props) + return () + }, + ...components, + } +} +``` + +Line 18: we create a new **InputPropsType** + +Lines 56 to 59: we add type information to the input props, then we do a `console.log` to get an idea of what the props values are that we might get + +If we now have the dev server running and reload the `http://localhost:3000/gfm_playground` page in our browser, we will see that in the VSCode terminal our `console.log` will print the following few lines: + +```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{50} {50-55} +input: (props: InputPropsType) => { + console.log(props) + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { disabled, ...newProps } = props + return () +}, +``` + +Lines 50 to 55: we add a comment to disable the eslint `@typescript-eslint/no-unused-vars` for the next line, as we won't use the `disabled` variable; we then use a destructuring assignment to split the original props into the disabled attribute and the remaining props; 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 to 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 too + +However, if we remove the `disabled` attribute and start our dev server, we see that we get an error in the browser console: + +> 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`. + +So this message tells us that by removing `disabled` we have changed this checkbox from **read only** to a **mutable** checkbox. It tells us that we have NO `onChange` handler (which you should have if your checkbox is mutable) and that it is recommended to use `defaultChecked` to tell the checkbox if it should be checked or not when it gets rendered for the first time. + +### Checkbox react component + +In this chapter we will fix the warnings, by creating a custom React **checkbox 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) => { + + // 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 it 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, the new state value will be the opposite of the previous value (if was true its now false and vice versa) + +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 {8} {58} +import type { ComponentPropsWithoutRef } from 'react' +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 TocHighlight from '@/components/toc/Highlight' +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. + +type ListPropsType = ComponentPropsWithoutRef<'ul'> +type AnchorPropsType = ComponentPropsWithoutRef<'a'> +// Note: ImageProps get imported from 'next/image' +type AsidePropsType = ComponentPropsWithoutRef<'aside'> +type InputPropsType = ComponentPropsWithoutRef<'input'> + +// 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 }: ListPropsType) => ( +
    + {children} +
+ ), + a: ({ children, href, ...props }: AnchorPropsType) => ( + + {children} + + ), + img: (props) => (), + aside: ({ children, ...props }: AsidePropsType) => { + const tocHighlightProps = { + headingsToObserve: 'h1, h2, h3', + rootMargin: '-5% 0px -50% 0px', + threshold: 1, + ...props + } + return ( + <> + {props.id === 'articleToc' ? ( + + {children} + + ) : ( + + ) + } + + ) + }, + input: (props: InputPropsType) => props?.type === 'checkbox' ? () : (), + ...components, + } +} +``` + +Line 8: we import our checkbox component + +Line 58: we remove the previous code, then we add a function that checks if it the input field is of type checkbox, if it is we use our checkbox component and if it is NOT we just create a default input and pass the original props to it + +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 that uses a POST request to send some data to the server or add code that updates a value in the localstorage of the user's browser + +There is only problem though with this solution, the checkboxes have no **name** or **ID**, so you might want edit the HTML that gets rendered for a tasklist or switch to a completely different plugin that has more of the features you need. A very good read at this point is this ["HtmlExtension" chapter](https://github.com/micromark/micromark?tab=readme-ov-file#htmlextension) in the micromark readme ([micromark](https://github.com/micromark/micromark) is a commonmark parser used by @next/mdx) + +> [!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 {18-19} +
+ +~~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 18 to 19: we add an example for footnotes + +If you launch the dev server and then open the playground URL `http://localhost:3000/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/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 {58-61} /remarkRehypeOptions/#special +import { withSentryConfig } from '@sentry/nextjs'; +//import type { NextConfig } from 'next' +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' +import rehypeSlug from 'rehype-slug' +import { remarkTableOfContents } from 'remark-table-of-contents' +import remarkGfm from 'remark-gfm' + +const nextConfig = (phase/*: string*/) => { + + 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', + }, + 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: /\.mdx$/, + options: { + // optional remark and rehype plugins + remarkPlugins: [remarkGfm, [remarkTableOfContents, remarkTableOfContentsOptions]], + rehypePlugins: [rehypeSlug, [rehypePrettyCode, rehypePrettyCodeOptions], rehypeMDXImportMedia], + remarkRehypeOptions: { + footnoteLabel: 'Notes', + footnoteLabelTagName: 'span', + }, + }, + }) +``` + +Lines 58 to 61: we add the options for the footnotes to the **remarkRehypeOptions** object and NOT (as one might assume) to the **remarkGfmOptions** object (which is at lines 48 to 50) + +If you launch the dev server then open the playground URL `http://localhost:3000/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 `

{:html}`) to a `{:html}` + +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 + + + +> [!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-first-mdx-starterkit/github-like-alerts-plugin/page.mdx b/app/web_development/tutorials/next-js-static-first-mdx-starterkit/github-like-alerts-plugin/page.mdx new file mode 100644 index 00000000..d68be572 --- /dev/null +++ b/app/web_development/tutorials/next-js-static-first-mdx-starterkit/github-like-alerts-plugin/page.mdx @@ -0,0 +1,290 @@ +--- +title: GitHub-like alerts using the rehype-github-alerts plugin - Next.js 15 Tutorial +description: GitHub-like alerts (admonitions/callouts) using the rehype-github-alerts plugin - Next.js 15 static first MDX starterkit | Web development tutorials | www.chris.lu +keywords: ['GitHub', 'alerts', 'rehype', 'plugin', 'mdx', 'markdown', 'admonitions', 'callouts'] +published: 2024-12-31T23:00:00.000Z +modified: 2024-12-31T23:00:00.000Z +permalink: https://chris.lu/web_development/tutorials/next-js-static-first-mdx-starterkit/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' +import DonationsMessage from '@/shared/donations-message.mdx' + +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-js-static-first-mdx-starterkit/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 + +We are about to use 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 the alerts + +This plugin attempts to mimic the GitHub alerts by implementing the same features, it is however possible to configure some of the features, for example, add new custom types of alerts (the default GitHub alert types are **Note**, **Tip**, **Important**, **Warning**, and **Caution**). You can also customize the CSS, to change how the alerts 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 15 configuration file to add the plugin to our MDX setup: + +```js title="next.config.mjs" showLineNumbers {11} {53} /rehypeGithubAlerts/2#special +import { withSentryConfig } from '@sentry/nextjs'; +//import type { NextConfig } from 'next' +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' +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/*: string*/) => { + + 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', + }, + 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, + } + + const withMDX = createMdx({ + extension: /\.mdx$/, + options: { + // optional remark and rehype plugins + remarkPlugins: [remarkGfm, [remarkTableOfContents, remarkTableOfContentsOptions]], + rehypePlugins: [rehypeGithubAlerts, rehypeSlug, [rehypePrettyCode, rehypePrettyCodeOptions], rehypeMDXImportMedia], + remarkRehypeOptions: { + footnoteLabel: 'Notes', + footnoteLabelTagName: 'span', + }, + }, + }) +``` + +Line 10: we import the `rehype-github-alerts` plugin + +Line 57: 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 github alerts examples to our playground: + +```md title="/app/(tutorial_examples)/gfm_playground/page.mdx" showLineNumbers {22-35} {37}#special +
+ +
+ +~~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] + +> [!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. + +[^1]: This is the text of the note, [it can be a link too](https://www.example.com) + +
+ +
+ +``` + +Lines 22 to 35: We added an example note, a tip, an important alert, a warning and a caution, those are the 5 types of alerts that GitHub supports + +Line 37: we moved the footnote definition the bottom as it is mandatory that the footnote definition is placed after the last content (you will also get a [remark-lint-final-definition](https://github.com/remarkjs/remark-lint/tree/main/packages/remark-lint-final-definition) warning if you don't place the footnotes definitions last) + +> [!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"](/web_development/tutorials/next-js-static-first-mdx-starterkit/eslint-mdx-plugin-and-remark-lint#markdown-linting-using-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/gfm_playground` in your browser, you will see that alerts get rendered + +The alerts should be visible, they are not well aligned and all have the same color, which is why in the next chapter we will add some CSS to improve that + +### 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); +} +``` + +Have another look at `http://localhost:3000/gfm_playground`, to have a look at the updated alerts style + +The alerts should now use similar colors than those used by github (github has different colors for light and dark schemes, so I took those for light text on a darker background, but feel free to change the colors to whatever works well for your project) + +Congratulations 🎉 you have now added support for markdown alerts that are compatible with GitHub (flavored markdown) + + + +> [!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-first-mdx-starterkit/headings-id-plugin/page.mdx b/app/web_development/tutorials/next-js-static-first-mdx-starterkit/headings-id-plugin/page.mdx new file mode 100644 index 00000000..d1b8797a --- /dev/null +++ b/app/web_development/tutorials/next-js-static-first-mdx-starterkit/headings-id-plugin/page.mdx @@ -0,0 +1,181 @@ +--- +title: Rehype slug plugin to add IDs to headings - Next.js 15 Tutorial +description: Rehype slug plugin to add IDs to headings - Next.js 15 static first MDX starterkit | Web development tutorials | www.chris.lu +keywords: ['CI/CD', 'Vercel', 'build', 'Production', 'preview'] +published: 2024-12-31T23:00:00.000Z +modified: 2024-12-31T23:00:00.000Z +permalink: https://chris.lu/web_development/tutorials/next-js-static-first-mdx-starterkit/headings-id-plugin +section: Web development +--- + +import { sharedMetaDataArticle } from '@/shared/metadata-article' +import Breadcrumbs from '@/components/tutorial/Breadcrumbs' +import Pagination from '@/components/tutorial/Pagination' +import DonationsMessage from '@/shared/donations-message.mdx' + +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-js-static-first-mdx-starterkit/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) plugin does not do much on its own, but it does something very useful needed by other plugins, by automatically transforming the text of headings into heading IDs. It will ensure IDs are unique, if detects that two (or more) headings have the same text, then the plugin will add a number to the ID. + +It uses [github-slugger](https://github.com/Flet/github-slugger) under the hood, meaning the slugs will be similar to those GitHub produces. + +The heading IDs 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) + +## 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 15 configuration file and add it to our MDX setup: + +```js title="next.config.mjs" showLineNumbers {8} {39} /rehypeSlug/2#special +import { withSentryConfig } from '@sentry/nextjs'; +//import type { NextConfig } from 'next' +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' +import rehypeSlug from 'rehype-slug' + +const nextConfig = (phase/*: string*/) => { + + 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', + }, + 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: /\.mdx$/, + options: { + // optional remark and rehype plugins + remarkPlugins: [], + rehypePlugins: [rehypeSlug, [rehypePrettyCode, rehypePrettyCodeOptions], rehypeMDXImportMedia], + }, + }) +``` + +Line 8: we import the **rehypeSlug** plugin + +Line 39: 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: + +{/* spellchecker: disable */} +```md title="/app/(tutorial_examples)/toc_playground/page.mdx" +
+ +# headline level 1 + +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris tincidunt eros sed pellentesque rhoncus. In est ante, dictum id turpis id, rutrum pellentesque nisi. Morbi euismod velit lacinia metus rutrum, non bibendum urna rutrum. Nunc ac mauris ut sem mollis lacinia. Suspendisse cursus augue est, eu eleifend leo venenatis sit amet. Nullam id arcu vel lacus accumsan efficitur. Pellentesque sodales commodo odio, at tempus magna cursus non. Curabitur ex diam, bibendum ac quam in, efficitur luctus ex. Donec ultricies feugiat semper. Sed nec posuere leo. + +Cras ultrices nisi enim, nec aliquet tellus fermentum in. Sed imperdiet lorem nec elit dictum elementum. In sit amet rhoncus lorem. Quisque gravida dictum pharetra. Phasellus lacinia, dui ut faucibus volutpat, nisi purus mattis nunc, eu elementum dolor elit eget lorem. Phasellus sagittis auctor tellus nec commodo. Mauris tristique fringilla ligula ut iaculis. Nullam id condimentum dolor, ac fringilla lorem. + +Cras faucibus magna nec orci feugiat, a accumsan velit posuere. Nam volutpat consequat ornare. Phasellus gravida aliquam nisl quis commodo. Nunc consectetur enim eu ipsum dapibus, a aliquet justo egestas. Curabitur id ultricies odio. Suspendisse eget vehicula mauris, non fermentum diam. Fusce laoreet ullamcorper dignissim. + +## headline level 2 + +In hac habitasse platea dictumst. Morbi semper efficitur orci vitae vulputate. Duis mauris sapien, dignissim sed arcu sed, imperdiet finibus erat. Integer eget convallis tortor. In elementum eget urna vel congue. Donec sagittis ut justo nec maximus. Quisque vestibulum quam ut pellentesque vestibulum. Pellentesque sagittis lobortis libero, id laoreet odio mollis et. Etiam id nisl et magna pellentesque tincidunt quis id nulla. + +Nullam mattis mollis lacus id dapibus. Donec tincidunt magna ac eros pellentesque, eu elementum ipsum luctus. Vivamus tortor dui, varius ac accumsan id, ullamcorper facilisis felis. Etiam porttitor maximus semper. Integer sem ex, bibendum vel tempor sit amet, volutpat sollicitudin mauris. Interdum et malesuada fames ac ante ipsum primis in faucibus. Phasellus mattis lectus eget nisl porttitor, at ullamcorper neque hendrerit. Etiam posuere, purus sit amet mollis lacinia, lorem purus volutpat velit, a rutrum risus orci a orci. Vestibulum tincidunt massa id vulputate interdum. Cras tristique lacinia vestibulum. Sed nec tortor nibh. Duis rutrum, ligula at vulputate ullamcorper, neque urna lobortis enim, id scelerisque sapien ex non ipsum. Vivamus urna quam, volutpat vel urna at, elementum vulputate nibh. Nam ornare nunc nec lacus convallis fermentum. Pellentesque quam diam, lobortis vulputate ligula id, convallis sollicitudin mi. + +Fusce luctus mollis orci interdum venenatis. Cras volutpat nibh quis rhoncus porttitor. Fusce enim orci, ultricies ut dolor et, bibendum consequat sem. Quisque urna mi, congue ut tempus sed, bibendum id ante. Maecenas viverra risus in dolor faucibus, vel lacinia sapien viverra. Aenean porta dictum enim vel luctus. Suspendisse maximus consectetur enim a molestie. Etiam vehicula est eget porta molestie. Vestibulum augue turpis, aliquam eget suscipit nec, lacinia sit amet diam. Aenean rhoncus, enim sagittis fermentum pharetra, lectus urna tristique sapien, commodo efficitur arcu nibh non urna. Aenean tempor, leo a ultrices tincidunt, massa arcu facilisis purus, non fermentum quam nulla id nulla. + +### headline level 3 + +In in euismod massa, ut vulputate urna. Aliquam molestie lacus est, non interdum urna aliquam ut. Sed vel sagittis eros, ut elementum dui. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Nam nec tempus enim. Phasellus sollicitudin luctus justo, a imperdiet lectus commodo ut. Fusce facilisis justo nunc, in aliquet diam ornare nec. + +Donec tincidunt aliquam arcu in pharetra. Cras ut tincidunt est. Donec erat nulla, tempus et accumsan sit amet, malesuada nec mauris. Morbi ipsum dolor, auctor non sem sit amet, mattis mollis sapien. Nam at arcu venenatis, volutpat erat vitae, accumsan neque. Donec mattis, odio vel aliquam tincidunt, lorem ipsum cursus velit, ut porttitor sem urna non sapien. Curabitur interdum ligula odio, eu volutpat arcu aliquam a. Nulla a libero non mauris ornare tincidunt sed eget magna. + +Sed posuere eu elit vitae mollis. Nulla a leo finibus, faucibus justo id, pharetra nibh. Nunc ut blandit ligula. Fusce nibh risus, elementum a dictum sed, mattis vel turpis. Cras lectus sem, luctus at justo vel, hendrerit congue risus. Nullam suscipit ex quis ex laoreet rhoncus. Donec augue dui, sodales at lorem id, finibus dignissim libero. Suspendisse finibus mi id nibh rhoncus, ut accumsan velit sagittis. + +#### headline level 4 + +Morbi et tortor accumsan dolor rutrum rhoncus. Quisque faucibus tincidunt nulla, non faucibus purus suscipit non. Aliquam sed dignissim nisl. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. In tellus enim, tincidunt in ante at, commodo malesuada orci. Ut accumsan tempor sem, ut imperdiet nisi facilisis eu. Fusce eu mattis elit. Fusce a purus ac dolor venenatis tincidunt ut sed sem. Nam cursus eu leo et aliquet. In mattis sagittis felis, nec blandit justo eleifend at. Aenean consequat fringilla feugiat. + +Etiam lectus massa, aliquet congue eros at, dictum cursus lectus. Maecenas eu dapibus sapien, a dignissim lacus. Sed viverra et lacus porttitor porttitor. Suspendisse tincidunt augue ut cursus tempus. Nulla nec metus ultrices libero commodo faucibus et et nisi. Etiam ac vulputate neque, sit amet consequat tellus. Sed placerat urna a tristique placerat. Sed quis porta tellus, ac posuere augue. Sed tristique quam id dignissim euismod. Suspendisse posuere vel quam non euismod. In molestie varius fermentum. Aliquam ut efficitur ipsum. Cras eget nunc ut dolor aliquet porta nec in odio. Phasellus dapibus ligula eros, eleifend pulvinar metus vulputate et. In convallis ornare mollis. Morbi sit amet placerat dui. + +Duis cursus suscipit lorem consectetur imperdiet. Cras nec luctus odio. Pellentesque dapibus nunc et facilisis pellentesque. Donec augue massa, aliquam quis ornare placerat, facilisis in lacus. In at maximus turpis. Cras sed metus vel orci ultricies consequat ut vitae tortor. Morbi sed pretium eros, a rhoncus augue. Ut quis finibus massa. Suspendisse placerat nisl id congue molestie. Pellentesque in ipsum mi. + +
+ +``` +{/* spellchecker: enable */} + +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 `
{:html}` 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/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 + + + +> [!MORE] +> [npmjs.com "rehype-slug" page](https://www.npmjs.com/package/rehype-slug) + + + +
diff --git a/app/web_development/tutorials/next-js-static-first-mdx-starterkit/introduction/page.mdx b/app/web_development/tutorials/next-js-static-first-mdx-starterkit/introduction/page.mdx new file mode 100644 index 00000000..e7530d1f --- /dev/null +++ b/app/web_development/tutorials/next-js-static-first-mdx-starterkit/introduction/page.mdx @@ -0,0 +1,136 @@ +--- +title: Introduction - Next.js 15 static first MDX starterkit +description: Introduction - Next.js static MDX blog | Web development tutorials | www.chris.lu +keywords: ['Introduction', 'tsx', 'mdx', 'static', 'pages', 'developer', 'blog', 'nextjs', 'Next.js', 'developer profile', 'markdown'] +published: 2024-12-31T23:00:00.000Z +modified: 2024-12-31T23:00:00.000Z +permalink: https://chris.lu/web_development/tutorials/next-js-static-first-mdx-starterkit/introduction +section: Web development +--- + +import { sharedMetaDataArticle } from '@/shared/metadata-article' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { faBug, faComments } from '@fortawesome/free-solid-svg-icons' +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-js-static-first-mdx-starterkit/opengraph-image' + }], + url: frontmatter.permalink, + section: frontmatter.section, + publishedTime: frontmatter.published, + modifiedTime: frontmatter.modified, + tags: frontmatter.keywords, + }, +} + +%toc% + +
+ + + +# Introduction + +The tutorial is about adding **MDX support** to a **Next.js 15** (and **React 19**) project because MDX pages can be great to create a **developer portfolio**, a **documentation website**, a **blog** and a lot more. To achieve this we will add several features through **MDX (remark / rehype) plugins**, like **code highlighting**, automatic **table of contents** generation, **github flavored markdown** and **alerts**, **frontmatter**, and a few more. But the tutorial goes beyond adding MDX support, as it contains several chapters about, improving security (for example by adding a **content security policy (CSP)** header), speed optimizations that will **improve** your **web vital metrics**. + +The tutorial is also about creating a solid **static first** core, where we use static site generation (SSG) to generate our pages at **build time**, to avoid Server Side Rendering (SSR) at **run time** as much as possible. Pre-rendering (parts of) your pages at built time (when deploying), will minimize the amount of SSR and lead to decreased loading times as well as decreased load on your servers. Beyond this tutorial, I recommend looking into Next.js 15 features like **Partial Pre-rendering (PPR)** (experimental) and **component streaming** (in Next.js 15 using React 19 suspend) to add in dynamic parts, but also **Incremental Static Regeneration (ISR)**. + +We will also make sure the project includes features that hopefully increase your (our teams) **developer experience (DX)**, like using the **Typescript** (VSCode autocomplete, type information), by setting up linting support using **ESLint v9** and **flat config** files (with no RC compatibility mode), by adding **extensions** to **VSCode** to improve linting while you code or write markdown (in MDX files). We will use Vercel for our deployment (CI/CD and hosting) needs (feel to replace that part with a deployment on GitHub pages (setting Next.js "output" configuration option to "export") or you could self host your project (Next.js 15 comes with some improvements for self hosting). We will also add **error logging** and **CSP violations logging** using Sentry.io (again if you prefer to use another service, you can swap that part out and use your preferred solution instead). + +> [!NOTE] +> I tried to make this tutorial as **beginner-friendly** as possible, but also complete enough to be helpful for **senior developers** + +This tutorial is partially based on my experience building chris.lu (the source code of my blog be found in my [chris.lu repository](https://github.com/chrisweb/chris.lu) on GitHub). I mention this because it allowed me to test the performance in production (with real visitors from all around the world). So hopefully after reading this tutorial you will be able to build your own and get a great lighthouse score, too: + +![lighthouse analysis results showing a 100% score for performance, accessibility, best practices, SEO](../../../../../public/assets/images/app/web_development/tutorials/next-js-static-first-mdx-starterkit/lighthouse.png) + +## Contributing & Questions + +Any contributions are always welcome 🙂. If you have a question feel free to ask on the [discussion](https://github.com/chrisweb/chris.lu/discussions) page and if you find a bug please report it using the [issues](https://github.com/chrisweb/chris.lu/issues) (PRs for any open Issue are welcome too, if unsure about your PR, ask in the ticket first). + +## Source code + +All the source code for this tutorial can be found in the [next-js-static-first-mdx-starterkit](https://github.com/chrisweb/next-js-static-first-mdx-starterkit_tutorial_chris.lu) repository on GitHub, every chapter of this tutorial is a separate branch in the repository (over time I will probably not keep each branch updated, but I will try to keep the main branch updated for a while and of course PRs are welcome 😉). + +## If you are migrating to Next.js 15 and React 19 + +After transitioning from the pages router to the app router in Next.js 14, yes we are now back at updating the core of our project because of changes in Next.js 15, but hey look at all the goodies we get 🤩. + +### migration steps + +1. if you are migrating a project that uses a previous version of Next.js, then you probably want to first create a new branch (or at least make sure you have no uncommitted changes) +1. then we need to update the version of our dependencies (in our package.json) + +Next.js 15 has a nice command that will check out your project and depending on its findings it will suggest codemods to use: + +```shell +npx @next/codemod@canary upgrade latest +``` + +Or if you prefer use a custom command, similar to this: + +```shell +npm i next@latest react@latest react-dom@latest --save-exact +``` + +and if you use Typescript, update the the React and React-dom types too: + +```shell +npm i @types/react@latest @types/react-dom@latest --save-exact --save-dev +``` + +Or yet another solution is to use the [versionlens for VSCode extension](https://marketplace.visualstudio.com/items?itemName=pflannery.vscode-versionlens), which is a great extension for quick version updates + +1. next we will use **Codemods** (automated code transformations) to speed up the transition from an older version to Next.js 15. Codemods are little helpers that can speed up the migration process by going through your existing code, finding places where you use a feature that needs to get updated and finally upgrade those parts of your code for you. One such example are cookies, which were previously synchronous functions but are now asynchronous functions (cookies are now in the "dynamic APIs" family). Having to await cookies is a breaking change that will require you to upgrade your code. The good news is that [Next.js 15 codemods](https://nextjs.org/docs/app/building-your-application/upgrading/codemods) can help with that. The the Next.js ["Upgrading: Version 15"](https://nextjs.org/docs/app/building-your-application/upgrading/version-15) documentation is worth reading too, as it has information that can potentially save you a lot of time during the migration to Next.js 15 +1. same for React 19, you will likely have to do some updates in your code, like converting React 18 components using forwardRef to React 19 components using the ref that is now in component props, but again to speed things up you might want to first give the codemods a try: [React 19 codemods](https://react.dev/blog/2024/04/25/react-19-upgrade-guide#codemods) and check out their official [React 19 upgrade guide](https://react.dev/blog/2024/04/25/react-19-upgrade-guide) +1. before starting to try out new features, getting your project back to building without errors, then adjust to changes like new [caching semantics](https://nextjs.org/blog/next-15#caching-semantics) +1. now you can start to add some of the new features like converting your Next.js configuration file to a typescript (next.config.ts) file, convert a regular React 18 form with an API request on submit to a React 19 version that uses server functions, or try TurboPack in development to decrease your dev build time + +> [!NOTE] +> regarding ESLint v9 and flat config files, this is something that is covered in the upcoming [ESLint setup page](/web_development/tutorials/next-js-static-first-mdx-starterkit/linting-setup-using-eslint)) + +> [!TIP] +> I have more details about the changes you can expect when upgrading to Next.js 15 and React 19 in my [The road to Next.js 15 and React 19](/web_development/posts/road-to-react-19-next-js-15) post. + +> [!MORE] +> [Next.js "Upgrading: Version 15" documentation](https://nextjs.org/docs/app/building-your-application/upgrading/version-15) +> [React.dev "React 19 upgrade" guide](https://react.dev/blog/2024/04/25/react-19-upgrade-guide) + +## Not all Next.js 15 (and Turbopack) features are stable + +There are a few features that I could NOT use in this tutorial or had to disable and I wanted you to be aware of it before we start with the actual tutorial + +* Converting the **next.config.js** file to a next.config.ts file: as I describe in the upcoming "Next.js 15 config" page there is [NO support for ESM only packages](/web_development/tutorials/next-js-static-first-mdx-starterkit/next-config#nextconfigts-does-not-support-esm-only-packages) (but it is coming) +* Using **Turbopack** in development: as we will see in a future chapter does NOT always work (don't get me wrong, I love Turbopack and plan on using it fully as soon as possible, but I also feel like we need to give Turbopack a bit more time to mature), one such case where Turbopack will fail is when [you also intend to enable typed routes](/web_development/tutorials/next-js-static-first-mdx-starterkit/typescript-plugin-and-typed-routes#turbopack-in-nextjs-15-does-not-yet-support-typed-routes), we will see another case when we start working on logging errors, we will use the Sentry SDK for Next.js, which works only without Turbopack as the [Sentry SDK is NOT compatible with Turbopack](/web_development/tutorials/next-js-static-first-mdx-starterkit/error-handling-and-logging#sentry-does-not-yet-support-turbopack) (just yet), then in Next.js v15.0 Turbopack has NO support for MDX (remark / rehype) plugins, in Next.js v15.1 they just added a new **experimental** loader 🎉 that allows you to use MDX plugins written in Javascript with Turbopack (which is written in Rust), more about this in the upcoming [MDX plugins page](/web_development/tutorials/next-js-static-first-mdx-starterkit/mdx-plugins#turbopack-support-for-mdx-remark--rehype-plugins) +* Support for ESLint **flat config** files: Next.js 15 supports ESLint flat config files, meaning you can create a flat config and Next.js will recognize it, but [Next.js packages are NOT using flat configs](/web_development/tutorials/next-js-static-first-mdx-starterkit/linting-setup-using-eslint#nextjs-15-needs-eslint-compatibility-mode-optional), which means you have to use the ESLint compatibility mode. But we are getting there, ~~CNA v15.0 for example still installs ESLint v8~~ this is fixed if you use CNA version >= 15.1 (CNA 15.1 now installs ESLint v9), so now everything is ESLint v9 compatible. Both Next.js ESLint packages (the config and the plugin) still use classic configuration files (RC) and NOT flat config, which means you need to use the FlatCompat mode if you want to use them "as is" in your flat config +* There are also features like **PPR** (which we will not use it in this tutorial) that work already well today but are still **experimental**, meaning you need to use a canary version and then enable the feature in the Next.js configuration. + +## Next.js 14 version of this tutorial + +If you don't plan on migrating just yet and still use Next.js 14 then know that I also have a [Next.js 14 version of this tutorial](/web_development/tutorials/next-js-static-mdx-blog) + +The Next.js 14 version covers the same topics but there some differences in the content due to the changes that happened between Next.js 14 and Next.js 15 + + + +
diff --git a/app/web_development/tutorials/next-js-static-first-mdx-starterkit/linting-in-vscode-using-extensions/page.mdx b/app/web_development/tutorials/next-js-static-first-mdx-starterkit/linting-in-vscode-using-extensions/page.mdx new file mode 100644 index 00000000..47be9a61 --- /dev/null +++ b/app/web_development/tutorials/next-js-static-first-mdx-starterkit/linting-in-vscode-using-extensions/page.mdx @@ -0,0 +1,369 @@ +--- +title: Linting in VSCode using ESLint and MDX extensions - Next.js 15 Tutorial +description: Linting in VSCode using ESLint and MDX extensions - Next.js 15 static first MDX starterkit | Web development tutorials | www.chris.lu +keywords: ['CI/CD', 'Vercel', 'build', 'Production', 'preview'] +published: 2024-12-31T23:00:00.000Z +modified: 2024-12-31T23:00:00.000Z +permalink: https://chris.lu/web_development/tutorials/next-js-static-first-mdx-starterkit/linting-in-vscode-using-extensions +section: Web development +--- + +import { sharedMetaDataArticle } from '@/shared/metadata-article' +import Breadcrumbs from '@/components/tutorial/Breadcrumbs' +import Pagination from '@/components/tutorial/Pagination' +import DonationsMessage from '@/shared/donations-message.mdx' + +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-js-static-first-mdx-starterkit/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 a command to [lint our markdown content as well as the code](/web_development/tutorials/next-js-static-first-mdx-starterkit/linting-setup-using-eslint) of our Next.js 15 project, but we have no linting in VSCode itself (yet) + +For that reason we are now going to install 2 **VSCode extensions** (one for ESLint and one for MDX) + +The first one will add linting messages directly into our code, and the second one will add MDX language support to VSCode + +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) + +### Adding the ESLint extension to VSCode + +If you need help installing the **ESLint** extension, have a look at my ["Installing extensions" chapter in the VSCode post](/web_development/posts/vscode#vscode-extensions-view) + +Open the extensions view, then search for an extension named **ESLint** (published my Microsoft) and then click on the install button + +### ESLint extension settings + +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 + +> [!WARN] +> As of now (nov. 2024) if you use typescript for you eslint.config file (so .ts and not .mjs), then the extension will return the following error: +> +> > Error: Could not find config file. +> +> To have the ESLint extension support typescript configuration files, you need to add the same flag we previously added to our linting command in the package.json (in the [Linting setup using ESLint](/web_development/tutorials/next-js-static-first-mdx-starterkit/linting-setup-using-eslint) page) +> +> The ESLint extension which flag it should add to the ESLint command, we need to edit the extension settings, an easy way to do this is to edit the settings file (in the .vscode folder): +> +> ```json title=".vscode/settings.json" +> "eslint.options": { +> "flags": ["unstable_ts_config"] +> } +> ``` + +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 title="/.vscode/settings.json" showLineNumbers {3-14} /,/1#special +{ + "typescript.tsdk": "node_modules\\typescript\\lib", + "eslint.debug": true, + "eslint.options": { + "flags": ["unstable_ts_config"] + }, + "eslint.validate": [ + "markdown", + "mdx", + "javascript", + "javascriptreact", + "typescript", + "typescriptreact" + ] +} +``` + +Add a comma at the end of the `typescript.tsdk` line that is already in the settings.json file, then add our ESLint extension settings below + +If you are using typescript ESLint config file (eslint.config.ts) then it is very important that you also add the flags options to unable the (currently still experimental) typescript support for ESLint configuration files + +> [!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), as this extension will add MDX language support to VSCode. Another great feature this extension offers is the support for remark-lint. + +If you want to know more about VSCode extensions when working with MDX and markdown, have a look at the [extensions chapter of the MDX post](/web_development/posts/mdx#vscode-extensions) + +> [!MORE] +> [VSCode "MDX (Language support for MDX)" extension](https://marketplace.visualstudio.com/items?itemName=unifiedjs.vscode-mdx) +> [chris.lu "VSCode markdown and MDX extension(s)" post](/web_development/posts/mdx#vscode-extensions) + +## 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 (optional) + +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 + +> [!NOTE] +> The next few chapters are to verify our linting setup and give you and idea what kind of warning and errors to expect from the linting tools, if you prefer you can skip them and continue with the chapter [Disabling rules using comments](#disabling-rules-using-comments), but I recommend checking them out at some point (it would be sad to have put all that effort in setting up linting, but then because of a small issue it fails to run, and then later when you discover the problem you get flooded all at once with a long list of errors and warnings) + +### 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 and a second error from jsx-a11y/alt-text because of the missing alt attribute (which you should always set but you can leave it empty if the image is decorative) +- 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: + +{/* spellchecker: disable */} +```shell +Unexpected duplicate toplevel heading, exected a single heading with rank `1` eslint(remark-lint-no-multiple-toplevel-headings) +``` + +(In case you wonder, yes I'm aware that there is a **typo** (exected instead of expected) in the above message, but as the typo is in the original error I decided to leave it unfixed) +{/* spellchecker: enable */} + +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** (which we added in our custom next (flat) config), 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 `{:html}` 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) (which we added in our custom next (flat) config) 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 another 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 React from 'react' + +interface PropsInterface { + clickCallback?: () => void +} + +const Button: React.FC = (props) => { + + const { clickCallback, ...rest } = props + + const foo = 'bar' + + const buttonClickHandler = (/*event: React.MouseEvent*/) => { + + if (typeof clickCallback === 'function') { + clickCallback() + } + + } + + return ( + <> + + + ) +} + +export default Button + +/* linting errors I should see in this test component +- line 13: 'foo' is assigned a value but never used. (@typescript-eslint/no-unused-vars) +- line 29: `'` can be escaped with `'`, `‘`, `'`, `’`. (react/no-unescaped-entities) +*/ +``` + +The first error ESLint will find is the foo constant we never used, the rule that spots this is [no-unused-vars](https://typescript-eslint.io/rules/no-unused-vars/) rule from the **typescript-eslint** plugin that tells us we have assigned a value to a constant but then we never use it + +The second error is because of the [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** that tells us to escape entities + +> [!NOTE] +> side note, errors are usually underlined with a red wavy line and warnings have a green one, the same is true for file names, if there is an error the filename will be red and if there are warnings it will be green, in the file explorer on the right you will also see a number next to the file name (on the right) that indicates the amount of errors and warnings in that file + +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 +``` + +> [!TIP] +> Also remember that a lot of errors can be fixed automatically, using the `npm run lint-fix` command would automatically fix the **missing final newline character** problem + +This should display all the warnings and errors we found in the two previous chapters, similar to this: + +{/* spellchecker: disable */} +```shell +PATH_TO_MY_PROJECT\tests\eslint\components\Button.tsx + 13:11 error 'foo' is assigned a value but never used @typescript-eslint/no-unused-vars + 29:18 error `'` can be escaped with `'`, `‘`, `'`, `’` react/no-unescaped-entities + +PATH_TO_MY_PROJECT\tests\eslint\content.mdx + 3:1 warning Unexpected duplicate toplevel heading, exected a single heading with rank `1` remark-lint-no-multiple-toplevel-headings + 5:1 warning Using `` could result in slower LCP and higher bandwidth. Consider using `` from `next/image` or a custom image loader to automatically optimize images. This may incur additional usage or cost from your provider. See: https://nextjs.org/docs/messages/no-img-element @next/next/no-img-element + 5:1 warning img elements must have an alt prop, either with meaningful text, or an empty string for decorative images jsx-a11y/alt-text + 10:175 warning Unexpected missing final newline character, expected line feed (`\n`) at end of file remark-lint-final-newline + +✖ 6 problems (2 errors, 4 warnings) + 0 errors and 1 warning potentially fixable with the `--fix` option. +``` +{/* spellchecker: enable */} + +> [!TIP] +> In VSCode if you hover over the errors and warnings you should see them getting underlined, this because you can press Ctrl and then click on then, which will open the file at the line where the error (or warning) got triggered, so that you can quickly fix the problem and then save the 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 **ignoresConfig** in our `eslint.config.ts` file, like this: + +```js title="eslint.config.ts" showLineNumbers {10-15} +const ignoresConfig = [ + { + name: 'custom/eslint/ignores', + // the ignores option needs to be in a separate configuration object + // replaces the .eslintignore file + ignores: [ + '.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/', + ] + }, +] as FlatConfig.Config[] +``` + +Lines 10 to 15: by adding 'tests/eslint/' to the **ignores** list, we ensure that the linting process will not lint those files when we do a deployment and hence prevent our build process from completing + +If later you want to run the tests (in development) again, you only need to comment this line out (and eventually [restart the ESLint server in VSCode](#restarting-the-eslint-server-in-vscode) for it to take effect) + +## 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 `{:html}` 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) + +Congratulations 🎉 you just added linting to your VSCode IDE using extensions, meaning you will now get linting messages as you code (instead of having to manually run the command from time to time or even worse have all the linting errors and warnings show up the moment you create a build to deploy) + + + + + +
diff --git a/app/web_development/tutorials/next-js-static-first-mdx-starterkit/linting-setup-using-eslint/page.mdx b/app/web_development/tutorials/next-js-static-first-mdx-starterkit/linting-setup-using-eslint/page.mdx new file mode 100644 index 00000000..58856e84 --- /dev/null +++ b/app/web_development/tutorials/next-js-static-first-mdx-starterkit/linting-setup-using-eslint/page.mdx @@ -0,0 +1,587 @@ +--- +title: Linting setup using ESLint 9 flat config - Next.js 15 Tutorial +description: Linting setup using ESLint - Next.js 15 static first MDX starterkit | Web development tutorials | www.chris.lu +keywords: ['Linting', 'ESLint', 'flat config', 'nextjs', 'mdx', 'plugin', 'parser'] +published: 2024-12-31T23:00:00.000Z +modified: 2024-12-31T23:00:00.000Z +permalink: https://chris.lu/web_development/tutorials/next-js-static-first-mdx-starterkit/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' +import DonationsMessage from '@/shared/donations-message.mdx' + +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-js-static-first-mdx-starterkit/opengraph-image' + }], + url: frontmatter.permalink, + section: frontmatter.section, + publishedTime: frontmatter.published, + modifiedTime: frontmatter.modified, + tags: frontmatter.keywords, + }, +} + +%toc% + +
+ + + +# Next.js 15 and ESLint v9 linting setup + +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 + +> [!NOTE] +> the 5 upcoming chapters only contain background information, if you prefer you can also go straight to coding part in the ["Custom ESLint 9 flat config for Next.js 15" chapter](#custom-eslint-9-flat-config-for-nextjs-15) + +## Linting library choice (optional) + +If you would prefer an alternative linting solution, I recommend checking out [Biome](https://biomejs.dev/), they have an interesting [playground](https://biomejs.dev/playground/) to learn more about biome (compared to Prettier), they also have a very good documentation, for example I like that their [Getting Started](https://biomejs.dev/guides/getting-started/) page will show you the commands for npm, yarn, pnpm, bun and deno + +## Why does Next.js have two packages related to ESLINT? (optional) + +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/api-reference/config/eslint#rules) or have a look at the [eslint-plugin-next rules directory on GitHub](https://github.com/vercel/next.js/tree/canary/packages/eslint-plugin-next/src/rules) + +> [!MORE] +> [Next.js "ESLint" documentation](https://nextjs.org/docs/app/api-reference/config/eslint) + +## The state of ESLint v9 and flat config (optional) + +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. + +You can still use **eslintrc** (classic) configuration files but it is recommended that you switch to the new [flat config files](https://eslint.org/docs/latest/use/configure/configuration-files) + +Most projects have added support for ESLint v9: + +* For example the eslint-plugin-react-hooks [announced on Oct. 2024](https://github.com/facebook/react/releases/tag/eslint-plugin-react-hooks%405.0.0) that released eslint-plugin-react-hooks v5.0.0 with support for ESLint v9 (this is one of the plugins Next.js was waiting for, before starting their own v9 migration) +* Another plugin that you see in a lot of projects is typescript-eslint, in May 13 2024 they announced in their [typescript-eslint Issue #8211](https://github.com/typescript-eslint/typescript-eslint/issues/8211) that the first alpha of [typescript-eslint v8 (with ESLint v9 support)](https://github.com/typescript-eslint/typescript-eslint/pull/9002) got merged + +If you are interested in the progress of more packages and 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) + +> [!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) + +## Next.js 15 ESLint v9 update (optional) + +In ESLint v9 the eslintrc files are **deprecated**, support for [**eslintrc** (classic) configuration files will be removed in ESLint version 10.0.0](https://eslint.org/blog/2023/10/flat-config-rollout-plans/#eslintrc-removed-in-eslint-v10.0.0) + +The [next 15 blog post](https://nextjs.org/blog/next-15#eslint-9-support) mentioned that: + +* you can now use ESLint v9 (or continue using v8) +* if Next.js detects that you are still using ESLint v8 they automatically set the `ESLINT_USE_FLAT_CONFIG=false` flag, which [enables support for flat config files in ESLint v8](https://eslint.org/docs/latest/use/configure/migration-guide#start-using-flat-config-files) + +Most of that work happened in the [PR #71218](https://github.com/vercel/next.js/pull/71218/files) + +> [!MORE] +> [ESLint "migrate to v9.x" documentation](https://eslint.org/docs/latest/use/migrate-to-9.0.0) + +### Next.js 15 needs ESLint compatibility mode (optional) + +If you use the latest **create-next-app** (CNA) it and chose to use ESLint then it will install ESLint v9 for you (in Next.js > 15.1, prior CNA versions did still install ESLint v8), or if you use npm run lint (next lint cli) and have no eslint configuration file, then next lint will also install ESLint v9 for you + +But no matter which one you used (CNA or next lint), both still create **eslintrc** files and NOT flat config files, so yes Next.js has updated their packages to support ESLint v9 but (as of Next.js v15.0) both won't create flat config files and Next.js ESLint plugin as well as the Next.js ESLint config packages have not migrated to [shareable flat config](https://eslint.org/docs/latest/extend/shareable-configs) files + +Both CNA and next lint installed the latest [eslint-config-next](https://github.com/vercel/next.js/tree/canary/packages/eslint-config-next), however the latest **eslint-config-next** is not compatible with flat config files, as we will see in one of the upcoming chapters, which does NOT mean you can NOT use eslint-config-next with flat configs, but it means that if you do you will need to add a compatibility layer for it wo work (which is what we will do in one of the upcoming chapters) + +## Custom ESLint 9 flat config for Next.js 15 + +First we are going to make sure we have the latest ESLint v9.x version installed by using the following command: + +```shell +npm i eslint@latest --save-dev --save-exact +``` + +In the [Project setup and Next.js 15 installation](/web_development/tutorials/next-js-static-first-mdx-starterkit/project-setup-using-CNA-and-first-commit) chapter we used **create-next-app** (CNA) which created an .eslintrc.json, which is a **classic** ESLint configuration file, in this chapter we want to convert that **classic** configuration file into a [**flat config** file](https://eslint.org/docs/latest/use/configure/configuration-files) + +At this point we can check out the [ESLint "Migrate Your Config File" documentation](https://eslint.org/docs/latest/use/configure/migration-guide) which tells us to use the [ESLint Configuration Migrator](https://eslint.org/blog/2024/05/eslint-configuration-migrator/) which is tool to help us convert eslintrc configuration files to Flat Config files + +Our eslintrc file currently looks like this: + +```json title=".eslintrc.json" +{ + "extends": [ + "next/core-web-vitals", + "next/typescript" + ] +} +``` + +To convert it we launch the ESLint Configuration Migrator, using the following command (depending on what extension you use for your eslintrc, if you do NOT use json then you need to adjust the file name in the command): + +```shell +npx @eslint/migrate-config .eslintrc.json +``` + +The configuration migrator will convert your `.eslintrc.json` classic configuration file into an `eslint.config.mjs` flat config file + +At the end of the update the configuration migrator will display a message, in which it recommends to now install the following dependencies, we follow through by executing the following command: + +```shell +npm install @eslint/js@latest @eslint/eslintrc@latest --save-dev --save-exact +``` + +The 2 dependencies we just installed are needed by the `eslint.config.mjs` file, as it will import and use code from both packages (**@eslint/js** and **@eslint/eslintrc**). The ESLint **js** package is the ESLint JavaScript Plugin and contains everything we need to parse Javascript, **eslintrc** adds a compatibility layer for packages that still use the classic configuration (eslintrc) format + +Now that we have converted our classic configuration to a flat configuration, let's open the `eslint.config.mjs` file and have a look at what is inside: + +```js title="eslint.config.mjs" +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import js from "@eslint/js"; +import { FlatCompat } from "@eslint/eslintrc"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const compat = new FlatCompat({ + baseDirectory: __dirname, + recommendedConfig: js.configs.recommended, + allConfig: js.configs.all +}); +export default [...compat.extends("next/core-web-vitals", "next/typescript")]; +``` + +We can see that the extends that was in our `.eslintrc.json` file got converted into the extends from the compatibility layer (which it has created using the `FlatCompat` class from the `@eslint/eslintrc` package) + +> [!TIP] +> If you are not creating a new project from scratch but instead have to upgrade a legacy ESLint setup, then I recommend checking out the [ESLint Compatibility Utilities](https://eslint.org/blog/2024/05/eslint-compatibility-utilities/) blog post from May 2024, it has some details about yet another package which has tools that can fix problems common problems that arise when using legacy rules and configurations + +One last thing, the migration script has NOT deleted our original eslintrc file, so I recommend deleting the `.eslintrc.json` (in the root of the project) manually, we won't need it anymore + +At this point you have a working ESLint setup using ESLint v9 and flat config files, you can use the following command to try it out: + +```shell +npm run lint +``` + +### Typescript ESLint configuration files + +As we converted our **next.config** file to a (`next.config.ts`) typescript file, to be consistent we will also convert our **eslint.config** to an `eslint.config.ts` file (but if you prefer to keep the eslint.config.mjs as is, then feel free to skip to the [ESLint debugging tools](#eslint-debugging-tools) chapter) + +First we rename our ESlint configuration file to `eslint.config.ts` (you can also use `eslint.config.mts` or `eslint.config.cts` if you prefer), in this tutorial I will use **eslint.config.ts** (to match how we named the **next.config.ts** file) + +Next we need to install additional [@types/eslint__eslintrc](https://www.npmjs.com/package/@types/eslint__eslintrc) types for the [@eslint/eslintrc](https://www.npmjs.com/package/@eslint/eslintrc) package: + +```shell +npm i @types/eslint__eslintrc@latest --save-dev --save-exact +``` + +Then we need to add the [jiti](https://www.npmjs.com/package/jiti) dependency (when using Node.js, Deno and Bun users don't need it) which is required to read typescript configuration files: + +```shell +npm i jiti@latest --save-dev --save-exact +``` + +> [!MORE] +> [ESLint "typescript configuration files" documentation](https://eslint.org/docs/latest/use/configure/configuration-files#typescript-configuration-files) + +#### Add types to the ESLint configuration file + +We edit the `eslint.config.ts` file and make the following few changes: + +```ts title="eslint.config.ts" showLineNumbers {5} {17} +import path from 'node:path' +import { fileURLToPath } from 'node:url' +import js from '@eslint/js' +import { FlatCompat } from '@eslint/eslintrc' +import type { Linter } from 'eslint' + +const __filename = fileURLToPath(import.meta.url) +const __dirname = path.dirname(__filename) +const compat = new FlatCompat({ + baseDirectory: __dirname, + recommendedConfig: js.configs.recommended, + allConfig: js.configs.all +}) + +export default [ + ...compat.extends('next/core-web-vitals', 'next/typescript'), +] satisfies Linter.Config[] +``` + +Line 5: we import the ESLint **Linter** type + +Line 17: we use the [satisfies operator](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-9.html#the-satisfies-operator) (that got introduced in TypeScript 4.9) to tell Typescript that our default export will be an array of **Linter.Config**, each **ESLint Linter Config** contains information about which files should get included or ignored, custom rules configurations, you can give it a name and some more which we will see more in detail in the upcoming chapters + +> [!NOTE] +> Because we added the types for `@eslint/eslintrc` package in the previous chapter we now have a typed FlatCompat (in case you want to check out what other options are available) + +#### Replacing next lint with eslint + +The typescript eslint configuration files are not supported by the Next.js 15.x lint cli (as of now), the file gets mentioned in the code as can be seen in the [**runLintCheck** git blame](https://github.com/vercel/next.js/blame/canary/packages/next/src/lib/eslint/runLintCheck.ts#L363) but there is no implementation (yet) + +If you have converted your ESLint configuration to an `eslint.config.ts` file and use the linting command `npm run lint` then `next lint` will not find our configuration file, and instead starts the wizard (which is supposed to guide us through setting up a new ESLint classic configuration) + +As we can't use next lint we need to create our own linting command: + +* as we can NOT use **next lint**, we will use the **eslint cli** instead +* we will also enable the [(experimental) typescript support](https://eslint.org/docs/latest/use/configure/configuration-files#typescript-configuration-files) for ESLint Flat Config files by adding an extra flag to our command (typescript support for configuration files is a feature that got added in [ESLint v9.9.0](https://eslint.org/blog/2024/08/eslint-v9.9.0-released/#experimental-typescript-configuration-files)) +* finally we will enable caching (as would next lint do) and we set the cache folder path to the same folder that Next.js uses + +```shell +npx eslint --flag unstable_ts_config --cache --cache-location .next/cache/eslint/ +``` + +Now that we have created a new command we can update our package.json (in the root of the project) to update our linting script(s): + +```json title="package.json" {6} +"scripts": { + "dev": "next dev", + "dev-turbo": "next dev --turbopack", + "build": "next build", + "start": "next start", + "lint": "eslint --flag unstable_ts_config --cache --cache-location .next/cache/eslint/" +}, +``` + +There is one more problem left, the **next build** script uses **next lint** by default, which will check if we have a Javascript eslint.config, but as we switched to a Typescript eslint file (which next lint does NOT yet support), we need to add our custom linting command to build script (we use the nocache version of our linting command, feel free to use the one with cache if that works better for your use case): + +```json title="package.json" {4} +"scripts": { + "dev": "next dev --turbopack", + "dev-turbo": "next dev --turbopack", + "build": "npm run lint && next build", + "start": "next start", + "lint": "eslint --flag unstable_ts_config --cache --cache-location .next/cache/eslint/" +}, +``` + +And now that we have updated the build script to use our custom linting command, we need to edit the Next.js configuration file and tell Next.js to NOT do its own linting during builds: + +```ts title="next.config.ts" showLineNumbers{43} +eslint: { + // we have added a lint command to the package.json build script + // which is why we disable the default next lint (during builds) here + ignoreDuringBuilds: true, +}, +``` + +> [!MORE] +> [Next.js "ignoreDuringBuilds" documentation](https://nextjs.org/docs/app/api-reference/config/next-config-js/eslint) + +### Adding even more scripts + +We are going to add two more scripts to our package.json and then document them in the readme + +We update our `package.json` file (in the root) and add the following scripts: + +```json title="package.json" showLineNumbers {6} {8-9} +"scripts": { + "dev": "next dev --turbopack", + "dev-turbo": "next dev --turbopack", + "build": "npm run lint && next build", + "start": "next start", + "next-lint": "next lint", + "lint": "eslint --flag unstable_ts_config --cache --cache-location .next/cache/eslint/", + "lint-nocache": "eslint --flag unstable_ts_config", + "lint-fix": "eslint --flag unstable_ts_config --fix" +}, +``` + +Line 6: we add a backup of the command that Next.js used + +Lines 8 and 9: we add a command for linting but without using the cache and a second command which will attempt to apply fixes automatically + +Then if you want you could update the README with these explanations: + +```md title="README.md" +`npm run next-lint`: a backup of the original next.js linting command +`npm run lint`: to manually use our (custom) linting command, it will scan our code and help us find problems in it (gets used by the build command before building) +`npm run lint-nocache`: same as **lint** command without cache, takes longer but can be useful when testing changes +`npm run lint-fix`: the **lint** command with the **fix** flag activated (to automatically fix errors and warnings if it can), you probably want to create a new branch before running this as it might produce a big quantity of changed files +``` + +## ESLint debugging tools + +Here are 3 tools I found while working on this ESLint tutorial (I wish I had found earlier 😉) that will improve your ESLint debugging experience greatly + +Our first 2 tools are cli commands: + +```shell +npx eslint --debug eslint.config.ts +``` + +This is a fantastic tool (cli command) that will output a lot of information that could help you while debugging, run the same command without a path to your configuration file and it will show you even more debugging information by performing a very very verbose output of the entire linting process (if you don't use a typescript eslint flat config then you need to adjust the name, for example to `npx eslint --debug eslint.config.mjs` for a javascript flat config) + +```shell +npx eslint --print-config eslint.config.ts +``` + +Another cli command that will print a json representation of what it found in your configuration file, which is a great tool to verify if everything you have set up is included + +The third tool is the one that helped me the most while writing this guide, the [ESLint Config Inspector](https://github.com/eslint/config-inspector) greatly improves the **(developer experience) DX** when working with the new flat config files by helping you visualize the structure and content of your eslint flat config: + +```shell +npx eslint --inspect-config eslint.config.ts +``` + +After running the command, the **ESLint Config Inspector** will launch a server on `http://localhost:7777/` (it should automatically use your default browser) + +The main page displays a nice overview of your files, ignores, options, plugins and rules, for each of your configurations + +On this overview page you see why it is a good idea to give your configurations a name 😉 + +Each configuration (row) is a disclosure widget, expand it and it will show you even more information about each of your configurations + +If you want you can add those commands to your package json scripts: + +```json title="package.json" showLineNumbers {10-12} +"scripts": { + "dev": "next dev --turbopack", + "dev-turbo": "next dev --turbopack", + "build": "npm run lint && next build", + "start": "next start", + "next-lint": "next lint", + "lint": "eslint --flag unstable_ts_config --cache --cache-location .next/cache/eslint/", + "lint-nocache": "eslint --flag unstable_ts_config", + "lint-fix": "eslint --flag unstable_ts_config --fix", + "lint-debug-config": "eslint --flag unstable_ts_config --debug eslint.config.ts", + "lint-print-config": "eslint --flag unstable_ts_config --print-config eslint.config.ts", + "lint-inspect-config": "eslint --flag unstable_ts_config --inspect-config eslint.config.ts" +}, +``` + +Next update your README accordingly: + +```md title="README.md" +`npm run lint-debug-config`: will print debugging information about what gets loaded by our ESLint config +`npm run lint-print-config`: print out a json representation of what is in our ESLint config +`npm run lint-inspect-config`: will open `http://localhost:7777/` in your browser, which is a tool to help you visualize the content of our ESLint config +``` + +Congratulations 🎉 you are now an ESLint debugger expert (and it's a good time to commit the latest changes) + +> [!MORE] +> [ESLint "Debug Your Configuration" documentation](https://eslint.org/docs/latest/use/configure/debug) +> [ESLint "Config Inspector" readme](https://github.com/eslint/config-inspector) + +## Language and Linter Options (optional) + +We could add global **language options** like the **ecmaVersion** or set a default **parser**, but in the upcoming chapters we will use shared configuration objects, which will set those options for us + +If however you want to learn more about the options that are available, then have a look at the [ESLint "configuration objects" documentation](https://eslint.org/docs/latest/use/configure/configuration-files#configuration-objects) + +> [!MORE] +> [ESLint "configuration objects" documentation](https://eslint.org/docs/latest/use/configure/configuration-files#configuration-objects) + +### Reporting unused "eslint-disable comments" (optional) + +When experimenting with ESLint and trying out setups, you might at some point add disable comments, but then later you decide to completely disable the rule using the ESLint rules configuration, in which case the disable comment becomes unused + +> [!WARN] +> If you are using the ESLint recommend rules, then ESLint will already add the **reportUnusedDisableDirectives** to the configuration + +A failsafe way to spot those unused comments is by activating the ESLint Linter option: + +```ts title="eslint.config.mjs" +{ + linterOptions: { + reportUnusedDisableDirectives: 'warn' + } +}, +``` + +> [!MORE] +> [ESLint "unused eslint-disable comments" documentation](https://eslint.org/docs/latest/use/configure/rules#report-unused-eslint-disable-comments) + +## Ignore folders + +In this chapter we will add an **ignores** config to exclude some folders from linting, so that we end up with a file that looks like this: + +```ts title="eslint.config.ts" showLineNumbers {15-26} {30} +import path from 'node:path' +import { fileURLToPath } from 'node:url' +import js from '@eslint/js' +import { FlatCompat } from '@eslint/eslintrc' +import type { Linter } from 'eslint' + +const __filename = fileURLToPath(import.meta.url) +const __dirname = path.dirname(__filename) +const compat = new FlatCompat({ + baseDirectory: __dirname, + recommendedConfig: js.configs.recommended, + allConfig: js.configs.all +}) + +const ignoresConfig = [ + { + name: 'custom/eslint/ignores', + // the ignores option needs to be in a separate configuration object + // replaces the .eslintignore file + ignores: [ + '.next/', + '.vscode/', + 'public/', + ] + }, +] as Linter.Config[] + +export default [ + ...compat.extends('next/core-web-vitals', 'next/typescript'), + ...ignoresConfig, +] satisfies Linter.Config[] +``` + +Line 15: we create a new **ignoresConfig** flat config + +Line 17: we give our config a name (we lousily follow the [ESLint Configuration Naming Conventions](https://eslint.org/docs/latest/use/configure/configuration-files#configuration-naming-conventions)), having a name will make it easier to find the config when using debugging tools (as we saw in the [ESLint debugging tools](#eslint-debugging-tools) chapter), feel free to for example replace "custom" in the name with the name of your project, that works well too + +Lines 20 to 24: we use the **ignores** option to tell ESLint which folders it should exclude from linting + +Line 30: we add the custom **ignoresConfig** to the default export + +> [!TIP] +> when the **ignores** option gets used in its own configuration block, then it acts as global ignores, meaning it tells ESLint to always exclude those files from linting +> +> however if the ignores is in a configuration block with other keys, then the ignores only applies to that configuration + +> [!MORE] +> [ESLint "ignores" documentation](https://eslint.org/docs/latest/use/configure/ignore) +> [ESLint "Report unused eslint-disable comments" documentation](https://eslint.org/docs/latest/use/configure/rules#report-unused-eslint-disable-comments) +> [ESLint "Configuration Naming Conventions" documentation](https://eslint.org/docs/latest/use/configure/configuration-files#configuration-naming-conventions) + +### Ignoring folders by using gitignore + +An alternative to having an ignores list in the ESLint configuration is to use the content of the `.gitignore` file to create an ESLint ignores list + +For this to work you need to install one more dependency [@eslint/compat](https://www.npmjs.com/package/@eslint/compat): + +```shell +npm i @eslint/compat@latest --save-dev --save-exact +``` + +Then we change the code of our `eslint.config.ts` to this: + +```ts title="eslint.config.ts" showLineNumbers {6} {15} {19} +import path from 'node:path' +import { fileURLToPath } from 'node:url' +import js from '@eslint/js' +import { FlatCompat } from '@eslint/eslintrc' +import type { Linter } from 'eslint' +import { includeIgnoreFile } from '@eslint/compat' + +const __filename = fileURLToPath(import.meta.url) +const __dirname = path.dirname(__filename) +const compat = new FlatCompat({ + baseDirectory: __dirname, + recommendedConfig: js.configs.recommended, + allConfig: js.configs.all +}) +const gitignorePath = path.resolve(__dirname, '.gitignore') + +export default [ + ...compat.extends('next/core-web-vitals', 'next/typescript'), + includeIgnoreFile(gitignorePath), +] satisfies Linter.Config[] +``` + +Line 6: we import the **includeIgnoreFile** function from ESLint compat package + +Line 15: we set the path to our gitignore file, which is at root of our project + +Line 19: we use the **includeIgnoreFile** function, which will scan the gitignore file content and turn it into a list of files and folders ESLint will **ignore** + +## ESLint recommended + +The second custom config that we are going to add is an ESLint custom config, which help us get rid of some FlatCompat options: + +```ts title="eslint.config.ts" showLineNumbers {1} {5} {7-13} {30} +import eslintPlugin from '@eslint/js' +import { FlatCompat } from '@eslint/eslintrc' +import type { Linter } from 'eslint' + +const compat = new FlatCompat() + +const eslintConfig = [ + { + name: 'custom/eslint/recommended', + files: ['**/*.ts?(x)'], + ...eslintPlugin.configs.recommended, + }, +] + +const ignoresConfig = [ + { + name: 'custom/eslint/ignores', + // the ignores option needs to be in a separate configuration object + // replaces the .eslintignore file + ignores: [ + '.next/', + '.vscode/', + 'public/', + ] + }, +] as Linter.Config[] + +export default [ + ...compat.extends('next/core-web-vitals', 'next/typescript'), + ...eslintConfig, + ...ignoresConfig, +] satisfies Linter.Config[] +``` + +Line 1: we renamed the **@eslint/js** import from **js** to **eslintPlugin** for consistency (with the next plugin imports we will add soon), we also removed the two Node.js modules (path & url) as we will NOT need them anymore + +Line 5: we remove the **FlatCompat** options as the custom eslint config (that we are adding next) will take care of adding the recommended ESLint rules for Javascript (and Typescript) files + +Lines 7 to 13: we create a custom ESLint configuration to apply a recommended set of linting rules to our Javascript (and Typescript files): + +* we add a name to our config +* we set the files option to `'**/*.ts?(x)'` because now that converted the last 2 .mjs files (eslint.config.mjs and next.config.mjs) to typescript we don't need to include the .mjs extension anymore (if your project still uses .mjs files then change the files option to `'**/*.ts?(x)', '**/*.mjs'`) + +Line 30: we add the custom **eslintConfig** to the default export + +> [!NOTE] +> we moved the usage of ESLint recommended from the **FlatCompat** options and moved it to a custom **flat config** (custom/eslint/recommended), because it allows us to use the flat config **files option**, to make sure that these rules will only get applied to Javascript (and Typescript) files and NOT to other files, like the MDX files we are about to create +> +> it is also more future proof, as in an upcoming chapter we will get rid of the compat mode completely + +Congratulations 🎉 you just transitioned from classic (RC) configuration files to flat config and we converted our Next.js configuration file into a typescript file (next.config.ts), on the next page we will extend our setup by adding linting for Typescript files and also + + + + + +
diff --git a/app/web_development/tutorials/next-js-static-first-mdx-starterkit/mdx-components-file/page.mdx b/app/web_development/tutorials/next-js-static-first-mdx-starterkit/mdx-components-file/page.mdx new file mode 100644 index 00000000..c0fecbc9 --- /dev/null +++ b/app/web_development/tutorials/next-js-static-first-mdx-starterkit/mdx-components-file/page.mdx @@ -0,0 +1,140 @@ +--- +title: The mdx-components file - Next.js 15 Tutorial +description: The mdx-components file - Next.js 15 static first MDX starterkit | Web development tutorials | www.chris.lu +keywords: ['CI/CD', 'Vercel', 'build', 'Production', 'preview'] +published: 2024-12-31T23:00:00.000Z +modified: 2024-12-31T23:00:00.000Z +permalink: https://chris.lu/web_development/tutorials/next-js-static-first-mdx-starterkit/mdx-components-file +section: Web development +--- + +import { sharedMetaDataArticle } from '@/shared/metadata-article' +import Breadcrumbs from '@/components/tutorial/Breadcrumbs' +import Pagination from '@/components/tutorial/Pagination' +import DonationsMessage from '@/shared/donations-message.mdx' + +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-js-static-first-mdx-starterkit/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 one of the next pages, 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 of our Next.js 15 setup), as the file is required to make MDX work, but we did not add any components yet + +Using this file you can do some of the things a plugin would do, but without having to install an additional package, for example, `mdx-components.tsx` is great if we want to quickly and easily replace an HTML Element with a React component + +Or we can use it to quickly add a CSS class to HTML elements, for example, add a class to all our `
    {:html}` [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 to a markdown element, in a similar way that we add a class to an HTML element, the second difficulty with ul lists, is that the `
      {:html}` element itself 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, which then could create parsing errors on that platform + +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 the next part of this tutorial, we will create a custom link component, which we will only need to import once into the **mdx-components** file, but it will then transform every link in every page of our project. + +In the next chapter we will use the next/mdx `mdx-components.tsx` file to add a class to all of our lists without using an extra plugin, without importing a component in every page, and without having to introduce any new markdown syntax + +> [!MORE] +> [mdxjs.com "components" documentation](https://mdxjs.com/table-of-components/) +> [Next.js "Add an mdx-components.tsx file" documentation](https://nextjs.org/docs/app/building-your-application/configuring/mdx#add-an-mdx-componentstsx-file) + +## 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 (replace the commented out example), like so: + +```tsx title="mdx-components.tsx" showLineNumbers {1} {9} {15-19} +import type { ComponentPropsWithoutRef } from 'react' +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. + +type ListPropsType = ComponentPropsWithoutRef<'ul'> + +// 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 }: ListPropsType) => ( +
        + {children} +
      + ), + ...components, + } +} +``` + +Line 1: we import the ComponentPropsWithoutRef type from React, this will allow us to add types for the props of each MDX component (which are React components with NO ref) + +Line 9: we create a props type for components, we use the **ComponentPropsWithoutRef type** from react and as it is a **generic** type that we set to a list (`'ul'`) **element type** + +Lines 15 to 19: we create a simple custom component for **ul lists** and we used the **ListPropsType** to add type information for the list component props + +When you use mdx-components, then the markdown parser will first turn the markdown list into a list (`
        `) component, we then use the mdx-components file to further transform the list component by adding a custom `listContainer` CSS class + +## 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/mdx-components_playground`, then use your browser developer tools inspect tool and you will see that the `
          {:html}` 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 + + + + + +
diff --git a/app/web_development/tutorials/next-js-static-first-mdx-starterkit/mdx-plugins/page.mdx b/app/web_development/tutorials/next-js-static-first-mdx-starterkit/mdx-plugins/page.mdx new file mode 100644 index 00000000..7db7e200 --- /dev/null +++ b/app/web_development/tutorials/next-js-static-first-mdx-starterkit/mdx-plugins/page.mdx @@ -0,0 +1,396 @@ +--- +title: MDX (remark/rehype) plugins - Next.js 15 Tutorial +description: MDX (remark/rehype) plugins - Next.js 15 static first MDX starterkit | Web development tutorials | www.chris.lu +keywords: ['MDX', 'remark', 'rehype', 'plugin', 'markdown', 'nextjs'] +published: 2024-12-31T23:00:00.000Z +modified: 2024-12-31T23:00:00.000Z +permalink: https://chris.lu/web_development/tutorials/next-js-static-first-mdx-starterkit/mdx-plugins +section: Web development +--- + +import { sharedMetaDataArticle } from '@/shared/metadata-article' +import Breadcrumbs from '@/components/tutorial/Breadcrumbs' +import Pagination from '@/components/tutorial/Pagination' +import DonationsMessage from '@/shared/donations-message.mdx' + +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-js-static-first-mdx-starterkit/opengraph-image' + }], + url: frontmatter.permalink, + section: frontmatter.section, + publishedTime: frontmatter.published, + modifiedTime: frontmatter.modified, + tags: frontmatter.keywords, + }, +} + +%toc% + +
+ + + +# MDX plugins + +You might have heard the word "MDX plugins" before, these **MDX plugins are either remark, rehype or recma plugins** and in in the next chapter we will see why there are 3 flavors of plugins + +MDX plugins are a great way to add support for new features into your existing MDX setup, for example in this tutorial we will use one MDX (remark) plugin in the [Optimizing images](/web_development/tutorials/next-js-static-first-mdx-starterkit/optimizing-using-next-image) chapter to turn the path of our images into imports, we will add a [Code highlighting plugin](/web_development/tutorials/next-js-static-first-mdx-starterkit/code-highlighting-plugin) (with VSCode themes support), a [Table of Contents](/web_development/tutorials/next-js-static-first-mdx-starterkit/table-of-contents-plugin) plugin, a [github like alerts](/web_development/tutorials/next-js-static-first-mdx-starterkit/github-like-alerts-plugin) plugin and several more + +In Next.js 15 and before you could NOT use plugins at all if you wanted to use Turbopack Rust bundler and / or the experimental mdxRs Rust compiler. + +In Next.js 15.1 the Next.js team added a plugins loader written in Javascript, to add support for MDX plugins when using mdxRs and Turbopack. There is a catch though, for it to work the plugin options need to be serializable + +## Difference between Remark, Rehype and Recma plugins (optional) + +This chapter contains mostly explanations about what Remark and Rehype plugins are. I won't mention Recma plugins as of today only a few exist, if someone mentions MDX plugins they probably either mean Rehype or Remark plugins and only very rarely Recma. + +To answer what Remark is I will quote the (very informative) Remark readme: + +> Remark is a tool that transforms markdown with plugins. These plugins can inspect and change your markup + +Regarding what Rehype is, here is a 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 and do their work. Then the HTML gets transformed into JSX, and then other JSX, like a React component you imported into your MDX page gets added. There is an online tool that let's you experiment with this workflow, it is called [MDX playground](https://mdxjs.com/playground/), you can 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{:md}`, `## bar{:md}`, `### baz{:md}` in your markdown, while a similar **Rehype** plugin would look for headings `

foo

{:html}`, `

bar

{:html}`, `

baz

{:html}` in the HTML (after markdown got converted to HTML). In such a case it is up to you to decide 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 parts of the tutorial, 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) to add some interesting features to our MDX setup + +> [!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) +> [recma "plugins" README](https://github.com/mdx-js/recma/blob/main/doc/plugins.md#list-of-plugins) +> [mdxjs.com "MDX playground"](https://mdxjs.com/playground/) + +## Turbopack (optional) + +[Turbopack](https://turbo.build/pack/docs) is a bundler, other bundlers you may know are [Webpack](https://webpack.js.org/) and [Rolldown](https://rolldown.rs/about) (the bundler from the vite team). A major difference between Webpack (which is the standard bundler in Next.js) and **Turbopack** (which is now the default bundler in development but is NOT getting used in production), is that Webpack is written Javascript and Turbopack is written in Rust. + +The goal when using Turbopack, is to speed up the building process by using a memory safe programming language like Rust. One problem is that you can't just use your previous Webpack plugins written in Javascript and use them in Turbopack. + +The same day the Next.js team announced the release of Next.js 15 they also announced that [Turbopack is now stable **for dev**](https://nextjs.org/blog/turbopack-for-development-stable) + +> [!MORE] +> [Turbopack documentation](https://turbo.build/pack/docs) +> [Next.js "turbopack" api-reference](https://nextjs.org/docs/app/api-reference/turbopack) +> [Next.js "next.config turbo option" api-reference](https://nextjs.org/docs/app/api-reference/config/next-config-js/turbo) + +## SWC (optional) + +[SWC](https://swc.rs) is a Javascript compiler, similar to [Babel](https://babeljs.io/). A big difference is that Babel is written in Javascript and **SWC** is written in Rust. The reasons behind switching programming languages are the same as for Turbopack, using a memory safe programming language like Rust that can do the work faster than a Javascript. The disadvantages are the same, you can't just take Babel plugins written in Javascript (like for example the React compiler, which is a babel plugin won't work when using SWC) + +> [!MORE] +> [SWC documentation](https://swc.rs/docs/getting-started) +> [Next.js "Next.js compiler / SWC" documentation](https://nextjs.org/docs/architecture/nextjs-compiler) +> [Next.js "next.config turbo option" api-reference](https://nextjs.org/docs/app/api-reference/config/next-config-js/turbo) + +## mdxjs-rs (optional) + +[Next.js v13.2](https://nextjs.org/blog/next-13-2#rust-mdx-parser) was the first version to add support for the MDX Rust compiler (**mdxjs-rs**) by introducing the [**mdxRs** (experimental) configuration option](https://nextjs.org/docs/app/api-reference/config/next-config-js/mdxRs) + +The MDX Rust compiler ([mdxjs-rs](https://github.com/wooorm/mdxjs-rs)) is written in Rust (like SWC and Turbopack) and can replace [@mdx-js/mdx](https://www.npmjs.com/package/@mdx-js/mdx) the MDX compiler written in Javascript (as we saw in the [Next.js 15 MDX Rust compiler](/web_development/tutorials/next-js-static-first-mdx-starterkit/nextjs-mdx-setup#nextjs-15-mdx-rust-compiler) (experimental) page earlier in this tutorial) + +**mdxjs-rs** compiles your MDX content (markdown with JSX, JavaScript expressions, and ESM import/exports) into JavaScript. It uses [markdown-rs](https://github.com/wooorm/markdown-rs) to compile markdown to javascript and it uses [SWC](https://github.com/swc-project/swc) to compile the Javascript code that us in your MDX content. + +> [!MORE] +> [mdxjs-rs repository](https://github.com/wooorm/mdxjs-rs) +> [markdown-rs repository](https://github.com/wooorm/markdown-rs) + +## Do mdxRs, Turbopack, MDX plugin(s) imports work??? + +I you are NOT interested in the details you can jump to the [tldr](#tldr) summary else read on: + +Experiment 1: **mdxRs OFF** and **Turbopack OFF** and **plugins get imported** in next.config + +Result: plugins work, you use a compile and build pipeline that is fully written in Javascript, it is NOT fast but it works + +Experiment 2: for some time the Next.js MDX documentation has a chapter to encourage you to try out the experimental **mdxRs** option, this time **mdxRs ON** and **Turbopack OFF** and **plugins get imported** in next.config + +Result: plugin(s) loading silently fails + +When you enable the [mdxRs (experimental) configuration option](https://nextjs.org/docs/app/api-reference/config/next-config-js/mdxRs),**@next/mdx** will use its custom **mdx-rs-loader loader** to configure **mdxjs-rs (the MDX rust compiler)** + +As they mention in their **mdxjs-rs** [README](https://github.com/wooorm/mdxjs-rs#when-should-i-use-this): + +> This project does not yet support plugins. + +This is why our plugins silently fail, the mdxjs-rs (MDX compiler written in rust) does compile our markdown using markdown-rs, but as markdown-rs has no support for plugins it just ignores them. + +Even if mdxjs-rs (markdown-rs) had support for plugins, I assume that you would have to use plugins written in Rust and not the plugins we currently use which are written in Javascript. (For example the markdown-it rust compiler has a few [plugins written in rust](https://github.com/markdown-it-rust/markdown-it-plugins.rs)) + +> [!NOTE] +> markdown-rs has an Issue that tracks the [Enable custom plugins #32](https://github.com/wooorm/markdown-rs/issues/32) feature, which you may want to subscribe to if you are interested in getting notifications about the progress + +Experiment 3: now we also enable **Turbopack**, which gives us **mdxRs ON** and **Turbopack ON** and **plugins get imported** in next.config + +Result: Error: loader @next\mdx\mdx-rs-loader.js for match "*.mdx" does not have serializable options. Ensure that options passed are plain JavaScript objects and values. + +We get an Error from SWC (which has detected that we use Turbopack). It did an additional check to see if our plugin options are serializable and the **plugin name is a sting** and **NOT an import** + +When using [MDX plugins with Turbopack](https://nextjs.org/docs/app/building-your-application/configuring/mdx#using-plugins-with-turbopack), the plugin options MUST be serializable. As Turbopack is written in Rust, you can't pass Javascript functions to it, but you can pass it a text string (even if it does NOT know that this string is a serialized Javascript object). + +Experiment 4: we convert the plugin import to using the plugin name (a string), so **mdxRs ON** and **Turbopack ON** and **plugin name as string** in next.config + +Result: again plugin(s) loading silently fails + +What happens here is that **@next/mdx** will check if we use mdxRs and as we do @next/mdx will use its **mdx-rs-loader** + +But wait, isn't that exactly what we just did? Yes it is... The plugins silently failing is the same result we have in the second experiment, the difference is that this time we have enabled Turbopack and are using a the plugin name instead of an import (as this is a Turbopack requirement), but in the end we still use mdxRs which as we saw will silently fail as it does NOT support plugins + +> [!NOTE] +> In the Next.js 15.1 release notes, there is the following entry: +> +> > \[Improvement\] Support providing MDX plugins as strings for Turbopack compatibility (PR) +> +> However after digging a bit in the [@next/mdx code](https://github.com/vercel/next.js/blob/canary/packages/next-mdx) I noticed that the **new loader they added in Next.js 15.1, only gets used when mdxRs is disabled** +> +> In Next.js < 15.1, @next/mdx will not have the new loader and instead it will use the [@mdx-js/loader](https://mdxjs.com/packages/loader/) directly + +Experiment 5: we disable mdxRs, this time we have **mdxRs OFF** and **Turbopack ON** and **plugin name as string** in next.config + +Result: TypeError: Expected usable value, not `rehype-slug` + +@mdx-js/loader is NOT happy with the string we passed him + +I digged in the code of the [@next/mdx/mdx-js-loader.js](https://github.com/vercel/next.js/blob/canary/packages/next-mdx/mdx-js-loader.js) and noticed that this new loader expects plugins to be an array (prior versions of @next/mdx allowed us to NOT use an array if we only had the plugin itself without plugin options) + +meaning that the following example will lead to the TypeError above: + +```shell +const withMDX = createMDX({ + options: { + remarkPlugins: [], + rehypePlugins: ['rehype-slug'], + }, +}) +``` + +To fix the configuration you need to use: + +```shell +const withMDX = createMDX({ + options: { + remarkPlugins: [], + rehypePlugins: [['rehype-slug']], + }, +}) +``` + +Experiment 6: same setup but this we put the plugin name (string) into an array (even though it has no options) + +This is when I bumped into something I assume is a bug on windows: + +```shell +Error: Only URLs with a scheme in: file, data, and node are supported by the default ESM loader. On Windows, absolute paths must be valid file:// URLs. Received protocol 'c:' +``` + +Confirmed, on Mac the code works 🎉 but this also confirms there is a windows bug 😭 (TODO: I did not find a ticket for this bug, so I will finish and publish the tutorial, then check again if a ticket exists, if not create one, I found a similar PR but in an other area of Next.js, the solution is probably similar: [use pathToFileUrl to make esm import()s work with absolute windows paths](https://github.com/vercel/next.js/pull/64386/files)) + +### tldr + +| I need plugins (plugin options are serializable) | I need plugins (plugin options are NOT serializable) | I do NOT need plugins (lucky you 😉) | +| ----- | ----- | ----- | +| [solution 1](#solution-1-nextconfigmjs-with-turbopack-but-no-mdxrs) | [solution 2](#solution-2-nextconfigmjs-with-plugins-but-no-turbopack-and-no-mdxrs) | [solution 3](#solution-3-nextconfigts-with-mdxrs-but-no-turbopack-and-no-plugins) | + +#### Solution 1 (next.config.mjs with Turbopack, but NO mdxRs) + +If you use Turbopack, then make sure your MDX plugins have serializable options and use the plugin name instead of an import. Also make sure mdxRs is disabled, because when Next.js 15.1 added support for Turbopack it also removed support for mdxRs + +> [!NOTE] +> this only works with > Next.js 15.1 (Next.js 15 and before will not have the @next/mdx/mdx-js-loader which is part of the [Support providing MDX plugins as strings for Turbopack compatibility PR #72802](https://github.com/vercel/next.js/pull/72802) that got released in [Next.js v15.1](https://nextjs.org/blog/next-15-1)), instead versions prior + +> [!WARN] +> this version has a bug on windows, the path resolve in windows returns an absolute path but node.js expects a URL: +> +> ```shell +> Error: Only URLs with a scheme in: file, data, and node are supported by the default ESM loader. On Windows, absolute paths must be valid file:// URLs. Received protocol 'c:' (ERR_UNSUPPORTED_ESM_URL_SCHEME) +> ``` +> +> this version uses the new @next/mdx/mdx-js-loader, which fails to get imports right on windows + +```js title="next.config.mjs" +import createMDX from '@next/mdx' + +/** @type {import('next').NextConfig} */ +const nextConfig = { + pageExtensions: ['js', 'jsx', 'md', 'mdx', 'ts', 'tsx'], + experimental: { + mdxRs: false, + }, +} + +/** @type {import('remark-gfm').Options} */ +const remarkGFMOptions = { + singleTilde: false, +} + +const withMDX = createMDX({ + options: { + // @ts-ignore wrong types + remarkPlugins: [['remark-gfm', remarkGFMOptions]], + rehypePlugins: [], + }, +}) + +export default withMDX(nextConfig) +``` + +**Incompatible types problem:** if you use the typescript config, know that you will likely have a types error: + +```shell +Type 'string' is not assignable to type 'Plugin'.ts(2322) +``` + +This is because createMDX (as of now Next.js 15.1.2) still uses the Option type from the @mdx-js/loader. + +We tell typescript to ignore this error by using a @ts-ignore comment: + +```ts +const withMDX = createMDX({ + options: { + // @ts-ignore wrong types + remarkPlugins: [[remarkGfm, remarkGFMOptions]], + rehypePlugins: [], + }, +}) +``` + +With this configuration you can use turbopack: + +```json title="package.json" +{ + "name": "nextjs_turbo_mdx-rs_no_plugins", + "version": "0.0.1", + "scripts": { + "dev": "next dev --turbopack", + }, +} +``` + +#### Solution 2 (next.config.mjs with plugins but NO Turbopack and NO mdxRs) + +If you use plugins, but NOT all your plugin options are serializable, then your only option is to disable Turbopack (as it needs serializable options). You can also NOT use Javascript plugins with mdxjs-rs (it will compile your MDX but ignore your plugins) which is why mdxRs is disabled too + +As Turbopack is disabled you can import MDX plugins, unless you use a next.config.ts, then you will likely NOT be able to import the plugin(s) because most MDX (remark, rehype and recma) plugins are ESM plugins (for more details check out: [next.config.ts does NOT support ESM only imports](/web_development/tutorials/next-js-static-first-mdx-starterkit/next-config#nextconfigts-does-not-support-esm-only-packages)) + +```js title="next.config.mjs" +import createMDX from '@next/mdx' +import remarkGfm from 'remark-gfm' + +/** @type {import('next').NextConfig} */ +const nextConfig = { + pageExtensions: ['js', 'jsx', 'md', 'mdx', 'ts', 'tsx'], + experimental: { + mdxRs: false, + }, +} + +/** @type {import('remark-gfm').Options} */ +const remarkGFMOptions = { + singleTilde: false, +} + +const withMDX = createMDX({ + options: { + // @ts-ignore wrong types + remarkPlugins: [[remarkGfm, remarkGFMOptions]], + rehypePlugins: [], + }, +}) + +export default withMDX(nextConfig) +``` + +> [!NOTE] +> as [next.config.ts does NOT support ESM only imports](/web_development/tutorials/next-js-static-first-mdx-starterkit/next-config#nextconfigts-does-not-support-esm-only-packages) we use a **.mjs** config (instead of a typescript config) + +With this configuration you can NOT use turbopack: + +```json title="package.json" +{ + "name": "nextjs_turbo_mdx-rs_no_plugins", + "version": "0.0.1", + "scripts": { + "dev": "next dev", + }, +} +``` + +#### Solution 3 (next.config.ts with mdxRs, but NO Turbopack and NO plugins) + +If you do NOT use plugins, then I recommend you enable mdxRs and if you want the github flavored markdown (gfm) extensions (like tables, footnotes, strikethrough, ...) then I recommend setting the configuration option to gfm + +```ts title="next.config.ts" +import type { NextConfig } from 'next' +import createMDX from '@next/mdx' + +const nextConfig: NextConfig = { + pageExtensions: ['js', 'jsx', 'md', 'mdx', 'ts', 'tsx'], + experimental: { + mdxRs: { + // 'gfm' | 'commonmark' + mdxType: 'gfm', + }, + }, +} + +const withMDX = createMDX({ + options: { + remarkPlugins: [], + rehypePlugins: [], + }, +}) + +export default withMDX(nextConfig) +``` + +With this configuration you can use turbopack (without turbopack works too): + +```json title="package.json" +{ + "name": "nextjs_turbo_mdx-rs_no_plugins", + "version": "0.0.1", + "scripts": { + "dev": "next dev --turbopack", + }, +} +``` + +I had NO major problems using the MDX rust compiler, but encountered a problem when using tables, for which there is an open [markdown-rs issue #111](https://github.com/wooorm/markdown-rs/issues/111) but no fix yet + +```shell +In HTML, whitespace text nodes cannot be a child of . Make sure you don't have any extra whitespace between tags on each line of your source code. +This will cause a hydration error. +``` + +One workaround would be to go back to using the Javascript compiler (by setting mdxRs to false), another workaround is to create tables using JSX (as recommend in this similar [mdx-js issue #2000](https://github.com/mdx-js/mdx/issues/2000#issuecomment-1280603924) a React UI component to render tables (its MDX after all) + + + + + + diff --git a/app/web_development/tutorials/next-js-static-first-mdx-starterkit/metadata/page.mdx b/app/web_development/tutorials/next-js-static-first-mdx-starterkit/metadata/page.mdx new file mode 100644 index 00000000..3db5c1e2 --- /dev/null +++ b/app/web_development/tutorials/next-js-static-first-mdx-starterkit/metadata/page.mdx @@ -0,0 +1,375 @@ +--- +title: Next.js 15 Metadata (for tsx and mdx pages) - Next.js 15 Tutorial +description: Next.js 15 Metadata (for tsx and mdx pages) - Next.js 15 static first MDX starterkit | www.chris.lu Web development tutorials +keywords: ['Metadata', 'tsx', 'mdx', 'pages', 'OpenGraph', 'SEO', 'nextjs'] +published: 2024-12-31T23:00:00.000Z +modified: 2024-12-31T23:00:00.000Z +permalink: https://chris.lu/web_development/tutorials/next-js-static-first-mdx-starterkit/metadata +section: Web development +--- + +import { sharedMetaDataArticle } from '@/shared/metadata-article' +import Breadcrumbs from '@/components/tutorial/Breadcrumbs' +import Pagination from '@/components/tutorial/Pagination' +import DonationsMessage from '@/shared/donations-message.mdx' + +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-js-static-first-mdx-starterkit/opengraph-image' + }], + url: frontmatter.permalink, + section: frontmatter.section, + publishedTime: frontmatter.published, + modifiedTime: frontmatter.modified, + tags: frontmatter.keywords, + }, +} + +%toc% + +
+ + + +# Next.js 15 Metadata (for tsx and mdx pages) + +In this chapter, we will add the **metadata** to our pages (and layout) by using the [Next.js 15 Metadata API](https://nextjs.org/docs/app/building-your-application/optimizing/metadata), this will add meta tags inside of the `{:html}` element of our HTML documents, which is essential to help the crawlers from search engines and social networks better understand the content of our pages. These efforts will result in improved SEO scores, which means we will be getting more traffic from organic sources + +As we saw early in this tutorial, Next.js has created a `layout.tsx` file in the `/app` folder and already added a basic **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 {7-10} {11} +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', +} +``` + +Lines 7 to 10: we edit the title meta tag, the default title value was previously a string, but we turned it into an object with two properties + +Line 8: by using the [title.template](https://nextjs.org/docs/app/api-reference/functions/generate-metadata#template) we ensure that all will titles have a similar structure on every page and it helps reduce repetition (DRY) + +Line 9: the second property is the default title for the homepage (which is required, when using template) + +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** + +When we visit one of pages, Next.js will replace title.template `%s` placeholder with the title of the current page we are visiting + +Line 11: we only changed the description default value to something else + +Launch the dev server, then open the "home" page at `http://localhost:3000/`, and then right-click and select **inspect** + +Look at what is inside your page's `{:html}` element. There are, for example, some Next.js `