AstroブログにJSON-LDと検索計測を後付けした
Astro + microCMS + Cloudflare Pagesで作った個人ブログ(息子の闘病記録)にテクニカルSEOの基礎をひと通り入れたので、その手順と内容を残しておく。
公開URL: https://west-blog.pages.dev/
状況
スタート時点の状態
- Lighthouse Mobile: Performance / Accessibility / Best Practices / SEOすべて100
@astrojs/sitemapでsitemap-index.xml自動生成済みpublic/robots.txtにSitemap指定ありSEO.astroでtitle / description / canonical / OGP / Twitter Cardはすべて出力- 記事詳細では本文140字をdescriptionに自動抽出、og:imageは記事のアイキャッチを動的に使う
- RSSフィード(
/rss.xml) +@astrojs/rss実装済み
今回の実装
- 計測基盤が無い (Google Search ConsoleもAnalyticsも未導入で、表示回数0なのかインデックス未済なのか判別不能)
- JSON-LD構造化データが未実装 (Googleが記事として認識するための補助情報が無い)
一旦おいておく問題...
- ドメインが
*.pages.dev(Cloudflare共有ドメイン。検索評価上のハンデになりやすい)→個人記録としての気軽さを優先する。
採用方針
その前提で、計測基盤と構造化データだけは入れる方針にした。
| 項目 | 方針 |
|---|---|
| アクセス解析 | Cloudflare Web Analytics (Cookie不要・プライバシー配慮) |
| カスタムドメイン | 取得しない (west-blog.pages.devのまま) |
| og画像動的生成 | 今回スコープ外 (アイキャッチが無い記事は固定のog-default.pngのまま) |
| JSON-LD | 全ページにWebSite、記事詳細にBlogPosting + BreadcrumbListを追加 |
| 検索順位計測 | Google Search Console + Bing Webmaster Tools |
JSON-LD構造化データの追加
SEO.astroに3種類のスキーマを出力する処理を追加した。
- 全ページ:
WebSite ogType === 'article'の記事詳細のみ:BlogPosting+BreadcrumbList
publisherは記事用とWebSite用で再利用する。authorもOrganization扱いで「ちいさな日々」を入れている(個人名は出さない方針)。
---
import { SITE } from '../consts';
interface Props {
title?: string;
description?: string;
ogImage?: string;
ogType?: 'website' | 'article';
publishedAt?: string;
updatedAt?: string;
}
const {
title,
description = SITE.description,
ogImage = SITE.defaultOgImage,
ogType = 'website',
publishedAt,
updatedAt,
} = Astro.props;
const fullTitle = title ? `${title} | ${SITE.title}` : SITE.title;
const canonical = new URL(Astro.url.pathname, SITE.url).toString();
const ogImageUrl = ogImage?.startsWith('http')
? ogImage
: new URL(ogImage, SITE.url).toString();
const publisher = {
'@type': 'Organization' as const,
name: SITE.title,
url: SITE.url,
logo: {
'@type': 'ImageObject' as const,
url: new URL(SITE.defaultOgImage, SITE.url).toString(),
},
};
const websiteSchema = {
'@context': 'https://schema.org',
'@type': 'WebSite',
name: SITE.title,
alternateName: SITE.titleEn,
url: SITE.url,
description: SITE.description,
inLanguage: 'ja',
publisher,
};
const blogPostingSchema =
ogType === 'article' && title && publishedAt
? {
'@context': 'https://schema.org',
'@type': 'BlogPosting',
headline: title,
description,
image: ogImageUrl,
datePublished: publishedAt,
dateModified: updatedAt ?? publishedAt,
author: {
'@type': 'Organization',
name: SITE.authorName,
url: SITE.url,
},
publisher,
mainEntityOfPage: {
'@type': 'WebPage',
'@id': canonical,
},
inLanguage: 'ja',
}
: null;
const breadcrumbSchema =
ogType === 'article' && title
? {
'@context': 'https://schema.org',
'@type': 'BreadcrumbList',
itemListElement: [
{
'@type': 'ListItem',
position: 1,
name: SITE.title,
item: SITE.url,
},
{
'@type': 'ListItem',
position: 2,
name: title,
item: canonical,
},
],
}
: null;
---
<title>{fullTitle}</title>
<meta name="description" content={description} />
<link rel="canonical" href={canonical} />
<!-- 既存のOGP/Twitter Card出力は省略 -->
<script type="application/ld+json" is:inline set:html={JSON.stringify(websiteSchema)} />
{blogPostingSchema && (
<script type="application/ld+json" is:inline set:html={JSON.stringify(blogPostingSchema)} />
)}
{breadcrumbSchema && (
<script type="application/ld+json" is:inline set:html={JSON.stringify(breadcrumbSchema)} />
)}is:inlineの指定
<script type="application/ld+json" set:html={...} />をis:inlineなしで書くと、Astroのcheckが次のヒントを出した。
Add the is:inline directive explicitly to silence this hint.Astroの<script>はデフォルトでバンドル対象になるが、JSON-LDのスクリプトはバンドルされない生のテキストとしてHTMLに残ってほしい。is:inlineを付けるとAstroが処理せずそのまま出力する。
Search Console認証スロット
GoogleとBingの所有権確認をHTMLメタタグ方式で行う。トークンはsrc/consts.tsに置き、BaseLayout.astroで条件付き出力する。空文字なら出力されない仕様にしておく(ローカル運用時にトークンを気にしなくていい)。
// src/consts.ts (抜粋)
export const SITE = {
title: 'ちいさな日々',
// ...
googleSiteVerification: '', // GSCで取得したcontent値を貼る
bingSiteVerification: '', // Bing Webmaster Toolsで取得したcontent値を貼る
cloudflareAnalyticsToken: '', // Cloudflare Web Analyticsのtoken
authorName: 'ちいさな日々',
} as const;<!-- src/layouts/BaseLayout.astro (抜粋) -->
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
{SITE.googleSiteVerification && (
<meta name="google-site-verification" content={SITE.googleSiteVerification} />
)}
{SITE.bingSiteVerification && (
<meta name="msvalidate.01" content={SITE.bingSiteVerification} />
)}Google Search Consoleの登録手順
- https://search.google.com/search-console/ でプロパティを追加(URLプレフィックス型)
- 「HTMLタグで確認」を選び、
<meta name="google-site-verification" content="..." />のcontent値だけコピー consts.tsのgoogleSiteVerificationに貼り付け- コミットしてpush → Cloudflare Pagesが自動デプロイ
- GSCに戻って「確認」ボタン
- サイトマップ → 新しいサイトマップの追加 →
sitemap-index.xmlを送信
Bing Webmaster Toolsの登録手順
Bingには「Google Search Consoleからインポート」というショートカットがある。ただし今回これを試したら「we didn't find any sites from GSC」と表示されて失敗した。原因はGSC側のプロパティ同期が間に合っていない(またはBing側のログインアカウントとGSCのアカウントが食い違っている)。
手動追加に切り替えた。
- https://www.bing.com/webmasters/ で「Add a site manually」を選ぶ
- URLに
https://west-blog.pages.dev/を入力 - 所有権確認の方法で「HTML Meta Tag」を選ぶ
- 表示される
<meta name="msvalidate.01" content="..." />のcontent値をコピー consts.tsのbingSiteVerificationに貼り付け- コミットしてpush
- Bingに戻って「Verify」 → サイトマップ送信
Cloudflare Web Analytics
Cookieを使わないアクセス解析。手動セットアップフローでした。
Web Analyticsを開始する
① ホスト名を設定 → west-blog.pages.dev を入力
② インストール → ビーコンスニペットを表示
③ 分析を取得②で出てきたスニペットは次の形だった。
<!-- Cloudflare Web Analytics -->
<script defer src='https://static.cloudflareinsights.com/beacon.min.js'
data-cf-beacon='{"token": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"}'></script>
<!-- End Cloudflare Web Analytics -->これもconsts.tsのcloudflareAnalyticsTokenに値だけ置いて、BaseLayout.astroで条件付き出力にした。
{SITE.cloudflareAnalyticsToken && (
<script
defer
src="https://static.cloudflareinsights.com/beacon.min.js"
data-cf-beacon={`{"token": "${SITE.cloudflareAnalyticsToken}"}`}
is:inline
></script>
)}ここでも<script>にis:inlineを付ける。これがないとAstroがビルド時にバンドル対象としてしまい、外部スクリプトをそのままdefer読み込みする挙動から外れてしまう。
ビルド検証
ローカルで以下を確認した。
npx astro check
# Result (25 files):
# - 0 errors
# - 0 warnings
# - 0 hints
npm run buildビルド後のdist/index.html(トップページ)にはWebSiteスキーマだけが入る。
<script type="application/ld+json">
{"@context":"https://schema.org","@type":"WebSite","name":"ちいさな日々",
"alternateName":"CHIISANA HIBI","url":"https://west-blog.pages.dev",
"description":"...","inLanguage":"ja",
"publisher":{"@type":"Organization","name":"ちいさな日々",
"url":"https://west-blog.pages.dev",
"logo":{"@type":"ImageObject","url":"https://west-blog.pages.dev/og-default.png"}}}
</script>記事詳細(dist/articles/<slug>/index.html)にはWebSite + BlogPosting + BreadcrumbListの3つが入る。それぞれ独立した<script type="application/ld+json">タグとして並ぶ。
consts.tsの各トークンを空文字にした状態でビルドしても、Search Console認証メタタグもCloudflare beaconも一切出力されないことを併せて確認した。
JSON-LDの内容はGoogle公式のリッチリザルトテストに貼ってBlogPostingとして認識されるかをチェックできる。
今後の運用
検索順位は数日では変わらないのでデータが溜まる順に観測していく。えいえいおー! 独自ドメインにしないのがどれほど影響してしまうのか... 個人的なブログなので試してみよう。
- 1週間後: GSCの「ページ」タブでインデックス済ページ数を確認。0のままなら、主要記事をURL検査ツールから手動で「インデックス登録をリクエスト」(1日10件程度の制限)
- 2週間後: GSC「検索パフォーマンス」で表示回数・クエリ・平均掲載順位を確認
- 1ヶ月後: Cloudflare Web Analyticsで実アクセス傾向(国・デバイス・リファラ)を確認
- 中長期: GSCの「表示はされているがクリックされていないクエリ」を起点に、記事のタイトル・descriptionを見直す
参考リンク:
- Google Search Console
- Bing Webmaster Tools
- Cloudflare Web Analytics
- Schema.org BlogPosting
- Schema.org BreadcrumbList
- Google検索セントラル: 構造化データの仕組み
- Google検索セントラル: Article (Article, NewsArticle, BlogPosting) structured data
- リッチリザルトテスト
- Astro: Template directives reference (
is:inline) - Astro: Add client-side scripts (default bundling behaviour)
- @astrojs/sitemap (デフォルトで
sitemap-index.xmlとsitemap-0.xmlを生成) - Cloudflare Web Analytics: Get started (beaconスニペットの正式フォーマット)
- Cloudflare Pages: Enable Web Analytics (Pagesは自動連携可)
- Google Search Console ヘルプ: サイトの所有権を確認する (HTMLタグ)
- Bing Webmaster Tools: Add and verify site (Meta tag authentication)