<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Muazzam</title>
    <description>The latest articles on DEV Community by Muazzam (@muazzami).</description>
    <link>https://dev.to/muazzami</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F151435%2F44dc0c44-7eec-433e-b494-e84d8c7a0ffc.jpeg</url>
      <title>DEV Community: Muazzam</title>
      <link>https://dev.to/muazzami</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/muazzami"/>
    <language>en</language>
    <item>
      <title>From Audit Findings to Low-Risk Fixes on a Managed WordPress Site</title>
      <dc:creator>Muazzam</dc:creator>
      <pubDate>Mon, 27 Apr 2026 10:18:36 +0000</pubDate>
      <link>https://dev.to/muazzami/from-audit-findings-to-low-risk-fixes-on-a-managed-wordpress-site-4jjf</link>
      <guid>https://dev.to/muazzami/from-audit-findings-to-low-risk-fixes-on-a-managed-wordpress-site-4jjf</guid>
      <description>&lt;h1&gt;
  
  
  From Audit Findings to Low-Risk Fixes on a Managed WordPress Site
&lt;/h1&gt;

&lt;p&gt;A routine security review exposed several gaps on a live WordPress site hosted on WP Engine, including a missing &lt;code&gt;security.txt&lt;/code&gt; file, missing HTTP security headers, and broader concerns around admin exposure. The challenge was not just fixing the findings, but doing so in a way that avoided downtime, avoided unnecessary plugins, and respected the realities of an already-running client environment.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;The site had landed in the familiar middle ground that many long-running WordPress projects occupy: stable enough to stay online, but missing a few modern security controls that external audits now expect to see. The audit highlighted missing &lt;code&gt;security.txt&lt;/code&gt;, missing headers such as &lt;code&gt;Permissions-Policy&lt;/code&gt;, &lt;code&gt;Referrer-Policy&lt;/code&gt;, &lt;code&gt;Strict-Transport-Security&lt;/code&gt;, and &lt;code&gt;X-Content-Type-Options&lt;/code&gt;, and recommendations around access restrictions for admin-related paths. None of these items existed in isolation; together, they raised the real question every developer has to answer on production work: how to improve security without creating avoidable breakage.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this was tricky
&lt;/h2&gt;

&lt;p&gt;On paper, the fixes looked simple. In practice, applying security controls to a live managed-hosting setup can be risky when plugins, third-party scripts, old content, and operational habits have built up over time. A strong policy applied too aggressively can block legitimate assets, disrupt logins, or create support noise for a site that had otherwise been functioning normally.&lt;/p&gt;

&lt;p&gt;That meant the job was not “apply every recommendation immediately.” The job was to split findings into safe wins, cautious changes, and deferred items that required testing or approval.&lt;/p&gt;

&lt;h2&gt;
  
  
  The first fix: &lt;code&gt;security.txt&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;The first step was to publish a valid &lt;code&gt;security.txt&lt;/code&gt; file at the standard discovery path expected by security tooling. The useful lesson here is that &lt;code&gt;security.txt&lt;/code&gt; does not need to be overengineered from day one. Under RFC 9116, only &lt;code&gt;Contact&lt;/code&gt; and &lt;code&gt;Expires&lt;/code&gt; are required; fields such as &lt;code&gt;Policy&lt;/code&gt;, &lt;code&gt;Encryption&lt;/code&gt;, and &lt;code&gt;Acknowledgments&lt;/code&gt; are optional.&lt;/p&gt;

&lt;p&gt;That matters because many organizations do not yet have a formal vulnerability disclosure program, a public policy page, or a dedicated PGP key. In that situation, the right move is to publish a minimal but accurate file rather than a polished-looking file full of placeholders nobody will maintain.&lt;/p&gt;

&lt;p&gt;A minimal version looked like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Contact: mailto:security@example.com
Expires: 2027-03-31T23:59:59Z
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That solved the audit finding while keeping the implementation honest and maintainable.&lt;/p&gt;

&lt;h2&gt;
  
  
  The second fix: security headers in the right place
&lt;/h2&gt;

&lt;p&gt;The next step was to add the missing HTTP security headers, but not through another WordPress plugin. On WP Engine, platform-level configuration through Web Rules is a cleaner and more supportable place to manage response headers than patching them into the application layer.&lt;/p&gt;

&lt;p&gt;This distinction matters. A plugin-based fix may technically work, but it also adds more moving parts to the same WordPress stack that is already being audited for risk and complexity. Using the hosting platform for headers reduces that dependency footprint and makes the behavior easier to reason about across environments.&lt;/p&gt;

&lt;p&gt;The rollout focused first on the safer headers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;X-Content-Type-Options: nosniff&lt;/code&gt; to reduce MIME-type sniffing risk.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Referrer-Policy: strict-origin-when-cross-origin&lt;/code&gt; to improve privacy without usually breaking normal behavior.&lt;/li&gt;
&lt;li&gt;A conservative &lt;code&gt;Permissions-Policy&lt;/code&gt; based on what the site actually needed, instead of a copied policy template.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The fix that needed caution
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;Strict-Transport-Security&lt;/code&gt; was not treated as a copy-paste checkbox. HSTS is valuable, but it changes browser behavior in a sticky way by telling clients to use HTTPS only for a defined period, which means a bad rollout can have consequences beyond a single deploy.&lt;/p&gt;

