AstroでSEO・構造化データを自動化する 記事ごとの設定が不要になる実装
この記事の要点
Astroでは、メタタグ・OGP・Article JSON-LD・FAQPage構造化データ・sitemap・llms.txtを一度実装すれば、記事を追加するだけで自動適用される仕組みを作れる。実装コードと設計の考え方を解説する。
結論
AstroのSEO自動化は、1つのレイアウトコンポーネントに実装すればすべての記事に自動適用される。メタタグ・OGP・Article JSON-LD・FAQPage JSON-LDをまとめたHeadコンポーネントを作り、それを記事レイアウトから呼び出す設計にすることで、記事ごとのSEO設定が不要になる。
基本的な設計方針
Astroの記事レイアウト(src/layouts/ArticleLayout.astro)にSEO設定を集中させる。記事のfrontmatter(title・description・pubDate・faq等)からHeadタグを自動生成する設計だ。
frontmatter(記事ファイル)
↓
ArticleLayout(レイアウトコンポーネント)
↓
SeoHead(SEO用のコンポーネント)
├── メタタグ・canonical
├── OGP(Open Graph Protocol)
├── Article JSON-LD
└── FAQPage JSON-LD(faqがある場合)
sitemapの自動生成
まず最も簡単な施策から始める。公式プラグインを追加するだけでサイトマップが自動生成される。
npx astro add sitemap
astro.config.mjsを更新する:
import { defineConfig } from 'astro/config';
import sitemap from '@astrojs/sitemap';
export default defineConfig({
site: 'https://your-site.com', // 本番URLを指定
integrations: [sitemap()],
});
ビルド後に/sitemap-index.xmlと/sitemap-0.xmlが生成される。Google Search Consoleにこのsitemapを送信することで、インデックス促進ができる。
SEOHeadコンポーネントの実装
src/components/SeoHead.astroを作成する。
---
interface Props {
title: string;
description: string;
pubDate?: Date;
updatedDate?: Date;
slug?: string;
category?: string;
tags?: string[];
faq?: Array<{ q: string; a: string }>;
ogImage?: string;
}
const {
title,
description,
pubDate,
updatedDate,
slug,
category,
tags = [],
faq = [],
ogImage,
} = Astro.props;
const canonicalUrl = new URL(slug ? `/articles/${slug}/` : '/', Astro.site).toString();
const imageUrl = ogImage ?? new URL('/og-default.png', Astro.site).toString();
// Article構造化データ
const articleJsonLd = pubDate ? JSON.stringify({
"@context": "https://schema.org",
"@type": "Article",
"headline": title,
"description": description,
"datePublished": pubDate.toISOString(),
"dateModified": (updatedDate ?? pubDate).toISOString(),
"author": {
"@type": "Organization",
"name": "Meliorra編集部"
},
"publisher": {
"@type": "Organization",
"name": "Meliorra AI Media",
"url": Astro.site?.toString()
},
"mainEntityOfPage": canonicalUrl,
"keywords": tags.join(', '),
}) : null;
// FAQPage構造化データ
const faqJsonLd = faq.length > 0 ? JSON.stringify({
"@context": "https://schema.org",
"@type": "FAQPage",
"mainEntity": faq.map(item => ({
"@type": "Question",
"name": item.q,
"acceptedAnswer": {
"@type": "Answer",
"text": item.a
}
}))
}) : null;
---
<!-- 基本メタタグ -->
<meta name="description" content={description} />
<link rel="canonical" href={canonicalUrl} />
<!-- Open Graph -->
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:url" content={canonicalUrl} />
<meta property="og:type" content={pubDate ? "article" : "website"} />
<meta property="og:image" content={imageUrl} />
<meta property="og:site_name" content="Meliorra AI Media" />
<meta property="og:locale" content="ja_JP" />
<!-- Twitter Card -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={title} />
<meta name="twitter:description" content={description} />
<meta name="twitter:image" content={imageUrl} />
<!-- Article固有のOGP -->
{pubDate && <meta property="article:published_time" content={pubDate.toISOString()} />}
{updatedDate && <meta property="article:modified_time" content={updatedDate.toISOString()} />}
{category && <meta property="article:section" content={category} />}
{tags.map(tag => <meta property="article:tag" content={tag} />)}
<!-- 構造化データ -->
{articleJsonLd && <script type="application/ld+json" set:html={articleJsonLd} />}
{faqJsonLd && <script type="application/ld+json" set:html={faqJsonLd} />}
ArticleLayoutでの利用
src/layouts/ArticleLayout.astroでSeoHeadを呼び出す。
---
import SeoHead from '../components/SeoHead.astro';
import type { CollectionEntry } from 'astro:content';
interface Props {
post: CollectionEntry<'articles'>;
}
const { post } = Astro.props;
const { title, description, pubDate, updatedDate, tags, faq, category } = post.data;
---
<html lang="ja">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{title} | Meliorra AI Media</title>
<SeoHead
title={title}
description={description}
pubDate={pubDate}
updatedDate={updatedDate}
slug={post.slug}
category={category}
tags={tags}
faq={faq}
/>
</head>
<body>
<slot />
</body>
</html>
記事ページ(src/pages/articles/[slug].astro)でこのレイアウトを使うことで、全記事に自動でSEO設定が適用される。
OGP画像の自動生成(応用)
SNSでシェアされたときに表示されるOGP画像を、記事タイトルから動的生成できる。AstroのAPI Routesと@vercel/ogライブラリを組み合わせると実装できる。
GET /api/og.png?title=記事タイトル&category=security
→ タイトルが入ったOGP画像を動的生成して返す
毎回Canvaで画像を作る手間が省けるため、記事量産ワークフローとの相性がよい。
robots.txtでAIクローラーを許可する
public/robots.txtに主要なAIクローラーへのアクセスを明示的に許可する。
User-agent: GPTBot
Allow: /
User-agent: ClaudeBot
Allow: /
User-agent: PerplexityBot
Allow: /
User-agent: Applebot-Extended
Allow: /
User-agent: *
Allow: /
Sitemap: https://your-site.com/sitemap-index.xml
実装の確認方法
構造化データの検証:Google の Rich Results Test(search.google.com/test/rich-results)でページURLを入力すると、構造化データが正しく認識されているかを確認できる。
OGPの確認:Twitter Card Validator(developer.x.com/docs/twitter-for-websites/cards/guides/troubleshooting-cards)や、Facebook Sharing Debuggerでシェア時の表示を確認できる。
metaタグの確認:ブラウザのデベロッパーツールで<head>の内容を確認する。canonicalのURLが正しいか、descriptionが意図した内容かを記事ごとに抜き打ちでチェックする。
llms.txtの実装についてはllms.txtとは?AIに見つかるサイトにする最適化ガイドを、Astroの基本的なセットアップはAstroで爆速ブログ・メディアを作る入門で解説している。
まとめ
AstroのSEO自動化は、SeoHeadコンポーネントを1つ作って記事レイアウトから呼び出す設計で完結する。frontmatterのtitle・description・pubDate・faqを渡すだけで、メタタグ・OGP・Article/FAQPage JSON-LDが全記事に適用される。sitemapは公式プラグインで自動生成できる。一度実装してしまえば、記事を追加するごとにSEO設定が自動で完成する状態になる。
よくある質問
AstroでSEOの設定は難しいですか
基本的なメタタグとsitemapは公式プラグインで数行の設定で完了します。構造化データ(JSON-LD)はコンポーネントとして一度書けば全記事に自動適用できます。難しさより、一度設定すると何もしなくてよくなる恩恵の方が大きいです。
構造化データとは何ですか
Googleなどの検索エンジンやAIがページの内容を理解しやすくするための、機械可読な情報です。Article・BreadcrumbList・FAQPage・HowToなどのタイプがあり、正しく実装するとリッチリザルト(検索結果に星や価格が表示されるもの)や、AIからの引用率向上につながります。
FAQPage構造化データを実装すると何がいいですか
Googleの検索結果でQ&Aが展開表示されるリッチリザルトが表示される可能性があります。また、ChatGPT・Perplexity・Claudeなどの対話型AIが回答を生成するとき、明確なQ&A形式の情報は引用しやすいため、AI経由のトラフィックにも効きます。
sitemapは手動で更新が必要ですか
@astrojs/sitemapを使うと、ビルド時に全ページのURLからsitemapが自動生成されます。記事を追加するたびに自動更新されるため、手動操作は不要です。