1
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?

LaravelでX(Twitter) BOTのOGP画像を動的生成した話 — Browsershot断念からQuickChartへ

1
Posted at

はじめに

個人開発のバイクポータルサイト 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  週間トレンド

image.png

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枚に合成します。

image.png

// 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グラフ。新着データを毎朝集計して投稿します。

image.png

3. レビュー紹介(TweetReviews)

5項目評価のレーダーチャートと販売KPIを組み合わせた画像。

image.png

4. 週間トレンド(TweetTrending)

TOP5車種の販売台数を横棒グラフで表示。1位金色・2位銀色・3位銅色のメダルカラー。

image.png

OGPエンドポイント: /bikes/{listing}/ogp.png

BOT投稿だけでなく、車両ページ自体のOGP画像も動的生成しています。

image.png

// 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.confdefault.confで上書きしてサイトがダウンしました。

image.png

# 本番: default.prod.conf(SSL付き)
# ローカル: default.conf(SSL無し)

# ❌ ローカル用設定で本番を上書き → SSL消失 → Cloudflare 521エラー

教訓: 本番のNginx設定ファイルはdefault.prod.conf。絶対にdefault.confで上書きしない。docker-compose.ymlNGINX_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で複数チャートを合成すれば、リッチなダッシュボード画像が作れる
1
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
1
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?