Publishing a blog post to Hashnode using a custom editing interface

··

8 min read

Cover Image for Publishing a blog post to Hashnode using a custom editing interface

Introduction

Today, we’re going to learn how to publish blog posts using our custom editing interface. We will use Hashnode API and MDXEditor for demo purposes.

Step 1: Creating a new React project

With node.js, npm installed, please run the following command to install the React boilerplate using Vite.

npm create vite@latest my-editor -- --template react-ts

Don’t worry if you don’t have Vite installed locally, npm will prompt you to do so. Let’s install all the dependencies, including MDXEditor:

cd my-editor
npm install --save @mdxeditor/editor

Now, we are ready to serve our project using the CLI command npm run dev:

npm run dev

  VITE v5.1.3  ready in 80 ms

  ➜  Local:   http://localhost:5173/
  ➜  Network: use --host to expose
  ➜  press h + enter to show help

Hot Reloading is enabled by default. Any changes will have an immediate effect on the browser window. Navigate to http://localhost:5173/

A screenshot of a computer browser displaying a simple web application interface with the title "Vite + React" and logos of Vite and React, showing a counter set to zero.

Step 1.2: Styling

Vite serves our project from src/main.tsx. We also have an example component in src/App.tsx. Let’s clean up the boilerplate code in the following places:

  • Delete public/vite.svg

  • Delete src/assets/react.svg

  • Clean up everything in src/index.css, so the file should be completely empty. We’re going to add Tailwind in the next steps and use this file.

  • Delete src/App.css

  • Clean up the src/App.tsx The App component should look like this after the cleanup:

function App() {
  return (
    <>
        <h1>My Blog Editor</h1>
    </>
  )
}

export default App

Step 1.3: Setting up Tailwind

Install Tailwind and @tailwindcss/typograph package, using npm and npx,

npm install -D tailwindcss postcss autoprefixer @tailwindcss/typography
npx tailwindcss init -p

We also need to set up the Tailwind config file, tailwind.config.js:

/** @type {import('tailwindcss').Config} */
export default {
  content: [
    "./index.html",
    "./src/**/*.{js,ts,jsx,tsx}",
  ],
  theme: {
    extend: {},
  },
  plugins: [
    require('@tailwindcss/typography'),
  ],
}

Next, we can add the base Tailwind styles in the src/index.css file:

@tailwind base;  
@tailwind components;  
@tailwind utilities;

We’re ready to use Tailwind. Now, the npm run dev command will build Tailwind styles too! We can already wrap our app into a container by adding the following in main.tsx:

  <React.StrictMode>
      <div className="container mx-auto px-4 w-1/2 py-8">
          <App/>
      </div>
  </React.StrictMode>

That’s it! We will continue with styling later when we set up our custom editor interface.

Step 2: Setting up the editor

We can import the MDXEditor React component and use it in our project. We need to include the CSS file as well, we can do this directly in our App component:

import { MDXEditor } from '@mdxeditor/editor'
import '@mdxeditor/editor/style.css'

This is the basic setup, and this setup doesn’t allow advanced text editing. It only supports bold, italic, underline, and inline code. We can already add some other plugins to support advanced editing. MDXEditor is highly customizable, you can head to the MDXEditor docs and get more information about all available plugins and usages. For demo purposes, we will only add some of them. Let’s set up the editor component with plugins:

import {
  MDXEditor,
  BlockTypeSelect,
  UndoRedo,
  BoldItalicUnderlineToggles,
  toolbarPlugin,
  headingsPlugin,
  markdownShortcutPlugin,
  linkPlugin,
} from "@mdxeditor/editor";
import "@mdxeditor/editor/style.css";

function App() {
  const editorRef = React.useRef<MDXEditorMethods>(null);

  return (
    <>
      <h1 className="text-5xl">My Blog Editor</h1>

      <div className="prose max-w-none">
        <MDXEditor
          ref={editorRef}
          className="my-5"
          markdown="Hello world"
          plugins={[
            headingsPlugin(),
            linkPlugin(),
            markdownShortcutPlugin(),
            toolbarPlugin({
              toolbarContents: () => (
                <>
                  {" "}
                  <UndoRedo/>
                  <BlockTypeSelect/>
                  <BoldItalicUnderlineToggles/>
                </>
              ),
            }),
          ]}
        />
      </div>
    </>
  );
}

