Skip to content

Category: 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.

  • Why I Built Lumenare Search: A Search Engine, Not a Search Theme

    Why I Built Lumenare Search: A Search Engine, Not a Search Theme

    If you’ve ever gone shopping for a WordPress search plugin, you know the drill. On one end, there’s the default WordPress search—which is “functional” in the same way a tricycle is a “vehicle.” On the other, you have premium plugins that offer incredible results but come buried under a mountain of configuration tabs you never asked for.

    I built Lumenare Search because I wanted the “Goldilocks” solution: a genuinely free, developer-friendly engine that does the heavy lifting of intelligent search without the “Pro” price tag or the administrative bloat.

    The Problem with “Pro” Search

    Commercial search plugins often follow the same playbook: pack in every conceivable toggle, custom CSS editor, and drag-and-drop builder to justify a premium license. If you’re a site owner who wants to build a UI from a settings panel, that’s great. But for developers, all that UI is just overhead.

    For quite some time, my “go-to” search plugin was always Ajax Search Pro. It’s a great plugin and I have nothing but good things to say about it and it’s developer. However, as the plugin matured and added more features, I found myself struggling to integrate the frontend search presentation into the custom themes I had been tasked with building.

    I kept hitting the same wall with Ajax Search Pro, I’d spend days fighting its built-in styles and themes to match my design. The plugin was trying to own the entire experience which is okay and not a criticism but I needed something I could work into my design so that it looked like it was supposed to be there. With Ajax Search Pro, I had to shoehorn a great search engine that didn’t quite look right into my custom themes and hope no one noticed. I wanted a search engine instead of a search theme.

    What’s Under the Hood?

    At its core, Lumenare Search swaps out the basic LIKE queries of default WordPress for a sophisticated, weighted keyword index. It prioritizes matches where they matter most—titles carry more weight than excerpts, and excerpts more than body content.

    But relevance is about more than just weights. I’ve included the features that actually move the needle on user experience:

    • Fuzzy Matching: Because humans are bad at typing. A search for “WordPres” still finds your content using Levenshtein distance.
    • Synonyms: Map “car” to “automobile” or “vehicle” so your users find what they need, even if they don’t use your specific vocabulary.
    • Stop Words: We strip out the noise (like “a,” “the,” and “is”) so the index stays lean and focused on meaningful keywords.
    • Dynamic Trending Ranking: Automatically surface the content that’s actually gaining traction on your site right now.
    • Actionable Analytics: Track zero-result queries to identify content gaps and see exactly what your users are looking for.

    The Developer-First Philosophy

    Here is the Lumenare difference: It stays out of your way. There is no visual builder. No custom CSS editor baked into your admin panel. The settings that exist are there to tune search behavior—match modes, weights, and logic.

    How the form looks and how the results are displayed is your domain. Whether you’re using the Gutenberg block, a shortcode, or a widget, Lumenare provides the data and the functionality; you provide the CSS and the theme integration. A search plugin should empower your frontend architecture, not dictate it.

    Free, Private, and Local

    I believe core search functionality shouldn’t be locked behind a paywall. There is no “Pro” version of Lumenare Search. No feature gating, no upsells, and no nag screens.

    More importantly, Lumenare Search is private by default. It runs entirely on your server. No external API calls, no third-party data transmission, and no cloud dependencies. Your index and your analytics live in your database. If you clear them, they’re gone.

    Ready to Try It?

    Lumenare Search is available now on the WordPress Plugin Repository.

    Install it, build your index, and see how it feels. If you want to dive deep into the technical weeds, check out our Getting Started Guide or our Weight Tuning Guide.

    If you have a feature request or run into a bug, catch me over at the Support Forums. Let’s make search better together.

  • 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.

  • Say Goodbye to chown: Fixing WordPress File Permissions in Docker on Windows with WSL

    Say Goodbye to chown: Fixing WordPress File Permissions in Docker on Windows with WSL

    If you’re developing WordPress themes or plugins locally on Windows using Docker inside of WSL, you’ve likely run into a frustrating and time-consuming problem: the constant battle with file permissions.

    One minute, you need to set your theme files to adam:adam so you can edit them in your IDE like PhpStorm or VS Code. The next, you need to switch them to www-data:www-data so that the web server can run a server-side process, like an automatic SCSS compiler. Then, to update a plugin from the WordPress admin panel, you have to switch the permissions back again.

    This constant chown shuffle is a major drag on productivity. I recently faced this exact scenario with a specific local development stack:

    • Host OS: Windows 11
    • Linux Environment: WSL (Ubuntu)
    • Containerization: Docker Desktop
    • WordPress Image: The official wordpress:latest
    • Workflow: WordPress files mounted from my local WSL filesystem into the container.

    The goal was simple: edit files as my local user while allowing the WordPress container to perform its necessary file operations without constant permission changes. After some trial and error, I landed on a clean, permanent solution. This fix has been tested for the environment above and works perfectly.

    The Core of the Problem: A User ID Mismatch

    The friction comes from a fundamental conflict between users. On your WSL instance, your user (adam in my case) owns the project files. This user has a specific User ID (UID) and Group ID (GID), which are typically 1000 and 1000 for the default user on most Linux distributions.

    However, inside the official wordpress:latest Docker container, the Apache web server runs as the www-data user by default. This www-data user has a different UID and GID (usually 33). When WordPress needs to write a file—like scssphp compiling a stylesheet or a plugin being updated—it does so as www-data. Since the UIDs don’t match, you get permission errors unless you manually change the file owner to www-data.

    Worse yet, I noticed that on some occasions, a process would create a file owned by root, adding yet another layer of complexity. The solution is to stop juggling users and just make them the same.

    The Solution: Synchronize Your User with the Container

    The most robust fix is to tell Docker to run the Apache process inside the container using your own user’s UID and GID. This way, from the file system’s perspective, your local user and the web server user are identical. Any file you create can be read/written by the server, and any file the server creates is owned by you.

    Here’s how to do it in two simple steps.

    Step 1: Find Your User and Group ID in WSL

    First, you need to find the UID and GID of your user inside your WSL terminal. It’s almost certainly 1000:1000, but it’s always best to verify.

    Open your WSL terminal and run the following command:

    Bash

    id -u && id -g
    

    This will print your UID and GID. Take note of these numbers.

    Step 2: Update Your docker-compose.yml

    Next, open the docker-compose.yml file in your project root. We are going to add a single, powerful directive to your WordPress service configuration: user.

    Find the service definition for your WordPress container and add the line user: "1000:1000", replacing 1000:1000 with your actual UID and GID if they are different.

    Here is a before-and-after example:

    Before:

    YAML

    version: '3.8'
    services:
      wordpress:
        image: wordpress:latest
        volumes:
          - ./wp-content:/var/www/html/wp-content
          # other configurations...
        ports:
          - "8080:80"
        restart: always
    

    After:

    YAML

    version: '3.8'
    services:
      wordpress:
        image: wordpress:latest
        user: "1000:1000" # Add this line!
        volumes:
          - ./wp-content:/var/www/html/wp-content
          # other configurations...
        ports:
          - "8080:80"
        restart: always
    

    Step 3: Rebuild and Verify

    Save your docker-compose.yml file. Now, stop and rebuild your container to apply the change. Run this command from your project directory in the WSL terminal:

    Bash

    docker-compose down && docker-compose up -d --build
    

    Once the container is running, you can verify that it’s working. Have WordPress perform an action that creates a file. In my case, I deleted my old compiled CSS file and reloaded a page, which triggered the scssphp compiler.

    Then, check the file’s ownership in your terminal:

    Bash

    ls -l wp-content/themes/your-theme/path/to/main.css
    

    The owner should now be your local user (adam adam). Success! You can now edit the file, and the server can still overwrite it when it recompiles, with zero permission conflicts.

    A Smoother Path Forward

    By synchronizing the container’s user with your local user, you eliminate the source of the permission conflict entirely. This simple, one-line change to your docker-compose.yml creates a seamless and efficient local development workflow. No more context switching, no more running chown—just smooth, productive coding.

    Disclaimer: This solution has been specifically tested and confirmed to work with a development environment running on Windows, using Docker Desktop with the WSL backend, and running the official wordpress:latest image with locally mounted files. While the principles apply more broadly, the exact implementation may differ in other environments.

  • I published an accessibility plugin called Open Accessibility!

    Within the last couple of months, I was updating plugins for one of my employer’s WordPress sites. The accessibility plugin that I had been using and that had been an incredibly popular plugin had been called One Click Accessibility. There was a major version update from 2.x to 3 and when I installed it, I was met with a wildly different plugin. Long story short, the plugin was sold to Elementor and then went commercial.

    This really, really pissed me off. I could have just stayed on version 2 of the plugin perpetually but so many other people got screwed as well. There are many other accessibility plugins available in the WordPress plugin repository but I decided to make my own out of spite.

    Accessibility platforms like UserWay and Accessibe are prevalent and big business but while they will give you a widget or something to add to your site to assist those who need with things like text size, color contrast and the like, what they are selling is a “cover your ass” service in the even that you get sued. They aren’t marketing a product to help people, they are selling fear and then a handy peace of mind solution to those who think they need it.

    I don’t know, I have a fundamental problem with making money directly or indirectly from people who have a disability or who are differently-abled. The argument can be made that commercial accessibility offerings aren’t protecting site owners from people with disabilities but rather the trolls that will sue anyone for anything and then offer a settlement to make a quick, dirty buck. Those people exist and they suck but site owners that genuinely give a shit about their users should have options a-plenty.

    The project was always intended to be open source and while it can’t provide any type of legal services, it can at least help make the best effort possible to accommodate all type of different use cases for people with different needs.

    You can find my official plugin here. This project is (and will always be) open source and my GitHub repo can be forked if you want to make it your own. I plan to actively work on this and incorporate feedback as well as new features so please let me know what you think and what you might want to see!

    By the way, I am using the plugin on this site so you can take it for a test drive and see if you think it could work for you and your sites.

  • 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.

  • Out With Ghost, In With WordPress

    As much as using WordPress is cliche, my current job revolves around WordPress so it seemed fitting to use it personally as well. I had been using Ghost since I had been fully immersed in NodeJS, Next, React and researching any ‘new hotness’ JS frameworks or libraries that seemed to materialize out of thin air every few days.

    I like Ghost and it did everything I wanted and was very easy to set up and maintain on my Raspberry Pi web server. I appreciated not having to use plugins for features that WordPress should have built into the core long ago. I really appreciated the content editor which was very fast, and had all of the content editing and layout features I wanted. The templating engine was a bit of a learning curve but once I looked at some different themes and poked around under the hood, I was able to figure out how to get the frontend to do what I wanted it to do.

    Having been a PHP person for almost my entire professional career, WordPress was always looming over my shoulder. When I started to delve deep into NodeJS and other JavaScript or TypeScript-based frameworks, using Ghost for my own site seemed befitting. It was easy enough to set up and self-host on a Raspberry Pi so that’s what I did.

    Alas, I find myself fully immersed in WordPress on a daily basis. I wouldn’t have considered myself a WordPress developer before but I certainly would now. In less than a year’s time, I’ve learned sooooooo much about what WordPress can do and some quirks and shortcomings about what it can’t. As much as I want to be one of the cool kids running a JavaScript-based thing for my own site, it makes more sense to use WordPress and use it as a playground of sorts to experiment and get weird if I feel like it.

    Just like the answer to every car question is “Miata”, the answer to every website platform question is “WordPress”…