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 );
@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