Notion apiからhtmlに変換する方法
notion apiは以下のようなブロックで値を返してくる。
[
{
type: 'paragraph',
blockId: '0ce996f3-ec3c-41eb-a938-a62a5bf175b1',
parent: '',
children: []
}
]
これをhtmlに変換していく必要がある。
全てのブロックに対してhtmlに変換する処理をれてもいいのだがそれだと結構手間である。
そこでnotion-to-mdを使ってまずブロックからマークダウン形式に変換する。
それをreact-markdownを使ってマークダウンからHTMLに変換する。
これである程度のブロックはいい感じに変換されるのだが、動画やツイッター、bookmark(embed)などは、全部以下のような感じのリンクになってしまう。
// bookmark
[bookmark](https://www.yahoo.co.jp/)
// video
[video](https://www.youtube.com/watch?v=UvHjYtmOSXk)
// tiwtter
[embed](https://twitter.com/infoNumber333/status/1801148281315025350?ref_src=twsrc%5Etfw%7Ctwcamp%5Etweetembed%7Ctwterm%5E1801148281315025350%7Ctwgr%5Eceafc7a3d9f8a71a966156d81bb481f6fa329b94%7Ctwcon%5Es1_&ref_url=https%3A%2F%2Fpublish.twitter.com%2F%3Furl%3Dhttps%3A%2F%2Ftwitter.com%2FinfoNumber333%2Fstatus%2F1801148281315025350)
これを動画なら動画再生出来るようにtwitterならtwitterカードにbookmarkならembedカードとして出力出来るようにしたい。
画像だけは
![image](https://images.unsplash.com/photo-1492707892479-7bc8d5a4ee93?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb)
という感じで!
がついていて自動でimgタグに変換される。
ブロックからマークダウンに変換するタイミングか、
マークダウンからHTMLに変換するタイミングか、
結果として後者でやることになった。
理由としては前者の場合notion-to-mdのCustom Transformersを使うことになるのだが、このタイミングでreact componentを使うことはできなかった。
なので結局マークダウンに変換された状態からbookmarkやvideoを自作のvideoやbookmarkのコンポーネントに変換するようにすることにした。
そこでvideoを変換する際の例を紹介します。
import Markdown from "react-markdown";
import { visit } from "unist-util-visit";
import NotionVideo from "@/components/notionBlocks/Video";
import NotionEmbed from "@/components/notionBlocks/Embed";
const remarkVideo = () => {
return (tree: any) => {
visit(tree, "link", (node: any) => {
const { url } = node;
if (url && url.includes("youtube.com/watch")) {
node.type = "video";
node.data = {
hName: "video",
hProperties: { src: url },
};
}
});
};
};
const remarkEmbed = () => {
return (tree: any) => {
visit(tree, "link", (node: any) => {
const { url } = node;
if (
(url && node.children[0].value === "embed") ||
node.children[0].value === "bookmark"
) {
node.type = "html";
node.value = `<embed-link url="${url}"></embed-link>`;
}
});
};
};
const customVideo = (props: any) => {
const { src } = props;
return <NotionVideo url={src} />;
};
const MdContent = ({ content }: { content: string }) => {
return (
<Markdown
remarkPlugins={[remarkVideo]}
components={{
video: CustomVideo,
@ts-ignore
"embed-link": ({ node }: { node: any }) => (
<NotionEmbed url={node.properties.url} />
),
}}
>
{content}
</Markdown>
);
};
まずはremarkVideoでは[video](https://www.youtube.com/watch?v=UvHjYtmOSXk)
があった場合videoのnodeに変換する処理になります。
customVideoではvideoタグをNotionVideoというコンポーネントに変換する処理をしています。
remarkVideoでこの処理も含めたかったのですが無理でした。
NotionVideoは
easy-notion-blogのnotion-blocks中の一部を参考にさせていただきました。
NotionVideo
"use client";
import React from "react";
import YouTube, { YouTubeProps } from "react-youtube";
const isYouTubeURL = (urls: URL): boolean => {
if (["www.youtube.com", "youtube.com", "youtu.be"].includes(urls.hostname)) {
return true;
}
return false;
};
const parseYouTubeVideoId = (urls: URL): string => {
if (!isYouTubeURL(urls)) return "";
if (urls.hostname === "youtu.be") {
return urls.pathname.split("/")[1];
} else if (urls.pathname === "/watch") {
return urls.searchParams.get("v") || "";
} else {
const elements = urls.pathname.split("/");
if (elements.length < 2) return "";
if (
elements[1] === "v" ||
elements[1] === "embed" ||
elements[1] === "live"
) {
return elements[2];
}
}
return "";
};
const Video = ({ url }: { url: string }) => {
let urls: URL;
try {
urls = new URL(url);
} catch {
return null;
}
if (!isYouTubeURL(urls)) {
return null;
}
const onPlayerReady: YouTubeProps["onReady"] = (event) => {
event.target.pauseVideo();
};
const opts: YouTubeProps["opts"] = {
height: "390",
width: "640",
playerVars: {
autoplay: 1,
},
};
const videoId = parseYouTubeVideoId(urls);
if (videoId === "") {
return null;
}
return <YouTube videoId={videoId} opts={opts} onReady={onPlayerReady} />;
};
export default Video;
その他の参考になるcomponents
- easy-notion-blog/components/notion-blocks /video.tsx
- easy-notion-blog/components/notion-blocks /tweet-embed.tsx
- easy-notion-blog/components/notion-blocks /bookmark.tsx
- easy-notion-blog/components/notion-blocks /embed.tsx
ただeasy-notion-blogはアーカイブされているのでastroで書かれてはいるが現在も開発中のastro-notion-blogも参考にした方が良さそうです。
astroの方では無駄なpackageをinstallしないで良い記述方になっていました。
ちなみにzenn-markdown-htmlを使ってmarkdownからhtmlにする方法も有りました。
参考: Notion APIとzenn-markdown-htmlを利用したブログのテンプレート「n2zm-blog-nextjs」を公開しました!
以上のようなやり方だとpタグに括られた状態になってしまいembedカードでdevタグなどを使うとpタグの中にdivが含まれいることによるerrorが出てしまいまいました。
const CustomParagraph = (props: any) => {
const { node, children } = props;
if (
node.tagName === "p" &&
node.children[0]?.tagName === "a" &&
node.children[0]?.children.length === 1
) {
if (
node.children[0]?.children[0]?.value === "embed" ||
node.children[0]?.children[0]?.value === "bookmark"
) {
const url = node.children[0]?.properties?.href;
return <NotionEmbed url={url} />;
} else if (node.children[0]?.children[0]?.value === "video") {
}
}
return <p>{children}</p>;
};
const MdContent = ({ content }: { content: string }) => {
return (
<Markdown
components={{
p: CustomParagraph,
}}
>
{content}
</Markdown>
);
};
{
type: 'element',
tagName: 'p',
properties: {},
children: [
{
type: 'element',
tagName: 'a',
properties: [Object],
children: [Array],
position: [Object]
}
],
position: {
start: { line: 129, column: 1, offset: 906 },
end: { line: 129, column: 333, offset: 1238 }
}
}