Developing a modern Blogging Application with Next.js and Editor.js

2021-7-16

If you are within the React ecosystem, I am sure you have heard of Next.js( a production-ready framework for react), and on the other hand, Editor.js is a modern block-styled editor that has some exciting features.

Recently, I created a blogging platform as a personal project and used these technologies in the front-end. Even though both of these technologies are great, it took some tinkering in integrating them, as, as of now, Editor.js only works on the client-side, and the whole point of using Next.js was leveraging its server-side rendering feature. In this post, I will go through how I went about integrating them.

Project Setup

First, let's start with an empty Next.js project.

npx create-next-app next-editor-js-example

To make this work, firstly, we will need Editor.js. Other than that, we also have plugins that add to the editor to provide more features. Let’s install those.

npm i @editorjs/editorjs @editorjs/delimiter @editorjs/embed @editorjs/header @editorjs/list @editorjs/marker @editorjs/paragraph

Finally, we have a wrapper react component for editor.js that makes our work so much simpler. It also supports all major editor.js plugins, so it is a great one to use. We will install that too.

npm i react-editor-js

To start, let’s create a new route /blog/create. Doing this is fairly simple in Next.js as we just need to add this path in the pages folder in our project root. While we are here, let's also set up other files. We also want a dynamic page for blogs, as we will also look at how to server-side render data output by editor.js. So now we also have, /blog/[slug] in pages. Finally, create a components folder in root, in there add two files, Editor/Editor.js, Editor/EditorConstants.js. With this, the final folder structure of our project will look like this.

Setting up the Editor component

While I was thinking about structuring the code and creating the custom Editor component, one thing I noticed was that it needs to be reusable. Say I have a create page to create new blogs and an edit page to edit existing blogs. My editor component should be such that, I should be able to include it in either of these pages, and it should work perfectly. Now, react-editor-js is already a wrapper around editor.js and does a pretty good job, but since the create and edit pages were going to be so similar, I decided to take it one step further. We also need to take care of the fact that react-editor-js and the plugins won't work on server-side rendering, so, creating a custom component with react-editor-js might be a good idea here.

First, bring in the plugins in the EditorConstants.js file, import all the plugins you are using, and export them as one object.

import Embed from '@editorjs/embed';
import Header from '@editorjs/header';
import Delimiter from '@editorjs/delimiter';
import List from '@editorjs/list';
import Marker from '@editorjs/marker';

const constants = {
  embed: Embed,
  list: List,
  marker: Marker,
  header: Header,
  delimiter: Delimiter,
};

export default constants;

Now let’s work on the Editor.js file(our custom editor component). Since editor.js doesn’t work with SSR, we need to find a workaround here which imports editor.js only once the code is running on the client-side. Next.js gives us an elegant way to solve this through dynamic imports and specifying the {ssr: false} option. Using this, we can dynamically import the react-editor-js package. But we also have the plugins, which are also to be included on the client-side. I tried to do this in many ways, but if we want to import all the plugins at once using the EditorConstants.js module that we created, I found the most effective way to do it is using a useEffect hook to dynamically import the plugins. The useEffect ensures that the module is imported only on the client-side.

Let’s also create a save button and an onSaveHandler which receives the editor instance and gives us the data that we added in the editor. We can then have an onSave prop passed down from the parent that triggers a function in the parent and provides the content of the blog. The example below will make this clear.

Finally, I also added two input fields for title and description as we might want such things in a real blog creator.

import { useEffect, useState } from 'react';
import Head from 'next/head';
import dynamic from 'next/dynamic';
const EditorJs = dynamic(() => import('react-editor-js'), { ssr: false });

let editorInstance;

