How to create a beautiful page with NextJS, MDX

Van Nguyen Nguyen -

Edit on Github on DEV

If you are a developer, there is at least one time on your dev journey that you did come across some beautiful codeblock with nice custom theme color, showing proper line, color syntax, showing name file type,... And you also want to make the same thing. In this post, I will show you everything I know about how to make a custom digital blog by MDX.

This is a codeblock on MDX websiteThis is a codeblock on MDX website

Prerequisites

  1. You have to be somewhat familiar with NextJS. If you have not tried NextJS before, I highly recommend you to follow NextJS tutorial from their official website (since they explained everything quite clearly and help you create a small website with it).


  2. About styling, I'm using ChakraUI to style my website but I will not recommend you to follow the same strategy. Instead, I suggest you to use CSS framework (or even pure CSS) that you are good at currently. I will try as much as I can to explain what the property of each ChakraUI component so you can apply same idea.

  3. About MDX, I highly recommend you to follow checkout their Getting started page , there may be many integrations process with other framework that you have not heard of, but let just focus on their NextJS section for now. Then reading the page Using MDX to have some ideas how they use MDX, you could go ahead and try out MDX with NextJS first since you already have some idea how to generation page in NextJS from section 1.


If something goes wrong, please refer to this repo for more information or you could make an issue in my main website repo for more clarification so I can improve the content.

Installation and Configuration

There are some packages that you will need you to install before hand. I will explain what is the purpose for each of them:

  • mdx-js/loader . This is webpack version of MDX that help you to load MDX (you can imagine it is like a compiler to translate MDX to HTML structure). If your intention is to use MDX directly in the page directory of NextJS, you have to install this package since this is the requirement for MDX. There is other option which I am currently using is that I totally separate the contents out of the page folder and using next-mdx-remote (which I will introduce below) to fetch the content for getStaticProps. Config your next.config.js (If you just want to put the contents in the page folder for nextjs to automatically render them):
javascript
1module.exports = {
2 reactStrictMode: true,
3
4 // Prefer loading of ES Modules over CommonJS
5 experimental: { esmExternals: true },
6 // Support MDX files as pages:
7 pageExtensions: ['md', 'mdx', 'tsx', 'ts', 'jsx', 'js'],
8 // Support loading `.md`, `.mdx`:
9 webpack(config, options) {
10 config.module.rules.push({
11 test: /\.mdx?$/,
12 use: [
13 // The default `babel-loader` used by Next:
14 options.defaultLoaders.babel,
15 {
16 loader: '@mdx-js/loader',
17 /** @type {import('@mdx-js/loader').Options} */
18 options: {
19 /* jsxImportSource: …, otherOptions… */
20 },
21 },
22 ],
23 });
24
25 return config;
26 },
27};
  • date-fns . This is totally optional, you do not need to install this since it is just a tool to format the date for meta-data.
  • gray-matter . This is also optional, it is similar to YAML key/value that help you to have some extra data (meta-data) in your mdx. Example (the highlight parts is meta-data):
markdown
1author: Van Nguyen Nguyen
2date: "2022-02-05"
3summary: "Something"
4
5---
6
7Your content go here
  • next-mdx-remote . If you do not want to use mdx-js/loader and want the fetching content outside, this is a requirement since this package will allow your MDX to be loaded within getStaticProps or getServerSideProps (you should know these things by now) from NextJS. There is are some alternative for this: mdx-bundler and the one from NextJS themself next-mdx . I will point out the pros and cons of them later.

  • prism-react-renderer . This is the package that help you to custom your codeblock. This a recommendation because there are mutiple package out there to do the same things. I will explain the logic later.

  • mdx-js/react . This package will provide the MDXProvider for you to pass the custom components

Create custom tags for the page

Set up fundamental logic for rendering MDX

First, we need some content for the website. I highly recommend you to use web tutorial project from NextJS that you already finished beforehand. Then we can create a folder with a MDX file at the root level:

markdown

try-mdx/test.mdx

