Faster browse pages, smarter analytics, keyboard fixes
Authors page loads almost instantly now. It used to be the slowest browse page on the site, doing a lot of database work on every visit to figure out how many books and series each author had. With the catalogue growing, that work was only getting heavier. Now each author keeps a running total of their book and series counts, updated whenever something relevant changes. First-paint dropped from around 2.5 seconds to roughly a quarter of a second.
Same trick on Series, Universes and Groups. The same speed-up applies to the homepage Featured Series carousel, the Universes list, and the Groups list (especially the "sort by member count" view). The Groups change also quietly fixed a small inconsistency where the list page and the detail page could disagree on member count when a character had been deleted - both now match.
Page-view tracking stopped counting crawlers. The little ticker that powers the "incomplete characters" ranking (and the future popular-content feature) was being thrown off by Google, Slack, Discord and other automated previewers that load pages exactly like a browser would. Production data showed 199 page views with zero recognised as bot traffic, even though the timing pattern was very obviously a crawler walking the sitemap. Bot detection got an upgrade, bot hits no longer get recorded at all, and the historical noise has been cleared so the rankings reflect actual readers from here on.
Glossary character-excerpt links now preview. When a glossary entry's "Where it appears" excerpts mentioned another entity, the link rendered as plain text instead of the usual hover preview. Fixed - they now behave the same as every other rich-text link on the site.
Hidden links no longer steal keyboard focus. On long descriptions that get clipped with a "see more" button, the hidden portion contained links that were still reachable by tabbing. A keyboard user would have to tab through every invisible link before getting to the next visible thing on the page. The hidden links are now skipped until you expand the description.
Search dropdown stops flipping to wrong results. Typing one query then quickly changing to another could leave a slower response from the earlier query arriving last and overwriting the correct one. Typing "look to" might briefly show "Look to Windward" (the Banks book) then flip to a list of characters with "jo" in their name from an earlier keystroke. The search box now cancels the older request whenever you keep typing.
Glossary attribution field simplified. The glossary edit form had two attribution fields side by side - a leftover plain text box and the structured source list that every other entity uses. Dropped the text box. As a bonus, this fixed a hidden bug where saving any change to a glossary entry (even just fixing a typo in the description) was silently wiping every source attribution row for the entry.
Search was sometimes returning results for a completely different query. Someone reported that typing "malazan" on the live site brought back results for "The Culture". Quick check confirmed it - every search query was returning the same handful of results, no matter what was typed. Turned out the CDN sitting in front of the site was treating every /api/search?q=... request as the same URL for caching purposes, so whichever query happened to be cached first got served to everyone after that until the cache expired. Fix: the search endpoint (and five sibling API endpoints that take search/filter parameters in the URL) no longer ask the CDN to cache their responses at all. Your browser still caches them briefly for fast back-and-forth, and the database has its own caching that's correctly keyed on the query, so the speed difference is negligible.
Defensive follow-up on empty search results. While digging into the above, also added a self-healing check: if the database-level cache returns no results for a query, the endpoint quietly re-runs the search uncached to confirm. If it turns out the cache was stale (real matches exist), it flushes the cache so the next person gets the correct results. Stops a small class of bugs where a brief blip during data seeding could leave a "no results" cached for hours.