Site cover image

📖Ast-Rock Blog

Notion + astro なブログ

🤖astro-notion-blog のOGイメージを自動生成しよう

記事のタイトルを画像化しよう

OGPとは以下のような画像のことです。

Image in a image block

ブログの記事にOG画像を設定することで、XなどのSNSにURLを貼ると画像付きでシェアすることができるようになります。

この記事では astro-notion-blog で記事のタイトルからOG画像を自動的に作る方法を紹介します。

easy-notion-blog ではサポートされていたのですが、astro-notion-blogでも自動生成OGを使うことができます。

Image in a image block

Xでシェアした場合の表示

💻
テスト環境:
- astro-notion-blog 0.9.1
- Cloudflare Pages

この記事の実際のOG画像

Image in a image block

インストール

npm install satori sharp
npm install -D @types/sharp
npx astro add react

npm install astro-seo

変更するファイル

  1. src/components/OgImage.tsx 新規作成
  2. src/pages/og/[slug].png.ts 新規作成
  3. src/layouts/layout.astro
  4. src/pages/posts/[slug].astro

OgImage.tsx

新規作成
src/components/OgImage.tsx

import satori from 'satori';
import sharp from 'sharp';

// サイト名
const site = 'XXX';
const url = 'XXX';

// ユーザー
const user = 'XXX';
const x = '@XXX';