export default App;

Viola! We’re ready to write our blog posts! 🎉

A screenshot of a blog editor interface with a toolbar and sample text, displayed on a web browser with a purple wavy background.

Step 2.2 Adding the title input and publish button

Let’s create an input to write our blog titles! We can replace the h1 My Blog Editor element with a text input. Then, we can add the “Publish” button next to it. It’s really easy to style them using Tailwind. We just need to wrap it into an outer box and add the button after the h1 element. We will also leverage flexbox and ml-auto Tailwind class:

const [title, setTitle] = useState<string>('');  

return (  
  <>  
    <div className="flex">  
      <input  
        onChange={(event: React.ChangeEvent<HTMLInputElement>) => setTitle(event.target.value)}  
        value={title}  
        className="text-3xl focus:outline-none"  
        placeholder="Title..." />  
      <button  
        onClick={publishPost}  
        className="ml-auto bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">Publish  
      </button>  
    </div>
    {/* Editor code... */}
  </>  
);

Here is our initial editor design!

A screenshot of a web-based text editor interface with a document titled "Title..." that includes sample text and headings. The editor toolbar shows formatting options and a "Publish" button.

Did you notice the publishPost reference on onClick attribute? Now, we can program its functionality, the publishPost method itself. First, let’s define the API request we’re going to send to Hashnode GraphQL API. We'll use the publishPost mutation but if you don't want to publish your posts immediately, you can also use the createDraft mutation.

// imports

const publishPostMutation = `
mutation ($input: PublishPostInput!) {
  publishPost(input: $input) {
     post {
      url
     }
  }
}
`

// App component

Let’s create a corresponding type to this request so we can map the response output and use it in a type-safe way.

// imports

type ApiResponse = {
  data: {
    publishPost: {
      post: {
        url: string
      }
    }
  }
}

// mutation

// App component

Almost all Hashnode GraphQL queries can be accessed without any authentication mechanism. But, all mutations need an authentication header. We’re going to use publishPost mutation for this tutorial, therefore, we need to authenticate our user. It’s really easy to do, we just need to include our Personal Access Token (PAT) in the Authorization header. Please go ahead and grab your token from the Hashnode Developer Settings page. Click the “Generate new token” button and copy the generated token.

A screenshot of a webpage interface for managing Personal Access Tokens, featuring a description of the token's purpose and a "Generate new token" button.

Let’s put this in a const, so we can use it later. Of course, in a bigger project, you can store this information somewhere secure, and access it as you wish. For the demo purposes, we will store in a const.

const PERSONAL_ACCESS_TOKEN = 'XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX';

Next, we need our publication ID. We can easily copy the publication ID from our blog’s dashboard URL.

A screenshot of a user settings page on the Hashnode website, displaying sections for profile, basic info, social media links, and blog management options.

Web browser screenshot showing the Hashnode website with an arrow pointing to the URL in the address bar.

Now, using both the recently generated token and the publication ID, we can send a publish post request. Let’s go back to creating the publishPost function:

  const publishPost = () => {
    const response = await fetch("https://gql.hashnode.com", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        Accept: "application/json",
        Authorization: PERSONAL_ACCESS_TOKEN,
      },
      body: JSON.stringify({
        query: publishPostMutation,
        variables: {
          input: {
            publicationId: "65d99cd9447d15d98a2fc264",
            title: title,
            contentMarkdown: editorRef.current?.getMarkdown(),
          },
        },
      }),
    });
  };

Step 3: Showing success message

Now, we can add a little container that gives us feedback when we successfully publish our blog post! We can store preview URL in state and show that container based on the previewUrl state changes.

const [previewUrl, setPreviewUrl] = useState<string>("");  

