Custom WordPress Importer vs Plugin: How to Choose
By Luke O'Regan
Published
Use a custom importer when the job is narrow and stable, and someone can maintain the code. Choose a plugin when you need its broader feature set, interface, support or built-in handling of edge cases.
I keep coming back to a practical question: which option meets this site’s needs with the least complexity?
Start with the job to be done
A general-purpose importer has to work across different sites, data sources and content models. That breadth helps when your requirements vary. For example, WordPress.org lists import plugins that support XML and RSS sources, multiple post types, field mapping and custom fields.8
Some projects need much less: read from one known source, map a few fields to a content type, and update existing entries. If that workflow is stable, a focused integration can be easier to manage than a large set of general-purpose settings.
That does not make the plugin a poor choice, and custom code is not automatically faster, cheaper or safer. Choose based on the requirements, not a blanket rule to minimise the plugin count.
Plugin or custom importer: a comparison
| Decision | Maintained import plugin | Focused custom importer |
|---|---|---|
| Sources and mapping | Useful when formats or mappings vary; check supported features | Fits one predictable source and a small fixed mapping |
| Editorial control | May offer an interface; verify update and overwrite settings | Must explicitly define source-owned fields and review rules |
| Repeat runs | Check matching rules, retries and concurrency behaviour | You own stable identifiers, locking and duplicate recovery |
| Maintenance | Updates and support depend on the supplier and licence | Your team owns tests, security, compatibility and documentation |
| Cost | Compare licence, setup and ongoing operation | Compare development, monitoring and long-term ownership |
Neither option is automatically faster or safer. Test the workflow, including failed imports and repeat runs.
When a focused importer may make sense
A small custom integration is worth considering when:
- the source and content structure are predictable;
- the site only needs a small, clearly defined part of a broader importer’s functionality;
- matching existing entries can be based on a stable identifier;
- the team can test, document and maintain the integration; and
- the import has a clear recovery path when the source or network is unavailable.
If the source changes frequently, the mapping is managed by non-developers, or the site needs many import formats and options, a maintained plugin may be the more responsible choice.
The important bit: make repeat runs safe
An automated import may run more than once. If the code cannot recognise an entry it has already processed, repeat runs can create duplicates. A stable ID from the source gives the integration a way to find the corresponding WordPress post and update it instead of inserting another copy.
This small, fictional example shows only the persistence step. It accepts a normalised item from a parser; it does not fetch or parse a feed. It uses synthetic keys, creates new posts as drafts, and keeps an existing post’s status on later imports. Treat it as a teaching example, not production code.
/**
* Save one normalised external item as a reviewable WordPress post.
* Assumes imports run serially and the source provides a stable ID.
*
* @param array $item Normalised data with id, title, and optional content.
* @return int|WP_Error Post ID on success, WP_Error on failure.
*/
function demo_save_imported_item( $item ) {
if (
! isset( $item['id'], $item['title'] ) ||
! is_scalar( $item['id'] ) ||
! is_scalar( $item['title'] )
) {
return new WP_Error( 'invalid_item', 'The item is missing an ID or title.' );
}
$external_id = sanitize_text_field( (string) $item['id'] );
$title = sanitize_text_field( wp_strip_all_tags( (string) $item['title'] ) );
$content = isset( $item['content'] ) && is_scalar( $item['content'] )
? wp_kses_post( (string) $item['content'] )
: '';
if ( '' === $external_id || '' === $title ) {
return new WP_Error( 'invalid_item', 'The item has no usable ID or title.' );
}
$matches = get_posts( array(
'post_type' => 'post',
'post_status' => 'any',
'posts_per_page' => 1,
'fields' => 'ids',
'no_found_rows' => true,
'meta_key' => '_demo_external_id',
'meta_value' => $external_id,
) );
$post = array(
'post_type' => 'post',
'post_status' => 'draft',
'post_title' => $title,
'post_content' => $content,
'meta_input' => array(
'_demo_external_id' => $external_id,
),
);
if ( ! empty( $matches ) ) {
$post['ID'] = (int) $matches[0];
$post['post_status'] = get_post_status( $post['ID'] ) ?: 'draft';
}
return wp_insert_post( wp_slash( $post ), true );
} Limits of this example
- Concurrency: the lookup and insert are separate operations, not an atomic upsert. Two overlapping workers can both find no match and create duplicates. Serial scheduling alone does not guarantee exclusivity; production code needs a reliable lock or a unique source-to-post mapping with conflict handling and recovery.
- Editorial overwrites: preserving a post’s status does not preserve its title or content. Every successful rerun overwrites both, and missing content becomes an empty string. Decide which fields the source owns, preserve editorial changes where needed, and consider a review queue before updating published content.
- Identifiers:
sanitize_text_field()is not an identity-preserving encoding. Distinct raw IDs may collapse to the same value. Validate the source’s ID format, preserve identity without lossy normalisation, namespace IDs by source, and define how to handle ID changes, collisions, deleted posts and multiple existing matches. This example selects only the first match and does not repair duplicates.
WordPress’s wp_insert_post() creates a post when no ID is supplied and updates the post when an existing ID is supplied; it can return a WP_Error when requested.2 The example uses wp_kses_post() to filter post content to the HTML allowed in post content.4 When displaying any stored value later, output should still be escaped for its specific context.
The example leaves out feed retrieval, parsing, scheduling, logging, admin controls and tests. A production importer needs to handle those too. If users can set the import URL, use WordPress’s safe HTTP request functions. wp_safe_remote_get() validates the URL and redirect targets to help guard against server-side request forgery.3 Plan for failed requests, malformed responses and items missing required fields.
Scheduling is not the same as guaranteed timing
WordPress’s built-in WP-Cron system checks scheduled work during page loads rather than running continuously. That makes it convenient, but a quiet site may not execute a task at the exact time it is due.5 If timing matters, decide whether WP-Cron is sufficient or whether a real server scheduler should trigger the task. In either case, avoid scheduling the same recurring event repeatedly, and clean up scheduled tasks when they are no longer needed.6
Custom code still has a maintenance bill
A focused importer means your team owns compatibility checks, tests, error reporting and documentation. Someone also needs to update it when the source format or site requirements change. If the importer should survive a redesign, put it in a site-specific plugin rather than the theme. WordPress’s plugin handbook recommends keeping small plugins simple and giving custom code a clear prefix or namespace.7
Some import plugins already offer admin controls and support for multiple formats, post types and custom fields.8 Rebuilding features for one narrow workflow may create more work than it saves. If you use only a small, stable part of a plugin, a focused integration may be easier to maintain.
A quick decision checklist
Before replacing an import plugin, ask:
- Does a maintained plugin already meet the actual requirements?
- Which plugin features are genuinely needed, and which are unused complexity?
- Is the source stable enough for a fixed mapping?
- Can the import identify previously processed items reliably?
- What should happen when a fetch or parse fails?
- Who will maintain and test the custom code?
- Does this functionality need to survive a theme change?
If the workflow is narrow and stable, and someone can maintain it, custom code may fit. For broader or changing requirements, a well-supported plugin is often simpler. Choose the option that meets the actual need with the least complexity, and make sure someone owns its upkeep.
Related reading
For another small, self-contained WordPress example, see how to disable HTML in WordPress comments without a plugin.1
Sources
- [WordPress comments article](https://lbor.dev/how-to-disable-html-in-wordpress-comments-without-a-plugin/)
- [WordPress wp_insert_post reference](https://developer.wordpress.org/reference/functions/wp_insert_post/)
- [WordPress wp_safe_remote_get reference](https://developer.wordpress.org/reference/functions/wp_safe_remote_get/)
- [WordPress wp_kses_post reference](https://developer.wordpress.org/reference/functions/wp_kses_post/)
- [WordPress Cron handbook](https://developer.wordpress.org/plugins/cron/)
- [Scheduling WP-Cron events](https://developer.wordpress.org/plugins/cron/scheduling-wp-cron-events/)
- [WordPress plugin best practices](https://developer.wordpress.org/plugins/plugin-basics/best-practices/)
- [WordPress.org Import XML and RSS Feeds plugin](https://wordpress.org/plugins/import-xml-feed/)