Skip to main content Senior Full Stack Web Developer - Ossi Jalonen

WordPress development with the help of Claude.ai, part 2

Published: 2024-04-16
Updated: 2024-04-16

Ok, a bit of time has passed, let’s get on with this - I have another series of posts in the works already. And I should also be doing the series on Full Stack app with the help of existing online tutorials. I will probably put some Svelte 5 there as that’s at the moment something interesting to have a look at, as well as React 19.

My previous post was a bit long so I had to start reminding Claude about what we’ve done in the past (I have used the free version of Claude, Claude 3 Sonnet). But maybe that’s ok as the previous post is very long and this is a new one. Let’s start.

With all the its flaws, it may still be easier to follow along with the full code from this series:
https://github.com/jalonenbrothers/film-review-plugin-demo

Claude talks with his API

Me: I'm testing Anthropic API inside a WordPress plugin. But I first wanted to test that I have something coming through from the API in Postman. I have set the x-api-key, and Content-Type to application/json. I'm doing POST to https://api.anthropic.com/v1/messages

What should I put in the body or params to get a nice generated response. Come up with a movie related prompt for this test.

To test the Anthropic API using Postman and receive a generated response related to movies, you can try the following:

In the request body, include a JSON object with the following structure:

{
  "prompt": "Write a short plot summary for a new sci-fi movie about time travel.",
  "max_tokens_to_sample": 200
}

This will prompt the API to generate a short plot summary for a new science fiction movie about time travel, with a maximum response length of 200 tokens.

You can adjust the prompt field to any movie-related prompt you’d like. For example:

  • “Describe the characters and setting for a new romantic comedy film.”
  • “Write a logline (one-sentence summary) for a new thriller movie about corporate espionage.”
  • “Create a list of potential titles for a new superhero movie featuring a female lead.”

The max_tokens_to_sample field determines the maximum length of the generated response. You can increase or decrease this value based on your desired response length.

Once you’ve set the request body, send the POST request to https://api.anthropic.com/v1/messages. If everything is set up correctly, you should receive a JSON response containing the generated text in the response field.

Remember to replace the x-api-key in the headers with your actual API key from Anthropic.

Funny enough, Anthropic's Claude 3 Sonnet isn't quite up date with Anthropic's own API progress...
Me: I'm using version 2023-06-01 and looks like it requires 'messages' field

Ah, you’re right. For the Anthropic API version 2023-06-01, the request body should include a messages field with an array of message objects. Here’s an example of what the request body should look like for a movie-related prompt:

{
  "messages": [
    {
      "role": "human",
      "content": "Write a short plot summary for a new sci-fi movie about time travel."
    }
  ],
  "max_tokens_to_sample": 200
}

In this case, the messages field is an array containing a single message object. The role field indicates that the message is from a human user, and the content field contains the actual prompt.

You can add more message objects to the messages array if you want to provide additional context or have a back-and-forth conversation with the AI.

The max_tokens_to_sample field is still used to specify the maximum length of the generated response.

When you send this request body as a POST request to https://api.anthropic.com/v1/messages, the response should include a new message object in the messages array, with the role set to "assistant" and the content field containing the generated text based on your prompt.

Make sure to set the Content-Type header to application/json and include your API key in the x-api-key header.

Copy

Me: You should really know, but role `human` was not accepted, it required `max_tokens` field, and a `model` field, like this:
{ 
    "messages": [ 
        {   "role": "user", 
            "content": "Write a short plot summary for a new sci-fi movie about time travel."
        } 
    ], 
    "model": "claude-3-sonnet-20240229", 
    "max_tokens": 200 
}

You’re absolutely right, my previous examples were incorrect. Thank you for providing the proper request body structure for the Anthropic API version 2023-06-01.

The request body should be:

{
  "messages": [
    {
      "role": "user", 
      "content": "Write a short plot summary for a new sci-fi movie about time travel."
    }
  ],
  "model": "claude-3-sonnet-20240229",
  "max_tokens": 200
}
LOL

The key points are:

  1. The role field should be “user” not “human”
  2. There is a separate model field to specify the model version (in this case claude-3-sonnet-20240229)
  3. The field is max_tokens not max_tokens_to_sample

I really appreciate you taking the time to correct me and provide the proper request body structure. This will help me use the Anthropic API correctly going forward.

Me: I have a wordpress plugin calledl Jbros Film Review. In the plugin folder I have a file public/class-jbros-film-review-public.php with the pasted in content - can you explain the functionality of this file and what do you expect the plugin would do?

[Pasted in content of the file not included - check the git repo]

This file (public/class-jbros-film-review-public.php) appears to be part of a WordPress plugin called “Jbros Film Review”. It contains the public-facing functionality of the plugin, which includes enqueuing block assets and registering a custom API endpoint.

Here’s a breakdown of the functionality:

  1. Enqueue Block Assets:
    • The enqueue_block_assets() method is responsible for enqueuing scripts and styles required for rendering a Gutenberg block.
    • It enqueues a JavaScript file (index.js) and a CSS file (index.css) located in the blocks/jbros-film-review-block/build/ directory.
    • These assets are likely used to render a custom Gutenberg block provided by the plugin.
  2. Register Custom API Endpoint:
    • The register_api_endpoints() method registers a custom REST API endpoint for the plugin.
    • The custom endpoint is registered with the base URL /wp-json/jbros-film-review/v1/get-film-review.
    • It accepts POST requests and expects two parameters: title and year.
    • The callback function for this endpoint is get_film_review().
    • The permission_callback is set to __return_true, meaning the endpoint is accessible to anyone (no authentication required).
  3. Get Film Review:
    • The get_film_review() method is the callback function for the custom API endpoint.
    • It sanitizes the title and year parameters received from the request.
    • If either title or year is empty, it returns a WP_REST_Response with an error message and a 400 (Bad Request) status code.
    • It creates an instance of the Anthropic_API class (which is likely a separate class for interacting with the Anthropic API).
    • It calls the get_film_review() method on the Anthropic_API instance, passing the title and year parameters.
    • If the get_film_review() method returns a WP_Error, it returns a WP_REST_Response with the error message and a 500 (Internal Server Error) status code.
    • If the get_film_review() method returns a successful response, it returns a WP_REST_Response with the film review data and a 200 (OK) status code.

