Notion を Hugo のヘッドレスCMSとして利用する方法 [Notion Hugo Exporter]

Hugo のサイトで Notion をヘッドレスCMSとして利用するためのソフトウェア

English version here

GitHub - dobassy/notion-hugo-exporter: Use "Notion" as a Headless CMS to generate a content file for Hugo.

GitHub - dobassy/notion-hugo-exporter: Use "Notion" as a Headless CMS to generate a content file for Hugo.

Use "Notion" as a Headless CMS to generate a content file for Hugo. - dobassy/notion-hugo-exporter

Use “Notion” as a Headless CMS to generate a content file for Hugo.

このソフトウェアは、Webサイトのコンテンツ管理を Notion で実現することを目的に作成されました。Notion 上で作成したページを Hugo のコンテンツ(=Markdownファイル)として出力するように設計しています。Markdownファイルとして出力した後の挙動は Hugo の仕様に準拠します。

この役割の分離により、仮にコンテンツ管理を Notion からその他のサービスへ移行したいと思った時、データベースのエクスポートなどの面倒な処理が発生しないので容易に乗り換えることが可能となります。

バージョン

2022/12現在、バージョン 0.x の状態です。破壊的な仕様変更には最大限考慮しますが、破壊的変更が発生する可能性もあります。

提供機能

  • Notion 上の指定するデータベースに作成されているページ単位でMarkdownファイルを生成します
  • Notion API へのリクエストを軽減するためにローカルキャッシュを持っています
  • Notion ページに含まれる画像が Amazon S3 に保存されている場合、有効期限付きのURLであり、このまま公開すると不都合があるため処理を停止します(この挙動を無効化することもできます)
  • Notion ページに含まれる画像をダウンロードします。ダウンロードした画像は static ディレクトリに保存して公開できるほか、独自に作成した Javascript の callback 処理をフックすることができます
  • ダウンロードした画像は、 .webp フォーマットに変換することができます
  • 記事の絶対パス (Url ) あるいは相対パス( Slug ) に記入した文字列は kebab-case で変換するため、URLの作成が少し楽になります
    • UrlSlug は Hugo の仕様による設定項目です
  • その他いくつかの設定は Configuration を参照してください

Permission (Notion API)

Notion Hugo Exporter は Notion ページへの書き込みを行いません。よって、Read 権限の Token のみで動作します。

書き込み権限があればデータベースの初回セットアップが楽になりますが、裏返すとその時にしか主な活躍場面がありません。余計な書き込み権限を有することによる、不本意なデータ破壊を回避することを優先しています。

実行の前提条件

  • Node を利用します (v18で開発していますが、一般的なライブラリしか利用していないので v12 くらいまでは動作すると思いますが未確認です)
  • npm へは公開していません(まだ)
  • Docker Image を提供しています

Quickstart

1. トークンの設定

export NOTION_TOKEN=
export NOTION_BLOG_DATABASE_ID=
  • NOTION_TOKEN: “my integrations” でトークンを作成します
  • NOTION_BLOG_DATABASE_ID: See this page (developers.notion.con) for how to find your database id.
    • データベースIDの確認方法は以下を参考にしてください。
    • https://www.notion.so/{workspace_name}/{database_id}?v={view_id}

2. Notion データベースプロパティの作成

プロパティは、以下の図で表示対象としている (Shown in table)プロパティがあれば動作します。その他、任意で非表示(Hidden in table)部分にあるプロパティのように Hugo のコンテンツで利用する値としてカスタマイズしたり、自身の管理メモとして任意の数のプロパティを追加できます。(なお、ここで説明した表示・非表示には意味はありません。図の説明用でグルーピングしているだけです)

必要プロパティの詳細は後述していますので参照してください。

3. コマンドの実行

ここでは Docker で動かす場合とします。

