Build a Next.js Interactive Web-Based Markdown Notes App

Written by

in

In today’s fast-paced digital world, the ability to quickly jot down ideas, organize thoughts, and format text seamlessly is crucial. Markdown, a lightweight markup language, has become the go-to for its simplicity and readability. Imagine being able to create and edit Markdown notes directly in your browser, with a real-time preview of how your text will look. This is where a web-based Markdown Notes app built with Next.js becomes incredibly useful. This tutorial will guide you, step-by-step, through building your own interactive Markdown Notes app.

Why Build a Markdown Notes App?

There are several compelling reasons to embark on this project:

  • Learn Next.js: This project provides a practical, hands-on opportunity to learn and solidify your understanding of Next.js fundamentals, including routing, components, state management, and server-side rendering.
  • Improve Productivity: A web-based Markdown editor allows you to take notes from any device with a browser, enhancing your productivity and organization.
  • Understand Markdown: Building this app will familiarize you with Markdown syntax, making you more proficient in using it for various writing tasks.
  • Develop a Portfolio Piece: This project is an excellent addition to your portfolio, showcasing your skills in front-end development and your ability to build interactive web applications.

Prerequisites

Before we dive in, ensure you have the following:

  • Node.js and npm (or yarn): Make sure you have Node.js and npm (Node Package Manager) or yarn installed on your system. This is essential for managing project dependencies and running the development server.
  • Basic JavaScript Knowledge: A foundational understanding of JavaScript, including concepts like variables, functions, and objects, is necessary to follow along.
  • Text Editor or IDE: Choose a code editor or Integrated Development Environment (IDE) like VS Code, Sublime Text, or Atom.

Step-by-Step Guide

Step 1: Setting Up Your Next.js Project

Let’s start by creating a new Next.js project. Open your terminal and run the following command:

npx create-next-app markdown-notes-app
cd markdown-notes-app

This command creates a new Next.js project named “markdown-notes-app”. It also navigates you into the project directory.

Step 2: Installing Dependencies

Next, we need to install a few dependencies to help us with Markdown parsing and styling. Run the following command in your terminal:

npm install react-markdown remark-html

Here’s what these dependencies do:

  • react-markdown: A React component that renders Markdown.
  • remark-html: A plugin for remark, a Markdown processor, that converts Markdown to HTML.

Step 3: Creating the Markdown Editor Component

Create a new file named MarkdownEditor.js in the components directory (you might need to create this directory). This component will handle the Markdown input and the preview.

// components/MarkdownEditor.js
import React, { useState } from 'react';
import ReactMarkdown from 'react-markdown';

function MarkdownEditor() {
  const [markdown, setMarkdown] = useState('');

  const handleChange = (event) => {
    setMarkdown(event.target.value);
  };

  return (
    <div className="container">
      <div className="editor-container">
        <textarea
          className="editor"
          value={markdown}
          onChange={handleChange}
          placeholder="Write your Markdown here..."
        />
      </div>
      <div className="preview-container">
        <ReactMarkdown className="preview" children={markdown} />
      </div>
      <style jsx>{`
        .container {
          display: flex;
          flex-direction: column;
          padding: 20px;
          font-family: sans-serif;
        }

        .editor-container, .preview-container {
          margin-bottom: 20px;
        }

        .editor {
          width: 100%;
          height: 300px;
          padding: 10px;
          font-size: 16px;
          border: 1px solid #ccc;
          border-radius: 4px;
          resize: vertical;
        }

        .preview {
          padding: 10px;
          border: 1px solid #ccc;
          border-radius: 4px;
          background-color: #f9f9f9;
          line-height: 1.6;
        }

        @media (min-width: 768px) {
          .container {
            flex-direction: row;
          }

          .editor-container, .preview-container {
            width: 50%;
            margin: 0 10px;
          }
        }
      `}</style>
    </div>
  );
}

export default MarkdownEditor;

