最近、Notion API系でなんかできないかな、ということを考えてます。
Googleスプレッドシートをソースにコードを書く、みたいなDBとしての利用はNotionでもすぐにできそうなのは思いつきますが、Notionの場合はなんらかの簡易的なHeadless CMSとしても使えそうですね。
- Notionにはデータベースがある
- データベースのアイテム(ページ)には各種任意のプロパティを付与できる
- アイテム(ページ)はコンポーネント(=Notionのブロック)によって はMarkdownやHTMLに平易に変換可能
なため、CMSとしての治安が最小限でよければ充分に利用可能なはずです。
今回はその素振りとして、NotionのページをAPI経由で取得するところまでやってみます。
簡単なページを用意する
今回解析するNotionのページ内容はざっくり以下のようなものです。
一般的なHTMLというかMarkdown等で表現可能なプロパティだけいったん使っています。
まずはインテグレーション定義
API Tokenを取得するために、適当なインテグレーションを定義しておきます。
会社のNotionなら管理者に作ってもらったほうが良いですね。
その後、対象のページ以下に認可を与えるためにこのインテグレーションを特定ページ以下に招待しておきます。
Golangでの簡単な取得例
それでは実際に取得していきましょう。
Golangの場合、jomei/notionapi というライブラリが便利そうでした。
Claudeあたりにこのライブラリを見せつつ、最低限の内容をPrintしてみます。
package main
import (
"context"
"fmt"
"github.com/jomei/notionapi"
"log"
"os"
"strings"
)
func main() {
// Notion APIのトークンを環境変数から取得
token := os.Getenv("NOTION_TOKEN")
if token == "" {
log.Fatal("NOTION_TOKEN environment variable is not set")
}
// Notion クライアントの初期化
client := notionapi.NewClient(notionapi.Token(token))
const PageID = `c632803bc4f54f04b6729358470cfd0f`
pageData, err := client.Page.Get(context.Background(), notionapi.PageID(PageID))
if err != nil {
log.Fatalf("Error querying database: %v", err)
}
pageProps := pageData.Properties
title := getPageTitle(pageProps)
fmt.Printf("Title: %s\n", title)
// 他のpropsを取得して表示
printPageProperties(pageProps)
fmt.Println(`----------------------------------------`)
printPageContent(client, notionapi.BlockID(PageID))
}
func printPageContent(client *notionapi.Client, pageID notionapi.BlockID) {
fmt.Println("Page Content:")
var cursor notionapi.Cursor
for {
blocks, err := client.Block.GetChildren(context.Background(), pageID, ¬ionapi.Pagination{
StartCursor: cursor,
PageSize: 100,
})
if err != nil {
log.Printf("Error getting page content: %v\n", err)
return
}
for _, block := range blocks.Results {
printBlock(client, block, " ")
}
if !blocks.HasMore {
break
}
cursor = notionapi.Cursor(blocks.NextCursor)
}
}
func getPageTitle(properties notionapi.Properties) string {
for _, prop := range properties {
if titleProp, ok := prop.(*notionapi.TitleProperty); ok {
if len(titleProp.Title) > 0 {
return titleProp.Title[0].PlainText
}
return ""
}
}
return ""
}
func printPageProperties(properties notionapi.Properties) {
fmt.Println("Page Properties:")
for key, prop := range properties {
fmt.Printf(" %s: ", key)
switch v := prop.(type) {
case *notionapi.TitleProperty:
if len(v.Title) > 0 {
fmt.Println(v.Title[0].PlainText)
}
case *notionapi.RichTextProperty:
if len(v.RichText) > 0 {
fmt.Println(v.RichText[0].PlainText)
}
case *notionapi.NumberProperty:
fmt.Println(v.Number)
case *notionapi.SelectProperty:
if v.Select.Name != "" {
fmt.Println(v.Select.Name)
}
case *notionapi.MultiSelectProperty:
var names []string
for _, option := range v.MultiSelect {
names = append(names, option.Name)
}
fmt.Println(strings.Join(names, ", "))
case *notionapi.DateProperty:
if v.Date != nil {
fmt.Printf("Start: %v, End: %v\n", v.Date.Start, v.Date.End)
}
case *notionapi.CheckboxProperty:
fmt.Println(v.Checkbox)
default:
fmt.Println("[Unsupported property type]")
}
}
fmt.Println()
}
func printBlock(client *notionapi.Client, block notionapi.Block, indent string) {
switch b := block.(type) {
case *notionapi.ParagraphBlock:
fmt.Printf("%sParagraph: %s\n", indent, b.GetRichTextString())
case *notionapi.Heading1Block:
fmt.Printf("%sHeading 1: %s\n", indent, b.GetRichTextString())
case *notionapi.Heading2Block:
fmt.Printf("%sHeading 2: %s\n", indent, b.GetRichTextString())
case *notionapi.Heading3Block:
fmt.Printf("%sHeading 3: %s\n", indent, b.GetRichTextString())
case *notionapi.BulletedListItemBlock:
fmt.Printf("%s• %s\n", indent, b.GetRichTextString())
case *notionapi.NumberedListItemBlock:
fmt.Printf("%s1. %s\n", indent, b.GetRichTextString())
case *notionapi.ToDoBlock:
checkbox := "[ ]"
if b.ToDo.Checked {
checkbox = "[x]"
}
fmt.Printf("%s%s %s\n", indent, checkbox, b.GetRichTextString())
case *notionapi.ToggleBlock:
fmt.Printf("%sToggle: %s\n", indent, b.GetRichTextString())
case *notionapi.CodeBlock:
fmt.Printf("%sCode (%s):\n", indent, b.Code.Language)
texts := b.Code.RichText
fmt.Println("```")
for _, text := range texts {
fmt.Println(text.PlainText)
}
fmt.Println("```")
case *notionapi.QuoteBlock:
fmt.Printf("%sQuote: %s\n", indent, b.GetRichTextString())
case *notionapi.CalloutBlock:
fmt.Printf("%sCallout: %s\n", indent, b.GetRichTextString())
case *notionapi.ImageBlock:
fmt.Printf("%sImage: %s\n", indent, b.Image.GetURL())
case *notionapi.BookmarkBlock:
fmt.Printf("%sBookmark: %s\n", indent, b.Bookmark.URL)
default:
fmt.Printf("%s[Unsupported block type: %s]\n", indent, block.GetType())
}
if block.GetHasChildren() {
children, err := client.Block.GetChildren(context.Background(), block.GetID(), nil)
if err != nil {
log.Printf("Error getting child blocks: %v\n", err)
return
}
for _, child := range children.Results {
printBlock(client, child, indent+" ")
}
}
}
結果は以下でした
Title: 記事タイトル
Page Properties:
名前: 記事タイトル
ID: [Unsupported property type]
状態: 前
----------------------------------------
Page Content:
Paragraph:
Heading 2: H2
Paragraph: h3のコンテンツ
Paragraph: h3のコンテンツ
Code (javascript):
```
const a = 100;
function hoge (){
return 1000;
}
```
Paragraph:
Heading 3: h3
Paragraph: h3のコンテンツ
Paragraph:
Image: https://prod-files-secure.s3.us-west-2.amazonaws.com/1422afef-ef18-4209-b105-9507d3caacc1/0bff6871-59ce-4405-9d16-94ec34204c43/download.jpg?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=AKIAT73L2G45HZZMZUHI%2F20241005%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Date=20241005T073646Z&X-Amz-Expires=3600&X-Amz-Signature=ab53ade9643030d7c7f065a71b3851245db1cb8e856f62a55bc16708ac5ad4fc&X-Amz-SignedHeaders=host&x-id=GetObject
Paragraph:
Paragraph:
ちゃんととれてますね。
というか image URLは S3のus-west-2のpresign URLなんですね...
仮にNotionをHeadless CMSとして利用する場合、なんらかのタイミングで画像を自分のオブジェクトストレージに転写するなどの対応が必要そうです。