&lt;p&gt;That is why the safer approach was to stage HSTS behind an HTTPS readiness check instead of enabling an aggressive policy immediately. If any assets, legacy links, or integrations still depend on &lt;code&gt;http://&lt;/code&gt;, turning on HSTS too early can break parts of the site experience rather than harden it cleanly.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually solved the problem
&lt;/h2&gt;

&lt;p&gt;The real solution was not one header or one file. The solution was a triage model:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Implement immediately what is low-risk and clearly beneficial.&lt;/li&gt;
&lt;li&gt;Test carefully anything that can affect site behavior across browsers or subdomains.&lt;/li&gt;
&lt;li&gt;Defer controls that require stakeholder approval, process change, or operational support.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That approach turned a potentially messy audit response into a practical remediation plan. It also kept the work aligned with the reality of WordPress development: good security changes are the ones that survive production, not just the ones that look impressive in a checklist.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this is really about
&lt;/h2&gt;

&lt;p&gt;Security work on WordPress often gets framed as a plugin hunt or a list of hardening tips. In production, it is usually a prioritization problem instead: deciding what belongs in hosting, what belongs in the app, what needs testing, and what should wait for process maturity.&lt;/p&gt;

&lt;p&gt;That is the more useful lesson from this kind of task. The best security fix is often not the most aggressive one; it is the one that improves the posture of the site while keeping risk, complexity, and maintenance load under control.&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>security</category>
      <category>wpengine</category>
      <category>devops</category>
    </item>
    <item>
      <title>Escaping Managed Hosting: What Happened When We Migrated a WooCommerce Site to a VPS (And Got Attacked)</title>
      <dc:creator>Muazzam</dc:creator>
      <pubDate>Sat, 25 Apr 2026 03:02:47 +0000</pubDate>
      <link>https://dev.to/muazzami/escaping-managed-hosting-what-happened-when-we-migrated-a-woocommerce-site-to-a-vps-and-got-365m</link>
      <guid>https://dev.to/muazzami/escaping-managed-hosting-what-happened-when-we-migrated-a-woocommerce-site-to-a-vps-and-got-365m</guid>
      <description>&lt;h1&gt;
  
  
  Escaping Managed Hosting: What Happened When We Migrated a WooCommerce Site to a VPS (And Got Attacked)
&lt;/h1&gt;

&lt;p&gt;Managed WordPress hosting sounds like a great deal — until it isn't. This is the story of migrating a WooCommerce + WPML site off a major managed host, the chaos that followed immediately after, and the hard lessons learned about what managed hosting was silently doing for us that we didn't fully appreciate until it was gone.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why We Left Managed Hosting
&lt;/h2&gt;

&lt;p&gt;The decision wasn't dramatic. It came down to three compounding frustrations:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cost vs. control.&lt;/strong&gt; Managed WordPress hosting at the enterprise tier isn't cheap. As traffic and complexity grew, so did the invoice. But the control stayed locked down — no custom server config, limited cache tuning, no ability to see what was actually hitting the server at a low level.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Performance ceiling.&lt;/strong&gt; A WooCommerce store with WPML (multilingual) generates a lot of unique URLs — filtered shop pages, language variants, paginated archives. The managed host's caching layer was a black box. When performance degraded, the answer was always "upgrade your plan." There was no way to diagnose what was actually happening underneath.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Visibility.&lt;/strong&gt; When something went wrong, we couldn't see access logs in real time, couldn't inspect PHP worker counts, couldn't adjust server-level settings. Everything was abstracted. The host was the gatekeeper between us and the actual machine.&lt;/p&gt;

&lt;p&gt;The move to a self-managed VPS with RunCloud and OpenLiteSpeed (OLS) promised full visibility and control. It delivered on that promise — but it also immediately exposed us to everything the managed host had been silently absorbing on our behalf.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Migration: What We Were Walking Into
&lt;/h2&gt;

&lt;p&gt;The stack:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;WordPress + WooCommerce&lt;/strong&gt; with WPML (multilingual, two languages)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Elementor&lt;/strong&gt; as the page builder&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LSCache&lt;/strong&gt; for caching, &lt;strong&gt;Redis&lt;/strong&gt; for object caching&lt;/li&gt;
&lt;li&gt;Migrating from managed hosting to a &lt;strong&gt;VPS via RunCloud&lt;/strong&gt; with &lt;strong&gt;OpenLiteSpeed&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The migration itself was technically straightforward: export, import, update DNS. What wasn't straightforward was what we discovered in the database afterward.&lt;/p&gt;

&lt;h3&gt;
  
  
  Problem 1: Serialized Data URL Contamination
&lt;/h3&gt;

&lt;p&gt;During migration, the site temporarily lived on a staging domain (something like &lt;code&gt;mysite.staging.temphost.link&lt;/code&gt;). The standard search-replace after migration should catch all references to the old domain and replace them with the new one.&lt;/p&gt;

