DEV3 Technologies home

One-click demo content importer

An admin tools page that imports pages and posts from a bundled JSON file idempotently, behind a nonce and capability check.

WordPressUtilitiesimportdemo-contentadmin-pagejson

Code

<?php
/**
 * Demo content importer (Tools -> Demo content).
 *
 * Reads demo/content.json: [ { "key": "home", "type": "page", "title": "Home", "content": "<p>...</p>" } ]
 *
 * @package Acme_Theme
 */

defined( 'ABSPATH' ) || exit;

add_action( 'admin_menu', static function (): void {
	add_management_page( __( 'Demo content', 'acme-theme' ), __( 'Demo content', 'acme-theme' ), 'manage_options', 'acme-demo', 'acme_demo_page' );
} );

function acme_demo_page(): void {
	if ( ! current_user_can( 'manage_options' ) ) {
		wp_die( esc_html__( 'Not allowed.', 'acme-theme' ) );
	}
	$done = isset( $_GET['imported'] ) ? absint( $_GET['imported'] ) : null;
	?>
	<div class="wrap">
		<h1><?php esc_html_e( 'Demo content', 'acme-theme' ); ?></h1>
		<?php if ( null !== $done ) : ?>
			<div class="notice notice-success"><p><?php echo esc_html( sprintf( _n( '%d item imported.', '%d items imported.', $done, 'acme-theme' ), $done ) ); ?></p></div>
		<?php endif; ?>
		<form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>">
			<input type="hidden" name="action" value="acme_demo_import">
			<?php wp_nonce_field( 'acme_demo_import' ); ?>
			<?php submit_button( __( 'Import demo content', 'acme-theme' ) ); ?>
		</form>
	</div>
	<?php
}

add_action( 'admin_post_acme_demo_import', static function (): void {
	if ( ! current_user_can( 'manage_options' ) ) {
		wp_die( esc_html__( 'Not allowed.', 'acme-theme' ), '', array( 'response' => 403 ) );
	}
	check_admin_referer( 'acme_demo_import' );

	$file = get_template_directory() . '/demo/content.json';
	$rows = is_readable( $file ) ? json_decode( (string) file_get_contents( $file ), true ) : array();
	$new  = 0;

	foreach ( is_array( $rows ) ? $rows : array() as $row ) {
		$key  = sanitize_key( $row['key'] ?? '' );
		$type = in_array( $row['type'] ?? '', array( 'page', 'post' ), true ) ? $row['type'] : 'page';
		if ( '' === $key ) {
			continue;
		}

		// Skip items imported before, so running it twice never duplicates.
		$existing = get_posts( array( 'post_type' => $type, 'post_status' => 'any', 'meta_key' => '_acme_demo_key', 'meta_value' => $key, 'fields' => 'ids', 'numberposts' => 1 ) );
		if ( $existing ) {
			continue;
		}

		$id = wp_insert_post( array(
			'post_type'    => $type,
			'post_status'  => 'publish',
			'post_title'   => sanitize_text_field( $row['title'] ?? $key ),
			'post_content' => wp_kses_post( $row['content'] ?? '' ),
		), true );

		if ( ! is_wp_error( $id ) ) {
			update_post_meta( $id, '_acme_demo_key', $key );
			if ( 'home' === $key ) {
				update_option( 'show_on_front', 'page' );
				update_option( 'page_on_front', $id );
			}
			++$new;
		}
	}

	wp_safe_redirect( add_query_arg( 'imported', $new, admin_url( 'tools.php?page=acme-demo' ) ) );
	exit;
} );

How it works

What it solves

Buyers of a theme want the preview site, not an empty install. A small importer creates sample pages and posts from a JSON file, can be run more than once without duplicating anything, and needs no external plugin.

How it works

A Tools submenu page shows a single button inside a form that posts to admin-post.php. The handler checks the manage_options capability, verifies the nonce, then reads demo/content.json. Each row has a stable key; if a post with that key stored in hidden meta already exists, it is skipped. New items are cleaned with sanitize_text_field and wp_kses_post before wp_insert_post, and a row called home also becomes the static front page.

The result count is shown through a redirect notice.

Adapting it

Extend the JSON with a parent key, menu items or featured images imported from files in your theme (use media_handle_sideload). Add a reset button that deletes only posts carrying the demo meta key. Keep the data small; for large sites, process in batches with AJAX.

Pitfalls

  • Never import with a nonce or capability missing; this writes to the database.
  • Do not trust the JSON blindly if users can upload their own.
  • file_get_contents on a local path is fine; avoid remote URLs here.
  • Keys must never change between releases or duplicates appear.

How to use it

Save as inc/demo-importer.php, require it from functions.php, and add demo/content.json next to it.

Open Tools, then Demo content, and press the button.

Example row: { "key": "home", "type": "page", "title": "Home", "content": "<p>Welcome</p>" }.

Licence: GPL-2.0-or-later. This snippet comes from WordPress code and is GPL-2.0-or-later.

Related snippets