Core¶
PHP foreach with reference + ?? []: mutations don't persist¶
From legacy section: WordPress / MCP
Why:
foreach ($node['elements'] ?? [] as &$child)iterates over a copy — the??operator returns a value, not a reference, so the&$childreference is bound to a temporary array. Mutations to$childare lost. Burnt 45 minutes on the strip-recaptcha-v3 script (May 13) because the function reported success but the JSON output was byte-identical to input. Switched to index-based recursion (for ... in_place ...) and it worked. How to apply: When recursing through a tree to mutate nodes in PHP, prefer index-based iteration (for ($i = 0; $i < count(...); $i++)) or build a new tree ($new_children = []then return). Never useforeach (...$expression... as &$x)— the expression result isn't a reference target. Same trap exists for any non-variable expression:??, ternary, function calls. Date: 2026-05-14
AI-generated hero images — dodge the cliches with specific scenes, not generic prompts¶
From legacy section: WordPress / MCP
Why: The original IA design research warned against AI imagery because "diverse business team handshaking in modern office" reads as fake instantly. But Gemini Imagen 3 at 2K with specific prompts (named Capital Region location, named architectural style, explicit NO callouts for scales/gavel/handshake, "editorial documentary photography" style cue) produces credible heros that pass for stock photography. Generated 29 page-specific images on 2026-05-14, all passed Jim's review. The bad AI is generic prompts; the good AI is hyper-specific scene + style + explicit negatives. How to apply: When generating AI imagery for professional sites: (1) name a specific place or scene context per page, not a generic concept; (2) specify camera/lens/composition ("35mm wide lens", "shallow depth of field", "golden hour light"); (3) explicit negative prompts ("NO handshakes, NO scales of justice, NO gavel, NO modern glass towers, NO diverse-team-around-laptop"); (4) request "editorial documentary photography" style not "professional" or "corporate"; (5) use
useGoogleSearch: truefor real-place imagery so the model anchors in actual architecture/landmarks. Date: 2026-05-14
Shared template sections need generic copy, not page-1 copy baked in¶
From legacy section: WordPress / MCP
Why: Master template's "Meet Our Real Estate Attorneys" heading was authored when the template was built for /real-estate/. Then the same template got applied to 29 other pages — criminal defense, family law, business, hubs, homepage — where the team handles many more practice areas than real estate. Read inaccurate on every non-RE page. Jim caught it on the homepage. Should have been generic ("Meet Our Attorneys" + practice-spanning intro) from day one. How to apply: When designing a template that will be reused across multiple page types, audit every heading, intro, and CTA copy for category-specific language. If a section will appear on /family-law/ and /business/, it should not say "real estate" or any other specific category. Reserve practice-specific copy for the body content sections that the adapter swaps per page. Date: 2026-05-14
wp_get_post Only Works for Posts¶
From legacy section: WordPress / MCP
Pattern: Called
wp_get_postwith a page ID and received "Endpoint not found." Rule: Never usewp_get_postfor pages. Usewp_get_pagesto confirm existence, or rebuild from source files on disk, then push viawp_update_page. Date: 2026-03-05
wp_redirect() is undefined in mu-plugin top-level code — use raw header() + exit¶
From legacy section: WordPress / MCP
Why: Mu-plugins load BEFORE
wp-includes/pluggable.php(which defineswp_redirect(),wp_safe_redirect(),wp_get_current_user(), etc.). Callingwp_redirect()at the top of a mu-plugin throws a fatal "Call to undefined function" 500. Burnt one deploy cycle on 2026-05-18 buildingevolve-keyword-domain-redirects.phpon evolvebusiness.com — plugin fired (response had ourX-Redirect-Byheader) but returned 500 instead of 301. Fix was one-line:wp_redirect($target, 301); exit;→header('Location: ' . $target, true, 301); exit;. How to apply: Any mu-plugin that needs to redirect, render output, or call WP user/auth functions at the top level (before any hook fires) must use native PHP —header()for redirects,$_SERVERfor request data, raw exit. The pluggable functions only become available insideinitor later. If you must usewp_redirect, hook intoinitortemplate_redirect— but for host-match redirects (where you want to short-circuit BEFORE WP boots fully), top-level +header()is the right pattern. Date: 2026-05-18
Antispam Mu-Plugin Blocks WebFetch on ialawny.com + iclawny.com — CORRECTED 2026-04-26¶
From legacy section: Schema Auditing & Deployment
Pattern (original 2026-04-16, partially wrong): WebFetch returned 403 on
ialawny.comand we attributed it to the Hostinger antispam mu-plugin. Correction (2026-04-26): Re-investigated when Ianniello Chauvin (iclawny.com) Full audits kept timing out. Tested curl with five different user-agents (default, empty, Firefox, Anthropic, claude-web) — ALL returned HTTP 200. Read the actualevolve-antispam.phpsource: it only blocks Elementor + Gravity Forms submissions viaelementor_pro/forms/validationandgform_entry_is_spamfilters. There is NO page-level access control, NO 403 on GET, NO user-agent blocking. The earlier 403 must have been transient or attributed to the wrong layer. Rule: Don't accept "WebFetch returned 403" as a finished diagnosis. Test with curl + multiple UAs. Read the actual mu-plugin source to see what it does. If a curl test works but WebFetch fails, the issue is on the WebFetch side (header normalization, IP blocks at the CDN/firewall layer, or transient network) — not the WordPress plugin. Suspect-and-verify, not suspect-and-document. Date: 2026-04-16 (original) / 2026-04-26 (corrected)
Hostinger Preview Domain Mu-Plugin Can Interfere¶
From legacy section: Hostinger / Server
Pattern:
hostinger-preview-domain.phpin mu-plugins intercepted requests and could serve stale/cached content even after theme and page changes. Rule: On new Hostinger sites, disablehostinger-preview-domain.phpby renaming to.disabledonce the real domain is configured:mv hostinger-preview-domain.php hostinger-preview-domain.php.disabledDate: 2026-03-24
URL slug migrations: rename slug + mu-plugin 301 redirect = clean cutover¶
From legacy section: Competitive Audit Automation (v1.1/v1.2)
Pattern:
/free-analysis/was the original parent path for all audit reports. Once paid Medium ($49) + Full ($199) tiers launched alongside Light, the "free-analysis" framing was actively misleading. Slug migration to/audits/needed to (a) preserve every URL ever shared in emails, (b) update code that constructs URLs, (c) redirect transparently for end users. Rule: WordPress slug migration playbook for evolvebusiness.com (works on any WP + mu-plugins setup): 1. Update slug via REST:POST /wp-json/wp/v2/pages/{id}with{ "slug": "new-slug" }. WP keeps an internal old-slug record but does NOT cascade for child pages. 2. Flush rewrite rules:wp rewrite flushvia SSH — without this, the new URL returns 404 even though the page exists at the new slug. 3. Drop a mu-plugin at/wp-content/mu-plugins/evolve-{name}-redirects.phpwith atemplate_redirectaction that 301s^/old-path(/.*)?$→/new-path$1. mu-plugins auto-load (no activation needed) and survive theme/plugin changes. 4. Update every code path that constructs the old URL — search bothsrc/and skill files. For competitive-audit:wordpress.js(5 refs) +competitive-analysis.mdskill (5 refs). Verify with curl: legacy URL → 301 → new URL → 200. Date: 2026-04-25
Convert WooCommerce → lead-gen with a single mu-plugin (preserve SEO equity)¶
From legacy section: SEO NEO / Workbook
Pattern: POLY Wellness needed e-commerce shut off without losing the SEO equity on /shop-by-product/, the 9 product pages, or the existing dealer-acquisition funnel. Plugin-deactivation would have cratered the URL space; manually editing each Elementor page would have been hours of work and would re-set with future template edits. Rule: For any "kill the cart, keep the URLs" engagement, deploy ONE mu-plugin (
wp-content/mu-plugins/{client}-no-cart.php) that does all of: (1)template_redirecthook 301-redirects/cart/,/checkout/,/my-account/to/contact/; (2)woocommerce_loop_add_to_cart_linkfilter swaps shop-archive Add-to-Cart buttons → "View Details" links (preserves URLs, removes purchase intent); (3) remove_action onwoocommerce_single_product_summarypriority 30 stripssingle_add_to_cart_buttonfrom each product page, replaced with a styled "Call / Email" CTA block viawoocommerce_after_single_product_summarypriority 5; (4) loop through Woo's 11 transactional emails and disable each with thewoocommerce_email_enabled_*filter; (5) defensive site-wide CSS.cart-icon, .header-cart, .woocommerce-mini-cart { display: none !important; }. Result: SEO equity preserved on all product/category URLs, zero edits to Elementor templates, single rollback point. Reference implementation:clients/_active/poly/website-update-2026-04/scripts/poly-no-cart.php. Date: 2026-04-28
WordPress canonical redirect only enforces homepage cross-domain — internal paths need a mu-plugin¶
From legacy section: SEO NEO / Workbook
Pattern: During the sandmans.net swap, after WP
siteurl/homewas updated tohttps://sandmans.net, requests tohttps://sandmansblasting.com/correctly returned 301 tohttps://sandmans.net/(WP'sredirect_canonical()kicked in). Buthttps://sandmansblasting.com/about/,/services/cerakote/,/services/media-blasting/all returned HTTP 200 — not redirected. Body inspection showed WordPress was serving sandmans.net's content under the wrong hostname, with<link rel="canonical" href="https://sandmans.net/about/">in the head. WP's canonical redirect only forces a 301 on homepage and certain post-type/permalink mismatches; for internal page paths under the wrong hostname, it serves content with a canonical<link>tag instead of redirecting at the HTTP layer. Google generally handles this fine (consolidates ranking signals via the link tag), but it leaves duplicate content technically accessible at the secondary hostname and doesn't provide a true 301 to crawlers, scrapers, or browser users. Not a true canonical swap. Rule: When you need true 301s across all paths from a secondary hostname to a primary canonical (e.g., old domain → new domain post-swap, or a parked alias), don't rely on WP's canonical redirect. Add a mu-plugin that hooksinitpriority 1 (fires before WP canonical attemplate_redirectpriority 10), checks$_SERVER['HTTP_HOST']against the secondary hostname(s), andwp_redirect('https://canonical.com' . $_SERVER['REQUEST_URI'], 301); exit;. This guarantees a true 301 on every path including query strings. Pair with the same plugin handling any legacy URL slug remaps via a static[old => new]map in the same hook. Skip whenis_admin() || wp_doing_ajax() || wp_doing_cron() || REST_REQUEST || WP_CLIto avoid breaking the admin/API surface. Reference implementation lives in source control atclients/_active/sandmans/build/sandmans-net-legacy-redirects.php— clone for any future similar swap, swap host strings + slug map, deploy towp-content/mu-plugins/. Date: 2026-04-28
WP REST API + plugin meta visibility gap → use a save_post mu-plugin¶
From legacy section: SEO NEO / Workbook
Pattern: The competitive-audit pipeline writes audit reports to evolvebusiness.com via WP REST API (
POST /wp-json/wp/v2/pages). To make those pages noindex via TSF, we needed to set_genesis_noindex=1post_meta. But TSF's_genesis_*keys are NOT registered for REST visibility — passingmeta: { _genesis_noindex: 1 }in the REST POST body silently fails (WordPress strips unregistered meta keys from REST writes). Building a custom REST endpoint, or registering the meta keys ourselves, would either require touching TSF (fragile) or adding a parallel REST surface (overkill). The clean answer: WordPress fires the standardsave_postaction on every save regardless of who initiated it (REST API, WP-CLI, admin UI, programmatic save_post). A 25-line mu-plugin hookingsave_post_pageand applying the meta on a condition (post_parent === 2304) handles every code path with zero pipeline changes required. Rule: When a pipeline that writes via WP REST API needs to set post_meta that the relevant plugin doesn't expose to REST, the right pattern is asave_postmu-plugin, not REST acrobatics. Skeleton:Always include the autosave + revision guards (otherwise the hook fires multiple times during a single save). Use priority 20 to run after most plugins. Castadd_action('save_post_<post_type>', function($post_id, $post) { if (wp_is_post_autosave($post_id) || wp_is_post_revision($post_id)) return; if (/* condition that identifies target posts */) { update_post_meta($post_id, '_some_plugin_key', 1); } }, 20, 2);post_parentto(int)if the condition involves it (REST API sometimes sends it as a string). The mu-plugin makes the pipeline change non-load-bearing — even if the pipeline is unchanged, the meta still gets stamped. Reference implementation:clients/_active/evolve-business/build/evolve-audits-noindex.php(lives in worktree atbuild/evolve-audits-noindex.php— deployed to/wp-content/mu-plugins/evolve-audits-noindex.phpon evolvebusiness.com). Date: 2026-04-30
WordPress wpautop breaks CSS grids with nested <a display:block> elements¶
From legacy section: SEO NEO / Workbook
Pattern: Built a 4-card grid on
/audit/using<div style="display:grid">containing 4 child<a style="display:block">cards. Looked perfect locally. When pushed to WordPress via the REST API and rendered, the grid showed phantom empty cells — cards rendered in wrong positions, ghost boxes between them. Inspection of the rendered HTML showed WordPress'swpautopfilter had injected phantom</p>tags immediately after each<a>opening tag and<br>tags between cards. The grid container then saw a different DOM structure than intended (more children, broken inheritance), and the auto-fit grid algorithm allocated cells to the phantom elements. Rule: Any HTML containing block-level<a>elements inside a grid/flex container must be wrapped in a<!-- wp:html --> ... <!-- /wp:html -->Gutenberg HTML block when published via WP REST API. The wp:html block opts out ofwpautopprocessing for that section. Without it,wpautopwill break the layout silently — the markup looks fine in get_page raw response, but the rendered output is mangled. Detection:curl <url> | grep -A2 '<a [^>]*>'— if you see<a ...></p>instead of<a ...><p>...</p>...</a>, wpautop is mangling. Why: Surfaced 2026-05-11 on/audit/redesign. The 4-card sample showcase was edge-to-edge OR rendered as broken cells until wrapped in wp:html. The existing/audit/tier cards used divs (not anchors), which is why they were never affected. How to apply: Whenever creating WordPress page content via REST API that includes ANY grid/flex layout with anchor-element children, wrap the section in<!-- wp:html -->markers. Also applies to pages built/edited via the Gutenberg block editor where you want to bypass auto-paragraph. Doesn't apply to Elementor pages (Elementor manages its own DOM). Don't worry about it for the Canvas template wp:html blocks I already build for/free-audit/-style pages — those are wp:html-wrapped by design. Date: 2026-05-12