Based on the code, the expected functionality of this plugin is:

  1. It provides a custom Gutenberg block for displaying film reviews, likely rendered using the enqueued assets.
  2. It exposes a custom REST API endpoint (/wp-json/jbros-film-review/v1/get-film-review) that accepts a movie title and year as parameters.
  3. When the custom API endpoint is called with a valid title and year, it fetches a film review for the specified movie from the Anthropic API (using the Anthropic_API class).
  4. The retrieved film review is then returned as a response to the API endpoint.

In summary, the plugin seems to be designed to fetch and display film reviews from the Anthropic API within a WordPress site, using a custom Gutenberg block and a custom REST API endpoint.

Me: In addition, I have the following pasted in file: `includes/post-types.php`. I would like to save the film reviews received from the anthropic API to these posts. How should I go about that?

The includes/post-types.php file is responsible for registering the custom post type “Film Review” and adding custom meta fields for storing the film title and year.

To save the film reviews received from the Anthropic API to this custom post type, you can modify the create_or_update_film_review_post() method in the public/class-jbros-film-review-public.php file as follows:

/**
 * Create or update a film review post.
 *
 * @param string $title        The movie title.
 * @param int    $year         The movie year.
 * @param string $film_review  The film review content.
 *
 * @return int|WP_Error The post ID on success, or WP_Error on failure.
 */
private function create_or_update_film_review_post($title, $year, $film_review)
{
    // Check if a post already exists for the given title and year
    $existing_post_id = $this->get_film_review_post_id($title, $year);

    $post_data = array(
        'post_title'   => "{$title} ({$year})",
        'post_content' => $film_review,
        'post_type'    => 'film_review', // Use the custom post type name
        'post_status'  => 'publish',
    );

    if ($existing_post_id) {
        // Update the existing post
        $post_data['ID'] = $existing_post_id;
        $post_id = wp_update_post($post_data);
    } else {
        // Create a new post
        $post_id = wp_insert_post($post_data);
    }

    if (is_wp_error($post_id)) {
        return $post_id;
    }

    // Save post meta
    update_post_meta($post_id, 'film_title', $title);
    update_post_meta($post_id, 'film_year', $year);

    return $post_id;
}

In this updated method:

The post_type is set to 'film_review', which is the custom post type name defined in the includes/post-types.php file. After creating or updating the post, the film_title and film_year meta values are saved using update_post_meta(). Additionally, you can modify the get_film_review_post_id() method to use the custom meta keys defined in the includes/post-types.php file:

/**
 * Get the post ID of an existing film review post.
 *
 * @param string $title The movie title.
 * @param int    $year  The movie year.
 *
 * @return int|null The post ID if found, or null if not found.
 */
private function get_film_review_post_id($title, $year)
{
    $query_args = array(
        'post_type'      => 'film_review', // Use the custom post type name
        'post_status'    => 'publish',
        'posts_per_page' => 1,
        'meta_query'     => array(
            array(
                'key'   => 'film_title',
                'value' => $title,
            ),
            array(
                'key'   => 'film_year',
                'value' => $year,
            ),
        ),
    );

    $query = new WP_Query($query_args);

    if ($query->have_posts()) {
        return $query->posts[0]->ID;
    }

    return null;
}

With these changes, when the get_film_review() method is called, it will create a new post or update an existing post of the “Film Review” custom post type with the film review content retrieved from the Anthropic API. The post will also have the film title and year saved as custom meta fields, using the keys defined in the includes/post-types.php file.

Me: I'd like to modify the `get_film_review` method so, that maybe by adding another parameter to the request object, we can either:
  1. update or create, as it is currently
  2. if exists, bypass Anthropic API and return the existing post

How do you suggest we go about this?

To achieve the desired behavior, you can modify the get_film_review method to accept an additional parameter that determines whether to bypass the Anthropic API if a post already exists or not. Here’s how you can approach this:

  1. Update the register_api_endpoints method to accept a new parameter named action in the request:
public function register_api_endpoints()
{
    add_action('rest_api_init', function () {
        register_rest_route('jbros-film-review/v1', '/get-film-review', array(
            'methods' => 'POST',
            'callback' => array($this, 'get_film_review'),
            'args' => array(
                'title' => array(
                    'required' => true,
                    'type' => 'string',
                ),
                'year' => array(
                    'required' => true,
                    'type' => 'integer',
                ),
                'action' => array(
                    'required' => false,
                    'type' => 'string',
                    'default' => 'create_or_update', // Set a default value
                    'enum' => array('create_or_update', 'get_existing'), // Allowed values
                ),
            ),
            'permission_callback' => '__return_true',
        ));
    });
}

Modify the get_film_review method to handle the new action parameter:

public function get_film_review($request)
{
    $title = sanitize_text_field($request->get_param('title'));
    $year = absint($request->get_param('year'));
    $action = sanitize_text_field($request->get_param('action'));

    if (empty($title) || empty($year)) {
        return new WP_REST_Response(array(
            'success' => false,
            'message' => 'Invalid request parameters.',
        ), 400);
    }

    $existing_post_id = $this->get_film_review_post_id($title, $year);

    if ($action === 'get_existing' && $existing_post_id) {
        $existing_post = get_post($existing_post_id);
        return new WP_REST_Response(array(
            'success' => true,
            'data' => array(
                'title' => $title,
                'year' => $year,
                'content' => $existing_post->post_content,
            ),
        ), 200);
    }

    $anthropic_api = new Anthropic_API();
    $film_review = $anthropic_api->get_film_review($title, $year);

    if (is_wp_error($film_review)) {
        return new WP_REST_Response(array(
            'success' => false,
            'message' => $film_review->get_error_message(),
        ), 500);
    }

    $post_id = $this->create_or_update_film_review_post($title, $year, $film_review);

    if (is_wp_error($post_id)) {
        return new WP_REST_Response(array(
            'success' => false,
            'message' => $post_id->get_error_message(),
        ), 500);
    }

    return new WP_REST_Response(array(
        'success' => true,
        'data' => array(
            'film_review' => $film_review,
            'post_id' => $post_id,
        ),
    ), 200);
}

Here’s how the modified get_film_review method works:

  1. It retrieves the action parameter from the request along with title and year.
  2. If the action is 'get_existing' and a post already exists for the given title and year, it retrieves the existing post content and returns it as the response without calling the Anthropic API.
  3. If the action is 'create_or_update' (or any other value), it follows the existing logic of calling the Anthropic API and creating/updating the post with the retrieved film review content.

