astro-notion-blogとは?

Article Cover Image

本記事ではastro-notion-blogをカスタマイズしたい方向けに、astro-notion-blogがどの様な構造となっているかを私自身の備忘録がてら解説していきます。

1. ダイアグラム

構造のダイアグラムを見ながらそれぞれの概要について説明します。

  • Server: astro-notion-blogを動かすサーバです。AWS, GCP, Azure, Cloudflareなどのサーバを指します。(おそらくオンプレの方はいないかと)
  • Frontend: astroのコンポーネントからページを表示します。*.astro拡張子のファイルを主に指します。(astro全体がフロントエンドとみなすこともできます。ただここではビューを担当する箇所を指すこととします。)
  • Cache: astro-notion-blogでは全てのpostデータとdbデータをキャッシュしています。キャッシュしているといってもredisやmemcachedなどのサービスを使用しているわけではなく、以下のコードの様にメモリ(おそらくヒープ領域)に記録しているだけです。
    let postsCache: Post[] | null = null
    let dbCache: Database | null = null
    
    export async function getAllPosts(): Promise<Post[]> {
    	// キャッシュしていればキャッシュしたものを返す
      if (postsCache !== null) {
        return Promise.resolve(postsCache)
      }
    	
    	// allPostsを取得
      ...
    	
    	// キャッシュに入れる
      postsCache = results
        .filter((pageObject) => _validPageObject(pageObject))
        .map((pageObject) => _buildPost(pageObject))
      return postsCache
    }
    
    ...
    
    export async function getDatabase(): Promise<Database> {
      // キャッシュしていればキャッシュしたものを返す
      if (dbCache !== null) {
        return Promise.resolve(dbCache)
      }
    
      // databaseを取得
      ...
      
    	// キャッシュに入れる
      const database: Database = {
        Title: res.title.map((richText) => richText.plain_text).join(''),
        Description: res.description
          .map((richText) => richText.plain_text)
          .join(''),
        Icon: icon,
        Cover: cover,
      }
    
      dbCache = database
      return database
    }
    src/lib/notion/client.ts
  • api endpoint: astro自体がapiのエンドポイント作成できる機能を提供しています。astro-notion-blogではGETリクエストのfeedのみapiエンドポイントとして登録されています。

2. 備忘録

astro-notion-blogをカスタマイズする際に気を付けるべき特徴をメモしていきます。

1. public

1-1. notion

public/notion ディレクトリではnotionに添付した画像やファイル(PDF等)などがダウンロードされて保存されています。なざこの様にダウンロードして保存しておくことが必要であるか説明します。

まずは一旦astro-notion-blogのライフサイクル(staticモードでのastroのライフサイクル)を確認してみましょう。以下の様にastro-notion-blogのライフサイクルはとてもシンプルで、あくまで静的コンテンツの配信を目的としています。つまり、buildの段階で全てのコンテンツができている必要があります。

ここで問題となってくるのが、notionの画像やファイルの公開URLには以下の様に有効期限(ExpiryTime)が設定されていることです。ちなみにこれよりnotionはアップロードしたファイルをAWSのS3に保存していることがわかります。またnotion apiに公開URLのリクエストをすると、s3 presigned URL(期限付きURL)が返ってくることがわかります。

{
  Type: 'file',
  Url: 'https://prod-files-secure.s3.us-west-2.amazonaws.com/e9a2c221-2a31-4e2b-a0aa-d6307489249f/6dd338e7-42f6-4688-8a67-0d0c07e87788/sample.pdf?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=AKIAT73L2G45HZZMZUHI%2F20240424%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Date=20240424T090527Z&X-Amz-Expires=3600&X-Amz-Signature=d038a2c5928985139a079bfccd60a12649c59aa97c07ab89df5d12e76d0172a1&X-Amz-SignedHeaders=host&x-id=GetObject',
  ExpiryTime: '2024-04-24T10:05:27.244Z'
}

