Speculatr for Craft CMS

Configuration

Settings live at Settings → Plugins → Speculatr, and every one of them can be set in config/speculatr.php instead. The defaults are chosen to be useful and hard to regret, so plenty of sites never open this screen.

<?php
return [
    'mode' => 'both',
    'eagerness' => 'moderate',
    'forLoggedIn' => true,
    'excludePaths' => ['checkout/*', 'account/*'],
    'excludeParams' => ['add-to-cart'],
    'prefetchUrls' => ['/contact'],
];

What to do

mode

prefetch, prerender or both. Default prerender.

ModeWhat the browser doesWhat it costs
prefetchdownloads the page's HTML and stopsa few kilobytes; no scripts run, nothing on the page fires
prerenderloads and runs the whole page in a hidden taban entire page load that may never be used
bothprefetches at your eagerness, prerenders one step behind itthe two layer

both is the interesting one. The browser prefetches on hover and upgrades the same URL to a prerender once the reader commits, so the expensive half is never spent on a link the pointer merely crossed. It roughly doubles the size of the rules block, because each action needs its own copy of the exclusion list.

eagerness

conservative, moderate, eager or immediate. Default moderate.

ValueTrigger
conservativepointer or touch down — the reader has committed
moderateabout 200 ms of hover, or pointerdown
eagerabout 10 ms of hover — nearly anything the pointer crosses
immediateas soon as the rules are seen, with no interaction at all

Chrome holds only two speculations at a time at anything other than immediate, discarding the oldest. So a keener setting does not mean more speculation — it means it starts earlier and is thrown away more often. moderate is the default because 200 ms of hover is a good proxy for intent.

Who gets it

SettingDefault
forGueststruesigned-out visitors
forLoggedInfalseeverybody signed in
forAdminsfalseadmins, independent of the above, so you can try it on yourself first
downgradeForLoggedIntrueprerendering becomes prefetching for anyone signed in

Signed-in visitors are off by default and it is worth understanding why. A prerender runs the page's JavaScript in a hidden tab. A page rendered for one specific person is both the most expensive page you serve and the one most likely to have something on it that should not happen twice.

downgradeForLoggedIn is the compromise that makes signed-in speculation defensible: those visitors still get their next page out of the prefetch cache, without a hidden tab running their scripts.

What never to speculate

Craft's own configuration is read first, so all of this is already excluded under whatever names your site gives them: the control panel (cpTrigger), action requests (actionTrigger, and ?action= too), every account path (loginPath, logoutPath, setPasswordPath and the rest), preview and site and CSRF tokens (tokenParam, siteToken, csrfTokenName), and published resources (resourceBaseUrl).

Speculatr → Rules lists every one of them with its reason. On top of that:

SettingDefault
optOutClassno-speculationa class on a link that opts it out; empty for none
excludeNofollowtrueskip rel="nofollow" links
excludeDownloadstrueskip download links, and links to files
excludeNewWindowtrueskip target="_blank" links
excludeQueryStringsfalseskip every URL carrying a query string
excludePaths[]URI patterns, * wildcards, leading slash optional
excludeParams[]a URL carrying one of these is never speculated
excludeSelectors[]CSS selectors matched against the link itself
excludeExtensionsa long listtreated as files rather than pages

excludeNewWindow is on because a prerender cannot be handed to a new tab — one started for a target="_blank" link is thrown away on activation, so it is all cost and no benefit.

excludeQueryStrings is blunt and is the right answer for a site whose query strings mean "do something" rather than "show something".

Craft Commerce: its action requests are already covered, but a cart or checkout page with a URI of its own is not — Speculatr cannot know what you called it. Add it to excludePaths. The Rules screen says so too, when Commerce is installed.

Named URLs

prefetchUrls (default []) — URLs prefetched immediately on every page, whether or not anything links to them. For the two or three pages everybody ends up on.

Always prefetched and never prerendered: an unconditional prerender of a handful of pages, on every single page view, is a lot of page loads to spend on a guess about where somebody is going next. If you have decided otherwise for one page, craft.speculatr.prerender() is there.

Tracking parameters

SettingDefault
ignoreTrackingParamstruedeclare that the parameters below do not change the page
trackingParamsutm_*, gclid, fbclid, msclkid, mc_cid, …the parameters in question

With this on, Speculatr sends a No-Vary-Search header on front-end responses and mirrors it into the rules as expects_no_vary_search, so a prefetch of /pricing is still a hit when the reader actually arrives at /pricing?utm_source=newsletter.

Both halves are needed and they do different jobs. The header is what makes the cached entry match at all; the rule is what stops the browser racing its own in-flight prefetch. Turn the whole thing off if any parameter in your list genuinely changes what you serve.

Delivery

SettingDefault
deliveryinlineinline or header
skipOnSpeculativetruea page fetched by a speculation gets no rules of its own
varyOnSecPurposetruesend Vary: Sec-Purpose when the response depends on it
excludePages[]URI patterns whose own pages carry no rules

delivery: header sends a Speculation-Rules header pointing at a JSON document at /speculatr/rules.json, fingerprinted so a change to your exclusions reaches everybody on their next page view. It is the answer for a site whose Content-Security-Policy will not allow 'inline-speculation-rules'. Per-page additions from templates still go inline in that mode — a rules file is one document for the whole site by definition, so it cannot carry them.

skipOnSpeculative stops a prerender from starting its own prerenders. The second hop is a guess about a guess.

varyOnSecPurpose should stay on while skipOnSpeculative is on. Without it, a shared cache stores the rules-free copy a prefetch received and hands it to the next real visitor — a bug that only appears behind a CDN and only intermittently.

excludePages is a different question from excludePaths, and the two are easy to confuse. excludePaths is about where links point; excludePages is about which pages carry rules. A checkout page should not be linked to speculatively; an account page might be fine to link to but should not itself hand out rules.