Category: Code Snippets

  • Semantic search using AI, for Framer sites

    Framer’s built-in search component matches substrings. A visitor types “sustainable sneakers”, your product is called “eco-friendly trainers”, the results panel is empty. The page loads fine, the visitor leaves, and nothing in your analytics tells you what happened.

    Swapping that out for a semantic layer is a small amount of specific work. This is what it looks like on a Framer site.

    Why substring search quietly loses sales

    Substring match is cheap, instant, and only ever finds exactly the words the author happened to write. It cannot see synonyms. It cannot see translations. It cannot see intent. On a small marketing site that is fine, because people mostly navigate. On a site with any kind of catalogue – products, articles, case studies, talks – it starts to leak.

    Semantic search fixes exactly that gap: it compares the meaning of the query to the meaning of each item, by turning both into vectors with an embeddings model. Cosine similarity between those vectors is your score. It does not replace keyword match – you still want “Model 3” to pin exactly – but it is what you reach for when the visitor speaks their own language, not yours.

    The three pieces

    You need three small things working together:

    • An indexer that reads your Framer CMS, turns each item into an embedding, and stores it somewhere you can query.
    • A search endpoint that takes a user query, embeds it, and returns the top matches by cosine similarity.
    • A Code Component on your Framer site that talks to that endpoint and renders the results.

    None of these are large. The whole thing lives in a few hundred lines.

    Indexing the CMS

    Framer exposes CMS collections as JSON at a public URL – the same one the site uses to render itself. You read it, feed each item’s text through an embeddings model, and store the resulting vector against the item’s id. Do this on a schedule, or on a webhook if your workflow has one.

    Pick a small embeddings model – text-embedding-3-small is 1536 dimensions, costs almost nothing, and is more than accurate enough for a marketing-site catalogue. Store the vectors anywhere you can list and compare them: a single JSON file in R2, a Supabase table with pgvector, a KV namespace keyed by item id. For sites below a few thousand items the simplest thing – a flat array in memory – will outperform anything fancier.

    The search endpoint

    At query time you embed the incoming query with the same model, score it against every stored vector, sort, return the top few. Cosine similarity reduces to a dot product when both vectors are L2-normalised, which is the first micro-optimisation worth doing.

    The threshold is the one knob you will revisit. Too low and unrelated items start creeping in. Too high and the search feels empty. For a marketing catalogue, somewhere between 0.30 and 0.45 usually lands.

    The Code Component

    Framer lets you drop a React component onto the canvas. For search, you want a debounced input, an AbortController so stale responses do not overwrite fresh ones, and a small results panel that renders whatever shape you returned.

    Two details that matter more than they look. Debounce at ~200ms: anything less and you pay for every keystroke, anything more and the search feels sluggish. And abort in-flight requests on every new keystroke, otherwise a slow response for “sus” will land after “sustainable sneakers” and briefly overwrite the correct results.

    Where this breaks

    Semantic search is not a free upgrade. Three things to be honest about.

    Exact matches still matter. If someone types a SKU or a product name verbatim, you want that result pinned first, not ranked 0.78 behind something fuzzier. Keep a keyword pre-pass; only fall back to vectors when it returns nothing – or use both, and merge, with keyword matches on top.

    Latency compounds. The embeddings call for the query is a network round-trip. If your endpoint lives on the other side of the planet, 150ms feels like a long time. Co-locate the endpoint with the visitor if you can; cache popular queries if you cannot.

    CMS drift. Nothing is worse than semantic search that confidently returns an item that no longer exists. Your indexer has to run often enough that deletes and renames propagate – or the endpoint has to re-validate hits against the live collection before returning them.

    The shortcut

    The snippets above are the whole pattern – if you want to own it end to end, that is all you need. If you do not, I have packaged exactly the three pieces into AI Search for Framer, on the Framer Marketplace. You install it, point it at your CMS collection, and it handles the indexer, the embeddings endpoint, and the Code Component that renders the results. One threshold slider is the only knob, and it reindexes automatically when the CMS changes.

    Either way, the thing worth internalising is the shape: embeddings turn “words your visitors use” and “words you wrote” into vectors in the same space, cosine similarity scores how close they are, and everything else is plumbing.

  • Do the slow work after the page is sent: WordPress’s shutdown hook

    Last month I added a search log to my WordPress plugin: every time the AI layer surfaced a product that plain keyword matching had missed, I wanted a row in a table saying so. Useful feature. Also a great way to make every single search on someone’s store slower, if you write it the obvious way.

    The fix is a WordPress hook that does not get talked about enough: shutdown.

    The problem with the obvious version

    You are already inside the search filter, you have the data, so you write the row there:

    It works. It is also bookkeeping that the shopper is paying for with their time. One INSERT is a millisecond or two, which sounds like nothing until you remember that this runs on every search, that the database might be on another host, and that your “one small write” has a way of becoming three writes and a lookup six months later.

    The visitor needs the search results. They do not need your analytics.

    What the shutdown hook actually is

    shutdown is the last action WordPress fires. Core registers it as a PHP shutdown function, so it runs at the very end of the request – after the template has rendered, after the response has been handed off. It fires on normal page loads, on AJAX, on REST requests, and even when something calls wp_die().

    That makes it the natural home for work that must happen during a request but that nobody is waiting for.

    Why it feels instant: fastcgi_finish_request()

    On PHP-FPM – which is what most decent hosting runs – PHP can tell the web server “I am done, send it” and keep executing. WordPress core and various plugins call fastcgi_finish_request() at the end of a request, so by the time your shutdown callback runs, the browser has already received the page and started rendering it.

    Be honest with yourself about what that means, though. The visitor is not waiting, but the PHP worker still is. You have moved the cost off the perceived page load and onto your pool of workers. That is a very good trade for a single INSERT. It is a terrible trade for something that takes two seconds, because under traffic you will run out of workers and the queue will bite you in a much less obvious way.

    On old mod_php or CGI setups the connection may not be closed early at all, so the technique degrades to “same total time, tidier code”. Nothing breaks – it just stops being a performance win.

    The pattern, in three moves

    This is the actual code from AI Search 1.31.0, trimmed for reading. First, register the hook where you register the others:

    Second, during the request you only collect. No queries, no writes – just put what you know on the object and carry on:

    Third, the callback that actually writes. Note the early return and the null: this thing must be safe to call when there is nothing to do, and safe to call twice.

    That is the whole trick. The search path does zero database work for logging; the row still lands on every relevant request; and the shopper never waits for it.

    When you should not reach for it

    • Anything the response depends on. Obvious when written down, easy to forget at 11pm. If the page needs the result, it cannot happen after the page.
    • Anything slow. Calling an external API, resizing images, sending mail through a slow SMTP server – all of these hold a worker hostage. That is what WP-Cron, or a real queue, is for.
    • Anything that needs to output. Headers are long gone. No redirects, no cookies, no printing. If you echo something here you will either see it glued to the end of your page or nowhere at all.
    • Anything that must not be lost. A fatal error, a killed worker or an aggressive timeout can end the request before your callback runs. For a search log that is fine. For an order, it is not.

    Small gotchas worth knowing

    $wpdb is still perfectly alive at this point, so queries work normally. Object cache writes, on the other hand, are mostly pointless: nothing else in this request will read them. And if you use a persistent object cache, remember that other requests may already be serving cached output that does not know about your write – so do not use shutdown to fix up something the current page displayed incorrectly.

    Also, keep the callback idempotent. In unusual flows (some AJAX handlers, some error paths) you can end up with the shutdown sequence running more than once. Nulling the pending payload after reading it, as above, is the cheapest possible insurance.

    The result in practice

    In AI Search this powers the Insights tab: store owners can see which searches the AI actually rescued – the queries where semantic matching surfaced products that keyword matching had missed – without the logging costing their shoppers anything. Same feature, same data, moved a few milliseconds to the right in the request lifecycle.

    It is a small technique. But “does this actually need to block the response?” is a question worth asking about half the code in a typical plugin, and shutdown is the tool for the times when the answer is no.

  • Your Search Bar Is Your Back Door: And Here’s How to Lock It

    Last Saturday I spoke at WordCamp Europe 2026 in Kraków about a topic that’s been on my mind for a while: the WordPress search endpoint as a DDoS attack vector.

    The talk was called “The Hidden DDoS Threat in WordPress: Abusing the Search Endpoint“ and the response was incredible (thank you very much!!). However, some feedback came after the talk, and it pushed me to build something better.

    You can see the session here: https://europe.wordcamp.org/2026/session/the-hidden-ddos-threat-in-wordpress-abusing-the-search-endpoint/

    What the talk covered

    The core idea is simple. Every WordPress site has a search endpoint (/?s=). Unlike your regular pages, search queries bypass CDN and cache entirely. Every single search request hits your database directly with a LIKE ‘%query%’ full table scan.

    An attacker doesn’t need a botnet. Just flood /?s= with random strings and your database is on its knees.

    During the talk, I showed a live benchmark comparing DDoS attacks on cached pages versus the search endpoint. Same number of requests. Completely different results. The cached pages didn’t even make the server sweat. The search endpoint brought it down.

    Photo by Claudio Schwarz

    The solution approach and the feedback

    In the talk, I proposed using wp_create_nonce() as a first line of defense. The idea was straightforward: generate a nonce on page load, require it for search requests, and block anything without it. Bots don’t load pages, so they never get the nonce.

    The point I was making is that WordPress has had tools to mitigate this since 2006. We just don’t use them, however some people raised a valid point:

    I can JUST USE the nonce and use it in my DDoS LIKE IN THE BROWSER!
    random hacker saying correct things

    They’re right. And I appreciate that feedback. The nonce approach raises the bar and stops the simplest and automated attacks. But it’s not bulletproof. A determined attacker can work around it. So let me prepare something and bring a solution that will meet the expectations. I was so frustrated with myself that I basically started to think about a solution in Krakow yet.

    Introducing WP Search Shield

    WP Search Shield is a WordPress plugin that protects your search endpoint with two layers of defense that work together.

    Layer 1: Single-use tokens

    Instead of a WordPress nonce that lasts hours, WP Search Shield generates a unique token on every page load. This token can only be used once. The moment it’s used for a search request, it’s deleted from the database and a new one is issued.

    This means:

    • An attacker extracts the token from your HTML? It works once. That’s it.
    • They want to search again? They need to load the full page again to get a new token.
    • At that point, they’re just a regular user. DDoS over.

    Layer 2: IP rate limiting

    Even if someone finds a way to rotate tokens quickly, they’re still limited to a configurable number of searches per time window. Default is 10 searches per 60 seconds per IP.

    After that? 429 Too Many Requests.

    How they work together

    AttackWithout pluginWith plugin
    Bot hits /?s=random directlyHits database403 — No token
    Bot extracts token, sends 1000 requestsAll hit databaseFirst works, rest get 403
    Bot refreshes token each timeAll hit databaseRate limited after 10
    Real user searches normallyWorksWorks seamlessly

    The plugin handles everything automatically. It injects tokens into search forms, patches fetch() and XMLHttpRequest for REST API calls, and rotates tokens after each search. Zero configuration needed.

    It works with your existing stack

    WP Search Shield is built on WordPress transients. If you’re running Redis or Memcached (which you should be), token validation and rate limiting happen in memory. No extra database queries. No external services.

    It protects both traditional search (/?s=) and REST API search (/wp-json/wp/v2/search).

    Try it yourself

    The plugin is open source and available on GitHub:

    check it here: github.com/samuelsilvapt/wp-search-shield

    Install it, activate it, done. It works out of the box with sensible defaults.

    If you want to customize it, everything is configurable via filters:

    You can also hook into blocked requests for monitoring:

    The bigger picture

    This plugin isn’t a replacement for a CDN or a WAF. It’s a targeted fix for a specific problem that those tools can’t solve.

    Your CDN caches static content beautifully. Your object cache speeds up repeated queries. But search queries are unique by nature. They can’t be cached. And that’s exactly what attackers exploit.

    WP Search Shield closes that gap.

    chilling in a random cafe in kraków while working on the plugin

    Thank you, WCEU

    To everyone who came to the talk, asked questions, and challenged the nonce approach: thank you. You made this better. But I want to say something beyond the technical stuff.

    I reconnected with old friends I hadn’t seen in years. I made new ones over coffee, over beers, over late-night conversations about everything but WordPress. I created memories that I know I’ll carry for a long time.

    There’s something special about this community. It’s not just about work, code or plugins or themes. It’s about people who genuinely care about each other and push each other to grow. I felt that in every conversation, every handshake, every “great talk” from someone in the hallway.

    And kraków, you were unforgettable.

  • Everyday Efficiency: PHP 8 Techniques for WordPress Engineers

    As a software engineer navigating the daily challenges of WordPress development, embracing the latest tools and tricks is essential to staying efficient and effective. In this post, I want to share three PHP 8 tricks I incorporate into my everyday tasks as a software engineer working on WordPress projects.

    When PHP 8.0 was released, I wrote about my favourite improvements here, and you’ll see some of them in this post – however, this time those techniques are part of my day as an engineer.

    These tricks aren’t just about staying up-to-date; they’re practical enhancements that make my coding life easier and my WordPress development more streamlined. Let’s dive in!

    Easier Null Handling with “?->” Operator


    In PHP 8, there’s a cool trick called the Nullsafe Operator (?->). It helps when you’re dealing with nested objects and saves you from writing lots of null-checks. Check out this example:

    // classic one
    if ($user !== null && $user->getProfile() !== null) {
       $avatar = $user->getProfile()->getAvatar();
    } else {
       $avatar = 'default.jpg';
    }
    // With PHP 8 Nullsafe Operator
    $avatar = $user?->getProfile()?->getAvatar() ?? 'default.jpg';

    This makes your code shorter and easier to read, especially when working with WordPress objects.

    Cleaner Conditionals with “match” Instead of “switch”


    PHP 8 has a new way to handle conditions using the match expression. It’s like the old switch statement but more concise. Look at this:

    
    // Traditional switch
    switch ($status) {
    case 'published':
       $message = 'Post is live!';
       break;
    case 'draft':
       $message = 'Post is still in draft.';
       break;
    default:
       $message = 'Post status unknown.';
    }
    // PHP 8 match expression
    $message = match( $status ) {
       'published' => 'Post is live!',
       'draft'     => 'Post is still in draft.',
       'default'   => 'Post status unknown.',
    };

    Personally, I love this one! When you avoid overusing If/else statements in your code, matching labels with indexes is always a great solution – even better when you can match the index with the actual status.

    Constructor Property Promotion for Cleaner Class Definitions:

    PHP 8 introduces a streamlined way to define classes with the Constructor Property Promotion feature. This feature is a game-changer for WordPress developers, allowing for more concise and readable class definitions. It’s particularly useful when creating custom components like widgets or API responses. Instead of declaring class properties and then assigning them in the constructor, PHP 8 allows you to do both simultaneously, thereby reducing boilerplate code.

    For example, consider a custom widget in a WordPress theme:

    // PHP 7 style
    class CustomWidget {
        private $title;
        private $content;
    
        public function __construct($title, $content) {
            $this->title = $title;
            $this->content = $content;
        }
    }
    
    // PHP 8 Constructor Property Promotion
    class CustomWidget {
        public function __construct(
            private $title, 
            private $content
        ) {}
    }

    This approach makes the code significantly more concise and enhances readability, which is essential in complex WordPress projects.

    Named Arguments for More Readable Function Calls

    PHP 8’s Named Arguments feature significantly enhances the readability of function calls, especially in WordPress where functions often come with multiple parameters. This feature allows developers to pass arguments to a function based on their names, rather than their position, making the code more understandable and less prone to errors related to the ordering of arguments.

    Again, considering a WordPress function call scenario:

    // PHP 7 positional arguments
    add_theme_support('custom-background', array(
        'default-color' => '000000',
        'default-image' => '',
    ));
    
    // PHP 8 named arguments
    add_theme_support('custom-background', default-color: '000000', default-image: '');
    
    // Another example with WP_Query
    $query = new WP_Query(array(
        'post_type' => 'post',
        'posts_per_page' => 10,
        'orderby' => 'title',
        'order' => 'ASC'
    ));
    
    // With PHP 8 named arguments
    $query = new WP_Query(
        post_type: 'post',
        posts_per_page: 10,
        orderby: 'title',
        order: 'ASC'
    );

    Using named arguments not only clarifies the purpose of each parameter but also makes the code more flexible for future changes, an invaluable asset in the evolving landscape of WordPress development.

    More Control with Union Types

    PHP 8 introduces something called union types. It’s a way to say a variable can be one of several types. This is handy when working with functions that can take different kinds of data:


    // Without union types
    function processUserData($data) {
    // Function logic here
    }
    // With union types
    function processUserData( array|string $data ) {
    // Function logic here
    }


    By using union types, you can be clearer about what kind of data your functions expect, making your WordPress code more robust.

    Is WordPress core already prepared for PHP 8?

    It’s very important that you run your local PHP environment as similar as possible to your project’s final server, because although WP core is compatible with PHP, you’ll see that officially the stable version continues to be 7.4. But my advice: run at least version 8.1 🙂

    Conclusion

    These PHP 8 tricks might seem small, but they can make a big difference in your WordPress development. The Nullsafe Operator, Match Expression, and Union Types are easy to understand and implement. By using these tricks, you’ll not only stay current but also write code that’s simpler, cleaner, and more effective in your WordPress projects.

    All these details will reveal that you’re proud of your work. In a world where >40% of the Internet potentially can run your code, keep in mind you have the mission to create a web that is being designed to last.

  • PHP 8.0 – 4 new features in this latest version

    PHP 8.0 – 4 new features in this latest version

    New PHP major versions are always very expected by PHP developers, we already know good things coming when a new release is announced. This time we have a lot of performance improvements and new features that improve our code, using modern programming methods in a badass old-school language.

    Besides those new features and recommendations, I strongly recommend we use them only for environments that we can expect and have sure that 8.0 is running. In some cases, we can use polyfills instead. If you’re a plugin developer, it’s too soon to start using the new features of this version.

    Nullsafe operator

    I really love clean and efficient code, so I’m not a fan of the classic if/else statements (why we shouldn’t use them? Read more here). However, there is always a time when we need to use a simple if/then, and the best way to do that is using an inline if statement Like this:

     $today_exists = time() ? true : false;

    Clean, useful and works like a charm. I can’t know how many times I’ve tried to check a null value of a class method (for example), giving a “then” statement in the same line. Simple – it wasn’t possible, and the solution was based into split the statement into two or more lines.

    The good news is called Nullsafe operator. PHP 8 now provides a null validation without any errors, so the following example now is pretty possible:

    $today_exists = $this->get_time()?->$this->is_true();

    str_contains() function

    With this new function, we no longer need to use the strpos() function. This last one is more recognized by developers to be used to verify if a string contains a word, like this:

    $today_exists = ( false !== strpos( 'Today is the day', 'Today' ) );

    Yes, that’s what we had to do to check if a string contains X word 🙂

    Checking the documentation, it’s really strange to see str_contains() only is available from 8.0. In this link, you can also find a pollyfill alternative.

    New match() expression

    That’s a good one for clean code. Imagine we have a switch case expression, but optimized to be used without breaks, and organized as a class sintax. I’m particullary interested into starting to use this new approach, check some examples in the documentation.

    $food = 'cake';
    
    $return_value = match ($food) {
        'apple' => 'This food is an apple',
        'bar' => 'This food is a bar',
        'cake' => 'This food is a cake',
    };
    
    var_dump($return_value);

    Enums

    Exactly like other languages (e.g. javascript), enums provides us to call a function and have a definite number of cases on the args. The result is a more stable code. This one will be available in the 8.1 release only, but it’s a great example of what it’s happening with PHP – giving to the developers great native features we can use like a modern programming language.

    I’m looking forward to use these features on my code, and I’m confident that will be possible soon – PHP 8.0 has been launched in November 2020, so please read more about WordPress and 8.0 compatibility before starting to code.

  • Stock Sync – How to sync products in a WooCommerce Multisite

    There are a lot of reasons to use the Multisite feature on a WordPress environment – the challenge is when we need to synchronize information between them. Same database, but different tables, languages and product stocks are great examples of common data between all websites. Let’s start from the beginning.

    What is WordPress Multisite?

    Using the same hosting, network and dashboard, WordPress allow us to create several websites in the same place. The advantages:

    • Same plugins/version on all websites (less maintenance time);
    • Same code, different content;
    • Same themes across the websites;
    • Better content and network management;
    • Allow us to divide the websites using subdirectories (e.g. samuelsilva.pt/courses) or subdomains (e.g. courses.samuelsilva.pt);
    • Multisite means less code, literally.

    How do the same products sync on all websites?

    They don’t sync – there is no communication between websites. Each website works with different tables in the database – sometimes can be redundant, but remember – different content is the main advantage of a WP multisite environment.

    How to sync the products on the same multisite network?

    That’s why you’re here. There are many ways to sync the products, so I’ll suggest to you which I consider the best one (without plugins).

    Before anything, please check the most important WordPress functions we will use on this code snippet:

    • get_sites() – retrieves the list of the websites from our network, depending of the args we use. In this specific case, we will use the default args;
    • switch_to_blog() – sending the blog_id (site ID) as a parameter, all your further queries results will come from that specific blog;
    • restore_current_blog() – come back the queries to the main website.

    Using these functions, we will be able to create a bridge between all the websites in the multisite network. We have a lot of possibilities, but let’s focus on a specific need – synchronize stock units between websites.

    Important question: Which field will be our key (foreign key for SQL people)?

    I don’t recommend we use the most obvious – product_id. Remember, there isn’t any relationship between the websites, so we can’t trust an autoincremented field. Let’s use the product SKU in our example. Let’s go forward to our snippet:

    Stock Products between websites – code snippet

    Follow we can find my suggestion on a code snippet. If you have any questions or suggestions, please write a comment on this post.

    /* 
    * Sync products stocks by the lowest. Run this function for each product
    * @param	string	$sku	Product SKU
    */
    public function sync_stock_by_sku ( $sku ){
    
    	// Firstly, check if we're on a WP multisite environment
    	if( ! function_exists( 'get_sites' ) || $sites = get_sites() ){
    		return;
    	}
    	
    	$stores = array();
    
    	// Prepare and array with the sites Ids
    	foreach ( $this->sites as $site ) {
    
    		// Organize the multidimensional array, indexing by country code
    		$country_c 			  = str_replace( '/', '', $site->path );	
    		$stores[ $country_c ] = $site->blog_id;
    
    	}
    
    	// in this case, we will sync all the products to the lowest stock store for this specific product
    	$lowest_stock = false;
    
    	$product_id = wc_get_product_id_by_sku( $sku );
    	$product    = wc_get_product( $product_id );
    
    	// Websites loop
    	foreach( $stores as $c_code => $store_id ){
    
    		// prepare the queries to run for each website
    		switch_to_blog( $store_id );
    
    		
    
    		
    		// Continue the loop if the line item is empty, for example
    		if( ! $product ){
    			continue;
    		}
    		
    		// If this is the first product, override the lowest stock entry
    		// and continue the loop
    		if ( ! $lowest_stock && is_bool( $lowest_stock ) ) {
    			$lowest_stock = $product->get_stock_quantity();
    			continue;
    		}
    	
    		// Check if the stock from this store is lower than the current one
    		$stock = $product->get_stock_quantity();
    
    		if ( $lowest_stock > $stock ){
    			$lowest_stock = $stock;
    		}
    
    
    	}
    
    	// Now we have the lowest stock value. Let's reloop and sync stock quantity to the lowest value
    
    	foreach( $stores as $c_code => $store_id ){
    
    		// prepare the queries to run for each website
    		switch_to_blog( $store_id );
    
    		$product_id = wc_get_product_id_by_sku( $sku );
    		$product    = wc_get_product( $product_id );
    
    
    		// Update the stock quantity
    		wc_update_product_stock( $product, $lowest_stock );
    	}
    
    	// Restore queries to the current site
    	restore_current_blog();
    
    	// Return the new stock
    	return $lowest_stock;
    
    }