The WordPress Loop: Outputting Post Types and Categories

Photo by Stanislav Kondratiev, Pexels

Simple working examples

Playing with simple working examples is my favorite way of figuring out more advanced functionality. Hope these are useful!

The Loop is WordPress

WordPress is a secure, user-friendly “free and open source content management system” (Wikipedia) that stores and retrieves posts (pieces of content, various types). So, by retrieving posts, “The Loop” essentially powers what WordPress “does.”

The following default and custom queries both look for template tag information (data from posts) from the database, stored in the global $post variable, codex, global variables, WP_Post class.

Terms

The Loop (default query), WP developer docs — the PHP that outputs both regular and custom posts onto their default pages, whether single or archive (a collection of posts). The “posts page,” assigned in the Reading Settings, is the archive that displays regular WordPress posts.

The single and archive pages both use index.php, archive.php or single.php, which have almost identical content, the default query loop.

And, within The Loop, WP_Query (custom query), WP developer docs — The WP_Query class lets you query the database directly to output posts that aren’t default to that page. So, for instance, you can output regular posts outside the posts page by assigning categories to the posts, and then retrieving posts by those categories. Essentially, WP_Query lets you retrieve posts by type and category.

The WP_Query class object provides safety and simplicity (protects against SQL injection attacks, ensures proper data types, just “create an arguments array and instantiate the class!” Smashing Magazine) but use the default loop whenever possible, instead, to prevent WP_Query from eating RAM.

Template tags, WP Codex Tag Templates — PHP functions that operate within the loop, and are used to retrieve post data like the post title, URL or author.
Example: <h2><?php get_title(); ?></h2>

the_post() function — “Sets up the global variable with the current post, so that the_title() or the_content() use the title and content from the current post in the query as you loop through it,” Peattie, WP docs

get_posts(), WP Developer docs — a WP_Query class method that can also retrieve posts. It automatically resets The Loop after each call to the database, and outputs posts using a foreach method. More on the differences between get_posts() and WP_Query from Giorgos Sarigiannidis, who prefers get_posts() for simple tasks.
Avoid query_posts(), though, because it changes the variables of the global.

Example of get_posts() from Hubspot

<?php

  $count = 1;
  $sample_array = get_posts();

  foreach( $sample_array as $post )

  {
    echo "<h3> ${count}. " . $post->post_title . "</h3>";

    ++$count;
  }

?>

You can also directly access the post object, but it’s better to use template tags within The Loop (ie, the previous examples). “Only access the $post global directly if you’re unable to access the information you want through an existing function,” NateWR.

That being said, here’s an example of retrieving a post ID from the global $post

<?php global $post;
echo $post->ID; ?>
My sample page

(Nothing fancy)

The default loop

A basic loop (below), returns a regular post type to its default pages

<?php
	if ( have_posts() ) :
		while ( have_posts() ) : the_post(); ?>

			<a href="<?php get_permalink() ?>">
				<h2><?php get_title() ?></h2>
			</a>

		<?php endwhile;

	else :
		_e( 'Sorry, no matches' );

	endif;
?>

Output category and taxonomy names

These names link to the archive of all the posts, regular and custom, in that category or taxonomy. Essentially, it’s a clickable “menu” output by the default loop (get_categories(), WP docs).

<div class="container">

    <h3>Return category names</h3>

    <?php $args = array(
        'taxonomy' => 'category',
        // for a custom taxonomy, instead use the taxonomy's name: 
        // 'taxonomy' => 'subjects',
        'orderby' => 'name',
        'order'   => 'ASC',
        // to return both used and unused categories
        'hide_empty' => false
        );

        $cats = get_categories( $args );

        foreach( $cats as $cat ) { ?>
                        
          <a href="<?php echo get_category_link( $cat );?>">
          // for a custom taxonomy, instead use: 
          // <a href="<?php echo get_term_link( $cat );?>">
            <?php echo $cat->name; ?><br>
          </a>
    
    <?php } ?>

</div>

Helpful post: SO: how to list all category from custom post type?

Output a specific post’s categories

Linked title of the archive page for each category. You might want to add this after the post content on single.php.

