静的サイトのOPG画像生成をNode.js、GitLab CI/CDで自動化した話

本記事は[Cocone Advent Calendar 2022](https://qiita.com/advent-calendar/2022/cocone)の22日目の記事になります。

# はじめに

今回は[全リヴリー大図鑑](https://www.livly.com/picturebook/
)で導入した、静的サイトのOPG画像生成をNode.js、GitLab CI/CDで自動化した話をしたいと思います。

https://www.livly.com/picturebook/

全リヴリー大図鑑はリヴリーアイランドの20周年記念のWEBコンテンツになります。2002年リリースから現在までに登場した全てのリヴリーを紹介しています。

サイトはAmazon S3 を使用した静的ウェブサイトになります。ビルド時にリヴリーデータを取得し、各リヴリーの詳細ページを生成後S3にデプロイしています。 

そして、詳細ページは319ページあり（2022年12月現在）、OGP画像も詳細ページ数と同様に、319枚用意しています。

![OPG画像サンプル](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/31885/442aa585-2379-f491-cdec-d1fbffae0078.png)

# 開発前に決めたこと

OGP画像生成ツールの開発にあたり、事前に決めたことは次の4点になります。

- 詳細ページは静的ページとしてAmazon S3に格納するため、AWS Lambdaの動的生成は利用しない。
- Node.jsを利用し、リヴリーデータの取得から、OGP画像の生成までを行う（あわせて減色も行う）。
- ヘッドレスブラウザの[Puppeteer](https://pptr.dev/)を利用し、テンプレートHTMLにデータをはめ込んだページをキャプチャ・OGP画像として保存する。
- GitLab CI/CDを利用し、OGP画像の生成からAmazon S3へのデプロイを、一気通貫で実行できるようにする。

![生成の流れ](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/31885/e97f057c-9981-a715-698f-efcc93e21275.png)

# 実際のコードとポイント

実際のコードから、主要となるコードとポイントをまとめてみました。

## OGP画像の画像生成

Puppeteerを利用し、OGP画像の生成を行うコードになります。

```typescript:generate.ts
puppeteer
.launch({
    ignoreDefaultArgs: ["--disable-extensions"],
    args: ["--no-sandbox", "--disable-setuid-sandbox"]
})
.then(async (browser) => {
    await Promise.all(
        items.map(async (item) => {
            return await browser.newPage().then(async (page) => {
                // リヴリー情報の取得
                const { name, scientific, imageUrl, imageName } = item;

                // リヴリー画像データの取得・Base64に変更
                const livlyImage = await fetchImage(imageUrl);
                const { headers, data } = livlyImage;
                const livlyImageBase64 = convertImageToBase64(
                    headers["content-type"],
                    data
                );

                // キャプチャサイズの設定
                await page.setViewport({ width: 1200, height: 630 });

                // 取得したリヴリー情報をはめ込んだHTMLコードの取得
                const html: string = templateHTML(
                    name,
                    scientific,
                    livlyImageBase64,
                    backgroundImageBase64
                );

                // HTMLをページに設定
                await page.setContent(html);

                // キャプチャ・保存
                const output = `${dist}/${imageName}`;
                await page.screenshot({ path: output });
            });
        })
    );

    // ブラウザを閉じる
    await browser.close();
});
```

### OGP画像の画像生成コードについて

- 画像データはBase64に変換する必要があります。
- Puppeteerは、次のメソッドを利用しました。
    メソッド  | 機能
    ------------- | -------------
    puppeteer.launch()  | puppeteerを起動
    browser.newPage() | 新規ページを開く
    page.setViewport() | ページのキャプチャサイズの設定（OPG画像の縦横幅になります）
    page.setContent() | ページのHTMLを設定
    page.screenshot() | スクリーンショットの撮影と保存（`{ path: test.png}` と指定すると、test.pngで保存）
    browser.close() | ブラウザを閉じる

## CI/CDワークフロー

GitLab CI/CDパイプラインの設定コードになります。

```yml:.gitlab-ci.yml
stages:
    - build
    - deploy

default:
    image: buildkite/puppeteer #Puppeteerイメージを指定
    cache:
        key:
            files:
                - package-lock.json
                - package.json
        paths:
            - .npm/
    before_script:
        - echo "checking node version"
        - node -v
        - apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 4EB27DB2A3B88B8B
        - apt-get update -yqq
        - apt-get install -yqq fonts-noto-cjk --no-install-recommends #日本語フォントのインストール
        - npm config set "//registry.npmjs.org/:_authToken=${NPM_AUTH_TOKEN}"
        - npm ci --cache .npm/ --prefer-offline --no-audit
    interruptible: true

.build:
    stage: build
    script: API_BASE_URL=$API_BASE_URL npm run build #OPG画像生成プログラムの実行
    artifacts:
        paths:
            - ogp/
        expire_in: 1 day

build_staging:
    extends: .build
    rules:
        - if: $CI_COMMIT_BRANCH == "develop"
        - if: $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "master"
        - if: $CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "master"
    variables:
        API_BASE_URL: "(省略)"

build_prod:
    extends: .build
    rules:
        - if: $CI_PIPELINE_SOURCE == "web" && $CI_COMMIT_BRANCH == "master"
    variables:
        API_BASE_URL: "(省略)"

.deploy:
    stage: deploy
    image: registry.gitlab.com/gitlab-org/cloud-deploy/aws-base:latest
    variables:
        GIT_STRATEGY: none
    cache: {}
    before_script: []
    script: aws s3 sync "${DIST_DIR}" "${S3_URI}" --delete --acl bucket-owner-full-control
    interruptible: false

deploy_staging:
    extends: .deploy
    needs: [build_staging]
    rules:
        - if: $CI_COMMIT_BRANCH == "develop"
    variables:
        DIST_DIR: "./(OGP画像の格納ディレクトリ)"
        S3_URI: "(省略)"
    environment:
        name: staging
        url:  "(省略)"

deploy_prod:
    extends: .deploy
    needs: [build_prod]
    rules:
        - if: $CI_PIPELINE_SOURCE == "web" && $CI_COMMIT_BRANCH == "master"
    variables:
        DIST_DIR: "./(OGP画像の格納ディレクトリ)"
        S3_URI: "(省略)"
    environment:
        name: prod
        url: "https://www.livly.com/picturebook/"

```

### CI/CDワークフローの設定コードについて

-  Dockerのイメージは`buildkite/puppeteer`を利用しました。
-  OGP画像で日本語を動的に表示する場合、日本語フォントのインストールが必要です。


# おわりに

プロジェクトのキックオフMTG時の話では、クオリティを配慮し、デザイン担当者が１枚１枚作成する予定でした。1枚30分で300枚作成した場合でも、大変な制作工数が想定できました。
OPG画像生成を自動化したことで、デザイン担当者の工数の大幅な削減が実現できました。

現在、運用フェーズですが、サイト更新時にパイプラインから `Run pipeline` を実行するだけで作業が終わるのも、大きなメリットです。

静的ウェブサイトでOGP画像生成を検討している場合、参考いただければ嬉しいです。