&lt;p&gt;It didn't catch everything.&lt;/p&gt;

&lt;p&gt;Several plugins store data in WordPress's &lt;code&gt;wp_options&lt;/code&gt; table as &lt;strong&gt;serialized PHP arrays&lt;/strong&gt;. A normal string search-replace on serialized data corrupts it — because serialized strings encode their own length, and changing the URL changes the string length without updating the length prefix.&lt;/p&gt;

&lt;p&gt;The plugins that caused contamination:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A media carousel plugin&lt;/strong&gt; — stored 400KB+ of serialized image data in &lt;code&gt;wp_options&lt;/code&gt;, all with the temp domain baked in for image paths&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Elementor&lt;/strong&gt; — had generated CSS files on disk at &lt;code&gt;wp-content/uploads/elementor/google-fonts/css/&lt;/code&gt; with &lt;code&gt;http://&lt;/code&gt; URLs hardcoded, not regenerated after migration&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An image optimization plugin&lt;/strong&gt; — had a WebP cache directory full of references to the old domain&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The fix required using WP-CLI's search-replace with the &lt;code&gt;--precise&lt;/code&gt; flag, which handles serialized data correctly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;wp search-replace &lt;span class="s1"&gt;'mysite.staging.temphost.link'&lt;/span&gt; &lt;span class="s1"&gt;'mysite.com'&lt;/span&gt; &lt;span class="nt"&gt;--precise&lt;/span&gt; &lt;span class="nt"&gt;--all-tables&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For the Elementor font CSS files on disk, a direct &lt;code&gt;find&lt;/code&gt; + &lt;code&gt;sed&lt;/code&gt; was needed since WP-CLI doesn't touch files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;find /var/www/mysite/wp-content/uploads/elementor/google-fonts/css/ &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s2"&gt;"*.css"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-exec&lt;/span&gt; &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s1"&gt;'s|http://mysite.staging.temphost.link|https://mysite.com|g'&lt;/span&gt; &lt;span class="o"&gt;{}&lt;/span&gt; &lt;span class="se"&gt;\;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Problem 2: Missing SSL Config in wp-config.php
&lt;/h3&gt;

&lt;p&gt;Despite the site running on HTTPS, Elementor kept generating &lt;code&gt;http://&lt;/code&gt; asset URLs. The reason was that &lt;code&gt;wp-config.php&lt;/code&gt; was missing two lines that tell WordPress it's behind HTTPS:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$_SERVER&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'HTTPS'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'on'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nb"&gt;define&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'FORCE_SSL_ADMIN'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without these, WordPress doesn't know the request came in over SSL (especially behind a proxy or load balancer), so dynamic URLs default to &lt;code&gt;http://&lt;/code&gt;. A subtle issue that caused a surprisingly large number of mixed content problems.&lt;/p&gt;

&lt;h3&gt;
  
  
  Problem 3: A Security Plugin Blocking Its Own Admin
&lt;/h3&gt;

&lt;p&gt;One of the installed security plugins had added a &lt;code&gt;.htaccess&lt;/code&gt; rule that was blocking all PHP file access:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight apache"&gt;&lt;code&gt;&lt;span class="nc"&gt;RewriteRule&lt;/span&gt; ^.*\.php$ - [F,L,NC]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This rule was intended to block direct PHP file execution in upload directories. But it was placed at the wrong level — it blocked &lt;code&gt;wp-admin&lt;/code&gt;, &lt;code&gt;wp-login.php&lt;/code&gt;, and every other PHP file on the site. The admin panel was completely inaccessible. The fix was removing that rule from &lt;code&gt;.htaccess&lt;/code&gt; manually via SSH before anything else could be done.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Attack: Day One on the VPS
&lt;/h2&gt;

&lt;p&gt;The migration was complete. The site was live. And then within hours, the server was at &lt;strong&gt;700%+ CPU&lt;/strong&gt; — that's 7 full cores pinned on a machine that should have been comfortably handling the traffic.&lt;/p&gt;

&lt;p&gt;On managed hosting, this had never happened. Not because the traffic wasn't there — but because the managed host was absorbing it silently. Now it was hitting our VPS directly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Attacker 1: Malicious Bots
&lt;/h3&gt;

&lt;p&gt;Access log analysis revealed two suspicious IPs hammering the site:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s2"&gt;"111.88.x.x"&lt;/span&gt; /var/log/ols/mysite.com_access.log | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'{print $7}'&lt;/span&gt; | &lt;span class="nb"&gt;sort&lt;/span&gt; | &lt;span class="nb"&gt;uniq&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; | &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="nt"&gt;-rn&lt;/span&gt; | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-30&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both IPs were sending dozens of requests per minute to &lt;code&gt;wp-admin/admin-ajax.php&lt;/code&gt;, WooCommerce AJAX endpoints, and Contact Form 7 REST endpoints — the fingerprint of bots probing for vulnerabilities and scraping data.&lt;/p&gt;

