Join me on this project where we build an online Markdown editor using the latest version of Nextjs.
Objectives
- Rendering markdown in an Next.js project
- Use custom components
- Add Remark and Rehype plugins
- Learn to change states in parent component from child.
- Have fun 🔥
To check the finished build click here
1. Create the landing page
I want a simple layout so I divided the screen into two parts; the left being the editor and we see the markdown rendering on the right.
const Homepage = () => {
return (
<div className='h-screen flex justify-between'>
// Input the markdown
<section className='w-full pt-5 h-full'>
<textarea
className='w-full ... placeholder:opacity-80'
placeholder='Feed me some Markdown 🍕'
autoFocus
/>
</section>
<div className='fixed ... border-dashed' />
// Render the markdown
<article className='w-full pt-5 pl-6'>
Markdown lies here
</article>
</div>
)
}
return Homepage
2. Add states to store data
Now let's change it into a client component and add the useState
hook.
"use client"
import { useState } from "react"
const Homepage = () => {
const [source, setSource] = useState('');
return (
...
<textarea
className='w-full ... placeholder:opacity-80'
placeholder='Feed me some Markdown 🍕'
value={source}
onChange={(e) => setSource(e.target.value)}
autoFocus
/>
...
)
}
3. Setup react-markdown and @tailwindcss/typography
We use react-markdown
to render markdown and @tailwindcss/typography
to style the markdown. Install them by firing the following commands.
npm install react-markdown
npm install -D @tailwindcss/typography
Now import and add the Markdown component and pass source
as children. Remember to add the prose
classname to the Markdown component.
import Markdown from 'react-markdown'
const Homepage = () => {
return (
...
<div className='fixed ... border-dashed' />
// Render the markdown
<article className='w-full pt-5 pl-6'>
<Markdown
className='prose prose-invert min-w-full'
>
{source}
</Markdown>
</article>
...
)
}
Now if you type any markdown you'd still not find any changes. This is because we forgot to add the @tailwindcss/typography
plugin to the tailwindcss config 💀
Change your tailwind.config.ts
to the following:
import type { Config } from 'tailwindcss'
const config: Config = {
content: [
'./src/pages/**/*.{js,ts,jsx,tsx,mdx}',
'./src/components/**/*.{js,ts,jsx,tsx,mdx}',
'./src/app/**/*.{js,ts,jsx,tsx,mdx}',
],
// Add the plugin here
plugins: [require('@tailwindcss/typography')]
}
export default config
Now write some markdown and you will see the changes live 🚀
4. Code Highlighting and Custom Components
Now we need to install the react-syntax-highlighter
package to add code highlighting to our project.
npm i react-syntax-highlighter
npm i --save @types/react-syntax-highlighter
Now we are going to create a Custom Component for the Code Highlighter.
Create a folder called components
inside the src
folder. Now create a file called Code.tsx
inside the components folder.
Add the following code from the documentation of react-syntax-highlighter:
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import { materialOceanic } from 'react-syntax-highlighter/dist/cjs/styles/prism';
export const CodeBlock = ({ ...props }) => {
return (
<SyntaxHighlighter
language={props.className?.replace(/(?:lang(?:uage)?-)/, '')}
style={materialOceanic}
wrapLines={true}
className='not-prose rounded-md'
>
{props.children}
</SyntaxHighlighter>
)
}
Here the props contain a classname with the language of the code in the format: lang-typescript
or sometimes language-typescript
so we use some regexto remove everything except the name of the language. The not-prose
classname is going to remove the default typography styles.
Now comeback to the main page.tsx
file and import the CodeBlock
component and pass it to the original <Markdown />
component
import Markdown from 'react-markdown'
import { CodeBlock } from '@/components/Code'
const Homepage = () => {
const options = { code: CodeBlock }
return (
...
<Markdown
className='prose prose-invert min-w-full'
components={options}
>
{source}
</Markdown>
...
)
}
This is going to replace every occurrence of code
with our Custom CodeBlock
component.
Optional
BUG (🐛): You might have a weird dark border around your code component which is caused by the pre
tag and tailwind styles.
To fix this go back to your Code.tsx
and add the following code that removes the tailwind styles from the pre tag.
export const Pre = ({ ...props }) => {
return (
<div className='not-prose'>
{props.children}
</div>
)
}
Import this into your page.tsx
and add it into the options
variable:
const Homepage = () => {
const options = {
code: CodeBlock,
// Add here
pre: Pre,
}
return ( ... )
}
This is going to remove that border.
5. Adding Rehype and Remark Plugins
Rehype and Remark are plugins used to transform and manipulate the HTML and Markdown content of a website, helping to enhance its functionality and appearance.
We are going to use the following:
-
rehype-sanitize
: Sanitize the markdown -
rehype-external-links
: Add an 🔗 icon on links -
remark-gfm
: Plugin to support GFM (Supporting tables, footnotes, etc.)
Install the Plugins:
npm i remark-gfm rehype-external-links rehype-sanitize
Back to our page.tsx
import remarkGfm from 'remark-gfm'
import rehypeSanitize from 'rehype-sanitize'
import rehypeExternalLinks from 'rehype-external-links'
...
<Markdown
...
remarkPlugins={[remarkGfm]}
rehypePlugins={[
rehypeSanitize,
[rehypeExternalLinks,
{ content: { type: 'text', value: '🔗' } }
],
]}
>{source}</Markdown>
Pass the remark plugins within remarkPlugins
and rehype plugins in rehypePlugins
(I know very surprising).
If any plugin needs any customization put them in square brackets followed by the plugin name and the options in curly brackets in this syntax: [veryCoolPlugin, { { options } }]
6. Header with Markdown buttons
Next we add a Header component that has buttons which on clicked inserts certain Markdown elements.
First create a Header.tsx
in the components
folder and write the following code:
const Header = () => {
const btns = [
{ name: 'B', syntax: '**Bold**' },
{ name: 'I', syntax: '*Italic*' },
{ name: 'S', syntax: '~Strikethrough~' },
{ name: 'H1', syntax: '# ' },
]
return (
<header className="flex ... bg-[#253237]">
{btns.map(btn => (
<button
key={btn.syntax}
className="flex ...rounded-md"
>
{btn.name}
</button>
))}
</header>
)
}
export default Header
Import it in the main page.tsx
import Header from '@/components/Header'
const Homepage = () => {
const options = { code: CodeBlock }
return (
<>
<Header /> // Should be on top
<div className='h-screen flex justify-between'>
...
</div>
</>
)
}
Now here's the catch. Our states lie in the parent component and the Header
is a child component.
How do we work with the states in the child component? The best solution is we create a function to change the state in parent component and pass the function to the child component. Read this article
const Homepage = () => {
const [source, setSource] = useState('');
const feedElement = (syntax: string) => {
return setSource(source + syntax)
}
return (
<>
<Header />
...
)
}
In Header.tsx
we need to accept the function as a parameter and add it to the button using the onClick
attribute:
const Header = (
{ feedElement }:
{ feedElement: (syntax: string) => void }
) => {
const btns = [ ... ]
return (
...
<button
key={btn.syntax}
className="flex ...rounded-md"
onClick={() => feedElement(btn.syntax)}
>
{btn.name}
</button>
)
}
Back to page.tsx
we pass the feedElement
function to the Header
const feedElement = (syntax: string) => {
return setSource(source + syntax)
}
return (
<>
<Header feedElement={feedElement} />
...
)
Now anytime you click on a button you should get the following Markdown Element.
Wrapping Up
There you go. We now have a fully functioning Markdown Editor built using Nextjs.
If you liked this article or gained something please give this one a red heart 💖 and follow me for more.
Top comments (4)
Nice Tutorial!
Thanks. Can you suggest some more projects like this?
Nice 🔥
Thank you 😊