はじめに
個人開発のバイクポータルサイト MotoHub では、X(Twitter)BOTで中古バイクのお買い得情報を自動投稿しています。テキストだけの投稿からグラフ付きダッシュボード画像に進化させた過程で、Browsershotの挫折、QuickChart APIの活用、Nginx設定ミスによるサイトダウンなど、様々な学びがありました。
BOT投稿の全体像
MotoHubでは1日6回、4種類のBOT投稿を行っています。
08:00 お買い得車両(ダッシュボードグラフ付き)
10:00 新着入荷まとめ
12:00 お買い得車両(2回目)
14:00 新着レビュー紹介
20:00 お買い得車両(3回目)
日曜11:00 週間トレンド
Browsershotを断念した理由
最初はBrowseshotでHTMLをレンダリングしてスクリーンショットを撮る方針でした。しかしDocker環境(Alpine/Ubuntu)でChromeを動かすのは茨の道でした。
❌ libXfixes.so.3 が見つからない
❌ crashpad_handler が起動しない
❌ --no-sandbox でも権限エラー
❌ puppeteer用のchromiumも同様
Docker内でヘッドレスChromeを安定稼働させるには、かなりのシステムパッケージが必要で、VPSのリソースも圧迫します。
結論: 画像生成のためだけにChrome環境を整備するのは割に合わない。
QuickChart APIで解決
QuickChart.io はChart.jsの構文でグラフ画像をAPI生成できるサービスです。
$chartConfig = [
'type' => 'line',
'data' => [
'labels' => ['1月', '2月', '3月', '4月', '5月', '6月'],
'datasets' => [[
'label' => '相場推移',
'data' => [45, 42, 48, 44, 46, 43],
'backgroundColor' => 'rgba(59,130,246,0.3)',
'borderColor' => '#3b82f6',
'fill' => true,
]],
],
'options' => [
'plugins' => ['legend' => ['display' => false]],
],
];
$response = Http::post('https://quickchart.io/chart', [
'chart' => json_encode($chartConfig),
'width' => 600,
'height' => 400,
'devicePixelRatio' => 2,
'backgroundColor' => '#0f172a',
]);
$chartImage = $response->body(); // PNG画像
注意点: Chart.js v2.9.4互換
QuickChartはChart.js v2.9.4 がデフォルトです。v3/v4の構文とは微妙に異なるので注意。
// ❌ v3構文(QuickChartで動かない)
options: { plugins: { legend: { display: false } } }
// ✅ v2構文(QuickChartで動く)
options: { legend: { display: false } }
callbackの書き方
Y軸に「万円」を表示するなど、callbackを使いたい場合はJS文字列リテラルで渡します。
// ❌ PHP配列 → json_encode(callbackが文字列化されない)
'callback' => function($value) { return $value + '万円'; }
// ✅ JS文字列として直接記述
'ticks' => [
'callback' => 'function(value) { return value + "万円"; }',
],
4種類のダッシュボード画像
1. お買い得車両(TweetBargains)
QuickChartで4枚のグラフを生成し、Intervention Imageで1枚に合成します。
// 4枚のチャート画像を生成
$charts = [
$this->generatePriceTrend($model), // 左上: 相場推移(面グラフ)
$this->generatePriceDistribution($model), // 右上: 価格帯分布(横棒)
$this->generateRegionStock($model), // 左下: 地域別在庫TOP5(横棒)
$this->generateYearDistribution($model), // 右下: 年式分布(縦棒)
];
// 1200x630の背景に4枚を配置
$canvas = Image::canvas(1200, 630, '#0f172a');
$canvas->insert($charts[0], 'top-left', 20, 80);
$canvas->insert($charts[1], 'top-right', 20, 80);
$canvas->insert($charts[2], 'bottom-left', 20, 20);
$canvas->insert($charts[3], 'bottom-right', 20, 20);
2. 新着入荷(TweetNewStock)
メーカー別・価格帯別・車種別・入荷推移の4グラフ。新着データを毎朝集計して投稿します。
3. レビュー紹介(TweetReviews)
5項目評価のレーダーチャートと販売KPIを組み合わせた画像。
4. 週間トレンド(TweetTrending)
TOP5車種の販売台数を横棒グラフで表示。1位金色・2位銀色・3位銅色のメダルカラー。
OGPエンドポイント: /bikes/{listing}/ogp.png
BOT投稿だけでなく、車両ページ自体のOGP画像も動的生成しています。
// DealOgpController.php
public function show(Listing $listing)
{
$chartImage = $this->chartService->generatePriceChart($listing);
return response($chartImage)
->header('Content-Type', 'image/png')
->header('Cache-Control', 'public, max-age=86400');
}
Nginx設定の罠
OGP画像のURLパターン(/bikes/{id}/ogp.png)をNginxでfastcgiに通す設定を追加した際、うっかりdefault.prod.confをdefault.confで上書きしてサイトがダウンしました。
# 本番: default.prod.conf(SSL付き)
# ローカル: default.conf(SSL無し)
# ❌ ローカル用設定で本番を上書き → SSL消失 → Cloudflare 521エラー
教訓: 本番のNginx設定ファイルはdefault.prod.conf。絶対にdefault.confで上書きしない。docker-compose.ymlのNGINX_CONF環境変数で切り替える。
LaravelのDB::avg()がstringを返す問題
// avg()の戻り値はstring
$avgPrice = DB::table('listings')->avg('total_price');
// → "478000" (string型)
// Cache::remember経由でもstring
$cached = Cache::remember('avg', 3600, fn() => DB::table('listings')->avg('total_price'));
// → "478000" (string型のまま)
// 数値として使うなら明示的にキャスト
$avgPrice = (float) Cache::remember('avg', 3600, fn() => ...);
QuickChart APIの制限と対策
| 項目 | 制限 |
|---|---|
| 無料枠 | 月500リクエスト |
| 画像サイズ | 最大500KB |
| タイムアウト | 10秒 |
月500リクエストは、6投稿/日 × 30日 = 180リクエスト + OGP生成で十分収まります。有料プランは月$40〜ですが、個人開発なら無料枠で十分です。
まとめ
- Docker環境でのBrowseshotは茨の道。QuickChart APIが個人開発には最適解
- Chart.js v2互換であることを忘れずに
- OGP画像のNginx設定変更は本番設定ファイルの取り扱いに細心の注意
-
DB::avg()の戻り値はstring。キャスト忘れに注意 - Intervention Imageで複数チャートを合成すれば、リッチなダッシュボード画像が作れる






