Skip to content

Tag: fixing WordPress

  • Session Management in WordPress Is a Hard Nut to Crack

    I did not add session management to Guard Dog because it sounded like a nice security feature to have.

    I added it because I had already run into the problem in a very real way.

    Before Guard Dog‘s session management became a product feature, I built a very bespoke integration between WordPress and Omeda, a customer data platform with some challenging quirks, for a website with a global audience. That integration raised the stakes immediately. WordPress was no longer just rendering pages and handling logins. It was sitting in the middle of a live connection to reall customer data, including personally identifiable information, and that changed everything about how I thought about sessions.

    Once WordPress becomes a gateway into customer records representative of an actual person, session management stops being a nice-to-have. It becomes part of the security boundary.

    That experience is what pushed me to bring session management into Guard Dog.

    And the deeper I got into it, the more obvious it became that session management is one of the hardest problems to solve well in WordPress.

    Why This Became So Important to Me

    When I was working on that Omeda integration, the security concerns were not theoretical.

    The website served users across different regions, different networks, different devices, and different usage patterns. There was a real-time relationship between the WordPress layer and the CDP, and that meant a logged-in session could potentially expose or interact with sensitive customer information in ways that demanded a much higher standard of trust.

    In that kind of environment, you start asking different questions.

    Not just:

    • Is the user logged in?
    • Did WordPress set the auth cookie correctly?

    But also:

    • Can I see every active session?
    • Can I revoke a session remotely?
    • Can I detect when a session suddenly looks suspicious?
    • Can I expire inactive sessions with confidence?
    • Can I trust the IP information I am seeing?
    • Can I do all of that without breaking legitimate users on real infrastructure?

    Those questions are what led me here.

    WordPress Was Not Built Around Modern Session Management

    WordPress absolutely has authentication. It has cookies. It has session tokens. It has the pieces.

    But modern, security-focused session management was never really treated as a first-class product surface in the way it is in newer frameworks and platforms.

    That is not a knock on WordPress. It is just a reflection of when WordPress came into the world and what it originally needed to do well.

    WordPress grew up in a web that was simpler in a lot of ways. The hosting environments were simpler. The application expectations were simpler. The line between “content site” and “application” was clearer.

    That is not the world WordPress lives in now.

    Now WordPress is asked to behave like a secure application layer in front of APIs, identity systems, commerce stacks, customer platforms, and global user bases. We expect it to work across admin requests, frontend requests, AJAX, REST endpoints, object caches, reverse proxies, CDNs, WAFs, and multi-node environments.

    That is where session management gets hard.

    The Same Feature Has to Work Everywhere

    One of WordPress’s greatest strengths is that it can run almost anywhere.

    It can live on a bargain shared hosting plan for three dollars a month. It can also live in a highly customized environment with Kubernetes, multiple app nodes, load balancers, reverse proxies, Redis, Cloudflare, and all kinds of enterprise edge infrastructure in front of it.

    That flexibility is amazing.

    It is also exactly why building proper session management is so difficult.

    Because the moment you try to do more than the bare minimum, the environment starts mattering.

    A lot.

    A security feature like session management sounds simple at first. Track active sessions. Show them in the UI. Allow remote termination. Extend them when the user is active. Expire them when they are not. Flag suspicious changes.

    But every one of those actions depends on assumptions, and WordPress deployments are full of assumptions that do not hold consistently from one site to the next.

    The Infrastructure Makes It Messy

    This is the part that humbled me most.

    In theory, an IP change during a session might look suspicious.

    In reality, that could mean almost anything.

    It could mean the user changed networks.

    It could mean the site is behind Cloudflare.

    It could mean there is a load balancer in front of the app.

    It could mean forwarded headers are being handled incorrectly.

    It could mean the request is coming through a trusted proxy.

    It could mean the application is seeing infrastructure noise instead of the real client IP.

    And when you are dealing with a global audience, these edge cases get even more interesting. Users travel. Networks change. Mobile carriers route traffic in strange ways. Enterprise traffic can come through layers you do not control. Suddenly the line between suspicious behavior and normal behavior gets blurry.

    That is what makes session security hard. It is not just about catching bad behavior. It is about not misclassifying normal behavior as bad behavior.

    Aggressive security is easy.

    Correct security is hard.

    Legacy Patterns and Modern Expectations Collide

    WordPress is a platform with a long memory.

    When you build something like session management inside it, you are not starting from a clean slate. You are working across classic admin page loads, frontend navigation, background requests, AJAX, REST traffic, and plugin ecosystems that may hook into authentication in different ways.

    That means even something as simple as “keep the session alive while the user is active” is not always simple.

    What counts as activity?

    A page load?

    An admin click?

    A background request?

    A REST call from Gutenberg?

    An AJAX heartbeat?

    If the user is actively working but the wrong type of request is extending or validating the session, you can end up with exactly the kind of subtle, frustrating bugs that make session management feel unreliable.

    And those bugs are the worst kind, because they often only show up in real environments with real traffic patterns.

    I ran into exactly this recently on a site with session management enabled. A user working in the block editor was generating multiple requests to admin-ajax.php, and those perfectly legitimate editor-driven requests ended up falsely flagging the session as suspicious. That is the kind of issue that really captures the challenge for me. Nothing malicious was happening. The user was just doing normal editorial work. But in WordPress, especially where older request patterns and newer editor behavior overlap, “normal” can still look suspicious unless the session logic is extremely careful.

    And that is still an ongoing hurdle. It is one thing to say you want suspicious-session detection. It is another thing entirely to make that detection smart enough to understand how a real WordPress site behaves under modern usage.

    What I Wanted Guard Dog to Do

    When I brought session management into Guard Dog, I did not want to just add another checkbox feature.

    I wanted something that respected how messy WordPress can be in the real world.

    That meant building toward things like:

    • visibility into active sessions
    • remote session termination
    • inactivity timeouts
    • suspicious-session detection
    • IP-shift and geographic anomaly awareness
    • safer handling for reverse proxies and load balancers
    • more defensive behavior around stale state and infrastructure edge cases

    Just as importantly, I wanted to avoid the trap that a lot of security tooling falls into: becoming destructive in the name of being secure.

    In a plugin like this, it is very easy to terminate first and think later. It is easy to treat every inconsistency like compromise. It is easy to build logic that looks tough in theory but causes false logouts and broken trust in practice.

    That was never the goal.

    The goal was to build session management that is actually usable, actually protective, and actually realistic for WordPress.

    Why This Problem Still Matters

    The reason I keep coming back to this is that WordPress is no longer just a blogging platform or a CMS for brochure sites. For a lot of organizations, it is part of a much larger system. It connects to identity platforms, CRMs, CDPs, subscription systems, commerce systems, and internal services.

    That was exactly my experience with the Omeda integration.

    Once that connection exists, the quality of your session management matters more. If authenticated access can expose or influence customer data in real time, then session handling is no longer background plumbing. It becomes part of the product’s security posture.

    And if you want that posture to be strong, you cannot treat session management like an afterthought.

    I Am Close, But I Would Not Call It Solved

    That is also why I am careful not to overstate where things are today.

    I do think the session management feature in Guard Dog is good. I think I am close. But I would not honestly say it is fully solved yet.

    There are still edge cases I did not, and can not, anticipate. There are still real-world usage patterns that only show themselves once the feature is out on actual sites, with actual editors, admins, infrastructure layers, and traffic behavior that no clean development environment fully reproduces.

    That has probably been the most important lesson in all of this: session management is not the kind of feature you declare finished just because the core logic works. It gets better by being exposed to reality. The edge cases are not distractions from the feature. In many ways, they are what shape the feature into something mature.

    So while I am proud of how far Guard Dog’s session management has come, I also see it as something that is still being refined by the field. Every unexpected behavior, every false positive, and every unusual deployment pattern helps steer it toward being more accurate, more resilient, and more trustworthy.

    The Honest Answer

    If someone asks me why proper session management in WordPress is so hard, my honest answer is this:

    It is hard because WordPress carries legacy assumptions from an older web, while being expected to operate securely in a much more complex modern one.

    It is hard because WordPress runs everywhere.

    It is hard because infrastructure changes the meaning of signals like IP addresses and request patterns.

    It is hard because the same logic has to survive cheap shared hosting, custom enterprise stacks, reverse proxies, WAFs, CDNs, and global traffic.

    And it is hard because once WordPress is connected to systems holding real customer data and personally identifiable information, getting sessions wrong is no longer a small mistake.

    That is why this feature mattered enough for me to build.

    And that is why it has been such a hard nut to crack.

  • Guard Dog: Building the WordPress Security Tool I Actually Wanted to Use

    Guard Dog: Building the WordPress Security Tool I Actually Wanted to Use

    There’s a specific kind of clarity that comes from what I call “rage coding.”

    A while back, I started a high-stakes, custom WordPress integration for my employer. The project had unique security needs to keep personally identifiable information as well as financial information safe and secure. I had a specific list of security layers and features I needed that would have taken at least a few plugins and some money check all the boxes and even then – I knew there would inevitably be some custom code I would have to write.

    I’d been using a plugin called AIO Login for years, but mostly just to change the admin URL. When I looked into their “Pro” features to fill the gaps in my project, I hit a wall. I don’t begrudge anyone for wanting to get paid for their software, but the “freemium” shift in the WordPress ecosystem—where foundational tools like Pojo One Click Accessibility or Aryo Activity Log are sold off or locked behind subscriptions—started to wear on me.

    I realized I could either pay a nominal fee for a tool that mostly did what I wanted, or I could build the exact tool I needed.

    I chose the latter. Guard Dog is the result.

    The “Crown Jewel” of My Workflow

    Guard Dog has been in the official WordPress repository for about five months, following a six-month deep-dive in development. It is, without hyperbole, my “crown jewel.”

    I use it every single day. It’s installed on my personal sites (including this one), as well as sites that handle hundreds of thousands of users from a global audience each month. I depend on it to secure sites that handle personal data and actual revenue streams. Because I’m the primary user, I’m obsessive about its stability. If it breaks, any and all of the fallout is on me and that’s not something I take lightly at all.

    Built for Me, Shared with You

    I am a very particular person. I built Guard Dog to meet my “needs” first, and then I started adding “wants”—quality-of-life features that make things easier for both admins and end-users.

    Recently, I pushed a major update: Passkey support. I added this primarily because I wanted to use Passkeys on my own sites. It’s the gold standard for modern, passwordless security, and I didn’t think it was something users should have to pay a premium for.

    What’s currently under the hood:

    • Passkey Support: Secure, biometric, or hardware-key logins.
    • Session Management: See who is logged in and where, with the ability to kill sessions instantly.
    • Limit Login Attempts: Brute-force protection that actually works.
    • Temporary User Access: Grant secure, timed access to devs or support staff without permanent credentials.
    • 2FA & IP Whitelisting: The essentials for any hardened site.

    What’s Next: Reducing Friction

    The next phase of the project is focused on OAuth and Social Login.

    As a content consumer, I’ve come to expect “Login with Google”, “Login with Facebook” or some other platform provider. It’s fast, it’s easy, and it reduces the “password fatigue” we all feel. As an admin, it makes user acquisition much smoother. This is a pure quality-of-life play, and I’m currently mapping out the most secure way to bring that to the plugin.

    Why It’s Free (And Will Stay That Way)

    I’m not a salesperson, and this isn’t a pitch. I’m not looking to make money off this plugin. If you use Guard Dog and it helps you sleep better at night, that’s fantastic. If you don’t, that’s okay too.

    The internet runs on WordPress. Big companies have budgets for custom security, but the individual creator or the small team shouldn’t be penalized or left vulnerable because they don’t have (or didn’t think they would need) a budget for basic security.

    I built this for me, but I’m sharing it because a more secure WordPress ecosystem benefits everyone.


    Where to find it

  • Remove Unwanted Paragraph Tags Around Shortcodes in WordPress Widgets

    If you’ve ever created a custom WordPress shortcode and used it in a widget, you’ve probably encountered a frustrating issue: WordPress automatically wraps your shortcode output in empty <p> tags. These unwanted paragraph tags can break your layout, add unnecessary spacing, and make your carefully crafted HTML look messy.

    In this article, I’ll show you exactly why this happens and how to fix it with the correct filter priority.

    The Problem

    When you create a widget (particularly with the block editor’s Shortcode block) and add a simple shortcode like:

    [custom_shortcode src="#" size="medium"]

    You expect clean HTML output. Instead, WordPress wraps your content in paragraph tags like this:

    <section id="block-3" class="widget">
        <p></p>
        <div class="your-container-class" data-size="medium">
            <!-- Your shortcode content -->
        </div>
        <p></p>
    </section>

    Those empty <p></p> tags add unwanted margin and spacing, breaking your carefully designed layout.

    Why This Happens

    WordPress has a built-in filter called wpautop that automatically converts line breaks into HTML paragraph tags. This is great for blog content, but it becomes problematic when working with shortcodes that output block-level elements like <div> tags.

    The block editor’s Shortcode block processes content through several filters, including wpautop, which tries to be helpful by wrapping your content in paragraph tags. Even though your shortcode outputs a complete <div> element, WordPress still adds those <p> tags around it.

    The Solution: Custom Filter with Proper Priority

    The fix involves creating a custom filter that strips out these unwanted tags. But here’s the critical part: filter priority matters.

    Many developers try using WordPress’s built-in shortcode_unautop() function at the default priority, but this doesn’t work reliably with block-based widgets. The key is to run your cleaning filter at a very high priority (999) so it executes after all other filters have finished processing the content.

    Here’s the complete solution:

    class Your_Shortcode_Class {
    
        public function __construct() {
            add_shortcode( 'your_shortcode', [ $this, 'render_shortcode' ] );
    
            // Ensure shortcodes work in widgets
            add_filter( 'widget_text', 'do_shortcode', 11 );
    
            // Handle block-based widgets (Gutenberg)
            // Process shortcodes first, then clean up <p> tags at very high priority
            add_filter( 'widget_block_content', 'do_shortcode', 11 );
            add_filter( 'widget_block_content', [ $this, 'clean_widget_shortcode_output' ], 999 );
            add_filter( 'widget_text', [ $this, 'clean_widget_shortcode_output' ], 999 );
        }
    
        /**
         * Clean widget shortcode output by removing empty <p> tags
         *
         * @param string $content Widget content.
         * @return string Cleaned content.
         */
        public function clean_widget_shortcode_output( $content ) {
            // Only process if your shortcode container is present in the output
            if ( strpos( $content, 'your-container-class' ) === false ) {
                return $content;
            }
    
            // Remove empty <p> tags (including those with whitespace)
            $content = preg_replace( '/<p>(\s|&nbsp;)*<\/p>/i', '', $content );
    
            // Remove <p> tags that wrap our container divs
            $content = preg_replace( '/<p>(\s*)(<div[^>]*your-container-class[^>]*>)/i', '$2', $content );
            $content = preg_replace( '/(<\/div>)(\s*)<\/p>/i', '$1', $content );
    
            // Remove stray <br> tags around containers
            $content = preg_replace( '/<br\s*\/?>(\s*)(<div[^>]*your-container-class)/i', '$2', $content );
            $content = preg_replace( '/(<\/div>)(\s*)<br\s*\/?>/i', '$1', $content );
    
            return $content;
        }
    
        public function render_shortcode( $atts ) {
            // Your shortcode rendering logic
            return '<div class="your-container-class">Your content</div>';
        }
    }

    Understanding Filter Priority

    WordPress filters execute in order of priority, from lowest to highest numbers:

    1. Priority 10 (default): Most filters run here, including wpautop
    2. Priority 11: The do_shortcode filter processes shortcodes
    3. Priority 999: Our cleanup filter runs last

    If you run your cleanup filter at priority 10, it will execute before wpautop adds the paragraph tags, making it ineffective. By setting it to 999, we ensure it runs after all other filters have finished, giving us the final say on the output.

    Breaking Down the Regex Patterns

    The cleaning function uses multiple regex patterns instead of trying to match everything at once:

    1. Remove empty paragraphs: /<p>(\s|&nbsp;)*<\/p>/i catches <p></p>, <p> </p>, and <p>&nbsp;</p>
    2. Remove opening <p> before your container: /<p>(\s*)(<div[^>]*your-container-class[^>]*>)/i strips the opening paragraph tag that appears before your div
    3. Remove closing </p> after your container: /(<\/div>)(\s*)<\/p>/i removes the closing paragraph tag after your closing div
    4. Clean up break tags: Two patterns remove <br> tags that appear before or after your container

    This multi-pattern approach is more reliable than trying to match the entire wrapped structure in a single regex.

    Implementation Tips

    1. Replace the identifier: Change your-container-class to whatever unique class or attribute your shortcode uses. This ensures the filter only processes your specific shortcode output.
    2. Add to both filters: Apply the cleanup to both widget_block_content (Gutenberg blocks) and widget_text (classic text widgets) to cover all use cases.
    3. Test thoroughly: Check your widgets in different contexts – sidebars, footers, etc. – to ensure the cleanup works everywhere.
    4. Don’t forget do_shortcode: Make sure you’re enabling shortcode processing in widget_block_content at priority 11, before your cleanup runs at priority 999.

    Testing the Fix

    After implementing this solution, inspect your widget output in the browser’s developer tools. You should see clean HTML like this:

    <section id="block-3" class="widget">
        <div class="your-container-class">
            <!-- Your shortcode content -->
        </div>
    </section>

    No more empty paragraph tags, no more unexpected spacing!

    Conclusion

    WordPress’s automatic paragraph insertion is helpful for blog content but can wreak havoc on shortcode output in widgets. The solution isn’t just about adding a filter – it’s about understanding filter priority and ensuring your cleanup code runs at the right time.

    By running your cleanup filter at priority 999, you guarantee it executes after all other filters, giving you complete control over the final output. This simple change can save hours of debugging and ensure your shortcodes render exactly as intended.

    Have you encountered this issue in your WordPress projects? Let me know in the comments how you solved it!

  • WordPress 404 Fix: Why You Just Need to “Resave” Your Permalinks

    If you’ve ever moved a WordPress site, updated a major plugin, or added a Custom Post Type, you’ve probably encountered the dreaded “404 Not Found” error on pages that you know exist.

    And if you’ve searched for the fix, you know the most common, and seemingly illogical, solution: Go to Settings > Permalinks and simply click “Save Changes,” without actually changing anything.

    Why does this magic button press work? It’s not magic; it’s a necessary synchronization between WordPress and your web server. Here is a history of the issue, a simple explanation of why it happens, and how to fix it for good.


    Part 1: The Core Problem—A Mismatch in Maps

    To understand the fix, you first have to understand the job of a permalink.

    Permalinks: The Simple Address

    A “permalink” is just the permanent, clean, and readable URL (like /my-awesome-post/). When a visitor types this into their browser, two main things have to happen:

    1. The Server’s Job (Nginx/Apache): The server has to receive the request and figure out that the clean-looking URL actually needs to be processed by one specific file: the main WordPress script (usually `index.php).
    2. WordPress’s Job: Once WordPress takes the request, it looks at its internal rules to determine exactly whichpiece of content (Post ID 123, Custom Post Type “Sponsored Content”, etc.) matches the URL.

    The technical glue that makes step 1 work is a set of instructions called Rewrite Rules.

    The History of the Problem

    When WordPress was first developed, the default URLs were ugly, using IDs and question marks (e.g., ?p=123). As websites became more advanced and SEO became critical, WordPress needed a clean URL structure.

    The permalink feature was created to solve this. It allowed WordPress to generate those complex Rewrite Rulesautomatically. The problem that has persisted is that WordPress is not always perfect at telling the web server or its own database that a new rule is needed.

    This is where the mismatch happens:

    • WordPress knows the page exists.
    • The Server’s Routing Map (the Rewrite Rules) is either missing the rule or using an outdated version.

    Part 2: The Role of Custom Post Type Plugins

    While WordPress core sometimes causes these issues, they are far more common when using third-party tools to create custom content structures.

    Plugins like WCK (WordPress Creation Kit)Custom Post Type UI, or similar builders are fantastic for easily adding new content types (like “Sponsored Content,” “Case Studies,” or “Products”) without writing code.

    However, they introduce an additional step in the communication chain:

    The ActionThe Expected ResultThe Potential Breakdown
    You create a new CPTThe CPT Plugin tells WordPress to register a new route.The plugin fails to send the final signal to save the updated Master Route List.
    You edit a CPT slugThe plugin updates the page, but doesn’t touch the routes.WordPress core doesn’t get the signal to check the routes, and the new URL is stuck in limbo.

    This creates a communication gap. The plugin is being “too efficient”—it only updates the content, not the site-wide map. The result is the same: the server looks at the old, incomplete map and throws a 404 Not Found error.


    Part 3: Why the “Save Changes” Button Works

    When you click “Save Changes” on the Permalinks screen, you are performing a critical synchronization step known as “Flushing the Rewrite Rules.”

    Think of your website as a Librarian (WordPress) with a Master Catalog (the Database).

    The Fix (Why it Works)

    By clicking the “Save Changes” button, even if you change nothing, you are essentially bypassing the complex, multi-layered system and giving WordPress this simple, non-negotiable command:

    “Discard the current, potentially stale Rewrite Rules, recalculate the entire list from scratch based on everything I have—all my posts, pages, and custom types—and then force that fresh list into the database and server configuration.”

    This simple action forces synchronization and instantly corrects the outdated routing map, clearing up the 404 errors caused by core glitches or plugin communication issues.


    Part 4: The Troubleshooting Steps

    The “resave” trick works over 90% of the time. If it doesn’t, here are the next steps to ensure the fix is permanent.

    1. The Standard Permalink Fix (The First Step)

    1. Navigate to Settings > Permalinks in your WordPress dashboard.
    2. Note your current setting (e.g., “Post Name”).
    3. Click the Save Changes button.

    2. The Cache Flush (The Second Step)

    Sometimes the fix works, but your visitors (or you) are seeing an older version of the site saved by a caching system.

    • Clear all forms of caching on your site: Server-level cache, WordPress plugin cache (WP Rocket, LiteSpeed, etc.), and CDN cache (Cloudflare, etc.).

    3. The Plugin Conflict Check (The Root Cause)

    If you find you have to resave permalinks constantly, the CPT plugin itself might be the conflict point.

    1. Deactivate all plugins except the one causing the issue (e.g., your CPT builder).
    2. Try to reproduce the 404 error (e.g., by changing a page slug).
    3. If the error stops, reactivate your plugins one by one, checking for the 404 each time, until you isolate the offender.

    By understanding that this is simply a synchronization error, often exacerbated by the tools we use for efficiency, you can confidently explain the “magic” of the resave button and troubleshoot any future routing headaches.

  • Set Tag Order WordPress Plugin

    A few months ago, I was asked to solve what I thought was a simple problem. The question was, “Can we set the display order of tags?” It seemed like a very benign question until I started to dig into it and discovered that it’s, in fact, not a simple question at all. WordPress, by default, sorts tags alphabetically when you use the get_tags() function.

    The problem I was asked to create a solution for was to allow the editor to specify the order that tags are rendered on the corresponding post page. It was way more involed thatn I had anticipated but ultimately, I created a plugin that works with both the block editor and classic editor and should also work with any theme that displays tags using the get_tags() function.

    I’ve attached some screenshots to help illustrate but the plugin can be downloaded directly from my GitHub. I have made this public and welcome any and all feedback or feature requests!

    This plugin is now also listed in the WordPress plugin directory! You can download it from WordPress.org here or add it directly from your WordPress installation.