OGPを設定してみた by Remix
前書き
個人開発として、知恵を紹介する「チエっと!」というWEBサイトを作りまして、
その時に設定したOGPの設定について纏めます。
前提
- こちらの記事の実装例では
Remix v2
を使用しております。 - あくまで最低限動くまでをゴールとしており、一部最適化はしておりません。
- 例えば画像については別サーバを用意する、キャッシュを活用するなどの最適化は検討できますが、ここでは触れません。
- 各プロパティの詳細はここでは詳しく解説しておりません。以下などを別途ご参照ください。
基本的な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" },
];
};
twitter:xx
の設定はname
という属性名となるので注意です。(≠property
)
画像について
以下のように、titleやdescriptionに加えて画像が表示されるようにできます。
手順は以下です。
- 表示させたい画像を用意します。
- (余談ですが、チエっとはFigmaで作成しております。)
- 用意した画像を、
public/
配下に配置します。- Remixでは、public配下に置くことで公開アセットとして認識されます。
-
og:image
とtwitter:image
のcontent
値に画像へのパスを設定します。- 絶対パスである必要があります。
以下は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関数の引数のdata
keyの型として定義することができます。
参考までに、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 });
}
画像について
ここではQiitaの記事ページのように、各記事のタイトルを画像に埋め込めるような設定を仕様を想定します。
例↓(かなり古い私の記事なので内容は当てにせず...)
実際に実装したものがこちら
設計の要点としては以下です。
- Remixで画像を返すAPIエンドポイントを用意する
- 雛形画像を用意し、
Canvas
を用いて文字列を合成する
まずは以下のライブラリをインストールします。
尚、Vercelにデプロイする場合にはnode-canvas
が動かない事象が発生するため、その場合は@napi-rs/canvas
で代用可能です。
(私はこちらを使用しました。)
以下の記事を参考にしております。
次に、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などを使用できます。
※2:
元となる画像を用意し、Canvasに取り込みます。
※3:
ここでは中心に位置するように調整しております。
以上で実装が完了です。
/api/ogp/articles/{id}
にアクセスして画像が取得できれば成功です。
余談
設定が完了したら、以下のサイトなどから挙動を確認してみましょう。
後書き
適切に設定すれば訴求効果が見込まれ、なんと言ってもサイトの見栄えが良くなるので、
是非設定してみましょう。
参考記事
Discussion