1---
2title: "This is for Trying MDX"
3date: "2020-01-02"
4summary: "This is the summary testing for MDX"
5---
6
7# Ahihi this is a custome Heading
8
9<Test>
10 <Something>Hello World </Something>
11</Test>
12
13a [link](https://example.com), an ![image](./image.png), some *emphasis*,
14something **strong**, and finally a little `<div/>`.
15**strong**
16
17```javascript file=testing.js highlights=1,2
18const test= 1;
19const funnyThing = () => {
20 console.log(test);
21}
22funnyThing()```

Now, we need to find the way to fetch the content of the MDX file. If you already completed the NextJS tutorial, you know that you can get the path and the content by applying the some logic but instead of getting file with .md, you will get the file with .mdx

javascript

lib/posts.js

1import fs from 'fs';
2import path from 'path';
3// Using gray matter for getting metadata
4import matter from 'gray-matter';
5
6const postsDirectory = path.join(process.cwd(), '/try-mdx');
7
8export function getSortedPostsData() {
9 // Get file names under /posts
10 const fileNames = fs.readdirSync(postsDirectory);
11 const allPostsData = fileNames.map(fileName => {
12 const ext = fileName.split('.')[1];
13 // Remove ".mdx" from file name to get id
14 const id = fileName.replace(/\.mdx$/, '');
15
16 // Read markdown file as string
17 const fullPath = path.join(postsDirectory, fileName);
18 const fileContents = fs.readFileSync(fullPath, 'utf8');
19
20 // Use gray-matter to parse the post metadata section
21 const matterResult = matter(fileContents);
22 // Combine the data with the id
23 return {
24 id,
25 ...matterResult.data,
26 };
27 });
28 // Sort posts by date
29 return allPostsData.sort(({ date: a }, { date: b }) => {
30 if (a < b) {
31 return 1;
32 } else if (a > b) {
33 return -1;
34 } else {
35 return 0;
36 }
37 });
38}
39
40export function getAllPostIds() {
41 // Read all the filename in the directory path
42 const fileNames = fs.readdirSync(postsDirectory);
43
44 // Filter out the ext, only need to get the name of the file
45 return fileNames.map(fileName => { return {
46 // Following routing rule of NextJS
47 params: {
48 id: fileName.replace(/\.mdx$/, ''),
49 },
50 };
51 });
52}
53
54export async function getPostData(id) {
55 // For each file name provided, we gonna file the path of the file
56 const fullPath = path.join(postsDirectory, `${id}.mdx`);
57 // Read the content in utf8 format
58 const fileContents = fs.readFileSync(fullPath, 'utf8');
59
60 // Using gray-matter to get the content and that data
61 const { content, data } = matter(fileContents);
62
63 // provide what need to be rendered for static-file-generation
64 return {
65 id,
66 content,
67 ...data,
68 };
69}

From now, I assume that you understand about Static Generation as well as Dynamic Routing (since these are fundamental topics that got covered in NextJS tutorial course) like how to use getStaticPaths and getStaticProps.

If you follow the mdx-js/loader approach, you can just create some [filename].mdx and see the magic happens, the content you write in the MDX file will be translated into HTML format. Do not forget the config your next.config.js and install mdx-js/loader


If you follow the next-md-remote, you have to separate your blog contents out of the page/ folder so NextJS will not render it. Then using dynamic route to fetch them.

structure
1pages/
2...
3├── posts
4│   └── [id].js // Dynamic Routing
5...

Inside [id].js file:

jsx

pages/posts/[id].js

1// Getting component from NextJS tutorial
2// Layout is just the wrapper with the styling width to move page to the center with
3// some extra metadata
4import Layout from '../../components/layout';
5// Head component is add the title for the page
6import Head from 'next/head';
7// Date component from NextJS tutorial, basically it will format the date for you
8// but you could just print a raw date string
9import Date from '../../components/date';
10
11// Function to get path and contents of the .mdx file (already mentioned above)
12import { getAllPostIds, getPostData } from '../../lib/posts';
13
14// This is just come basic class for styling some tags
15import utilStyles from '../../components/utils.module.css';
16
17// Two important function from next-mdx-remote that make the magic happens
18// serialize will help us to convert raw MDX file into object that will be passed
19to MDXRemote for rendering HTML on the page
20import { serialize } from 'next-mdx-remote/serialize';
21// MDXRemote is the component for rendering data that get from serialize
22import { MDXRemote } from 'next-mdx-remote';
23
24export async function getStaticPaths() {
25
26 // Get all the unique path that we need( the name of the folder)
27 const paths = getAllPostIds();
28 return {
29 // Return the path
30 paths,
31 fallback: false,
32 };
33}
34
35export async function getStaticProps({ params }) {
36 // Get the raw data of the MDX file according to the path that we get
37 // Including the metadata and the raw content
38 const postData = await getPostData(params.id);
39
40 // Translating the raw content into readable object by serialize
41 // I recommend you to console.log the value to see how they look like
42 const mdxSource = await serialize(postData.content, {
43 // next-mdx-remote also allow us to use remark and rehype plugin, reading MDX docs for more information
44 // I am currently not using any plugin, so the array will be empty.
45 mdxOptions: {
46 remarkPlugins: [],
47 rehypePlugins: [],
48 },
49 });
50 return {
51 // we only need 2 things from the props
52 // postData (we dont care about the content since that one we will get from the mdxSource)
53 // We care about getting the metadata here so that is why we still need to get postData
54 props: {
55 postData,
56 mdxSource,
57 },
58 };
59}
60
61export default function Post({ postData, mdxSource }) {
62 return (
63 <Layout>
64 <Head>
65 <title>{postData.title}</title>
66 </Head>
67 <article>
68 <h1 className={utilStyles.headingXl}>{postData.title}</h1>
69 <div className={utilStyles.lightText}>
70 <Date dateString={postData.date} />
71 </div>
72 // MDXRemote is the components to render the actual content, other components above is just for
73 // metadata
74 <MDXRemote {...mdxSource} />
75 </article>
76 </Layout>
77 );
78}

You may want to ask "hmm, why I have to use next-remote-mdx to set up everything like this?, instead I could just use mdx-js/loader and let NextJS render my page automatically". Well, I choose to go this way because I want to easily add more customization on the my page like having more components in my <Post/>. "But hey, hasn't MDX allowed you to import new components already?". Yes, but controlling through JSX is always easier and better .For example, you can have some logic right in the <Post/> component which is annoying to do in MDX.


Your page will probably look like this.
Initial SetupInitial Setup

Styling your tags

MDX Docs actually show you the way to style your components through MDXProvider that come from mdx-js/react or other web framework as well. Let apply it to our NextJS app.


NextJS allow you to custom App , what does it benefit you for this case:

  • Inject additional data into pages (which allows us to wrap every new component and import new data, and these thing will got added to the whole website across multiple page).
  • Persisting layout between page change (which means you can wrap the whole app by custom component these new component will be applied globally).
  • Add global CSS (which allow you to apply the color theme for your code block).

Create a customHeading.js in your components folder

structure
1components/
2├── customHeading.js
3├── ...

Inside customHeading.js

jsx

components/customHeading.js

1//This is custom h1 tag = '#'
2const MyH1 = props => <h1 style={{ color: 'tomato' }} {...props} />;
3
4//This is custom h2 tag = '##'
5const MyH2 = props => <h2 style={{ color: 'yellow' }} {...props} />;
6
7
8//This is custom link tag = '[<name>](<url>)'
9const MyLink = props => {
10 console.log(props); // Will comeback to this line
11 let content = props.children;
12 let href = props.href;
13 return (
14 <a style={{ color: 'blue' }} href={href}>
15 {content}
16 </a>
17 );
18};
19
20const BoringComponent = () => {
21 return <p>I am so bored</p>
22}
23
24export { MyH1, MyH2, MyLink, BoringComponent };

Look at the code, you wonder "Okay, but what is the variable props there?". I will explain the idea later. Now let get the custom components work first.


Create a _app.js in your page folder or if you already had one, you do not need to create new one anymore

structure
1pages/
2...
3├── _app.js
4...

Inside _app.js

jsx

pages/_app.js

1// You do not need to worry about these things
2// it just give you some extra global style for the page
3import '../styles/global.css';
4import '../src/theme/style.css';
5import { ChakraProvider } from '@chakra-ui/react';
6import theme from '../src/theme/test';
7
8// These are important line
9import { MyH1, MyH2, MyLink, BoringComponent } from '../components/CustomHeading';
10import { MDXProvider } from '@mdx-js/react';
11
12// MDXProvider accept object only
13const components = { h1: MyH1, h2: MyH2, a: MyLink, BoringComponent };
14
15export default function App({ Component, pageProps }) {
16 return (
17 // Do not worry about the <ChakraProvider/>, it just give you the global style
18 <ChakraProvider theme={theme}>
19 // Wrapping the <Component/> by <MDXProvider/> so everypage will get applied
20 //the same thing
21 <MDXProvider components={components}>
22 // <Component/> is the feature of NextJS which identify the content of your
23 // current page. <Component/> will change its pageProps to new page when you change to new
24 // page
25 <Component {...pageProps} />;
26 </MDXProvider>
27 </ChakraProvider>
28 );
29}

Now you can see that the heading will turn into red because we are using h1 if you are familiar with markdown and the link will turn into blue.


Now let go back to the props variable before. If you scroll up, you can see I did console.log(props). Let see what it is from the console

console.log(props)console.log(props)

If you know about ReactJS (I assume you did), if you pass any key value to a component, you can get it value through props. So MDX under the hood already parse the whole file to know which one is a link, image, heading, codeblock,... So you can get the value from there.


To this point, you know how MDX interact with its custom components by just getting information from the props and passed it into the new custom components you can skip next explanation.

Simple explain MDXProvider

markdown
1import Random from 'somewhere'
2
3# Heading
4
5<Random/>
6
7I feel bored

This is what we get when MDX translate the file into JSX

jsx
1import React from 'react'
2import { MDXTag } from '@mdx-js/tag'
3import MyComponent from './my-component'
4
5export default ({ components }) => (
6 <MDXTag name="wrapper" components={components}>
7 <MDXTag name="h1" components={components}>
8 Heading
9 </MDXTag>
10 <Random />
11 <MDXTag name="p" components={components}>
12 I feel bored
13 </MDXTag>
14 </MDXTag>
15)

We see that the exports default take a components from props. The name props of MDXTag will maps to a component defined in the components props. That why when we construct our components variable, we have to specify which tag this component mapping to. Or if you dont want to map anything but simply just for using it in MDX file, we do not need to specify any name tag.

Styling your codeblock

This is probably the one that most people are waiting for. Let's walk through it together.


Choosing your syntax highlight theme is quite important since it will make your codeblock more
readable. I personally using my favorite theme GruvBox Dark . Or you can find more beautiful themes through this repo .


My approach for this is that I will apply this syntax highlight theme globally, I do not want to change dynamically and I know the purpose of my website is just a small blog so there's no need to using multiple syntax highlighting colors.


First put the code highlighting css somewhere. I recommend create a folder styles/ in the root

structure
1styles/
2└── gruvBox.css
3...

Go to your _app.js and add the styling

jsx

pages/_app.js

1import '../styles/global.css';
2import '../src/theme/style.css';
3import { ChakraProvider } from '@chakra-ui/react';
4import theme from '../src/theme/test';
5
6import { MyH1, MyH2, MyLink, BoringComponent } from '../components/CustomHeading';
7import { MDXProvider } from '@mdx-js/react';
8
9// When you put the styling in _app.js the style will be applied across the whole website
10import '../styles/gruvBox.css';
11
12const components = { h1: MyH1, h2: MyH2, a: MyLink, BoringComponent };
13
14export default function App({ Component, pageProps }) {
15 return (
16 <ChakraProvider theme={theme}>
17 <MDXProvider components={components}>
18 <Component {...pageProps} />;
19 </MDXProvider>
20 </ChakraProvider>
21 );
22}

Wow, colour changed!! Actually not quite, if you check your page right now, the color would be really weird. Let me explain why. Firstly, this is what you get from the HTML structure on your page (you can just inspect from your own browser to check for the markup and styling). Just a whole string of code got cover by <code/> tag

markup
1<pre><code class="language-javascript" metastring="file=testing.js highlights=1,3-9" file="testing.js" highlights="1,3-9">
2"const ahihi = 1;
3export async function getStaticProps({ params }) {
4 const postData = await getPostData(params.id);
5 const mdxSource = await serialize(postData.content);
6 console.log(postData);
7 console.log(mdxSource);
8 return {
9 props: {
10 postData,
11 mdxSource,
12 },
13 };
14}"
15</code></pre>

And this is the only styling that got applied to that markup above

css
1code[class*="language-"], pre[class*="language-"] {
2 color: #ebdbb2;
3 font-family: Consolas, Monaco, "Andale Mono", monospace;
4 direction: ltr;
5 text-align: left;
6 white-space: pre;
7 word-spacing: normal;
8 word-break: normal;
9 line-height: 1.5;
10 -moz-tab-size: 4;
11 -o-tab-size: 4;
12 tab-size: 4;
13 -webkit-hyphens: none;
14 -ms-hyphens: none;
15 hyphens: none;
16}

But if you check your favorite syntax styling sheet, we have a lot of different things like: token, comment, delimiter, operator,... So where does all these things come from? Well they are
from the tokenize process for code. So you have to find some way to tokenize that string so you will be able to apply those styling. prism-react-renderer is going to be a great tool for this.


If you go to their usage example, you can clearly see how we are going to use it. Since they already provided a wrapper example for us, we just need to pass our content data.


Create a customCodeblock.js in your components/ folder

jsx

components/customCodeblock.js

1// I'm using styled components here since they also recommend using it but you can
2// just create some custom class or applied style directly into the components like the
3// React way.
4import styled from '@emotion/styled';
5// This is their provided components
6import Highlight, { defaultProps } from 'prism-react-renderer';
7
8// Custom <pre/> tag
9const Pre = styled.pre`
10 text-align: left;
11 margin: 1em 0;
12 padding: 0.5em;
13 overflow: scroll;
14 font-size: 14px;
15`;
16
17// Cutom <div/> (this is arrangement of the line)
18const Line = styled.div`
19 display: table-row;
20`;
21
22// Custom <span/> (this is for the Line number)
23const LineNo = styled.span`
24 display: table-cell;
25 text-align: right;
26 padding-right: 1em;
27 user-select: none;
28 opacity: 0.5;
29`;
30
31// Custom <span/> (this is for the content of the line)
32const LineContent = styled.span`
33 display: table-cell;
34`;
35
36
37const CustomCode = props => {
38 // Pay attention the console.log() when we applied this custom codeBlock into the
39 //_app.js. what metadata you are getting, is there anything you did not expect that actually
40 // appear. Can you try out some extra features by changing the MDX codeblock content
41 console.log(props);
42
43 // From the console.log() you will be able to guess what are these things.
44 const className = props.children.props.className || '';
45 const code = props.children.props.children.trim();
46 const language = className.replace(/language-/, '');
47
48 return (
49 <Highlight
50 {...defaultProps}
51 theme={undefined}
52 code={code}
53 language={language}
54 >
55 {({ className, style, tokens, getLineProps, getTokenProps }) => (
56 <Pre className={className} style={style}>
57 {tokens.map((line, i) => (
58 <Line key={i} {...getLineProps({ line, key: i })}>
59 <LineNo>{i + 1}</LineNo>
60 <LineContent>
61 {line.map((token, key) => (
62 <span key={key} {...getTokenProps({ token, key })} />
63 ))}
64 </LineContent>
65 </Line>
66 ))}
67 </Pre>
68 )}
69 </Highlight>
70 );
71};
72
73export default CustomCode;

Let apply this this CustomCode into your MDXProvider

jsx

pages/_app.js

1import '../styles/global.css';
2import { ChakraProvider } from '@chakra-ui/react';
3import theme from '../src/theme/test';
4import '../src/theme/style.css';
5import { MyH1, MyH2, MyLink } from '../components/CustomHeading';
6import { MDXProvider } from '@mdx-js/react';
7import CustomCode from '../components/customCode';
8import '../styles/gruvBox.css';
9
10const components = {
11 h1: MyH1,
12 h2: MyH2,
13 a: MyLink,
14 pre: CustomCode };
15
16export default function App({ Component, pageProps }) {
17 return (
18 <ChakraProvider theme={theme}>
19 <MDXProvider components={components}>
20 <Component {...pageProps} />;
21 </MDXProvider>
22 </ChakraProvider>
23 );
24}

I hope you get what you want, the color should be as what you are expecting. If there something wrong, please refer to this repo


prism-react-renderer actually provide you color theme, they did show you how to apply it in their docs, but they dont have GruvBox that why I have to find the GruvBox style for global style to override their default color. If you are able to find your favorite theme in their list, there is no need to add global style, you can remove it.

Create file name for you codeblock

I hope that you did check the console.log(props) from the your custom codeblock. This is what we see on in the console:

[console.log(props) from the custom codeblock[console.log(props) from the custom codeblock

There is some interesting props here: file, highlights, metastring. If you comeback to the content that I already gave in the beginning, there are some extra key value I put in the codeblock which for a usual markdown syntax, it is kind of useless. But this is MDX, MDX actually parses the codeblock and give us some metadata. From this data, we will be able to make some extra features. Let add the file name/path for it:

jsx

components/customCodeblock.js

1import styled from '@emotion/styled';
2import Highlight, { defaultProps } from 'prism-react-renderer';
3
4const Pre = styled.pre`
5...
6`;
7
8const Line = styled.div`
9...
10`;
11
12const LineNo = styled.span`
13...
14`;
15
16const LineContent = styled.span`
17...
18`;
19
20const CustomCode = props => {
21 console.log(props);
22 const className = props.children.props.className || '';
23 const code = props.children.props.children.trim();
24 const language = className.replace(/language-/, '');
25 const file = props.children.props.file;
26
27 return (
28 <Highlight
29 {...defaultProps}
30 theme={undefined}
31 code={code}
32 language={language}
33 >
34 {({ className, style, tokens, getLineProps, getTokenProps }) => (
35 <>
36 <h2>{file}</h2>
37 <Pre className={className} style={style}>
38 {tokens.map((line, i) => (
39 <Line key={i} {...getLineProps({ line, key: i })}>
40 <LineNo>{i + 1}</LineNo>
41 <LineContent>
42 {line.map((token, key) => (
43 <span key={key} {...getTokenProps({ token, key })} />
44 ))}
45 </LineContent>
46 </Line>
47 ))}
48 </Pre>
49 </>
50 )}
51 </Highlight>
52 );
53};
54
55export default CustomCode;

Your homework is styling that file name for your code block.

Create highlights for you codeblock

Now, if you look at the highlights metadata, you probably wonder what I am trying to accomplish here. My idea is simple:

1if my highlights = 1,3-5
2I want the value I parse from this string to be like this [1, 3, 4, 5]
3
4if my highlights = 1,2,3 or 1-3
5I want the value I parse from this string to be like this [1, 2, 3]
6
7You get it right? the '-' will detect the range that I want to loop through.

Since we are able to get the highlights value now, we need to find the way to parse this string Let create lib/parseRange.js

javascript

lib/parseRange.js

1function parsePart(string) {
2 // Array that contain the range result
3 let res = [];
4
5 // we split the ',' and looping through every elemenet
6 for (let str of string.split(',').map(str => str.trim())) {
7 // Using regex to detect whether it is a number or a range
8 if (/^-?\d+$/.test(str)) {
9 res.push(parseInt(str, 10));
10 } else {
11 // If it is a range, we have to contruct that range
12 let split = str.split('-');
13 let start = split[0] - '0';
14 let end = split[1] - '0';
15 for (let i = start; i <= end; i++) {
16 res.push(i);
17 }
18 }
19 }
20 return res;
21}
22
23export default parsePart;

Let use this thing your customCodeblock.js:

jsx

components/customCodeblock.js

1import styled from '@emotion/styled';
2import Highlight, { defaultProps } from 'prism-react-renderer';
3// import your function
4import parsePart from '../lib/parseRange';
5
6const Pre = styled.pre`
7...
8`;
9
10const Line = styled.div`
11...
12`;
13
14const LineNo = styled.span`
15...
16`;
17
18const LineContent = styled.span`
19...
20`;
21
22// shouldHighlight will return a function to be called later
23// that function will return true or false depend on whether the index will appear
24// inside our parsed array
25const shouldHighlight = raw => {
26 const parsedRange = parsePart(raw);
27 if (parsedRange) {
28 return index => parsedRange.includes(index);
29 } else {
30 return () => false;
31 }
32};
33
34const CustomCode = props => {
35 console.log(props);
36 const className = props.children.props.className || '';
37 const code = props.children.props.children.trim();
38 const language = className.replace(/language-/, '');
39 const file = props.children.props.file;
40
41 // Getting the raw range
42 const rawRange = props.children.props.highlights || '';
43 // assign the checking function
44 const highlights = shouldHighlight(rawRange);
45
46 return (
47 <Highlight
48 {...defaultProps}
49 theme={undefined}
50 code={code}
51 language={language}
52 >
53 {({ className, style, tokens, getLineProps, getTokenProps }) => (
54 <>
55 <h2>{file}</h2>
56 <Pre className={className} style={style}>
57 // Getting the index from the mapping line
58 {tokens.map((line, i) => (
59 <Line key={i} {...getLineProps({ line, key: i })}>
60 <LineNo>{i + 1}</LineNo>
61 <LineContent
62 style={{
63 background: highlights(i + 1) ? 'gray' : 'transparent',
64 }}
65 >
66 {line.map((token, key) => (
67 <span key={key} {...getTokenProps({ token, key })} />
68 ))}
69 </LineContent>
70 </Line>
71 ))}
72 </Pre>
73 </>
74 )}
75 </Highlight>
76 );
77};
78
79export default CustomCode;

I hope you will get the highlight styling that you want. You now get the basic idea of how to highlight line. Making it look better will be your homework.

Making a copy functionality for your codeblock

We gonna utilize a web API called Clipboard API to accomplish this. I am not going to explain the mechanism since the main website does a way better job than me. You can check out their explainaion here


Let modify our customCodeblock.js

jsx

components/customCodeblock.js

1// useState to change the text of copy button
2import { useState } from 'react';
3import styled from '@emotion/styled';
4import Highlight, { defaultProps } from 'prism-react-renderer';
5import parsePart from '../lib/parseRange';
6
7const Pre = styled.pre`
8...
9`;
10
11const Line = styled.div`
12...
13`;
14
15const LineNo = styled.span`
16...
17`;
18
19const LineContent = styled.span`
20...
21`;
22
23const shouldHighlight = raw => {
24 ...
25};
26
27const CustomCode = props => {
28
29 const [currLabel, setCurrLabel] = useState('Copy');
30
31 const copyToClibBoard = copyText => {
32 let data = [
33 new ClipboardItem({
34 'text/plain': new Blob([copyText], { type: 'text/plain' }),
35 }),
36 ];
37 navigator.clipboard.write(data).then(
38 function () {
39 setCurrLabel('Copied');
40 setTimeout(() => {
41 setCurrLabel('Copy');
42 }, 1000);
43 },
44 function () {
45 setCurrLabel(
46 'There are errors'
47 );
48 }
49 );
50 };
51
52 const className = props.children.props.className || '';
53 const code = props.children.props.children.trim();
54 const language = className.replace(/language-/, '');
55 const file = props.children.props.file;
56
57 const rawRange = props.children.props.highlights || '';
58 const highlights = shouldHighlight(rawRange);
59
60 return (
61 <Highlight
62 {...defaultProps}
63 theme={undefined}
64 code={code}
65 language={language}
66 >
67 {({ className, style, tokens, getLineProps, getTokenProps }) => (
68 <>
69 <h2>{file}</h2>
70 <button
71 onClick={() => copyToClibBoard(props.children.props.children)}
72 >
73 {currLabel}
74 </button>
75 <Pre className={className} style={style}>
76 {tokens.map((line, i) => (
77 <Line key={i} {...getLineProps({ line, key: i })}>
78 <LineNo>{i + 1}</LineNo>
79 <LineContent
80 style={{
81 background: highlights(i + 1) ? 'gray' : 'transparent',
82 }}
83 >
84 {line.map((token, key) => (
85 <span key={key} {...getTokenProps({ token, key })} />
86 ))}
87 </LineContent>
88 </Line>
89 ))}
90 </Pre>
91 </>
92 )}
93 </Highlight>
94 );
95};
96
97export default CustomCode;

Summary

I hope you achieve what you are looking for when you reading my post. This is just some basic logic to automate custom tag for your website. Create as much custom components as possible to fulfill your need, styling thing in your favorite color. And from now on you could just focus on your content. Good luck on your dev journey

© 2022 Van Nguyen Nguyen. All Rights Reserved.

Feel free to contribute to the website on Github if you see something go wrong

Contact me: nguyenvannguyen.oc@gmail.com