<?php
	foreach((get_the_category()) as $cat){ ?>
        
        // to output custom taxonomies ("subjects" is the taxonomy name)
        // foreach (get_the_terms(get_the_ID(), 'subjects') as $cat) { ?>
	
        <a href="<?php echo get_category_link( $cat->term_id );?>">
        // <a href="<?php echo get_term_link( $cat->term_id ) ?>">
		
            <?php echo $cat->name; ?><br>
	</a>
<?php } ?>

Helpful posts: SO, List categories of a custom post type, SO: Correct use of get the terms

WP_Query

<?php
	/* the query */
	$query = new WP_Query( $args );

	if ( $the_query->have_posts() ) :

	/* the loop */
	while ( $the_query->have_posts() ) : $the_query->the_post(); ?>
		<h2><?php the_title(); ?></h2>
	<?php endwhile;
	/* end of the loop */

/* if the query uses the_post(), this reset lets template tags use the main query’s current post again */
	wp_reset_postdata();

	else :
	_e( 'Sorry, no posts match.' );
	endif; 
?>

WP_Query uses the loop as a method of the object:
$category_posts->have_posts()
vs. the default loop:
have_posts()

Restore the $post global to its former state with wp_reset_postdata(); otherwise, the $post global will be “stuck on your custom query.” NateWR, SO: “When to use global $post and other global variables”

<?php
        endwhile; 
        wp_reset_postdata(); 
        else: 
        endif;
    ?>

Custom Posts

Whether or not you use custom posts depends on how your site is organized. They can be a logical way to sort topics, though. If you create “apple” and “orange” categories for posts about fruit and want to get more specific, it might be simpler to register an “apple” custom post type, and assign it “red” and “golden” categories.

Helpful post: When Do You Need a Custom Post Type, WP Beginner

Register a new custom post type

functions.php
A sample of arguments for a simple, working example. (FYI, if you’re new at this, it’s safest to add code to the bottom of functions.php.)

/* custom post type */

/* Allows your theme to use post thumbnails (images) */
add_theme_support('post-thumbnails');

/* Which post type gets this feature? Also, which feature? */
add_post_type_support( 'Articles', 'thumbnail' );

function create_posttype() {
register_post_type( 'articles',
array(
  // How the labels will appear to a user (here, capitalized)
  'labels' => array(
   'name' => __( 'Articles' ),
   'singular_name' => __( 'Article' )
  ),
  'public' => true,
  // Allows these posts to show on an archive page
  'has_archive' => true,
  'rewrite' => array('slug' => 'articles'),
  // Makes the posts available to the REST API and the block editor
  'show_in_rest' => true,
  // Allows the posts to be in both custom and regular categories
  'taxonomies'  => array( 'subjects', 'category' ),
  // Allows you to edit the title, content or add a featured image in the WP backend 
  'supports' => array('title', 'editor', 'thumbnail'),
 )
);
}
add_action( 'init', 'create_posttype' );

The custom post option should now automatically show up here:

Flush permalinks

If everything isn’t displaying at this point, you may need to flush permalinks.
Go to settings > permalinks
You don’t need to make any actual changes, just click “save changes”

Custom post archive and single pages

Create a page in the backend called “Articles” (use the name of your own custom post type) and WordPress will output the custom posts using archives.php (to display all posts) and single.php (single posts).

To customize the output, create an archive-articles.php (paste in a copy of archives.php) and single-articles.php (single.php), and edit them. You can place them directly in the child theme folder.

Output custom post types

<div class="container">
    <h3>Returning custom posts</h3>

    <?php $args = array(
        'post_type'=> 'articles',
        'order'    => 'ASC',
        'posts_per_page' => -1,
    );              
    
    $query = new WP_Query( $args );
        if($query->have_posts() ) :                     
        while ( $query->have_posts() ) :
        $query->the_post(); ?>

    <div class="row">
        <a href="<?php the_permalink(); ?>">
            <h5><?php the_title(); ?></h5>
        </a>

        <?php the_excerpt(); ?>
    </div>

     <?php
        endwhile; 
        wp_reset_postdata(); 
        else: 
        endif;
    ?>

</div>

Custom Taxonomies

Register a new custom taxonomy

“Subjects” is the name of my sample custom taxonomy.

functions.php