つまりbuildのタイミングでnotion apiを叩いて取得した画像やファイルの公開URLはこの有効期限を過ぎると無効なURLになってしまいます。また上述した様にastro-notion-blogは静的にコンテンツを配信するstatic設定で使用しているため、有効期限が切れた画像やファイルの公開URLをnotion apiにリクエストして再取得するといったことができません。

⚠️
厳密にはastroのSSRを有効にすればnotion apiを叩いて公開URLの再取得などができますが、それだとastroの本来の良さである静的コンテンツ配信による配信速度の良さがなくなってしまいます。簡単に言うと「Reactなどで良いのでは?」となってしまいます。

これを解決するにはいくつか方法があります。astro-notion-blogではbuildするタイミングで全ての画像とファイルを(astro-notion-blogを動かす)サーバにダウンロードし、表示においては直接ダウンロードしたものを参照する様に置き換えています。

このダウンロードした画像やファイルを保存しているのがpublic/notionディレクトリです。実際のダウンロードのコードは以下のようになっています。

  • 記事ページ内のnotionにアップロードされたファイルのダウンロード
    ...
    
    const fileAtacchedBlocks = extractTargetBlocks('image', blocks)
      .concat(extractTargetBlocks('file', blocks))
      .filter((block) => {
        if (!block) {
          return false;
        }
        const imageOrFile = block.Image || block.File;
        return imageOrFile && imageOrFile.File && imageOrFile.File.Url;
      });
    
    // Download files
    await Promise.all(
      fileAtacchedBlocks
        .map(async (block: interfaces.Block) => {
          let expiryTime = '';
          if (block.Image) {
            expiryTime = block.Image.File?.ExpiryTime as string;
          } else if (block.File) {
            expiryTime = block.File.File?.ExpiryTime as string;
          }
          if (Date.parse(expiryTime) > Date.now()) {
            return Promise.resolve(block);
          }
          return getBlock(block.Id);
        })
        .map((promise) => promise.then((block) => {
          let url!: URL;
          try {
            if (block.Image) {
              url = new URL(block.Image.File?.Url as string);
            } else if (block.File) {
              url = new URL(block.File.File?.Url as string);
            } else {
              throw new Error('Invalid file URL');
            }
          } catch (err) {
            console.log('Invalid file URL');
            return Promise.reject();
          }
          return Promise.resolve(url);
        }))
        .map((promise) => promise.then(downloadFile)),
    );
    
    ...
    src/pages/posts/[slug].astro
  • FeaturedImage, CoverImage, Iconなどのダウンロードコードはsrc/integrations下にあります。そしてastroのコンフィグで実行させています。
    ...
    
    // https://astro.build/config
    export default defineConfig({
      site: getSite(),
      base: BASE_PATH,
      integrations: [
        CoverImageDownloader(),
        CustomIconDownloader(),
        FeaturedImageDownloader(),
        PublicNotionCopier(),
      ],
    });
    astro.config.mjs
1-2. scripts

ここではhtmlに埋め込むscriptファイルを置いておきます。astro-notion-blogではfslightbox.jsをここに置いてます。ここに置くことで以下のようにhtml内に埋め込むことが可能となります。fslighbox.js自体はこちらから参照やダウンロードができます。

<!DOCTYPE html>
<html lang="en" prefix="og: https://ogp.me/ns#">
  <head>
    ...
  </head>
  <body>
    ...
    <SearchModal />
    {
      ENABLE_LIGHTBOX && (
        <script src={getStaticFilePath('/scripts/fslightbox.js')} />
      )
    }
  </body>
</html>
src/layouts/Layout.astro

2. src/components/notion-blocks

notionの様々なブロックをastroコンポーネントとして作っています。細かいデザインの変更はここのastroコンポーネントファイルを変更することで実現できます。例えば私はTable.astrooverflow: auto;を設定しています。これによってモバイルデバイス等画面が小さいデバイスでテーブルが画面サイズを超過したときにスクロールできるようにしました。

3. src/lib/notion/client.ts

notion apiに対してどの様なリクエストを送って、何を取得しているかを定義しています。基本的にはこれとnotion apiのドキュメントを見ることで何をしているかがわかります。

関連記事