Let’s break down this code:

  • Import Statements: We import `useState` from React for managing the state of the Markdown text and `ReactMarkdown` from `react-markdown` for rendering Markdown.
  • State Management: `useState(”)` initializes a state variable called `markdown` to an empty string. This variable will hold the user’s input.
  • handleChange Function: This function updates the `markdown` state whenever the user types in the textarea.
  • JSX Structure: The component renders a `div` with class `container`. Inside, there are two `div` elements, one for the editor (textarea) and one for the preview.
  • Textarea: The `textarea` element allows the user to input Markdown. The `value` prop is bound to the `markdown` state, and `onChange` calls the `handleChange` function.
  • ReactMarkdown Component: The `ReactMarkdown` component renders the Markdown text in the `preview` div. The `children` prop is set to the `markdown` state.
  • Styling: Inline CSS using `<style jsx>` is used to style the editor and preview areas. This includes responsive design for larger screens.

Step 4: Integrating the Editor into the Page

Now, let’s integrate the `MarkdownEditor` component into our main page. Open pages/index.js and modify it as follows:

// pages/index.js
import MarkdownEditor from '../components/MarkdownEditor';

function Home() {
  return (
    <div>
      <MarkdownEditor />
    </div>
  );
}

export default Home;

This code imports the `MarkdownEditor` component and renders it within the main page.

Step 5: Running Your Application

Now, it’s time to run your application. In your terminal, run the following command:

npm run dev

This command starts the Next.js development server. Open your web browser and go to http://localhost:3000. You should see your Markdown editor and preview!

Advanced Features and Enhancements

Once you have the basic app working, you can enhance it with more features:

1. Add Markdown Syntax Highlighting

To highlight Markdown syntax, you can use a library like `prismjs`. Install it using:

npm install prismjs

Then, modify your `MarkdownEditor.js` component to include:

// components/MarkdownEditor.js
import React, { useState, useEffect } from 'react';
import ReactMarkdown from 'react-markdown';
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import { dark } from 'react-syntax-highlighter/dist/cjs/styles/prism';

function MarkdownEditor() {
  const [markdown, setMarkdown] = useState('');

  const handleChange = (event) => {
    setMarkdown(event.target.value);
  };

  const renderers = {
    code: ({ language, value }) => {
      return (
        <SyntaxHighlighter style={dark} language={language} children={value} />
      );
    },
  };

  return (
    <div className="container">
      <div className="editor-container">
        <textarea
          className="editor"
          value={markdown}
          onChange={handleChange}
          placeholder="Write your Markdown here..."
        />
      </div>
      <div className="preview-container">
        <ReactMarkdown className="preview" children={markdown} renderers={renderers} />
      </div>
      <style jsx>{`
        // ... (rest of your styles)
      `}</style>
    </div>
  );
}

export default MarkdownEditor;

This code:

  • Imports `Prism` and the `dark` theme.
  • Defines a `renderers` object that overrides the default `code` rendering to use `SyntaxHighlighter`.
  • Passes the `renderers` prop to `ReactMarkdown`.

2. Implement Local Storage for Saving Notes

To save the notes to the browser’s local storage, update your `MarkdownEditor.js` component:

// components/MarkdownEditor.js
import React, { useState, useEffect } from 'react';
import ReactMarkdown from 'react-markdown';
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import { dark } from 'react-syntax-highlighter/dist/cjs/styles/prism';

function MarkdownEditor() {
  const [markdown, setMarkdown] = useState('');

  useEffect(() => {
    // Load from local storage on component mount
    const savedMarkdown = localStorage.getItem('markdown');
    if (savedMarkdown) {
      setMarkdown(savedMarkdown);
    }
  }, []);

  useEffect(() => {
    // Save to local storage whenever markdown changes
    localStorage.setItem('markdown', markdown);
  }, [markdown]);

  const handleChange = (event) => {
    setMarkdown(event.target.value);
  };

  const renderers = {
    code: ({ language, value }) => {
      return (
        <SyntaxHighlighter style={dark} language={language} children={value} />
      );
    },
  };

  return (
    <div className="container">
      <div className="editor-container">
        <textarea
          className="editor"
          value={markdown}
          onChange={handleChange}
          placeholder="Write your Markdown here..."
        />
      </div>
      <div className="preview-container">
        <ReactMarkdown className="preview" children={markdown} renderers={renderers} />
      </div>
      <style jsx>{`
        // ... (rest of your styles)
      `}</style>
    </div>
  );
}

