0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Claude APIでバイクニュースを全自動生成 → X投稿まで自動化した話🤖

0
Posted at

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の新型発表で旧型中古相場はどう動く?|データで予測

image.png

image.png

記事の中身は、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

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?