&lt;p&gt;Fix: blocked in &lt;code&gt;.htaccess&lt;/code&gt; at the top of the file, before any WordPress rules:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight apache"&gt;&lt;code&gt;&lt;span class="c"&gt;# Block malicious IPs&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nl"&gt;RequireAll&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;
&lt;/span&gt;    &lt;span class="nc"&gt;Require&lt;/span&gt; &lt;span class="ss"&gt;all&lt;/span&gt; granted
    &lt;span class="nc"&gt;Require&lt;/span&gt; not ip 111.88.x.x
    &lt;span class="nc"&gt;Require&lt;/span&gt; not ip 45.77.x.x
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nl"&gt;RequireAll&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Attacker 2: The Meta Crawler (The Real Problem)
&lt;/h3&gt;

&lt;p&gt;Blocking the malicious IPs brought load down — but not to safe levels. Something else was still hammering the server. Back to the access logs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s2"&gt;"meta-externalagent"&lt;/span&gt; /var/log/ols/mysite.com_access.log | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'{print $7}'&lt;/span&gt; | &lt;span class="nb"&gt;sort&lt;/span&gt; | &lt;span class="nb"&gt;uniq&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; | &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="nt"&gt;-rn&lt;/span&gt; | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-30&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Facebook's Meta crawler (&lt;code&gt;meta-externalagent/1.1&lt;/code&gt;) was systematically crawling every combination of the WooCommerce shop's filter URLs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/shop/?filter_color=red&amp;amp;filter_size=small
/shop/?filter_color=red&amp;amp;filter_size=medium
/shop/?filter_color=blue&amp;amp;filter_size=small
... hundreds of unique combinations
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here's why this is catastrophic for a WooCommerce + WPML site:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Every unique URL is a cache miss.&lt;/strong&gt; LSCache serves cached pages instantly with zero PHP. But a cached page is keyed by URL. Each filter combination is a different URL — so each one bypasses the cache entirely, boots WordPress, boots WooCommerce, boots WPML, runs a database query, and renders a response. The crawler was generating thousands of cache misses per hour.&lt;/p&gt;

&lt;p&gt;The fix — immediate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight apache"&gt;&lt;code&gt;&lt;span class="c"&gt;# Block Meta crawler&lt;/span&gt;
&lt;span class="nc"&gt;RewriteEngine&lt;/span&gt; &lt;span class="ss"&gt;On&lt;/span&gt;
&lt;span class="nc"&gt;RewriteCond&lt;/span&gt; %{HTTP_USER_AGENT} meta-externalagent [NC]
&lt;span class="nc"&gt;RewriteRule&lt;/span&gt; ^ - [F,L]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After adding this rule and restarting OLS:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;load average: 0.94 — 4 lsphp processes
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From 700%+ CPU to essentially idle. The Meta crawler was the primary load driver the entire time.&lt;/p&gt;

&lt;p&gt;The longer-term fix: add the shop's filter URL pattern to &lt;code&gt;robots.txt&lt;/code&gt; so crawlers stop attempting them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="n"&gt;User&lt;/span&gt;-&lt;span class="n"&gt;agent&lt;/span&gt;: &lt;span class="n"&gt;meta&lt;/span&gt;-&lt;span class="n"&gt;externalagent&lt;/span&gt;
&lt;span class="n"&gt;Disallow&lt;/span&gt;: /

&lt;span class="n"&gt;User&lt;/span&gt;-&lt;span class="n"&gt;agent&lt;/span&gt;: *
&lt;span class="n"&gt;Disallow&lt;/span&gt;: /&lt;span class="n"&gt;shop&lt;/span&gt;/?*
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  What Managed Hosting Was Silently Doing
&lt;/h2&gt;

&lt;p&gt;This is the part that changed how we think about managed hosting.&lt;/p&gt;

&lt;p&gt;The managed host had several layers we never thought about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Bot filtering at the edge&lt;/strong&gt; — known bad actors and aggressive crawlers were blocked before they reached WordPress at all&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DDoS mitigation&lt;/strong&gt; — volumetric attacks were absorbed by the host's network layer&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rate limiting&lt;/strong&gt; — aggressive crawlers were throttled automatically&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this was documented prominently. It was just... happening. Moving to a raw VPS removed all of it at once. The site went from being behind a shield to being fully exposed to the internet with only &lt;code&gt;.htaccess&lt;/code&gt; and OLS between it and every bot on the planet.&lt;/p&gt;

&lt;p&gt;The visibility was exactly what we wanted — we could finally see everything. But we also now had to handle everything ourselves.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Permanent Solution: Cloudflare
&lt;/h2&gt;

&lt;p&gt;Fixing bots with &lt;code&gt;.htaccess&lt;/code&gt; rules is whack-a-mole. Block one IP, another appears. Block one user agent, it rotates. The real fix is a layer in front of the server that handles this at scale before it ever reaches OLS.&lt;/p&gt;

