iTranslated by AI

The content below is an AI-generated translation. This is an experimental feature, and may contain errors. View original article
📖

Implementing Tweet Embedding in a Markdown Editor Built with Next.js

に公開

Introduction

In this article, we will implement an X (formerly Twitter) tweet embedding function into the Markdown editor we created previously.

For the previous content, please refer to the following article:
https://zenn.dev/milky/articles/markdown-content

Environment

  • remark-directive - 3.0.0
  • unist-util-visit - 5.0.0
  • react-twitter-widgets - 1.11.0
  • @fortawesome/fontawesome-free - 6.6.0

Implementation

Explanation of Libraries Required for Implementation

  • remark-directive (GitHub)
    A plugin for adding extended syntax (directives) to Markdown.

  • unist-util-visit (GitHub)
    A utility for traversing unist nodes.

  • react-twitter-widgets (GitHub)
    This library is used to embed tweets.

  • @fortawesome/fontawesome-free (GitHub)
    Used when setting the icon for the tweet embedding button.

Install the required libraries.

npm install remark-directive unist-util-visit react-twitter-widgets @fortawesome/fontawesome-free

1. Split the previous implementation into Components

First, split the content of the MarkdownEditor.tsx file implemented last time into SimpleMdeEditor.tsx and PreviewMarkdown.tsx components by referring to the following.

Editor (MarkdownEditor.tsx)
MarkdownEditor.tsx
- import { useEffect, useMemo, useState } from "react";
- import dynamic from "next/dynamic";
- const ReactSimpleMdeEditor = dynamic(() => import("react-simplemde-editor"), {
  ssr: false,
});
- import "easymde/dist/easymde.min.css";

- import ReactMarkdown from "react-markdown";
- import breaks from "remark-breaks";
- import remarkGfm from "remark-gfm";
- import "github-markdown-css/github-markdown.css";
+ import { useEffect, useState } from 'react';
+ import { PreviewMarkdown } from './PreviewMarkdown';
+ import { SimpleMdeEditor } from './SimpleMdeEditor';

export const MarkdownEditor = () => {
  const [markdownValue, setMarkdownValue] = useState("");

  useEffect(() => {
    const savedContent = localStorage.getItem("smde_saved_content") ?? "";
    setMarkdownValue(savedContent);
  }, []);

  const onChange = (value: string) => {
    setMarkdownValue(value);
  };

- const options = useMemo(() => {
-   return {
-     autofocus: true,
-     spellChecker: false,
-     autosave: {
-       enabled: true,
-       uniqueId: "saved_content",
-       delay: 1000,
-     },
-   };
- }, []);

  return (
    <>
-     <ReactSimpleMdeEditor value={markdownValue} onChange={onChange} options={options} />
-     <h1 className="text-4xl font-bold mb-4 text-left pl-4">プレビュー</h1>
-     <div
-       className="markdown-body p-4 border border-gray-300 h-100 overflow-y-auto"
-       style={{ fontFamily: "inherit", fontSize: "inherit" }}
-     >
-       <ReactMarkdown remarkPlugins={[remarkGfm, breaks]}>{markdownValue}</ReactMarkdown>
-     </div>
+     <SimpleMdeEditor markdownValue={markdownValue} onChange={onChange} />
+     <PreviewMarkdown markdownValue={markdownValue} />
    </>
  );
};
Markdown Editor (SimpleMdeEditor.tsx)
SimpleMdeEditor.tsx
import { useMemo } from "react";
import dynamic from "next/dynamic";
const ReactSimpleMdeEditor = dynamic(() => import("react-simplemde-editor"), {
  ssr: false,
});
import "easymde/dist/easymde.min.css";

type Props = {
  markdownValue: string;
  onChange: (value: string) => void;
};

