NotionのDBのページ数をgithub actionsで定期的に監視しNotionで可視化する
はじめに
筆者は日頃から個人でnotionを利用しています。
本記事は、notionに記載した知識の量を把握できるようにするために、ページ数の可視化に取り組んだ記録です。
完成イメージ
前提として筆者のnotionの運用について記載します。
もともとScrapboxという、ページ同士をリンクを用いて関連付けるメモサービスを使っていたため、Notionでもそれに似た運用を目指しています。
浅い階層にいくつかのデータベース(以下:DB)を作成し、その要素としてページを追加していく運用をしています。
// データベースの構造
suzuhiki-memo
|- 記録DB - ページ
|- 語彙DB - ページ
|- 作品DB - ページ
︙
この構造のnotionプロジェクトを対象に、各DBのページ数をカウントして、チャート表示用のDBに書き込み、それをnotion上で可視化する方法について記載します。
作業
可視化を実現するためには大きく4つのステップがあります。
- NotionのAPIを利用できるようにする
- 対象とするDBからDBの要素数を取得して、可視化用のDBに保存する
- Github Actionsを用いて定期実行できるようにする
- Notionのチャート機能を利用して可視化
以下でそれぞれについて説明します。
1. NotionのAPIを利用できるようにする
トークンを取得してNotion APIにアクセスできるようにします。
まず、プロジェクトを作る必要があります。
今回は勉強も兼ねてGo言語を利用しました。
公式サイトからダウンロードします。
筆者はMacのhomebrewからインストールしました。
notion-db-counterというリポジトリをgithubに作成し、git clone
でローカルに持ってきます。
cloneしたディレクトに移動しgo mod init
からgo言語のプロジェクトを作成します。
go mod init notion-db-counter
環境変数を定義する.env
ファイルの雛形(.env.template
)を作成します。
# please run source .env
export NOTION_TOKEN=<YOUR_NOTION_TOKEN>
export WRITE_DB_ID=<YOUR_WRITE_DB_ID>
このファイルをコピーして、名前を.env
にします。
githubに環境変数がアップロードされないように、.gitignore
に.env
を追加します。
Notion Tokenの取得
上記記事にしたがって、任意の名前のインテグレーションを作成します。
内部インテグレーションシークレットの項目にあるトークンを、.env
の<YOUR_NOTION_TOKEN>
の部分に貼り付けます。
権限は以下のように設定します。
忘れずにnotionワークスペースのページにこのインテグレーションを接続しておきます。
対象となる全てのDBより階層が上(親)であるページで右上の…
ボタンから接続を選択し、先程作成したインテグレーションを登録します。
2. 対象とするDBからDBの要素数を取得して、可視化用のDBに保存する
可視化用DBを環境変数に登録する
先ほどインテグレーションを設定したページより下の階層に可視化用の可視化DBを作成します。
DBの名前は何でもかまいません。
目標とするDB構造は以下の図のようになります。
これに従って、title、date、label、lengthのカラムを持つように可視化DBを編集して下さい。
目標とするDB構造
可視化DBのデータベースIDをURLから取得し、.env
の<YOUR_WRITE_DB_ID>
に登録して下さい。
データベースIDはURLに以下のように含まれています。
https://www.notion.so/<データベースID>?...
可視化対象のDBをcsvファイルとして保存する
監視対象のDBをcsvファイルの形で記載します。
db_ids.csv
を以下のように作成してください。
db_name, db_id
記録DB,<db id>
語彙DB,<db id>
楽曲DB,<db id>
作品DB,<db id>
db_nameは可視化の際に、ラベル名として利用されます。
db_idは先程の可視化DBと同じように取得して貼り付けてください。
なお、ディレクトリ構造は以下の図の通りとなります。
APIを利用するgolangのコードを用意する
以下のようなコードを作成しました。
詳細には説明しません。
package main
import (
"bytes"
"encoding/csv"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"time"
)
func main() {
db_ids := map[string]string{}
db_id_data, err := readCSV("db_ids.csv")
if err != nil {
fmt.Println("Error reading db_ids.csv:", err)
os.Exit(1)
}
for i, record := range db_id_data {
if i == 0 {
// ヘッダーをスキップ
continue
}
// record[0] is db_name, record[1] is db_id
db_ids[record[0]] = record[1]
}
fmt.Println("Database IDs loaded")
fmt.Println(db_ids)
notion_token := os.Getenv("NOTION_TOKEN")
if notion_token == "" {
fmt.Println("Error: NOTION_TOKEN environment variable is not set")
os.Exit(1)
}
fmt.Println("Notion token loaded")
db_name_data_counts := map[string]int{}
for db_name, db_id := range db_ids {
start_cursor := ""
data_counts := 0
for true {
// リクエストの作成
url := fmt.Sprintf("https://api.notion.com/v1/databases/%s/query", db_id)
var body []byte
if start_cursor != "" {
body = []byte(fmt.Sprintf(`{"start_cursor": "%s"}`, start_cursor))
}
req, _ := http.NewRequest("POST", url, bytes.NewBuffer(body))
req.Header.Set("Authorization", "Bearer "+notion_token)
req.Header.Set("Notion-Version", "2022-06-28")
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
fmt.Printf("Error sending request for %s: %v\n", db_name, err)
break
}
defer resp.Body.Close()
// レスポンスの読み取り
response, err := io.ReadAll(resp.Body)
if err != nil {
fmt.Printf("Error reading response for %s: %v\n", db_name, err)
os.Exit(1)
}
// レスポンスのパース
var responseData map[string]interface{}
err = json.Unmarshal(response, &responseData)
if err != nil {
fmt.Printf("Error parsing response for %s: %v\n", db_name, err)
os.Exit(1)
}
// エラーレスポンスのチェック
if status, ok := responseData["status"].(float64); ok && status != 200 {
message, _ := responseData["message"].(string)
fmt.Printf("Error: API request failed for %s\nStatus: %v\nMessage: %s\n", db_name, status, message)
os.Exit(1)
}
// resultsの長さをdata_countsに加算
if results, ok := responseData["results"].([]interface{}); ok {
data_counts += len(results)
}
// start_cursorの処理
if has_more, ok := responseData["has_more"].(bool); ok && has_more {
start_cursor = responseData["next_cursor"].(string)
fmt.Println("DB name:", db_name, "Next cursor:", start_cursor)
} else {
db_name_data_counts[db_name] = data_counts
fmt.Println("DB name:", db_name, "No next cursor break")
break
}
}
}
fmt.Println("DB data counts fetched:", db_name_data_counts)
// データをnotionページに書き込み
write_db_id := os.Getenv("WRITE_DB_ID")
if write_db_id == "" {
fmt.Println("Error: WRITE_DB_ID environment variable is not set")
os.Exit(1)
}
fmt.Println("Writing to Notion...")
for db_name, data_counts := range db_name_data_counts {
// 現在の日付を取得
currentDate := time.Now().Format("2006-01-02")
// リクエストボディの作成
body := fmt.Sprintf(`{
"parent": { "database_id": "%s" },
"properties": {
"title": {
"title": [
{
"text": {
"content": ""
}
}
]
},
"label": {
"rich_text": [
{
"text": {
"content": "%s"
}
}
]
},
"length": { "number": %d },
"date": {"date": {"start": "%s"}}
}
}`, write_db_id, db_name, data_counts, currentDate)
// リクエストの作成
url := "https://api.notion.com/v1/pages"
req, _ := http.NewRequest("POST", url, bytes.NewBuffer([]byte(body)))
req.Header.Set("Authorization", "Bearer "+notion_token)
req.Header.Set("Notion-Version", "2022-06-28")
req.Header.Set("Content-Type", "application/json")
// リクエストの送信
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
fmt.Printf("Error sending request for %s: %v\n", db_name, err)
os.Exit(1)
}
defer resp.Body.Close()
// レスポンスの読み取り
response, err := io.ReadAll(resp.Body)
if err != nil {
fmt.Printf("Error reading response for %s: %v\n", db_name, err)
os.Exit(1)
}
// レスポンスのパース
var responseData map[string]interface{}
err = json.Unmarshal(response, &responseData)
if err != nil {
fmt.Printf("Error parsing response for %s: %v\n", db_name, err)
os.Exit(1)
}
// エラーレスポンスのチェック
if status, ok := responseData["status"].(float64); ok && status != 200 {
message, _ := responseData["message"].(string)
fmt.Printf("Error: API request failed for %s\nStatus: %v\nMessage: %s\n", db_name, status, message)
os.Exit(1)
}
fmt.Printf("Successfully created page for %s\n", db_name)
}
}
func readCSV(filePath string) ([][]string, error) {
file, err := os.Open(filePath)
if err != nil {
return nil, err
}
defer file.Close()
reader := csv.NewReader(file)
records, err := reader.ReadAll()
if err != nil {
return nil, err
}
return records, nil
}
以下のコマンドをterminalで実行することでテストできます。
可視化用DBに要素が追加されれば成功です。
source .env #環境変数の適用
go run main.go
実行が確認できればcommit
してpush
してください。
3. Github Actionsを用いて定期実行できるようにする
2で作ったコードを定期実行できるようにします。
今回はGithub Actionsを利用します。
notion-db-counter/.github/workflows/notion.yml
を作成し、以下のように記載してください。
name: Run Notion DB counts
on:
schedule:
- cron: '0 20 * * *' # 定期実行の設定(毎朝5:00)
workflow_dispatch: # 手動実行も可能にする
jobs:
run-go:
runs-on: ubuntu-latest
environment: actions
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: '1.24'
- name: Run Go script
env:
NOTION_TOKEN: ${{ secrets.NOTION_TOKEN }}
WRITE_DB_ID: ${{ secrets.WRITE_DB_ID }}
run: go run main.go
環境変数のgithubへの登録
.env
に記載した環境変数はローカルでのみ利用しているため、github actionsには別で設定する必要があります。
以下の記事を参考に、Environment Secretを作成してください。
この状態でgithubのActionsタブより手動で実行できます。
実行中にエラーが出ず、Actionsの画面に✅が表示され、notionの可視化DBにデータが追加されていれば成功です。
4. Notionのチャート機能を利用して可視化
NotionのDBの表示形式の一つである、チャートを利用して可視化します。
以下の画像のように新しいチャートビューを作成してください。
以下のように設定すると折れ線グラフとしてDB内のページ数の変化を記録できます。
チャートの設定
チャート設定の「その他のスタイルオプション」
おわりに
NotionのDBの要素数をNotionのチャート機能を用いて可視化する試みについて紹介しました。
今回はNotion APIのデータベース関連のものを利用しましたが、検索関連のAPI (api.notion.com/v1/search) などを利用するとより柔軟な可視化が可能だと思います。
みなさんのお役に立てれば幸いです。
Discussion