Category: Web patterns

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

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