Custom WordPress Importer vs Plugin: How to Choose
By Luke O'Regan
Published
A custom WordPress importer can be a better fit than a general-purpose plugin when a site has one narrow, stable import workflow—and someone is prepared to maintain the code. A plugin is often the better choice when its flexibility, interface, support and established edge-case handling outweigh the cost of features the site does not need.
That is the trade-off I have increasingly found myself weighing: not “plugins or code?”, but “what is the simplest solution that fits this particular job?”
Start with the job to be done
A general-purpose importer is designed to work across different sites, data sources and content models. That breadth can be valuable. For example, WordPress.org lists import plugins that support XML and RSS sources, multiple post types, field mapping and custom fields.8
But some projects have a much narrower requirement: read entries from one known source, map a small set of fields into a particular content type, and keep existing entries up to date. If the workflow is stable and understood, a focused integration may be easier to reason about than a large set of general-purpose settings.
That does not mean the plugin is poor, or that custom code is automatically faster, cheaper or safer. It means the choice should reflect the real 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 approach is automatically faster or safer. Test the actual workflow, including failures and repeated 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.
Here is a deliberately small, fictional example of that persistence step. It accepts a normalised item from a parser; it does not fetch or parse a real feed. The example uses synthetic keys, creates new items as drafts for editorial review, and preserves the status of an existing item on later imports. It is an educational pattern, not production-ready importer 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 deliberately leaves out feed retrieval, parser behaviour, scheduling, logging, admin controls and tests. Those are not minor details in a production importer. If an import URL can be configured by a user, use WordPress’s safe HTTP request functions and validate the URL; wp_safe_remote_get() validates the URL and redirect targets to help guard against server-side request forgery.3 Also consider what happens when the remote source returns an error, a malformed response, or an item without 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 trades configuration breadth for responsibility. Someone needs to own compatibility checks, tests, error reporting, documentation and changes when the source format or site requirements evolve. The code should live in a place that matches its purpose: if the functionality needs to survive a visual redesign, it generally belongs in a site-specific plugin rather than being coupled to the theme. WordPress’s plugin handbook also recommends keeping small plugins simple and using a clear prefix or namespace for custom code.7
Some import plugins already offer admin controls, field mapping and support for multiple XML/RSS formats, post types and custom fields.8 Rebuilding a broad set of features for one narrow workflow can create more work than it removes. Conversely, if a site uses only a small, stable subset of a much broader importer, a focused integration can make the intended behaviour and ownership clearer.
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 answers point to a narrow, durable workflow and an available maintainer, custom code may be a good fit. If the requirement is broad or still changing, a well-supported plugin is often the simpler option.
The aim is not to write more code. It is to choose the least complicated solution that meets the need—and to be honest about who owns it afterwards.
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/)