本文へ移動
aapp-hacks
← 記事一覧へ
著者
app-hacks
公開日
更新日
確認日

Astro / SEO / OGP

Astroで構築するSEOに強いメタタグとOGP画像の共通コンポーネント設計

canonical、description、OGP、TwitterカードをAstroの共通レイアウトで安全に管理する設計を解説する。

Astroの各ページへ直接メタタグを書くと、タイトルの区切り文字、canonical URLの組み立て、OGPの項目がページごとにずれやすい。SEO用コンポーネントを一つ作り、ページ固有の値だけをPropsで受け取る構成にすると、静的ページと記事ページの両方を同じ規則で扱える。

メタタグは検索順位を自動的に上げる仕掛けではない。検索結果や共有画面へページの内容を正しく伝え、同一内容のURLを正規化するための文書情報である。本文と一致しないタイトルや説明を作らず、ページごとの主題を短く具体的に表す。

対象バージョンと確認範囲

この記事は2026年9月11日に、このリポジトリのAstro 7.3.2とNode.js 22.22.2で確認した。SEOHead.astroはtitle、description、canonical、OGP、Twitterカードを出力し、画像はPropsで渡された場合だけメタタグを追加する。サイト共通の実画像はまだ配置していないため、現在の生成HTMLにog:imagetwitter:imageは出力されない。

サイト共通値を分離する

サイト名、説明、URLは一か所に置く。URLは末尾スラッシュの有無をページ側で連結せず、URLコンストラクタへ渡す。

// src/consts.ts
export const SITE = {
  name: 'app-hacks',
  title: 'app-hacks | Web開発の実践ノート',
  description: 'Web開発で再利用できる設計と実装を整理する技術ブログ。',
  url: 'https://app-hacks.net',
} as const;

同じURLはastro.config.mjssiteにも設定する。Sitemap integrationやAstro.siteが絶対URLを生成する基準になるためである。

import { defineConfig } from 'astro/config';

export default defineConfig({
  site: 'https://app-hacks.net',
});

仮ドメインで開発している場合は、公開前にこの二か所を同時に変更する。値を環境変数へ移す設計も可能だが、公開URLが一つだけなら定数の方が構成は単純である。

SEOHeadコンポーネントを作る

タイトル、説明、canonical、ページ種別、OGP画像を受け取る。画像は存在するページだけが指定できる任意項目にし、存在しないURLをメタタグへ出さない。

---
// src/components/SEOHead.astro
import { SITE } from '../consts';

interface Props {
  title?: string;
  description?: string;
  canonical?: string | URL;
  type?: 'website' | 'article';
  image?: string | URL;
}

const {
  title = SITE.title,
  description = SITE.description,
  canonical = new URL(Astro.url.pathname, Astro.site ?? SITE.url),
  type = 'website',
  image,
} = Astro.props;

const canonicalUrl = canonical instanceof URL
  ? canonical
  : new URL(canonical, Astro.site ?? SITE.url);
const imageUrl = image
  ? image instanceof URL
    ? image
    : new URL(image, Astro.site ?? SITE.url)
  : undefined;
---

<title>{title}</title>
<meta name="description" content={description} />
<link rel="canonical" href={canonicalUrl} />

<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:type" content={type} />
<meta property="og:url" content={canonicalUrl} />
<meta property="og:site_name" content={SITE.name} />
<meta property="og:locale" content="ja_JP" />
{imageUrl && <meta property="og:image" content={imageUrl} />}

<meta name="twitter:card" content={imageUrl ? 'summary_large_image' : 'summary'} />
<meta name="twitter:title" content={title} />
<meta name="twitter:description" content={description} />
{imageUrl && <meta name="twitter:image" content={imageUrl} />}

canonicalに相対パスを受け取っても、最終的なhrefは絶対URLになる。OGPのog:urlにも同じURLを使うことで、検索エンジン向けの正規URLとSNS向けURLを一致させる。Astro.url.originは開発時にlocalhostになり、プロキシ環境では意図しないホストを含むことがあるため、本番URLの基準にはAstro.siteまたは管理済みの定数を使う。

OGP画像は相対パスではなく絶対URLが必要である。public/og/default.webpを用意したならimage="/og/default.webp"と渡し、コンポーネント内で絶対URLへ変換する。画像がないのに仮のパスを出力すると、クローラーが404を取得する。画像を用意するまでは画像メタタグを省略し、Twitterカードもsummaryにする方が整合的である。

BaseLayoutから呼び出す

HTML文書の基本要素と共通ヘッダーを持つBaseLayout.astroでSEOHeadを呼ぶ。

---
import SEOHead from '../components/SEOHead.astro';

interface Props {
  title?: string;
  description?: string;
  canonical?: string | URL;
  type?: 'website' | 'article';
  image?: string | URL;
}

const props = Astro.props;
---

<!doctype html>
<html lang="ja">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <SEOHead {...props} />
  </head>
  <body>
    <main><slot /></main>
  </body>
</html>

固定ページはタイトルと説明だけを渡せばよい。canonicalを省略した場合は現在のpathnameから作られる。

<BaseLayout
  title="このサイトについて | app-hacks"
  description="app-hacksの運営方針と掲載内容について。"
>
  <h1>このサイトについて</h1>
</BaseLayout>

記事ページではFrontmatterの値をそのまま渡す。og:typearticleにし、公開日時がある場合はarticle:published_timeもSEOHeadの任意Propsとして追加できる。タイトル末尾へサイト名を付ける規則は記事レイアウト側で一度だけ適用する。

<BaseLayout
  title={`${post.data.title} | app-hacks`}
  description={post.data.description}
  canonical={`/blog/${post.id}/`}
  type="article"
>
  <Content />
</BaseLayout>

メタタグの検証では、ブラウザ表示だけでなく生成されたHTMLを見る。dist内の各ページにタイトル、description、canonical、OGPが一組ずつ存在し、記事ごとの値になっていることを確認する。URLの二重スラッシュ、localhost、空のdescription、存在しない画像パスは公開前に除く。

共通化の目的は項目数を増やすことではなく、正しい値の流れを小さく保つことである。サイト定数、ページProps、絶対URL化という三段階を分離すれば、ドメイン変更やOGP画像の追加でも各ページを個別に修正せずに済む。

構造化データと確認手順

現在の実装では、記事ページに実際のタイトル、著者表記、公開日、更新日を使ったBlogPostingと、ホームから記事までのBreadcrumbListをJSON-LDで出力する。更新日がFrontmatterにない記事は、公開日をdateModifiedとして出力する。存在しない著者名、日付、画像URLは構造化データに補わない。

確認時はnvm useの後にnpm run buildを実行し、生成されたdist/blog/.../index.htmlapplication/ld+json、canonical、OGPの各項目を確認する。実画像を追加する前にimage Propsへ仮のパスを渡すと、共有クローラーが404を取得するため、画像メタタグは省略したままにする。

公式資料と関連記事