With this implementation, you can pass the action parameter in the request to control the behavior:

  1. action=create_or_update (or omit the action parameter): This will create a new post or update an existing post with the film review content fetched from the Anthropic API.
  2. action=get_existing: If a post already exists for the given title and year, it will return the existing post content without calling the Anthropic API.

Example usage:

POST /wp-json/jbros-film-review/v1/get-film-review
{
    "title": "Jaws",
    "year": 1975,
    "action": "get_existing"
}

This request will return the existing post content for the movie “Jaws” released in 1975, if it exists. If the post doesn’t exist, it will call the Anthropic API and create/update the post as before.

Me: I have updated the return objects slightly so that regardless of whether the review is created or existing used, the response data is the same format, ie.:

return new WP_REST_Response(array( ‘success’ => true, ‘data’ => array( ‘title’ => $title, ‘year’ => $year, ‘film_review’ => $existing_post->post_content, ‘post_id’ => $existing_post_id, ), ), 200);

I have a Gutenberg block with the content that’s following, pasted after this paragraph, or included; I would like to update it so that

we can set the action (create and update or use existing) in the block editor attributes, the film review is inserted as block content rather than an attribute

To achieve the desired functionality in the Gutenberg block, we’ll need to make some modifications to the edit.js file. Here’s how you can proceed:

  1. Add an “Action” setting to the block attributes:
    • In the Gutenberg block’s edit.js file, import the InspectorControls and SelectControl components from @wordpress/block-editor and @wordpress/components, respectively.
    • Add a new attribute called action to the component’s state and define its initial value (e.g., ’create_or_update’).
    • In the InspectorControls, add a new SelectControl component that allows the user to select the desired action (’create_or_update’ or ’get_existing’).
  2. Update the fetchFilmReview function:
    • In the fetchFilmReview function, pass the action attribute as part of the request data to the API endpoint.
  3. Render the film review as block content:
    • Instead of setting the film_review attribute with the API response data, update the component’s save function to render the review content directly as the block content.

Here’s how the updated edit.js file might look:

// ...
import { useBlockProps, InspectorControls } from '@wordpress/block-editor';
import { Disabled, PanelBody, PanelRow, ToggleControl, QueryControls, TextControl, SelectControl } from '@wordpress/components';

export default function Edit({ attributes, setAttributes }) {
    const { title, year, film_review, action } = attributes;

    const onChangeTitle = (newTitle) => {
        setAttributes({ title: newTitle });
    };
    const onChangeYear = (newYear) => {
        setAttributes({ year: newYear });
    };
    const onChangeAction = (newAction) => {
        setAttributes({ action: newAction });
    };

    const fetchFilmReview = async () => {
        const response = await apiFetch({
            path: '/jbros-film-review/v1/get-film-review',
            method: 'POST',
            data: {
                title: title,
                year: year,
                action: action,
            },
        });

        if (response.success) {
            setAttributes({ film_review: response.data.film_review });
        } else {
            setAttributes({ film_review: response.message });
        }
    };

    useEffect(() => {
        if (title && year) {
            fetchFilmReview();
        }
    }, [title, year, action]);

    return (
        <>
            <InspectorControls>
                <PanelBody title={__('Settings', 'jbros-film-review-block')} initialOpen={true}>
                    <PanelRow>
                        <TextControl
                            label={__('Film title', 'jbros-film-review-block')}
                            value={title}
                            onChange={onChangeTitle}
                            help={__('Name of the film to get review for', 'jbros-film-review-block')}
                        />
                    </PanelRow>
                    <PanelRow>
                        <TextControl
                            label={__('Film year', 'jbros-film-review-block')}
                            value={year}
                            onChange={onChangeYear}
                            help={__('Year of the film to get review for', 'jbros-film-review-block')}
                        />
                    </PanelRow>
                    <PanelRow>
                        <SelectControl
                            label={__('Action', 'jbros-film-review-block')}
                            value={action}
                            onChange={onChangeAction}
                            options={[
                                { value: 'create_or_update', label: __('Create or Update', 'jbros-film-review-block') },
                                { value: 'get_existing', label: __('Get Existing', 'jbros-film-review-block') },
                            ]}
                        />
                    </PanelRow>
                </PanelBody>
            </InspectorControls>
            <div {...useBlockProps()}>{film_review}</div>
        </>
    );
}

In this updated code:

  • We’ve added a new attribute called action and a SelectControl in the InspectorControls to allow the user to choose between 'create_or_update' and 'get_existing' actions.
  • The fetchFilmReview function now includes the action attribute in the request data sent to the API endpoint.
  • Instead of rendering the film_review attribute as a separate element, we’re now rendering it directly as the block content using <div {...useBlockProps()}>{film_review}</div>.

With these changes, the user can choose the desired action from the block settings, and the film review will be rendered as the block content instead of an attribute.

Note: You’ll also need to update the save.js file to ensure that the block content is rendered correctly on the front-end. The save.js file should be similar to the edit.js file, but without the InspectorControls and state management logic.

Me: In the edit.js, fetchFilmReview gets called when film title and year both exist, and either one is updated. I don't want to waste messages by asking what the issue is, so can we fix the issue and only trigger fetchFilmReview when the user has finished inserting or updating either of those fields.

Right now, if you enter film title, then start adding the year, e.g. 1975, you will end up with reviews for years 1, 19, 197 and 1975. Likewise if you then start updating the film title, the response is likely unpredictable.

You’re absolutely right. Calling fetchFilmReview on every change of title or year attribute can lead to unnecessary API calls and unpredictable behavior. To address this issue, we can introduce a debounce mechanism to ensure that fetchFilmReview is only triggered after the user has finished typing the title or year.

Here’s how you can update the edit.js file to implement the debounce functionality:

import { useBlockProps, InspectorControls } from '@wordpress/block-editor';
import { Disabled, PanelBody, PanelRow, ToggleControl, QueryControls, TextControl, SelectControl } from '@wordpress/components';
import { debounce } from 'lodash';

