WP Manifestindependent plugin directory
manifest / content / expire

Expire

Expiration date for post types

★ 0stars

Install

Download source zip No release zip yet

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.zip
No built-in updater. New versions won't appear under Dashboard → Updates. Install new releases yourself, or let Git Updater track the repository.

Schedule 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

  1. Upload this directory to wp-content/plugins/expire.
  2. Activate Expire in Plugins.
  3. Edit a post or page and click Never in the Expires row directly below Publish in the editor's document settings.
  4. 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_rest for 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-date Unix 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.