Building a Custom Markdown Previewer with Next.js and Styled Components.

·

12 min read

Originally Published at OpenReplay

Markdown created by John Gruber in 2004 is one of the most popular markup languages. The Markdown format converts plain text into HTML and is easy to write and read. The reason Markdown is popular is that the syntax used is simple to grasp. In addition, people who do not know HTML can use Markdown and convert their documents automatically into HTML pages.

We are aiming to create a Markdown previewer with Next.js, and customize the components using Styled-Components. By the end of this article, you will be able to implement the previewer on a blog project.

Prerequisites

  1. Node.js Development Environment for Next.js installation.
  2. Basic knowledge of React, Styled-Components, and Markdown.
  3. Visual Studio code editor.

Development Environment Setup

The most important thing is ensuring that we have a Node.js environment setup in our Operating System. We can first check the version of Node.js installed.

node -v

If you are not yet a Node.js user, follow the instructions in this link. The process may differ depending on the operating system. After a successful installation of Node.js, we can begin by initializing our project. In this phase, we will create a Next.js app using create-next-app. Next.js community has made the installation process easier by implementing program prompts that guide developers during the process. Open the terminal by typing “cmd” in the program search area, then use the command below to create a Next.js app.

npx create-next-app@latest

If the command doesn’t work for you, you can try these two alternatives.

yarn create-next-app

or

pnpm create-next-app

In our case, we will name our project markdownpreviewer and since we only need JavaScript, we will not include the typescript template, select no when prompted. We now have a new directory called markdownpreviewer. Let’s navigate into it:

cd markdownpreviewer

Then, run the project using the command:

npm run dev

The web app is now visible in our browser.

Installing Required Packages

The next step is installing the packages needed for our project. The first package will be Styled-Components. This is our choice for styling our application and it will also help us when customizing our markdown output.

npm i styled-components

React-Markdown We will use React-markdown for our markdown rendering logic. Let’s install the package.

npm i react-markdown

Now we are ready for the fun part. Let’s ensure that the room has no distractions before starting our hacking endeavor.

Building the markdown previewer

Since we had already installed the dependencies, we can open the editor. With visual studio code installed, we can open it from our command prompt using the command below.

cd markdownpreviewer
code .

When we press enter, the editor will open with the project as the current workspace.

The first thing that we should do is try and re-create our project structure. Mostly, it’s recommended that we create a source folder src, and get every project file from that folder.

Project Structure:

If you are having trouble setting up Styled-Components, I have you covered. Let’s clear the files in our styles folder and then paste this code in a new Globals.js file.

Globals.js

import { createGlobalStyle } from "styled-components";

const GlobalStyle = createGlobalStyle`
body{
background:white;
margin:0;
padding:0;
}
`;

export default GlobalStyle;

The second step is importing and using our styles in our root page, which is _app.js in our pages folder.

_app.js

import { Fragment } from "react";
import GlobalStyle from "../styles/Globals";

function MyApp({ Component, pageProps }) {
  return (
    <Fragment>
      <GlobalStyle />
      <Component {...pageProps} />
    </Fragment>
  );
}

export default MyApp;

After setting the global styled-components, let’s create a Previewer.js file in our components folder, here we will create a functional component that will hold the logic needed for both our editor and previewer. Just as usual, we will start by importing our modules at the top of our file.

Previewer.js

import React from "react";
import { useState } from "react";
import styled from "styled-components";
import ReactMarkdown from "react-markdown";

At the bottom of our imports, create an Editortool functional component by adding the code below.

Previewer.js

export function Editortool() {
  const [form, setForm] = useState({ markdown: "" });

  const handleChange = (e) => {
    setForm({
      ...form,
      [e.target.name]: e.target.value,
    });
  };

  return (
    <div>
      <h1>Markdown Previewer</h1>
      <div>
        <textarea
          id="textarea_id"
          name="markdown"
          type="text"
          onChange={handleChange}
        />
        <ReactMarkdown>{form.markdown}</ReactMarkdown>
      </div>
    </div>
  );
}

