Keep similar-ai from tripping the global circuit on a lone URL 502, clamp Qdrant search to 100, and add Server-Timing plus slow-request logging. Studio shared props, Academy S3 exists caching, heat chunking, and Redis/scheduler hygiene stay in this rollout.
205 lines
7.3 KiB
PHP
205 lines
7.3 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
namespace App\Console\Commands;
|
||
|
||
use App\Services\Sitemaps\SitemapBuildService;
|
||
use Illuminate\Console\Command;
|
||
use Illuminate\Support\Facades\Storage;
|
||
|
||
/**
|
||
* Builds all sitemap documents and writes them as static .xml files to the
|
||
* public disk (default: public/sitemaps/sitemap.xml and public/sitemaps/{name}.xml).
|
||
*
|
||
* Nginx can then serve those files directly (try_files $uri @php) without
|
||
* hitting PHP at all. The SitemapController falls back to these same files
|
||
* on the PHP path if a request does reach it before a static file exists.
|
||
*
|
||
* Run manually: php artisan skinbase:sitemaps:generate
|
||
* With filtering: php artisan skinbase:sitemaps:generate --only=artworks,users
|
||
*/
|
||
final class GenerateSitemapsCommand extends Command
|
||
{
|
||
protected $signature = 'skinbase:sitemaps:generate
|
||
{--only=* : Limit to specific sitemap families (comma or space separated)}
|
||
{--disk= : Override the target filesystem disk (default: sitemaps.static_publish.disk)}';
|
||
|
||
protected $description = 'Build all sitemaps and write them as static .xml files to the configured public sitemap disk.';
|
||
|
||
public function handle(SitemapBuildService $build): int
|
||
{
|
||
$totalStart = microtime(true);
|
||
$families = $this->selectedFamilies($build);
|
||
|
||
if ($families === []) {
|
||
$this->error('No valid sitemap families selected.');
|
||
|
||
return self::INVALID;
|
||
}
|
||
|
||
$diskName = (string) ($this->option('disk') ?: config('sitemaps.static_publish.disk', 'sitemaps_public'));
|
||
$disk = Storage::disk($diskName);
|
||
$written = 0;
|
||
$failed = 0;
|
||
|
||
$this->info('Disk: ' . $diskName);
|
||
$this->info('Families: ' . implode(', ', $families));
|
||
$this->newLine();
|
||
|
||
// ── Root sitemap index ────────────────────────────────────────────
|
||
// Write several paths so nginx `location = /sitemap.xml` and child
|
||
// listings stay in sync. `sitemaps/index.xml` is the canonical generated
|
||
// index: it is a new filename, so a scheduler user can create it even
|
||
// when a stale `sitemaps/sitemap.xml` is owned by another account.
|
||
$t = microtime(true);
|
||
$index = $build->buildIndex(force: true, persist: false, families: $families);
|
||
$indexPaths = ['sitemaps/index.xml', 'sitemaps/sitemap.xml', 'sitemap.xml'];
|
||
$indexWritten = 0;
|
||
|
||
foreach ($indexPaths as $path) {
|
||
if ($this->writeXml($disk, $path, $index['content'])) {
|
||
$written++;
|
||
$indexWritten++;
|
||
}
|
||
}
|
||
|
||
if ($indexWritten === 0) {
|
||
$this->error('Failed to write any root sitemap index file. Check ownership of public/sitemap.xml and public/sitemaps/.');
|
||
|
||
return self::FAILURE;
|
||
}
|
||
|
||
$this->line(sprintf(
|
||
' <info>✔</info> sitemap index %d entries %d path(s) <comment>%.3fs</comment>',
|
||
$index['url_count'],
|
||
$indexWritten,
|
||
microtime(true) - $t,
|
||
));
|
||
|
||
// ── Per-family documents ──────────────────────────────────────────
|
||
foreach ($families as $family) {
|
||
$familyStart = microtime(true);
|
||
$names = $build->canonicalDocumentNamesForFamily($family);
|
||
|
||
$this->newLine();
|
||
$this->line(sprintf(' <fg=cyan>%s</> (%d document(s))', $family, count($names)));
|
||
|
||
foreach ($names as $documentName) {
|
||
$t = microtime(true);
|
||
$built = $build->buildNamed($documentName, force: true, persist: false);
|
||
|
||
if ($built === null) {
|
||
$this->line(sprintf(' <comment>–</comment> %s <fg=red>SKIPPED</> (builder returned null)', $documentName));
|
||
$failed++;
|
||
|
||
continue;
|
||
}
|
||
|
||
$path = 'sitemaps/' . $documentName . '.xml';
|
||
if (! $this->writeXml($disk, $path, $built['content'])) {
|
||
$this->line(sprintf(' <fg=red>✖</> %s write failed', $documentName . '.xml'));
|
||
$failed++;
|
||
|
||
continue;
|
||
}
|
||
$written++;
|
||
|
||
$this->line(sprintf(
|
||
' <info>✔</info> %s %d URLs <comment>%.3fs</comment>',
|
||
$documentName . '.xml',
|
||
$built['url_count'] ?? 0,
|
||
microtime(true) - $t,
|
||
));
|
||
}
|
||
|
||
$this->line(sprintf(
|
||
' <fg=cyan>%s</> done <comment>%.3fs</comment>',
|
||
$family,
|
||
microtime(true) - $familyStart,
|
||
));
|
||
}
|
||
|
||
foreach ($build->enabledGroupIndexes() as $groupName => $groupFamilies) {
|
||
foreach ($groupFamilies as $family) {
|
||
if (! in_array($family, $families, true)) {
|
||
continue 2;
|
||
}
|
||
}
|
||
|
||
$t = microtime(true);
|
||
$built = $build->buildNamed($groupName, force: true, persist: false);
|
||
|
||
if ($built === null) {
|
||
$this->line(sprintf(' <comment>–</comment> %s.xml <fg=red>SKIPPED</> (group builder returned null)', $groupName));
|
||
$failed++;
|
||
|
||
continue;
|
||
}
|
||
|
||
$path = 'sitemaps/' . $groupName . '.xml';
|
||
if (! $this->writeXml($disk, $path, $built['content'])) {
|
||
$this->line(sprintf(' <fg=red>✖</> %s.xml write failed', $groupName));
|
||
$failed++;
|
||
|
||
continue;
|
||
}
|
||
$written++;
|
||
|
||
$this->line(sprintf(
|
||
' <info>✔</info> %s %d entries <comment>%.3fs</comment>',
|
||
$groupName . '.xml',
|
||
$built['url_count'] ?? 0,
|
||
microtime(true) - $t,
|
||
));
|
||
}
|
||
|
||
// ── Summary ───────────────────────────────────────────────────────
|
||
$this->newLine();
|
||
$this->info(sprintf(
|
||
'Done: %d file(s) written, %d failed total <comment>%.3fs</comment>',
|
||
$written,
|
||
$failed,
|
||
microtime(true) - $totalStart,
|
||
));
|
||
|
||
return $failed > 0 ? self::FAILURE : self::SUCCESS;
|
||
}
|
||
|
||
/** @return list<string> */
|
||
private function selectedFamilies(SitemapBuildService $build): array
|
||
{
|
||
$only = [];
|
||
|
||
foreach ((array) $this->option('only') as $value) {
|
||
foreach (explode(',', (string) $value) as $family) {
|
||
$normalized = trim($family);
|
||
if ($normalized !== '') {
|
||
$only[] = $normalized;
|
||
}
|
||
}
|
||
}
|
||
|
||
$enabled = $build->enabledFamilies();
|
||
|
||
if ($only === []) {
|
||
return $enabled;
|
||
}
|
||
|
||
return array_values(array_filter($enabled, fn (string $f): bool => in_array($f, $only, true)));
|
||
}
|
||
|
||
private function writeXml(\Illuminate\Contracts\Filesystem\Filesystem $disk, string $path, string $content): bool
|
||
{
|
||
$ok = $disk->put($path, $content);
|
||
|
||
if ($ok !== true) {
|
||
$this->warn(' Could not write '.$path);
|
||
|
||
return false;
|
||
}
|
||
|
||
return true;
|
||
}
|
||
}
|