[Astro] SEO実装

2026-03-28 hit count image

AstroブログでSEO(検索エンジン最適化)を実装する方法を共有します。メタタグ、Open Graph、JSON-LD構造化データ、サイトマップ、RSS、Google Analyticsなどを扱います。

astro

概要

前回の記事多言語(i18n)実装で3言語をサポートする多言語システムを実装しました。今回の記事では検索エンジン最適化(SEO)のための様々な手法をAstroで実装する方法を扱います。

すべてのSEO関連ロジックはsrc/components/Head.astroコンポーネントに集中しているため、一箇所で管理できます。

Head.astroコンポーネント

Head.astroはすべてのページの<head>領域を担当するコンポーネントです。Propsでページ情報を受け取り、適切なメタタグと構造化データを生成します。

// src/components/Head.astro
interface Props {
  title: string;
  description: string;
  image?: string;
  lang: Lang;
  permalink: string;
  isPost?: boolean;
  date?: string;
  categoryPosts?: CategoryPostItem[];
  noindex?: boolean;
  category?: string;
  isHome?: boolean;
  isCategory?: boolean;
}
Props説明
titleページタイトル
descriptionページ説明(160文字超過時に自動切り詰め)
image代表画像パス
lang現在の言語(jakoen
permalinkページURLパス
isPostブログポストかどうか(BlogPosting JSON-LD出力用)
date作成日(ポストの場合)
categoryPostsカテゴリポスト一覧(ItemList JSON-LD出力用)
noindex検索エンジンインデックス除外有無
isHomeホームページかどうか(WebSite JSON-LD出力用)
isCategoryカテゴリページかどうか(CollectionPage JSON-LD出力用)

ページの種類に応じて異なるPropsを渡し、適切なSEOタグを生成します。

基本メタタグ

<meta charset="utf-8" />
<meta
  name="viewport"
  content="width=device-width, initial-scale=1, shrink-to-fit=no"
/>
<meta name="description" content="{description}" />
<meta name="author" content="dev-yakuza" />
<title>{title} | DeKu</title>

descriptionは160文字を超えると自動的に切り詰められます。検索結果に表示される長さ制限に合わせるためです。

const description =
  rawDescription && rawDescription.length > 160
    ? rawDescription.slice(0, 160)
    : rawDescription;

Canonical URLとhreflang

多言語サイトで最も重要なSEO要素です。

Canonical URL

<link rel="canonical" href="{canonicalUrl}" />

canonicalタグは現在のページの正規URLを指定します。重複コンテンツの問題を防止し、検索エンジンに「このURLがこのページの代表URL」と伝えます。

hreflang

<link rel="alternate" hreflang="ja" href={`${siteUrl}${rawPermalink}`} />
<link rel="alternate" hreflang="ko" href={`${siteUrl}/ko${rawPermalink}`} />
<link rel="alternate" hreflang="en" href={`${siteUrl}/en${rawPermalink}`} />
<link rel="alternate" hreflang="x-default" href={`${siteUrl}${rawPermalink}`} />

hreflangタグは同じコンテンツの他言語バージョンを検索エンジンに伝えます。例えば、日本で検索すると日本語版を、韓国で検索すると韓国語版を検索結果に表示します。

x-defaultはどの言語にも該当しないユーザーに表示するデフォルトページを指定します。

rawPermalinkは言語プレフィックスを除去したパスです。

const rawPermalink = permalink
  .replace(/^\/(ko|en)\//, '/')
  .replace(/^\/(ko|en)$/, '/');

Open Graphタグ

ソーシャルメディアでリンクを共有する時に表示される情報を制御します。

<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:image" content={ogImage} />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta property="og:url" content={canonicalUrl} />
<meta property="og:type" content={isPost ? 'article' : 'website'} />
<meta property="og:site_name" content="DeKu" />
<meta property="og:locale" content={localeMap[lang]} />

主要ポイント

Open Graphの主要タグは以下の通りです。

  • og:type:ブログポストはarticle、それ以外はwebsiteに設定
  • og:locale:言語別ロケールマッピング(ja_JPko_KRen_US
  • og:locale:alternate:他言語バージョンのロケールも一緒に提供
  • og:image:画像がない場合はサイトアイコンをデフォルト値として使用

ポストの場合は追加情報も含めるようにしました。

{isPost && date && <meta property="article:published_time" content="{date}" />}
{isPost && <meta property="article:author" content="dev-yakuza" />} {isPost &&
category && <meta property="article:section" content="{category}" />}

Twitter Cards

Twitter(X)でリンクを共有する時のカード形式を指定します。

<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:site" content="@DevYakuza" />
<meta name="twitter:creator" content="@DevYakuza" />
<meta name="twitter:title" content="{title}" />
<meta name="twitter:description" content="{description}" />
<meta name="twitter:image" content="{ogImage}" />
<meta name="twitter:image:alt" content="{title}" />

summary_large_imageカードタイプを使用すると大きな画像が含まれたカードとして表示されます。

JSON-LD構造化データ

検索エンジンがページの内容をより正確に理解できるように構造化データを提供します。JSON-LD形式を使用し、ページの種類に応じて異なるスキーマを出力します。

Person(すべてのページ)

著者のソーシャルメディアプロフィールを連携して検索エンジンに著者情報を提供します。

{
  "@context": "https://schema.org",
  "@type": "Person",
  "name": "dev-yakuza",
  "url": "https://deku.posstree.com",
  "sameAs": [
    "https://github.com/dev-yakuza",
    "https://www.facebook.com/yakuza.dev.54",
    "https://www.instagram.com/dev_yakuza/",
    "https://twitter.com/DevYakuza"
  ]
}

Organization(すべてのページ)

サイト(または組織)の基本情報を検索エンジンに伝えます。サイト名、URL、ロゴ画像を含め、検索結果でブランド情報が正確に表示されるようにします。

{
  "@context": "https://schema.org",
  "@type": "Organization",
  "name": "DeKu",
  "url": "https://deku.posstree.com",
  "logo": "https://deku.posstree.com/assets/images/icon.jpg"
}

BlogPosting(ポストページ)

以下のように設定してGoogle検索結果でリッチスニペットとして表示されるようにします。

{
  "@context": "https://schema.org",
  "@type": "BlogPosting",
  "headline": "ポストタイトル",
  "description": "ポスト説明",
  "datePublished": "2026-04-29T00:00:00.000Z",
  "author": {
    "@type": "Person",
    "name": "dev-yakuza",
    "url": "https://deku.posstree.com"
  },
  "publisher": {
    "@type": "Organization",
    "name": "DeKu",
    "logo": {
      "@type": "ImageObject",
      "url": "https://deku.posstree.com/assets/images/icon.jpg"
    }
  },
  "mainEntityOfPage": {
    "@type": "WebPage",
    "@id": "https://deku.posstree.com/astro/seo/"
  }
}

WebSite(ホームページ)

SearchActionを含めるとGoogle検索結果でサイト内検索ボックスが表示される可能性があります。

{
  "@context": "https://schema.org",
  "@type": "WebSite",
  "name": "DeKu",
  "url": "https://deku.posstree.com",
  "inLanguage": ["ja", "ko", "en"],
  "potentialAction": {
    "@type": "SearchAction",
    "target": {
      "@type": "EntryPoint",
      "urlTemplate": "https://deku.posstree.com/search/?q={search_term_string}"
    },
    "query-input": "required name=search_term_string"
  }
}

ItemList(カテゴリページ)

カテゴリページに属するポスト一覧を順序付きリストとして検索エンジンに渡します。各項目の順序(position)、URL、タイトルを含め、検索エンジンがカテゴリ内のコンテンツ構造を把握できるようにします。

{
  "@context": "https://schema.org",
  "@type": "ItemList",
  "itemListElement": [
    { "@type": "ListItem", "position": 1, "url": "...", "name": "..." },
    { "@type": "ListItem", "position": 2, "url": "...", "name": "..." }
  ]
}

CollectionPage(カテゴリページ)

当該ページが複数のコンテンツをまとめたコレクションページであることを検索エンジンに伝えます。カテゴリ名、説明、URLを含め、検索エンジンがこのページを個別ポストではなくコンテンツ集約ページとして正しく認識するようにします。

{
  "@context": "https://schema.org",
  "@type": "CollectionPage",
  "name": "カテゴリ名",
  "description": "カテゴリ説明",
  "url": "https://deku.posstree.com/astro/"
}

サイトマップ

@astrojs/sitemapプラグインで多言語サイトマップを自動生成します。このブログでは各ポストのfrontmatterにあるdateフィールドをlastmodとして使用するために、ビルド時にコンテンツファイルを読み込んでURL別の日付マップを生成する方式を使用しています。

// astro.config.mjs
import sitemap from '@astrojs/sitemap';
import fs from 'node:fs';

/** コンテンツfrontmatterからURLパス → lastmod日付マップを生成 */
function buildLastmodMap() {
  const map = new Map();
  const files = fs.globSync('src/content/**/*.md');
  for (const file of files) {
    const content = fs.readFileSync(file, 'utf-8');
    const match = content.match(/^---\s*\n([\s\S]*?)\n---/);
    if (!match) continue;
    const fm = match[1];
    const dateMatch = fm.match(/^date:\s*['"]?(\d{4}-\d{2}-\d{2})['"]?/m);
    const permalinkMatch = fm.match(/^permalink:\s*['"]?([^\s'"]+)['"]?/m);
    const langMatch = fm.match(/^lang:\s*['"]?(\w+)['"]?/m);
    if (!dateMatch || !permalinkMatch || !langMatch) continue;
    const date = new Date(dateMatch[1]);
    const permalink = permalinkMatch[1];
    const lang = langMatch[1];
    const prefix = lang === 'ja' ? '' : `/${lang}`;
    const path = `${prefix}${permalink}`;
    map.set(path, date);
  }
  return map;
}

const lastmodMap = buildLastmodMap();

export default defineConfig({
  integrations: [
    sitemap({
      serialize(item) {
        const url = new URL(item.url);
        const lastmod = lastmodMap.get(url.pathname);
        if (lastmod) {
          item.lastmod = lastmod;
        }
        return item;
      },
      i18n: {
        defaultLocale: 'ja',
        locales: {
          ja: 'ja',
          ko: 'ko',
          en: 'en',
        },
      },
    }),
  ],
});
  • buildLastmodMap():すべてのマークダウンファイルのfrontmatterからdatepermalinklangを抽出してURLパス別の日付マップを生成します。これによりすべてのURLのlastmodが現在の日付ではなく、該当ポストの実際の作成日に設定されます。
  • serialize:サイトマップの各URLに対してlastmodMapから該当する日付を探してlastmodとして設定します。
  • i18n:多言語設定でhreflangタグが含まれたサイトマップを生成します。検索エンジンが各ページの言語別バージョンを認識できます。

ビルド時にdist/sitemap-index.xmldist/sitemap-0.xmlなどのファイルが自動的に生成されます。

RSSフィード

@astrojs/rssパッケージで言語別RSSフィードを生成します。

npm install @astrojs/rss

各言語別に別々のRSSフィードファイルを生成します。

  • 日本語:/feed.xml
  • 韓国語:/ko/feed.xml
  • 英語:/en/feed.xml

Head.astroで現在の言語に合ったRSSリンクを提供します。

<link
  rel="alternate"
  type="application/rss+xml"
  title="DeKu RSS Feed"
  href="{rssPath}"
/>
const rssPath = getLocalizedPath('/feed.xml', lang);

Google Analytics

Google Analytics 4(GA4)を連携して訪問者統計を収集します。

<script
  async
  src="https://www.googletagmanager.com/gtag/js?id=G-FR62E3SBVX"
></script>
<script is:inline>
  window.dataLayer = window.dataLayer || [];
  function gtag() {
    dataLayer.push(arguments);
  }
  gtag('js', new Date());
  gtag('config', 'G-FR62E3SBVX');
</script>

is:inlineディレクティブを使用するとAstroがこのスクリプトをバンドルせずそのままHTMLに含めます。外部分析スクリプトはバンドルする必要がないためis:inlineを使用します。

robots.txtと検索エンジン登録

robots.txt

public/robots.txtファイルで検索エンジンクローラーのアクセスを制御します。

User-agent: *
Allow: /
Sitemap: https://deku.posstree.com/sitemap-index.xml

すべてのクローラーにサイト全体へのアクセスを許可し、サイトマップの場所を知らせます。

検索エンジン登録

Google Search ConsoleとNaverウェブマスターツールにサイトを登録するための認証ファイルをpublic/ディレクトリに配置します。

public/
├── googleXXXXXXXXXXXXXXXX.html    # Google認証ファイル
└── naverXXXXXXXXXXXXXXXX.html     # Naver認証ファイル

またHead.astroにメタタグでも認証します。

<meta
  name="google-site-verification"
  content="rHTd8BRbOeSqAskkyVusn529XiwZ2eHbv1tnB1IDnjA"
/>

完了

今回の記事ではAstroブログのSEO実装を見てきました。

  • Head.astroコンポーネントですべてのSEOメタタグを一元管理
  • canonical URLとhreflangで多言語重複コンテンツ防止
  • Open GraphとTwitter Cardsでソーシャルメディア共有を最適化
  • JSON-LD構造化データでリッチスニペットサポート(Person、BlogPosting、WebSite、CollectionPage、ItemList)
  • @astrojs/sitemapで多言語サイトマップ自動生成
  • @astrojs/rssで言語別RSSフィード提供
  • Google Analytics 4(GA4)連携

JekyllでのSEO実装と比較すると、Astroではコンポーネント方式で型安全にSEOタグを管理できるためメンテナンスがはるかに楽です。以前のJekyll SEO実装についてはjekyll SEOポストを参考にしてください。

次の記事画像最適化 — カスタムrehypeプラグインでは、Sharpを利用したAVIF/WebP自動変換プラグインの実装を扱います。

シリーズ案内

このポストはJekyllからAstroへのマイグレーションシリーズの一部です。

  1. JekyllからAstroへマイグレーションした理由
  2. Astroのインストールとプロジェクト構成
  3. Content Collectionsとマークダウンマイグレーション
  4. 多言語(i18n)実装
  5. SEO実装
  6. 画像最適化 — カスタムrehypeプラグイン
  7. コメントシステム(Utterances)
  8. 広告連携(Google AdSense)
  9. Pagefindを利用した検索実装
  10. レイアウトとコンポーネントアーキテクチャ
  11. GitHub Pagesデプロイ
  12. ソーシャル共有自動化スクリプト
  13. トラブルシューティングとヒント

私のブログが役に立ちましたか?下にコメントを残してください。それは私にとって大きな大きな力になります!

アプリ広報

今見てるブログを作成たDekuが開発したアプリを使ってみてください。
Dekuが開発したアプリはFlutterで開発されています。

興味がある方はアプリをダウンロードしてアプリを使ってくれると本当に助かります。



SHARE
Twitter Facebook RSS