export default MarkdownEditor;

Here’s what changed:

  • useEffect for Loading: The first `useEffect` hook runs when the component mounts. It attempts to load the Markdown from local storage using `localStorage.getItem(‘markdown’)`. If a value exists, it sets the `markdown` state.
  • useEffect for Saving: The second `useEffect` hook runs whenever the `markdown` state changes. It saves the current Markdown to local storage using `localStorage.setItem(‘markdown’, markdown)`.

3. Add Features for File Upload and Download

To enable file upload, you can add an input field in the editor and use a package like `file-saver` to download the content.

// components/MarkdownEditor.js
import React, { useState, useEffect } from 'react';
import ReactMarkdown from 'react-markdown';
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import { dark } from 'react-syntax-highlighter/dist/cjs/styles/prism';
import { saveAs } from 'file-saver';

function MarkdownEditor() {
  const [markdown, setMarkdown] = useState('');

  useEffect(() => {
    const savedMarkdown = localStorage.getItem('markdown');
    if (savedMarkdown) {
      setMarkdown(savedMarkdown);
    }
  }, []);

  useEffect(() => {
    localStorage.setItem('markdown', markdown);
  }, [markdown]);

  const handleChange = (event) => {
    setMarkdown(event.target.value);
  };

  const handleDownload = () => {
    saveAs(new Blob([markdown], { type: "text/markdown;charset=utf-8" }), "my-note.md");
  };

  const renderers = {
    code: ({ language, value }) => {
      return (
        <SyntaxHighlighter style={dark} language={language} children={value} />
      );
    },
  };

  return (
    <div className="container">
      <div className="editor-container">
        <textarea
          className="editor"
          value={markdown}
          onChange={handleChange}
          placeholder="Write your Markdown here..."
        />
        <button onClick={handleDownload}>Download</button>
      </div>
      <div className="preview-container">
        <ReactMarkdown className="preview" children={markdown} renderers={renderers} />
      </div>
      <style jsx>{`
        // ... (rest of your styles)
        button {
          margin-top: 10px;
          padding: 10px 20px;
          background-color: #4CAF50;
          color: white;
          border: none;
          border-radius: 4px;
          cursor: pointer;
        }
      `}</style>
    </div>
  );
}

export default MarkdownEditor;

In this code, we have:

  • Imported `saveAs` from `file-saver`.
  • Added a `handleDownload` function that uses `saveAs` to trigger a file download.
  • Added a button to trigger the download.

4. Implement Image Upload

Implementing image upload involves adding an input of type ‘file’, handling the selection and upload of the file, and displaying the image in the preview. This typically involves handling the file input change event, reading the file data, and generating a URL for the image to be displayed within the Markdown. This is a more involved process. Here is a simplified version of the code for image upload.


import React, { useState, useEffect } from 'react';
import ReactMarkdown from 'react-markdown';
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import { dark } from 'react-syntax-highlighter/dist/cjs/styles/prism';
import { saveAs } from 'file-saver';

function MarkdownEditor() {
  const [markdown, setMarkdown] = useState('');
  const [image, setImage] = useState(null);

  useEffect(() => {
    const savedMarkdown = localStorage.getItem('markdown');
    if (savedMarkdown) {
      setMarkdown(savedMarkdown);
    }
  }, []);

  useEffect(() => {
    localStorage.setItem('markdown', markdown);
  }, [markdown]);

  const handleChange = (event) => {
    setMarkdown(event.target.value);
  };

  const handleDownload = () => {
    saveAs(new Blob([markdown], { type: "text/markdown;charset=utf-8" }), "my-note.md");
  };

  const handleImageChange = (event) => {
    const file = event.target.files[0];
    if (file) {
      const reader = new FileReader();
      reader.onload = (e) => {
        const imageUrl = e.target.result;
        setMarkdown(prevMarkdown => `${prevMarkdown}![](${imageUrl})`);
      };
      reader.readAsDataURL(file);
    }
  };

  const renderers = {
    code: ({ language, value }) => {
      return (
        <SyntaxHighlighter style={dark} language={language} children={value} />
      );
    },
  };

  return (
    <div className="container">
      <div className="editor-container">
        <textarea
          className="editor"
          value={markdown}
          onChange={handleChange}
          placeholder="Write your Markdown here..."
        />
        <button onClick={handleDownload}>Download</button>
        <input type="file" accept="image/*" onChange={handleImageChange} />
      </div>
      <div className="preview-container">
        <ReactMarkdown className="preview" children={markdown} renderers={renderers} />
      </div>
      <style jsx>{`
        // ... (rest of your styles)
        button {
          margin-top: 10px;
          padding: 10px 20px;
          background-color: #4CAF50;
          color: white;
          border: none;
          border-radius: 4px;
          cursor: pointer;
        }
      `}</style>
    </div>
  );
}

