0

I'm attempting to store both an Image's URL and ID in a post's meta field via a Gutenberg component, but am having trouble figuring out how to store those multiple values in an array.

I can store a single value as a meta value without a problem. I can even store a single value in the first spot of an array without a problem. But I cannot seem to work out the proper syntax to take advantage of Wordpress's ability to store an array as a meta value.

The overarching goal is to override the image being used when a post is shared on social media. The issue is that I need to transfer both the URL and ID over to a PHP hook for it to work properly with a plugin (The SEO Framework). Unfortunately, I can't just store the ID because I need the URL to render the image within the Gutenberg component.

Full Component Code:

const OGImage = compose(
    withDispatch( function( dispatch, props ) {
        return {
            setMetaValue: function( metaValue ) {
                dispatch( 'core/editor' ).editPost(
                    { meta: { [ props.metaKey ]: metaValue } }
                );
            }
        }
    } ),
    withSelect( function( select, props ) {
        return {
            metaValue: select( 'core/editor' ).getEditedPostAttribute( 'meta' )[ props.metaKey ]
        }
    } ) )( function( props ) {
            const fallbackInstructions = <p>{ __( 'To edit the OG image, you need permission to upload media.', 'atlantis') }</p>;
            const onUpdateImage = img => { props.setMetaValue( img.url ); };
            const onRemoveImage = () => { props.setMetaValue( null ); };
        return (
        <div className="og-image__wrapper">
            <MediaUploadCheck fallback={ fallbackInstructions }>
                <MediaUpload
                    title={ OG_IMAGE_LABEL }
                    onSelect={ onUpdateImage }
                    allowedTypes={ ALLOWED_MEDIA_TYPES }
                    render={ ( { open } ) => (
                        <Button
                            className={ ! props.metaValue ? 'og-image__toggle' : 'og-image__preview' }
                            onClick={ open }
                            aria-label={ ! props.metaValue ? null : EDIT_OR_UPDATE_OG_IMAGE_LABEL }>
                            { !! props.metaValue &&
                                    <img className='og-image__button' src={ props.metaValue } alt="" />
                            }
                            { ! props.metaValue && SET_OG_IMAGE_LABEL }
                        </Button>
                    ) }
                />
            </MediaUploadCheck>
            { !! props.metaValue &&
                    <MediaUploadCheck>
                        <MediaUpload
                            title={ OG_IMAGE_LABEL }
                            onSelect={ onUpdateImage }
                            allowedTypes={ ALLOWED_MEDIA_TYPES }
                            render={ ( { open } ) => (
                                <Button onClick={ open } isDefault isLarge>
                                    { REPLACE_OG_IMAGE_LABEL }
                                </Button>
                            ) }
                        />
                    </MediaUploadCheck>
                }
                { !! props.metaValue &&
                    <MediaUploadCheck>
                        <Button onClick={ onRemoveImage } isLink isDestructive>
                            { REMOVE_OG_IMAGE_LABEL }
                        </Button>
                    </MediaUploadCheck>
                }
        </div>
    );
    }
);

export default OGImage;

Bits that matter:

const OGImage = compose(
    withDispatch( function( dispatch, props ) {
        return {
            setMetaValue: function( metaValue ) {
                dispatch( 'core/editor' ).editPost(
                    { meta: { [ props.metaKey ]: metaValue } }
                );
            }
        }
    } ),
    withSelect( function( select, props ) {
        return {
            metaValue: select( 'core/editor' ).getEditedPostAttribute( 'meta' )[ props.metaKey ]
        }
    } ) )( function( props ) {

            const onUpdateImage = img => { props.setMetaValue( img.url ); };

        return (

            <img className='og-image__button' src={ props.metaValue } alt="" />

    );
    }
);

export default OGImage;

My optimal solution would be to do something like this, but this just stores the first value specified (so in this case, [img.url]):

const onUpdateImage = img => { props.setMetaValue( img.url, img.id ); };

...

<img className='og-image__button' src={ props.metaValue[0] } alt="" />

or even:

const onUpdateImage = img => { props.setMetaValue( [img.url, img.id] ); };

...

<img className='og-image__button' src={ props.metaValue[0] } alt="" />

I'm declaring the meta value as:

  register_meta(
    'post',
    '_og_image',
    [
      'show_in_rest' => true,
      'single' => false,
      'type' => 'string'
    ]
  );

As a last ditch effort, I'm considering combining both the Image URL and ID in a string and parsing it out on both ends. This solution feels hacky to me and I would prefer to store the two values independently as an array.

This is a crosspost.

Appendix for Tom. This worked once -- out of some strange fluke I'm assuming -- but I'm unable to reproduce it. The image url is being changed, but the image id refuses to save in the meta value.

PHP

function set_og_image() {
  register_meta(
    'post',
    'og_image_url',
    [
      'show_in_rest' => true,
      'single' => true,
      'type' => 'string'
    ]
  );
  register_meta(
    'post',
    'og_image_id',
    [
      'show_in_rest' => true,
      'single' => true,
      'type' => 'string' // Solution (thank you to Tom): This needs to be an integer type.
    ]
  );
}

