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 ──────────────────────────────────────────── $t = microtime(true); $this->line(' Building sitemap index…'); $index = $build->buildIndex(force: true, persist: false, families: $families); $disk->put('sitemap.xml', $index['content']); $written++; $this->line(sprintf( ' sitemap.xml %d entries %.3fs', $index['url_count'], microtime(true) - $t, )); // ── Per-family documents ────────────────────────────────────────── foreach ($families as $family) { $familyStart = microtime(true); $this->newLine(); if ($family === 'artworks') { // Direct MySQL path — no cursor-scan shard window computation [$shardNames, $fw, $ff] = $this->buildArtworksDirect($disk, (bool) $this->option('progress')); $written += $fw; $failed += $ff; $this->line(sprintf( ' artworks done %d file(s) %.3fs', $fw, microtime(true) - $familyStart, )); continue; } $names = $build->canonicalDocumentNamesForFamily($family); $this->line(sprintf(' %s (%d document(s))', $family, count($names))); foreach ($names as $documentName) { $t = microtime(true); $this->line(sprintf(' Building %s…', $documentName)); $built = $build->buildNamed($documentName, force: true, persist: false); if ($built === null) { $this->line(sprintf(' %s SKIPPED (builder returned null)', $documentName)); $failed++; continue; } $disk->put('sitemaps/' . $documentName . '.xml', $built['content']); $written++; $this->line(sprintf( ' %s %d URLs %.3fs', $documentName . '.xml', $built['url_count'] ?? 0, microtime(true) - $t, )); } $this->line(sprintf( ' %s done %.3fs', $family, microtime(true) - $familyStart, )); } // ── Summary ─────────────────────────────────────────────────────── $this->newLine(); $this->info(sprintf( 'Done: %d file(s) written, %d failed total %.3fs', $written, $failed, microtime(true) - $totalStart, )); return $failed > 0 ? self::FAILURE : self::SUCCESS; } /** * Stream artworks directly from MySQL using chunkById — avoids cursor-scan shard windows. * * @return array{0: list, 1: int, 2: int} [shardNames, written, failed] */ private function buildArtworksDirect(Filesystem $disk, bool $showProgress = false): array { $chunkSize = max(1, (int) config('sitemaps.shards.artworks.size', 10_000)); $padLen = max(1, (int) config('sitemaps.shards.zero_pad_length', 4)); $shardNum = 0; $shardNames = []; $written = 0; $failed = 0; $baseQuery = Artwork::query() ->public() ->published(); $total = $showProgress ? (clone $baseQuery)->count() : null; $this->line(sprintf( ' artworks (chunk size: %d%s)', $chunkSize, $total !== null ? ', total: ' . number_format($total) : '', )); $bar = null; if ($total !== null) { $bar = $this->output->createProgressBar($total); $bar->setFormat(' %current%/%max% [%bar%] %percent:3s%% shard %message% elapsed: %elapsed:6s% mem: %memory:6s%'); $bar->setMessage('—'); $bar->start(); $this->newLine(); } $baseQuery ->select(['id', 'slug', 'title', 'updated_at', 'published_at', 'created_at', 'hash', 'file_path', 'file_name']) ->orderBy('id') ->chunkById($chunkSize, function ($artworks) use ($disk, $padLen, $bar, &$shardNum, &$shardNames, &$written, &$failed): void { $shardNum++; $t = microtime(true); $name = 'artworks-' . str_pad((string) $shardNum, $padLen, '0', STR_PAD_LEFT); if ($bar !== null) { $bar->setMessage($name); } else { $this->line(sprintf(' Building %s (%d rows)…', $name, $artworks->count())); } /** @var list $items */ $items = $artworks ->map(fn (Artwork $artwork): ?SitemapUrl => $this->artworkSitemapUrl($artwork)) ->filter() ->values() ->all(); $xml = view('sitemaps.urlset', [ 'items' => $items, 'hasImages' => collect($items)->contains(fn (SitemapUrl $item): bool => $item->images !== []), ])->render(); if (! $disk->put('sitemaps/' . $name . '.xml', $xml)) { if ($bar !== null) { $bar->advance($artworks->count()); $this->newLine(); } $this->line(sprintf(' %s WRITE FAILED', $name . '.xml')); $failed++; return; } $shardNames[] = $name; $written++; if ($bar !== null) { $bar->advance($artworks->count()); } else { $this->line(sprintf( ' %s %d URLs %.3fs', $name . '.xml', count($items), microtime(true) - $t, )); } }); if ($bar !== null) { $bar->setMessage('done'); $bar->finish(); $this->newLine(); } if ($shardNames === []) { return [[], 0, $failed]; } // Write artworks-index.xml when there are multiple shards (matches SitemapShardService behaviour) if (count($shardNames) > 1) { $t = microtime(true); $indexItems = array_map( fn (string $n): SitemapIndexItem => new SitemapIndexItem(url('/sitemaps/' . $n . '.xml')), $shardNames, ); $indexXml = view('sitemaps.index', ['items' => $indexItems])->render(); $disk->put('sitemaps/artworks-index.xml', $indexXml); $written++; $this->line(sprintf( ' artworks-index.xml %d shards %.3fs', count($shardNames), microtime(true) - $t, )); } return [$shardNames, $written, $failed]; } private function artworkSitemapUrl(Artwork $artwork): ?SitemapUrl { $slug = Str::slug((string) ($artwork->slug ?: $artwork->title)); if ($slug === '') { $slug = (string) $artwork->id; } $preview = ThumbnailPresenter::present($artwork, 'xl'); $images = []; if (! empty($preview['url'])) { $images[] = new SitemapImage((string) $preview['url'], $artwork->title ?: null); } $timestamps = array_filter(array_map( static fn (mixed $v): ?Carbon => $v instanceof Carbon ? $v : (is_string($v) ? Carbon::parse($v) : null), [$artwork->updated_at, $artwork->published_at, $artwork->created_at], )); usort($timestamps, static fn (Carbon $a, Carbon $b): int => $b->timestamp <=> $a->timestamp); return new SitemapUrl( route('art.show', ['id' => (int) $artwork->id, 'slug' => $slug]), $timestamps[0] ?? null, $images, ); } /** * @return list */ 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 $family): bool => in_array($family, $only, true))); } }