プッシュごとに Algolia に同期する GitHub Actions


MarkdownをGitHubリポジトリにプッシュする形式で更新するブログサイトで、検索機能を実現したい。Algoliaを用いて、ブログ記事がプッシュされるごとに、記事の内容をGitHub Actions経由でAlgoliaに連携する例をまとめる。流れは以下のようになる。

  1. Markdown形式のデータを取得して適当に加工し、AlgoliaのAPIを呼んでデータを登録(更新)するスクリプトを書く
  2. GitHub Actionsで1.のスクリプトを実行する

Algoliaの設定

Algoliaに新規登録して、インデックスを作成する。インデックス名はpostsとする。APIキーの画面に表示されるApplication IDAdmin API Key、それとインデックス名は後で必要になる。

Algoliaには、ブログ記事ごとにJSON形式のデータを登録していく。今回、JSONのスキーマは以下のようにする。

type AlgoliaIndexData = {
  objectID: string;
  title: string;
  date: string;
  description?: string;
  content: string;
};

objectIDはAlgoliaのインデックスにおいて一意のIDとなる。今回はブログ記事のファイル名(.mdを除く)をそのまま使うことにする。AlgoliaのAPIでデータを投入する際、同じobjectIDが存在する場合は上書きされ、存在しない場合は新規に登録される。titleはブログ記事のタイトル、dateはブログ記事の日付、descriptionはブログ記事の概要、contentはブログ記事の本文を表す。

Algoliaにデータを登録するスクリプトの作成

ブログ記事は、以下のような形式でMarkdownファイルとして保存しているとする。

  • ブログ記事のMarkdownファイルは、/postsに配置している
  • 各Markdownファイルの先頭には、---で囲まれたYAML形式のメタデータ(上のtitle、data、description)がある

スクリプトでは以下のような処理を行う。

  • 全ての記事のパスを取り出し、各ファイルを読み込み、gray-matterでメタデータを取り出す。gray-matterは、Markdownファイルのメタデータを取り出すライブラリで、gray-matterdataプロパティにメタデータが格納される。gray-mattercontentプロパティには、メタデータを除いたMarkdownの本文が格納される。
  • remove-markdownを使って、Markdownの本文からMarkdown記法を除去する。remove-markdownは、Markdownの本文からMarkdown記法を除去するライブラリで、removeMarkdownの戻り値はMarkdown記法を除去した文字列となる。
  • AlgoliaIndexDataの形式にデータを変換する。AlgoliaIndexDatacontentプロパティには、removeMarkdownの戻り値を格納する。以下のスクリプトでは、記事内の改行やタブをスペースに置換している。
  • algoliasearchを使って、AlgoliaのAPIを呼び出し、データを登録する。APIキーは.envファイルから読み込む。
import fs from 'fs';
import path from 'path';
import matter from 'gray-matter';
import 'dotenv/config';
import removeMarkdown from 'remove-markdown'
import { MatterData, AlgoliaIndexData } from 'types';
import algoliasearch from 'algoliasearch';

const postsDirectory = path.join(process.cwd(), 'posts');

const algolia = algoliasearch(
  process.env.ALGOLIA_APPLICATION_ID as string,
  process.env.ALGOLIA_ADMIN_API_KEY as string,
)

const index = algolia.initIndex(
  process.env.ALGOLIA_PRIMARY_INDEX as string,
)

export async function getRawContentsForAlgolia() {
  const fileNames = fs.readdirSync(postsDirectory);
  const indexData: AlgoliaIndexData[] = fileNames.map((fileName) => {
    const slug = fileName.replace(/\.md$/, '');
    const fullPath = path.join(postsDirectory, fileName);
    const fileContents = fs.readFileSync(fullPath, 'utf8');
    const matterResult = matter(fileContents);

    const rawText = removeMarkdown(matterResult.content);
    const contentText = rawText.replace(/[\n\t]/g, ' ');
    return {
      objectID: slug,
      content: contentText,
      ...matterResult.data as MatterData,
    };
  });
  return indexData;
}

async function syncAlgolia() {
  const res = await getRawContentsForAlgolia();
  console.log(res);

  try {
    await index.saveObjects(res);
  } catch (e) {
    console.log(e);
    process.exit(1);
  }
}

syncAlgolia();

これをsync.tsとして保存する。sync.tsを実行すると、Algoliaにデータが登録される。

ts-nodeを用いたスクリプトの実行

sync.tsを実行するには、ts-nodeを使う。ts-nodeは、TypeScriptを実行するためのライブラリで、ts-nodeを使うと、TypeScriptのコードをそのまま実行できる。ts-nodeをインストールする。

npm install -D ts-node

sync.tsを実行する際に適用する、tsconfig.sync.jsonを作成する。

{
  "extends": "./tsconfig.json",
  "compilerOptions": {
    "module": "commonjs",
    "outDir": "dist",
    "noEmit": false
  },
  "exclude": ["node_modules"],
  "include": ["sync.ts"]
}

これでsync.tsを実行して、Algoliaにデータが登録されるか確認する。

./node_modules/.bin/ts-node --project tsconfig.sync.json ./sync.ts

package.jsonscriptssyncを追加しておく。

{
  "scripts": {
    "sync": "./node_modules/.bin/ts-node --project tsconfig.sync.json ./sync.ts"
  }
}

GitHub Actionsで、プッシュごとの実行を設定

.github/workflows/algolia.ymlを以下のように定義する。流れは以下の通り。

  • mainブランチの/posts配下のファイルがプッシュされた場合に、sync.tsを実行する
  • GitHub Actionの環境変数にあるAlgoliaのApplication IDAdmin API Key、インデックス名を.envに書き込む
  • npm run syncを実行する
name: Sync Algolia

on:
  push:
    branches:
      - main 
    paths:
      - 'posts/*'

jobs:
  build:
    runs-on: ubuntu-latest

    strategy:
      matrix:
        node-version: [18.x]

    steps:
    - uses: actions/checkout@v4
    - name: Use Node.js ${{ matrix.node-version }}
      uses: actions/setup-node@v3
      with:
        node-version: ${{ matrix.node-version }}
    - run: npm ci
    - name: Create .env file
      run: |
        echo "ALGOLIA_ADMIN_API_KEY=${{ secrets.ALGOLIA_ADMIN_API_KEY }}" >> .env
        echo "ALGOLIA_APPLICATION_ID=${{ secrets.ALGOLIA_APPLICATION_ID }}" >> .env
        echo "ALGOLIA_PRIMARY_INDEX=${{ secrets.ALGOLIA_PRIMARY_INDEX }}" >> .env
    - name: Sync Algolia
      run: npm run sync

AlgoliaのApplication IDAdmin API Key、インデックス名は、GitHubのSettingsSecretsから設定する。ALGOLIA_ADMIN_API_KEYALGOLIA_APPLICATION_IDALGOLIA_PRIMARY_INDEXという名前で設定する。これを設定すると、GitHub Actionsがプッシュごとにsync.tsを実行するようになる。