docker run -it --rm \
  -e NOTION_TOKEN=$NOTION_TOKEN \
  -e NOTION_BLOG_DATABASE_ID=$NOTION_BLOG_DATABASE_ID \
  -v $(pwd)/notion-hugo.config.js:/work/notion-hugo.config.js \
  -v $(pwd)/content/:/work/content/ \
  -v $(pwd)/static/:/work/static/ \
  -v $(pwd)/.notion-hugo-cache/:/work/.notion-hugo-cache/ \
  dobassy/notion-hugo-exporter:latest

Mount four files or directories.

  • notion-hugo.config.js: いろいろな設定ファイル(後述)
  • content/: Markdown コンテンツを出力する先
  • static/: 画像をダウンロードする場合は static ディレクトリへ配置します
  • .notion-hugo-cache/: Notion Hugo Exporter の管理用キャッシュデータです。仮に削除しても、全てのファイルを Notion API から取得し直すだけです。

ローカル環境で動かしたい場合は以下のようにビルドしてください。

git clone ...
npm install
npm link

Configuration

notion-hugo.config.js に設定可能なパラメタは以下の通りです。サンプルファイルがあるので参照してください。

module.exports = {
  directory: string,
  concurrency: number,
  authorName: string,
  s3ImageUrlWarningEnabled: boolean,
  s3ImageUrlReplaceEnabled: boolean,
  s3ImageConvertToWebpEnalbed: boolean,
  useOriginalConverter: boolean,
  saveAwsImageDirectory: null | string,
  downloadImageCallback: null | func(),
  customTransformerCallback: null | func(),
};
  • directory: (必須) コンテンツファイルを書き出すディレクトリ
  • concurrency: 同時実行数。増やしすぎないほうがいいかもしれない。Defaults to 5.
  • authorName: 記事の author を強制で固定します。一人でサイト運営している時には都合がいいでしょう
  • s3ImageUrlWarningEnabled: Defaults to true. Amazon S3の画像が含まれている場合に警告を出してプログラムの動作を停止します
  • s3ImageUrlReplaceEnabled (Experimental): Defaults to false. Notion コンテンツに S3 URL が含まれている場合は、ダウンロード後にそれらをローカル パスに置き換えます。 この機能は、画像管理にかかる時間と労力の削減を目的としています。
  • s3ImageConvertToWebpEnalbed: Defaults to false. ダウンロードした画像を .webp へ変換します
  • useOriginalConverter: Defaults to false. Markdown へ変換した後の行間の調整をしています。利用している Markdown 変換ライブラリの都合ですが、癖があって好みではなかったので微調整しました。その挙動を On/Offできます。お好みで。
  • utcOffset: Defaults to null as “Z”. Notion で設定した時刻が UTC だった場合に調整するものです。日本なら “+09:00
  • saveAwsImageDirectory: Defaults to null. S3画像をダウンロードするディレクトリ
  • downloadImageCallback: Defaults to null. ダウンロードした画像に対して任意のコールバック処理を加えることができます。例えば、ダウンロード後に WordPress API を利用して画像をアップロードするサンプルを notion-hugo.config.02callback-sample.js に記載しています。
  • fetchInterval: Server Mode で動かすときのインターバル。60秒より短くしても意味がない
  • customTransformerCallback: Notion API のブロックに対する独自処理を追加。オリジナルライブラリの説明を参照 See also https://github.com/souvikinator/notion-to-md#custom-transformers

プロパティ

Notion データベースには以下のプロパティを設定してください。

Property Name Type Required Default value
isPublished Boolean
Category Select
Tags multi_select
PublishedAt date
UpdatedAt date
Url Text ✅ (Either Url or Slug)
Slug Text ✅ (Either Url or Slug)
Description Text
Image Image (external url)
Section Select
Author Text or Select “Writer”
isDraft Boolean false
filepath Text
  • これらプロパティは手動で作成してください(前述の通り)
  • filepath property: ディレクトリとファイル名を記入することで、Markdownの書き出す場所を強制することができます。例えば blog/category/_index.md のような特殊なファイルを作成する時に都合が良いです。

その他任意のプロパティ

Property Name Type
linkTitle Text
weight Number

