DEV Community

Cover image for I built an Markdown editor using Next.js and TailwindCss 🔥
Zeeshan 🎖
Zeeshan 🎖

Posted on

I built an Markdown editor using Next.js and TailwindCss 🔥

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
Enter fullscreen mode Exit fullscreen mode

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
        />
        ...
    )
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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>
        ...
    )
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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>
  )
}
Enter fullscreen mode Exit fullscreen mode

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>
        ...
    )
}
Enter fullscreen mode Exit fullscreen mode

This is going to replace every occurrence of code with our Custom CodeBlockcomponent.

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>
  )
}
Enter fullscreen mode Exit fullscreen mode

Import this into your page.tsx and add it into the options variable:

const Homepage = () => {
    const options = { 
        code: CodeBlock,
        // Add here
        pre: Pre,
    }
    return ( ... )
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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>
        </>
    )
}
Enter fullscreen mode Exit fullscreen mode

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 />
    ...
  )
}
Enter fullscreen mode Exit fullscreen mode

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>
  )
}
Enter fullscreen mode Exit fullscreen mode

Back to page.tsx we pass the feedElement function to the Header

const feedElement = (syntax: string) => {
  return setSource(source + syntax)
}
  
return (
  <>
  <Header feedElement={feedElement} />
  ...
)
Enter fullscreen mode Exit fullscreen mode

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.

Like

Top comments (4)

Collapse
 
iamspathan profile image
Sohail Pathan

Nice Tutorial!

Collapse
 
acidop profile image
Zeeshan 🎖

Thanks. Can you suggest some more projects like this?

Collapse
 
taqui_786 profile image
TAQUI ⭐

Nice 🔥

Collapse
 
acidop profile image
Zeeshan 🎖

Thank you 😊