Offer and search the tags the desk already uses - #207
Merged
Conversation
The SEO Tags box was a bare text field, so the boilerplate tags that go on nearly every article -- triangle, drexel, drexel triangle -- were retyped by hand each time. WordPress offered them as a click-to-add list and editors relied on it. There is no tags table: `articles`.`tags` holds a JSON array per article, so "frequently used" can only come from aggregating the column. The ranking runs in Go rather than as a JSON_TABLE query, because the column is not reliably JSON -- FormatTags falls back to a comma-joined string, and untagged articles store "" -- and parseStringListField already absorbs both spellings. Reusing it means the suggestion counts cannot disagree with what the editor reads back. Case is folded, since the imported archive carries Drexel and drexel as separate strings and offering both is the duplicated list this replaces. Clicking a suggestion blurs the tag input, and blur commits the draft, so typing "drex" and clicking "drexel" added both tags. The button suppresses the blur. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The suggestions only filtered the cached top 50, so a tag like "Men's Lacrosse" -- 46 articles, nowhere near the top -- was invisible the moment somebody started typing it. Retyping it from memory is how a near-duplicate of an existing tag gets coined, which is the thing the suggestions exist to prevent. The cached ranking now holds every distinct tag rather than the top slice. That is ~20k entries against ~9k articles, a couple of megabytes, and far cheaper than a per-keystroke scan of the article table; only the response is capped. Matches are ordered by how they match before how popular they are: exact, then prefix, then a word starting with the query, then anything containing it. Somebody typing "lacrosse" means the tag "lacrosse", and ranking on uses alone buries it under a better-used "Women's Lacrosse". GET /v1/tags/popular becomes GET /v1/tags?q=, since it is no longer only about popularity. The box is searched on a 200ms debounce, so a typed word is one request; the popular tags stay on screen as the instant fallback, and a query that finds nothing says so rather than leaving the row empty. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Sanjana asked for this in Slack on Aug 5: in WordPress the tag field offered the tags she adds to nearly every article, and she'd click them instead of retyping. The CMS's SEO Tags box is a bare text field, so that shortcut was gone.
Two things now sit under the box. With it empty, the tags the desk uses most. Once you start typing, a search over every tag the archive has — 19,893 of them — because the tags nobody remembers the spelling of are exactly the ones that aren't popular. Retyping a tag from memory is how near-duplicates get coined, so finding the existing one has to be easier than retyping it.
How the ranking is built
There is no tags table.
articles.tagsholds a JSON array per article, so both the popular list and the search come from aggregating that column.The aggregation runs in Go rather than as a
JSON_TABLEquery. The column is not reliably JSON —FormatTagsfalls back to a comma-joined string when marshalling fails, and untagged articles store""— andparseStringListFieldalready absorbs both spellings. Reusing it means the suggestion counts cannot disagree with what the article editor itself reads back.Case is folded, because the archive carries
Drexelanddrexelas separate strings, and offering both as separate suggestions is exactly the duplicate-looking list this is meant to replace. The displayed spelling is the commonest one, with a deterministic tie-break so the row does not rename itself when the cache rebuilds.The cache holds the whole ranking, not the top slice — ~20k entries against ~9k articles, a couple of megabytes, and far cheaper than a per-keystroke scan of the article table. Only the response is capped. Archived articles are excluded; drafts are not.
Ordering
Matches are ranked by how they match before how popular they are: exact, then prefix, then a word starting with the query, then anything containing it. Somebody typing "lacrosse" means the tag "lacrosse", and ordering on uses alone buries it under a better-used "Women's Lacrosse".
Against the real archive,
q=lacrossereturns:Lacrosse(70),lacrosse dragons(1),Women's Lacrosse(51),Men's Lacrosse(46),Drexel Women's Lacrosse(24),Drexel Men's Lacrosse(21).In the editor
The box is searched on a 200ms debounce, so a typed word is one request rather than six. The popular tags stay on screen as the instant fallback and are filtered client-side on the keystroke, so the row narrows immediately instead of sitting empty until the search returns; stale responses for a prefix already typed past are dropped. A query that matches nothing says "No existing tag matches. Press Enter to create it." — coining a new tag is fine, but an editor should know that is what they're doing.
The trap worth knowing about
Clicking a suggestion blurs the tag input, and blur commits whatever is in it. Typing
drexand clickingdrexeladded both tags. The button suppresses the blur viaonMouseDownpreventDefault; removing that line makes the covering test fail, which I checked.Tests
CMS_TEST_DSNeditArticleView.test.tsx, covering the blur trap, the debounce collapsing to one request, and finding a tag that is in the archive but not in the popular listFull server suite, frontend suite (19/19), and
tsc -bare green. Search quality was also eyeballed against the live 9,371-article archive with a read-only probe.Two things for the reviewer
Based on #206, not main. This builds on the featured-article branch because the editor page changes sit next to each other. Merge #206 first and this retargets cleanly.
The swagger regen swept in unrelated catch-up.
server/docswas already stale —canonical_url,photo_alt,featured_image_altand others were missing — soswag initadded lines that aren't this endpoint. All generated, no deletions, but say the word and I'll drop the docs from the branch.Not visually checked. The authed editor page needs the stubbed-entry-point + Playwright route to screenshot, which I did not set up. Behavior is covered by component tests, but nobody has looked at the row yet.
🤖 Generated with Claude Code