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/
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
TheApp
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! 🎉
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!
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.
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.
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 */}
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!
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.