Skip to content

Instantly share code, notes, and snippets.

@frzsombor
Last active July 26, 2024 08:59
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 );
@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!

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