&lt;p&gt;Cloudflare's free plan provides:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Bot Fight Mode&lt;/strong&gt; — automatically identifies and blocks known bot fingerprints&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rate limiting&lt;/strong&gt; — caps any single IP's request rate before it can cause load spikes
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Edge caching&lt;/strong&gt; — WooCommerce shop pages can be cached at Cloudflare's edge, so even cache misses on the origin become cache hits at the CDN level&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Real-time traffic analytics&lt;/strong&gt; — finally, full visibility into what's hitting the site and from where&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The architecture after adding Cloudflare:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Internet → Cloudflare edge (bot filtering, rate limiting, CDN cache)
         → VPS / OpenLiteSpeed (LSCache, Redis)
         → WordPress / WooCommerce / WPML
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bad traffic is rejected at Cloudflare before it touches the server. The PHP worker pool stays available for real users.&lt;/p&gt;

&lt;p&gt;This is what managed hosting was providing implicitly. Cloudflare makes it explicit, configurable, and visible — and the free tier handles the vast majority of what a typical WooCommerce store needs.&lt;/p&gt;




&lt;h2&gt;
  
  
  Additional Server Hardening After Stabilization
&lt;/h2&gt;

&lt;p&gt;With the immediate crisis resolved, we locked down the remaining attack surface:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Security headers via OLS:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight apache"&gt;&lt;code&gt;X-Frame-Options: SAMEORIGIN
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Strict-Transport-Security: max-age=31536000; includeSubDomains
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;PHP worker limits&lt;/strong&gt; — OLS was configured with a hard cap on concurrent PHP workers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;maxConns 8
&lt;span class="nv"&gt;PHP_LSAPI_CHILDREN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;8
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without this cap, a flood of requests spawns unlimited PHP workers and exhausts RAM. With the cap, excess requests queue rather than spawning new processes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Snapshot immediately after stabilization&lt;/strong&gt; — once the server was clean and stable, we took a VPS snapshot as a known-good baseline. If anything goes wrong in future, rollback is one click.&lt;/p&gt;




&lt;h2&gt;
  
  
  Lessons Learned
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Managed hosting hides its value until it's gone.&lt;/strong&gt;&lt;br&gt;
The bot filtering, DDoS mitigation, and crawler management that managed hosts provide are rarely documented but deeply valuable. Budget for replacing that capability explicitly when moving to a VPS.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Always use &lt;code&gt;--precise&lt;/code&gt; for search-replace on migrated WordPress sites.&lt;/strong&gt;&lt;br&gt;
Standard search-replace corrupts serialized data. The &lt;code&gt;--precise&lt;/code&gt; flag in WP-CLI handles it correctly. Make it a default step in every migration checklist.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Cloudflare is not optional for a self-managed WooCommerce store.&lt;/strong&gt;&lt;br&gt;
Put it in front on day one. Not after you've been attacked. The free plan covers the essentials, and the visibility alone is worth it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. WooCommerce filter URLs are a crawler trap.&lt;/strong&gt;&lt;br&gt;
Any WooCommerce store with faceted filtering generates effectively infinite unique URLs. Configure LSCache to ignore query strings on shop pages, and disallow filter URL patterns in &lt;code&gt;robots.txt&lt;/code&gt; before crawlers index them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Access logs are your best friend on a VPS.&lt;/strong&gt;&lt;br&gt;
The moment something goes wrong, SSH in and read the access log. The answer is almost always there — which IPs, which user agents, which URLs, how many requests per minute. On managed hosting, you often can't do this. On a VPS, it's the first thing you reach for.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Outcome
&lt;/h2&gt;

&lt;p&gt;The site runs more reliably now on the VPS than it ever did on managed hosting — and at significantly lower cost. Load averages stay under 1.0 under normal traffic. The Cloudflare layer handles bot traffic before it reaches the server. LSCache and Redis handle the WordPress-level caching. And when something goes wrong, we can actually see it.&lt;/p&gt;

&lt;p&gt;The migration pain was real. But it was a one-time cost that permanently increased visibility, control, and resilience. The managed host was comfortable — but comfort was masking problems we couldn't see or fix.&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>woocommerce</category>
      <category>devops</category>
      <category>security</category>
    </item>
    <item>
      <title>Sorting a Bricks query loop by a custom field chosen in a JetSmartFilters radio filter</title>
      <dc:creator>Muazzam</dc:creator>
      <pubDate>Sat, 25 Apr 2026 02:45:19 +0000</pubDate>
      <link>https://dev.to/muazzami/how-to-filter-and-sort-posts-by-custom-field-value-using-jetsmartfilters-bricks-builder-57an</link>
      <guid>https://dev.to/muazzami/how-to-filter-and-sort-posts-by-custom-field-value-using-jetsmartfilters-bricks-builder-57an</guid>
      <description>&lt;p&gt;I needed a sidebar radio list of custom field names (for example "Weight", "Length", "Rating") on a listing built with Bricks Builder, JetEngine and JetSmartFilters (JSF). When a user picks one, the listing should only show posts that have a value for that field, sorted highest to lowest by it, and any other active filters (taxonomy, search) should keep working alongside it.&lt;/p&gt;

&lt;p&gt;JSF has a Sort widget and a Radio filter widget, but they work independently. Picking a radio value doesn't trigger a sort, and there's no setting for "when this value is selected, sort by that meta key". So I used the radio filter only to pass a plain value to the server, and changed the query in PHP through Bricks' &lt;code&gt;bricks/posts/query_vars&lt;/code&gt; filter. No JavaScript needed.&lt;/p&gt;

