How to import custom posts with featured images and custom fields by fetching an API using a single AJAX call

Schermata-2022-11-17-alle-11.54.24

In this tutorial, we will import a list of movies – including images and custom fields – inside a custom post type by fetching a public API.

Table of Contents

Introduction

First of all, this tutorial is a follow-up to the great video shared in the group recently by Cédric Bontems where he explained in detail how to fetch data from an API to recreate the Netflix homepage. In this tutorial, we’ll use a different approach to reach the same result by importing the data to WordPress instead of calling the API on each page load.

Why does it matter? Well, there are several benefits to doing so:

  • With this method, you just need to fetch the API once. In fact, you could completely remove the code once the data has been imported since all the content will be accessible from your WordPress data tables.
  • Once the data is saved in WordPress, you can manipulate it without using custom PHP code. That means you can create query loops inside the Bricks Builder, create nested sliders, filters, single pages, category pages, etc…
  • Using data from your WordPress tables will be much faster than calling the API every time. It allows you to cache both the content and the WP queries.
  • It doesn’t rely on an external source to show your content. if the API is down for some reason, you are not in trouble!

This tutorial is also massively inspired by the WPCasts video. Make sure to visit his channel to learn advanced WordPress tips and tricks.

Get access to the API

In this tutorial, we’ll use the same API shared by Cédric: https://www.themoviedb.org/settings/api. He explained how to create an account and get access to it so I won’t be too long here. Make sure to define your API key inside your functions.php file:

define ('TMDB_API_KEY', '<YOUR_API_KEY>' );

Get the response from API

We’ll follow the exact same logic described in my Introduction to Fetching API in Bricks, so make sure to read it if you haven’t yet. Let’s create our main function:

function bl_get_movies_from_api(){

  $moviedb_api = 'https://api.themoviedb.org/3/movie/top_rated?api_key=' . TMDB_API_KEY . '&language=en-US';
  $response = wp_safe_remote_get($moviedb_api);

  // Stop the function if the response returns an error
  if ( is_wp_error( $response ) ) {
	return false;
  }
  
  // Get the body part 
  $response_body = wp_remote_retrieve_body( $response );
  
  // Decode the JSON object into a PHP array
  $response_body_decoded = json_decode( $response_body, true );

  return $response_body_decoded['results'];
}

At this point, we are returning a PHP array with the data of each movie. The output looks like this:

array(20) {
  [0]=>
  array(14) {
    ["adult"]=>
    bool(false)
    ["backdrop_path"]=>
    string(32) "/rl7Jw8PjhSIjArOlDNv0JQPL1ZV.jpg"
    ["genre_ids"]=>
    array(2) {
      [0]=>
      int(10749)
      [1]=>
      int(18)
    }
    ["id"]=>
    int(851644)
    ["original_language"]=>
    string(2) "ko"
    ["original_title"]=>
    string(15) "20 Century Girl"
    ["overview"]=>
    string(377) "Yeon-du asks her best friend Bora to collect all the information she can about Baek Hyun-jin while she is away in the U.S. for heart surgery. Bora decides to get close to Baek's best friend, Pung Woon-ho first. However, Bora's clumsy plan unfolds in an unexpected direction. In 1999, a year before the new century, Bora, who turns seventeen, falls into the fever of first love."
    ["popularity"]=>
    float(314.92899999999997)
    ["poster_path"]=>
    string(32) "/od22ftNnyag0TTxcnJhlsu3aLoU.jpg"
    ["release_date"]=>
    string(10) "2022-10-06"
    ["title"]=>
    string(17) "20th Century Girl"
    ["video"]=>
    bool(false)
    ["vote_average"]=>
    float(8.8000000000000007)
    ["vote_count"]=>
    int(248)
  }
  [1]=> etc....

In this tutorial, we’ll use:

  • ['backdrop_path'] to get the image link and use it as the featured image of our post
  • ['title'] will be our post title
  • ['id'] will be used inside the post slug to make sure the post is unique
  • ['overview'] for the post excerpt
  • ['release_date'], ['vote_average'], and ['vote_count'] to fill our ACF custom fields

Create the custom posts

At this point, we have a big array of all the movie information. Now we want to loop inside the array and create a custom post for each movie, but before let’s create a custom function to handle our post slug:

function bl_slugify($text){

  // remove unwanted characters
  $text = preg_replace('~[^-\w]+~', '', $text);

  // trim
  $text = trim($text, '-');

  // remove duplicate -
  $text = preg_replace('~-+~', '-', $text);

  // lowercase
  $text = strtolower($text);

  if (empty($text)) {
    return 'n-a';
  }

  return $text;
}

This function will make sure to replace any special character, separate each word with “-“, and transform the text to lowercase. Nothing fancy here.

It’s time to loop inside the array and create the custom posts to our CPT movie:

// Loop in each movie
foreach($response_body_decoded['results'] as $movie){

  // Slug
  $movie_slug = bl_slugify( $movie['title'] . '-' . $movie['id'] );     

  // Existing movie inside the CPT
  $existing_movie = get_page_by_path( $movie_slug, 'OBJECT', 'movie' );

  // Check if the movie post has already been created
  if( $existing_movie === null  ){
    
    // Create a new custom post
    $inserted_movie = wp_insert_post( [
      'post_name' => $movie_slug,
      'post_title' => $movie['title'],
      'post_excerpt' => $movie['overview'],
      'post_type' => 'movie',
      'post_status' => 'publish'
    ] );
    
    // Continue to the next loop iteration if the post was not correctly created
    if( is_wp_error( $inserted_movie ) || $inserted_movie === 0 ) {
      continue;
    }
  }
}

Few considerations:

  • to create our slug, we concatenate the movie title and the ID, so we’re sure the slug will be unique.
  • before creating the post, we run the get_page_by_path() function that will check if the slug already exists inside our custom post type movie. if it doesn’t (which is what we want), it returns null and will create a new post – making sure we don’t duplicate an existing post if we run the function multiple times.
  • We use the core WordPress function wp_insert_post() to create the custom post. This function allows passing an array as an argument to set the post name (the slug), the post title, etc…
  • after the post has been created, we check if everything was correctly processed. If the function detects an error, or if the custom post returns no post_id, then the loop iteration will stop and process the next movie.

Let’s download the image from the API, place it inside our media library and set it as the featured image of the post:

// Set Featured Image
$url = 'https://image.tmdb.org/t/p/original' . $movie['backdrop_path'];
$img = media_sideload_image( $url, $inserted_movie);//download image to wpsite from url
$img = explode("'",$img)[1];// extract http.... from <img src'http...'>
$attId = attachment_url_to_postid($img);//get id of downloaded image
set_post_thumbnail( $inserted_movie, $attId );//set the given image as featured image for the post
  • First of all, we compose the image URL using the [‘backdrop_path’] value from the API.
  • Then we use the media_sideload_image() function to download the image to our server
  • We extract the URL from the img tag and attach it to our post using the attachment_url_to_postid() function. At this point, the image is inside our media library.
  • The last step is to set the image as the featured image of the post using the set_post_thumbnail() function.

Populate the ACF fields

First of all, lets create our fields in ACF:

Make sure to check the Field Keys checkbox from the Screen Options.

Now, let’s map our custom fields with the API values. On the left of the arrow, the ACF fields key. On the right, the array value from the API:

// Map ACF keys with the API values
$acf_fields = array(
   'field_6375ea77d3491' => 'release_date',
   'field_6375ea8fd3492' => 'vote_average',
   'field_6375ea9fd3493' => 'vote_count',
);

Finally, let’s update each field with the API value:

// Update each ACF value
foreach( $acf_fields as $key => $name ) {
   update_field( $key, $movie[$name], $inserted_movie );
}

Bear with me, we are pretty close to the end.

Enable AJAX

So the idea is to run our function once through an AJAX call and populate all our custom posts. In order to enable the AJAX call and use our function as a callback we need to use the wp_ajax hook:

add_action( 'wp_ajax_nopriv_get_movies_from_api', 'get_movies_from_api' );
add_action( 'wp_ajax_get_movies_from_api', 'get_movies_from_api' );

Full code

OK. All our code is ready:

// Movies API

add_action( 'wp_ajax_nopriv_get_movies_from_api', 'get_movies_from_api' );
add_action( 'wp_ajax_get_movies_from_api', 'get_movies_from_api' );

function bl_get_movies_from_api(){

  define ('TMDB_API_KEY', '<YOUR_API_KEY>' );

  $moviedb_api = 'https://api.themoviedb.org/3/movie/top_rated?api_key=' . TMDB_API_KEY . '&language=en-US';
  $response = wp_safe_remote_get($moviedb_api);

  // Stop the function if the response returns an error
  if ( is_wp_error( $response ) ) {
	return false;
  }
  
  // Get the body part 
  $response_body = wp_remote_retrieve_body( $response );
  
  // Decode the JSON object into a PHP array
  $response_body_decoded = json_decode( $response_body, true );

  // Loop in each movie
  foreach($response_body_decoded['results'] as $movie){

    // Slug
    $movie_slug = bl_slugify( $movie['title'] . '-' . $movie['id'] );     

    // Existing movie inside the CPT
    $existing_movie = get_page_by_path( $movie_slug, 'OBJECT', 'movies' );

    // Check if the movie post has already been created
    if( $existing_movie === null  ){
      
      // Create a new custom post
      $inserted_movie = wp_insert_post( [
        'post_name' => $movie_slug,
        'post_title' => $movie['title'],
        'post_excerpt' => $movie['overview'],
        'post_type' => 'movie',
        'post_status' => 'publish'
      ] );
      
      // Continue to the next loop iteration if the post was not correctly created
      if( is_wp_error( $inserted_movie ) || $inserted_movie === 0 ) {
        continue;
      }

      // Set Featured Image
      $url = 'https://image.tmdb.org/t/p/original' . $movie['backdrop_path'];
      $img = media_sideload_image( $url, $inserted_movie);//download image to wpsite from url
      $img = explode("'",$img)[1];// extract http.... from <img src'http...'>
      $attId = attachment_url_to_postid($img);//get id of downloaded image
      set_post_thumbnail( $inserted_movie, $attId );//set the given image as featured image for the post

      // Map ACF keys with the API values
      $acf_fields = array(
        'field_6375ea77d3491' => 'release_date',
        'field_6375ea8fd3492' => 'vote_average',
        'field_6375ea9fd3493' => 'vote_count',
      );

      // Update each ACF value
      foreach( $acf_fields as $key => $name ) {
        update_field( $key, $movie[$name], $inserted_movie );
      }
    }
  }
}

function bl_slugify($text){

  // remove unwanted characters
  $text = preg_replace('~[^-\w]+~', '', $text);

  // trim
  $text = trim($text, '-');

  // remove duplicate -
  $text = preg_replace('~-+~', '-', $text);

  // lowercase
  $text = strtolower($text);

  if (empty($text)) {
    return 'n-a';
  }

  return $text;
}

Run the AJAX call

The easy part! Go into /wp-admin/ and paste the following URL: your-domain.com/wp-admin/admin-ajax.php?action=bl_get_movies_from_api (change your-domain to your actual domain). This will run our AJAX call through the admin-ajax.php method.

Now just wait. The function is running in the background and importing all the data from the API.

When the function has finished running, it will return 0. That’s totally normal.

Let’s check our custom post type movie!

All the movies have been imported! Now let’s check the post data:

Everything is correctly filled and saved.

Congrats, you just imported a bunch of movies inside a custom post type using a public API!

  • Saleh Alnaggar

    I can’t thank you enough for this amazing tutorial, it helped me a lot, I wish you all the best.

  • Same comment as Mike 😊🙏🏼‍💚

  • This has genuinely changed the way I’ll work from now on.
    For a long time, I’ve thought “I’ll learn something like this one day”, and the day has never come, until now.
    I probably wouldn’t have given it a second look had it not been from BricksLabs, but you guys keep doing cool stuff 🙂
    I’m one big step closer to signing up with you guys because of this!
    Cheers.

  • Would this be possible to run on a custom taxonomy instead of a custom post type?

  • This is one of the best tutorials. I’ve been waiting a lot for this.
    Thank you very much Maxime

Leave your comment