Claude APIでバイクニュースを全自動生成 → X投稿まで自動化した話🤖
はじめに
バイクポータルサイト「MotoHub」を個人開発しています。
第1弾ではサイトマップを3.4万URLに削った話、第2弾では残したページの品質を上げた話を書きました。
シリーズ最終回は攻めの施策。Claude APIを使って、毎日オリジナルニュースを自動生成し、X(Twitter)に自動投稿するまでの仕組みを構築した話です。
人間が何もしなくても、毎日独自コンテンツが増え続ける。
環境
さくらVPS 4GB
├── Docker Compose
│ ├── Nginx
│ ├── PHP-FPM(Laravel 12 / PHP 8.3)
│ ├── MySQL 8.0
│ ├── Redis
│ └── Meilisearch
├── Cloudflare(Free プラン)
└── 外部API
├── Anthropic Claude API(claude-sonnet-4-20250514)
└── Twitter API v2
なぜ「ニュース自動生成」なのか
Googleコアアプデで35%減った後、サイトマップを削り、ページの品質を上げました。でも、それだけでは「現状維持」です。
回復するには、Googleに「このサイトは成長している」と示す必要がある。
そのためには、定期的に新しい独自コンテンツが追加されていることが重要です。でも個人開発者が毎日記事を書くのは現実的じゃない。
そこで考えたのが、MotoHubが持っている13万台の中古バイクデータを使って、AIに市場分析ニュースを書かせること。データは毎日変わるので、毎日違うニュースが生まれます。
全体のアーキテクチャ
[cron] 毎日 9:00
│
▼
[Artisan Command] news:generate-new-model-impact --publish
│
├── 1. 新車発表ニュースをWeb検索で取得
├── 2. 該当車種のMotoHub実データを集計
├── 3. Claude APIに「新車→旧型中古への影響分析」を依頼
├── 4. 生成された記事をDBに保存(bike_news テーブル)
└── 5. published_at をセットして公開
│
▼
[->then() コールバック]
│
▼
[Artisan Command] news:tweet-latest-report
│
├── 1. bike_news から最新のオリジナル記事を取得
├── 2. ツイート文を生成(タイトル + 要約 + URL + ハッシュタグ10-13個)
└── 3. Twitter API v2 で投稿
人間の介入はゼロ。 cronが回っている限り、毎日ニュースが生まれてXに投稿されます。
4種類のニュースを自動生成
1本だけだと飽きるので、4種類のニュースを異なるスケジュールで生成しています。
| コマンド | 頻度 | 内容 |
|---|---|---|
news:generate-new-model-impact |
毎日 9:00 | 新車発表 → 旧型中古への影響分析 |
news:generate-weekly-report |
毎週月曜 8:30 | 週間相場速報 |
news:generate-market-report |
毎月1日 7:30 | 月間値下がり/高騰ランキング |
news:generate-monthly-report |
毎月1日 8:00 | 月次市場総合レポート |
Laravelのスケジュール設定
// routes/console.php
Schedule::command('news:generate-new-model-impact --publish')
->dailyAt('09:00')
->then(function () {
Artisan::call('news:tweet-latest-report');
});
Schedule::command('news:generate-weekly-report --publish')
->weeklyOn(1, '08:30')
->then(function () {
Artisan::call('news:tweet-latest-report');
});
Schedule::command('news:generate-market-report --publish')
->monthlyOn(1, '07:30')
->then(function () {
Artisan::call('news:tweet-latest-report');
});
Schedule::command('news:generate-monthly-report --publish')
->monthlyOn(1, '08:00')
->then(function () {
Artisan::call('news:tweet-latest-report');
});
->then() で記事生成の直後にツイートコマンドを実行しています。実はGWの最終日にこの ->then() が1つ抜けていたことに気づいて修正しました。 テストは大事。
Claude APIへのプロンプト設計
一番苦労したのがプロンプト設計です。AIに「それっぽい記事」を書かせるのは簡単ですが、データに基づいた正確な分析を書かせるのは難しい。
新車影響分析のプロンプト(要約)
$prompt = <<<EOT
以下のデータに基づいて、新型{$modelName}の発表が中古市場に与える影響を分析する記事を書いてください。
## 現在の中古市場データ(MotoHub実データ)
- 掲載台数: {$listingCount}台
- 平均価格: {$avgPrice}万円
- 3ヶ月前の平均価格: {$avgPrice3MonthsAgo}万円
- 価格変動率: {$priceChangeRate}%
- 最安値: {$minPrice}万円
- 最高値: {$maxPrice}万円
## 新車ニュース
{$newsContent}
## 記事の要件
- MotoHubの実データを必ず引用すること
- 推測ではなくデータに基づく分析をすること
- 800〜1200文字程度
- 見出し(h2, h3)を使った構造的な記事にすること
EOT;
Claude APIの呼び出し
// app/Services/ClaudeApiService.php
public function generateArticle(string $prompt): string
{
$response = Http::withHeaders([
'x-api-key' => config('services.anthropic.api_key'),
'anthropic-version' => '2023-06-01',
'content-type' => 'application/json',
])->post('https://api.anthropic.com/v1/messages', [
'model' => 'claude-sonnet-4-20250514',
'max_tokens' => 2000,
'messages' => [
['role' => 'user', 'content' => $prompt],
],
]);
return $response->json('content.0.text');
}
モデルは claude-sonnet-4-20250514 を使用。 Opusほど高くなく、Haikuより品質が高い。毎日生成するニュースにはちょうどいいバランスです。
誤検知対策
新車ニュースの自動検出で一番困ったのが誤検知です。「ホンダ」というキーワードで検索すると、車のニュースも拾ってしまう。
// 除外キーワード(16個)
private array $excludeKeywords = [
'四輪', '自動車', 'SUV', 'セダン', 'ミニバン',
'ハイブリッド車', '電気自動車', 'EV', 'PHEV',
'軽自動車', 'コンパクトカー', 'ピックアップトラック',
'F1', 'スーパーGT', 'WRC', 'ル・マン',
];
// 車種名が2文字以下はスキップ(「CB」だけでマッチしてしまう)
if (mb_strlen($modelName) <= 2) {
continue;
}
// 掲載3台未満はスキップ(データ不足で分析の信頼性が低い)
if ($listingCount < 3) {
continue;
}
この3つのフィルターで、誤検知がほぼゼロになりました。
RSSフィード — Google News対応
生成したニュースをGoogle Newsに拾ってもらうため、RSSフィードを3本構築しました。
// routes/web.php
Route::get('/feed/news', [FeedController::class, 'news']); // オリジナルニュース専用
Route::get('/feed/blog', [FeedController::class, 'blog']); // ブログ専用
Route::get('/feed/original', [FeedController::class, 'original']); // 統合フィード
ニュースフィードの実装
// app/Http/Controllers/FeedController.php
public function news()
{
$articles = BikeNews::where('source', 'MotoHub')
->whereNotNull('published_at')
->orderByDesc('published_at')
->limit(50)
->get();
return response()
->view('feeds.news', compact('articles'))
->header('Content-Type', 'application/rss+xml; charset=UTF-8');
}
RSSのXML
<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<title>MotoHub オリジナルニュース</title>
<link>https://motohub.jp/news</link>
<description>MotoHubのオリジナル記事 - バイク中古相場分析・市場レポート</description>
<language>ja</language>
<lastBuildDate>{{ $articles->first()->published_at->toRssString() }}</lastBuildDate>
<atom:link href="https://motohub.jp/feed/news" rel="self" type="application/rss+xml" />
@foreach($articles as $article)
<item>
<title>{{ $article->title }}</title>
<link>https://motohub.jp/news/{{ $article->id }}</link>
<description>{{ Str::limit(strip_tags($article->content), 200) }}</description>
<pubDate>{{ $article->published_at->toRssString() }}</pubDate>
<guid isPermaLink="true">https://motohub.jp/news/{{ $article->id }}</guid>
<author>MotoHub</author>
<category>バイク相場</category>
</item>
@endforeach
</channel>
</rss>
2025年3月以降、Google NewsはPublisher CenterのRSS設定を使わなくなりました。 Googleが自動的にニュースコンテンツを検出・評価する方式に変わっています。RSSフィードは直接的なGoogle News対策というよりも、一般的なRSSリーダーからの読者獲得と、サイトの「ニュースメディアとしての体裁」を整えるために構築しました。
X自動投稿 — ハッシュタグ戦略
生成したニュースをXに自動投稿します。
ハッシュタグが10個 vs 3個でインプレッション2.7倍
GWにX BOTの投稿データを分析して、面白い発見がありました。
| 投稿タイプ | ハッシュタグ数 | インプレッション |
|---|---|---|
| お買い得BOT | 10〜13個 | 169, 188 |
| 新着入荷 | 3個 | 69 |
| レビュー | 3個 | 11 |
ハッシュタグの数とインプレッションに明確な相関がある。 特に #バイク乗りと繋がりたい #バイク好きと繋がりたい #バイクのある生活 のようなコミュニティ系タグが効いています。
これを受けて、全投稿コマンドのハッシュタグを10〜13個に統一しました。
ハッシュタグの構成
// 共通タグ(6個)
$baseTags = [
'#バイク乗りと繋がりたい',
'#バイク好きと繋がりたい',
'#バイクのある生活',
'#中古バイク',
'#MotoHub',
'#ツーリング',
];
// 動的タグ(4〜7個)— 記事の内容から自動生成
$dynamicTags = [];
// 記事に登場するメーカー名
foreach ($mentionedManufacturers as $mfr) {
$dynamicTags[] = "#{$mfr}";
}
// 記事に登場する車種名
foreach ($mentionedModels as $model) {
$dynamicTags[] = "#{$model}";
}
// カテゴリ別タグ
$dynamicTags[] = '#バイク相場';
$allTags = array_merge($baseTags, array_slice($dynamicTags, 0, 7));
共通6個 + 動的タグで合計10〜13個。 多すぎるとスパム判定されるので、13個を上限にしています。
コスト
気になるコスト面。
| 項目 | 月額(概算) |
|---|---|
| Claude API(Sonnet 4) | 約$5〜10 |
| Twitter API(Basic) | $200 |
| 合計 | 約$210 |
正直、Twitter APIが高い。 Claude APIは毎日数記事生成しても$10以内に収まります。Sonnet 4のコスパが良すぎる。
Twitter API Basic($200/月)は個人開発にはきつい出費ですが、毎日の自動投稿で得られるインプレッション(月間数千〜数万)を考えると、広告費として見れば安いとも言える。
生成されるニュースの例
実際に自動生成された記事のタイトル:
gsx-8ttの新型発表で旧型中古相場はどう動く?|データで予測
x-advの新型発表で旧型中古相場はどう動く?|データで予測
レブル250の新型発表で旧型中古相場はどう動く?|データで予測
記事の中身は、MotoHubの実データ(掲載台数・平均価格・3ヶ月推移)に基づいた分析になっています。「AIが適当に書いた記事」ではなく、「AIがデータを分析して書いた記事」。この違いはSEO的にも重要です。
まとめ
| やったこと | 効果 |
|---|---|
| 4種のニュース自動生成(cron) | 毎日・毎週・毎月、自動で独自コンテンツが増える |
| Claude API(Sonnet 4) | 実データに基づく分析記事を自動生成 |
| X自動投稿 + ハッシュタグ統一 | インプレッション2.7倍(3個→10-13個) |
| RSSフィード3本 | ニュースメディアとしての体裁を整備 |
| 誤検知対策(3フィルター) | 除外キーワード16個 + 車種名2文字以下スキップ + 掲載3台未満スキップ |
シリーズ全体の振り返り
Googleコアアプデで35%減った後、GWでやった3つの施策:
| 記事 | 施策 | 一言 |
|---|---|---|
| 第1弾 | サイトマップ21万→3.4万 | 守り — 低品質ページを削る勇気 |
| 第2弾 | 特集LP + 駐車場UI改善 | 整備 — 残したページの質を上げる |
| 第3弾(この記事) | ニュース全自動生成 | 攻め — AIで独自コンテンツを量産 |
削って、磨いて、増やす。 この順番が大事でした。
低品質ページが大量にある状態で独自コンテンツを追加しても、サイト全体の評価が低いままでは効果が薄い。まず削って、残ったページを磨いて、その上で新しいコンテンツを追加する。
5/12以降のGSCデータが楽しみです。
前回の記事:コアアプデ後に「残したページ」の品質を上げた話
🏍 MotoHub: https://motohub.jp
X: https://x.com/motohub_jp
GitHub: https://github.com/ausssxi/MotoHub

