Skip to content

Instantly share code, notes, and snippets.

@frzsombor
Last active September 28, 2024 15:49
Show Gist options
  • Save frzsombor/c53446050ee0bb5017e29b9afb039309 to your computer and use it in GitHub Desktop.
Save frzsombor/c53446050ee0bb5017e29b9afb039309 to your computer and use it in GitHub Desktop.
Fix WordPress Shortcodes in Query Loop Post Templates

Fix WordPress Shortcodes in Query Loop Post Templates

The problem

Before rendering a page, WordPress first generates the output for all blocks without actually printing them. When using a Query Loop, during each "loop cycle" WordPress generates the block code for each loop item using its render_block() function. Here, we still have access to the "current" post, however, the issue is that WordPress does not process shortcodes in its render function. Instead, shortcodes are only processed when the full page content is outputted (composed of the previously rendered blocks). By that point, the shortcodes will no longer have access to their corresponding post but instead will refer to the current post, which is the rendered page itself.

The solution

TL;DR:

1 ) Copy the JS code and upload it to the server (e.g., in a child theme or add it to the site in any way, but ensure it loads on the block editor page!). 2 ) Copy the PHP code and add it to the site as custom code (using a plugin or in the child theme's PHP file). If you load the JS file with this PHP code, make sure to set its URL and PATH correctly. If not, remove that code part.

Detailed Description:

I was looking for a solution that doesn't require "hacking" the basic functionality of WordPress, is lightweight, universal, easy to use, and configurable, while also ensuring everything works as originally intended where this solution isn't needed. I found the solution on the PHP side using the render_block hook, and on the block editor side through filters that allow custom attributes to be added by block type.

First, we need to add a new block attribute (let's call it "forceShortcodes") to the block types where shortcodes might appear. This should be set individually for each block in the block editor from the sidebar, ideally placed at the bottom of the sidebar in the "Advanced" section. Thanks to this, we can set the "forceShortcodes" custom attribute for any block, for example, those used in a query loop for displaying shortcodes. Then, we need to hook into the render_block filter and check if the currently rendered block has "forceShortcodes" enabled. If it does, we process the shortcode immediately and return the block's content this way.

Usage

Below you will find two files necessary for this solution.

  1. The JS file is complete and will work as it is. If you want to make any changes, you can find a "Basic config" section at the beginning of the JS code, where you can adjust the name of the parameter used (default is "forceShortcodes") and the text displayed in the sidebar control box. If you change the "attributeName" make sure to also change it in the PHP code. Below that, there is an "Advanced config" section. Here you can change the attribute control box location (by default, on the bottom of the sidebar in "Advanced" group or a separate custom control box). More importantly, here you can configure which block types can have the forced shortcode rendering mode set. By default, I've enabled it for all core WordPress blocks in the "Text" group. If you want to change this or also enable it for your own custom block types, I've included instructions as comments in the file on how to make these changes in seconds.
  2. In the PHP file, you only have to change the URL and the PATH of the JavaScript file if you want to load it with this PHP code. Also, if you changed the "attributeName" in the JS, make sure to change it accordingly here too.

Follow-up

Please, if you encounter any issues with the code's functionality despite following the steps, or if you have any suggestion for improvement, let me know in the comments!

/**
* Add "Force Shortcodes" setting to specific blocks
* by @frzsombor - 2024
*/
(function() {
/* Basic config */
const attributeNS = 'frzsombor/force-shortcodes'; // Namespace for JS filters
const attributeName = 'forceShortcodes'; // camelCasedName for storing data
const attributeTitle = 'Shortcode rendering method';
const attributeLabel = 'Force render shortcodes';
/* Advanced config */
// addInAdvancedControl (bool)
// [true] : Add to existing "Advanced" control block
// [false] : Add as a standalone control block
const addInAdvancedControl = true;
// addToBlocks (bool|array)
// [true] : Add attribute to all blocks
// [array] : Add attribute to blocks matching the categories in this array
// [false] : Add attribute to blocks in addToCustomBlocks only
const addToBlocks = ['text'];
// addToCustomBlocks (array)
// Array of full block names like ['core/heading', 'core/paragraph', etc.]
// These are added along with the blocks defined by addToBlocks
const addToCustomBlocks = [
'core/shortcode'
];
/* Initialization */
const { addFilter } = wp.hooks;
const { createElement, Fragment } = wp.element;
const { createHigherOrderComponent } = wp.compose;
/* Register the custom attribute for required blocks */
function addCustomAttribute(settings, name) {
const hasAttributes = typeof settings.attributes !== 'undefined';
const includeAll = (addToBlocks === true);
const matchesCategory = Array.isArray(addToBlocks) ? addToBlocks.includes(settings.category) : addToBlocks;
const inCustomList = addToCustomBlocks.includes(name);
if (hasAttributes && (includeAll || matchesCategory || inCustomList)) {
settings.attributes[attributeName] = {
type: 'boolean',
default: false
};
}
return settings;
}
addFilter(
'blocks.registerBlockType',
attributeNS,
addCustomAttribute
);
/* Add custom control to the block settings sidebar */
const { InspectorControls } = wp.blockEditor; // For standalone control
const { InspectorAdvancedControls } = wp.blockEditor; // For adding to Advanced control
const { PanelBody, CheckboxControl } = wp.components;
const { withSelect, withDispatch } = wp.data;
const { compose } = wp.compose;
const CustomAttributeControl = compose(
withSelect((select) => {
return {
selectedBlock: select('core/block-editor').getSelectedBlock(),
};
}),
withDispatch((dispatch) => {
return {
updateBlockAttributes: dispatch('core/block-editor').updateBlockAttributes,
};
})
)(({ selectedBlock, updateBlockAttributes }) => {
// If there is no selectedBlock or block doesn't have the custom attribute defined
if (!selectedBlock || typeof selectedBlock.attributes[attributeName] === 'undefined') {
return null;
}
const attributeValue = selectedBlock.attributes[attributeName] || false;
if (addInAdvancedControl) {
// Add to Advanced control block
return createElement(
InspectorAdvancedControls,
null,
createElement(CheckboxControl, {
help: attributeTitle,
label: attributeLabel,
checked: attributeValue,
onChange: (value) => updateBlockAttributes(selectedBlock.clientId, { [attributeName]: value })
})
);
}
else {
// Add as a standalone control block
return createElement(
InspectorControls,
null,
createElement(
PanelBody,
{ title: attributeTitle, initialOpen: true },
createElement(CheckboxControl, {
label: attributeLabel,
checked: attributeValue,
onChange: (value) => updateBlockAttributes(selectedBlock.clientId, { [attributeName]: value })
})
)
);
}
});
const withInspectorControl = createHigherOrderComponent((BlockEdit) => {
return (props) => {
return createElement(
Fragment,
null,
createElement(BlockEdit, props),
createElement(CustomAttributeControl, {
...props
})
);
};
}, 'withInspectorControl');
addFilter(
'editor.BlockEdit',
attributeNS.replace('/', '/with-') + '-inspector-control',
withInspectorControl
);
/* Ensure attribute value is saved and rendered */
const withCustomAttributeSave = createHigherOrderComponent((BlockListBlock) => {
return (props) => {
return createElement(BlockListBlock, props);
};
}, 'withCustomAttributeSave');
addFilter(
'editor.BlockListBlock',
attributeNS.replace('/', '/with-') + '-save',
withCustomAttributeSave
);
function addCustomAttributeSave(element, blockType, attributes) {
if (attributes[attributeName]) {
element.props[attributeName] = attributes[attributeName];
}
return element;
}
addFilter(
'blocks.getSaveElement',
attributeNS + '-save',
addCustomAttributeSave
);
})();
<?php
// Enqueue the JS file (TODO: change url and path!)
add_action( 'enqueue_block_editor_assets', function() {
wp_enqueue_script(
'fzs-force-shortcodes-setting',
URL_TO_THE_JS_FILE, // Change this!
array( 'wp-blocks', 'wp-element', 'wp-editor' ),
filemtime( PATH_TO_THE_JS_FILE ), // Change (or remove)! This is a path! eg: /home/username/etc/etc/force-shortcodes-setting.js
true
);
} );
// Handle the blocks with "forceShortcodes" attribute (chage attribute if you changed it in the JS)
add_filter( 'render_block', function( $block_content, $block, $instance ) {
if ( isset( $block['attrs'] ) && ! empty ( $block['attrs']['forceShortcodes'] ) ) {
return do_shortcode($block_content);
}
return $block_content;
}, 10, 3 );
@spreaderman
Copy link

spreaderman commented Jul 8, 2024

Wonderful and am trying to get it working.

I added force-shortcodes-setting.php to functions.php. I then changed force-shortcodes-setting.php to below. Not sure how to get this going:

// https://gist.github.com/frzsombor/c53446050ee0bb5017e29b9afb039309
// Enqueue the JS file (TODO: change url and path!)
add_action( 'enqueue_block_editor_assets', function() {
    wp_enqueue_script(
        'fzs-force-shortcodes-setting',
        'https://www.example.com/wp-content/plugins/fs-api/js/', // Change this!
        array( 'wp-blocks', 'wp-element', 'wp-editor' ),
        filemtime( '/var/www/example.com/public_html/wp-content/plugins/fs-api/js/force-shortcodes-settings.js' ), // Change (or remove)! This is a path! eg: /home/username/etc/etc/force-shortcodes-setting.js
        true
    );
} );

// Handle the blocks with "forceShortcodes" attribute (chage attribute if you changed it in the JS)
add_filter( 'render_block', function( $block_content, $block, $instance ) {
    if ( isset( $block['attrs'] ) && ! empty ( $block['attrs']['forceShortcodes'] ) ) {
        return do_shortcode($block_content);
    }
    return $block_content;
}, 10, 3 );

@frzsombor
Copy link
Author

@spreaderman At the URL_TO_THE_JS_FILE line, it seems like you missed adding the actual file name: force-shortcodes-settings.js to the URL.

@spreaderman
Copy link

@frzsombor I have updated the filename. Not working yet.

When I go to the URL the js file loads:
https://www.example.com/wp-content/plugins/fs-api/js/force-shortcodes-settings.js

When I 'more' the file it finds it:
/var/www/example.com/public_html/wp-content/plugins/fs-api/js/force-shortcodes-settings.js

The script is placed in functions.php. It is run. I inserted echo "hello"; and it is also displayed on the screen.

When I go to the block editor, the toggle to force shortcodes is not displayed.

image

Any pointers appreciated.

Wordpress 6.5.5

@frzsombor
Copy link
Author

I see, thanks for the screenshot! On your image, currently the Query Loop is selected, but you have to enable the added "Force Render" feature on a block, that actually contains the shortcode. As you can see in the Usage description, by default I've enabled the feature for all blocks in the "text" category (with variable name: addToBlocks) and additionally for the "core/shortcode" block (in addToCustomBlocks variable). It is good that you pointed out that the "core/html" can also contain shortcodes, I will update my gist soon to include it too! Until then, you can update the addToCustomBlocks manually to this:

const addToCustomBlocks = [
	'core/shortcode',
	'core/html'
];

This way, the "Force Render" feature will be enabled for HTML blocks too and hopefully, you will be able to use it with your HTML blocks inside the Post Template.

Please let me know if it works or not!

@spreaderman
Copy link

@frzsombor I did not realize that. I have added, 'core/html'. The box now appears, however, how my shortcodes continue to show the same data repeatedly. Below is a screen shot with the data. I thought that if I am in the loop, the id will be made available.

image

This a portion of the shortcode;

`
add_shortcode('fs_ski_resort_field', 'fs_ski_resort_field_fn');

function get_acf_key($key){
$fs_ski_resort_data = get_field('field_64b38c4e3ec56'); // this is the group
switch ($key) {
case 'aa_min': $result = $fs_ski_resort_data['fs_aa_min'] ?: $fs_ski_resort_data['qgis_aa_min']; break; // min elevation
case 'aa_max': $result = $fs_ski_resort_data['fs_aa_max'] ?: $fs_ski_resort_data['qgis_aa_max']; break; // max elevation
return $result;
}

function fs_ski_resort_field_fn($attr){
$key = shortcode_atts(array(
'key' => ''
), $attr);
$the_value_of_fs_field = get_acf_key($key['key']);
return $the_value_of_fs_field;
}
`
The above code works with non block editor theme type.

Any suggestions super appreciated.

@mwt
Copy link

mwt commented Jul 13, 2024

It works for me, but I get the following error when I try to edit the page again:
This block contains unexpected or invalid content. Attempt Block Recovery?

It logs the following js error. I guess it's adding an extra div somewhere. In my case, I decided it was better to just go PHP only anyway. I can add the property manually. Thanks!

blocks.min.js?ver=6612d078dfaf28b875b8:19 Block validation: Block validation failed for `core/shortcode` ({name: 'core/shortcode', icon: {…}, keywords: Array(0), attributes: {…}, providesContext: {…}, …}).

Content generated by `save` function:

<div><div>[metalookup field="position" default="Board Member"]</div></div></div></div>

Content retrieved from post body:

<div>[metalookup field="position" default="Board Member"]</div></div></div>

@spreaderman
Copy link

@mwt How are you doing it "manually" with PHP? I have struggled with this issue for a couple of months so if there is a quick and dirty way to fix, really curious.

@mwt
Copy link

mwt commented Jul 14, 2024

My current solution is a small edit of yours. It runs if I set the name of my shortcode block to "Forced Shortcode". I like this because it's easy to tell when the hack is being used by looking at the name of the block.

// Handle the blocks named "Forced Shortcode" (use this attribute inside query loops!)
// Usage example:
// <!-- wp:shortcode {"metadata":{"name":"Forced Shortcode"}} -->
// <div>[metalookup field="position" default="Board Member"]</div>
// <!-- /wp:shortcode -->
// Source: https://gist.github.com/frzsombor/c53446050ee0bb5017e29b9afb039309
add_filter('render_block', function ($block_content, $block, $instance) {
    if (isset($block['attrs']) && isset($block['attrs']['metadata']) && isset($block['attrs']['metadata']['name']) && $block['attrs']['metadata']['name'] === 'Forced Shortcode') {
        return do_shortcode($block_content);
    }
    return $block_content;
}, 10, 3);

https://github.com/mwt/dcpbk-twenty-twenty-four/blob/af3db49742969527ff729229da9986f00216e97e/functions.php#L41L52

@frzsombor
Copy link
Author

@spreaderman in your fs_ski_resort_field_fn function, try to check if you can get the correct $post object with:

function fs_ski_resort_field_fn($attr){
    global $post;
    var_dump($post->ID ?? null); //or var_dump($post);
    //...
}

If here, the var_dump logs correct posts/post IDs in the loops, the solution is working well and the problem is somewhere in the reading of the parameters. Also, you can try to pass the $post object's ID to the get_field function as a second parameter to make sure ACF uses the Post object that you want.

@frzsombor
Copy link
Author

frzsombor commented Jul 15, 2024

@mwt You are using the Shortcode block as a HTML block and it contains invalid HTML. If you are using the shortcode block, do not add additional HTML content, just the shortcode. If you want to use HTML, then use the HTML block. Also, enable the script for "core/html" too. In your log, you can see your shortcode block contains two opening divs and four closing divs. This is not added by my script, but either added by you or the result of an invalid html structure in your page.

@mwt
Copy link

mwt commented Jul 15, 2024

This is not added by my script, but either added by you or the result of an invalid html structure in your page.

Weird. I definitely only added a shortcode block. I have no custom html tags on my site. The actual bug is that it adds one div closing tag on each save and I get an opening tag when I press the attempt recovery button.

I don't load the JavaScript anymore and my site is working fine now.

@spreaderman
Copy link

spreaderman commented Jul 20, 2024

Many thanks for the help. I have tried:

global $post;
var_dump($post->ID ?? null); //or var_dump($post);

And it gives an ID of 2019 and 6 times. So it is repeating the first ID.

I tried it just with shortcode block (not html) and same thing.

@mwt
Copy link

mwt commented Jul 20, 2024

And it gives an ID of 2019 and 6 times. So it is repeating the first ID.

If you check the code editor, do you see the forceShortcodes attribute? If so, it's an issue with the filter in the second half of the PHP.

@spreaderman
Copy link

spreaderman commented Jul 20, 2024

@mwt yes, I see and have checked the box to enable forceshortcodes on each html block.

You mean there is a problem with below code?

add_filter( 'render_block', function( $block_content, $block, $instance ) {
if ( isset( $block['attrs'] ) && ! empty ( $block['attrs']['forceShortcodes'] ) ) {
return do_shortcode($block_content);
}
return $block_content;
}, 10, 3 );

When I print_r( $block['attrs']); I do not see forceShortcodes in the array.

@mwt
Copy link

mwt commented Jul 20, 2024

@spreaderman that's what I meant, but I must be wrong then if it's not in the array. I don't know.

Maybe some other attr would work? You can try what I do. I use this PHP snippet without any js and set the name of the block to "Forced Shortcode" instead of setting the attribute: https://gist.github.com/frzsombor/c53446050ee0bb5017e29b9afb039309?permalink_comment_id=5120588#gistcomment-5120588

@spreaderman
Copy link

spreaderman commented Jul 20, 2024

@mwt thanks. I am no expert with the block editor. Where does the below snippet go please? Much appreciated.

 <!-- wp:shortcode {"metadata":{"name":"Forced Shortcode"}} -->
 <div>[metalookup field="position" default="Board Member"]</div>
 <!-- /wp:shortcode -->

@mwt
Copy link

mwt commented Jul 20, 2024

That's just the specific shortcode that I was trying to get working. You could paste it in the non-visual post editor and change the short code or you could just rename your html/short code block to Forced Shortcode in the GUI.

@spreaderman
Copy link

I think I only have access to the block editor. I tried to use HTML but it doesn't work.

Is there anything in the works with wordpress to fix this problem? Any other work arounds that might work? Thankyou.

@mwt
Copy link

mwt commented Jul 25, 2024

You can rename the block in the editor:

Screenshot of user renaming block to "Forced Shortcode" in the left side block menu

@spreaderman
Copy link

spreaderman commented Jul 26, 2024

Sorry, I am really lost and not sure how to do this. I have this in my functions.php:

add_filter('render_block', function ($block_content, $block, $instance) {
if (isset($block['attrs']) && isset($block['attrs']['metadata']) && isset($block['attrs']['metadata']['name']) && $block['attrs']['metadata']['name'] === 'Forced Shortcode') {
return do_shortcode($block_content);
}
return $block_content;
}, 10, 3);

I understand that in order to return do_shortcode(), $block['attrs'] must be set and $block['attrs'] must be set and $block['attrs']['metadata']['name'] must be set and $block['attrs']['metadata']['name'] must be set but what is supposed to be in there? Not understanding at all. Also, if I place <!-- wp:shortcode etc into html it just displays the same code. Why does the block need to be renamed?

I appreciate all the help you have given very much!

@mwt
Copy link

mwt commented Jul 31, 2024

That $block['attrs']['metadata']['name'] field is the name of the block. The filter only runs do_shortcode if the name of the block is Forced Shortcode.

That's why you need to rename the block. Pasting that content into the code editor (not the html block) would set the block's name correctly. However, it's probably easier to just rename the block using the rename option in the menu.

@spreaderman
Copy link

Thanks. I have a block called "code". I have entered as follows;

image

The block was renamed Forced Shortcode as follows:

image

Code in plugin as follows;

add_filter('render_block', function ($block_content, $block, $instance) {
    if (isset($block['attrs']) && isset($block['attrs']['metadata']) && isset($block['attrs']['metadata']['name']) && $block['attrs']['metadata']['name'] === 'Forced Shortcode') {
        return do_shortcode($block_content);
    }
    return $block_content;
}, 10, 3);

But it just displays this;

image

@mwt
Copy link

mwt commented Aug 2, 2024

You need a shortcode block named "Forced Shortcode" containing only the shortcode. You don't want the comments if you're using the visual editor.

@spreaderman
Copy link

Thank you @mwt. I got the shortcode working but it repeats the same data.

image

@spreaderman
Copy link

spreaderman commented Aug 2, 2024

This is interesting. I added this to your php:

        global $post;
        return do_shortcode($block_content).$post->ID;

I can see the unique post id now !!

I am using https://www.advancedcustomfields.com/resources/get_field/

In the method you provided, I clearly get the unique post id. When I add the below to my method with get_field only the first post id is repeated (in my case, repeated 6x).

below code in my method only repeats the first id 6x:

          global $post;
      echo "test<br>";
    var_dump($post->ID ?? null);

@jrevillini
Copy link

YOU SAVED MY LIFE!!! 🙏🙏🙏
For SEO purposes and other developers sakes, I'm adding the following:

[SOLVED] Real-time render of shortcodes in Wordpress Query Loop block

SOLVED 2024: Get $post->ID in shortcodes in Wordpress Query Loop block

SOLVED 2024: Get $post data within shortcode inside Wordpress Query Loop block

Others who find this solution and implement: PLEASE ADD MORE TAGS and let's get this moved up on WP's list of core features PLEASE

@jrevillini
Copy link

I am already using a plugin called Attributes for Blocks which lets me set custom attributes on most blocks (but unfortunately not the shortcode or HTML block ... kinda curious how this works in your JS). I thought I could just add an attribute forceShortcodes=1 on a paragraph block, but it wasn't working. Turns out that while the HTML winds up rendering as expected, the $block is structured a little different (it puts attributes into $block['attrs']['attributesForBlocks']['...']). I'm providing my code and method for making this work without doing the JS file or modifying any PHP files:

  1. install Code Snippets plugin and Attributes for Blocks plugins and activate them.
  2. copy the PHP code below into a snippet, set it to run only on front end.
  3. Edit a page with a Query Loop block (save/reload if already open)
  4. Add a Group block to the Post Template block.
  5. In the Advanced Panel for the Group block, go to Additional Attributes, add a forceShortcodes attribute, set it to 1.
  6. Within the same Group block, add your shortcode block.
    Hit save and you should be done.

PHP for Code Snippet:

// Handle the blocks with "forceShortcodes" attribute (chage attribute if you changed it in the JS)
add_filter( 'render_block', function( $block_content, $block, $instance ) {
    if ( isset( $block['attrs'] ) && ! empty ( $block['attrs']['attributesForBlocks']['forceShortcodes'] ) ) {	// altered to use Attributes for Blocks plugin
        return do_shortcode($block_content);
    }
    return $block_content;
}, 10, 3 );

I realize that this still has so much room for error. Trying to think of how to make this easier to implement. @frzsombor what about if we just simplify this to work with a class that can be added to the block without having to modify the editor environment at all? I'll experiment and follow up.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment