A WP-CLI plugin that imports posts (with their categories and tags) from another WordPress site's REST API, and can later delete everything it imported.
It adds two WP-CLI commands: wp import-posts and wp delete-imported-posts. There is no admin UI — everything runs from the command line.
- WordPress 4.5+
- PHP 5.6+
- WP-CLI installed and available on the server
- The source site must expose the standard WordPress REST API (
/wp-json/wp/v2/posts)
- Copy the plugin folder into
wp-content/plugins/. - Activate it (via wp-admin or
wp plugin activate wp-cli-post-migration). - Run the commands below with WP-CLI.
Imports posts from another WordPress site into the current site.
wp import-posts
It will prompt for a REST API URL:
Importing posts...
Enter REST API Url to fetch posts:
> https://example.com/wp-json/wp/v2/posts
The URL must:
- be a valid URL,
- use
https, - contain
wp-json/wp/v2/posts.
The command then paginates through that endpoint (10 posts per page) and, for each post, inserts it into the local site if a post with the same title doesn't already exist.
Removes posts that were previously imported.
wp delete-imported-posts
It re-fetches the same source REST API URL used during the last wp import-posts run (stored internally, see below) and, for each post found there, deletes the matching local post permanently.
When you run wp import-posts, the REST API URL you enter is saved with:
set_transient( 'rest_api_url_to_import_posts', $rest_api_url, 60 * 60 * 24 );This transient (rest_api_url_to_import_posts, 24-hour lifetime) is what wp delete-imported-posts later reads to know which source site to re-check against when deleting.
handleFetchPosts( $rest_api_url, $page, $per_page ) in cli/manage-posts.php appends page and per_page query args to the URL, calls wp_remote_get(), and decodes the JSON response body. It also reads the X-WP-TotalPages response header so the caller knows when to stop paginating.
Both import_posts() and delete_imported_posts() loop over pages (10 posts per page) until page > total_pages.
gs_check_post_exists( $post_title ) runs a WP_Query for a post with a matching title (post_status => any). This is used in two places:
- During import, to skip posts that already exist locally.
- During delete, to find the local post ID that corresponds to a remote post's title, so it can be removed.
gs_manage_posts( $post ) is the core import routine for a single remote post:
- Skips the post if a local post with the same title already exists (logs a warning).
- Otherwise inserts it with
wp_insert_post(), copyingtitle.rendered→post_titleandcontent.rendered→post_content, publishing it underpost_author => 1. - Resolves and assigns categories: for each remote category ID, it swaps
postsforcategoriesin the saved source URL, fetches{taxonomy_url}/{id}to get the category name viafetch_taxonomy_name_from_api(), and creates/reuses a localcategoryterm withwp_insert_term()/term_exists(). - Does the same for tags, swapping
postsfortagsin the source URL and creating/reusingpost_tagterms. - Assigns the resolved term IDs with
wp_set_post_terms().
delete_imported_posts() re-fetches the same source pages, and for each remote post it finds the matching local post (via gs_check_post_exists()) and permanently deletes it with wp_delete_post( $post_id, true ) (bypassing trash).
Posts are matched between the source and local site by title only — there's no stored mapping (e.g. postmeta) linking an imported post back to its source ID. If two unrelated posts share the same title, or a title changes between import and delete, the match can be wrong or missed.
wp-cli-post-migration.php Plugin bootstrap, loads cli/manage-posts.php
cli/manage-posts.php Manage_Posts class — both WP-CLI commands live here
readme.txt WordPress.org-format readme
pluginaudit.md WordPress.org Plugin Directory guideline audit