export async function getOgImage(title: string) {
  const fontData = (await getFontData()) as ArrayBuffer;
  const svg = await satori(
    <div
      style={{
        width: '1200px',
        height: '630px',
        backgroundColor: '#52ACFF',
        backgroundImage: 'linear-gradient(225deg, #52ACFF 34%, #FFE32C 100%)',
        display: 'flex',
        flexWrap: 'nowrap',
        justifyContent: 'center',
        alignItems: 'center',
      }}
    >
      <div
        style={{
          display: 'flex',
          width: '1140px',
          height: '567px',
          background: 'rgba(255,255,255,0.7)',
          borderRadius: '8px',
          flexWrap: 'wrap',
          justifyContent: 'center',
        }}
      >
        <div
          style={{
            width: '960px',
            height: '80%',
            fontSize: '64px',
            color: '#222',
            textShadow: '2px 2px 3px #d5d5d5',
            alignItems: 'center',
          }}
        >
          {title}
        </div>
        <div
          style={{
            display: 'flex',
            width: '960px',
            paddingBottom: '4px',
            height: '40px',
            fontSize: '2rem',
          }}
        >
          {user + x}
        </div>
        <div
          style={{
            flexBasis: '54%',
            marginRight: '5.5rem',
            display: 'flex',
          }}
        ></div>
      </div>
    </div>,
    {
      width: 1200,
      height: 630,
      fonts: [
        {
          name: 'Noto Sans JP',
          data: fontData,
          style: 'normal',
        },
      ],
    }
  );

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

async function getFontData() {
  const API = `https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@700`;

  const css = await (
    await fetch(API, {
      headers: {
        // Make sure it returns TTF.
        'User-Agent':
          'Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_8; de-at) AppleWebKit/533.21.1 (KHTML, like Gecko) Version/5.0.5 Safari/533.21.1',
      },
    })
  ).text();

  const resource = css.match(
    /src: url\((.+)\) format\('(opentype|truetype)'\)/
  );

  if (!resource) return;

  return await fetch(resource[1]).then((res) => res.arrayBuffer());
}

コードをカスタムして好きなようにレイアウトを変更できます。

サイト名、サイトURLなど表示させたい場合は任意の場所に{site}{url}などを設定して挿入してみてください。

slug.png.ts

新規作成
src/pages/og/[slug].png.ts

import type { APIContext } from 'astro';
import { getOgImage } from '../../components/OgImage';
import { getAllPosts, getPostBySlug } from '../../lib/notion/client';

export async function getStaticPaths() {
  const posts = await getAllPosts();
  return posts.map((post) => ({
    params: { slug: post.Slug },
  }));
}

export async function get({ params }: APIContext) {
  if (params.slug === undefined) return;
  const post = await getPostBySlug(params.slug);
  const body = await getOgImage(post?.Title ?? 'No title');

  return { body, encoding: 'binary' };
}

layout.astro

src/layouts/layout.astro

</head> 部分まで 置き換え

---
import { SEO } from 'astro-seo';
import { PUBLIC_GA_TRACKING_ID, ENABLE_LIGHTBOX } from '../server-constants.ts'
import { getDatabase } from '../lib/notion/client.ts'
import { getNavLink, getStaticFilePath, filePath } from '../lib/blog-helpers.ts'
import '../styles/syntax-coloring.css'
import GoogleAnalytics from '../components/GoogleAnalytics.astro'
import SearchModal from '../components/SearchModal.astro'
import SearchButton from '../components/SearchButton.astro'

export interface Props {
  title: string
  description: string
  path: string
  ogImage: string
	openGraph: OpenGraph
}

export type OpenGraph = {
  basic: {
    title: string;
    type: 'article' | 'website';
    image: string;
  };
  image: {
    alt: string;
  };
};

const database = await getDatabase()

const {
  title = '',
  description = '',
  path = '/',
  ogImage = '',
  openGraph,
} = Astro.props;

const siteTitle = title ? `${title} - ${database.Title}` : database.Title
const siteDescription = description ? description : database.Description
const siteURL = new URL(getNavLink(path), Astro.site).toString()
const siteOGImage = new URL(
  getStaticFilePath('/default-og-image.png'),
  Astro.site
)

let coverImageURL: string
if (database.Cover) {
  if (database.Cover.Type === 'external') {
    coverImageURL = database.Cover.Url
  } else if (database.Cover.Type === 'file') {
    try {
      coverImageURL = filePath(new URL(database.Cover.Url))
    } catch (err) {
      console.log('Invalid DB cover image URL')
    }
  }
}

let customIconURL: string
if (database.Icon && database.Icon.Type === 'file') {
  try {
    customIconURL = filePath(new URL(database.Icon.Url))
  } catch (err) {
    console.log('Invalid DB custom icon URL')
  }
}
---

<!DOCTYPE html>
<html lang="en" prefix="og: https://ogp.me/ns#">
  <head>
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta name="robots" content="max-image-preview:large" />
    <meta charset="UTF-8" />
    <meta name="generator" content={Astro.generator} />
    <SEO
      charset="UTF-8"
      title={siteTitle}
      description={siteDescription}
      openGraph={openGraph || {
        basic: {
          title: `${siteTitle}`,
          type: "website",
          image: `${siteOGImage}`,
        }
      }}
      twitter={{
        card: 'summary_large_image',
      }}
    />
    <link
      rel="stylesheet"
      href="https://cdn.jsdelivr.net/npm/[email protected]/dist/katex.min.css"
      integrity="sha384-vKruj+a13U8yHIkAyGgK1J3ArTLzrFGBbBc0tDp4ad/EyewESeXE/Iv67Aj8gKZ0"
      crossorigin="anonymous"
    />
  </head>

slug.astro

src/pages/posts/[slug].astro

<Layout>置き換え

<Layout
  title={post.Title}
  description={post.Excerpt}
  path={getPostLink(post.Slug)}
  ogImage={ogImage}
  openGraph={{
    basic: {
      title: post.Title,
      type: 'article',
      image: new URL(`/og/${post.Slug}.png`, Astro.url.origin).toString(),
    },
    image: { alt: post.Title },
  }}
>

記事内にOG画像を表示

記事の中にもOG画像を表示したい場合は以下のコードを挿入する

src/pages/posts/[slug].astro

任意の箇所に追加

<img
	src={'/og/' + `${post.Slug}` + '.png'}
	width="1200"
	height="630"
	alt="ogImage"
	loading="lazy"
	class="ogImage"
/>

一番下に追加 (幅いっぱいに表示する)

<style>
.ogImage {
  max-width: 100%;
  height: fit-content;
}
</style>

記事一覧にOG画像を表示

トップページや、タグページの記事リストにサムネイルとして表示

src/pages/posts/index.astro など

<img> を挿入

posts.length === 0 ? (
  <NoContents contents={posts} />
) : (
  posts.map((post) => (
    <div class={styles.post} key={post.Slug}>
      <PostDate post={post} />
      <PostTags post={post} />
      <PostTitle post={post} />
      <img
        src={'/og/' + `${post.Slug}` + '.png'}
        width="1200"
        height="630"
        alt="ogImage"
        loading="lazy"
        class="ogImage"
      />
      <PostExcerpt post={post} />
      <ReadMoreLink post={post} />
    </div>
  ))
)

最後に挿入

<style>
.ogImage {
  max-width: 100%;
  height: fit-content;
}
</style>

スタイルを整えてこんな感じの表示にすることもできます

Image in a image block

Satori の使い方

レイアウトやフォント変更など

ランダム画像

lorempicsum の画像を挿入

<img src="<https://picsum.photos/200/300>" width={200} height={300} />

PNG を変換

PNGアイコンなどを挿入したい場合はbase64に変換すると少し早く処理できる

pngはbase64に変換する
https://web-toolbox.dev/tools/base64-encode-image

DataURLについて
https://zenn.dev/goahi/articles/daf5ebefd13545

<img src="data:image/png;base64,..." width={200} height={200} />

ずれる場合はtransformで移動させる

transform: 'translate(-60px,-100px)',

フォントを変更

src/components/OgImage.tsx

Zen Kaku Gothic New

const API = `https://fonts.googleapis.com/css2?family=Zen+Kaku+Gothic+New:wght@700`;
fonts: [
        {
          name: 'Zen Kaku Gothic New',
          data: fontData,
          style: 'normal',
        },
      ],

サイトの雰囲気に合わせてフォントを変えてみましょう

Image in a image block

Image in a image block
Image in a image block

Image in a image block

自分で用意したフォントを使う場合

src/components/OgImage.tsx

import fs from 'fs';

publicディレクトリ(任意)にフォントを入れる

- const fontData = (await getFontData()) as ArrayBuffer;
+ const fontData = fs.readFileSync('./public/XXX.ttf');

表示確認用デモ

以下でデザインを確認しながら作業できます

コンテナサイズ (1.9:1)

  • 横: 1200
  • 縦: 630

参考になるサイト

Astroでsatoriを使ったOG画像の自動生成を実装する | Blog

https://blog.70-10.net/posts/satori-og-image/

satori 使い方
https://github.com/vercel/satori

Satori + SvelteKit で OGP 画像を自動生成する

https://azukiazusa.dev/blog/satori-sveltekit-ogp-image/