コンテンツへ移動

本番環境の設定

Proの日常的な処理であるサイトスキャン、リンク切れクロール、任意のリダイレクトヒット数の書き出し、404の古い記録の削除は、Laravelのキューとスケジューラーで実行します。このページは大規模運用のための共通ガイドです。専用キュー、スケジューラー、再試行と復旧の方針、保持期間、全体を監視するテレメトリを説明します。1日約20k訪問・約900ページの本番環境で使う構成を、再現できる形で示しています。

ここで扱うものはすべてFilamentに依存しません。パネルの有無にかかわらず、エンジン、コマンド、キュー、テレメトリは同じです。Filamentを使うと表示画面が追加されますが、処理のスケジュールや実行方法は変わりません。

安全な導入順序

次の順序で進めてください。各段階を検証してから次に進めます。

  1. インストール:設定とマイグレーションを公開し、実行します。

    bash
    php artisan seo-pro:install

    seo-pro:installconfig/seo-pro.phpとProのマイグレーションを公開してから、migrateを実行します。Proのマイグレーションは公開して使う方式で、パッケージが自動ロードすることはありません。そのため、この手順でcomposer requireだけの状態から実際に使えるスキーマになります。処理は冪等で、いつでも再実行できます。公開済みファイルを上書きするには--force、マイグレーションせず公開だけするには--no-migrateを指定します。

  2. サービスプロバイダー(AppServiceProvider::boot())でスキャン対象を登録します。

    php
    use Rankbeam\Seo\Pro\Facades\SeoPro;
    
    SeoPro::targets()->register('posts', Post::class);
    SeoPro::targets()->registerRoutes('static', ['home', 'pricing']);
    // or: SeoPro::targets()->fromSitemaps();
  3. バックグラウンド処理を有効にする前に、連携を検証します。

    bash
    php artisan seo:doctor

    出力された警告をすべて修正してください。各警告には具体的なコマンドや設定行が示されます。CIでは--jsonを加え、固定のチェックIDを使って判定します。

  4. キューとスケジューラーを設定し(後述)、キューワーカーとschedule:run用のcronエントリを配置します。

  5. 任意機能は最後に有効化します。リンク切れクローラー、AI支援、Search Consoleはすべてデフォルトで無効です。クローラーにはマイグレーション済みテーブル(手順1で公開済み)と専用ワーカー(後述)が必要です。

Pro 2.41.0への更新: スキャンワーカーを一時停止し、php artisan vendor:publish --tag=seo-pro-migrations --forceでマイグレーションを公開してphp artisan migrateを実行します。その後ワーカーを再起動し、php artisan seo:doctorを実行します。新しいseo_scan_target_completionsテーブルとseo_scan_runs.target_trackingカラムが必要です。実行・対象ごとの受理記録が、終端結果の重複でカウンターが増えるのを防ぎ、最初に受理した結果を採用します。まだ対象を処理していない古いキュー内の実行は継続します。更新前に一部を処理済みの実行は履歴を保持しますが、次にキュージョブを受け取ったとき、新規スキャンの案内とともに終了します。再試行を使い切った対象は新しい実行で再試行してください。ロールバックでは、ワーカーを止めてコードを戻した後にマイグレーションを戻します。その後のスキャンも取り消す必要がある場合に備え、更新前のデータベースバックアップを保持してください。

処理ごとの専用キュー

長いスキャンやクロールが、メールや通知などユーザー向けジョブの前をふさいではいけません。各SEO処理に専用のキューとワーカーを割り当ててください。

スキャンパイプラインとリンク切れクローラーには、それぞれ設定可能なキューがあります。

