3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

この記事の概要

Astrosatori を組み合わせて動的な OGP 画像を生成しようとしていました。

用意しておいた画像と組み合わせて OGP を生成しようとしたら苦戦したので、起きたことや解決策を備忘録として記事にします。

前提

環境

主要なものだけを記載します。

依存関係 バージョン
astro 4.9.2
@astrojs/react 3.4.0
react 18.3.1
satori 0.10.13
sharp 0.33.4

やろうとしていたこと

内容自体は簡単で、次の通りです。

  1. 背景に画像を敷く
    1. S3 などにアップされている画像ではなく、リポジトリ内に存在する画像
    2. background-image か、img 要素かなどは問わない
  2. その上に記事タイトルを配置する
  3. ビルド時に書き出して配信する

参考にしていた記事

1 番最初はこの記事を参考にして、文字を描画する段階までは進めました。

ただ、ローカル画像を使おうとしたら上手くいかなくなってしまいました。

解決策

最終的には次のようにして解消しました。

  1. OGP に使う元となる画像をpublicディレクトリに保存する
  2. 画像をbase64形式で読み込む
  3. satori内のimgsrcbase64形式で読み込んだ画像を指定する

コードの全体像はこのようになっています。
(この記事に関係ある部分だけを抜き出しています)

import { readFileSync } from "node:fs";
import satori from "satori";
import sharp from "sharp";

async function getFontData(text: string) {
  // フォントデータ取得のための処理
}

const imgBase64 = readFileSync(
  new URL("../../public/og-image-background.png", import.meta.url),
  { encoding: "base64" },
);
const imgDataUrl = `data:image/png;base64,${imgBase64}`;

export async function getOgImage(text: string) {
  const fontData = (await getFontData(text)) as ArrayBuffer;
  const svg = await satori(
    <div style={{ ... }}>
      <img style={{ ... }} src={imgDataUrl} />
      <div style={{ ... }}>{text}</div>
    </div>,
    {
      width: 1200,
      height: 630,
      fonts: [{ ... }],
    },
  );

  return await sharp(Buffer.from(svg)).png().toBuffer();
}

ダメだったこと

通常のimportimgsrcを指定する

最初はこのように書いてみました。

import imgSrc from "../assets/og-image-background.png";

...

export async function getOgImage(text: string) {
  const fontData = (await getFontData(text)) as ArrayBuffer;
  const svg = await satori(
    <div style={{ ... }}>
      <img style={{ ... }} src={imgSrc} />
      <div style={{ ... }}>{text}</div>
    </div>,
    // 中略
  );

  return await sharp(Buffer.from(svg)).png().toBuffer();
}

すると、次のようなエラーメッセージが出ました。

Unsupported image type: unknown

.astro以外のファイルで画像を指定するときは、書き方を変える必要がありました。1

  import imgSrc from "../assets/og-image-background.png";

  ...

  export async function getOgImage(text: string) {
    const fontData = (await getFontData(text)) as ArrayBuffer;
    const svg = await satori(
      <div style={{ ... }}>
-       <img style={{ ... }} src={imgSrc} />
+       <img style={{ ... }} src={imgSrc.src} />
        <div style={{ ... }}>{text}</div>
      </div>,
      // 中略
    );

    return await sharp(Buffer.from(svg)).png().toBuffer();
  }

しかしこれでもエラーが出ます。

Image source must be an absolute URL: ~~~

相対パス、絶対パスの両方を試しましたがダメでした。

楽に解決しようと外部にアップした画像の URL を指定することなのですが、色々と自分のやりたいこととの兼ね合いで、ローカル画像でも上手くいく方法を模索します。

base64で画像を読み込む

satori のドキュメントに「生成されたSVGをPNGなどの他の画像フォーマットにレンダリングしたい場合は、props.srcとして直接base64エンコードされた画像データ(またはバッファ)を使用した方が、Satoriでの余分なI/Oが不要になります。」といった記載があるのを見つけました。2

余分とかでなくそもそも生成できないのですが、base64 でのアプローチに価値はありそうなので試してみました。

- import imgSrc from "../assets/og-image-background.png";

  ...

+ const imgBase64 = readFileSync(
+   new URL("../assets/og-image-background.png", import.meta.url),
+   { encoding: "base64" },
+ );
+ const imgDataUrl = `data:image/png;base64,${imgBase64}`;

  export async function getOgImage(text: string) {
    const fontData = (await getFontData(text)) as ArrayBuffer;
    const svg = await satori(
      <div style={{ ... }}>
-       <img style={{ ... }} src={imgSrc.src} />
+       <img style={{ ... }} src={imgDataUrl} />
        <div style={{ ... }}>{text}</div>
      </div>,
      // 中略
    );

    return await sharp(Buffer.from(svg)).png().toBuffer();
  }

これにより、開発環境での問題はクリアしました。
記事ごとにちゃんとテキストの違う画像が生成できました。

ただし、ビルドするとエラーが出ました。

ENOENT: no such file or directory, open '/PATH/TO/REPOSITORY/dist/assets/og-image-background.png'

dist/はビルド生成物が格納されるディレクトリです。

そして、Astro はデフォルトでは画像ファイルをdist/_astroに格納します。3

そのためdist/assetsなどというディレクトリは存在せず、エラーになってしまいました。

ここでastro.config.mjsの設定を変えてみました。
この設定によりdist/_astrodist/assetsという名前になります。34

  export default defineConfig({
    ...
+   build: {
+     assets: "assets",
+   },
  });

ところが、これでもファイルは見つかりません。
画像が書き出される際、ファイル名にハッシュ値がついてog-image-background.ハッシュ値.pngという名前になり、見つけられなかったのです。

public/に画像を置く

実現したいのは、ビルド時にディレクトリ名やファイル名が変わらない状態です。

となるとsrc/以下ではなくpublic/におけば良いのでは?と思い試してみました。

  ...

  const imgBase64 = readFileSync(
-   new URL("../assets/og-image-background.png", import.meta.url),
+   new URL("../../public/og-image-background.png", import.meta.url),
    { encoding: "base64" },
  );
  const imgDataUrl = `data:image/png;base64,${imgBase64}`;

  export async function getOgImage(text: string) {
    const fontData = (await getFontData(text)) as ArrayBuffer;
    const svg = await satori(
      <div style={{ ... }}>
        <img style={{ ... }} src={imgDataUrl} />
        <div style={{ ... }}>{text}</div>
      </div>,
      // 中略
    );

    return await sharp(Buffer.from(svg)).png().toBuffer();
  }

こちらで解消し、冒頭に載せたコードと同じになりました。

まとめ

実現したいことの割に悩みましたが、無事解消して良かったです。

それなりにやりたい人が多そうな内容だと思っているのですが、X でも Stack Overflow でも個人ブログでも、実例を全然見つけられませんでした。

ややニッチですが、どなたかのお役に立てれば幸いです。

  1. https://docs.astro.build/en/guides/imports/#other-assets

  2. https://github.com/vercel/satori?tab=readme-ov-file#images

  3. https://docs.astro.build/en/reference/configuration-reference/#buildassets 2

  4. 自動で生成されるディレクトリの名前を不用意に(しかも、何かしらと衝突しそうなassetsなんて名前に)変えるなんてやめた方が良いとは思いますが、一旦試さずにはいられませんでした。

3
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?