Free · Craft CMS 5
Nobody deletes it, because nobody can prove it is unused
The images from a campaign that ended in 2022. The entries a section was restructured around. The tags somebody typo’d once and never used again. None of it is visible, all of it is in the nightly backup, and it stays there for years — because the only honest answer to “is this still used?” has always been “probably, but I am not deleting it to find out”. Zoo answers the question properly, and shows you its working.
The screen never says “nothing points at this”
It says how hard Zoo looked before saying so. Every finding carries the list of probes that ran — and a probe that could not run is shown as a probe that could not run, never as a clean result. “Nothing points at this” and “nothing I was able to check points at this” are different claims, and only one of them is a reason to delete something.
$ php craft zoo/census/run
unused-assets 312 of 4,109 scanned, 3,790 referenced, 1.4 GB reclaimable
unrelated-entries 18 of 2,204 scanned, 2,186 referenced
orphaned-nested-entries 0 of 8,812 scanned
unused-tags 44 of 391 scanned, 347 referenced
Census #12: 374 unreferenced, 1.4 GB reclaimable, 6.1s.
How hard Zoo looked:
✓ Relational fields 8,331 found
✓ Nested-entry ownership 612 found
✓ Structure parentage 88 found
✓ Reference tags in stored content 41 found
✓ URLs in stored markup 196 found
✓ Twig templates and config 7 found across 412 places
– Other tables keyed into elements Foreign-key scanning is turned off in settings.
Features
Seven ways of looking, eleven enclosures, and five things standing between a click and a permanent delete.
Seven probes, not one query
A relation row is one of at least seven ways one piece of content can point at another, and it is the only one most cleanup tools look at. An image dragged into a rich-text field creates no relation row at all — so a relations-only check will offer to delete it while it is visibly on the page.
- Relations, nested-entry ownership, structure parentage, reference tags, markup, templates, keyed columns
- The last three are in no table Craft can be asked about
It reads your templates
craft.entries.id(412) in a Twig file is a reference that no database column records anywhere, and it is exactly the kind a bulk delete destroys silently. Zoo walks your templates and your config looking for ids used as ids, and for quoted literals matching a slug, a URI or a filename.
- Integers only where they are being used as ids, so a loop index keeps nothing alive
- Says how many files it read, so a failed walk cannot look like a clean result
It reads the schema, not a list of plugins
Zoo has never heard of Commerce. It asks information_schema for every column with a foreign key into elements, and queries each one — a couple of dozen on a plain install, over a hundred and fifty on a site with Commerce, Formie and Navigation on it.
- A variant sitting in a five-year-old order keeps itself alive automatically
- Craft’s own bookkeeping is excluded by a list published on the settings screen
Eleven enclosures
Unused assets, unreferenced entries, orphaned Matrix blocks, abandoned drafts, empty categories, unused tags, empty folders, dormant users, dangling relations, fields in no layout and unused entry types.
- The schema and account audits are off by default — what they delete is project config, not content
- Add your own with one method and an event
Nothing is deleted from a stored list
A finding is a claim about the site at the moment the census ran. Between then and now somebody may have dragged that image into a page. So every element is proved again immediately before it goes, through the same call the census used, against an index built from the site as it is now.
- Anything that has picked up a reference is marked “left alone” and shown with the reference that saved it
- Never skipped silently, and never deleted on trust
A keep list, with reasons
Every site has content that is meant to be unreferenced — a press-kit PDF linked only from an email campaign, an entry an integration fetches by handle. Mark it kept and no census reports it again. An audit that reports the same false positive every month is one people stop reading.
- The reason is required: whoever reads the keep list next will not be you
- Anything on it can be put back in circulation in one click
Frequently Asked Questions
The questions worth answering before you point a deletion tool at your content.
Entirely, permanently, with no editions and no licence key. Every feature on this page is in the one version there is, and there is no licensing code in the plugin to remove later.
It cannot delete anything you have not selected and submitted. Beyond that: the default is Craft’s trash rather than a permanent delete, every element is re-proved immediately before it goes, a finding whose element has been edited since the census is refused, a census older than an hour cannot be acted on at all, a permanent delete has to be armed by typing the number of rows selected, and a database backup is taken first — and if the backup fails, the delete does not happen.
The honest answer is that any tool that deletes things can be wrong. Zoo’s design goal is that being wrong requires ignoring a screen that told you exactly what it had and had not checked.
Because it is wrong on every real site, and wrong in the direction that deletes things. An image dragged into a CKEditor field creates no relation row at all — it is an <img src> inside a JSON column. A reference tag is a string. A hard-coded id in a template is in no database anywhere. A variant held by a five-year-old Commerce order is a foreign key in somebody else’s table.
Those four are invisible to a relations-only query, and they are precisely the references whose loss you notice on a published page rather than in a log.
No. Zoo makes no outbound HTTP request anywhere — not at install, not during a census, not on a schedule. Everything it knows it works out from your own database, your own templates and your own schema.
Craft’s GC removes things Craft itself created and no longer needs — expired sessions, orphaned search rows, elements past softDeleteDuration. It has no opinion about your content. Zoo audits the content: the assets, entries, categories and tags nothing points at any more.
Nuke deletes what you name — a section, a volume, a group — with its drafts, revisions and relations, and sweeps the housekeeping tables on a schedule. Zoo finds what nobody named.
Nuke’s question is “delete all of this”. Zoo’s is “what is nobody using”. They compose, and neither needs the other.
No. An entry with its own URL counts as reachable by default, because a page is reachable whether or not anything on the site links to it — bookmarks, sitemaps, campaign emails and search results all point at pages without creating a row anywhere.
You can turn that off, and then you get a genuinely useful and completely different list: pages linked from nowhere on the site. That is not a list of things that are safe to delete, and Zoo says so on the setting.
Yes, without knowing they exist. Zoo reads information_schema for every column with a foreign key into elements and asks each one, so an element held by a line item, a submission or a navigation node keeps itself alive automatically.
The one thing it will not see is a plugin storing element ids in a column with no foreign key — a JSON blob of settings, say. Check the evidence on a few findings and keep-list anything held that way.
No. Zoo does nothing during a front-end request. There is no response filter, no Twig hook and no listener on saving. It runs when you press the button, and on a large site you queue it.
Yes, and reading the census is the safe half — zoo/census/report costs nothing. Give editors “view the census” and “mark things to keep” without “move findings to the trash” and they can see what the site is carrying, and correct Zoo when it is wrong, without being able to remove anything.
Candidates are per element, not per site row, and every site’s URI is given to the probes. An entry that is an orphan in one site and linked in another is not an orphan.
Its three tables are dropped and the project-config entry goes. Nothing Zoo found or deleted is affected — the deletions were Craft’s own, and anything in the trash is still in the trash.
Craft CMS 5.3+ and PHP 8.2+, with no runtime dependencies beyond Craft’s own and no build step.
Find out what your site is carrying
Free, no editions, no licence key. Install it, take a census, and read the evidence before you delete a single thing.