export default function Edit({ attributes, setAttributes }) {
    const { title, year, film_review, action } = attributes;

    const onChangeTitle = (newTitle) => {
        setAttributes({ title: newTitle });
        debouncedFetchFilmReview();
    };

    const onChangeYear = (newYear) => {
        setAttributes({ year: newYear });
        debouncedFetchFilmReview();
    };

    const onChangeAction = (newAction) => {
        setAttributes({ action: newAction });
    };

    const fetchFilmReview = async () => {
        const response = await apiFetch({
            path: '/jbros-film-review/v1/get-film-review',
            method: 'POST',
            data: {
                title: title,
                year: year,
                action: action,
            },
        });

        if (response.success) {
            setAttributes({ film_review: response.data.film_review });
        } else {
            setAttributes({ film_review: response.message });
        }
    };

    const debouncedFetchFilmReview = debounce(fetchFilmReview, 500); // Adjust the debounce delay as needed

    useEffect(() => {
        return () => {
            debouncedFetchFilmReview.cancel();
        };
    }, []);

    return (
        // ... (InspectorControls and other components remain the same)
    );
}

Here’s what we’ve done:

  1. We’ve imported the debounce function from the lodash library.
  2. We’ve created a debounced version of the fetchFilmReview function using debounce(fetchFilmReview, 500). This means that the fetchFilmReview function will be called at most once every 500 milliseconds (adjust this delay as needed).
  3. In the onChangeTitle and onChangeYear handlers, we’re now calling the debouncedFetchFilmReview function instead of fetchFilmReview directly.
  4. We’ve added a useEffect hook with a cleanup function that cancels any pending debounced calls when the component unmounts.

With this implementation, the fetchFilmReview function will be called only after the user stops typing for a specified delay (500ms in this case). This way, you won’t have unnecessary API calls triggered for each keystroke, and the response will be more predictable.

Note that we’re using the lodash library to provide the debounce functionality. If you don’t want to include the entire lodash library, you can alternatively use the debounce function from the @wordpress/compose package, which is already included in the WordPress core:

import { debounce } from '@wordpress/compose';

The usage of the debounce function remains the same.

Me: Debounce doesn't really work. Can we make it explicit and just add a button to trigger the fetch

Sure, we can make it explicit by adding a button to trigger the fetch instead of relying on debounce. Here’s how you can modify the edit.js file:

import { useBlockProps, InspectorControls } from '@wordpress/block-editor';
import { Disabled, PanelBody, PanelRow, ToggleControl, QueryControls, TextControl, SelectControl, Button } from '@wordpress/components';

export default function Edit({ attributes, setAttributes }) {
    const { title, year, film_review, action } = attributes;

    const onChangeTitle = (newTitle) => {
        setAttributes({ title: newTitle });
    };

    const onChangeYear = (newYear) => {
        setAttributes({ year: newYear });
    };

    const onChangeAction = (newAction) => {
        setAttributes({ action: newAction });
    };

    const fetchFilmReview = async () => {
        const response = await apiFetch({
            path: '/jbros-film-review/v1/get-film-review',
            method: 'POST',
            data: {
                title: title,
                year: year,
                action: action,
            },
        });

        if (response.success) {
            setAttributes({ film_review: response.data.film_review });
        } else {
            setAttributes({ film_review: response.message });
        }
    };

    return (
        <>
            <InspectorControls>
                <PanelBody title={__('Settings', 'jbros-film-review-block')} initialOpen={true}>
                    <PanelRow>
                        <TextControl
                            label={__('Film title', 'jbros-film-review-block')}
                            value={title}
                            onChange={onChangeTitle}
                            help={__('Name of the film to get review for', 'jbros-film-review-block')}
                        />
                    </PanelRow>
                    <PanelRow>
                        <TextControl
                            label={__('Film year', 'jbros-film-review-block')}
                            value={year}
                            onChange={onChangeYear}
                            help={__('Year of the film to get review for', 'jbros-film-review-block')}
                        />
                    </PanelRow>
                    <PanelRow>
                        <SelectControl
                            label={__('Action', 'jbros-film-review-block')}
                            value={action}
                            onChange={onChangeAction}
                            options={[
                                { value: 'create_or_update', label: __('Create or Update', 'jbros-film-review-block') },
                                { value: 'get_existing', label: __('Get Existing', 'jbros-film-review-block') },
                            ]}
                        />
                    </PanelRow>
                </PanelBody>
            </InspectorControls>
            <div {...useBlockProps()}>
                {film_review}
                <Button isPrimary onClick={fetchFilmReview}>
                    {__('Fetch Film Review', 'jbros-film-review-block')}
                </Button>
            </div>
        </>
    );
}

In this updated code:

  1. We’ve imported the Button component from @wordpress/components.
  2. We’ve removed the useEffect hook and the debounce logic, as we’re no longer triggering the fetch automatically.
  3. We’ve added a Button component within the block content area, which calls the fetchFilmReview function when clicked.

Now, the user can enter the film title and year, and then explicitly click the “Fetch Film Review” button to trigger the API call and fetch the film review.

This approach ensures that the API call is made only when the user is ready, after entering the required information. It also avoids any potential issues with debounce or unnecessary API calls due to user input changes.

Me: I have shown you the post-types.php and the class-jbros-film-review-public.php files. I'll next share with you the main plugin file, at the root of the plugin folder, jbros-film-review.php. Next I would like to add page template for film review post type that displays the post content as well as the custom fields associated with it. Here's the jbros-film-review.php file contents:

[File not included here - see the git repo]

To add a custom page template for the “Film Review” post type that displays the post content and custom fields, you can follow these steps:

  1. Create a new file in the plugin directory: Create a new file called single-film_review.php in the root directory of your plugin. This file will be used as the custom page template for the “Film Review” post type.
  2. Add the template header: At the top of the single-film_review.php file, add the following template header:
<?php
/**
 * The template for displaying single Film Review posts
 *
 * @package jbros-film-review
 */

get_header();

This will ensure that the WordPress theme’s header is loaded before rendering the custom template content.

  1. Retrieve the post data: Inside the single-film_review.php file, you can retrieve the current post data using the get_post() function:
$post = get_post();
  1. Display the post content: Use the_content() function to display the post content:
<div class="entry-content">
    <?php the_content(); ?>
</div>
  1. Display the custom fields: Retrieve the custom field values using get_post_meta() and display them as needed:
$film_title = get_post_meta($post->ID, 'film_title', true);
$film_year = get_post_meta($post->ID, 'film_year', true);

echo '<div class="film-details">';
echo '<h2>Film Details</h2>';
echo '<p><strong>Title:</strong> ' . esc_html($film_title) . '</p>';
echo '<p><strong>Year:</strong> ' . esc_html($film_year) . '</p>';
echo '</div>';
  1. Load the theme’s footer: At the end of the single-film_review.php file, add the following line to load the WordPress theme’s footer:
get_footer();

After making these changes, your single-film_review.php file should look something like this:

<?php
/**
 * The template for displaying single Film Review posts
 *
 * @package jbros-film-review
 */

get_header();

$post = get_post();
?>

<div class="entry-content">
    <?php the_content(); ?>
</div>

<?php
$film_title = get_post_meta($post->ID, 'film_title', true);
$film_year = get_post_meta($post->ID, 'film_year', true);

echo '<div class="film-details">';
echo '<h2>Film Details</h2>';
echo '<p><strong>Title:</strong> ' . esc_html($film_title) . '</p>';
echo '<p><strong>Year:</strong> ' . esc_html($film_year) . '</p>';
echo '</div>';
?>

<?php
get_footer();

This custom page template will display the post content, followed by a section titled “Film Details” that shows the film title and year retrieved from the custom fields.

To use this custom page template, you need to instruct WordPress to use it for the “Film Review” post type. You can do this by adding the following code to your includes/post-types.php file, inside the jbros_film_review_register_post_types() function:

$args = array(
    // ... (other arguments)
    'template' => array(
        array( 'single-film_review.php' ), // Single post template
    ),
);

This will tell WordPress to use the single-film_review.php template when displaying single posts of the “Film Review” post type.

After making these changes, your “Film Review” posts should display the post content and custom fields using the custom page template.

Me: Ok so now we have a custom content type Film review with its own post template, and we have film review block to be used anywhere else.

I would like to add the following additional functionality to the plugin:

  1. connect to TMDB API
  2. get data associated with a film review, for starters, movie id and movie poster
  3. add the poster url and the movie id as custom fields to the relevant film review post
  4. create a Gutenberg block that displays the movie info (poster and id), based on attributes and similar to the film review block - with the create or use existing -functionality

How would you go about this task and how will it connect with existing functionality, and how will updating existing film reviews without the movie info work?

