Skip to content

Developer Guide

Technical reference for extending and integrating with the Media Cleanup plugin.

All endpoints use the vmfa-cleanup/v1 namespace and require the manage_options capability.

MethodEndpointDescription
POST/scanStart a new scan. Optional body: { "types": ["unused","duplicate","oversized"] }
GET/scan/statusGet scan progress (status, phase, progress, total)
POST/scan/cancelCancel a running scan
POST/scan/resetReset all scan results
GET/statsDashboard statistics (unused_count, duplicate_count, oversized_count)
MethodEndpointDescription
GET/resultsPaginated list. Params: type, page, per_page, orderby, order
GET/results/{id}Detail for a single attachment
GET/duplicatesPaginated duplicate groups. Params: page, per_page
MethodEndpointDescription
POST/actions/trashTrash items. Body: { "ids": [...], "confirm": true }
POST/actions/restoreRestore trashed items. Body: { "ids": [...] }
POST/actions/deletePermanently delete. Body: { "ids": [...], "confirm": true }
POST/actions/archiveArchive to virtual folder. Body: { "ids": [...], "confirm": true }
POST/actions/flagFlag for review. Body: { "ids": [...] }
POST/actions/unflagRemove flag. Body: { "ids": [...] }
POST/actions/set-primarySet primary in duplicate group. Body: { "id": ..., "group_ids": [...] }
MethodEndpointDescription
GET/settingsGet current settings
POST/settingsUpdate settings (partial merge)
HookParametersDescription
vmfa_cleanup_scan_complete$resultsFires after a scan finishes. $results is an array of scan results grouped by type.
vmfa_cleanup_before_bulk_action$action, $idsFires before any bulk action
vmfa_cleanup_media_archived$attachment_id, $folder_idFires after archiving a media item. $folder_id is the archive folder term ID.
vmfa_cleanup_media_trashed$attachment_idFires after trashing a media item
vmfa_cleanup_media_flagged$attachment_idFires after flagging a media item
vmfa_cleanup_settings_updated$updated, $currentFires after settings are saved. $updated is the new settings, $current is the previous settings.
FilterDefaultDescription
vmfa_cleanup_is_unusedtrueOverride whether an attachment is unused. Return false to skip.
vmfa_cleanup_oversized_thresholdsper-type MB valuesModify size thresholds. Array keyed by MIME prefix (image, video, audio, document).
vmfa_cleanup_archive_folder_name"Archive"Change the archive virtual folder name
vmfa_cleanup_hash_algorithm"sha256"Change the file hash algorithm
vmfa_cleanup_reference_meta_keys[]Add custom meta keys to scan for attachment references
vmfa_cleanup_reference_sources[]Add custom reference sources beyond post content and featured images

Skip certain post types from unused detection

Section titled “Skip certain post types from unused detection”
add_filter( 'vmfa_cleanup_is_unused', function ( $is_unused, $attachment_id ) {
// Never flag WooCommerce product images as unused.
$product_ids = get_posts( [
'post_type' => 'product',
'meta_key' => '_thumbnail_id',
'meta_value' => $attachment_id,
'fields' => 'ids',
] );
return empty( $product_ids ) ? $is_unused : false;
}, 10, 2 );
add_filter( 'vmfa_cleanup_oversized_thresholds', function ( $thresholds ) {
$thresholds['image'] = 1;
return $thresholds;
} );
add_action( 'vmfa_cleanup_scan_complete', function ( $results ) {
// $results contains scan results grouped by type.
wp_mail( 'admin@example.com', 'Media scan complete', count( $results ) . ' issues found.' );
} );
vmfa-media-cleanup/
├── src/
│ ├── js/
│ │ ├── components/ # React components (Dashboard, Panels, Modals)
│ │ ├── hooks/ # Custom hooks (useScanStatus, useResults, useSettings)
│ │ └── index.js # Entry point
│ ├── php/
│ │ ├── CLI/ # WP-CLI command registration
│ │ ├── Detectors/ # UnusedDetector, DuplicateDetector, OversizedDetector
│ │ ├── REST/ # ScanController, ResultsController, ActionsController, SettingsController
│ │ ├── Services/ # ScanService, ReferenceIndex, HashService
│ │ ├── Update/ # GitHubPluginUpdater
│ │ └── Plugin.php # Bootstrap class
│ └── styles/
│ └── admin.scss # Admin dashboard styles
├── build/ # Compiled assets (generated)
├── languages/ # Translation files (POT, PO, MO, JSON, PHP)
├── tests/
│ ├── js/ # Vitest component & hook tests
│ └── unit/ # Pest PHP unit tests
├── docs/ # Developer documentation
├── i18n-map.json # Source → script handle map for JSON translations
├── package.json # Node dependencies & scripts
├── composer.json # PHP dependencies
└── vmfa-media-cleanup.php # Plugin entry file
  • Node.js 18+
  • PHP 8.3+
  • Composer 2+
Terminal window
composer install
npm install
Terminal window
npm run build # Production build
npm start # Watch mode with hot reload
Terminal window
npm test # JS tests (Vitest)
vendor/bin/pest # PHP tests (Pest)
Terminal window
npm run lint:js # ESLint
composer lint # PHP_CodeSniffer
Terminal window
npm run i18n # Full pipeline: POT → update PO → MO → JSON → PHP
npm run i18n:make-pot # Extract strings only

Individual steps: i18n:make-pot, i18n:update-po, i18n:make-mo, i18n:make-json, i18n:make-php.

The make-json step converts PO translations into JSON files that WordPress loads for JavaScript via wp_set_script_translations(). It needs to know which script handle each source file belongs to, so it can generate the correct filename hash.

i18n-map.json maps every source JSX file that contains translatable strings to the script handle (vmfa-media-cleanup-admin):

{
"src/js/components/BulkActionBar.jsx": "vmfa-media-cleanup-admin",
"src/js/components/CleanupDashboard.jsx": "vmfa-media-cleanup-admin",
...
}

The keys must use source file paths (not build/index.js), because the PO file’s #: reference comments point to source files. WordPress computes the JSON filename as {textdomain}-{locale}-{md5(handle)}.json, so the map ensures the hash matches the handle registered with wp_set_script_translations().

When adding a new component that uses __() or _n(), add its path to this file.