JS

const OGImage = compose(
    withDispatch( function( dispatch, props ) {
        return {
            setMetaValue: function( metaKey, metaValue ) {
                dispatch( 'core/editor' ).editPost(
                    { meta: { [ metaKey ]: metaValue } }
                );
            }
        }
    } ),
    withSelect( function( select, props ) {
        return {
            metaValueURL: select( 'core/editor' ).getEditedPostAttribute( 'meta' )[ 'og_image_url' ],
            metaValueID: select( 'core/editor' ).getEditedPostAttribute( 'meta' )[ 'og_image_id' ]
        }
    } ) )( function( props ) {
            const onUpdateImage = img => {
                props.setMetaValue( 'og_image_url', img.url );
                props.setMetaValue( 'og_image_id', img.id );
      };
            const onRemoveImage = () => {
                props.setMetaValue( props.metaKeyURL, null );
                props.setMetaValue( props.metaKeyID, null );
       };
        return (
                ...
    );
    }
);

Parent Component:

const { __ } = wp.i18n;
const { Fragment } = wp.element;
const { PanelBody, PanelRow } = wp.components;
const { registerPlugin } = wp.plugins;
const { PluginSidebar, PluginSidebarMoreMenuItem } = wp.editPost;

import OGImage from './components/og-image';

const GenericSidebar = props => {
  return (
    <Fragment>
      <PluginSidebarMoreMenuItem target="generic-sidebar">
        {__("Generic Sidebar", "atlantis")}
      </PluginSidebarMoreMenuItem>
      <PluginSidebar
        name="generic-sidebar"
        title={__("Generic Sidebar", "atlantis")}
      >
        <PanelBody title={__("OG Image", "atlantis")} >
          <PanelRow>
            <OGImage
              metaKeyURL = { 'og_image_url' }
              metaKeyID = { 'og_image_id' }
             />
          </PanelRow>
        </PanelBody>
      </PluginSidebar>
    </Fragment>
  );
};

registerPlugin("generic-sidebar", {
  icon: "visibility",
  render: GenericSidebar
});
  • Can you provide some context as to how this fits into Gutenberg? It looks like an arbitrary React component that calls the main data store, but it's not clear how it gets its props, where it gets them, or what it's doing – Tom J Nowell Oct 17 at 18:56
  • Sure. It's part of a fragment that goes inside a registerPlugin() call to create a generic sidebar. I'll update the post again with code. – gardinermichael Oct 17 at 18:58
0

Have you tried using:

            setImage: function( image_url, image_id ) {
                dispatch( 'core/editor' ).editPost(
                    {
                        meta: {
                            'og_image_url': image_url,
                            'og_image_id': image_id
                        }
                    }
                );
            }

Instead of using a generic setMetaValue function?

  • Just tried this out and unfortunately, it returns the same "updating failed" error when the post is saved, which in the console translates to a POST https://sagepipeline.valet/wp-json/wp/v2/posts/21085?_locale=user 400 – gardinermichael Oct 17 at 19:06
  • Are og_image_url and og_image_id registered meta with the appropriate types? Note that an ID is a number not a string. And can you confirm it works if you save just one or the other, but fails when both are present? Also when you get a 400 response, what does the response actually say? The REST API tends to tell you what you did wrong outright in the reply – Tom J Nowell Oct 17 at 19:11
  • Tom, you genius. – gardinermichael Oct 17 at 19:12
  • eh what did I do? – Tom J Nowell Oct 17 at 19:13
  • That did the trick! I change the ID meta key/value to an integer type and it worked brilliantly. Thank you so very much, I can't believe I missed that. I've been stewing on it for days. I wish I could give you more upvotes, you deserve them! – gardinermichael Oct 17 at 19:17
0

The short answer is, you shouldn't.

Post meta/etc store a single value, and to get around that some people PHP serialize the data. So much so that core started doing it automatically if you passed an array or object so that it could store a string.

The problem is this opens you up to PHP serialisation attacks, and is a security concern. Additionally, it makes it incredibly difficult to query for those values.

So instead, the correct solution, is to store 2 meta key/value pairs, not 1. That means calling register_meta twice, with different key names for the ID and the URL.

There is also the alternative of retrieving the URL given an attachment ID via the REST API, or the local storage. You don't need to store the URL in post meta to be able to show an attachment IDs preview. Gutenberg manages it with the featured image, so it's possible

  • Thank you for your response Tom. I initially tried to store two meta key/value pairs, but ran into an "updating failed/publishing failed" problem and was attempting to workaround by consolidating the API calls. I've posted an appendix above showcasing what I mean. – gardinermichael Oct 17 at 18:48
  • Regarding retrieving the URL given an attachment ID via the REST API, I've been unable to reverse engineer how the featured image component handles it. The entire image seems to be passed down as a prop into the component, with mediaSourceUrl being one of its attributes. – gardinermichael Oct 17 at 18:55

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.