const Editor = (props) => {
  const [title, setTitle] = useState('');
  const [description, setDescription] = useState('');
  const [editorTools, setEditorTools] = useState();

  const onSaveHandler = async (editorInstance) => {
    try {
      const blogData = await editorInstance.save();
      if (!title || title === '')
        throw new Error('Title cannot be empty. Please enter title');
      if (!blogData.blocks[0])
        throw new Error('Blog cannot be empty. Please enter some data');
      props.onSave(blogData, title, description);
    } catch (err) {
      console.log(err);
    }
  };

  let editorComponent;
  if (!editorTools) editorComponent = 'Loading...';
  else {
    editorComponent = (
      <EditorJs
        instanceRef={(instance) => (editorInstance = instance)}
        tools={editorTools}
        placeholder={`Let's write an awesome blog!`}
      />
    );
  }

  useEffect(() => {
    const importConstants = async () => {
      const tools = (await import('../../components/Editor/EditorConstants'))
        .default;
      setEditorTools(tools);
    };

    importConstants();
  }, []);

  const inputStyle = {
    maxWidth: '500px',
    marginBottom: '20px',
    height: '30px',
  };

  return (
    <div style={{ display: 'flex', flexDirection: 'column' }}>
      <Head>
        <title>Create Blog</title>
        <meta name='description' content='Generated by create next app' />
      </Head>

      <input
        style={inputStyle}
        placeholder='Your Blog Title'
        value={title}
        onChange={(event) => setTitle(event.target.value)}
      />

      <input
        style={inputStyle}
        placeholder='Your Blog Description'
        value={description}
        onChange={(event) => setDescription(event.target.value)}
      />

      {editorComponent}

      <div style={{ textAlign: 'center' }}>
        <button onClick={() => onSaveHandler(editorInstance)}>Save</button>
      </div>
    </div>
  );
};

export default Editor;

Using the editor in the app

Now let’s go to the create page and use our Editor component. We just need to import the custom Editor component and pass an onSave prop. The onSave prop will link to a function in the create page that will receive the data given by the custom Editor. Now, we can do anything with this data, like sending it to a server to store it in a database.

import Head from 'next/head';
import Editor from '../../components/Editor/Editor';

const CreateBlog = (props) => {
  const onSaveHandler = async (blogData, title, description) => {
    const toSaveData = {
      title,
      blogData,
      description,
    };

    console.log(toSaveData);
    //make your ajax call to send the data to your server and save it in a database
  };

  return (
    <div style={{ width: '80%', margin: '0 auto' }}>
      <Head>
        <title>Create new blog</title>
      </Head>
      <h1>Create Blog</h1>
      <Editor
        onSave={(editorData, title, description) =>
          onSaveHandler(editorData, title, description)
        }
      />
    </div>
  );
};

export default CreateBlog;

With this, we are done with our editor. Similarly, we can also create an edit page to edit the existing blogs, here, first we fetch the required blog from the server and pass it into our custom editor. Now in the custom editor, we need to do some refactoring to accept this data and display it in the editor. In the onSave function on the edit page, we can do something like a patch request to the server to change existing blogs after receiving the data from the editor component.

Displaying blogs through SSR

We created our blog, saved it, and that's great. But we also want to be able to display the blogs through SSR to be SEO friendly. Now, one can surely write custom renderers, which receive the data saved using Editor.js and render them with HTML and CSS. But there is again a great package that will do this for you and will serve most of your needs.

npm i editorjs-react-renderer

We just need to import it into our dynamic blog page/pages/blog/[slug] and pass on the blog data created through editor.js.

import Output from 'editorjs-react-renderer';

const BlogDetail = (props) => {
  const { data, error } = props;

  if (error) {
    console.log(error);
    return null;
  }

  return (
    <div style={{ width: '80%', margin: '0 auto' }}>
      <h1>{data.title}</h1>

      <div style={{ marginBottom: '3rem' }}>{data.description}</div>

      <div style={{ maxWidth: '800px', margin: '0 auto' }}>
        <Output data={data.blogData} />
      </div>
    </div>
  );
};

export default BlogDetail;

export async function getServerSideProps({ query }) {
  const { slug } = query;

  //make an ajax call to get your blog

  return {
    props: {
      data: {
        //return your blog data saved through editor.js
      },
    },
  };
}

export default BlogDetail;

Conclusion

You can see an example of this in this Github repo. If you want a more complete example, I also recently created a full-fledged blogging application with Node.js and MongoDB for backend and Next.js, Editor.js, and Material UI for frontend. You can check out the repo for that here.

Hope you will create something great using these tools and technologies.
Happy coding :)