We need to define the method that we want to use when handling our state. Since we have a functional component, we will use useState hook. Define the variable form as our current state, and update it through setForm function. We then set our current state to an empty string useState({markdown:’’}).

The handleChange arrow function is reserved for handling the change effect when typing in our textarea. The change of an input element is then detected through the onChange event. After creating the component we import it into our index.js page, which serves as our home page.

index.js

import Editortool from "../components/Editor"

export default function Home() {
  return (
    <>
      <Editortool />
    </>
  );
}

If we run our code, we get that the editor and previewer are working, but it still needs to be styled. Let’s execute the code using the command below.

npm run dev

Clicking on the link provided opens the browser.

Let’s create our styling components. The styled-components syntax involves creating a variable and assigning the styled function to it. We use the CSS inside backticks as Styled-Components.


const Styledexample = styled.div`
  position: absolute;
  top: 60px;
  min-height: 90%;
  width: 90vw;
  left: 5%;
`;

We will need three Styled-Components for our styling; Markdowncontainer, Editor, and Reviewarea components. Let’s add them to the code.

import React from "react";
import { useState } from "react";
import styled from "styled-components";
import ReactMarkdown from "react-markdown";

const H1 = styled.h1`
  position: absolute;
  left: 35%;
`;
const MarkdownContainer = styled.div`
  position: absolute;
  top: 60px;
  min-height: 90%;
  width: 90vw;
  left: 5%;
  display: grid;
  grid-template-columns: 1fr 1fr;
  grid-gap: 8px;
  border: 1px solid black;
  box-sizing: border-box;
`;
const Editor = styled.textarea`
  top: 0;
  font-size: 18px;
  background: #1e2126;
  color: white;
  border: 1px solid #c4c4c4;
  box-sizing: border-box;
  max-width: 45vw;
`;
const Reviewarea = styled(ReactMarkdown)`
  top: 0;
  padding: 20px;
  font-size: 18px;
  background: white;
  border: 1px solid blue;
  box-sizing: border-box;
  max-width: 45vw;
`;

export function Editortool() {
  const [form, setForm] = useState({ markdown: "" });

  const handleChange = (e) => {
    setForm({
      ...form,
      [e.target.name]: e.target.value,
    });
  };
  return (
    <div>
      <H1>Markdown Previewer</H1>
      <MarkdownContainer>
        <Editor
          id="textarea_id"
          name="markdown"
          type="text"
          onChange={handleChange}
        />
        <Reviewarea components={CodeBlock} remarkPlugins={[remarkGfm]}>
          {form.markdown}
        </Reviewarea>
      </MarkdownContainer>
    </div>
  );
}

The CSS in our styled component uses the grid system to arrange the elements, using Styled-Components is just like using CSS in JavaScript directly. The final output is created after running the code.

With fewer lines of code, we have our editor and previewer running. In the next step, we will customize the different components rendered by our previewer; fonts, font family, image size, tables, and code snippets. If you are new to Styled-Components and wonder how, then this is the easiest part of this whole tutorial.

Customize Components with Styled Components

Changing the styling of the text

The need to change the font size and font family may arise because of a client’s specification, or maybe the target of a specific audience. Let's see how we go about it.

We are going to start by creating a new file in our styles folder and name it Styledcomponents.js. The file acts like an external style sheet. This makes the code look clean and easy to follow up with if any debugging is required. Let’s copy the recently created styled components from our Previewer.js file into our newly created file, adding an export declaration at the beginning.

import styled from "styled-components";
import ReactMarkdown from "react-markdown";

export const MarkdownContainer = styled.div`
  position: absolute;
  top: 60px;
  min-height: 90%;
  width: 90vw;
  left: 5%;
  display: grid;
  grid-template-columns: 1fr 1fr;
  grid-gap: 8px;
  border: 1px solid black;
  box-sizing: border-box;
`;
export const Editor = styled.textarea`
  top: 0;
  font-size: 18px;
  background: #1e2126;
  color: white;
  border: 1px solid #c4c4c4;
  box-sizing: border-box;
  max-width: 45vw;
`;
export const Reviewarea = styled(ReactMarkdown)`
  top: 0;
  padding: 20px;
  font-size: 18px;
  background: white;
  border: 1px solid blue;
  box-sizing: border-box;
  max-width: 45vw;
`;
export const H1 = styled.h1`
  position: absolute;
  left: 35%;
`;