&lt;p&gt;The setup: a Bricks query loop with a JSF Query ID, a JetEngine CPT whose custom fields are all numeric, and a JSF Radio filter next to the existing taxonomy filters on the same page.&lt;/p&gt;

&lt;h2&gt;
  
  
  The radio filter
&lt;/h2&gt;

&lt;p&gt;In JSF, create a Radio filter:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Data Source: Manual Input&lt;/li&gt;
&lt;li&gt;Options List: one entry per field. Label is the display name, Value is the meta key.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Label: Weight       Value: weight
Label: Length       Value: length
Label: Rating       Value: rating
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;This filter for: Bricks query loop&lt;/li&gt;
&lt;li&gt;Query ID: the same ID set on your Bricks loop element (I used &lt;code&gt;my-listing&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Apply on: Value change&lt;/li&gt;
&lt;li&gt;Query Variable: &lt;code&gt;_plain_query::sort_by_field&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;code&gt;_plain_query::&lt;/code&gt; prefix tells JSF to pass the value through as a plain query variable instead of trying to build its own meta query from it.&lt;/p&gt;

&lt;p&gt;On the Bricks side, select the query loop element and set the Query ID in the JSF settings panel (JSF adds the field once the element is marked as filterable). Every JSF filter on the page needs the same ID in "This filter for".&lt;/p&gt;

&lt;h2&gt;
  
  
  The hook
&lt;/h2&gt;

&lt;p&gt;This goes in the theme's &lt;code&gt;functions.php&lt;/code&gt; or a site-specific plugin.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nf"&gt;add_filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'bricks/posts/query_vars'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$query_vars&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$settings&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$element_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$element_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="c1"&gt;// Target only your specific Bricks loop by its JSF Query ID&lt;/span&gt;
    &lt;span class="c1"&gt;// Note: Bricks stores this under 'jsfb_query_id', not 'query_id'&lt;/span&gt;
    &lt;span class="nv"&gt;$query_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$settings&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'jsfb_query_id'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$query_id&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="s1"&gt;'my-listing'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$query_vars&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// JSF sends filter data via POST during AJAX requests&lt;/span&gt;
    &lt;span class="c1"&gt;// The _plain_query:: prefix becomes _plain_query_ in the POST key&lt;/span&gt;
    &lt;span class="nv"&gt;$query_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$_POST&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'query'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
    &lt;span class="nv"&gt;$sort_field&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sanitize_key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$query_data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'_plain_query_sort_by_field'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;empty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$sort_field&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$query_vars&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// Whitelist allowed field keys, never trust user input directly&lt;/span&gt;
    &lt;span class="nv"&gt;$allowed_fields&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'weight'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'length'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'rating'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt; &lt;span class="c1"&gt;// add all your fields&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;in_array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$sort_field&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$allowed_fields&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$query_vars&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// Filter: only show posts that have a non-empty value for this field&lt;/span&gt;
    &lt;span class="nv"&gt;$query_vars&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'meta_query'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s1"&gt;'relation'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'AND'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="s1"&gt;'key'&lt;/span&gt;     &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$sort_field&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'compare'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'EXISTS'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="s1"&gt;'key'&lt;/span&gt;     &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$sort_field&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'value'&lt;/span&gt;   &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'compare'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'!='&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;];&lt;/span&gt;

    &lt;span class="c1"&gt;// Sort: numeric descending by the selected field&lt;/span&gt;
    &lt;span class="nv"&gt;$query_vars&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'orderby'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'meta_value_num'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nv"&gt;$query_vars&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'order'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'DESC'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nv"&gt;$query_vars&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'meta_key'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$sort_field&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$query_vars&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;99&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// Priority 99 matters, see below&lt;/span&gt;


&lt;span class="c1"&gt;// Register the query var so WordPress doesn't strip it&lt;/span&gt;
&lt;span class="nf"&gt;add_filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'query_vars'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$vars&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$vars&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'sort_by_field'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$vars&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The whitelist is not optional. The meta key comes from the request, and without the check anyone could sort or filter your listing by any meta key in the database.&lt;/p&gt;

&lt;h2&gt;
  
  
  What made it fail first
&lt;/h2&gt;

&lt;p&gt;The code above is short. Getting there took longer, and every failure had the same shape: the hook ran fine and did nothing, with no error anywhere.&lt;/p&gt;

&lt;h3&gt;
  
  
  The settings key is &lt;code&gt;jsfb_query_id&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Inside the hook, Bricks stores the JSF Query ID under &lt;code&gt;jsfb_query_id&lt;/code&gt;, not &lt;code&gt;query_id&lt;/code&gt;. Checking &lt;code&gt;query_id&lt;/code&gt; always returns empty, so the hook skips every request. I only found it by dumping the whole settings array:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nf"&gt;add_filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'bricks/posts/query_vars'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$query_vars&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$settings&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$element_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$element_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nb"&gt;error_log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;print_r&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$settings&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$query_vars&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Filter values arrive in POST
&lt;/h3&gt;

&lt;p&gt;When a user clicks a filter, JSF sends an AJAX request and the values are in &lt;code&gt;$_POST['query']&lt;/code&gt;. Reading &lt;code&gt;$_GET&lt;/code&gt; gives you nothing on filter interactions.&lt;/p&gt;

&lt;h3&gt;
  
  
  The prefix changes the key name
&lt;/h3&gt;

&lt;p&gt;With the Query Variable set to &lt;code&gt;_plain_query::sort_by_field&lt;/code&gt;, the key in the POST payload is &lt;code&gt;_plain_query_sort_by_field&lt;/code&gt;. The &lt;code&gt;::&lt;/code&gt; becomes &lt;code&gt;_&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  JSF overwrites you at default priority
&lt;/h3&gt;

&lt;p&gt;This was the most painful one. At the default priority of &lt;code&gt;10&lt;/code&gt;, JSF runs its own &lt;code&gt;bricks/posts/query_vars&lt;/code&gt; callback after mine and resets &lt;code&gt;orderby&lt;/code&gt; back to &lt;code&gt;date&lt;/code&gt;, so the code looks correct and the order is still wrong. Registering at &lt;code&gt;99&lt;/code&gt; makes my hook run last, so it has the final say on the query arguments.&lt;/p&gt;

&lt;p&gt;Also declare all four parameters (&lt;code&gt;$query_vars&lt;/code&gt;, &lt;code&gt;$settings&lt;/code&gt;, &lt;code&gt;$element_id&lt;/code&gt;, &lt;code&gt;$element_name&lt;/code&gt;) and pass &lt;code&gt;4&lt;/code&gt; as the last argument to &lt;code&gt;add_filter&lt;/code&gt;. &lt;code&gt;$element_name&lt;/code&gt; was added in Bricks 1.11.1.&lt;/p&gt;

&lt;h2&gt;
  
  
  The full flow
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User clicks radio → "Weight"
        ↓
JSF fires AJAX: POST query[_plain_query_sort_by_field] = weight
        ↓
bricks/posts/query_vars hook fires (priority 99)
        ↓
Hook reads _plain_query_sort_by_field from $_POST['query']
Validates against whitelist
Injects meta_query (EXISTS + not empty)
Injects orderby=meta_value_num, order=DESC, meta_key=weight
        ↓
Bricks executes WP_Query with modified args
        ↓
Results: only posts WITH a weight value, sorted highest → lowest
Other active filters (taxonomy, search) remain stacked, unaffected
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Debugging it
&lt;/h2&gt;

&lt;p&gt;When this kind of integration does nothing, I log each step so I can see exactly where the chain breaks instead of guessing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nf"&gt;add_filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'bricks/posts/query_vars'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$query_vars&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$settings&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$element_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$element_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="nb"&gt;error_log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'HOOK FIRED, element: '&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$element_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nb"&gt;error_log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'jsfb_query_id: '&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$settings&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'jsfb_query_id'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="s1"&gt;'NOT SET'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

    &lt;span class="nv"&gt;$query_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$_POST&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'query'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
    &lt;span class="nb"&gt;error_log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'POST query: '&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nb"&gt;print_r&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$query_data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

    &lt;span class="nv"&gt;$sort_field&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sanitize_key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$query_data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'_plain_query_sort_by_field'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nb"&gt;error_log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'sort_field extracted: '&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$sort_field&lt;/span&gt; &lt;span class="o"&gt;?:&lt;/span&gt; &lt;span class="s1"&gt;'EMPTY'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

    &lt;span class="c1"&gt;// ... rest of your logic&lt;/span&gt;

    &lt;span class="nb"&gt;error_log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Final query_vars: '&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nb"&gt;print_r&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$query_vars&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$query_vars&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;99&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With &lt;code&gt;WP_DEBUG_LOG&lt;/code&gt; enabled the output goes to &lt;code&gt;/wp-content/debug.log&lt;/code&gt;. Check it after each filter click. If "HOOK FIRED" shows up but &lt;code&gt;jsfb_query_id&lt;/code&gt; is empty, it's the key name. If the final &lt;code&gt;query_vars&lt;/code&gt; look right but the order is still wrong, something is running after you, which in my case was JSF at a later priority.&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>php</category>
      <category>webdev</category>
      <category>bricksbuilder</category>
    </item>
    <item>
      <title>How to Use the `bricks/form/custom_action` Hook in Bricks Builder</title>
      <dc:creator>Muazzam</dc:creator>
      <pubDate>Sat, 25 Apr 2026 02:41:00 +0000</pubDate>
      <link>https://dev.to/muazzami/how-to-use-the-bricksformcustomaction-hook-in-bricks-builder-1ca8</link>
      <guid>https://dev.to/muazzami/how-to-use-the-bricksformcustomaction-hook-in-bricks-builder-1ca8</guid>
      <description>&lt;h1&gt;
  
  
  How to Use the &lt;code&gt;bricks/form/custom_action&lt;/code&gt; Hook in Bricks Builder
&lt;/h1&gt;

&lt;p&gt;You can run custom PHP code after a Bricks Builder form submission using the &lt;code&gt;bricks/form/custom_action&lt;/code&gt; hook. This is ideal if you need to send emails, save entries, or connect with external services.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Set Up the Form Action
&lt;/h2&gt;

&lt;p&gt;In your Bricks Builder form settings, go to "Actions after successful form submit" and select &lt;strong&gt;Custom&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Write Your PHP Logic
&lt;/h2&gt;

&lt;p&gt;Add this code to your theme's &lt;code&gt;functions.php&lt;/code&gt; file or use a WordPress code snippets plugin.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Hook your function to the Bricks custom action&lt;/span&gt;
&lt;span class="nf"&gt;add_action&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'bricks/form/custom_action'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'my_form_custom_action'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;my_form_custom_action&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$form&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Get all submitted form fields as an array&lt;/span&gt;
    &lt;span class="nv"&gt;$fields&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$form&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;get_fields&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// Example: ['name' =&amp;gt; 'Jane', 'email' =&amp;gt; 'jane@example.com']&lt;/span&gt;

    &lt;span class="c1"&gt;// Always check the form ID so your code only runs for the intended form&lt;/span&gt;
    &lt;span class="nv"&gt;$form_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;isset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$fields&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'formId'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="nv"&gt;$fields&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'formId'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$form_id&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="s1"&gt;'replace_with_your_form_id'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Not the intended form — stop here&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Custom logic goes here!&lt;/span&gt;
    &lt;span class="c1"&gt;// Example: integrate with an API, save a post, or trigger a notification&lt;/span&gt;

    &lt;span class="c1"&gt;// Set a user-facing message after submit&lt;/span&gt;
    &lt;span class="nv"&gt;$form&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;set_result&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
        &lt;span class="s1"&gt;'action'&lt;/span&gt;  &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'my_custom_action'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'type'&lt;/span&gt;    &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'success'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// 'success', 'error', or 'info'&lt;/span&gt;
        &lt;span class="s1"&gt;'message'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;esc_html__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Custom action executed!'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'your-textdomain'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Helpful Notes for Junior Devs
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Always check &lt;code&gt;formId&lt;/code&gt;&lt;/strong&gt; — This ensures your code only runs for the correct form, especially if you use custom actions on more than one form.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;$form-&amp;gt;get_fields()&lt;/code&gt;&lt;/strong&gt; returns every value submitted by the user. Use it to process, validate, or forward data to external tools.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;$form-&amp;gt;set_result()&lt;/code&gt;&lt;/strong&gt; controls what users see after submitting. Always provide friendly messages and handle errors gracefully.&lt;/li&gt;
&lt;li&gt;For external integrations (webhooks, APIs), use WordPress's built-in &lt;code&gt;wp_remote_post()&lt;/code&gt; rather than raw curl.&lt;/li&gt;
&lt;li&gt;Replace all placeholder form IDs and field keys with the actual values from your Bricks form setup.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Example: Creating a WordPress Comment from Form Submission
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nf"&gt;add_action&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'bricks/form/custom_action'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'create_comment_from_form'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;create_comment_from_form&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$form&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$fields&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$form&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;get_fields&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="c1"&gt;// Only execute for the intended form&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$fields&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'formId'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="s1"&gt;'myformid'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// Prepare comment data from submitted fields&lt;/span&gt;
    &lt;span class="nv"&gt;$comment_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s1"&gt;'comment_post_ID'&lt;/span&gt;      &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$fields&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'postid'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="s1"&gt;'comment_author'&lt;/span&gt;       &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$fields&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'name'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="s1"&gt;'comment_author_email'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$fields&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'email'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="s1"&gt;'comment_content'&lt;/span&gt;      &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$fields&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'comment'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="s1"&gt;'comment_approved'&lt;/span&gt;     &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;];&lt;/span&gt;

    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;wp_insert_comment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;wp_slash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$comment_data&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

        &lt;span class="nv"&gt;$form&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;set_result&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
            &lt;span class="s1"&gt;'action'&lt;/span&gt;  &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'my_custom_action'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'type'&lt;/span&gt;    &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'success'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'message'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;esc_html__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Comment created!'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'your-textdomain'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;]);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Exception&lt;/span&gt; &lt;span class="nv"&gt;$e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$form&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;set_result&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
            &lt;span class="s1"&gt;'action'&lt;/span&gt;  &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'my_custom_action'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'type'&lt;/span&gt;    &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'error'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'message'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;esc_html__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Something went wrong. Please try again.'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'your-textdomain'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;]);&lt;/span&gt;

        &lt;span class="c1"&gt;// Log the actual error server-side for debugging&lt;/span&gt;
        &lt;span class="nb"&gt;error_log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Bricks form error: '&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$e&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getMessage&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note on error messages:&lt;/strong&gt; Avoid exposing raw exception messages to users with &lt;code&gt;$e-&amp;gt;getMessage()&lt;/code&gt; — they can leak implementation details. Log errors server-side and show a generic message to the user instead.&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>wordpress</category>
      <category>bricksbuilder</category>
      <category>php</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
