Custom importer for WP Recipe Maker

I frequently work with food bloggers to improve the design, speed, and functionality of their websites. This often includes migrating to WP Recipe Maker, the industry-leading recipe card plugin.

WP Recipe Maker already includes importers for many popular recipe plugins. You can skip to the end to see how to import your recipes if you are using BigOven, Cookbook, Cooked, Create, EasyRecipe, FoodiePress, Meal Planner Pro, Purr Design, Recipe Card, Simmer, Simple Recipe Pro, Tasty, WordPress.com, WP Ultimate Recipe, Yummly, or ZipList.

Sometimes during our discovery phase we’ll find the previous developer built their own custom recipe card directly in the theme. In the process of redesigning the site we’ll need build a custom importer and migrate all the recipes to WP Recipe Maker.

Switching to WP Recipe Maker lets us take advantage of all the modern features it provides (schema for SEO, advanced templating, user ratings…), and stay up-to-date with future changes required for SEO.

Review the recipe data structure

The first step is to figure out how the current plugin is storing the recipe data. Most custom built recipe cards will store it as post meta, but it could be stored in a separate post type or custom database tables.

A quick tip for inspecting post meta is to drop this code in functions.php in my local development environment:

/**
 * Display all post meta
 *
 */
add_action( 'wp_footer', function() {
	if( is_single() )
		ea_pp( get_post_meta( get_the_ID() ) );
});

/**
 * Pretty Printing
 *
 */
function ea_pp( $obj, $label = '' ) {
	$data = json_encode( print_r( $obj,true ) );
	?>
	<style type="text/css">
		#bsdLogger {
		position: absolute;
		top: 30px;
		right: 0px;
		border-left: 4px solid #bbb;
		padding: 6px;
		background: white;
		color: #444;
		z-index: 999;
		font-size: 1.25em;
		width: 400px;
		height: 800px;
		overflow: scroll;
		}
	</style>
	<script type="text/javascript">
		var doStuff = function(){
			var obj = <?php echo $data; ?>;
			var logger = document.getElementById('bsdLogger');
			if (!logger) {
				logger = document.createElement('div');
				logger.id = 'bsdLogger';
				document.body.appendChild(logger);
			}
			////console.log(obj);
			var pre = document.createElement('pre');
			var h2 = document.createElement('h2');
			pre.innerHTML = obj;
			h2.innerHTML = '<?php echo addslashes($label); ?>';
			logger.appendChild(h2);
			logger.appendChild(pre);
		};
		window.addEventListener ("DOMContentLoaded", doStuff, false);
	</script>
	<?php
}

When viewing a single post, there will be a column added to the right side of the screen that lists all of its metadata:

It’s also a good idea to dig into the theme/plugin to see how it’s accessing and displaying the recipe data. Simple fields like Prep Time and Cook Time are usually straightforward, but more complicated fields like Ingredients and Directions may require more work to assemble.

The site I’m working on right now is storing ingredients and instructions as serialized arrays.

$ingredients_data = maybe_unserialize( get_post_meta( $id, '_recipe_ingredients', true ) );

Write down all the data you’ll need to access and how it is stored. I’ll usually create a simple spreadsheet with Field Type (“ingredients”), Field Meta Key (_recipe_incredients), and Notes (“stored as serialized array”).

Where to store the custom importer

First you need to decide how you would like to store your custom importer. You could keep it in:

  • Your theme, in which case you’d create the importer file inside your theme and use the wprm_importer_directories filter in your functions.php file.
  • A plugin, in which case you’d create the importer file inside the plugin and use the wprm_importer_directories filter in the main plugin file. You could either build this as a standalone plugin or as part of a core functionality plugin (bundled with other functionailty)

I typically build it as a standalone plugin. We usually build these as part of our redesign process, and we want to get the importer functionality running on the live website before the redesign is deployed. Here’s an example: CCK Recipe Importer plugin.

Build the custom importer

You can find all of WP Recipe Maker’s current importers and their example importer in /plugins/wp-recipe-maker/includes/admin/import/class-wprm-import-example.php.

To create your own custom importer, create a file in your theme or core functionality plugin named class-wprm-import-{something}.php, where {something} is your unique importer name. I’ll usually use the client’s name for this, so in the custom importer I’m building for The Mom 100 it’s called class-wprm-import-themom100.php.

Then use the wprm_importer_directories filter to tell WP Recipe Maker the directory in which your importer can be found. It will search that directory for files using the above naming structure, so you can include a directory that has other files in it without issues.

I placed my custom importer in the /inc/ directory of my core functionality plugin, then added the following code:

