Code model (how modules behave)
Each module file: template-parts/module-{slug}.php
Probe mode (compile decision) At the top of the file:
If !empty($CLIENT_COMPILE_PROBE), set:
$client_module_dynamic = true; (only if this module must stay dynamic)
$client_module_config = [ ...minimal values read from ACF... ];
return;
Dynamic runtime render (frontend, no ACF) If !empty($CLIENT_DYNAMIC_RENDER), render only using $client_module_config; return;
Normal/editor render (ACF allowed) Regular template using get_sub_field() etc. (Optional: check $CLIENT_MODULE_STATIC if you need special snapshot-only output.)
Static by default: If a module doesn’t declare $client_module_dynamic = true in probe mode, it’s snapshotted to HTML on save.
Dynamic modules: Render live on requests (normal WP code), but do not call ACF during runtime render—use the baked config you produced in probe mode.
Functions you’ll implement
Compiler (runs on save) Hooked to acf/save_post → builds the compiled page HTML.
Name: client_compile_post($post_id) (wrap inside the hook)
Steps:
Guard: skip autosaves/revisions; restrict post types; set a transient build lock.
Loop modules rows:
Probe include module with $CLIENT_COMPILE_PROBE = true → module sets $client_module_dynamic and $client_module_config (optional).
If dynamic: append a small marker to compiled HTML, e.g.
If static: set $CLIENT_MODULE_STATIC = true; include the module; capture HTML; append to compiled string.
Persist compiled HTML:
Preferred: post_content = $compiled via wp_update_post(...)
Alternative: save to meta _client_compiled_modules
Clear build lock.
Frontend renderer (runs on request) Outputs compiled HTML and expands dynamic markers.
Name: client_print_compiled_modules(int $post_id)
Steps:
If is_preview() or no compiled yet → fallback to live loop (include modules normally).
Else get compiled HTML (from post_content via the_content() or from meta).
Expand dynamic markers with preg_replace_callback:
Parse marker JSON into $client_module_config
Set $CLIENT_DYNAMIC_RENDER = true
Include the module file (no ACF; render from config)
Echo final HTML. (If compiled stored in meta, wrap with apply_filters('the_content', $compiled) to keep embeds/srcset/CDN rewrites.)
(Optional) Helpers
client_dynamic_marker(string $slug, array $config): string → returns the marker HTML.
client_is_build_locked($post_id) / client_set_build_lock($post_id) / client_clear_build_lock($post_id) → tiny wrappers for the transient lock.
client_should_compile($post_id) → guard logic centralised.
Hooks you’ll use
acf/save_post → call client_compile_post($post_id)
Template render (e.g., page.php) → call client_print_compiled_modules(get_the_ID());
Storage choice
Best: write compiled HTML to post_content → keep Yoast analysis, excerpts, embeds, image CDN/srcset, lazyload.
Else: save in meta and output via apply_filters('the_content', $compiled).
Module file contract (at top) // Probe: declare dynamic & provide config (ACF allowed here) if (!empty($CLIENT_COMPILE_PROBE)) { // $client_module_dynamic = true; // only if dynamic // $client_module_config = [ ... values from ACF ... ]; return; }
// Dynamic runtime: render from $client_module_config only (NO ACF) if (!empty($CLIENT_DYNAMIC_RENDER)) { // echo runtime HTML using $client_module_config return; }
// Normal/editor/snapshot render (ACF OK) // $is_snapshot = !empty($CLIENT_MODULE_STATIC);
Performance & safety notes (big sites)
Build lock per post to avoid concurrent compiles.
Skip autosaves/revisions; restrict to target post types.
On compile errors, log and keep previous compiled content.
Dynamic modules: optimize with normal WP patterns (no_found_rows, specific fields, caches if needed).
That’s the whole model: compile static modules on save, let explicitly dynamic modules render live, with a tiny probe/runtime contract per module and two theme functions to compile and render.