Documentation

Developer documentation for Default Permalink: hooks, options and customization.

How slug replacement works

The plugin hooks into the wp_unique_post_slug filter (priority 10, 5 arguments) and replaces the slug only when all of the following conditions are met:

  1. The df_enable option is "Enable" (enabled on the settings page).
  2. The post type is targeted (all public post types when the option has never been saved).
  3. The slug matches the regular expression /(%[0-9a-f]{2})+/ (contains URL-encoded multi-byte characters).
  4. The user has not explicitly specified a slug (no post_name in the saved input; recorded at the start of the save via the wp_insert_post_empty_content filter).

For new posts created without an ID yet (REST API, WP-CLI, direct wp_insert_post() calls), an empty slug is returned first and the slug is replaced when wp_insert_post() regenerates it with the new ID after inserting. The generated slug is made unique with wp_unique_post_slug() (a suffix such as -2 is added on conflicts).

When a slug is replaced on an actual save, the new slug is stored in the _dp_slug_replaced post meta for the notice (permalink previews do not record it). In the block editor, it is included in the REST response of the save as dp_slug_replaced, the meta is deleted and the editor shows the notice. In the classic editor, the notice is shown on the next load of the edit screen.

The notice is recorded only for saves from the admin screens (except admin-ajax) and the REST API (since 1.9.2). Saves from WP-CLI, imports (WP_IMPORTING), cron (DOING_CRON) and ajax requests such as Quick Edit do not record it (the slug is still replaced). Change this with the default_permalink_record_notice filter (bool). In the block editor, the notice is shown only when the record matches the current slug of the post (since 1.10.2: an old record is not reported when the slug was changed in the save being made).

With the “post date and ID” format (date), saves of existing posts use the date being saved (for example the scheduled date; the current time for a draft without a fixed date) (since 1.10.1). A slug typed in the permalink box of the classic editor is not replaced in the preview either.

Options

OptionValuesDescription
df_enable"Enable" / ""Whether the plugin’s behavior is enabled
df_slug_formatpost_type / date / randomSlug format (default: post_type)
df_post_typesArray of post type namesTargeted post types. All public post types when never saved (installs older than v1.4)
df_taxonomiesArray of taxonomy namesTaxonomies whose term slugs are replaced (since 1.7.0). Empty (none) by default. post_format cannot be selected

The default_permalink_slug filter

The generated slug is passed through the default_permalink_slug filter, so themes and other plugins can freely customize the format.

/**
 * Example: change the generated slug to "item-{post_ID}".
 *
 * @param string $slug      The generated slug.
 * @param int    $post_id   The post ID.
 * @param string $post_type The post type.
 */
add_filter( 'default_permalink_slug', function ( $slug, $post_id, $post_type ) {
	return 'item-' . $post_id;
}, 10, 3 );

The default_permalink_term_slug filter

Slugs of new terms in the targeted taxonomies ({taxonomy}-{term_id} by default, with _ replaced by -) are passed through the default_permalink_term_slug filter. The result is made unique with sanitize_title() and wp_unique_term_slug().

/**
 * Example: change term slugs to "topic-{term_ID}".
 *
 * @param string $slug     The generated slug (e.g. category-12).
 * @param int    $term_id  The term ID.
 * @param string $taxonomy The taxonomy name.
 */
add_filter( 'default_permalink_term_slug', function ( $slug, $term_id, $taxonomy ) {
	return 'topic-' . $term_id;
}, 10, 3 );

The default_permalink_record_notice filter

Decides whether a save that replaced the slug leaves the notice record (_dp_slug_replaced) behind (since 1.9.2). The argument is the default decision (true for saves from the admin screens or the REST API, false for WP-CLI, imports, cron and ajax). To record it for every save, as in 1.9.1 and earlier:

add_filter( 'default_permalink_record_notice', '__return_true' );

WP-CLI commands

  • wp default-permalink status: shows whether the plugin is enabled, the slug format, the targeted post types and taxonomies, and the number of posts to convert.
  • wp default-permalink convert [--dry-run]: runs the same conversion as the settings screen without the 50-post limit. --dry-run lists the posts and their new slugs without changing anything. It moves on past posts that cannot be converted and exits with code 1 if any failed.
  • Options of convert (since 1.10.0): --post_type=<types> (comma-separated; non-targeted types are skipped with a warning), --limit=<number> (stops after that many posts and prints the remaining count and the --after value to continue), --after=<id> (since 1.10.1; processes only posts after that ID).
  • The bulk conversion (settings screen and WP-CLI) does not change the status or the publish schedule of a post. A scheduled post whose time has passed stays scheduled; only its slug is converted (since 1.10.2).

Site Health

The plugin adds the direct test default_permalink through the site_status_tests filter (since 1.10.0, WordPress 5.2 or later). It is “recommended” (with a link to the settings page) when df_enable is not "Enable", and “good” when enabled. Remaining posts with a multi-byte slug keep it “good” and add their number and a link to the bulk conversion to the description (since 1.10.1).

Uninstall

Deleting the plugin runs uninstall.php, which removes all of the options above and the _dp_slug_replaced notice post meta. On multisite, it removes them from every site of the network (since 1.9.2). Converted post slugs are part of the post data itself and remain unchanged.

Source code

Copied title and URL