💡

OGPを設定してみた by Remix

2024/10/31に公開

前書き

個人開発として、知恵を紹介する「チエっと!」というWEBサイトを作りまして、
その時に設定したOGPの設定について纏めます。

https://chietto.yu-ta-9.com

前提

  • こちらの記事の実装例ではRemix v2を使用しております。
  • あくまで最低限動くまでをゴールとしており、一部最適化はしておりません。
    • 例えば画像については別サーバを用意する、キャッシュを活用するなどの最適化は検討できますが、ここでは触れません。
  • 各プロパティの詳細はここでは詳しく解説しておりません。以下などを別途ご参照ください。

https://seolaboratory.jp/64252/

基本的なOGPの設定

タグについて

基本的なOGPの設定は以下となります。

<meta property="og:title" content="チエっと! | TOP">
<meta property="og:description" content="チエっと!は、あらゆる知恵を学べるサイトです!あなたの暮らしをちょっと便利にする知恵を紹介します!">
<meta property="og:site_name" content="チエっと!">
<meta property="og:type" content="website">
<meta property="og:url" content="https://chietto.yu-ta-9.com">
<meta property="og:image" content="https://chietto.yu-ta-9.com/ogp.png">
<meta name="twitter:title" content="チエっと! | TOP">
<meta name="twitter:description" content="チエっと!は、あらゆる知恵を学べるサイトです!あなたの暮らしをちょっと便利にする知恵を紹介します!">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:image" content="https://chietto.yu-ta-9.com/ogp.png"><meta name="twitter:site" content="@yuta9_drumming">
<meta name="twitter:creator" content="@yuta9_drumming">
<meta property="fb:app_id" content="xxx">

Remixにおいて、これらを実際にHTMLとして出力させるには、各routeファイルに以下のように記述します。

export const meta: MetaFunction = () => {
  return [
    { title: "チエっと! | TOP" },
    { name: "description", content: "チエっと!は、あらゆる知恵を学べるサイトです!あなたの暮らしをちょっと便利にする知恵を紹介します!" },
    { property: "og:title", content: "チエっと! | TOP" },
    { property: "og:description", content: "チエっと!は、あらゆる知恵を学べるサイトです!あなたの暮らしをちょっと便利にする知恵を紹介します!",
    },
    { property: "og:site_name", content: "チエっと!" },
    { property: "og:type", content: "website" },
    { property: "og:url", content: "https://chietto.yu-ta-9.com" },
    { property: "og:image", content: "https://chietto.yu-ta-9.com/ogp.png" },
    { name: "twitter:title", content: "チエっと! | TOP" },
    { name: "twitter:description", content: "チエっと!は、あらゆる知恵を学べるサイトです!あなたの暮らしをちょっと便利にする知恵を紹介します!" },
    { name: "twitter:card", content: "summary_large_image" },
    { name: "twitter:image", content: "https://chietto.yu-ta-9.com/ogp.png" },
    { name: "twitter:site", content: "@yuta9_drumming" },
    { name: "twitter:creator", content: "@yuta9_drumming" },
    { property: "fb:app_id", content: "xxx" },
  ];
};

https://remix.run/docs/en/main/route/meta

