blog-post-counter について
blog-post-counterは、ブログ記事数の比較、更新状況の監査、キーワード差分の分析を行うコンテンツ・SEO分析スキルです。サイトマップとAhrefsのデータを使い、競合比較とHTMLレポートを作成します。
主な機能
- 01対象と競合のブログ記事数を比較
- 02記事の日付と公開ペースを監査
- 03非ブランド情報系キーワードの差分を分析
- 04統合HTMLレポートを作成
活用シーン
- 01複数社のブログ記事数を比較
- 02更新停滞や一括更新日時を特定
- 03競合のみが獲得するキーワードを発見
MCP 対応
ワークフローではAhrefs MCPを使ってキーワード、トラフィック、ページのデータを取得し、統合HTMLレポートを作成します。
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ infrasity-labs/ dev-gtm-claude-skills --skill blog-post-counterインストール後、Claude Code で/blog-post-counterを入力して読み込まれたか確認します。
同梱ファイルREADME.md
詳細
ブログ記事数カウント+キーワードマッピングスキル
1 つのスキルで 2 つのワークフローを実行します。
- ブログ記事数カウント — 対象企業と競合他社のブログ記事数をカウントして比較し、必要に応じてコンテンツの鮮度を監査します
- キーワードマッピング — 競合他社のオーガニックキーワード(米国、非ブランド、情報収集目的)を取得し、対象企業のキーワードフットプリントと照合して、ギャップと機会を見つけます
ワークフロー A — ブログ記事数カウント
ステップ 1 — 各企業の URL を特定する
指定された各企業名について、次の手順を実行します。
- URL が直接指定されている場合は、それを使用します。
- 名前のみが指定されている場合は、
curl -sL "https://<likely-domain>/robots.txt"を実行して、サイトの存在を確認し、サイトマップを見つけます。404 が返された場合は、"<company name>" official websiteを使ってウェブを簡単に検索し、正しいドメインを確認します。
ステップ 2 — サイトマップを見つける
robots.txt から Sitemap: の行を抽出します。よくあるパターンは次のとおりです。
- 単一のサイトマップ:
Sitemap: https://example.com/sitemap.xml - サイトマップインデックス:複数の
Sitemap:行、またはサブサイトマップを参照する sitemap-index.xml
robots.txt にサイトマップがない場合は、次の代替手段を順番に試します。
/sitemap.xml
/sitemap_index.xml
/sitemap-index.xml
/blog/sitemap.xml
ステップ 3 — ブログ記事数をカウントする
サイトマップを取得し、ブログ記事の URL をカウントします。次の bash パターンを使用します。
curl -sL -A "Mozilla/5.0" "https://example.com/sitemap.xml" \
| grep -o '<loc>[^<]*</loc>' \
| grep -iE '/blog/|/posts?/|/articles?/|/news/' \
| grep -v -E '^<loc>https://[^/]+/blog/?</loc>$' \
| wc -l
重要: ブログのインデックスページ自体(例:/blog や /blog/)は除外し、個別の記事 URL のみをカウントしてください。
サイトマップインデックスの処理: サイトマップがインデックス(<url> タグではなく <sitemap> タグを含む)の場合は、サブサイトマップ URL を抽出し、ブログ専用のものを取得します。
curl -sL -A "Mozilla/5.0" "https://example.com/sitemap-index.xml" \
| grep -o '<loc>[^<]*</loc>' \
| grep -i 'blog'
# Then fetch that sub-sitemap and count
JavaScript でレンダリングされるサイト: 一部のブログページは JavaScript 経由でレンダリングされ、サイトマップから件数を確認できない場合があります。その場合は、検索演算子 site:example.com/blog を使って推定します。
例外的なケース:
/blog/の grep で 0 件になる場合、一部のサイトでは/resources/、/insights/、/learn/、/hub/を/blog/の代わりに使用していることがあります。サイトマップの構造を確認してください。- サイトマップが非常に大きい場合(>1MB)、複数のブログパスパターンを grep してください。
ステップ 4 — 出力を作成する
すべての件数を集めたら、対象企業を強調表示した順位付きの表を作成します。
Blog Post Count — [Target] vs Competitors
==========================================
Rank Company Posts URL
──── ─────────────── ────── ──────────────────────
1 Hackmamba 137 hackmamba.io ← COMPETITOR
2 Infrasity 137 infrasity.com ← COMPETITOR
3 ▶ Kubiya 95 kubiya.ai ← TARGET
4 Orgn 7 orgn.com ← COMPETITOR
▶ = your company Total companies analysed: 4
次に、簡潔な要約を追加します。
- 対象企業の順位(例:「4 社中 3 位」)
- 首位との差(例:「最上位の競合他社より記事が 42 件少ない」)
- 1 つ上の順位との差(すでに 1 位でない場合)
- 特筆すべき点(例:サイトが非常に新しい、または 1 位タイ)
コンテンツの鮮度監査(ブログ記事のみ)
ユーザーがコンテンツの鮮度、最近の更新状況、公開頻度について質問した場合、またはブログ記事数の比較に役立つ情報を加えられる場合は、この監査を実施します。
ステップ 5 — ブログ記事の lastmod 日付を抽出する
各ドメインについて、サイトマップからブログ記事の URL とその lastmod 日付のみを抽出します。
curl -sL "https://example.com/sitemap.xml" \
| python3 -c "
import sys, re
content = sys.stdin.read()
urls = re.findall(r'<url>(.*?)</url>', content, re.DOTALL)
for u in urls:
loc = re.search(r'<loc>(.*?)</loc>', u)
lastmod = re.search(r'<lastmod>(.*?)</lastmod>', u)
if loc and lastmod:
l = loc.group(1)
if re.search(r'/blog/|/posts?/|/articles?/|/news/', l) and not re.match(r'https?://[^/]+/(blog|posts?|articles?|news)/?$', l):
print(lastmod.group(1), l)
"
ステップ 6 — ビルド時のタイムスタンプによる水増しを検出する
lastmod の日付を実際の指標として扱う前に、投稿ごとの実際の編集日なのか、ビルド時に一括で付与されたタイムスタンプなのかを確認します。
from datetime import datetime, timezone
dates = [...] # all lastmod values for blog posts
unique_dates = set(d[:10] for d in dates) # compare date part only
if not dates:
stamp_type = "UNKNOWN"
elif len(unique_dates) <= 3:
# Likely build-time stamps — flag as unreliable
stamp_type = "BUILD-TIME"
else:
stamp_type = "REAL"
ビルド時のタイムスタンプ = ブログ記事のすべて(またはほぼすべて)に同じ日付が付いており、通常は数秒から数分の狭い時間範囲に収まります。これは、デプロイのたびにサイトマップを再生成する Next.js/Vercel などの SSG フレームワークでよく見られます。これらの日付は、コンテンツが実際に執筆または更新された時期を示すものではありません。
実際の日付 = 各記事の lastmod が異なり、週や月をまたいでばらついています。これらは信頼できる鮮度の指標です。
ビルド時のタイムスタンプが見つかった場合は、SEO やコンテンツ分析において日付に意味がないことをユーザーが理解できるよう、出力で必ず明確に指摘してください。
ステップ 7 — 鮮度バケットを計算する
今日の日付を基準に、各ブログ記事の lastmod を次のように分類します。
| バケット | 経過期間 |
|---|---|
| 過去 30 日 | 30 日未満 |
| 30~90 日 | 30~89 日 |
| 91~180 日 | 90~179 日 |
| 181~365 日 | 180~364 日 |
| 365 日超 | 365 日以上 |
from datetime import datetime, timezone
today = datetime.now(timezone.utc)
buckets = {"<30d": 0, "30-90d": 0, "91-180d": 0, "181-365d": 0, ">365d": 0}
for date_str in lastmod_dates:
dt = datetime.fromisoformat(date_str.replace('Z', '+00:00'))
if dt.tzinfo is None:
dt = dt.replace(tzinfo=timezone.utc)
days = (today - dt).days
if days < 30: buckets["<30d"] += 1
elif days < 90: buckets["30-90d"] += 1
elif days < 180: buckets["91-180d"] += 1
elif days < 365: buckets["181-365d"] += 1
else: buckets[">365d"] += 1
次の項目も計算します。
- 最も新しい記事の日付と経過日数
- 最も古い記事の日付
- 月ごとの公開ペース — 記事を
YYYY-MMごとにグループ化し、期間ごとの件数を表示 - 最終公開からの経過日数 — 鮮度を測る重要な指標
ステップ 8 — 鮮度の出力
ブログ件数の表と並べて(またはその下に)ドメインごとの鮮度概要を出力します。複数のドメインを比較する場合は、ビジュアライザーを使って横並びの比較ウィジェットを表示します。ウィジェットには、次の項目を含める必要があります。
- スタンプの種類を示すバッジ — 「ビルド時のスタンプ ⚠️」または「実際の日付 ✓」
- 主要な統計情報 — 記事の合計数、最も新しい記事の日付、最新記事からの経過日数、日付の重複を除いた件数
- 鮮度の棒グラフ — 各バケットにつき 1 本の棒で、件数と割合を表示
- 月ごとの公開状況 — 月ごとの小さな棒グラフ(ブログ記事のみ、過去 12 か月分)
- 警告バナー:
- ビルド時のスタンプ → 日付は実際の編集日ではないことを説明する警告バナー
- 90 日以上記事がない → コンテンツの空白期間を示す警告バナー
- 実際の日付で最近の記事がある → 信頼できる指標であることを示す情報バナー
概要に含める SEO の鮮度に関する解釈:
- ビルド時のスタンプ:Google は実際より新しく見せかけた
lastmodの日付を無視するため、鮮度の指標にはならないことを記載します - 実際の日付で、記事が 30 日未満:強い鮮度のシグナルであり、SEO に有効です
- 実際の日付で、90 日以上記事がない:コンテンツの空白期間があり、定期的に公開する競合に鮮度のランキングシグナルで遅れをとっている可能性があります
- 実際の日付で、一時的な大量公開の後に公開数が減少:そのパターンを指摘します(継続的な公開ペースではなく、移行や短期集中の可能性があります)
注記(ワークフロー A)
- 常にサイトマップから件数を集計します。最も正確で網羅性の高い情報源です。
- サイトマップ URL を探す際は、必ず最初に
robots.txtを確認します。 /blogのインデックスページは記事として数えません。- 企業にブログセクションがない場合は、件数を 0 と報告して、その旨を記載します。
- サイトマップのデータはリアルタイムより数日遅れることがあります。該当する場合はその旨を記載します。
- レート制限に達しないよう、企業ごとに 1 つの bash ブロックを使い、順番に検索を実行します。
- 鮮度の調査では、必ずブログ記事の URL のみを対象にします。静的ページ、サービスページ、用語集などは除外します。
- ビルド時のスタンプを実際の鮮度データとして扱ってはいけません。必ず検出して明示します。
ワークフロー B — キーワードマッピング
競合ドメインのオーガニック検索キーワードをターゲットドメインと照らし合わせ、コンテンツの空白やランキングの機会を明らかにします。
ステップ 1 — ドメイン指標を取得する
Ahrefs の site-explorer-metrics を使い、競合とターゲットの両方について org_keywords と org_traffic を取得します。
tool: site-explorer-metrics
params:
target: <domain>
mode: subdomains
date: <latest>
select: org_keywords, org_traffic
両方のドメインについて実行します。org_keywords の件数から、キーワードセット全体を取得するのに必要なバッチ数がわかります。
ステップ 2 — 競合のキーワードを取得する
Ahrefs の site-explorer-organic-keywords を使い、次の固定フィルターで競合のキーワードセット全体を取得します。
tool: site-explorer-organic-keywords
params:
target: <competitor domain>
country: us
mode: subdomains
is_branded: false
is_informational: true
limit: 500
select: keyword, volume, best_position, sum_traffic, best_position_url, is_branded
異なる order_by 値を使って複数のバッチを取得し、網羅性を高めます。
- バッチ 1:
order_by: sum_traffic:desc - バッチ 2:
order_by: volume:desc - バッチ 3:
order_by: best_position:asc
各バッチの取得後、それまでに取得したすべてのバッチ間でキーワードを重複排除します。各キーワードについて、best_position が最も低い(最良の)エントリを残します。
連続する 2 つのバッチで新しいキーワードが見つからなくなるか、バッチ数がステップ 1 で見積もったキーワード総数を超えるまで、バッチ取得を続けます。
ステップ 3 — ターゲットのキーワードを取得する
ターゲットドメインについて、まったく同じ方法で取得します。同じフィルター、複数の並べ替え順序、同じ重複排除ロジックを使います。
tool: site-explorer-organic-keywords
params:
target: <target domain>
country: us
mode: subdomains
is_branded: false
is_informational: true
limit: 500
select: keyword, volume, best_position, sum_traffic, best_position_url, is_branded
結果を検索用マップとして保存します:{ keyword → { best_position, sum_traffic, best_position_url } }
ステップ 4 — 相互参照
ステップ 2 の競合キーワードごとに、ステップ 3 のターゲットマップを検索します。ステータスを割り当てます。
両方で順位が付いている場合:
WINNING— ターゲットの順位 < 競合の順位(ターゲットが上位)IMPROVE— 競合の順位 < ターゲットの順位(競合が上位)
ターゲットの順位が付いていない場合:
CRITICAL— ボリューム ≥ 1,000HIGH— ボリューム 500–999MEDIUM— ボリューム 200–499LOW— ボリューム 100–199NEW— ボリューム < 100
出力の各行は、競合キーワード 1 つを表します。行ごとのフィールド:
keywordvolumecompetitor_rank(best_position)competitor_traffic(sum_traffic)target_rank(best_position、または null)target_traffic(sum_traffic、または null)target_url(best_position_url、または null)status
注記(ワークフロー B)
- 行に含まれるのは競合キーワードのみです。このレポートは、「ターゲットに存在感がない分野で、競合がどのようにオーガニック流入を獲得しているか」という問いに答えます。
is_branded=falseとis_informational=trueのフィルターは、データ取得時に適用します。後処理ではありません。これにより、最初からデータセットをクリーンな状態に保てます。- 競合とターゲットのキーワード取得には、同一のフィルターパラメーターを使用するため、対称的に比較できます。
- ターゲットのキーワードがマップに存在する一方で、競合キーワードの検索結果が null の場合、その競合キーワードは取得されていません。Ahrefs が追跡する上位 100 位の範囲外である可能性があります。
- 統計カードの件数には、フィルター適用後(非ブランド、情報提供目的のみ)のデータセットのみを反映します。
統合 HTML レポート
両方のワークフローの結果を、1 つの HTML レポートファイルにまとめます。まずワークフロー A とワークフロー B を最後まで実行してすべてのデータを収集し、その後、1 回の処理でレポートを生成します。
レポートの構成(上から下)
1. ヘッダー
- 暗い背景色(#0f1117)
- レポートタイトル:
[ターゲット] vs [競合] — コンテンツ&キーワードレポート - サブタイトル:ターゲットドメイン · 競合ドメイン · 日付 · 適用したフィルター
- 2 つのドメインピル:ターゲット(緑のドット)と競合(赤のドット)。それぞれの
org_keywordsとorg_trafficを表示
2. ブログ投稿数セクション
- セクション見出し:「ブログ投稿数」
- 順位付きテーブル:順位 · 会社 · 投稿数 · URL · ターゲット/競合ラベル
- ターゲットの行を強調表示
- サマリー行:ターゲットの順位、トップとの差、1 つ上の順位との差
3. コンテンツの鮮度セクション
- セクション見出し:「コンテンツの鮮度」
- ドメインごとにカードを 1 つ表示し、以下を含めます:
- スタンプタイプのバッジ(ビルド時 ⚠️ または実日付 ✓)
- 投稿総数
- 最新の投稿日時 + 経過日数
- 鮮度区分の内訳(区分ごとのバー)
- 月ごとの公開状況(過去 12 か月分、棒グラフ)
- 90 日以上投稿がない場合、またはビルド時のスタンプが検出された場合のアラートバナー
4. キーワードマッピングセクション
- セクション見出し:「キーワードギャップ — [競合] vs [ターゲット]」
- 統計カード:すべて · 勝ち · 改善 · 重大 · 高 · 中 · 低 · 新規(件数、クリック可能なフィルター)
- 競合キーワードごとに 1 行のテーブル:
- キーワード · ボリュームバー · 競合順位 · 競合トラフィック · ターゲット順位 · ターゲットトラフィック · ターゲット URL · ステータスバッジ
- デフォルトの並べ替え:競合トラフィックの降順
- 見出しをクリックすると、すべての列で並べ替え可能
5. フッター
- データソース:Ahrefs API(キーワードデータ) · サイトマップ(ブログ投稿数)
- データ取得日
- フィルター:米国 · 非ブランド · 情報提供目的
スタイル
- フォント: DM Sans(本文)+ DM Mono(数字、バッジ、等幅の値)
- ヘッダー: 背景色 #0f1117、テキスト色 #edeae3、サブタイトル色 #7a8190
- 本文の背景: #f7f6f2
- カード/テーブル: 背景色 #ffffff、枠線 #e4e2db、テーブルのヘッダー行 #faf9f6
- ステータスバッジの色:
- WINNING — 緑(#14724e 背景:#e5f5ee)
- IMPROVE — オレンジ(#994d00 背景:#fff0e0)
- CRITICAL — 赤(#c0392b 背景:#fce8e6)
- HIGH — アンバー(#b85c00 背景:#fdf0e0)
- MEDIUM — 黄色(#7c5c00 背景:#fdf7e0)
- LOW — ティール(#1a6e7a 背景:#e0f4f7)
- NEW — グレー(#555 背景:#efefef)
- 順位の色: 1~3位は緑 · 4~6位は中間色の緑 · 7~10位はオレンジ · 11位以降は赤
- ボリュームバー: 高さ 3px、データセット内の最大ボリュームを基準にスケーリング、控えめな塗り色(#ccc8be)
- すべてのセクションを、DM Mono の大文字のセクションラベル付きの視認できる区切り線で分割します
生成ルール
- HTML を作成する前に、ワークフロー A とワークフロー B からすべてのデータを収集します
- すべてのデータをインライン JavaScript 配列として埋め込みます。レンダリング時に外部からデータを取得しないでください
- 単一の自己完結型
.htmlファイルにします。外部依存は Google Fonts CDN のみです /mnt/user-data/outputs/report-[target]-vs-[competitor].htmlに保存します- 保存後、
present_filesで提示します