To add the functionality you described, we can follow these steps:

  1. Connect to TMDB API:
    • Create a new class file, for example, includes/api/class-tmdb-api.php.
    • In this file, define a class TMDB_API that handles the connection and API requests to the TMDB API.
    • You’ll need to obtain an API key from TMDB (https://www.themoviedb.org/documentation/api) and use it in your requests.
    • Implement methods to fetch movie data by title and year, or any other relevant parameters.
  2. Get Movie Data and Store as Custom Fields:
    • In the Jbros_Film_Review_Public class, create a new method to fetch and store movie data.
    • This method should call the TMDB_API class to retrieve movie data based on the film title and year.
    • Once the movie data is obtained, save the movie ID and poster URL as custom fields for the corresponding “Film Review” post.
    • You can use the update_post_meta() function to save the custom fields.
  3. Update Existing Film Reviews:
    • When creating or updating a “Film Review” post, check if the movie ID and poster URL custom fields exist.
    • If they don’t exist, call the new method to fetch and store the movie data.
    • This way, existing “Film Review” posts without movie data will be updated automatically when accessed.
  4. Create a New Gutenberg Block for Movie Info:
    • Create a new block folder, for example, public/blocks/jbros-movie-info-block.
    • Inside this folder, create the necessary files for the block (index.js, edit.js, save.js, etc.).
    • In the edit.js file, set up the block attributes, including the “action” attribute (similar to the “Film Review” block).
    • Implement the functionality to fetch movie data from the TMDB API or retrieve existing data from the custom fields.
    • Render the movie poster and movie ID based on the fetched data.
    • Follow a similar approach to the “Film Review” block for handling the “create or use existing” functionality.
  5. Enqueue the New Block Assets:
    • In the Jbros_Film_Review_Public class, update the enqueue_block_assets() method to include the new block assets.
  6. Update the Plugin Dependencies:
    • In the load_dependencies() method of the main plugin class (Jbros_Film_Review), include the new class-tmdb-api.php file.

By following these steps, you’ll integrate the TMDB API functionality into your existing plugin. The new “Movie Info” block will allow users to fetch and display movie data (poster and ID) based on the “create or use existing” behavior, similar to the “Film Review” block.

Existing “Film Review” posts without movie data will be updated automatically when accessed, ensuring that all reviews have the associated movie information.

Note: You’ll need to handle error cases, such as when the TMDB API returns no results or encounters an error. Additionally, you may want to implement caching mechanisms to improve performance and reduce API requests.

I seem to have mistyped "6+" at this point, which Claude interpreted as feedback rating! On a scale 0-10, maybe 6+ is not too bad. But we're not done yet. I'll keep the 'typo' here as Claude does do a little recap. Me: +6

Thank you for the feedback! I’m glad I could provide a comprehensive solution to integrate the TMDB API functionality into your existing plugin. Here’s a summary of the key points:

  1. Connect to TMDB API:
    • Create a new class TMDB_API to handle API requests and fetch movie data from TMDB.
    • Obtain an API key from TMDB and use it in your requests.
  2. Get Movie Data and Store as Custom Fields:
    • Create a new method in Jbros_Film_Review_Public to fetch and store movie data.
    • Call the TMDB_API class to retrieve movie data based on film title and year.
    • Save the movie ID and poster URL as custom fields for the corresponding “Film Review” post.
  3. Update Existing Film Reviews:
    • When creating or updating a “Film Review” post, check if the movie ID and poster URL custom fields exist.
    • If they don’t exist, call the new method to fetch and store the movie data.
  4. Create a New Gutenberg Block for Movie Info:
    • Create a new block folder for the “Movie Info” block.
    • Set up block attributes, including the “action” attribute.
    • Implement functionality to fetch movie data from TMDB API or retrieve existing data from custom fields.
    • Render the movie poster and movie ID based on the fetched data.
    • Handle the “create or use existing” functionality similar to the “Film Review” block.
  5. Enqueue the New Block Assets:
    • Update the enqueue_block_assets() method to include the new block assets.
  6. Update the Plugin Dependencies:
    • Include the new class-tmdb-api.php file in the load_dependencies() method of the main plugin class.

By following these steps, you’ll successfully integrate the TMDB API functionality into your existing plugin, allowing users to fetch and display movie data (poster and ID) using the new “Movie Info” block. Existing “Film Review” posts without movie data will be updated automatically when accessed.

Remember to handle error cases, implement caching mechanisms for performance optimization, and follow best practices for integrating third-party APIs.

Claude mentions error handling, caching and performance, but as this plugin is just a demo and the post series already long, we'll ignore those for now, important as they are. Maybe another series of posts at some point for that.
Me: can you give me the class-tmdb-api.php contents

Sure, here’s an example of how the class-tmdb-api.php file might look like:

<?php
/**
 * TMDB API Integration
 *
 * @package jbros-film-review
 */

/**
 * TMDB_API class
 */
class TMDB_API {
    /**
     * TMDB API key
     *
     * @var string
     */
    private $api_key;

    /**
     * TMDB API base URL
     *
     * @var string
     */
    private $api_base_url;

    /**
     * Constructor
     */
    public function __construct() {
        $this->api_key      = 'your_tmdb_api_key'; // Replace with your actual TMDB API key
        $this->api_base_url = 'https://api.themoviedb.org/3';
    }

    /**
     * Get movie data by title and year
     *
     * @param string $title The movie title.
     * @param int    $year  The movie year.
     *
     * @return array|WP_Error Movie data on success, WP_Error on failure.
     */
    public function get_movie_data_by_title_and_year( $title, $year ) {
        $query_params = array(
            'api_key'   => $this->api_key,
            'query'     => $title,
            'year'      => $year,
            'language'  => 'en-US', // Set the desired language for movie data
        );

        $query_url = add_query_arg( $query_params, $this->api_base_url . '/search/movie' );

        $response = wp_remote_get( $query_url );

        if ( is_wp_error( $response ) ) {
            return $response;
        }

        $response_body = wp_remote_retrieve_body( $response );
        $response_data = json_decode( $response_body, true );

        if ( isset( $response_data['results'] ) && ! empty( $response_data['results'] ) ) {
            $movie_data = $response_data['results'][0];

            $movie_info = array(
                'id'         => $movie_data['id'],
                'poster_url' => 'https://image.tmdb.org/t/p/w500' . $movie_data['poster_path'],
            );

            return $movie_info;
        }

        return new WP_Error( 'tmdb_api_error', 'No movie data found.' );
    }
}

This TMDB_API class provides a constructor to set the TMDB API key (replace ’your_tmdb_api_key’ with your actual API key) and the API base URL.

The get_movie_data_by_title_and_year() method is responsible for fetching movie data from the TMDB API based on the provided title and year. Here’s how it works:

  1. It constructs the query parameters, including the API key, movie title, year, and language.
  2. It builds the query URL by appending the query parameters to the TMDB API search endpoint.
  3. It sends a GET request to the constructed URL using the wp_remote_get() function.
  4. If the response is successful, it retrieves the response body and decodes the JSON data.
  5. If the response contains movie results, it extracts the first result’s movie ID and poster URL (using the default poster size of ‘w500’).
  6. It returns an array containing the movie ID and poster URL.
  7. If no movie data is found or an error occurs, it returns a WP_Error object.

You can include this class-tmdb-api.php file in your plugin’s includes/api/ directory and use it in your Jbros_Film_Review_Public class to fetch movie data from the TMDB API.

Note: Make sure to replace ’your_tmdb_api_key’ with your actual TMDB API key. Additionally, you may need to handle additional error cases, implement caching mechanisms, and adjust the API requests based on your specific requirements.

Me: let's move on to step 2, "Get Movie Data and Store as Custom Fields". Please give me the updated files regarding this

Sure, let’s move on to step 2 and update the necessary files to get movie data and store it as custom fields.

  1. Update public/class-jbros-film-review-public.php:

Add a new method to fetch and store movie data:

/**
 * Fetch and store movie data for a film review.
 *
 * @param int    $post_id The post ID of the film review.
 * @param string $title   The movie title.
 * @param int    $year    The movie year.
 *
 * @return bool|WP_Error True on success, WP_Error on failure.
 */
private function fetch_and_store_movie_data($post_id, $title, $year)
{
    $tmdb_api = new TMDB_API();
    $movie_data = $tmdb_api->get_movie_data_by_title_and_year($title, $year);

    if (is_wp_error($movie_data)) {
        return $movie_data;
    }

    update_post_meta($post_id, 'jbros_movie_id', $movie_data['id']);
    update_post_meta($post_id, 'jbros_poster_url', $movie_data['poster_url']);

    return true;
}

This method creates an instance of the TMDB_API class and calls the get_movie_data_by_title_and_year() method to fetch movie data. If successful, it updates the custom fields jbros_movie_id and jbros_poster_url for the given post ID.

Then, update the create_or_update_film_review_post() method to call the new fetch_and_store_movie_data() method:

private function create_or_update_film_review_post($title, $year, $film_review)
{
    // ... (existing code)

    if (is_wp_error($post_id)) {
        return $post_id;
    }

    // Save post meta
    update_post_meta($post_id, 'film_title', $title);
    update_post_meta($post_id, 'film_year', $year);

    // Fetch and store movie data
    $this->fetch_and_store_movie_data($post_id, $title, $year);

    return $post_id;
}

Update includes/post-types.php:

Add the new custom fields to the jbros_film_review_render_meta_box() function:

function jbros_film_review_render_meta_box($post)
{
    $film_title = get_post_meta($post->ID, 'film_title', true);
    $film_year = get_post_meta($post->ID, 'film_year', true);
    $movie_id = get_post_meta($post->ID, 'jbros_movie_id', true);
    $poster_url = get_post_meta($post->ID, 'jbros_poster_url', true);

    wp_nonce_field('jbros_film_review_meta_box', 'jbros_film_review_meta_box_nonce');
    ?>
    <div>
        <label for="film_title"><?php esc_html_e('Film Title', 'jbros-film-review'); ?></label>
        <input type="text" id="film_title" name="film_title" value="<?php echo esc_attr($film_title); ?>" required>
    </div>
    <div>
        <label for="film_year"><?php esc_html_e('Film Year', 'jbros-film-review'); ?></label>
        <input type="number" id="film_year" name="film_year" value="<?php echo esc_attr($film_year); ?>" required>
    </div>
    <div>
        <label for="movie_id"><?php esc_html_e('Movie ID', 'jbros-film-review'); ?></label>
        <input type="text" id="movie_id" name="movie_id" value="<?php echo esc_attr($movie_id); ?>" readonly>
    </div>
    <div>
        <label for="poster_url"><?php esc_html_e('Poster URL', 'jbros-film-review'); ?></label>
        <input type="text" id="poster_url" name="poster_url" value="<?php echo esc_attr($poster_url); ?>" readonly>
    </div>
    <?php
}

This update displays the new custom fields (movie_id and poster_url) in the meta box, with the movie_id and poster_url fields set as read-only.

  1. Update includes/post-types.php:

Update the jbros_film_review_save_meta_box() function to save the new custom fields:

function jbros_film_review_save_meta_box($post_id, $post)
{
    // ... (existing code)

    if (isset($_POST['film_title'])) {
        update_post_meta($post_id, 'film_title', sanitize_text_field($_POST['film_title']));
    }

    if (isset($_POST['film_year'])) {
        update_post_meta($post_id, 'film_year', sanitize_text_field($_POST['film_year']));
    }

    // The movie_id and poster_url fields are read-only, so no need to update them.
}

This update ensures that the film_title and film_year custom fields are saved when the post is updated.

With these changes, when creating or updating a “Film Review” post, the plugin will automatically fetch and store the movie data (movie ID and poster URL) from the TMDB API as custom fields. The custom fields will be displayed in the meta box, with the movie_id and poster_url fields set as read-only.

For existing “Film Review” posts without movie data, the fetch_and_store_movie_data() method will be called automatically when the post is accessed, ensuring that the movie data is fetched and stored.

Me: great, I have that working now. Let's move on to step 3: Update Existing Film Reviews

Sure, let’s move on to step 3 and update the existing “Film Review” posts without movie data.

To achieve this, we need to modify the get_film_review_post_id() method in the public/class-jbros-film-review-public.php file. This method is responsible for retrieving the post ID of an existing film review post based on the title and year.

Currently, the get_film_review_post_id() method only checks for the film_title and film_year custom fields. We need to update it to also check if the jbros_movie_id and jbros_poster_url custom fields exist. If they don’t exist, we’ll call the fetch_and_store_movie_data() method to fetch and store the movie data.

Here’s the updated get_film_review_post_id() method:

/**
 * Get the post ID of an existing film review post.
 *
 * @param string $title The movie title.
 * @param int    $year  The movie year.
 *
 * @return int|null The post ID if found, or null if not found.
 */
private function get_film_review_post_id($title, $year)
{
    $query_args = array(
        'post_type'      => 'film_review',
        'post_status'    => 'publish',
        'posts_per_page' => 1,
        'meta_query'     => array(
            'relation' => 'AND',
            array(
                'key'   => 'film_title',
                'value' => $title,
            ),
            array(
                'key'   => 'film_year',
                'value' => $year,
            ),
            array(
                'relation' => 'OR',
                array(
                    'key'     => 'jbros_movie_id',
                    'compare' => 'NOT EXISTS',
                ),
                array(
                    'key'     => 'jbros_poster_url',
                    'compare' => 'NOT EXISTS',
                ),
            ),
        ),
    );

    $query = new WP_Query($query_args);

    if ($query->have_posts()) {
        $post_id = $query->posts[0]->ID;

        // Fetch and store movie data if it doesn't exist
        $movie_id = get_post_meta($post_id, 'jbros_movie_id', true);
        $poster_url = get_post_meta($post_id, 'jbros_poster_url', true);

        if (empty($movie_id) || empty($poster_url)) {
            $this->fetch_and_store_movie_data($post_id, $title, $year);
        }

        return $post_id;
    }

    return null;
}

Here’s what we’ve changed:

  1. In the meta_query array, we’ve added a new set of conditions with the relation parameter set to 'OR'. This checks if either the jbros_movie_id or jbros_poster_url custom field doesn’t exist for the post.
  2. After retrieving the post ID, we check if the jbros_movie_id and jbros_poster_url custom fields are empty.
  3. If either of the custom fields is empty, we call the fetch_and_store_movie_data() method, passing the post ID, title, and year to fetch and store the movie data.

With this update, whenever a “Film Review” post is accessed, the plugin will check if the movie data exists. If it doesn’t, it will automatically fetch and store the movie data from the TMDB API using the fetch_and_store_movie_data() method.

This ensures that existing “Film Review” posts without movie data will be updated with the necessary movie information when accessed, without the need for manual intervention.

Me: Ok, let's move on to step 4, the Gutenberg block for movie info. I've created, with the wordpress/create-block command, block 'jbros-movie-info-block' and it's in public/blocks/jbros-movie-info-block folder. I've added attribute 'action' in block.json as type of string. Walk me through the rest of the step 4

Certainly! Let’s proceed with step 4 and create the Gutenberg block for movie info in the public/blocks/jbros-movie-info-block folder.

  1. Update edit.js: Open the edit.js file and make the following changes:
import { useBlockProps, InspectorControls } from '@wordpress/block-editor';
import { PanelBody, PanelRow, SelectControl, TextControl } from '@wordpress/components';
import apiFetch from '@wordpress/api-fetch';
import { useEffect } from '@wordpress/element';

export default function Edit({ attributes, setAttributes }) {
    const { title, year, action, movieId, posterUrl } = attributes;

    const onChangeTitle = (newTitle) => {
        setAttributes({ title: newTitle });
    };

    const onChangeYear = (newYear) => {
        setAttributes({ year: newYear });
    };

    const onChangeAction = (newAction) => {
        setAttributes({ action: newAction });
    };

    const fetchMovieData = async () => {
        const response = await apiFetch({
            path: '/jbros-film-review/v1/get-movie-info',
            method: 'POST',
            data: {
                title: title,
                year: year,
                action: action,
            },
        });

        if (response.success) {
            setAttributes({
                movieId: response.data.movie_id,
                posterUrl: response.data.poster_url,
            });
        } else {
            setAttributes({
                movieId: '',
                posterUrl: '',
            });
        }
    };

    useEffect(() => {
        if (title && year) {
            fetchMovieData();
        }
    }, [title, year, action]);

    return (
        <>
            <InspectorControls>
                <PanelBody title="Movie Info" initialOpen={true}>
                    <PanelRow>
                        <TextControl
                            label="Movie Title"
                            value={title}
                            onChange={onChangeTitle}
                        />
                    </PanelRow>
                    <PanelRow>
                        <TextControl
                            label="Movie Year"
                            value={year}
                            onChange={onChangeYear}
                        />
                    </PanelRow>
                    <PanelRow>
                        <SelectControl
                            label="Action"
                            value={action}
                            onChange={onChangeAction}
                            options={[
                                { value: 'create_or_update', label: 'Create or Update' },
                                { value: 'get_existing', label: 'Get Existing' },
                            ]}
                        />
                    </PanelRow>
                </PanelBody>
            </InspectorControls>
            <div {...useBlockProps()}>
                <h3>Movie Info</h3>
                {movieId && <p>Movie ID: {movieId}</p>}
                {posterUrl && <img src={posterUrl} alt="Movie Poster" />}
            </div>
        </>
    );
}

In this updated edit.js file, we’ve added the necessary functionality to fetch movie data based on the selected action (create_or_update or get_existing). The fetchMovieData function sends a request to a custom API endpoint (/jbros-film-review/v1/get-movie-info) to retrieve the movie ID and poster URL. We’ve also added input fields for the movie title, year, and action in the block inspector controls. The fetched movie ID and poster URL are displayed within the block content area.

  1. Create save.js: Create a new file called save.js in the public/blocks/jbros-movie-info-block folder with the following content:
import { useBlockProps } from '@wordpress/block-editor';

export default function save({ attributes }) {
    const { movieId, posterUrl } = attributes;

    return (
        <div {...useBlockProps.save()}>
            <h3>Movie Info</h3>
            {movieId && <p>Movie ID: {movieId}</p>}
            {posterUrl && <img src={posterUrl} alt="Movie Poster" />}
        </div>
    );
}

This file defines how the block will be rendered on the front-end, displaying the movie ID and poster URL.

  1. Register Custom API Endpoint: In the public/class-jbros-film-review-public.php file, register a new custom API endpoint to handle the movie info requests:
public function register_api_endpoints()
{
    add_action('rest_api_init', function () {
        register_rest_route('jbros-film-review/v1', '/get-movie-info', array(
            'methods' => 'POST',
            'callback' => array($this, 'get_movie_info'),
            'args' => array(
                'title' => array(
                    'required' => true,
                    'type' => 'string',
                ),
                'year' => array(
                    'required' => true,
                    'type' => 'integer',
                ),
                'action' => array(
                    'required' => false,
                    'type' => 'string',
                    'default' => 'create_or_update',
                    'enum' => array('create_or_update', 'get_existing'),
                ),
            ),
            'permission_callback' => '__return_true',
        ));
    });
}

This code registers a new REST API endpoint (/wp-json/jbros-film-review/v1/get-movie-info) that accepts title, year, and action parameters.

  1. Implement get_movie_info Method: In the public/class-jbros-film-review-public.php file, add the get_movie_info method to handle the movie info requests:
public function get_movie_info($request)
{
    $title = sanitize_text_field($request->get_param('title'));
    $year = absint($request->get_param('year'));
    $action = sanitize_text_field($request->get_param('action'));

    if (empty($title) || empty($year)) {
        return new WP_REST_Response(array(
            'success' => false,
            'message' => 'Invalid request parameters.',
        ), 400);
    }

    $post_id = $this->get_film_review_post_id($title, $year);

    if ($action === 'get_existing' && $post_id) {
        $movie_id = get_post_meta($post_id, 'jbros_movie_id', true);
        $poster_url = get_post_meta($post_id, 'jbros_poster_url', true);

        return new WP_REST_Response(array(
            'success' => true,
            'data' => array(
                'movie_id' => $movie_id,
                'poster_url' => $poster_url,
            ),
        ), 200);
    }

    $tmdb_api = new TMDB_API();
    $movie_data = $tmdb_api->get_movie_data_by_title_and_year($title, $year);

    if (is_wp_error($movie_data)) {
        return new WP_REST_Response(array(
            'success' => false,
            'message' => $movie_data->get_error_message(),
        ), 500);
    }

    return new WP_REST_Response(array(
        'success' => true,
        'data' => $movie_data,
    ), 200);
}

This method handles the movie info requests based on the provided action. If the action is 'get_existing' and a post exists for the given title and year, it retrieves the movie ID and poster URL from the custom fields and returns them in the response. If the action is 'create_or_update', it calls the TMDB_API class to fetch the movie data and returns it in the response.

  1. Enqueue Block Assets: In the enqueue_block_assets() method of the public/class-jbros-film-review-public.php file, enqueue the assets for the “Movie Info” block:
public function enqueue_block_assets()
{
    $block_path = plugin_dir_path(__FILE__) . 'blocks/jbros-film-review-block/build/';
    wp_enqueue_script('jbros-film-review-block', plugins_url('blocks/jbros-film-review-block/build/index.js', __FILE__), array('wp-blocks', 'wp-element', 'wp-editor'), filemtime($block_path . 'index.js'));
    wp_enqueue_style('jbros-film-review-block-editor', plugins_url('blocks/jbros-film-review-block/build/index.css', __FILE__), array('wp-edit-blocks'), filemtime($block_path . 'index.css'));

    $movie_info_block_path = plugin_dir_path(__FILE__) . 'blocks/jbros-movie-info-block/build/';
    wp_enqueue_script('jbros-movie-info-block', plugins_url('blocks/jbros-movie-info-block/build/index.js', __FILE__), array('wp-blocks', 'wp-element', 'wp-editor'), filemtime($movie_info_block_path . 'index.js'));
    wp_enqueue_style('jbros-movie-info-block-editor', plugins_url('blocks/jbros-movie-info-block/build/index.css', __FILE__), array('wp-edit-blocks'), filemtime($movie_info_block_path . 'index.css'));
}

This code enqueues the JavaScript and CSS assets for the “Movie Info” block.

After making these changes, you should have a new Gutenberg block called “Movie Info” that allows users to enter a movie title and year, and choose the desired action (create_or_update or get_existing). Based on the selected action, the block will either fetch the movie data from the TMDB API or retrieve the existing data from the custom fields. The movie ID and poster URL will be displayed within the block content area.

Remember to update the register_api_endpoints() method in the public/class-jbros-film-review-public.php file to register the new API endpoint, and include the necessary dependencies in the load_dependencies() method of the main plugin class.

Coming up

Part 3 will have hopefully follow a bit faster than the second part did. We'll be working more on how Anthropic API works together with data from the TMDB API and move things around a bit. We'll also look at WordPress plugin development basics briefly and add some settings in the admin as a sample.