When we use the components, we will be required to import them in our Previewer.js file.

import { Editor, H1, MarkdownContainer, Reviewarea } from “../styles/Styledcomponents”

The most common font sizes used in blog post range between 16-20 pixels. Let’s see how we can change our font sizes and aim at an older audience by changing the font size to 19 pixels and the font family to Monoscope. Add these lines of code in the Reviewarea component, this should change the styling of both heading and paragraph elements.

  h1 {
    color: brown;
    font-family: “Lucida Console”, “Courier New”, monoscope;
  }
  p {
    color: #333333;
    font-size: 21px;
  }

This is how our code will look when the styles are added.

export const Reviewarea = styled(ReactMarkdown)`
  position: relative;
  top: 0;
  padding: 20px;
  font-size: 18px;
  background: white;
  border: 1px solid blue;
  box-sizing: border-box;
  max-width: 45vw;

  h1 {
    color: brown;
    font-family: “Lucida Console”, “Courier New”, monoscope;
  }
  p {
    color: #333333;
    font-size: 21px;
  }
`;

The output created

The previewer is now customized, the header having a custom color and the paragraph font size larger than the default.

Positioning and Sizing Images

We may also need to reduce the size of images, either to reduce the size of web pages or create a custom hero image for a blog header section. If we try adding an image, we notice that it's added with its default size. This makes the previewer break since it’s larger than the previewer.

We don’t want our editor breaking each time, so let’s make our image maximum size 500px by 400px in dimension. This only takes two to three lines. The property for width and height is width:szpx; and height:szpx;. In the same Reviewarea component, add an img tag with these properties, and check the output once again.

export const Reviewarea = styled(ReactMarkdown)`
  top: 0;
  padding: 20px;
  font-size: 18px;
  background: white;
  border: 1px solid blue;
  box-sizing: border-box;
  max-width: 45vw;

  img {
    width: 500px;
    height: 400px;
  }
`;

The output generated:

As we can see, sizing images is very easy. In cases of smaller images, they tend to lie on one side. The image in the previewer below is 300px by 150px.

Let’s center the image using the margin CSS property in styled components.

export const Reviewarea = styled(ReactMarkdown)`
  top: 0;
  padding: 20px;
  font-size: 18px;
  background: white;
  border: 1px solid blue;
  box-sizing: border-box;
  max-width: 45vw;
  img {
    width: 300px;
    height: 150px;
    display: block;
    margin-left: auto;
    margin-right: auto;
  }
`;

When the margin property is set to auto on the right and left side, the image gets centered just like the one below.

Customizing and Including Support for Tables

Since table syntax in markdown isn’t considered standard, react-markdown does not support them by default. For tables to work, we need to use a plugin called remark-gfm. It’s the most suitable plugin for table functionality. Quick, let’s install the plugin using the command below.

npm i remark-gfm

We only need to import the remarkGfm module from the plugin and use it in our ReactMarkdown component. Below is the syntax used in a plain react-markdown component.

return (
  <ReactMarkdown remarkPlugins={[remarkGfm]}>{form.markdown}</ReactMarkdown>
);

Since we are using Styled-Components, the code looks different compared with the names given to our styling components.

import React from "react";
import { useState } from "react";
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";

export default function Editortool() {
  const [form, setForm] = useState({ markdown: "" });
  const handleChange = (e) => {
    setForm({
      ...form,
      [e.target.name]: e.target.value,
    });
  };

  return (
    <div>
      <MarkdownContainer>
        <Editor
          id="textarea_id"
          name="markdown"
          type="text"
          onChange={handleChange}
        />
        <Reviewarea remarkPlugins={[remarkGfm]}>{form.markdown}</Reviewarea>
      </MarkdownContainer>
    </div>
  );
}

