Version 5.4.0
Added
- Console commands.
tidytags/duplicatesruns the duplicate scan from the command line, for one source or all of them, with--strategy,--threshold,--scope,--siteand--format=table|json|csv;--fail-on-duplicatesexits 1 when any are found, so a CI job can fail on new duplicates.tidytags/merge --into=<id> <ids>merges tags the way the Merge button does.tidytags/unusedlists tags nothing relates to, and--deleteprunes them. - Merge and delete from the console are dry runs unless
--applyis passed, and--dry-runalways wins. The merge dry run shows, per tag, how many relations would move and how many would be dropped as duplicates. Tags::mergeProblems(),Tags::previewMerge(),Tags::deleteUnusedTags(),Sources::getUnused()andSources::findSourcesByHandle().
Changed
- Merge and its dry run share one routine for sorting relations into moved and dropped, so the preview always matches what the merge does.
Version 5.3.0
Added
- Pagination on the Manage screen. The group view now loads one page of items at a time instead of the whole source. A 17,000-tag group used to exhaust PHP's memory limit before the page rendered (#1). Paging keeps the search and site filter, and actions return you to the page you were on.
- Pagination on the Duplicates screen. Clusters are listed a page at a time, with a heading wherever the source changes, and only the clusters on the current page are loaded as elements.
pageSizeandduplicatesPageSizesettings. Items per Manage page (100 by default, 1–1000) and clusters per Duplicates page (25 by default, 1–500). Set them inconfig/tidytags.php.tidytags-manageTagsandtidytags-deleteTagspermissions. Rename and swap need manage, delete needs delete, and merge needs both. Actions the user can't run are hidden on the Manage and Duplicates screens.- The usage list says when it has been cut off at its 200-item limit.
DuplicateDetector::getDuplicatesPage(),Sources::countElementsInSource(),Sources::isSourceElement(),Tags::canRepointRelations(),TitleMatcher::strictKey()andTitleMatcher::compareFuzzyNormalized().Sources::getElementsInSource()takes optional$limitand$offsetarguments.
Changed
- Duplicate scans scale to large vocabularies. Scans used to load every element and compare every title with every other, so a 17,000-item source took minutes and could run out of memory. They now work on IDs and titles, find candidate pairs through an index (strict titles grouped by key, fuzzy titles through a pigeonhole segment index), and only load the elements they return. Results are identical to the old pairwise comparison; a property test checks this against brute force on random vocabularies. A 17,000-title source now clusters in well under a second.
- Scan results are cached until a tag or entry is saved or deleted, or the match settings change, so moving between pages doesn't rescan.
- The editor "did you mean?" lookup compares against the cached titles and only loads the elements it returns, instead of loading every element on each pause in typing.
- The Duplicates screen's similarity threshold is capped at 6.
- Upgrade note: non-admin users with only
accessPlugin-tidytagscan no longer rename, merge, delete, or swap. To keep those workflows, grant the new permissions in Settings → Users. - Merge and swap now invalidate element and template caches after rewriting relations. Running
clear-caches/allafterwards is no longer needed. - Control panel styling follows Craft's theme. Every inline style and hard-coded colour has moved into the plugin stylesheet, built on Craft's CP colour tokens, so the screens and the editor warning work in dark mode and high contrast. Selects, checkboxes, radios and text inputs use Craft's
_includes/formsmacros, which gives them proper labels and focus styles.
Security
- Merge, delete, rename, and swap only required plugin access, so any user who could open Tidy Tags could run them. Each now requires its own permission.
- Swap re-pointed relations between any two elements, including entries in sections the user couldn't edit. It now only accepts tags and entries in configured tag-like sections. Merge and swap also return 403 unless the user can save every element whose relations they rewrite.
- The usage lookup (
tidytags/tags/usages) returned titles for any element ID. It now only answers for Tidy Tags source elements, and leaves out usages the user can't view.
Version 5.2.0
Added
- Selectable match strategy. A new
matchStrategysetting chooses how titles are compared.fuzzy(the default, and the only behaviour before this release) keeps normalized equality plus Levenshtein distance.strictcompares titles after stripping affixes from the start and end, uses no edit distance at all, and rejects a pair outright when one title carries a qualifier token the other lacks. TitleMatcherservice. Title comparison now lives in its own service (Plugin::$plugin->titleMatcher) rather than insideDuplicateDetector, so the Duplicates dashboard, the cross-source scan and the editor "did you mean?" warning all agree on what counts as a duplicate. Public API:compare(),compareFuzzy(),compareStrict(),normalize(),coreKey(),qualifiers(),tokens(),getStrategy().affixTokenssetting. Words stripped from the start and end of a title under the strict strategy —fc,afc,cf,scand friends by default. Stripping is positional, so an affix in the middle of a title is left alone andFIFA Club World Cupis not reduced toFIFA World Cup.qualifierTokenssetting. Words that make two otherwise-matching titles different things —women,ladies,ii,reserves,u21,one,twoand similar. Under the strict strategy, an asymmetry here vetoes the match, soArsenalis never reported as a duplicate ofArsenal Women, norCounty ChampionshipofCounty Championship One.- Matching settings UI. Settings → Plugins → Tidy Tags gains a Matching section with the strategy picker and, under strict, editable token lists. Both lists accept a newline- or comma-separated paste, and both can still be driven from
config/tidytags.phpas arrays.
Why strict exists
Edit distance suits the vocabulary most sites have: free-text tags typed by editors, where duplicates are misspellings. It is the wrong tool for a controlled vocabulary of proper nouns — clubs, competitions, places, people — because those names sit close together in edit space. Measured against a real 2,633-title corpus of teams, competitions and tags, the fuzzy strategy's six near-matches were all false positives (Essex/Sussex, Durham/Fulham, Tampa/Samoa, Top 10/Top 14), while it missed every genuine variant, because those differ by a whole affix word rather than a typo — FC Bayern Munich/Bayern Munich is three edits apart. On the same corpus the strict strategy found the four real variants and none of the false positives.
Neither strategy is better in general, which is why this is a setting rather than a change of default.
Changed
DuplicateDetectordelegates all title comparison toTitleMatcher. Method signatures, thedefaultThresholdproperty and every returned item key are unchanged, and the fuzzy strategy produces identical results to 5.1.0.DuplicateDetector::findSimilar()still reports adistancefor each match under both strategies. Under strict, an exact match reports0and an affix-only match reports1(TitleMatcher::DISTANCE_AFFIX), so callers that sort by distance keep putting unambiguous matches first.- The
thresholdargument tofindDuplicates(),findAllDuplicates(),findCrossSourceDuplicates()andfindSimilar()is ignored under the strict strategy, which has no distance to threshold. - An unrecognised
matchStrategyvalue falls back tofuzzyrather than matching nothing, so a typo inconfig/tidytags.phpdegrades to previous behaviour.
Fixed
Tags::renameTag()returnedtruewhen the given tag ID matched nothing, so the control panel announced "Tag renamed." after a no-op. It now reports failure when the ID resolves to no tag in any site.
Development
- Added a Codeception unit suite that runs the plugin's services against a real, freshly installed Craft 5 database, with a DDEV config to run it. 156 tests / 323 assertions. See Development in the README.
Version 5.1.0
Added
- Per-source field configuration. Each source (tag group or tag-like entry section) can now be assigned a differentiator field and a list of display fields under Settings → Plugins → Tidy Tags. Differentiator values are used by the duplicate scanner and the "did you mean?" warning to keep deliberately same-named items apart (e.g. an
EnglandTeam withsport: Footballis no longer flagged against anEnglandTeam withsport: Cricket). Display field values appear next to each item so reviewers can tell similar items apart at a glance. - Cross-source duplicate detection. A new Across sources tab on the Duplicates page pools every source together and surfaces clusters that span more than one source — the case where an editor has created a Tag for something already maintained as a Team or Competition.
- Item links and source badges. Cluster items now link directly to their entry/tag edit screen and show the source they came from with a Tag/Entry badge, so jumping to fix a duplicate is one click.
- Inline usages browser. Each cluster item has a Show usages button that lists every entry, asset, etc. that holds a relation to it, with edit links — useful for previewing what would move in a swap or merge, or for cleaning up by hand.
- Cross-type relation swap. A new
tidytags/tags/swapaction re-points relations from any element to any other element without deleting the source. It powers the new cross-source clusters UI and the read-only entry-source clusters, and is safe across element types because it only touches the{{%relations}}table. Tag → tag merges still use the existing delete-after-swap path. - Editor "did you mean?" warning now spans configured entry sections. Typing a new tag warns you if a Team or Competition with the same name already exists, with a direct link to the existing entry. Honors the differentiator field so a new
England (Rugby)doesn't get flagged against an existingEngland (Football).
Changed
DuplicateDetector::findSimilar()andfindDuplicates()now return enriched item dicts that includecpEditUrl,displayValues,differentiator, and source metadata. Existing keys (id,title,siteId) are preserved.- The Duplicates page is split into Within source and Across sources tabs, scoped by the new
scopequery parameter. - Settings model gains
sourceFieldConfig, keyed by source UID and overlayable fromconfig/tidytags.php.
Action endpoints
| Action | Method | Params |
|---|---|---|
tidytags/tags/usages | GET | elementId |
tidytags/tags/swap | POST | targetId, sourceIds[] |
Version 5.0.1
Added
- Support for entrified tag sections. Channel sections produced by
php craft entrify/tagscan be opted in under Settings → Plugins → Tidy Tags and appear on the dashboard and in the duplicate scanner alongside native tag groups, with an Entries badge. - Per-site counts and title browsing for entry-backed sources.
- Cross-site duplicate-cluster detection for entry-backed sources (read-only — review clusters in their section).
tagLikeSectionUidssetting, overlayable fromconfig/tidytags.phpfor per-environment configuration.- New
Sourcesservice andSourcemodel that unify tag groups and tag-like entry sections behind a single interface.
Changed
- Dashboard, group view, and duplicate scanner now operate on unified sources instead of tag groups directly.
- Rename, merge, delete, and the "did you mean?" editor warning remain tag-only by design; entry-backed sources are intentionally read-only in Tidy Tags.
Version 5.0.0
- Initial release.