export default MarkdownEditor;

This code:

  • Adds an image input and a handler function.
  • Uses a `FileReader` to read the image and generate a data URL.
  • Appends the image markdown to the markdown string.

Common Mistakes and Troubleshooting

Here are some common mistakes and how to fix them:

  • Incorrect Dependency Installation: Double-check that you have installed the correct dependencies. Run npm install react-markdown remark-html again to ensure they are installed.
  • Syntax Errors: Carefully review your code for any syntax errors, especially in the JSX. Use your browser’s developer tools to identify and fix errors.
  • Missing Import Statements: Make sure you have imported all necessary components and functions.
  • Incorrect File Paths: Ensure your file paths are correct, especially when importing components.
  • CSS Issues: If the styling doesn’t look right, inspect the CSS using your browser’s developer tools. Make sure the CSS is being applied correctly and that there are no conflicting styles.
  • Markdown Rendering Issues: If the Markdown isn’t rendering correctly, double-check that you are using the correct Markdown syntax. Also, verify that the `ReactMarkdown` component is correctly configured.

Key Takeaways

  • You’ve learned how to set up a Next.js project.
  • You’ve created a basic Markdown editor using React and Markdown rendering libraries.
  • You understand how to manage state and handle user input.
  • You’ve seen how to add basic styling with CSS.
  • You know how to save the notes and download them.

FAQ

1. How do I deploy this app?

You can deploy your Next.js app to various platforms, such as Vercel, Netlify, or AWS. Vercel is the easiest option as it’s specifically designed for Next.js apps. Simply connect your GitHub repository, and Vercel will handle the build and deployment process.

2. Can I add more features to this app?

Absolutely! You can enhance the app with features such as:

  • Different themes: Implement light and dark themes.
  • Note organization: Add folders and tags to organize notes.
  • Rich text formatting: Support bold, italics, and other formatting options.
  • Collaboration: Allow users to share and collaborate on notes in real-time.

3. How can I improve the performance of the app?

Here are some tips for improving performance:

  • Code Splitting: Next.js automatically splits your code into smaller chunks.
  • Image Optimization: Use Next.js’s image optimization features to optimize images.
  • Caching: Implement caching strategies to reduce server load.
  • Lazy Loading: Lazy-load components and images that are not immediately visible.

4. How do I handle larger Markdown files?

For larger Markdown files, consider the following:

  • Pagination: Break down long notes into pages.
  • Virtualization: Use virtualization techniques to render only the visible part of the note.
  • Optimization: Optimize the Markdown rendering process to avoid performance bottlenecks.

5. How can I add authentication to the app?

To add authentication, you’ll need to use a library or service like:

  • NextAuth.js: A popular authentication library for Next.js.
  • Firebase Authentication: Google’s authentication service.
  • Auth0: A third-party authentication provider.

You would integrate these services into your application to enable users to sign up, log in, and manage their accounts. You would then need to secure your API routes and restrict access to the notes based on the logged-in user.

By following these steps, you should now have a functional web-based Markdown Notes app. This is just the beginning; you can expand upon this foundation and create a powerful and useful tool for your personal or professional use. Remember to continuously experiment, learn, and explore new features to make your app even better. The beauty of Next.js lies in its flexibility and ease of use, making it an excellent choice for this type of project.