return (  
  <>  
    <div id="preview-link" className={previewLink === "" ? "hidden" : "" + "absolute top-20 z-10 left-1/2 transform -translate-x-1/2 -translate-y-1/2 bg-green-200 border-2 border-green-500 rounded-lg w-1/2 px-8 py-4"}>  
      ✅ Successfully published the blog post! <br />  
      <a href={previewLink} className="text-blue-900" target="_blank">{previewLink}</a>  
    </div>
    {/* Other parts */}

A screenshot of a web development environment displaying a preview of a webpage with text content and a link notification.

Let’s add a state modifier to our request function and combine things:

const publishPost = async () => {
const response = await fetch("https://gql.hashnode.com", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    Accept: "application/json",
    Authorization: PERSONAL_ACCESS_TOKEN,
  },
  body: JSON.stringify({
    query: publishPostMutation,
    variables: {
      input: {
        publicationId: "65d99cd9447d15d98a2fc264",
        title: title,
        contentMarkdown: editorRef.current?.getMarkdown(),
      },
    },
  }),
});

const result = await response.json() as ApiResponse;

setPreviewLink(result.data.publishPost.post.url);
};

Ta-da! 🎉 We can click on the preview URL and see our new shiny blog post!

A screenshot of a blog post titled "Testing post" on Ozan Akman's team blog, featuring a brief text content and various user interaction icons.

Here is everything combined, the whole code of the App.tsx component:

import {
  MDXEditor,
  BlockTypeSelect,
  UndoRedo,
  BoldItalicUnderlineToggles,
  toolbarPlugin,
  headingsPlugin,
  markdownShortcutPlugin,
  linkPlugin, MDXEditorMethods,
} from "@mdxeditor/editor";
import "@mdxeditor/editor/style.css";
import React, {useState} from "react";

type ApiResponse = {
  data: {
    publishPost: {
      post: {
        url: string
      }
    }
  }
}

const PERSONAL_ACCESS_TOKEN = "REPLACE_THIS_WITH_YOUR_PERSONAL_ACCESS_TOKEN";

const publishPostMutation = `
  mutation ($input: PublishPostInput!) {
    publishPost(input: $input) {
       post {
        url
       }
    }
  }
`;

function App() {
  const [title, setTitle] = useState<string>("");
  const [previewLink, setPreviewLink] = useState<string>("");
  const editorRef = React.useRef<MDXEditorMethods>(null);

  const publishPost = async () => {
    const response = await fetch("https://gql.hashnode.com", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        Accept: "application/json",
        Authorization: PERSONAL_ACCESS_TOKEN,
      },
      body: JSON.stringify({
        query: publishPostMutation,
        variables: {
          input: {
            publicationId: "65d99cd9447d15d98a2fc264",
            title: title,
            contentMarkdown: editorRef.current?.getMarkdown(),
          },
        },
      }),
    });

    const result = await response.json() as ApiResponse;

    setPreviewLink(result.data.publishPost.post.url);
  };

  return (
    <>
      <div id="preview-link" className={previewLink === "" ? "hidden" : "" + "absolute top-20 z-10 left-1/2 transform -translate-x-1/2 -translate-y-1/2 bg-green-200 border-2 border-green-500 rounded-lg w-1/2 px-8 py-4"}>
        ✅ Here is your preview link: <br />
        <a href={previewLink} className="text-blue-900" target="_blank">{previewLink}</a>
      </div>
      <div className="flex">
        <input
          onChange={(event: React.ChangeEvent<HTMLInputElement>) => setTitle(event.target.value)}
          value={title}
          className="text-3xl w-full focus:outline-none"
          placeholder="Title..."/>
        <button
          onClick={publishPost}
          className="ml-auto bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">Publish
        </button>
      </div>

      <div className="prose max-w-none">
        <MDXEditor
          ref={editorRef}
          className="my-5"
          markdown="Hello world!"
          plugins={[
            headingsPlugin(),
            linkPlugin(),
            markdownShortcutPlugin(),
            toolbarPlugin({
              toolbarContents: () => (
                <>
                  {" "}
                  <UndoRedo/>
                  <BlockTypeSelect/>
                  <BoldItalicUnderlineToggles/>
                </>
              ),
            }),
          ]}
        />
      </div>
    </>
  );
}

export default App;

Conclusion

We’ve learned how to use Hashnode GraphQL API to create a custom React text editor. There are many examples on the API Docs page that you can use to extend your blog’s functionality.