astro-notion-blogでコードブロックからHTML要素を挿入する

Featured image of the post

目次

参考

まえがき

astro-notion-blogはNotionで書いた記事をHTMLに変換していますが、Notion上に記述したHTML要素の変換には対応していません。

今回参考にさせて頂いたastro-notion-blogでHTMLタグを文字列ではなくHTML要素として扱うという記事においては、paragraph(通常のテキストブロック)をHTMLに変換する手法を取っていました。

ブロックの種別ごとに処理を行う箇所をif文で分岐させ、特定文字列が含まれている場合にブロックを丸ごとHTMLに変換する、というものです。

この記事の終わりにあった、

複数のタグを設定したい場合は、設定ファイルなどで管理するようにしてもいいかもしれません。

という一文を読んで、環境変数から任意文字列を設定できたら良いなと思い、どのようにすれば実現しやすいか考えていたのですが、「コードブロックのキャプションに特定文字列を入れたらコードブロックの中身をHTMLに変換する」という方法を思いつき、やってみたらできました。変換のトリガとする文字列は1つにできるので、環境変数による設定も簡単です。

📄Arrow icon of a page linkastro-notion-blogカスタマイズメモ

↑この記事冒頭に書いた通り、Arduino言語レベルしか使えない人間なので非常に苦労しました。肩が凝りすぎて痛みだしている状態でこの記事を書いています。

先行事例の動作検証

AstroもCSSもHTMLもさっぱり分からないので、まずは先行事例の真似をしてどのような処理を行っているのか理解するところから始めました。

src\components\NotionBlocks.astroを開き、118行目で

これを

case 'paragraph':
	return <Paragraph block={block} headings={headings} />

こう書き換えます。

case 'paragraph':
        if(typeof block.Paragraph.RichTexts[0] != "undefined"){
          if(block.Paragraph.RichTexts[0].PlainText.indexOf('autodesk360') != -1
          ){
            return <Fragment set:html={block.Paragraph.RichTexts[0].PlainText}/>
          }
        }
        return <Paragraph block={block} headings={headings} />

Notionにただのテキストとして配置するHTMLタグはこれです。

<iframe src="https://myhub.autodesk360.com/ue2af766a/shares/public/SH56a43QTfd62c1cd968793ce3017c05306f?mode=embed" width="800" height="600" allowfullscreen="true" webkitallowfullscreen="true" mozallowfullscreen="true"  frameborder="0"></iframe>
自作の机の3Dモデルを埋め込めるやつ
  • 埋め込み(Notion上に埋め込まれて3Dモデルが見れる)
  • コードブロック
  • プレーンテキスト

の3種を配置して、ブラウザ上でどうなるのかを確認しました。

実験結果

Image in a image block
Notion上の見た目 埋め込みだけ動作してる
Image in a image block
astro-notion-blog上の見た目 プレーンテキストだけ動作してる

推測が合っているらしいということが分かりました。また、プレーンテキストは完全にプレーンである必要があり、URL部分に勝手に埋め込まれていたリンクを削除して本当にただのプレーンテキストにしないとこうなりませんでした。

実装の過程

情報収集

まず公式のAPIの解説からブロックの中身を拾ってきました。なんかズレていたり見づらかったりしたので整形済です。

{
	"type": "paragraph",
	"paragraph": {
		"rich_text": [
			{
				"type": "text",
				"text": {
					"content": "Lacinato kale",
					"link": null
				}
			}
		],
		"color": "default"
	}
}
paragraphの中身の例
{
	"type": "code",
	"code": {
		"caption": [],
		"rich_text": [
			{
				"type": "text",
				"text": {
					"content": "const a = 3"
				}
			}
		],
		"language": "javascript"
	}
}
Code blockの中身の例

captionの中にはコードブロックのキャプションのリッチテキストがそのまま入っているため、paragraphにおけるリッチテキストと同様に扱います。

次に、どこからか出てきたblock.Paragraph.RichTexts[0] などは何がどうなっているのかを調べます。

src\lib\interfaces.tsが由来であることが分かったので該当部分を持ってきます。

export interface Paragraph {
  RichTexts: RichText[]
  Color: string
  Children?: Block[]
}
Paragraph
export interface Code {
  Caption: RichText[]
  RichTexts: RichText[]
  Language: string
}
Code
export interface RichText {
  Text?: Text
  Annotation: Annotation
  PlainText: string
  Href?: string
  Equation?: Equation
  Mention?: Mention
}
RichText

src\lib\interfaces.tsで形を分かりやすくして送り込んでいることが分かりました。

処理の置き換え

これをなぞります。

case 'paragraph':
  if(typeof block.Paragraph.RichTexts[0] != "undefined"){
    if(block.Paragraph.RichTexts[0].PlainText.indexOf('autodesk360') != -1
    ){
      return <Fragment set:html={block.Paragraph.RichTexts[0].PlainText}/>
    }
  }
  return <Paragraph block={block} headings={headings} />
src/components/NotionBlocks.astro 117行目付近

最初の条件文typeof block.Paragraph.RichTexts[0] != "undefined"block.Paragraph.RichTextsが定義されているかの確認。
Code blockに置き換えるとtypeof block.Code.Caption[0] != "undefined"になる。

次の条件文block.Paragraph.RichTexts[0].PlainText.indexOf('autodesk360') != -1block.Paragraph.RichTexts[0].PlainTextの中にマッチする文字列があることの確認。
Code blockに置き換えるとblock.Code.Caption[0].PlainText.indexOf('文字列') != -1

マッチする場合にHTMLとして返すのはblock.Paragraph.RichTexts[0].PlainTextで、これはブロックのプレーンテキストがそのまま変換される。
コードブロックのキャプションで判定したあとは、コードブロック内の文字列をHTMLとして返したいから、block.Code.RichTexts[0].PlainTextにする。