export const SimpleMdeEditor = ({ markdownValue, onChange }: Props) => {
  const options = useMemo(() => {
    return {
      autofocus: true,
      spellChecker: false,
      autosave: {
        enabled: true,
        uniqueId: "saved_content",
        delay: 1000,
      },
    };
  }, []);
  return (
    <>
      <ReactSimpleMdeEditor
        value={markdownValue}
        onChange={onChange}
        options={options}
      />
    </>
  );
};
Preview Screen (PreviewMarkdown.tsx)
PreviewMarkdown.tsx
import ReactMarkdown from 'react-markdown';
import breaks from 'remark-breaks';
import remarkGfm from 'remark-gfm';
import 'github-markdown-css/github-markdown.css';

export const PreviewMarkdown = ({ markdownValue }: { markdownValue: string }) => {
  return (
    <>
      <h1 className="text-4xl font-bold mb-4 text-left pl-4">プレビュー</h1>
      <div
        className="markdown-body p-4 border border-gray-300 h-100 overflow-y-auto"
        style={{ fontFamily: 'inherit', fontSize: 'inherit' }}
      >
        <ReactMarkdown remarkPlugins={[remarkGfm, breaks]}>{markdownValue}</ReactMarkdown>
      </div>
    </>
  );
};

2. Customize the toolbar

If you have not added the toolbar option, the default toolbar will be set as shown below.


Add the toolbar settings to the options as follows.

SimpleMdeEditor.tsx
import { useMemo } from "react";
import dynamic from "next/dynamic";
import "easymde/dist/easymde.min.css";
const ReactSimpleMdeEditor = dynamic(() => import("react-simplemde-editor"), {
  ssr: false,
});

type Props = {
  markdownValue: string;
  onChange: (value: string) => void;
};

export const SimpleMdeEditor = ({ markdownValue, onChange }: Props) => {
+ const toolbarOptions = [
+   {
+     name: "heading",
+     action: (editor: any) => editor.toggleHeadingSmaller(),
+     className: "fa fa-header",
+     title: "見出し",
+   },
+   {
+     name: "bold",
+     action: (editor: any) => editor.toggleBold(),
+     className: "fa fa-bold",
+     title: "太字",
+   },
+ ];

  const options = useMemo(() => {
    return {
      autofocus: true,
      spellChecker: false,
+     toolbar: toolbarOptions,
      autosave: {
        enabled: true,
        uniqueId: "saved_content",
        delay: 1000,
      },
    };
  }, []);

  return (
    <>
      <ReactSimpleMdeEditor
        value={markdownValue}
        onChange={onChange}
        options={options}
      />
    </>
  );
};

This time, we simply added the heading and bold buttons.

3. Add a button to insert tweet embedding tags

Next, we will add a button to insert tweet embedding tags.

SimpleMdeEditor.tsx
import { useMemo } from "react";
import dynamic from "next/dynamic";
import "easymde/dist/easymde.min.css";
const ReactSimpleMdeEditor = dynamic(() => import("react-simplemde-editor"), {
  ssr: false,
});
+ import "@fortawesome/fontawesome-free/css/all.min.css";

type Props = {
  markdownValue: string;
  onChange: (value: string) => void;
};

export const SimpleMdeEditor = ({ markdownValue, onChange }: Props) => {
+ const handleInsertTwitter = (editor: any) => {
+   // Create the text to be inserted
+   const insertText = ":twitter[]";
+   // Get the CodeMirror instance of the editor
+   const cm = editor.codemirror;
+   // Insert text at the cursor position
+   cm.replaceRange(insertText, cm.getCursor());
+   // Set focus to the editor
+   cm.focus();
+ };

  const toolbarOptions = [
    {
      name: "heading",
      action: (editor: any) => editor.toggleHeadingSmaller(),
      className: "fa fa-header",
      title: "見出し",
    },
    {
      name: "bold",
      action: (editor: any) => editor.toggleBold(),
      className: "fa fa-bold",
      title: "太字",
    },
+   {
+     name: "twitter",
+     action: handleInsertTwitter,
+     className: "fa-brands fa-x-twitter",
+     title: "X (旧: Twitter)",
+   },
  ];

  const options = useMemo(() => {
    return {
      autofocus: true,
      spellChecker: false,
      toolbar: toolbarOptions,
      autosave: {
        enabled: true,
        uniqueId: "saved_content",
        delay: 1000,
      },
    };
  }, []);

  return (
    <>
      <ReactSimpleMdeEditor
        value={markdownValue}
        onChange={onChange}
        options={options}
      />
    </>
  );
};

