For every placement, on every request, AdPresso makes one central decision: render the ad server-side (SSR) and write it straight into the delivered HTML, or resolve it in the visitor’s browser instead? This decision is the core of AdPresso’s rendering pipeline – not a bolted-on caching feature, but the architecture that targeting, fallbacks, lazy loading, and several other modules are all built on top of. This article walks through how that decision gets made and where it happens in the request lifecycle.
A placement doesn’t necessarily show the same thing to every visitor. Targeting conditions – device type, browser language, consent status, and so on – are sometimes only known in the browser; while PHP is handling the request on the server, those signals aren’t available yet. For a placement whose output depends on one of them, the server can’t make a final call on what to show. For everything else, it can—and does —decide directly, with no extra round trip.
The pipeline doesn’t treat this as an edge case. It’s the default branch: for every placement, AdPresso checks whether a visitor-dependent decision is required, then picks the output path accordingly.
Shown schematically below (not a line-for-line copy of the actual class):
for each published placement:
item = resolve the assigned ad or group
needs_js = placement.requires_frontend_logic
OR item.requires_frontend_logic
needs_js = apply_filters( 'adpresso_requires_frontend_logic', needs_js, placement, item )
if needs_js:
build a frontend_config payload (item data, condition JSON, fallback chain)
→ handed to the browser later, in the footer
else:
call item.render_ssr()
→ resulting HTML is written straight into the page
requires_frontend_logic isn’t a single flag – it’s the outcome of evaluating the condition tree. If the condition tree on a placement or its assigned item contains at least one condition that can only be evaluated in the browser (execution context frontend_js/frontend_ajax — see the conditions reference), the decision tips toward JS output.
The pipeline runs exactly once per request, centrally orchestrated by Pipeline\Runtime:
Runtime::maybe_execute_processor(), hooked on wp at priority 5 – after the main query has resolved and the current user is known, but before any template output. This method instantiates AdPipelineProcessor and calls its process_context().AdPipelineProcessor::process_context() iterates every published placement, resolves the assigned item for each one, and applies the decision tree above. The result is an array keyed by placement ID, each entry carrying an output_mode (ssr or js) plus the corresponding data – either rendered HTML or a config payload.Runtime::localize_frontend_data(), hooked on wp_footer, collects the config payloads for every placement in JS mode and hands them to the frontend script via wp_localize_script, as adpressoData.placementConfig. Only there – in the browser, after the page has loaded – does the JS pipeline pick the item that actually gets shown, based on the condition JSON it was given.Because the processor runs once per request and caches its own result, it doesn’t matter how many times or from which theme template a placement ends up being output — the SSR/JS decision itself is never made twice.
The adpresso_requires_frontend_logic filter is the central extension point for this branch. Several independent modules use it to force JS output for their own, unrelated reasons – regardless of whether the placement or item itself carries a frontend-only condition:
Both cases go through the same filter and the same decision tree, not a separate special-case path. This is included here as evidence of how extensible the architecture is, not as a how-to for implementing your own filter callback – details on hooking your own conditions or rendering logic belong in the filters/actions reference.
Both the SSR and JS paths follow the same fallback order if the primary assigned item can’t be delivered (invalid, display limit reached, no consent): first a placement-specific privacy fallback, then a placement-specific general fallback, and finally nothing at all. In JS mode, the fallback chain is included in the config payload so the browser can apply it without an extra server round trip.
Because a visitor-dependent placement never ships fully-resolved HTML in the server response – only a generic config payload – the page the server delivers (and that a full-page cache like WP Rocket or LiteSpeed Cache stores) contains nothing visitor-specific at that spot. The actual selection happens afterward, per visitor, in the browser. A full-page cache plugin doesn’t need to do or configure anything special for this. It’s a consequence of the architecture described above, not a dedicated cache integration.