Icon in a callout block
astro-notion-blog開発者のおとよさんに、JS/TSでは厳密等価演算子を使うと教えていただきました。
下の完成形のコードにて演算子を変更してあります。

結果

src\components\NotionBlocks.astro 130行目付近

これが

case 'code':
        return <Code block={block} />

こうなる

case 'code':
  if(typeof block.Code.Caption[0] !== "undefined"){
    if(block.Code.Caption[0].PlainText.indexOf('文字列') !== -1
    ){
      return <Fragment set:html={block.Code.RichTexts[0].PlainText}/>
    }
  }
  return <Code block={block} />

環境変数が使えるようにするとこうなる。

case 'code':
  if(typeof block.Code.Caption[0] !== "undefined"){
    if(block.Code.Caption[0].PlainText.indexOf(HTML_CONVERSION_TRIGGER) !== -1
    ){
      return <Fragment set:html={block.Code.RichTexts[0].PlainText}/>
    }
  }
  return <Code block={block} />

src\server-constants.tsを編集。

末尾にexport const HTML_CONVERSION_TRIGGER = import.meta.env.HTML_CONVERSION_TRIGGERを追記する。

export const NOTION_API_SECRET =
  import.meta.env.NOTION_API_SECRET || process.env.NOTION_API_SECRET || ''
export const DATABASE_ID =
  import.meta.env.DATABASE_ID || process.env.DATABASE_ID || ''

export const CUSTOM_DOMAIN =
  import.meta.env.CUSTOM_DOMAIN || process.env.CUSTOM_DOMAIN || '' // <- Set your costom domain if you have. e.g. alpacat.com
export const BASE_PATH =
  import.meta.env.BASE_PATH || process.env.BASE_PATH || '' // <- Set sub directory path if you want. e.g. /docs/

export const PUBLIC_GA_TRACKING_ID = import.meta.env.PUBLIC_GA_TRACKING_ID
export const NUMBER_OF_POSTS_PER_PAGE = 10
export const REQUEST_TIMEOUT_MS = parseInt(
  import.meta.env.REQUEST_TIMEOUT_MS || '10000',
  10
)
export const ENABLE_LIGHTBOX = import.meta.env.ENABLE_LIGHTBOX
これが
export const NOTION_API_SECRET =
  import.meta.env.NOTION_API_SECRET || process.env.NOTION_API_SECRET || ''
export const DATABASE_ID =
  import.meta.env.DATABASE_ID || process.env.DATABASE_ID || ''

export const CUSTOM_DOMAIN =
  import.meta.env.CUSTOM_DOMAIN || process.env.CUSTOM_DOMAIN || '' // <- Set your costom domain if you have. e.g. alpacat.com
export const BASE_PATH =
  import.meta.env.BASE_PATH || process.env.BASE_PATH || '' // <- Set sub directory path if you want. e.g. /docs/

export const PUBLIC_GA_TRACKING_ID = import.meta.env.PUBLIC_GA_TRACKING_ID
export const NUMBER_OF_POSTS_PER_PAGE = 10
export const REQUEST_TIMEOUT_MS = parseInt(
  import.meta.env.REQUEST_TIMEOUT_MS || '10000',
  10
)
export const ENABLE_LIGHTBOX = import.meta.env.ENABLE_LIGHTBOX
export const HTML_CONVERSION_TRIGGER = import.meta.env.HTML_CONVERSION_TRIGGER
こうなる

src\components\NotionBlocks.astroで環境変数をimportさせる。

27行目付近にimport { HTML_CONVERSION_TRIGGER } from '../server-constants.ts’を追加

import Toggle from './notion-blocks/Toggle.astro'
import File from './notion-blocks/File.astro'
import LinkToPage from './notion-blocks/LinkToPage.astro'
import { HTML_CONVERSION_TRIGGER } from '../server-constants.ts'
こうなる

.envファイル及びCloudflareの環境変数に追加しておく。

Image in a image block
特定文字列は何が適切なのかよくわかってないので雑

所感

処理の置き換えのところが最高にオブジェクト指向~~~って感じで脳みそ融けそうになりました。デバッグの方法も何も分からず、取得できた結果の中身も見ずに、失敗したことだけが分かる状態での手探りは中々堪えますね。Arduinoと戯れたいです。

環境変数を使えるようにするときに他のファイルも沢山読んだので、どうやって動いているのか裏側がちょっとわかった気がします。最終的には勘になっちゃったので変な不具合あると困るんですが、動いてるのでヨシ!の精神でいきます。ブログなので。

デモ

📄Arrow icon of a page linkFusion360の埋め込みをレスポンシブ対応にする

ℹ️
上記記事でデモとして埋め込むコードをレスポンシブ対応にしました。
<div style="position: relative; padding-bottom: 75%;"><iframe style="position: absolute; top: 0; left: 0; width: 100%; height: 100%;" src="https://myhub.autodesk360.com/ue2af766a/shares/public/SH56a43QTfd62c1cd968793ce3017c05306f?mode=embed" allowfullscreen="true" webkitallowfullscreen="true" mozallowfullscreen="true" frameborder="0"></iframe></div>
これはキャプションが特定文字列じゃない例

これは僕の机です。アルミフレームが廃盤になったのでもう作れません。


Image in a image block
Notion上での見た目

同期ブロック内でも動作します。と書いて実際に見せたいところなのですが、この埋め込みを複数入れると机だけ消えるのでやってません。同期ブロックの場合見た目は完全に同じでした。

トグル内でも

表示できます。(が、2つしか埋め込んでないのにやっぱり机が消えてることがあります。なぜ。)