目次
概要
前回の記事多言語(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 | 現在の言語(ja、ko、en) |
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_JP、ko_KR、en_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からdate、permalink、langを抽出してURLパス別の日付マップを生成します。これによりすべてのURLのlastmodが現在の日付ではなく、該当ポストの実際の作成日に設定されます。serialize:サイトマップの各URLに対してlastmodMapから該当する日付を探してlastmodとして設定します。i18n:多言語設定でhreflangタグが含まれたサイトマップを生成します。検索エンジンが各ページの言語別バージョンを認識できます。
ビルド時にdist/sitemap-index.xmlとdist/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メタタグを一元管理canonicalURLと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へのマイグレーションシリーズの一部です。
- JekyllからAstroへマイグレーションした理由
- Astroのインストールとプロジェクト構成
- Content Collectionsとマークダウンマイグレーション
- 多言語(i18n)実装
- SEO実装
- 画像最適化 — カスタムrehypeプラグイン
- コメントシステム(Utterances)
- 広告連携(Google AdSense)
- Pagefindを利用した検索実装
- レイアウトとコンポーネントアーキテクチャ
- GitHub Pagesデプロイ
- ソーシャル共有自動化スクリプト
- トラブルシューティングとヒント
私のブログが役に立ちましたか?下にコメントを残してください。それは私にとって大きな大きな力になります!
アプリ広報
Dekuが開発したアプリを使ってみてください。Dekuが開発したアプリはFlutterで開発されています。興味がある方はアプリをダウンロードしてアプリを使ってくれると本当に助かります。