/* custom taxonomy */
add_action( 'init', 'create_subjects_hierarchical_taxonomy', 0 );
// highlighted part should be taxonomy name
function create_subjects_hierarchical_taxonomy() {
 
  $labels = array(
    'name' => _x( 'Subjects', 'taxonomy general name' ),
    'singular_name' => _x( 'Subject', 'taxonomy singular name' ),
    'search_items' =>  __( 'Search Subjects' ),
    'all_items' => __( 'All Subjects' ),
    'parent_item' => __( 'Parent Subject' ),
    'parent_item_colon' => __( 'Parent Subject:' ),
    'edit_item' => __( 'Edit Subject' ), 
    'update_item' => __( 'Update Subject' ),
    'add_new_item' => __( 'Add New Subject' ),
    'new_item_name' => __( 'New Subject Name' ),
    'menu_name' => __( 'Subjects' ),
  );    

  // can be assigned to both regular posts and the custom post type, "articles"
  register_taxonomy('subjects', array( 'post', 'articles' ), array(
    'hierarchical' => true,
    'labels' => $labels,
    'show_ui' => true,
    'show_in_rest' => true,
    'show_admin_column' => true,
    'query_var' => true,
    'rewrite' => array( 'slug' => 'subject' ),
  ));
}

Helpful posts: How to Create Custom Taxonomies in WordPress, WP Beginner,
WP Beginner custom taxonomies, Smashing Magazine custom taxonomies,
Add custom taxonomy to custom post type: WP Beginner, How to Create Custom Taxonomies,

Customize a taxonomy or category’s archive page

Regular category — To customize the page that shows all the posts in a specific, regular category, create a category.php file, and paste in the contents of archive.php. Then, edit it and place it in the child theme folder.

Custom taxonomy — To customize the page that shows all the posts in a specific custom taxonomy, create a taxonomy-subjects.php, edit it, and place it in the child theme folder.

Output posts from one or more custom taxonomies

<div>
<?php $args = array(
    'post_type'=> array('post', 'articles'),
    'order' => 'ASC',
    'posts_per_page' => -1,
    'tax_query' => array(
        'relation' => 'AND',
        array(
            'taxonomy' => 'subjects',
            'field'    => 'slug',
            'terms'    => 'math',
        ),
        array(
            'taxonomy' => 'subjects',
            'field'    => 'slug',
            'terms'    => 'english',
        ),
    ),
);

 $query = new WP_Query( $args );
        if($query->have_posts() ) :                     
        while ( $query->have_posts() ) :
        $query->the_post(); ?>

    <div class="row">
        <a href="<?php the_permalink(); ?>">
            <h5><?php the_title(); ?></h5>
        </a>

        <?php the_excerpt(); ?>
    </div>

     <?php
        endwhile; 
        wp_reset_postdata(); 
        else: 
        endif;
    ?>
</div> 

Output posts from one or more regular categories

<div class="container">
    <h3>Return posts by regular category</h3>

    <?php $args = array(
        // to return both regular and custom posts
        'post_type'=> array('post', 'articles'),
        // example of returning all posts from two categories:
        'category_name' => 'cheetahs, dolphins',
        'order'    => 'ASC',
        'posts_per_page' => -1,
    );              
    
    $query = new WP_Query( $args );
        if($query->have_posts() ) :                     
        while ( $query->have_posts() ) :
        $query->the_post(); ?>

    <div class="row">
        <a href="<?php the_permalink(); ?>">
            <h5><?php the_title(); ?></h5>
        </a>

        <?php the_excerpt(); ?>
    </div>

     <?php
        endwhile; 
        wp_reset_postdata(); 
        else: 
        endif;
    ?>

</div>

Feedback welcome! Find me on Twitter @SaraHarvy

wp-cli.phar, “no such file or directory”

I followed the instructions for installing WP-CLI globally on my Mac, then tried Homebrew, but couldn’t manage to connect with the wp-cli.phar file.
So, this may not be ideal, but I downloaded the phar file from WP guidebook’s for WP-CLI (click the hyperlink on that page, screenshot below), and manually created a “wp” folder in the user (which is my name, also called “home”). Then, I manually dragged the phar file into the wp folder.

wp --info

resulted in expected behavior

Comments welcome on Twitter at @saraharvy

Simple: style Grade.Us stream

For example, to remove the “see more reviews” text below the stream

Sign in to your account
Select your own site or a client’s from the dropdown menu

Click funnel > setup, and scroll down to the custom css field

Click the blue “update custom CSS” button directly below the field, and your change should be live