The tweet embedding button has now been added, and the tweet embedding tag can be inserted as shown below.


Here is a brief explanation of the following part.

const handleInsertTwitter = (editor: EasyMDE) => {
  // Create the text to be inserted
  const insertText = "::twitter[]";
  // Get the CodeMirror instance of the editor
  const cm = editor.codemirror;
  // Insert text at the cursor position
  cm.replaceRange(insertText, cm.getCursor());
  // Set focus to the editor
  cm.focus();
};

const insertText = ":twitter[]";
Defines the text to be inserted. In this case, the string :twitter[] will be inserted at the current cursor position.

const cm = editor.codemirror;
Gets the CodeMirror instance of the EasyMDE editor. Since EasyMDE uses CodeMirror internally, we manipulate text through this instance.

cm.replaceRange(insertText, cm.getCursor());
Uses the replaceRange method to insert the text specified in insertText at the cursor position returned by cm.getCursor().

cm.focus();
Sets the focus back to the editor after the text has been inserted.

4. Displaying Tweets

Next, we will add the logic to display tweets based on the entered tweet ID.

What is a Tweet ID?

A tweet ID is the numeric part at the end of a URL, following the status parameter. For example, in the following URL, the tweet ID is 1133841266427346945.
https://x.com/Support/status/1133841266427346945

PreviewMarkdown.tsx
import ReactMarkdown from 'react-markdown';
import breaks from 'remark-breaks';
import remarkGfm from 'remark-gfm';
import 'github-markdown-css/github-markdown.css';
+ import remarkDirective from 'remark-directive';
+ import { visit } from 'unist-util-visit';
+ import { Tweet } from 'react-twitter-widgets';

export const PreviewMarkdown = ({ markdownValue }: { markdownValue: string }) => {
+  const remarkLeafDirective = () => {
+    return (tree: any) => {
+      visit(tree, 'leafDirective', (node) => {
+        if (node.name === 'twitter') {
+          const valueChild = node.children.find((child: any) => 'value' in child);
+          if (valueChild) {
+            const { value } = valueChild;
+            node.value = <Tweet tweetId={value} />;
+            node.type = 'html';
+          }
+        }
+      });
+    };
+  };

  return (
    <>
      <h1 className="text-4xl font-bold mb-4 text-left pl-4">プレビュー</h1>
      <div
        className="markdown-body p-4 border border-gray-300 h-100 overflow-y-auto"
        style={{ fontFamily: 'inherit', fontSize: 'inherit' }}
      >
        <ReactMarkdown
          remarkPlugins={[
            remarkGfm,
            breaks,
+           remarkDirective,
+           remarkLeafDirective,
          ]}
        >
          {markdownValue}
        </ReactMarkdown>
      </div>
    </>
  );
};

Now, tweets based on the entered tweet ID are displayed as shown below.


Here is a brief explanation of the following part.

const remarkLeafDirective = () => {
  return (tree: any) => {
    visit(tree, "leafDirective", (node) => {
      if (node.name === "twitter") {
        const valueChild = node.children.find((child: any) => "value" in child);
        if (valueChild) {
          const { value } = valueChild;
          node.value = <Tweet tweetId={value} />;
          node.type = "html";
        }
      }
    });
  };
};

return (tree: any) => { ... }
This part returns a function that receives the remark tree. This function processes the entire tree and uses the visit function to traverse the nodes within the tree.

visit(tree, 'leafDirective', (node) => { ... })
The visit function recursively visits nodes in the remark tree and executes a callback function for nodes that match the specified node type (in this case, 'leafDirective').

node.value = <Tweet tweetId={value} />;
Sets node.value to the JSX <Tweet tweetId={value} />. This replaces the node's content with the Tweet component.

node.type = 'html';
Sets the node type to 'html'. This allows the node to be rendered as HTML.

Conclusion

In this article, we explained how to implement a tweet embedding function by customizing the Markdown editor created previously.

Discussion