Astro × microCMS で OGP・サイトマップ・画像の WebP 化を設定する
この回でやること
連載「Astro × microCMS でブログを作る」の第4回です。記事を検索エンジンや SNS に正しく伝えるための設定と、画像を軽くする設定をまとめて入れます。
- canonical・OGP・X(Twitter)カードのメタタグを共通レイアウトに入れる
- meta description を microCMS で入力できるようにし、空なら本文の冒頭から自動で作る
@astrojs/sitemapでサイトマップを生成し、robots.txtから案内する- microCMS の画像API で、画像を WebP に変換・リサイズする
- 本文の画像に
srcsetと遅延読み込みを付ける - 広告を含む記事に「PR表記」を出すフィールドを追加する
前回(第3回:コードブロックのシンタックスハイライト)までで、記事一覧・記事詳細・ページ送りと、コードの色分けができています。この回のコードは、ブランチ 04-seo-image の時点のものです。
なお、astro.config.mjs の site(https://cyber-blow.com)と trailingSlash: 'always' は、第1回で設定済みです。この回の canonical とサイトマップは、どちらもこの2つを前提にしています。
canonical と OGP のメタタグを共通レイアウトに入れる
メタタグは全ページで使うので、共通レイアウト(BaseLayout.astro)にまとめます。ページごとに変わる値は Props で受け取ります。
interface Props {
/** 未指定ならサイト名のみ */
title?: string;
description?: string;
/** OGP画像の絶対URL */
image?: string;
type?: 'website' | 'article';
publishedTime?: string;
modifiedTime?: string;
}
const {
title,
description = SITE_DESCRIPTION,
image,
type = 'website',
publishedTime,
modifiedTime,
} = Astro.props;
const pageTitle = title ? `${title} | ${SITE_NAME}` : SITE_NAME;
const canonical = new URL(Astro.url.pathname, Astro.site);<head> の中はこうなります。
<title>{pageTitle}</title>
<meta name="description" content={description} />
<link rel="canonical" href={canonical} />
<meta property="og:site_name" content={SITE_NAME} />
<meta property="og:type" content={type} />
<meta property="og:title" content={title ?? SITE_NAME} />
<meta property="og:description" content={description} />
<meta property="og:url" content={canonical} />
<meta property="og:locale" content="ja_JP" />
{image && <meta property="og:image" content={image} />}
{publishedTime && <meta property="article:published_time" content={publishedTime} />}
{modifiedTime && <meta property="article:modified_time" content={modifiedTime} />}
<meta name="twitter:card" content={image ? 'summary_large_image' : 'summary'} />ポイントは3つです。
- canonical は
new URL(Astro.url.pathname, Astro.site)で作っています。ドメイン部分は常に設定ファイルのsiteになるので、Cloudflare Pages のプレビュー用 URL(*.pages.dev)で開いても、canonical は本番のcyber-blow.comを指します。パスだけを使うので、クエリ文字列も付きません。 og:imageは画像があるときだけ出します。X のカードも、画像があれば大きい画像のカード(summary_large_image)、なければ小さいカード(summary)に切り替えます。- 記事ページでは
og:typeをarticleにし、公開日時と更新日時をarticle:published_time・article:modified_timeで出します。
記事詳細ページからは、次のように値を渡します。
<BaseLayout
title={blog.title}
description={description}
image={blog.eyecatch && imageUrl(blog.eyecatch.url, { width: 1200, height: 630, format: 'jpg' })}
type="article"
publishedTime={publishedAt}
modifiedTime={revisedAt}
>OGP 画像には、記事のアイキャッチを 1200×630 の JPEG に変換したものを使います。変換に使っている imageUrl は、後半の「microCMS の画像API」で説明します。
meta description を入力できるようにする
検索結果に表示される説明文(meta description)は、記事ごとに書けるようにします。microCMS の blogs API に、次の2つのフィールドを追加しました。
フィールドID | 表示名 | 種類 | 備考 |
|---|---|---|---|
| 説明文 | テキストエリア | meta description。空なら本文の冒頭から自動生成 |
| PR表記 | 真偽値 | 広告・アフィリエイトを含む記事は ON |
型にも追加します。
export type Blog = MicroCMSListContent & {
title: string;
/** リッチエディタのHTML */
content: string;
/** meta description 用。未入力なら本文の冒頭から自動生成する */
description?: string;
eyecatch?: MicroCMSImage;
category?: Category | null;
/** 広告・アフィリエイトを含む記事なら true(PR表記を出す) */
pr?: boolean;
};記事一覧では、転送量を抑えるために fields で取得するフィールドを絞っています(第2回)。新しく足したフィールドは、ここにも忘れずに追加します。
const SUMMARY_FIELDS = [
'id',
'title',
'description',
'eyecatch',
'category',
'pr',
// …省略(createdAt などの日時)
];説明文が空のときは、本文の冒頭120文字を使います。コードブロックの中身が説明文になると読みにくいので、<pre> は取り除いてから文字を取り出します。
/** 本文HTMLから meta description 用の抜粋を作る(コードブロックは除外) */
export function toExcerpt(html: string, length = 120): string {
const $ = cheerio.load(html, null, false);
$('pre').remove();
const text = $.text().replace(/\s+/g, ' ').trim();
return text.length > length ? `${text.slice(0, length)}…` : text;
}const description = blog.description || toExcerpt(blog.content);?? ではなく || にしているので、microCMS で一度入力してから消した場合(空文字)も、本文からの自動生成に切り替わります。
サイトマップを生成する
サイトマップは、Astro 公式の @astrojs/sitemap で生成します。パッケージをインストールして、設定ファイルの integrations に追加します。
import { defineConfig, envField } from 'astro/config';
import sitemap from '@astrojs/sitemap';
// https://astro.build/config
export default defineConfig({
site: 'https://cyber-blow.com',
// canonical・サイトマップ・内部リンクのURL表記を「/」終わりに統一する
trailingSlash: 'always',
integrations: [sitemap()],
// …省略(env の設定)
});@astrojs/sitemap はサイトの URL を知る必要があるので、site の設定が必須です。ビルドすると、出力先に sitemap-index.xml と sitemap-0.xml が作られます。trailingSlash: 'always' にしているので、サイトマップに載る URL も canonical と同じ「/」終わりにそろいます。
検索エンジンにサイトマップの場所を伝えるため、robots.txt にも書いておきます。
User-agent: *
Allow: /
Sitemap: https://cyber-blow.com/sitemap-index.xmlあわせて、共通レイアウトの <head> にも <link rel="sitemap"> を入れました。
<link rel="sitemap" href="/sitemap-index.xml" />参考:@astrojs/sitemap | Astro Docs
microCMS の画像API で WebP 化・リサイズする
microCMS にアップロードした画像は、URL にクエリパラメータを付けるだけで、形式やサイズを変えた画像を受け取れます(画像処理には imgix の Rendering API が使われています)。
パラメータ | 意味 |
|---|---|
| 出力形式( |
| 幅・高さ(ピクセル) |
|
|
| 縦横比を保ったまま指定サイズに収める。元画像のほうが小さければ拡大しない |
毎回 URL を組み立てるのは面倒なので、関数にしました。
type ImageOptions = {
width: number;
/** 指定すると width × height にトリミングする */
height?: number;
format?: 'webp' | 'jpg' | 'png';
};
/**
* microCMS の画像API(imgix互換)でフォーマット・サイズを指定したURLを返す。
* height 未指定時は fit=max なので、元画像より大きく引き伸ばされることはない。
*/
export function imageUrl(src: string, { width, height, format = 'webp' }: ImageOptions): string {
const url = new URL(src);
url.searchParams.set('fm', format);
url.searchParams.set('w', String(width));
if (height) {
url.searchParams.set('h', String(height));
url.searchParams.set('fit', 'crop');
} else {
url.searchParams.set('fit', 'max');
}
return url.toString();
}形式は指定しなければ WebP です。高さを指定したときだけ、その縦横比に切り抜きます。
サイト内では、次のように使い分けています。
使う場所 | 指定 |
|---|---|
記事一覧のカード | 640×360 に切り抜き、WebP |
記事詳細のアイキャッチ | 幅1520まで、WebP |
OGP 画像 | 1200×630 に切り抜き、JPEG |
本文中の画像 | 幅800と1600の2種類、WebP(次の節) |
一覧のカードの画像枠は、CSS で 16:9 に固定しています(aspect-ratio: 16 / 9; object-fit: cover)。そこで、画像API で 640×360(16:9)に切り抜いてから読み込み、<img> の width・height もその値にしました。
<img
class="card-image"
src={imageUrl(post.eyecatch.url, { width: 640, height: 360 })}
alt=""
width="640"
height="360"
loading="lazy"
decoding="async"
/>この変更までは、元画像をそのまま読み込み、ブラウザ側の CSS で 16:9 に切り抜いていました。先に画像API で切り抜いておけば、表示しない部分までダウンロードせずに済みます。
参考:画像API | microCMS ドキュメント、フォーマット | microCMS ドキュメント、Resize Fit Mode | imgix Docs
本文の画像に srcset と遅延読み込みを付ける
アイキャッチやカードの画像は Astro のテンプレートで書いていますが、本文中の画像は microCMS のリッチエディタが出す HTML の中にあります。そこで、第3回でコードブロックを置き換えたのと同じ renderRichText の中で、cheerio を使って <img> を書き換えます。
const MICROCMS_IMAGE_HOST = 'images.microcms-assets.io';
// …省略(コードブロックのハイライト)
$('img').each((_, el) => {
const $img = $(el);
$img.attr({ loading: 'lazy', decoding: 'async' });
const src = $img.attr('src');
if (!src || !isOptimizableImage(src)) return;
$img.attr({
src: imageUrl(src, { width: 800 }),
srcset: `${imageUrl(src, { width: 800 })} 800w, ${imageUrl(src, { width: 1600 })} 1600w`,
sizes: '(min-width: 800px) 760px, 100vw',
});
});// GIF(アニメーションが消える)と SVG は変換しない
function isOptimizableImage(src: string): boolean {
const url = URL.parse(src);
return url?.hostname === MICROCMS_IMAGE_HOST && !/\.(gif|svg)$/i.test(url.pathname);
}やっていることは次のとおりです。
- すべての画像に
loading="lazy"とdecoding="async"を付け、画面に入るまで読み込まないようにする - microCMS の画像(
images.microcms-assets.io)だけを WebP に変換し、幅800と1600の2種類をsrcsetで渡す。高解像度の画面では、ブラウザが1600のほうを選べる - GIF は変換するとアニメーションが消えるので変換しない。SVG も変換の対象から外す
- 外部サイトの画像は、URL を書き換えずにそのまま使う
sizes の 760px は、記事の本文エリアの最大幅(CSS の max-width: 760px)に合わせた値です。
一方、記事詳細のアイキャッチは画面の最初に見える位置にあるので、遅延読み込みにせず fetchpriority="high" を付けて優先して読み込ませています。
<img
class="post-eyecatch"
src={imageUrl(blog.eyecatch.url, { width: 1520 })}
alt=""
width={blog.eyecatch.width}
height={blog.eyecatch.height}
fetchpriority="high"
/>広告を含む記事に PR 表記を出す
先ほど追加した pr フィールドを ON にすると、記事タイトルの下に表記が出るようにしました。
{blog.pr && <p class="pr-notice">本記事にはプロモーション(広告)が含まれています。</p>}アフィリエイトや広告を含む記事に、なぜこの表記が必要なのか(ステマ規制)は、連載「技術ブログの収益化準備」の第1回で詳しく書いています。
注意点:microCMS の fields は綴りを間違えてもエラーにならない
この回では SUMMARY_FIELDS に description と pr を追加しました。ここで注意したいのが、microCMS は fields・orders・filters に存在しないフィールド名を指定してもエラーにならないことです。
たとえば fields に 'descripton' と書き間違えても、ビルドは普通に通ります。そのフィールドが返ってこないだけなので、一覧ページの説明文が出ない原因に気づきにくくなります。
このサイトでは、microCMS に API を作ったあと、各 API にダミーのコンテンツを1件ずつ公開して、API が返す JSON でフィールド名と型がコードと合っているかを確認しました。画像API についても、WebP(記事・カード用)と 1200×630 の JPEG(OGP 用)が実際に返ってくることを、このときに確かめています。
まとめ
- canonical・OGP・X カードのメタタグは共通レイアウトにまとめ、ページごとの値は Props で渡す
- canonical を
Astro.siteから作ると、プレビュー用 URL で開いても本番のドメインを指す - meta description は入力欄を用意し、空なら本文の冒頭(コードブロック以外)から自動で作る
- サイトマップは
@astrojs/sitemapとsiteの設定だけで生成でき、robots.txtと<link rel="sitemap">で場所を伝える - 画像は microCMS の画像API で WebP 化・リサイズし、本文の画像は cheerio で
srcsetと遅延読み込みを付ける
次回は、記事を「連載」としてまとめ、第N回の表示や目次ページを作ります(第5回:連載機能)。