Next.jsで作成したMarkdownエディタにツイート埋め込み機能を実装しよう
はじめに
今回は前回作成したMarkdownエディタにX(旧: Twitter)のツイート埋め込み機能を実装します。
前回の内容については、以下の記事をご覧ください。
環境
- remark-directive - 3.0.0
- unist-util-visit - 5.0.0
- react-twitter-widgets - 1.11.0
- @fortawesome/fontawesome-free - 6.6.0
実装
実装に必要なライブラリの説明
-
remark-directive (GitHub)
Markdown に拡張構文(ディレクティブ)を追加するためのプラグイン。 -
unist-util-visit (GitHub)
unist ノードを走査するためのユーティリティ。 -
react-twitter-widgets (GitHub)
このライブラリを使用してツイートを埋め込みます。 -
@fortawesome/fontawesome-free (GitHub)
ツイート埋め込みボタンのアイコンを設定するときに使用します。
必要なライブラリをインストールします。
npm install remark-directive unist-util-visit react-twitter-widgets @fortawesome/fontawesome-free
1. 前回実装した内容をComponentに切り分け
まず前回実装したMarkdownEditor.tsxファイルの内容を以下を参考にSimpleMdeEditor.tsxとPreviewMarkdown.tsxComponentに切り分けます。
エディタ(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エディタ(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}
/>
</>
);
};
プレビュー画面(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. ツールバーをカスタマイズ
toolbarオプションを追加していない場合は下のようにデフォルトのツールバーが設定されます。
以下のようにツールバーの設定をオプションに追加します。
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}
/>
</>
);
};
今回はシンプルにheadingとboldボタンを追加しました。
3. ツイート埋め込みタグの挿入ボタン追加
次にツイート埋め込みタグの挿入ボタンを追加します。
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) => {
+ // 挿入するテキストを作成する
+ const insertText = ":twitter[]";
+ // エディタのCodeMirrorインスタンスを取得
+ const cm = editor.codemirror;
+ // カーソル位置にテキストを挿入
+ cm.replaceRange(insertText, cm.getCursor());
+ // エディタにフォーカスを設定
+ 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}
/>
</>
);
};
これで下のようにツイート埋め込みボタンが追加され、ツイート埋め込みタグが挿入されるようになりました。
以下の部分について簡単に説明します。
const handleInsertTwitter = (editor: EasyMDE) => {
// 挿入するテキストを作成する
const insertText = "::twitter[]";
// エディタのCodeMirrorインスタンスを取得
const cm = editor.codemirror;
// カーソル位置にテキストを挿入
cm.replaceRange(insertText, cm.getCursor());
// エディタにフォーカスを設定
cm.focus();
};
const insertText = ":twitter[]";
挿入するテキストを定義しています。この場合、:twitter[] という文字列がカーソルの位置に挿入されます。
const cm = editor.codemirror;
EasyMDEエディタのCodeMirrorインスタンスを取得しています。EasyMDEはCodeMirrorを内部で使用しているため、これを介してテキストの操作を行います。
cm.replaceRange(insertText, cm.getCursor());
replaceRange メソッドを使用して、カーソル位置cm.getCursor()
にinsertText
で指定したテキストを挿入します。
cm.focus();
テキストの挿入後、エディタにフォーカスを設定します。
4. ツイートを表示させる
次に、入力されたツイートIDに基づいてツイートを表示する処理を追加します。
ツイートIDとは
ツイートIDとは、URL の末尾にある数字部分で、status パラメータの後に続く数字がツイートのIDです。例えば、以下のURLの場合、ツイートIDは 1133841266427346945
です。
https://x.com/Support/status/1133841266427346945
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>
</>
);
};
これで下のように入力したツイートID基づいたツイートが表示されるようになりました。
以下の部分について簡単に説明します。
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) => { ... }
この部分は、remark ツリーを受け取る関数を返します。この関数はツリー全体を処理し、ツリー内のノードを訪問するために visit 関数を使います。
visit(tree, 'leafDirective', (node) => { ... })
visit 関数は、remark ツリー内のノードを再帰的に訪問し、指定されたノードタイプ(ここでは 'leafDirective')に一致するノードに対してコールバック関数を実行します。
node.value = <Tweet tweetId={value} />;
node.value を JSX の <Tweet tweetId={value} />
に設定します。これにより、ノードの内容が Tweet コンポーネントに置き換えられます。
node.type = 'html';
ノードのタイプを 'html' に設定します。これにより、ノードが HTML としてレンダリングされるようになります。
さいごに
今回は前回作成したMarkdownエディタをカスタマイズして、ツイート埋め込み機能の実装方法を解説しました。
Discussion