これらのパラメタは Docsy テーマを利用するときに都合がいいです。(Docsyで利用します)

ユーザーが定義する任意のプロパティ

Frontmatter に任意のプロパティを追加できます。

例えば、技術系ドキュメントでは「この記事は古いです」というアラートを出すことがありますが、記事によってその挙動を変えたい時があるでしょう。その場合、 ToC フラグ(以下の例)がある記事にのみ挙動を適用する、といった柔軟性が向上します。

v0.6時点、 text型と boolean 型にのみ対応しています。

// array[0]: property name in Notion
// array[1]: Types identified when exporting to Frontmatter (Markdown)
customProperties: [
  ["ToC", "boolean"],
  ["AdditionalDescription", "text"],
],

実例

オリジナルページ(Notion)

FrontMatter

Notion Hugo Exporter を実行して生成された Markdown ファイル

---
sys:
  pageId: "01234567-0123-4567-8901-dummy1dummy2"
  createdTime: "2022-02-01T15:54:00.000Z"
  lastEditedTime: "2022-02-01T15:56:00.000Z"
title: "Example page"
date: "2022-02-01T00:00:00.000+09:00"
description: "Description here"
tags:
  - "API"
  - "Development"
categories:
  - "Notion"
toc: true
author: "Writer"
legacy_alert: false
draft: false
url: "/hugo-notion-example-page"
section: "technologies"
---
# Hugo Notion

hello, world

Body

本文の生成は、基本的には外部ライブラリ “notion-to-md” に依存しています。

souvikinator/notion-to-md: Convert notion pages, block and list of blocks to markdown (supports nesting)

キャッシュ

Notion の pageId を主キーとして、Notion のページの最終更新日をキャッシュします。

Notion API へのリクエスト数を最小限に抑えるために、取得したページの日時をキャッシュします。 ディレクトリ .notion-hugo-cache/ を削除すれば初期化されます(コンテンツ本体には影響はありません)

CustomTransformer の例

例えば以下のように、特定のURLに対して独自の Shortcode を適用することが可能です。CustomTransformer を使うには、 Notion APIのブロック構造を理解する力が必要です。

const customTransformerCallback = (n2m) => {
  n2m.setCustomTransformer("bookmark", async (block) => {
    const { bookmark } = block;
    if (!bookmark?.url) return "";
    return `\{\{<blogcard "${bookmark.url}">\}\}`;
  });
};

Original output:

[bookmark](https://github.com/)

After transform output:

{{<blogcard "https://github.com/">}}

Markdownファイルの行間の調整

notion-to-md version 2.5.1 での仕様変更により、段落に複数の改行が挿入されました。 好みの問題ですが、従来の仕様で段落の改行数を扱えるようにカスタマイズしました。 ライブラリの将来の更新により、この機能が廃止される可能性があります。

元のライブラリ仕様を使用する場合は、設定を true に設定することで動作を変更できます。

  • useOriginalConverter: true

例(検証コンテンツ)

左:オリジナル動作ではない場合

右:オリジナル動作の場合(行間が多い)

なおMarkdownファイル上でこの行間があっても、HTMLに変更する上では適切な paragraph

の単位に変換されるので結果的には問題ないのですが、好みによるものです。

Watch mode (Server mode)

Notion ページのエクスポートには時間がかかるため、コマンドを 1 つずつ実行するのは面倒です。 --server オプションを使用して監視モードに入ることができます。

このモードでは、Notion の更新が定期的にチェックされ、ページが生成されます。 Hugo の hugo server コマンドと併用することで、リアルタイムプレビューのような使い方ができます。ただし、Notion API経由での変更検知の最小単位が「分」だったので、同じ分数で何回変更しても変化を検知できません。将来的なNotion APIの仕様変更により改善されることを期待しています。

Foreman や Hivemind などのプロセス マネージャーを利用すると便利です。 以下は、Procfile の構成方法の例です。

notion: notion-hugo -S
hugo: hugo server --ignoreCache --buildFuture