Tracking Pagefind Search with Umami
Today I feel very productive. I set up a new Umami instance on Railway (that’s my referral link) to add some simple analytics to this site. Setting it up on Railway was super easy, just a few clicks and it was live. The only tricky part was setting up my own domain for it.
I also have Pagefind set up for site search. It’s a
fully static search library that runs in the user’s browser, which means I don’t
have to host any backend for it. Since my site is built with Astro, I use the
astro-pagefind
package to make integration simple.
Then I got an idea: could I track what people search for? It would be
interesting to see what topics visitors are trying to find. I checked the
Pagefind docs, but it turns out there isn’t a built-in event listener for
searches. You can’t just write some code to listen for a search-completed
event, because it doesn’t exist.
Watching for DOM Changes
After some digging, I found a
GitHub issue
where Tom Vincent posted a clever workaround. The
idea is to use a MutationObserver
.
This is a browser feature that lets you watch for changes to the HTML of a page. You can tell it to watch a specific element, and it will notify you whenever child elements are added, removed, or changed. When Pagefind shows search results, it adds new elements to the page, which is exactly the kind of change we can watch for.
Here is the basic concept:
new MutationObserver((mutations) => {
// This code runs whenever the observed element changes
}).observe(document.querySelector("#search-container"), {
childList: true, // Watch for added or removed child elements
subtree: true // Watch children of children too
});
When new search results are rendered inside #search-container
, the observer
will trigger. Based on that idea, I wrote a small script to tie everything
together. It watches for changes in the Pagefind UI, grabs the search query from
the input field, and sends it to Umami.
Here is the final script I added to my search page:
// Informs TypeScript that `window.umami` can exist on the global scope.
declare global {
interface Window {
umami?: {
track: (event: string, data?: Record<string, any>) => void;
};
}
}
/**
* Initializes the search tracker by directly observing the Pagefind container.
*/
function setupSearchTracker() {
const searchContainer = document.querySelector("#search");
const searchInput = document.querySelector<HTMLInputElement>(
".pagefind-ui__search-input"
);
// If the elements don't exist, do nothing.
if (!searchContainer || !searchInput) {
console.error(
"Pagefind UI elements not found. Tracker not initialized."
);
return;
}
let lastTrackedQuery = "";
const trackQuery = (query: string) => {
if (query && window.umami) {
window.umami.track("search", { query });
}
};
// Create an observer to watch for when search results are added.
const observer = new MutationObserver(() => {
const currentQuery = searchInput.value.trim();
// Track if the query is new and has a meaningful length.
if (currentQuery.length > 2 && currentQuery !== lastTrackedQuery) {
trackQuery(currentQuery);
lastTrackedQuery = currentQuery;
}
});
// Start observing the container for changes.
observer.observe(searchContainer, {
childList: true,
subtree: true
});
}
// Run the setup function.
setupSearchTracker();
The logic is straightforward. First, it finds the main search container and the
search input field. Then it sets up a MutationObserver
to watch the container.
Whenever the observer triggers, it reads the current text from the search input.
To avoid sending too many events, I added a check to only track the query if
it’s new and longer than two characters. The lastTrackedQuery
variable makes
sure I don’t send the same query over and over again as the user types.
If it’s a new query, the trackQuery
function calls window.umami.track()
.
This sends a custom event named “search” to my Umami dashboard, along with the
query itself as data.
Now I can see all the search queries right in my dashboard. A neat little solution for a feature that wasn’t there by default.
Tags | meta , javascript , astro , pagefind , umami |
---|