Our previewer now supports the use of markdown table syntax, we can try it out.

| Column 1 | Column 2 | Column 3 |
| -------- | -------- | -------- |
| Text | Text | Text |

We can now view the output in our previewer.

The table is generated successfully, but there is a minor problem, there are no borders. We can add them using the CSS border property. Let’s add a table property in our Reviewarea styled component.

export const Reviewarea = styled(ReactMarkdown)`
  top: 0;
  padding: 20px;
  font-size: 18px;
  background: white;
  border: 1px solid blue;
  box-sizing: border-box;
  max-width: 45vw;
  table {
    border-collapse: collapse;
    th,
    td {
      border: 0.5px solid #dfe2e5;
    }
  }
`;

In the code above, notice that we have added a border-collapse property? This is to remove the extra space in the cells. Below is the outstanding-looking table.

With Styled Components, you have unlimited capabilities that you can implement on your own, try adding different colors to the table background and text.

Custom Code block highlighter

In the case of a technical blog, we regard a code block as an essential component. In our previewer, we will need a code block syntax highlighter, and the best library for this would be no other than react-syntax-highlighter.

npm i react-syntax-highlighter

After a successful installation, navigate to the styles folder and create a new Highligher.js file. The required module imports are going to be our first lines of code.


import React from "react";
import styled from "styled-components";
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
import { dracula } from "react-syntax-highlighter/dist/cjs/styles/prism";

We will need the SyntaxHighlighter element the most, but we will still need a theme called dracula. Create a Styled-Component and call it CodeContainer, then assign it the SyntaxHighlighter element just like in the code below.

import React from "react";
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
import styled from "styled-components";
import { dracula } from "react-syntax-highlighter/dist/cjs/styles/prism";

const Codecontainer = styled(SyntaxHighlighter)`
  width: 98%;
  position: relative;
  left: 10px;
`;

const CodeBlock = {
  code({ node, inline, className, children, ...props }) {
    const match = /language-(\w+)/.exec(className || "");
    return !inline && match ? (
      <Codecontainer
        style={dracula}
        language={match[1]}
        PreTag="div"
        {...props}
      >
        {String(children).replace(/\n$/, "")}
      </Codecontainer>
    ) : (
      <code className={className} {...props}>
        {children}
      </code>
    );
  },
};

export default CodeBlock;

The functional component created above overwrites the normal handling of a code element by passing a component into it. Since we have a prop called code, we now can change the code element by rendering custom code elements.

We also use a ternary operator in the return statement. If the typed string matches with a coding language, then the Codecontainer will be returned and a normal code block will be returned if false.

The only required step after creating this component is exporting it to our Previewer.js file.

Previewer.js

import CodeBlock from "../styles/Highlighter";

This is how our final code will look after making the import and using it in our Reviewarea component.

Previewer.js

import React from "react";
import { useState, useEffect } from "react";
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
import CodeBlock from "../styles/Highlighter";

export default function Editortool() {
  const [form, setForm] = useState({ markdown: "" });
  const handleChange = (e) => {
    setForm({
      ...form,
      [e.target.name]: e.target.value,
    });
  };
  return (
    <div>
      <MarkdownContainer>
        <Editor
          id="textarea_id"
          name="markdown"
          type="text"
          onChange={handleChange}
        />
        <Reviewarea components={CodeBlock} remarkPlugins={[remarkGfm]}>
          {form.markdown}
        </Reviewarea>
      </MarkdownContainer>
    </div>
  );
}

Let’s test our syntax highlighter, and try using backticks or tildes syntax.

Congrats on getting this far. We now have our customized Next.js Markdown Previewer.

Conclusion

When we contemplate the infinite possibilities of customizing a react markdown previewer, then what we have covered is only a foundation for more ideas. You may decide to add support for flowcharts, including a display for math expressions, or also play with distinct color patterns. It’s all about your imagination.

Let’s meet in our next tutorial. I hope you had fun.

References

  1. React Markdown
  2. Create a Next.js App
  3. Styled Components