処理設定環境変数デフォルトのキュー
ページ内スキャンのジョブseo-pro.scan.queueSEO_PRO_SCAN_QUEUEデフォルトキュー
リンク切れクロールのジョブseo-pro.broken_links.queue.name(+ .connectionSEO_PRO_BROKEN_LINKS_QUEUE(+ _CONNECTIONseo-broken-links

Redisの例(本番構成)

.env

dotenv
QUEUE_CONNECTION=redis

# Dedicated queues so SEO work never starves user-facing jobs.
SEO_PRO_SCAN_QUEUE=seo
SEO_PRO_BROKEN_LINKS_QUEUE=broken_links
SEO_PRO_BROKEN_LINKS_QUEUE_CONNECTION=redis

キューごとにワーカーを起動します。それぞれ別プロセス・Supervisorプログラムです。

bash
# User-facing jobs — highest priority, most workers.
php artisan queue:work redis --queue=default --tries=3

# On-page scans — moderate; a scan target job is short.
php artisan queue:work redis --queue=seo --tries=3 --timeout=360

# Broken-link crawl — one worker is plenty; jobs are long and self-redispatch.
php artisan queue:work redis --queue=broken_links --tries=1 --timeout=240

クロールワーカーの--timeoutは、seo-pro.broken_links.batch.hard_time_budget_seconds(デフォルト180)とHTTPタイムアウトの合計より長くし、記録処理の途中でバッチが停止しないようにします。ジョブ自身の$timeoutもその合計に設定されるため、ワーカーのフラグを合わせてください。クロールには--tries=1を使います。停止したジョブは次の継続処理かseo-pro:broken-links-recoverが回収するので、キュー側の再試行は不要です。

seo:doctorは各処理のキューを報告し、syncに解決されると警告します。その場合、処理がその場で同期実行され、呼び出しをブロックするためです。

スケジューラー

Laravel 11、12、13では**routes/console.php**でスケジュールを定義します。app/Console/Kernel.phpschedule()メソッドがあるのはLaravel 10から更新したアプリだけです。まだ使っている場合は同じ項目をそこに置きます。スケジューラーを毎分動かすため、システムcronに1エントリ追加します。

bash
* * * * * cd /path/to/app && php artisan schedule:run >> /dev/null 2>&1

次に、繰り返すコマンドを推奨頻度で登録します。

php
// routes/console.php
use Illuminate\Support\Facades\Schedule;

// --- Core ---------------------------------------------------------------
// Regenerate the XML sitemap (needs spatie/laravel-sitemap + registered sources).
Schedule::command('seo:sitemap')->dailyAt('01:30');

// --- Scan pipeline ------------------------------------------------------
// Scan cadence: weekly suits most sites; go daily when content changes fast.
// Queued — pair with the `seo` queue worker above.
Schedule::command('seo-pro:scan')->weekly();
// Fail runs abandoned by a dead worker so they never hang the pipeline.
Schedule::command('seo-pro:scan-recover')->hourly();
// Delete finished runs (and their issues) past the retention window.
Schedule::command('seo-pro:scan-prune')->daily();

// --- Redirects & 404s ---------------------------------------------------
// Only needed when seo-pro.redirects.hits.flush_immediately=false.
Schedule::command('seo-pro:redirects-flush-hits')->everyFiveMinutes();
// Keep the 404 log within its retention window and row cap.
Schedule::command('seo-pro:404-prune')->daily();
// Re-fetch open 404 paths; mark any that return 200 again as recovered.
Schedule::command('seo-pro:404-recheck')->daily();

// --- Broken-link crawler (only when enabled) ----------------------------
Schedule::command('seo-pro:broken-links-scan')->weekly();
Schedule::command('seo-pro:broken-links-recover')->hourly();
Schedule::command('seo-pro:broken-links-prune')->daily();

推奨頻度の一覧です。

コマンド頻度理由
seo:sitemap毎日現在のコンテンツからサイトマップを更新
seo-pro:scan毎週(更新が多ければ毎日)全対象を再監査
seo-pro:scan-recover毎時ワーカー停止で取り残された実行を回収
seo-pro:scan-prune毎日スキャン実行の保持期間を適用
seo-pro:redirects-flush-hitsredirects.hits.flush_immediately=falseの場合のみ5分ごとキャッシュにまとめたヒットカウンターをDBへ書き出す
seo-pro:404-prune毎日404ログに保持期間と行数上限を適用
seo-pro:404-recheck毎日未解決404のパスを再取得し、元ページが直って200になったものを復旧済みにする
seo-pro:broken-links-scan毎週リンク切れを再クロール(複数回のスキャンで確定)
seo-pro:broken-links-recover毎時ワーカー停止で取り残されたクロールを回収
seo-pro:broken-links-prune毎日クローラーの保持期間を適用

seo-pro:scanseo-pro:broken-links-scanは処理をキューに入れるだけで、実行するのはワーカーです。recover/pruneコマンドはその場で実行され、負荷は小さい処理です。

複数回のスキャンによるリンク切れの確定

リンク切れと判定するのは、seo-pro.broken_links.mark_broken_after_failures回の連続したスキャンで到達できなかった場合だけです。1回でも成功すればカウンターをリセットします。単発でなく定期クロールにするのはこのためで、一時的な障害1回ではリンク切れにしません。デフォルトの3回なら、週次スキャンでは最初の失敗観測から約2週間、リンクが切れた時点から最大約3週間で確定します。早く確定したい場合は実行頻度を上げるか、しきい値を下げてください。

クロールは、上限を持ち、自分で継続ジョブを投入する複数のジョブで進みます。デフォルトには有限の上限があります。自サイトと確認先ホストの容量に合わせ、seo-pro.broken_linksで調整してください。

キーデフォルト制限するもの
max_pages_per_run20001回の実行で取得するページ数。nullは明示的に無制限を選ぶ値(デフォルトではない)
max_links_per_page200ページごとのリンク確認数
max_total_linksnull実行全体のリンク確認数に対する任意の上限
batch.max_pages_per_job50キュージョブごとのページ数
batch.max_links_per_job1500キュージョブごとのリンク確認数
batch.hard_time_budget_seconds180この時間を過ぎると新しい取得を開始せず、継続ジョブを再投入
batch.dispatch_delay_seconds1継続ジョブ間の遅延
http.timeout / http.connect_timeout10 / 5リクエストごとの上限
http.max_response_bytesseo-pro.http.max_response_bytesを継承ページ取得・宛先確認のレスポンス本文に適用するストリーム読み込み上限
seed.max_response_bytesクローラー・共通HTTP上限を継承シード生成時に取得する生のサイトマップXML / .gzのバイト数
seed.max_inflated_bytesシード・クローラー・共通上限を継承.gzサイトマップの展開後に受け入れるバイト数
http.per_host_delay_ms0相手への負荷を抑える確認間隔(internal_and_externalでは長くする)

batch.hard_time_budget_secondsはクロールワーカーの--timeoutより十分短くしてください。進行中のリクエストは途中で中断できず、http.timeoutで制限されます。そのため、ワーカーのタイムアウトは処理時間枠 + HTTPタイムアウト + 余裕時間とします。

internal_and_externalのクロールでは、SsrfGuardが外向き確認を許可するようseo-pro.http.scopeまたはseo-pro.http.allowed_hostsを広げ、他社ホストへ短時間にリクエストを集中させないようhttp.per_host_delay_msを長くします。seo:doctorはクロール範囲が外部なのにガード範囲が全確認を遮断する場合に警告します。

Horizon / Supervisor

Supervisor

キューごとに1プログラムを用意します。/etc/supervisor/conf.d/app-workers.confの例:

ini
[program:app-queue-default]
command=php /path/to/app/artisan queue:work redis --queue=default --tries=3 --max-time=3600
numprocs=4
autostart=true
autorestart=true
stopwaitsecs=3600
user=www-data

[program:app-queue-seo]
command=php /path/to/app/artisan queue:work redis --queue=seo --tries=3 --timeout=360 --max-time=3600
numprocs=2
autostart=true
autorestart=true
stopwaitsecs=400
user=www-data

[program:app-queue-broken-links]
command=php /path/to/app/artisan queue:work redis --queue=broken_links --tries=1 --timeout=240 --max-time=3600
numprocs=1
autostart=true
autorestart=true
stopwaitsecs=260
user=www-data

正常な再起動でバッチ処理中のジョブが停止しないよう、stopwaitsecsはワーカーの--timeoutより長くする必要があります。

Horizon

Horizonを使う場合は、config/horizon.phpで処理ごとのsupervisorを定義し、Supervisorの代わりにプロセス管理を任せます。

php
'environments' => [
    'production' => [
        'default' => ['connection' => 'redis', 'queue' => ['default'], 'maxProcesses' => 6],
        'seo'     => ['connection' => 'redis', 'queue' => ['seo'], 'maxProcesses' => 2, 'timeout' => 360],
        'crawler' => ['connection' => 'redis', 'queue' => ['broken_links'], 'maxProcesses' => 1, 'timeout' => 240, 'tries' => 1],
    ],
],

再試行と失敗処理

スキャン対象ジョブは設定から自身の再試行方針を取得し、ワーカーの--triesには依存しません

キーデフォルト意味
seo-pro.scan.tries3対象ジョブごとの試行回数
seo-pro.scan.backoff30試行間の秒数
seo-pro.scan.timeout300対象ジョブのタイムアウト(重複防止ロックはタイムアウト + 60で失効)

対象ジョブが再試行を使い切ると対象を失敗として記録し、実行自体は終了します(partialまたはfailed)。処理済みの対象失敗によって実行がrunningのまま残ることはありません。記録処理前にワーカーが強制終了した場合は、後述の復旧処理が必要です。失敗は標準のfailed_jobsテーブルに入り、通常の方法で管理できます。

bash
php artisan queue:failed
php artisan queue:retry all

テーブルを無制限に増やさないため、SEOのスケジュールとともにqueue:prune-failedを登録します。

php
Schedule::command('queue:prune-failed --hours=168')->daily();

リンク切れクロールは--tries=1を使います。停止したジョブはリースのハートビートが古くなると次の継続処理、またはseo-pro:broken-links-recoverが回収するため、キューの再試行は処理を重複させるだけです。

復旧

進捗記録が自動で修復できないのは、ジョブ途中でワーカーが停止した場合です。2つの整理処理で対応し、どちらも毎時実行します。

  • seo-pro:scan-recoverseo-pro.scan.recovery.stuck_scan_timeout_hours(デフォルト2)の間進捗がないページ内スキャンの実行を失敗にします。
  • seo-pro:broken-links-recover:リースのハートビートが古くなったクロール実行(seo-pro.broken_links.recovery.stuck_scan_timeout_hours、デフォルト2)を回収し、失敗として、スコープごとに1実行という枠を解放します。

seo:doctorはこれを最近のハートビートの証拠として表示します。スキャンを使い始めると、停止した実行を報告してrecoverコマンドを案内します。cronが実際に動くことまでは証明できません。どのコマンドにもできないことであり、実行履歴に現れた事実を報告します。

保持期間

テーブルの増加を制限してください。デフォルトは次のとおりです。すべてseo-pro.*にあり、nullで該当の削除処理を無効にします。

データ設定デフォルトコマンド
スキャン実行(+ 問題)scan.retention.scan_runs_days90seo-pro:scan-prune
404ログmonitor_404.retention_days(+ max_rows 1000090seo-pro:404-prune
クロール実行broken_links.retention.scan_runs_days90seo-pro:broken-links-prune
解決済み検出結果broken_links.retention.resolved_findings_days30seo-pro:broken-links-prune

運用テレメトリ

ページ内スキャンとリンク切れクロールは、完了するたびに構造化された完了ログを1行出力します。パネルなしでも指標の履歴を得られます。ペイロードは件数と時間だけで、URL、本文、ヘッダー、訪問者データは含みません。

指標スキャンクロール
pages_fetched
links_checked
links_broken
blocked_urls(SSRF対策で拒否した対象)
transient_failures(ネットワーク失敗。次回スキャンで再確認)
total_targets / completed_targets / failed_targets
issues_found
duration_seconds
queue_lag_seconds(キュー投入 → 最初のバッチ)

seo-pro.telemetryで設定します。

php
'telemetry' => [
    'enabled' => env('SEO_PRO_TELEMETRY_ENABLED', true),
    'channel' => env('SEO_PRO_TELEMETRY_CHANNEL'), // null = default log channel
    'level'   => env('SEO_PRO_TELEMETRY_LEVEL', 'info'),
],

channelに専用ログチャンネルを指定すれば、アプリケーションログと混ぜずにLoki / Datadog / CloudWatchなどの出力先へ送れます。

php
// config/logging.php
'channels' => [
    'seo' => ['driver' => 'single', 'path' => storage_path('logs/seo.log'), 'level' => 'info'],
],
dotenv
SEO_PRO_TELEMETRY_CHANNEL=seo

より詳しく処理する場合はイベントを直接購読します。それぞれ同じmetrics()ペイロードを公開しています。

php
use Rankbeam\Seo\Pro\Events\SeoScanCompleted;
use Rankbeam\Seo\Pro\BrokenLinks\Events\BrokenLinkScanCompleted;

Event::listen(SeoScanCompleted::class, function (SeoScanCompleted $event) {
    Metrics::gauge('seo.scan.issues', $event->metrics()['issues_found']);
});

Event::listen(BrokenLinkScanCompleted::class, function (BrokenLinkScanCompleted $event) {
    Metrics::gauge('seo.crawl.broken', $event->metrics()['links_broken']);
});

テレメトリはベストエフォートで、チャンネルの設定ミスでスキャンが失敗することはありません。

Filamentに依存しないデプロイ

このページの内容にパネルは不要です。エンジン、全コマンド、キュー、スケジューラー、復旧、保持期間、テレメトリはヘッドレスでも同じです。Filamentパネル(SeoProPlugin)が追加するのは、リアルタイムのスキャン進捗、問題一覧、リダイレクトCRUD、404監視、リンク切れダッシュボードという表示画面だけです。エンジンを配置してCLIとスケジューラーで運用し、後からパネルを追加しても、あるいは追加しなくても、移行ややり直しは必要ありません。全コマンドのリファレンスはヘッドレスでの利用を参照してください。

rankbeam/laravel-seoはMITライセンスで公開されています。