It’s so simple, I almost cried (when I couldn’t figure it out for almost an hour). You can’t just change the CSS on your site. You need to sign in and customize through “the funnel.” (Or edit the embed code? Grade.us help docs).

Hope this was helpful, @SaraHarvy

Remove “see more reviews” from Grade.Us stream

We wanted to remove this line (and replace it with a button):

Sign in to Grade.us
Access the right account for the site. (If you’re inheriting this task, clients are likely to have their own accounts.)

Go to setup > custom CSS
And this code worked to remove, “See more reviews”

#fullstream {
   display:none;
}

Helpful posts

Grade.Us help docs, how do I use custom CSS,
Styling external widget,
How do I customize the Review Stream embeddable,

On Twitter, @saraharvy

Return an ACF image subfield with an array or URL format

Two possible ways of doing it
Documentation for Advanced Custom Fields images

You’d be really upset if your crostini pic didn’t appear
<div class="col-md-9">

    <?php $signature_facial = get_field('signature_facial');
       if( $signature_facial ): 
        $image = $signature_facial['image'];
       ?>

    <!-- title -->
    <div class="title mb-4">
        <h3><?php echo $signature_facial['title']; ?></h3>
    </div>

    <div class="content mb-4">
        <div class="row">

            <!-- image -->           
            <div class="col-md-3">
                <img src="<?php echo $image['url'] ?>" alt="Signature Facials" class="photograph shadow-sm" />
            </div>

            <?php endif; ?>

Iterating down one level

<?php if( have_rows('columns') ): ?>
    <?php while( have_rows('columns') ): the_row(); ?>

    <?php if( have_rows('column') ): ?>
        <?php while( have_rows('column') ): the_row(); 
            $title = get_sub_field('title');
            $image = get_sub_field('image');
            $description = get_sub_field('description');
            $link = get_sub_field('link');
        ?>

    <div class="col-md-4">
        <p><img src="<?php echo $image['url']; ?>" alt="<?= $title; ?>" class="img-responsive" style="border: 1px solid #9b778e;"></p>

        <h3><?= $title; ?></h3>
        <ul class="square-bullets"><?= $description; ?></ul>
    </div>

    <?php endwhile; ?>
    <?php endif; ?>

    <?php endwhile; ?>
    <?php endif; ?>
</div>

Hope this was an inspiration! If you have comments or questions, DM me on Twitter @SaraHarvy

Click an ACF Pro gallery image to display larger on Bootstrap carousel

First, I returned an ACF gallery field below my Bootstrap carousel (the line of four images). Then, I linked the images so that a user could click each for the carousel to display it full sized.

<section class="row">

    /* ACF Pro */
    <?php
    $images = get_field('gallery'); 
    $count=0;

    foreach($images as $image):?>

        /* return gallery images into four columns */
        <div class="col-md-3">
            
            /* link each to the Bootstrap carousel */
            <a data-bs-target="#carouselExampleIndicators" 
            data-bs-slide-to="<?php echo $count; ?>" href="#">

                /* display the selected $image as medium size */
                <img src="<?php echo $image['sizes']['medium'];?>">

            </a>

        </div>
    <?php 

    $count++; 
    endforeach;?>

</section>

Helpful posts

How to jump to a specific item in the Bootstrap carousel, SO,
For each loop, php, add one, SO,
Bootstrap 5 help docs, carousel,
ACF Pro gallery field,

Hope it’s helpful! @SaraHarvy

Switching image size ACF Gallery for small screens

A simple solution, but I’ll write it down. Only using one size, large, kept the images small in a row and large on a small screen, but I lost the thumbnail’s square, cropped aspect.

Desktop

Mobile

<div class="row">
    <?php $images = get_field('gallery'); ?>
    <?php if($images) : ?>

    <?php foreach($images as $image):?>
        <div class="col-md-3 img-fluid text-center gallery-padding">
            <a href="<?php the_permalink(); ?>">
                <img class="gallery-thumbnail" src="<?php echo $image['sizes']['thumbnail'];?>">
                <img class="gallery-large" src="<?php echo $image['sizes']['large'];?>">
            </a>
        </div>
    <?php endforeach;
    endif;?>

</div>

SCSS

   .gallery-large {
    display: none;
}

@media only screen and (max-width: 767px) {
   .gallery-thumbnail {
        display: none;
    }
    .gallery-large {
        display: flex;
    }
}

