Editorial Style Guide Notes
Editorial Style Guide
The Challenge
One of the challenges I've encountered in migrating UCSC's Communications & Marketing (C&M) website over to the new network is how to address the Editorial Style Guide. The original site is built with a bespoke theme and relies heavily on Advanced Custom Fields (ACF). The new site will be on our campus WordPress network, using the official UCSC Block Theme.
Like most of the original C&M site, the Editorial Style Guide was built using ACF. The style guide itself is a custom post type (CPT) with 26 posts, and each post is a letter of the English alphabet, A through Z.
The Style Guide CPT has an ACF field group associated with it that holds the guide "definition" content displayed on the front-end. The ACF field group consists of a single repeater field that contains two additional fields, a text field for the "item" and a WYSIWYG Editor field for the "definition" (see image below).
The original site was built pre-Gutenberg and the ACF content is rendered in PHP
using custom loops on custom templates. The task is to import this content and incorporate it into the current Block Theme paradigm.
The Process
Export and import fields and content
I was able to export the ACF Field Groups from the current C&M site as .json
and import them into a local development site with ACF that I spun up using wp-env. Our campus network uses ACF, so if I can do this locally I can do this on the network.
In the original site, I registered the CPT via a custom plugin. I can't rely on this plugin on the new site, so I used ACF to create an "empty" CPT on my dev site using the exact same name and slug as on the original site.
Once this was done, I went into the original site's dashboard and exported just the Style Guide's CPT as a .xml
file and imported it into my dev site.
The import worked perfectly. I had all my CPT posts from the old site and in their editors, I had their ACF fields with all the content.
The Problem
The problem is displaying that content on the front-end.
wp_postmeta
vs post_content
ACF data is metadata -- it is stored as separate entries in the wp_postmeta
tables of the database; whereas block data is stored as part of post_content
. As mentioned, the original site was a complete custom build. I relied on custom templates containing custom loops to render my custom fields on the front-end of the site. I don't have the ability to create custom PHP
templates in the new site so I need a way to get this content onto the page.
Post content vs Post metadata
First Attempted Solution
ACF Block
ACF Pro provides a PHP
based framework for creating custom blocks for their fields. My first thought was that I needed to create a new custom block for my ACF Style Guide fields. Using a development plugin, I followed their tutorial for creating an ACF Block and was able to create a block that displayed my fields. However, the resulting block only allowed creating new entries with those fields; I was unable to display the entries that I imported from the original site via the block.
Custom ACF Block with fields but no data
After much time searching for an answer, I learned that because of the different places ACF data and Block data is stored, there is not an elegant situation for this situation.
Second Attempted Solution
Plugin
As part of my research, I came across the Meta Field Block plugin in WordPress' Plugin Directory. According to its directory page, it will display custom fields in WordPress Gutenberg effortlessly. This did not turn out to be the case in my experiment. While the plugin claims to support "all ACF field types," it did not work for my repeater field in this use case so I abandoned it.
Repeater field not supported in Meta Field Block plugin
My Solution
Shortcodes
Since ACF data is stored in wp_postmeta
and block data is stored in post_content
, I decided that a shortcode would be the best way to display this content. The WordPress Gutenberg editor provides a Shortcode block, so a properly developed shortcode (or two) should do the trick.
As mentioned above, the original bespoke theme used custom loops in custom page and post templates to render field data on the front-end. For the new site, I need to convert the custom loops into shortcodes.
We have a method for adding custom code to UCSC websites. We maintain a custom functionality plugin that we update regularly. We already provide a few shortcodes in this plugin. This means we have a place outside the theme to develop a bit more "custom functionality." The next update of the plugin would make it available for all of our sites (although it would only work on the new C&M site).
The Original Single Template Loop
The original site's theme was developed using the StudioPress Genesis Framework. The original theme's code below is from the CPT's "single.php
" template. It removes the default genesis_loop
and replaces it with the bb_a_z_style_guide_single_loop()
function, which returns the ACF field data.
remove_action( 'genesis_loop', 'genesis_do_loop' );
add_action( 'genesis_loop', 'bb_a_z_style_guide_single_loop' );
function bb_a_z_style_guide_single_loop(){
echo '<article class="post type-post status-publish entry">';
echo '<div class="entry-content" itemprop="text">';
if( have_rows('style_definitions') ):while( have_rows('style_definitions') ): the_row();
// vars
$azItem = get_sub_field('editorial_style_item');
$azDef = get_sub_field('editorial_style_definition');
echo '<p><b>'.$azItem.':</b></p>'.$azDef.'<hr>';
endwhile;
endif;
echo '</div>';
echo '</article>';
}
The Single Post Shortcode
Converting this to a shortcode is not to too difficult. Since I imported my field definitions from the original site, the names in field calls are exactly the same. I stripped out all the structural html
code, as it's no longer necessary.
return
, don't echo
An important thing to remember when writing shortcodes in PHP
is that they are return
ed not echo
ed. So, all the echo
statements in the above function need to be converted to a return
. I set up an empty string variable at the beginning that I call $finaldefs
. I build on it using .=
concatenation and the $finaldefs
variable is what is ultimately returned.
In the following example, the resulting shortcode is called [style-definition]
.
add_shortcode( 'style-definition','bb_a_z_style_guide_single_loop' );
function bb_a_z_style_guide_single_loop(){
$finaldefs = '';
if( have_rows('style_definitions') ):while( have_rows('style_definitions') ): the_row();
$azItem = get_sub_field('editorial_style_item');
$azDef = get_sub_field('editorial_style_definition');
$finaldefs .= '<p><b>'.$azItem.':</b></p>'.$azDef.'<hr>';
endwhile;
endif;
return $finaldefs;
}
The Original Archive Template Loop
For the "Archive template" (I actually used this in a Page not the archive template itself -- but the concept is the same), I wrote a similar loop. The "archive loop" needs to show all content from every CPT post in addition to some precursor content that appears before it. This is an "A to Z Style Guide" so there are 26 posts in this CPT. The "archive" loop needs to list all posts and their content alphabetically. Navigating the style guide is similar to navigating a dictionary or encyclopedia. The original site's code below does this.
remove_action( 'genesis_loop', 'genesis_do_loop' );
add_action( 'genesis_loop','bb_a_z_styles_archive_loop' );
function bb_a_z_styles_archive_loop() {
$pageContent = get_the_content();
$pageContentFormatted = apply_filters('the_content', $pageContent);
echo '<article class="entry">';
echo '<div class="entry-content">';
echo $pageContentFormatted;
echo '<div class="clear"></div>';
echo '<div class="two-thirds first">';
echo '<hr>';
// WP_Query $args for style guide post type
$args = array (
'post_type' => 'a_z_style_guide',
'orderby' => 'title',
'order' => 'ASC',
'posts_per_page' => -1,
);
// New WP_Query
$azDir = new \WP_Query( $args );
if ($azDir->have_posts()) :
while ($azDir->have_posts()) :
$azDir->the_post();
$azTitle = get_the_title();
echo '<h2>'.$azTitle.'</h2>';
// Second loop of style definitions
if( have_rows('style_definitions') ):while( have_rows('style_definitions') ): the_row();
// vars
$azItem = get_sub_field('editorial_style_item');
$azDef = get_sub_field('editorial_style_definition');
echo '<p><b>'.$azItem.':</b></p>'.$azDef.'<hr>';
endwhile;
endif;
endwhile;
endif;
wp_reset_postdata();
echo '</div>';
echo '</div>';
echo '</article>';
}
The Post Archive Shortcode
Converting the above code to a shortcode was similar to converting the single loop. I stripped out all structural html
and converted my echo
s to return
s. In this code, the returned variable is $finalloop
and the resulting shortcode is [style-archive]
.
I didn't need to include any of the get_the_content()
stuff from the original loop, as we will be able to use the Block Editor for that.
add_shortcode( 'style-archive','bb_a_z_styles_archive_loop' );
function bb_a_z_styles_archive_loop() {
$finalloop = '';
// Call Post
$args = array (
'post_type' => 'a_z_style_guide',
'orderby' => 'title',
'order' => 'ASC',
'posts_per_page' => -1,
);
$azDir = new \WP_Query( $args );
if ($azDir->have_posts()) :
while ($azDir->have_posts()) :
$azDir->the_post();
$azTitle = get_the_title();
$finalloop .= '<h2>'.$azTitle.'</h2>';
if( have_rows('style_definitions') ):
while( have_rows('style_definitions') ):
the_row();
// vars
$azItem = get_sub_field('editorial_style_item');
$azDef = get_sub_field('editorial_style_definition');
$finalloop .= '<p><b>'.$azItem.':</b></p>'.$azDef.'<hr>';
endwhile;
endif;
endwhile;
endif;
return $finalloop;
wp_reset_postdata();
}
Incorporating into a Block Theme
Now that I've converted my loop functions to shortcodes, I need to incorporate them into the new site and theme. Remember, in order to do so we need a way to add "custom functionality" via the shortcodes to the new site. So a plugin would be necessary.
Single posts
As mentioned above, the post titles of this CPT are simply letters of the alphabet; there are 26 posts, A through Z. As also mentioned, WordPress Core provides a Shortcode Block (shortcodes also work in the Paragraph Block).
One approach would be to put the Shortcode Block containing the [style-definition]
shortcode at the top of all 26 posts. This would work, but there is too much repetition (it is not very DRY).
The approach I'm taking on single posts is to use the WordPress Full Site Editor to create a new "Single Posts" template for my CPT.
The default single template has a Content Block that displays all content from a post or page's block editor. Because ACF data is not stored in post_content
, the Content Block will not work for displaying our ACF Data. So, in my CPT single post template I replaced the Content Block with the Shortcode Block and dropped my [style-definition]
shortcode into it.
Default Single Post/Page template with Content Block
CPT Single Post template with Shortcode Block and [style-definitions]
shortcode
Removing the Content Block from the Template will not remove the Content Editor from the single-post editor. As the first image in this post illustrates, the Content Editor will appear above the ACF Field Group.
To minimize confusion, it might be advisable to disable the Content Editor altogether for the Style Guide CPT.
A function such as the following will disable the editor altogether in the CPT, leaving only the ACF Field Group
add_action( 'init', 'disable_editor_style_guide', 99);
function disable_editor_style_guide() {
remove_post_type_support( 'a_z_style_guide', 'editor' );
}
Editor completely disabled on Style Guide CPT
Now, with my CPT Single Template configured, my ACF content shows on the front-end of each post and I can edit the entries in the ACF Field Group area of the editor:
Front-end Style Guide entry with ACF content rendered by shortcode
Archive "template"
The next task is to create an "archive" that displays all content from every post of the CPT on a single page.
When setting up a CPT in ACF, one of the options in the Advanced Settings allows one to create an Archive for the CPT that can be controlled via an archive template in the theme Full Site Editor.
ACF Archive option in CPT
I am not using the archive template because our "archive" page also needs to have additional precursor content added to it via the Block Editor that appears above the Style Guide content.
While I'm not using the archive template, I have this option turned on for navigational purposes. As seen in the image above, you have the option of defining an archive slug when creating a CPT in ACF (otherwise it inherits the slug of the CPT itself and you can also change its label). This slug will show up in the permalink.
CPT Single Permalink
With this option selected, its slug or its label (if defined) will also show up in the breadcrumbs for the single CPT post.
Breadcrumbs showing /a-z-style-guide/
CPT archive with Editorial Style Guide
label
If the Archive option is not selected, its slug will not appear in its breadcrumb (it'll still appear in the permalink).
Breadcrumbs of same without CPT Archive option
So, rather than creating a custom archive template for our Style Guide "archive" content, I created a new Page and named it accordingly. I used a redirection plugin to redirect the slug archive /a-z-style-guide/
to the page slug /editorial-style-guide/
. This way, when a user clicks "Editorial Style Guide" (the CPT slug's label) in the breadcrumbs, it redirects to the page called "Editorial Style Guide," which is our "archive" page.
I then added the precursor content to our page via the Content Editor and then placed the Shortcode Block below it with my [style-archive]
shortcode in it.
Style Guide Content Editor with precursor content and Shortcode block
Style Guide Page front-end with precursor content above and Style Guide content below
Using this approach, I was able to get the exact same functionality as I had on the original site I am migrating from.
Conclusion
As I mention at the top of this post, the development community has not come up with a way to bring legacy metadata content into the Block Editor paradigm. If I were starting "from scratch" on this site, I would have developed a custom ACF Block or perhaps explored the Meta Field Block Plugin more extensively. For now, though, developing a shortcode as described in this post seems to be the best way to achieve our desired functionality in the immediate term.