// Plugin directory
define( 'EA_DIR' , plugin_dir_path( __FILE__ ) );

/**
 * Include custom WPRM importer
 * @link https://www.billerickson.net/custom-importer-for-wp-recipe-maker/
 */
function be_custom_wprm_importer( $directories ) {
	$directories[] = EA_DIR . '/inc/';
	return $directories;
}
add_filter( 'wprm_importer_directories', 'be_custom_wprm_importer' );

Copy the contents of the class-wprm-import-example.php file into your new file, then rename the class from WPRM_Import_Example to WPRM_Import_{Something}, changing {Something} to match what you used in the file name.

Update the get_uid() method to use your slug (ex: themom100), and the get_name() method to use your name (ex: The Mom 100).

Get Recipes

The next step is to update the get_recipe_count() and get_recipes() methods used to find the recipes on the site.

On this project, recipes are every post in the recipe post type. If your recipes are mixed in with posts, you would write a query that searches for posts containing a meta key only used by recipes.

It’s also important to eliminate recipes you have already imported. After I finish importing a recipe, I’m going to run update_post_meta( $post_id, '_recipe_imported', 1 ); to indicate this one has been imported. In my query below, I’m looking for recipes that do not have that key because we only want to list recipes that haven’t been imported yet.

For get_recipe_count() you want to find the total recipe count, so set posts_per_page arbitrarily high and only query for IDs.

	/**
	 * Get the total number of recipes to import.
	 *
	 * @since    1.20.0
	 */
	public function get_recipe_count() {

		$loop = new WP_Query( array(
			'fields' => 'ids',
			'post_type' => 'recipe',
			'posts_per_page' => 999,
			'meta_query' => array(
				array(
					'key' => '_recipe_imported',
					'value' => 1,
					'compare' => 'NOT EXISTS',
				)
			)
		) );

		return $loop->found_posts;

	}

For the get_recipes() method, we’re getting a paginated list of recipes to import. You want to keep the list to a reasonable size (ex: 20) so it doesn’t time out if someone tries importing all the recipes in the list at once. You’ll also want to use the $page parameter so that clicking the pagination links actually changes the posts shown.

Use this loop to build the $recipes array that includes the post ID, post title, and edit post link.

	/**
	 * Get a list of recipes that are available to import.
	 *
	 * @since    1.20.0
	 * @param	 int $page Page of recipes to get.
	 */
	public function get_recipes( $page = 0 ) {
		// Return an array of recipes to be imported with name and edit URL.
		// If not the same number of recipes as in "get_recipe_count" are returned pagination will be used.

		$loop = new WP_Query( array(
			'post_type' => 'recipe',
			'posts_per_page' => 20,
			'paged' => $page,
			'meta_query' => array(
				array(
					'key' => '_recipe_imported',
					'value' => 1,
					'compare' => 'NOT EXISTS',
				)
			)
		) );

		$recipes = array();
		foreach( $loop->posts as $post ) {
			$recipes[ $post->ID ] = array(
				'name' => $post->post_title,
				'url'  => get_edit_post_link( $post->ID )
			);
		}
		return $recipes;
	}

Get Recipe

The get_recipe() method is where all the hard work happens. We’re going to take the recipe data we mapped out earlier to build the $recipe array used by WP Recipe Maker to generate its recipe.

The first parameter is $id which is the Post ID. Use this to access any necessary post data. Don’t assume the post data is already set up. Instead of using get_the_title(), use get_the_title( $id ).

The example file will already contain all the necessary fields for the recipe. Go through and replace them with how your current recipe data is stored. Here’s how the simple fields looked in my importer:

$recipe['name'] = get_the_title( $id );
$recipe['summary'] = get_post_meta( $id, '_recipe_subtitle', true );
$recipe['author_name'] = get_the_author( $id );
$recipe['image_id'] = get_post_thumbnail_id( $id );
$recipe['servings'] = get_post_meta( $id, '_r_yield', true );
$recipe['servings_unit'] = 'Servings';
$recipe['prep_time'] = get_post_meta( $id, '_r_prep_time', true );
$recipe['cook_time'] = get_post_meta( $id, '_r_grill_time', true );
$recipe['total_time'] = $recipe['prep_time'] + $recipe['cook_time'];

Ingredients and Instructions use arrays of groups, each group containing an optional name (ex: “For the Sauce”) and an array of ingredients/instructions. For each ingredient you can specify array( 'raw' => $ingredient ); and WP Recipe Maker will handle processing that raw data (ex: “1 tsp salt”) into its components (“1” count, “tsp” unit, and “salt” ingredient).

Here’s what my ingredients and instructions looked like:

$ingredients_data = maybe_unserialize( get_post_meta( $id, '_recipe_ingredients', true ) );
if( !empty( $ingredients_data ) ) {
	foreach( $ingredients_data as $data_group ) {
		$group = array();
		if( !empty( $data_group['title'] ) )
			$group['name'] = wp_strip_all_tags( $data_group['title'] );
		for( $i = 0; $i < count( $data_group['nums'] ); $i++ ) {
			$ingredient = '';
			if( !empty( $data_group['nums'][ $i ] ) )
				$ingredient .= $data_group['nums'][ $i ];
			if( !empty( $data_group['types'][ $i ] ) && 'no type' !== $data_group['types'][ $i ] )
				$ingredient .= ' ' . $data_group['types'][ $i ];
			if( !empty( $data_group['names'][ $i ] ) )
				$ingredient .= ' ' . $data_group['names'][ $i ];
			if( !empty( $ingredient ) )
				$group['ingredients'][] = array( 'raw' => $ingredient );
		}
		if( !empty( $group ) )
			$recipe['ingredients'][] = $group;
	}
}

$instructions_data = maybe_unserialize( get_post_meta( $id, '_recipe_steps_data', true ) );

$instructions = array();
foreach( $instructions_data as $item ) {
	$instructions[] = array(
		'text' => $item['content'],
	);
}

// Instructions have to follow this array structure consisting of groups first.
$recipe['instructions'] = array(
	array(
		'name' => '', // Group names can be empty.
		'instructions' => $instructions
	)
);

Replace Recipe

The replace_recipe() method lets you run additional code after the recipe has been imported. If your previous recipe plugin used a shortcode, use this to find/replace it with the WP Recipe Maker shortcode.

In my case the previous theme hardcoded the recipe below the post content, so I’m appending the WP Recipe Maker shortcode to the end of the post content.

I also use this method to mark the post as imported, using my custom field _recipe_imported, and to set the parent post for the recipe, using the custom field wprm_parent_post_id.

	/**
	 * Replace the original recipe with the newly imported WPRM one.
	 *
	 * @since    1.20.0
	 * @param	 mixed $id ID of the recipe we want replace.
	 * @param	 mixed $wprm_id ID of the WPRM recipe to replace with.
	 * @param	 array $post_data POST data passed along when submitting the form.
	 */
	public function replace_recipe( $id, $wprm_id, $post_data ) {
		// The recipe with ID $id has been imported and we now have a WPRM recipe with ID $wprm_id (can be the same ID).
		// $post_data will contain any input fields set in the "get_settings_html" function.
		// Use this function to do anything after the import, like replacing shortcodes.

		// Mark as migrated so it isn't re-imported
		update_post_meta( $id, '_recipe_imported', 1 );

		// Set parent post that contains recipe
		update_post_meta( $wprm_id, 'wprm_parent_post_id', $id );

		// Add the WPRM shortcode
		$post = get_post( $id );
		$content = $post->post_content . ' [wprm-recipe id="' . $wprm_id . '"]';
		wp_update_post( array( 'ID' => $id, 'post_content' => $content ) );

	}

That’s it! The importer is built and ready for testing. You’ll want to import a few recipes to ensure everything is working as expected. If you run into any issues, I recommend looking through the existing importers for guidance / ideas.

If your current recipe plugin stores the entire ingredient list as an HTML bulleted list, check out the parse_recipe_component_list() method in class-wprm-import-purr.php for an example of parsing the HTML into individual ingredients.

Importing recipes

Once you have a WP Recipe Maker importer for your current recipe plugin, the process of importing recipes is very straightforward.

In the WordPress backend go to WP Recipe Maker > Import Recipes. You should see a section with your recipe plugin’s name and the number of recipes it found. Click “Explore Import Options”

On the next screen you’ll see a list of recipes you can import. Select all the ones you’d like to import, then click “Import Selected Recipes”.

I recommend you import one recipe at a time, then view the post to make sure all the recipe data made it over.

Bill Erickson

Bill Erickson is the co-founder and lead developer at CultivateWP, a WordPress agency focusing on high performance sites for web publishers.

About Me
Ready to upgrade your website?

I build custom WordPress websites that look great and are easy to manage.

Let's Talk

Reader Interactions

Comments are closed. Continue the conversation with me on Twitter: @billerickson

Comments

  1. Penelope says

    Hi, I was really pleased to discover this article. Am working through it, but when I get to this:

    You can find all of WP Recipe Maker’s current importers and their example importer in /plugins/wp-recipe-maker/includes/import/

    I come up against the problem that in my plugin directory I only have /plugins/wp-recipe-maker/includes/ followed by directories admin and public. When I look in /admin, I find the import dir, but there is no file with “example” anywhere in the name in there. I am looking for recipes made by Food & Cook theme on site A, which I need to extract, convert and import to site B (the client’s new website that does not use the F&C theme, but WP Recipe Maker instead.

    Can you give me a clue what’s going on here?
    Thanks!
    SwissP, Switzerland

    • Bill Erickson says

      Sorry, you’re right, it’s in /includes/admin/import. The example importer is: /plugins/wp-recipe-maker/includes/admin/import/class-wprm-import-example.php.

      It’s also useful to look at the other importers in there to see how they process content in different ways.

  2. Penelope says

    One more question for you: I am using my child theme directory because this theme is essential to the website (a bare-bones one) and will never be changed. In your very first function you set the directories, thus:

    // Plugin directory
    define( ‘EA_DIR’ , plugin_dir_path( __FILE__ ) ); [ and so on ]

    Do I need to change plugin_dir_path to something else?

    Thanks …

    • Bill Erickson says

      Correct, you’ll want to change that to the path in your theme that contains the custom importer.

      If the custom importer is in the top level of your child theme, use get_stylesheet_directory(). If it’s inside a /inc/ directory in your child theme, use get_stylesheet_directory() . '/inc/.

  3. Shankar says

    Hi, Thanks the detailed explanation on how to write a custom importer. We are trying to write a custom importer to import the recipe from osetin recipe plugin to wprm.

    We are not sure where to place the funtion “function be_custom_wprm_importer( $directories )”. any help is greatly appreciated.

    Thanks

    • Bill Erickson says

      If you are building your custom recipe importer inside your theme, you can place that in functions.php. If the custom recipe importer will be located in a plugin (ex: a core functionality plugin), then I recommend including it there.

      I typically build a single purpose plugin for the importer (ex: “[Client Name] Recipe Importer”) and include everything there. This way we can install the plugin on the production site to start converting recipes without waiting until we deploy the new theme / core functionality plugin. Here’s an example: CCK Recipe Importer

        • shankar says

          Hi, We have built a plugin for the importer. But the recipe count is always 1 and the name of the recipe is Demo Recipe. The recipe that we are trying to import are stored under the recipe slug. But still it is not returning the total recipe count. Are we missing something? Can you please help?

          public function get_recipe_count() {
          // Return a count for the number of recipes left to import.
          // Don’t include recipes that have already been imported.

          $loop = new WP_Query( array(
          ‘fields’ => ‘ids’,
          ‘post_type’ => ‘recipe’,
          ‘posts_per_page’ => 999,
          ‘meta_query’ => array(
          array(
          ‘key’ => ‘_recipe_imported’,
          ‘value’ => 1,
          ‘compare’ => ‘NOT EXISTS’,
          )
          )
          ) );

          return $loop->found_posts;

          }

            • shankar says

              Hi,

              We are able to import most of the data from neptune by osetin recipe to wp recipe maker except the youtube video url. In osetin we use arve plugin to embed the youtube videos. Now we are not able to read this arve url and map it wprm video_embed field.

              Can you please advise us how to proceed?

              • Bill Erickson says

                I recommend you contact WP Recipe Maker support to see if they know how to make the arve plugin work with the recipe video feature.

  4. Chad says

    I understand this works in the case that the recipes already exist as posts/custom post types in WordPress – but what if I’m converting a static HTML site with thousands of recipes? Is there a way of making this custom import method work for something like that? Or will I have to import those into WordPress as Posts or a custom post type instead?

    Thanks!

    Chad

    • Bill Erickson says

      I would start by using a plugin like HTML Import 2 to import the static content into WordPress.

      In the get_recipe_count() and get_recipes() functions you would query for all posts that don’t have your custom meta field saying you’ve processed it already. In the get_recipe() you’ll have to parse the HTML of the page to try and extract just the recipe data.

      FWIW, in cases like this we typically support the legacy recipe card while the client manually rebuilds them slowly over time. Even if you can build an HTML parser that successfully gets the recipe data into WP Recipe Maker, you’ll want to review every recipe after import anyway to make sure it worked, and it’s likely the old recipe card is missing required information so you end up rewriting the recipe.

      We have a client right now who is moving from Blogger to WordPress. She has been using some online tool to type in her recipe and generate the schema + recipe card, then paste it into the Blogger post content. Rather than try to extract that data, we’re keeping it as-is at launch and using WP Recipe Maker for new and popular recipes. Over the next few months after launch she will manually rebuild them in WP Recipe Maker.

      • Chad says

        Makes sense, we’ll probably approach from that angle. Thanks for the quick response and insight, it’s much appreciated!