twitter:xxの設定はnameという属性名となるので注意です。(≠property

画像について

以下のように、titleやdescriptionに加えて画像が表示されるようにできます。

https://chietto.yu-ta-9.com

手順は以下です。

  1. 表示させたい画像を用意します。
    • (余談ですが、チエっとはFigmaで作成しております。)
  2. 用意した画像を、public/配下に配置します。
    • Remixでは、public配下に置くことで公開アセットとして認識されます。
  3. og:imagetwitter:imagecontent値に画像へのパスを設定します。
    • 絶対パスである必要があります。

以下はpublic/ogp.pngに配置した例です。

{ property: "og:image", content: "https://chietto.yu-ta-9.com/ogp.png" },
{ name: "twitter:image", content: "https://chietto.yu-ta-9.com/ogp.png" }

動的なページのOGPの設定

動的とは、/articles/{article_id}のような同一パスではあるがid値によって内容が変化することを指します。

タグについて

<meta property="og:title" content="チエっと! | 手に付いた油性ペンをキレイに落とす知恵">
<meta property="og:description" content="手に付いた油性ペンは、口紅を塗って馴染ませ、ティッシュで拭くだけで簡単に落とせます。">
<meta property="og:site_name" content="チエっと!">
<meta property="og:type" content="article">
<meta property="og:url" content="https://chietto.yu-ta-9.com/articles/1">
<meta property="og:image" content="https://chietto.yu-ta-9.com/api/ogp/articles/1">
<meta name="twitter:title" content="チエっと! | 手に付いた油性ペンをキレイに落とす知恵">
<meta name="twitter:description" content="手に付いた油性ペンは、口紅を塗って馴染ませ、ティッシュで拭くだけで簡単に落とせます。">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:image" content="https://chietto.yu-ta-9.com/api/ogp/articles/1">
<meta name="twitter:site" content="@yuta9_drumming">
<meta name="twitter:creator" content="@yuta9_drumming">
<meta property="fb:app_id" content="xxx">

Remixにおいて、これらを実際にHTMLとして出力させるには、各routeファイルに以下のように記述します。

export const meta: MetaFunction<typeof loader> = ({ data }) => {
  return [
    { title: `チエっと! | ${data?.article.title}` },
    { name: "description", content: `${data?.article.summary}` },
    { property: "og:title", content: `チエっと! | ${data?.article.title}` },
    { property: "og:description", content: `${data?.article.summary}` },
    { property: "og:site_name", content: "チエっと!" },
    { property: "og:type", content: "article" },
    { property: "og:url", content: `https://chietto.yu-ta-9.com/articles/${data?.article.id}` },
    { property: "og:image", content: `https://chietto.yu-ta-9.com/api/ogp/articles/${data?.article.id}` },
    { name: "twitter:title", content: `チエっと! | ${data?.article.title}` },
    { name: "twitter:description", content: `${data?.article.summary}` },
    { name: "twitter:card", content: "summary_large_image" },
    { name: "twitter:image", content: `https://chietto.yu-ta-9.com/api/ogp/articles/${data?.article.id}` },
    { name: "twitter:site", content: "@yuta9_drumming" },
    { name: "twitter:creator", content: "@yuta9_drumming" },
    { property: "fb:app_id", content: "345035648665601" },
  ];
};

ここではloader関数でDBから情報取得し、その値をmeta関数内で使用しています。
MetaFunction<typeof loader>と型を付けることで、
loader関数の戻り値の型を、meta関数の引数のdatakeyの型として定義することができます。
参考までに、loader関数は以下のように実装しています。
※ORMとしてprismaを使用した記述になっております。

export async function loader({ params }: LoaderFunctionArgs) {
  const article = await prisma.article.findUnique({
    where: {
      id: Number(params.id),
    },
  });

  if (!article) {
    throw new Response(null, {
      status: 404,
      statusText: "Not Found",
    });
  }

  return json({ article });
}

https://remix.run/docs/en/main/route/meta#meta-function-parameters

画像について

ここではQiitaの記事ページのように、各記事のタイトルを画像に埋め込めるような設定を仕様を想定します。

例↓(かなり古い私の記事なので内容は当てにせず...)
https://qiita.com/YU-TA-9/items/d7244ffd09bda08ce8ce

実際に実装したものがこちら

https://chietto.yu-ta-9.com/articles/1/

設計の要点としては以下です。

  1. Remixで画像を返すAPIエンドポイントを用意する
  2. 雛形画像を用意し、Canvasを用いて文字列を合成する

まずは以下のライブラリをインストールします。

https://github.com/Automattic/node-canvas

尚、Vercelにデプロイする場合にはnode-canvasが動かない事象が発生するため、その場合は@napi-rs/canvasで代用可能です。
(私はこちらを使用しました。)

https://github.com/Brooooooklyn/canvas

以下の記事を参考にしております。

https://zenn.dev/ytenden/articles/a4d57154c0ea4d

次に、routeファイルを作成します。
例として、app/routes/api.ogp.articles.$id/route.tsxを作成します。

これにより/api/ogp/articles/{id}のエンドポイントが設置されます。

こちらに以下のような処理を記載します。

import { GlobalFonts, createCanvas, loadImage } from "@napi-rs/canvas";
import type { LoaderFunctionArgs } from "@remix-run/node";
import { prisma } from "~/libs/prisma";

import fs from "node:fs";

import path, { join } from "node:path";
import { cwd } from "node:process";

/** 画像のwidth */
const width = 1200;
/** 画像のheight */
const height = 630;

export async function loader({ params }: LoaderFunctionArgs) {
  const article = await prisma.article.findUnique({
    where: {
      id: Number(params.id),
    },
  });

  if (!article) {
    throw new Response(null, {
      status: 404,
      statusText: "Not Found",
    });
  }

  // ※1 Fontの設定
  GlobalFonts.registerFromPath(join(cwd(), "app", "assets", "fonts", "NotoSansJP-Bold.ttf"), "Noto Sans JP");

  const canvas = createCanvas(width, height);
  const ctx = canvas.getContext("2d");

  // ※2 背景画像の取得
  const ogpArticle = path.resolve(process.cwd(), "app/assets/images/ogp-article.png");
  const image = await loadImage(fs.readFileSync(ogpArticle));
  ctx.drawImage(image, 0, 0, width, height);

  ctx.font = "48px Noto Sans JP";
  ctx.textAlign = "center";
  ctx.textBaseline = "middle";
  ctx.fillStyle = "#0a0805";

  // ※3 文字のセット
  const title = article.title;
  ctx.fillText(title, width / 2, height / 2);

  return new Response(canvas.toBuffer("image/png"), {
    headers: {
      "Content-Type": "image/png",
    },
  });
}

※1:
日本語のフォントがデフォルトでは存在しないので、自前で用意したものをインストールします。
Googleが提供しているfontなどを使用できます。

https://fonts.google.com/noto/specimen/Noto+Sans+JP

※2:
元となる画像を用意し、Canvasに取り込みます。

※3:
ここでは中心に位置するように調整しております。

以上で実装が完了です。
/api/ogp/articles/{id}にアクセスして画像が取得できれば成功です。

余談

設定が完了したら、以下のサイトなどから挙動を確認してみましょう。

https://rakko.tools/tools/9/

後書き

適切に設定すれば訴求効果が見込まれ、なんと言ってもサイトの見栄えが良くなるので、
是非設定してみましょう。

参考記事

https://zenn.dev/panda_program/articles/generate-og-image

Discussion