With srcset, nothing broke, but the images all sized large, losing the cropped, thumbnail aspect. It might be a simple mistake, but I’ll explore it more later.

    <?php foreach($images as $image):?>
        <div class="col-md-3 img-fluid text-center gallery-padding">
            <a href="<?php the_permalink(); ?>">
               <img srcset="
                  <?php echo $image['sizes']['thumbnail'];?> 200w,
                  <?php echo $image['sizes']['large']; ?> 600w,">
            </a>
        </div>
    <?php endforeach; ?>

Helpful posts

ACF support discussion on ACF Gallery image sizes:

John Huebner, April 28, 2017 at 8:06 am
Preview size is only available for the image field. The main reason that this is not available for the gallery has to do with the way the gallery is shown in the admin, the fact that it’s responsive and the drag/drop sorting feature. There’s quite a bit of CSS and JavaScript that depend on the images being a predictable size and square. I’m guessing that it would be a large amount of work to allow for multiple sizes.

The image that ACF uses is actually whatever you site has set for the “medium” size. The container for each image in the gallery is set by ACF with parts of the image being hidden.

WordPress 403 error migrating to Plesk with Gravity Forms

I launched a WordPress site on Plesk, and got a 403 error message from Plesk’s firewall, ModSecurity. The error logs mentioned Gravity Forms, with a line and an id. For example, in my case:

[line "20"] [id "329801"]

Plesk error log

If you’re new to Plesk, you can find the error logs by clicking domains (left sidebar) > the domain name > file manager > logs > error_log

I Googled “Plesk and Gravity Forms,” and found a helpful Gravity Forms support post. A user fixed a similar 403 error just by whitelisting the ids (from the error logs). I’m guessing ModSecurity didn’t like an iFrame, in their case.

I thought I could probably whitelist the problem id in the WordPress htaccess file, and Googled my way to “Can I disable ModSecurity rule in htaccess?” in Apache Stack Exchange. I didn’t try htaccess, though, because the post also mentioned a way to whitelist ids through the Plesk UI.

Disable individual ModSecurity rules in Plesk UI

Domains > the domain name > web application firewall > and there’s a field for security IDs

Helpful posts

A website hosted in Plesk fails to load when ModSecurity is enabled: Access denied with code 403, Plesk help docs
Disable modsecurity For a Specific Directory, Apache Stack Exchange

Not a chatty post, but if you want mirth and commentary, check out my Twitter @SaraHarvy. I just figure that, right now, you’re trying to fix a website.

New password for Local WP site

The password reset email may go to tools > Mailhog
Open and check there if you don’t get a password reset link

Otherwise, you should be able to regain access to a Local WP site by creating a new user via the site shell.

Right click the site name in Local’s left sidebar to open the menu > open site shell
In the terminal window, enter:
wp user create YOURNAME1 youraddress@example.com
role=administrator
user_pass=123456

Helpful posts

Local help docs,
Also good help doc post

CSS Cascade Layer Basics

As of Spring 2022, Google’s Chrome 99 stable release supports cascading css layers. Get familiar, for when you spot them (or use them) in the wild.

A simple example, based on simple examples found on the Chrome blog, and CSS-Tricks

CSS is fairly easy for most developers to learn, but it can be unforgiving to those who lack proper structural plans for their code. This is primarily due to the cascade’s ferocious resistance to control.


David Omotayo, LogRocket

Takeaways

Setting the cascade precedence

From CSS-Tricks, A Complete Guide to CSS Cascade Layers,

1. Set layer precedence at the top of a file — the order progresses in reverse, so “layout” would have higher precedence than “base.”

@layer base, components, generic, layout;

2. Or set precedence by the order of the layers themselves — the farther down the page, the higher the precedence). Try it out by reordering the layers of the Chrome blog‘s CodePen, here.

3. Using @layer with import files, Chrome blog section, “Organizing Imports.”

At-rules

From MDM Web Docs, @layer,

@layer “declares a cascade layer”

“With layered styles, the precedence of a layer always beats the specificity of a selector.”

The @layer at-rule “instructs CSS how to behave.”
Other familiar at-rules: @media, @import, @keyframes

Watch out

Chrome Developers blog, “Things to Look Out For.”

From the Chrome blog

Chrome blog, Cascade Layers are Coming to Your Browser, and New in Chrome 99,