feat(warmup): order the Breeze preload list by importance - #134
Merged
Conversation
score() re-read $record['lastmod'] directly after already guarding it with isset(), so a record with no lastmod key at all raised an E_WARNING that PHPUnit turns into a failure. Pass the guarded local through instead. Also drop the $now = 0 default: a caller who forgets it silently loses the whole freshness contribution with no signal. $now is now required, and freshness always participates in the sum.
…rd score read Add coverage for the leftover-slot branch in selectByLanguage() (previously untested — all six original fixtures divided evenly), plus a cap-exceeds- candidates case. Guard the score comparator with ?? 0 to match the existing lang guard, so an unscored record sorts last instead of raising an Undefined array key warning.
- Pair #[RunInSeparateProcess] with #[PreserveGlobalState(false)] on the
SignalCollector manualKeys tests, matching the existing convention.
- Isolate BreezeWarmupSitemapSetupTest's "Breeze absent" test the same
way, since its function_exists('breeze_get_option') check is
unreliable whenever any other test in the run mocks that function.
- menuKeys() now skips menu items that are not absolute http(s) URLs
(fragments, mailto: links), so they cannot become junk keys that can
never join with a sitemap record.
- Add missing coverage for activeLanguages() and the all-foreign
frontPages() fallback branch.
Memoize UrlCanonicalizer::canonicalize() per distinct input URL instead of recomputing it up to four times per existing entry and three times per ordered entry. Ordered can hold up to the store's cap (1000 URLs on one fleet site) and this runs synchronously inside the purge request. No observable behaviour change — existing MergeUrlsTest/FilterPreloadUrlsTest pass unedited. Also corrects PriorityStore::readUrls()'s docblock, which still described itself as serving only the legacy/ordering-off path; it now supplies the URL list on both branches of filterPreloadUrls().
register() previously fingerprinted self::$weights (unfiltered), while runRefresh() and rescoreOnMenuUpdate() fingerprint self::weights() (filtered). On any project using timberkit_warmup_priority_weights the two hashes could never agree, so weightsChanged() reported a mismatch on every purge and scheduled a needless sitemap refresh forever. Both sides now hash self::weights(). Also drops the over-specified ->once() filter-call-count assertion in test_the_filter_wins_over_the_declared_weights (an implementation detail, not a contract) and adds a test pinning the actual contract: the hash computed at registration must equal the hash a refresh write would store.
rescoreOnMenuUpdate() runs synchronously in wp_update_nav_menu at priority 5, so an exception from any of its collaborators would fatal the editor's Save request. Wrap it the same way runRefresh() already is, minus the finally: this path holds no lock. Also document that the timberkit_warmup_priority_weights filter must be pure, since its result is fingerprinted across requests to detect staleness.
This was referenced Aug 24, 2026
This was referenced Aug 24, 2026
parisek
added a commit
that referenced
this pull request
Aug 24, 2026
Cuts 1.39.0 over the two Breeze warmup changes already on `main`: #134 (order the preload list by importance) and #137 (move Breeze code under `src/Breeze`). No code changes — this only stamps the `[Unreleased]` section with a version and a date, the same shape as 9766ccc did for 1.38.0. ## Why now The ordering in #134 is the difference between the feature working and not working, measured on a real site rather than argued. On `sloneek` (2873 sitemap URLs, five languages, 10 sub-sitemaps), the pre-#134 behaviour spends the whole 200-URL cap inside the first sub-sitemap: | Page | before #134 | with #134 | | --- | --- | --- | | `/blog/` | 261 — missed | **17** | | `/sk/`, `/it/`, `/pl/` blog | 262–264 — missed | **18–20** | | 53 category archives | unreachable (last sub-sitemap) | **27–30** | | `/pricing/` | 64 | 7 | Those are the site's most expensive pages: 4.0–4.4 s cold TTFB against 0.07 s warm. Without ordering, the cap warmed 200 cheap pages and left every expensive one cold — which is why the flag read as pointless before this landed. Simulated by applying the weights `StarterBase` declares on `main` to the site's real sitemap, `lastmod` dates, 262 menu targets and Breeze's own manual list. ## One thing the measurement also showed Category archives score only through Breeze's **manual list** (weight 800). They are in no menu, are not a homepage, and their `lastmod` is old — so with the manual list absent they fall to position ~2864 of 2873. That list lives in a `wp_options` row, so a database import wipes it. Not a blocker for this release, and not something #134 claims to solve. Worth recording as the gap: a code-side curated list (`$breeze_warmup_urls`, scored like `manual`) would be the missing link, since the kit currently has no property or filter for explicit URLs — `SignalCollector::manualKeys()` reads the admin row. ## Verification `main` is green (PHP 8.3, PHP 8.4, PHPStan, composer hygiene). Locally: 1627 tests, 2707 assertions, 1 skipped. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01JMYy6JHLf4mU4H4Hd47spb
This was referenced Aug 24, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Breeze zpracovává preload frontu striktně od začátku, tempem zhruba sekunda na URL. Pozice v poli tedy přímo určuje, jak dlouho po vychladnutí cache dostane návštěvník stránku ze studena. Dosud o tom rozhodovalo pořadí, v jakém URL vyplivl generátor sitemapy.
Tenhle PR pořadí řídí podle důležitosti stránky — a tím mění strop z hrubého ořezu na vědomý výběr.
Jak to funguje
Skóre se počítá ve studené cestě (odložená cron obnova sitemapy) a do
wp_optionsjde hotové seřazené pole. Horká cesta — filtrbreeze_preload_urls, který běží synchronně v purge requestu, zatímco redaktor čeká na odpověď — jen přečte uložený seznam a pozičně ho sloučí se seznamem od Breeze. Její náklad neroste s velikostí sitemapy.Skóre je součet, ne maximum, aby čerstvá stránka v menu předběhla stránku v menu, na kterou rok nikdo nesáhl:
types)<lastmod>Remízu rozhoduje původní pořadí ze sitemapy, takže datové řazení AIOSEO zůstává jako poslední instance zdarma.
Tři věci, které je potřeba vědět předem
<lastmod>je datum úpravy, ne publikace. Deset let stará stránka s opravou překlepu se počítá jako čerstvá. Signál je zadarmo v XML, které stejně parsujeme; dotahovat skutečná data publikace by znamenalo párovat URL na příspěvky přes WPML a vlastní permalinky.Strop není celá cena.
timberkit_warmup_sitemap_max_urls(výchozí 200) platí jen na URL ze sitemapy. Položky od Breeze se nahřejí navrch a homepage i menu každého jazyka jsou garantované, i když strop překročí — je měkký záměrně.Nahřát stránku, kterou nikdo nenavštíví během TTL cache, je čistý odpad. Praktické měřítko pro nastavení stropu: kolik URL dostalo včera aspoň jedno zobrazení.
Zapnutí
Vypnuté ve výchozím stavu, jako každá nová funkce v tomhle balíčku.
Invalidace
Uložené pořadí přestane být pravdivé ze tří důvodů, a každý má svou cestu:
CACHE_TTL(hodina) — dosavadní chování.wp_update_nav_menuna prioritě 5 přeskládá pořadí na místě z uložených signálů, bez síťového požadavku, ještě než Breeze a kit na prioritě 10 spustí purge.Cron obnova a přeskládání při změně menu píší do stejného option řádku, takže přibyl optimistický zámek přes revizi: zapisovatel, jehož snímek mezitím zestárl, se zahodí místo aby přepsal čerstvá data. ADR-0006 popisuje, proti kterému prokládání to chrání a proti kterému ne.
Navíc
Site Health check
preload_chain_healthyhlásí zaseknutý preload chain. Dnes se ten výpadek nijak neprojeví — web jen tiše zůstane studený.Změna chování
fetchSitemapUrls()nově dedupuje podle kanonického tvaru URL, ne přesného řetězce. Dvě psaní téže stránky (lomítko na konci, velikost písmen ve schématu, výchozí port, fragment) se sloučí a vítězí první nalezené. Tatáž stránka se tak přestane počítat do stropu dvakrát.Tvar uloženého payloadu se rozšířil o signály, otisk vah a revizi. Migrace není potřeba — starší řádek se čte jako zastaralý a naplánuje se obnova.
Ověření
composer test— 1622 testů, 1 skipped, 0 selhánícomposer test:property— 18 testů, 15 703 asercí (invariant: řazení je permutace vstupu)composer phpstan— bez chybcomposer adr— 6 ADR, všechny v indexuŠest nových tříd v
Parisek\TimberKit\BreezeWarmup, z toho čtyři čisté funkce bez jediného volání WordPressu — proto se dají testovat bez Brain\Monkey a property test splňuje izolační konvenci z AGENTS.md.Známá omezení
preload_url()v Breeze. Každá doména se nahřívá sama ve svém purge requestu — korektní výsledek, ne chyba.weights().