Install
This is the repository archive. It installs, but the folder name will carry the branch suffix and updates will not flow.
Install straight from GitHub with WP-CLI:
wp plugin install https://github.com/horttcore/expire/archive/refs/heads/master.zipSchedule content expiration in WordPress. Expired content moves to a dedicated, non-public Expired post status instead of being deleted or converted to draft.
Requires WordPress 7.1+ and PHP 8.4+.
Installation
- Upload this directory to
wp-content/plugins/expire. - Activate Expire in Plugins.
- Edit a post or page and click Never in the Expires row directly below Publish in the editor's document settings.
- Choose a date and time in the calendar popover, then save the post normally.
Dates use the timezone configured in Settings > General, not the browser or server timezone. The classic editor has a date/time field in the Publish box. Choose Never in the popover, or clear the classic-editor field, to remove a schedule. Opening or closing the popover alone does not change the date.
Expiration and recovery
- Public posts, pages and public custom post types are supported automatically,
excluding attachments. A custom type needs
show_in_restfor the block editor. - At the deadline, published, private and scheduled content moves to Expired. Drafts and pending submissions are not expired automatically.
- Expired content remains editable in the dashboard, with an Expired filter in the content list. The expiration column is sortable, including undated posts.
- Quick Edit and Bulk Edit include the Expired status.
- The editor also offers Mark as expired and Restore to draft. Changes take effect when saved. To republish, clear the old deadline or move it into the future, restore to draft, then publish normally.
- WordPress's built-in block-editor status selector does not list custom statuses.
Use the expiration popover to manage Expired content and restore it to draft.
While the post status is
expired, the summary shows Status: Expired in place of the empty core status control. All other statuses keep the native WordPress control; a past expiration date alone does not trigger this label. - Clearing or extending the date does not automatically republish expired content.
- Existing
_expiration-dateUnix timestamps are preserved. The periodic sweep picks up overdue content from older versions, 100 items per run.
WordPress runs scheduled jobs when traffic triggers WP-Cron. Each dated post gets
a single event and a five-minute fallback sweep handles missed/legacy schedules.
Public queries (including secondary queries, Query Loop blocks and feeds) exclude
overdue dates even before cron changes the status. Public REST responses also
exclude overdue content; authorized context=edit requests remain available.
Existing meta queries are preserved.
Caches: a previously cached page, feed or API response can outlive its expiration. Configure the page cache/CDN TTL or purge mechanism accordingly. For reliable status transitions on quiet sites, invoke WP-Cron through a real system scheduler. To run a sweep manually:
wp cron event run expire
Deactivation removes the plugin's scheduled jobs but retains dates and statuses. While inactive, date-based hiding stops; the custom status is registered again on reactivation. No content is deleted.
Developer API
The existing template helpers remain available:
$timestamp = get_expiration_date( get_the_ID() ); // int or null.
$formatted = get_expiration_date( get_the_ID(), true ); // Site date format.
the_expiration_date( 'Y-m-d H:i' ); // Escaped output in the site timezone.
expire-date-format still filters the list-table display format. The obsolete
jQuery picker and its configuration filters have been removed; controls now use
WordPress components and the native classic-editor date/time input.
REST-enabled supported types expose protected meta under meta._expiration-date
in the edit context. Its value is a Unix timestamp string or "" for no
expiration, retaining the legacy storage format. Core REST authorization and an
edit_post capability check protect writes. The plugin enables custom-fields
support on these types because core requires it for REST meta persistence.
{ "meta": { "_expiration-date": "1893456000" } }
The status slug is expired. Core's post REST endpoint supports this status for
authorized updates. A non-public custom type can opt in before init priority 99
using add_post_type_support( $post_type, 'expire' ). To opt a public type out,
remove support after that priority.
Trusted internal WP_Query calls can set expire_include_expired => true to
bypass the date filter; this does not bypass WordPress status/capability checks.
Scheduling and transition failures are logged with an Expire: prefix and emit
expire_error with a WP_Error argument.
Development
No Node build step or third-party runtime dependencies are required. Editor assets use the packages bundled with WordPress.
The date row uses the official PluginPostStatusInfo slot. A small stylesheet
targets WordPress 7.1's summary layout to place it directly below Publish;
reading-flow: flex-visual also aligns keyboard navigation with the visual order
in supporting browsers. No core files or React-managed DOM nodes are modified.
The popover uses the public Dropdown and DateTimePicker components rather
than WordPress's private publishing-picker API.
Run the integration tests against a disposable local WordPress installation:
wp eval-file wp-content/plugins/expire/tests/integration.php
The tests create temporary content and users, exercise real REST requests and cron callbacks, and clean up their fixtures and schedules.
Changelog
2.0.0
- Require WordPress 7.1 and PHP 8.4.
- Add a non-public Expired status with admin filters and recovery controls.
- Add an inline block-editor expiration date with a calendar popover and protected REST metadata.
- Replace the bundled jQuery picker with native classic-editor controls.
- Add per-post scheduling, a bounded fallback sweep and deactivation cleanup.
- Correct timezone handling, validate dates, preserve existing query conditions, fix list-table hooks and retain undated content when sorting.
- Preserve legacy expiration dates and template helpers.
0.1.1
- Add a sortable expiration column and fix date formatting.
0.1.0
- Initial release.