Programming tricks used on this site

This is (or, will be) a meta-post listing all posts I’ve written about the techniques used to create this site.

    • WordPress Custom “Hero Header” Modifications (427 words) This site uses the "Sinatra" free Wordpress theme as its base. Sinatra includes a single "Hero Header" the row of 3 animated featured posts on the home page which changes background images as you mouse over post titles within it.

      I wanted this to be multi-row, a grid instead of just one row. The code natively contained an option to include up to 12 featured posts, which could easily be made to wrap around to new rows, but the problem was, the background of the entire section containing all rows changed when you moused over a post, not just the single row, and I wanted not just a row-by-row background, but I wanted potentially unlimited rows.

      I moved the code that generates the rows into a function, and then called it repeatedly, once for each row I want. At some point I may make it automatically add as many rows as are required to display all posts, but as of this writing, every time I add one more post than there is room for, I edit the bottom of this file to add another row.

      Furthermore, originally, the theme CSS was really sloppy:
      1.) There's a lot of identifying things by element, such as anchor or div tags, which the CSS then had to look up the ancestry of to see which formatting applied (as in rules similar to, "#page .hero-row .hero-slider
      a {blah-blah:blah}", which is horribly inefficient compared to just giving things a class.
      2.) All the changing images were done using CSS background images on <div> elements, making them hard to lazy-load and killing page performance.
      3.) Plus there were a lot of animations on non-compositor-only attributes, making Google PageSpeed complain.So a lot of CSS got rewritten. I don't mind telling you: it took a while. There are still a lot of inefficiencies, like divs directly nested as the only child of other divs, that can be cleaned up. Unfortunately that's going to take a while as a lot of things are done with flexboxes, which makes refactoring all the formatting a lot tougher. So for right now I'm leaving that alone, but it's on my "future plans" list.

      I'm proud to say that as of this writing (July 2, 2024) my great, big, overcomplicated (over 5000 elements) animated home page gets a 99 out of 1000 performance score on Google PageSpeed. See for yourself: Pagespeed Report

      Here, then, is my current updated version of Sinatra theme's /template-parts/hero/hero-hover-slider.php. This is dynamically read from the actual site files, this is what's currently in live use on the front page:
      <?php
      /**
       * The template for displaying Hero Hover Slider.
       *
       * @package     Sinatra
       * @author      Sinatra Team <hello@sinatrawp.com>
       * @since       1.0.0
       */


          

      $postsperrow 3;

      $sinatra_hero_categories = !empty($sinatra_hero_categories)
          ? 
      implode(", "$sinatra_hero_categories)
          : 
      "";

      // Setup Hero posts.

      //function herorow($postsperpage,$offset,$order,$sinatra_hero_categories) {return '<h2>hi '.$offset.'</h2>';}


      function count_total_post_media$id ) {
          

      $data getFunctionTransient(__FUNCTION__,    $id); if ( $data != null) {return $data;}
          
              
      // from https://wordpress.stackexchange.com/questions/246055/count-total-number-of-images-in-post-and-echo-results-as-number
              
      $array get_post_galleries$idfalse );
              
      $key 0;
          
      $theOut=array();
            
      //  $src = 0;
              
      while ( $key count$array ) ){
                
      //  $src += count( $array[$key]['src'] );
                  
      $theOutarray_merge($theOut,explode(',',$array[$key]['ids']));
                  
      $key++;
              }
      $newarray=array_column(get_children($id),'ID'); //get_children returns an array of obects, which has to be handled differently than ordinary arrays, because otherwise it wouldn't be PHP

      /*DEBUGGING if(is_user_logged_in()) {echo "<!-- ".print_r($newarray)." -->";echo "<!-- ".get_post_thumbnail_id($id, "full")." -->"; }  */
             
      $keynew 0;
            
      //  $srcnew = 0;
             
      $theOutnew=array();
              while ( 
      $keynew count$newarray ) ){
                
      //  $src += count( $array[$keynew]['src'] );
                  
      $theOutnew[]= $newarray[$keynew];
                  
      $keynew++;
              }


      $theFinalArray=array_unique(array_merge($theOut,$theOutnew));
      if(
      has_post_thumbnail$id )) {

      if ((
      $key array_search(get_post_thumbnail_id($id) , $theFinalArray)) !== false) { unset($theFinalArray[$key]); } /* else { echo "<!-- ".array_search(get_the_post_thumbnail_url($id, "full") , $theFinalArray)." -->";} */

      }
          
      /* NOTE: Tested, this already doesn't detect LJ-divider because it's added by a shortcode */
      $theFinalCount=count($theFinalArray); // was "$theFinalCount=count($theFinalArray)-$hasThumb;" at the end, but this appears to be leftover from something removed, $hasthumb isn't defined anywhere
      //    if(is_user_logged_in()) {echo "<!-- ".print_r($theOut)." -->"; }
            //  return intval( $src );
            //  
           
      setFunctionTransient(__FUNCTION__$theFinalCount ,$id);
          return 
      intval$theFinalCount );
          }



      function 
      herorow($postsperpage$offset$order$sinatra_hero_categories,$theHeroTag)
      {
          
      $sinatra_args = [
              
      "post_type" => "post",
              
      "post_status" => "publish",
              
      "order" => $order//'ASC',
               
      'orderby' => 'date',//'post_modified',
              
      "posts_per_page" => $postsperpage//3 //sinatra_option( 'hero_hover_slider_post_number' ), // phpcs:ignore WordPress.WP.PostsPerPage.posts_per_page_posts_per_page
              
      "offset" => $offset,
              
      "ignore_sticky_posts" => true,
              
      'tag' => $theHeroTag,
              
      "tax_query" => [
                  
      // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
                  
      [
                      
      "taxonomy" => "post_format",
                      
      "field" => "slug",
                      
      "terms" => ["post-format-quote"],
                      
      "operator" => "NOT IN",
                  ],
              ],
          ];

          
      $sinatra_hero_categories sinatra_option("hero_hover_slider_category");

      $atts=array($postsperpage$offset$order$sinatra_hero_categories,$theHeroTag );
      $data getFunctionTransient(__FUNCTION__,    $atts); if ( $data != null) {return /*"<!-- transient ".__FUNCTION__." code for atts ". json_encode( $atts ) ." -->".*/ $data;}

          if (!empty(
      $sinatra_hero_categories)) {
              
      $sinatra_args["category_name"] = implode(
                  
      ", ",
                  
      $sinatra_hero_categories
              
      );
          }
          
          
      $sinatra_args apply_filters(
              
      "sinatra_hero_hover_slider_query_args",
              
      $sinatra_args
          
      );
          
          
          
      $sinatra_posts = new WP_Query($sinatra_args);

          
      // No posts found.
          
      if (!$sinatra_posts->have_posts()) {
              return 
      false;
          }

          
      $sinatra_hero_bgs_html "";
          
      $sinatra_hero_items_html "";

          
      $sinatra_hero_elements = (array) sinatra_option(
              
      "hero_hover_slider_elements"
          
      );
          
      $sinatra_hero_readmore =
              isset(
      $sinatra_hero_elements["read_more"]) &&
              
      $sinatra_hero_elements["read_more"]
                  ? 
      " si-hero-readmore"
                  
      "";

          while (
      $sinatra_posts->have_posts()):
              
      $sinatra_posts->the_post();
      $this_image_id get_post_thumbnail_id();

      $this_image_alt get_post_meta($this_image_id'_wp_attachment_image_alt'TRUE);

      $this_image_title get_the_title($this_image_id);
          if (
      get_the_title()) {$thistitle get_the_title(); } 
          
          
      $pre_alt_tag $this_image_alt?($this_image_alt.", feature article by Mike Kupietz"):($thistitle?($thistitle.", feature art by Mike Kupietz"):"feature art by Mike Kupietz");
          
      $this_alt_tag= (strpos($pre_alt_tag': ') !== false) ? substr($pre_alt_tagstrpos($pre_alt_tag': ') + 1) : $pre_alt_tag;

          
      $thumb wp_get_attachment_image_srcget_post_thumbnail_id(get_the_ID()), 'full' );
          
      $url $thumb['0'];
          
      $width $thumb['1'];
          
      $height $thumb['2'];
          
      $midthumb wp_get_attachment_image_srcget_post_thumbnail_id(get_the_ID()), 'medium' );
          
      $midurl $midthumb['0'];
          
      $midwidth $midthumb['1'];
          
      $midheight $midthumb['2'];

              
      // Background images HTML markup.
              
      $sinatra_hero_bgs_html =
                  
                  
                
      // 1px included below to prompt the lazy loader to fetch the images. Silly, I know. For a while I used full size images with just 0.001 opacity but sometimes it ignored the opacity. 
                 
                  
      '<div class="hover-slide-bg lazyload">'  '<img alt="'.htmlspecialchars($this_alt_tag).'" class="mk-hero-row-bg" src="'
                   
      /*get_the_post_thumbnail_url(get_the_ID(), "full")*/$url .'" width="'.$width.'" height="'.$height.'" />' '</div>' .
                  
      $sinatra_hero_bgs_html//reversing the order. Must be reversed below too.

              // Post items HTML markup.
              //    ob_start();
      $postImgUrl=$midurl;//get_the_post_thumbnail_url(get_the_ID(), "medium");
              
      ob_start();
              echo 
      '<div class="col-xs-' .
                  
      intdiv(12$postsperpage) .
                  
      " xmkwrapper hover-slider-item-wrapper h-entry";
              echo 
      esc_attr($sinatra_hero_readmore);
              echo 
      '" itemscope itemtype="https://schema.org/Article"><link itemprop="image" href="'.$postImgUrl.'"><img class="mk-hero-row-bg-mobile" alt="'.$this_alt_tag.'" src="'
                   
      $postImgUrl .'" width="'.$midwidth.'" height="'.$midheight.'" />
                   <section style="display: none;" class="p-author h-card vcard">
            
                  <span class="p-name fn">
                      Michael Kupietz | ARTS+CODE - Featured articles
                  </span>
          
          </section>'
      .
       
      /* u-card was originally:
       <section style="display: none;" class="p-author h-card vcard">
            
                  <span class="p-name fn">
                      <span class="p-given-name given-name">Michael</span> <span class="p-family-name family-name">Kupietz</span> | ARTS+CODE - Featured articles
                  </span>
                  <a href="https://michaelkupietz.com/" class="u-url url">michaelkupietz.com</a>
                  <ul class="p-org h-card org">
                          <li>
                              <a href="https://michaelkupietz.com/" class="p-name u-url p-organization-name organization-name">Michael Kupietz | ARTS+CODE</a>
                          </li>
                           <!-- li>
                              <img src="/image/logo/kupietzlogo-256x256.png" alt="Logotype for michaelkupietz.com" class="u-logo logo">
                           </li -->
                      </ul> 
          </section>
      */ 


              
      '<div class="hover-slide-item xmkparent-div">
                  <div class="slide-inner xmkpost-div">'
      ;

              if ( 
                  isset(
      $sinatra_hero_elements["category"]) &&
                  
      $sinatra_hero_elements["category"]
              ) {
                  
      //echo '<div class="post-category">';
                  
      sinatra_entry_meta_category(" "true);
                  echo 
      " ";
                  
      sinatra_entry_meta_genre(" "false,false,0); 
                
      //  echo "</div>";
              
      }
              if (
      get_the_title()) {
                  echo 
      '<h2 class="hero_h3">';

                  
      $thepre get_post_meta(get_the_ID(), "hero_pre"true);
                  if (
      $thepre) {
                      echo 
      '<p class="hero_pre_p">' .
                          
      $thepre .
                          
      "</p>";
                  }
                  
      $theRel =   get_post_meta(get_the_ID(), 'rel'true );
                  
      $theRel .= ($theRel?' ':'').'bookmark'/* need bookmark for h-feed */
                  
      echo '<a class="hero_h3_a u-url" title="'.addslashes($thistitle).'" itemprop="headline" '.  (($theRel)?(' rel="'.$theRel.'" '):'') . 'href="';
                  echo 
      esc_url(sinatra_entry_get_permalink());
                  echo 
      '">';

                  
      /* was        
      the_title();
      ...which includes echo() in executing. */

                  //moving this up to use in alt tags $thistitle = get_the_title();
                  
      $thiscategory get_the_category(get_the_ID());

                  
      $thiscategoryterm = ($thiscategory[0]->name)=='feature'?($thiscategory[1]->term_id):($thiscategory[0]->term_id);
              
      /*    BugFu::log("title ",false);
                  BugFu::log($thistitle,false);
                  BugFu::log("thiscat ",false);
                  BugFu::log($thiscategory,false);
                  BugFu::log("thiscategoryterm ",false);
                  BugFu::log($thiscategoryterm,false); */
                  
                  
                      
      $titlepre get_post_meta(get_the_ID(), "titleprefix"true);
                               
      $colonpos strpos($thistitle":");
                              
                  
              
              
      //I don't think this is ever even used anymore. Check. 

                  
      if ($titlepre && !$thepre) { /* added && !$thepre in 2024aug6 to prevent both wihite italic pre and H6-style titlepre */
                                  
      $thistitle='<span class="titleprefix">' .$titlepre.'</span>'.'<span class="p-name">'.$thistitle.'</span>';
                                  
                              }
                              elseif (
      $colonpos !== false && !$thepre) {  /* added && !$thepre in 2024aug6 to prevent both wihite italic pre and H6-style titlepre */
                   
                               
      $thistitle =  '<span class="titleprefix'.($thepre?' displaynone':'').'">' .
                           
      substr_replace($thistitle":</span>"$colonposstrlen(":"));
                          
                     
      /*     str_replace(":", ":</span>"."<div class=\"" . getClassOfCatOrParentCat(get_the_category(get_the_ID())[0]->term_id)."\"></div>", $thistitle);
      */
          
                  
      } else {$thistitle/* Don't know what this was but as of 2024dec1 it's not defined anywhere $theParentCatClass. */ $thistitle;} 
              
      //    echo "<!-- id ".get_the_ID()."  cat ".print_r(get_the_category(get_the_ID()))." -->";
                   
      echo ($thistitle);
       
      $thepost get_post_meta(get_the_ID(), "hero_post"true);
                  if (
      $thepost) {
                      echo 
      '<p class="hero_pre_p"><i class="hero_h3_i">' .
                          
      $thepost .
                          
      "</i></p>";
                  }
                  
      /* end replacement */

                  
      echo "</a>";
                
                 
      /* WAS HERE   $thepost = get_post_meta(get_the_ID(), "hero_post", true);
                   if ($thepost) {
                      echo '<p class="heropre" style="font-size:.6em;padding:.25em 0;margin:0;"><i class="hero_h3_i">' .
                          $thepost .
                          "</i></p>"; 
                  } */
                  
      echo "</h2>";
              }

              if (
                  isset(
      $sinatra_hero_elements["meta"]) &&
                  
      $sinatra_hero_elements["meta"]
              ) {
                  echo 
      '    <div class="slider-entry-meta entry-meta">
                              <div class="slider-entry-meta-elements entry-meta-elements">'
      ;
      echo 
      '<div class="herosummary p-summary" itemprop="description">'.get_the_excerpt().'</div>';
                  
      //sinatra_entry_meta_author();

                /*  sinatra_entry_meta_date([
                      "show_modified" => false,
                      "published_label" => "",
                  ]); */

                  
      if (function_exists("bac_post_word_count")) {
                      
      // '<span class="wordcount">';
                      
      bac_post_word_count();
                      echo 
      " words";
                      
      //echo "</span>";
                  
      }
                  
      $theMediaCount =/* count(get_attached_media(get_the_ID())) + count(get_children(get_the_ID())) + */   count_total_post_media(get_the_ID()); // - count(get_post_galleries(get_the_ID(),false)) ; //length(get_attached_media()) + 
                  
                  
      if ($theMediaCount) {
      //if(is_user_logged_in()) {echo "<!-- ".var_dump(get_children(get_the_ID())).". -->"; }
                      
                      
      echo '&nbsp;|&nbsp;'//'<span class="mediacount">';
                      
      echo $theMediaCount;
                      echo 
      " media item";
                      echo 
      $theMediaCount == "" "s";
                       
      // echo "</span>";
                      
                    
                  
      }

      //  echo '<span class="updateddate">' ;
            
      echo '&nbsp;|&nbsp;';
                  echo 
      '<time datetime='.get_the_modified_date('Y-m-d',get_the_ID()).' class="dt-published">Updated '.get_the_modified_date('M j, Y',get_the_ID())/*publish date get_the_date('',get_the_ID())*/ /*get_post_modified_date_except(get_the_ID(), '2024-07-31')*/    .'</time>' /* must be <time>, not <span> for indieweb h-feeds */;
                  
      /*echo '</span>'; */
                  
              
                  #semrush says I'm using my name too much echo '   <span itemprop="author" itemscope itemtype="http://schema.org/Person"><span class="hero-author-name author-name" itemprop="name"><a class="url fn n" title="View all posts by Michael Kupietz" href="https://michaelkupietz.com/author/mike-kupietz/" rel="author" itemprop="url">Mike Kupietz</a></span></span>';

                  
      echo '</div>';
                  
                          echo
      '</div>';//<!-- END .entry-meta -->';
              
      }

         
       if (
      $sinatra_hero_readmore) {

                  echo 
      '<a href="';
                  echo 
      esc_url(sinatra_entry_get_permalink());
                  echo 
      '" class="slider-read-more read-more si-btn btn-small btn-outline btn-uppercase" role="button"><span>';
                  echo 
      esc_html_e("Click to Read More...""sinatra");
                  echo 
      "</span></a>";
              }

              echo 
      '</div>' comment('<!-- END .slide-inner -->').
              
      '</div>' comment('<!-- END .hover-slide-item -->').
          
      '</div>' comment('<!-- END .hover-slider-item-wrapper -->');

              
      $sinatra_hero_items_html ob_get_clean() . $sinatra_hero_items_html//reverse the order because I feel like having it that way. Remember to fix the order of the background images too, above, if yu change it back.
          
      endwhile;

          
      // Restore original Post Data.
          
      wp_reset_postdata();

          
      // Hero container.
          
      $sinatra_hero_container sinatra_option("hero_hover_slider_container");
          
      $sinatra_hero_container =
              
      "full-width" === $sinatra_hero_container
                  
      "si-container si-container__wide"
                  
      "si-container";

          
      // Hero overlay.
          
      $sinatra_hero_overlay absint(sinatra_option("hero_hover_slider_overlay"));

          
      $htmloutfinal =
              
      '<div class="si-hover-slider slider-overlay-' .
              
      esc_attr($sinatra_hero_overlay) .
              
      '" style="z-index:-' .
              
      $offset .
              
      '">
          <div class="hover-slider-backgrounds">' 
      .
              
      wp_kses_post($sinatra_hero_bgs_html) .
              
      '</div>' comment('<!-- END .hover-slider-items -->').

          
      '<div class="si-hero-container ' .
              
      esc_attr($sinatra_hero_container) .
              
      '">
              <div class="si-flex-row hover-slider-items">' 
      .
              
      //wp_kses_post
              
      ($sinatra_hero_items_html) .
              
      '</div>' comment('<!-- END .hover-slider-items -->').
          
      '</div>

          <div class="si-spinner visible">
              <div></div>
              <div></div>
          </div>
      </div>' 
      comment('<!-- END .si-hover-slider -->');



      setFunctionTransient(__FUNCTION__$htmloutfinal ,$atts)    ;

      return 
      /*"<!-- orignal code for atts ". json_encode( $atts ) ." -->".*/$htmloutfinal;


      }

      function 
      get_post_modified_date_except($post_id$cutoff_date '2024-07-31') {

          
      /* this was intended to cover for a bunch of records that had the mod date reseet to 7/31/24 by showing the last revision before that instead. But I went ahead and reverted them in the database instead so not using this. */
          
      $post get_post($post_id);
          
      $modified_date get_the_modified_date('Y-m-d'$post_id);

          if (
      $modified_date === $cutoff_date) {
              
      $revisions wp_get_post_revisions($post_id, array(
                  
      'before' => $cutoff_date,
                  
      'limit' => 1,
                  
      'orderby' => 'date',
                  
      'order' => 'DESC'
              
      ));

              if (!empty(
      $revisions)) {
                  
      $last_revision reset($revisions);
                  
      $modified_timestamp strtotime($last_revision->post_modified);
                  
                  
      // Check if the last revision's modified date is also the cutoff date
                  
      if (date('Y-m-d'$modified_timestamp) === $cutoff_date) {
                      
      // If the last revision's modified date is the cutoff date, get the previous revision
                      
      $previous_revision next($revisions);
                      if (
      $previous_revision) {
                          
      $modified_timestamp strtotime($previous_revision->post_modified);
                      }
                  }
                  
                  
      $modified_date date('Y-m-d'$modified_timestamp);
              }
          }

          return 
      date('M j, Y'strtotime($modified_date));
      }

      function 
      add_query_vars_filter$vars ) {
        
      $vars[] = "herotag";
        return 
      $vars;
      }
      add_filter'query_vars''add_query_vars_filter' );
      $theHeroTag =  $_GET['featured'] ?? "";
      echo 
      '<div class="featuredheader">Featured work — filter by: 
      <a '
      .(($theHeroTag=="")?('class="heroactive"'):'').' href="/">All</a>
      <a '
      .(($theHeroTag=="site-info")?('class="heroactive"'):'').' href="/?featured=site-info">Site Info</a>
      <a '
      .(($theHeroTag=="image-gallery")?('class="heroactive"'):'').' href="/?featured=image-gallery">Image Galleries</a>
      <a '
      .(($theHeroTag=="music-sounds")?('class="heroactive"'):'').' href="/?featured=music-sounds">Music & Sounds</a>
      <a '
      .(($theHeroTag=="photojournals")?('class="heroactive"'):'').' href="/?featured=photojournals">Photojournals</a>
      <a '
      .(($theHeroTag=="writing")?('class="heroactive"'):'').' href="/?featured=writing">Writing</a>
      <a '
      .(($theHeroTag=="code-algorithms")?('class="heroactive"'):'').' href="/?featured=code-algorithms">Code & Algorithms</a>
      </div>'
      ;

      $featured_posts_for_count get_posts(array(
        
      'cat' => 51,
        
      'numberposts' => -1,
      ));

      $featured_post_count_for_hero count($featured_posts_for_count) - 1;
      //$featured_post_count = count($featured_posts)-1; //3 posts should still return 0 since we want to add arow if there's 4 posts, not 4 
      $hero_height 1+floor($featured_post_count_for_hero 3);

      for (
      $i 0$i <= $hero_height$i++) {
          echo 
      herorow($postsperrow$postsperrow $i"DESC"$sinatra_hero_categories$theHeroTag);
      }



      ?>

    • Adding A Default Featured Image Or Thumbnail In Wordpress (176 words) On this site, any post without a featured image specifically set (such as this one) displays the default floral 'K' insignia anywhere on the site where a thumbnail is displayed. I looked around the web and none of the solutions I found worked. I eventually figured out that adding the following to my theme's functions.php file did the trick:

      /**
      * Default post thumbnail image.
      *
      */

      function mk_filter_thumbnail_id( $thumbnail_id, $post = null ) {
      if ( ! $thumbnail_id ) {
      $thumbnail_id = 289; //id of default featured image
      }
          return $thumbnail_id;
      }

      if (! is_admin() ) { //don't do this on admin pages, it sometimes overwrites thumbnails in the back-end view of the media library
      add_filter( 'post_thumbnail_id', 'mk_filter_thumbnail_id', 20, 5 );
      }


      By the way, I still haven't grasped what the third and fourth parameters of add_filter() do... I copied them out of a Stack Overflow answer and they worked. Wordpress's whole "filter" functionality is an odd way of calling functions, although I suppose it's a practical if roundabout way of hooking into existing functions to add your own code modifications.
    • WordPress Shortcode & Function Performance Optimization with Transients (1242 words) I suffer from that paradoxical form of laziness peculiar to computer geeks where I will save myself save myself 15 minutes of work on something by spending 4 hours creating a shortcut. As such, the menus on this site are dynamically generated by traversing the category tree in PHP and laying out menus and submenus from categories and subcategories, sparing me the trouble of updating them manually as I add new content. This took some effort.

      I created a shortcode that does this (well, modified a shortcode, originally from the Hierarchical HTML Sitemap plugin, by Alexandra Vovk & WP Puzzle) and soon my site was happily generating dynamic menus on the fly and keeping up with my work as I added pages, edited titles, and rearranged categories.

      And, along the way, getting slower.

      And slower. And slower.

      Finally the other night, due to a confluence of circumstance, it took me over a minute to load the front page. "This," I said, "has got to go."

      Fortunately Wordpress includes something called "transients", which allow you to quickly store and retrieve temporary values. I figured, the shortcode that generates the menus—as well as a number of others, such as the Recent Kwits section that appears in the sidebar of internal site pages—really don't change that often. The menus in particular are very calculation intensive... in addition to traversing the category tree and listing the pages, they calculate the number of words on each page (you did notice that mousing over a menu item shows you how many words the page is, didn't you?) which means not just loading and counting the page, but expanding every shortcode in the page body, to count the words they generate too. It's a lot. It certainly doesn't all need to be recalculated on the fly every single time any page loads. So, I thought, if I could keep track of when the last content update was that actually required them to be updated (such as changes to a page, post, category, or tag), I could otherwise store the value in a transient. Transients are not reliable, they can get wiped out occasionally, but considering that the worst that that can cause is having to recalculate them one time, it's not a huge worry. (As a bonus, for you serious wordpress geeks, if you have an object cache installed, transients are stored in memory, rather than in the database, for even better performance.)

      Long story short, that's what I did. I used the Query Monitor plugin to time my page loads, and with various optimizations to the server I run this site on, I had gotten the time "time to first byte"—the delay for the server to calculate everything before it could start serving the front page to the browser—down to about 4-5 seconds.

      After the below optimizations, time to first byte immediately dropped to a little over a second.

      And there was much rejoicing.

      It works like this. I've got a plugin I wrote, imaginatively called "MK Custom Plugins", which contains all my custom plugins. At the top of that file is this:

      /*
      * Plugin Name: MK Custom Shortcodes
      * Plugin URI: https://michaelkupietz.com/plugins/the-basics/
      * Description: My custom shortcodes.
      * Version: 1
      * Requires at least: 5.2
      * Requires PHP: 7.2
      * Author: Michael Kupietz
      * Author URI: https://michaelkupietz.com/
      * License: GPL v2 or later
      * License URI: https://www.gnu.org/licenses/gpl-2.0.html
      * Update URI: https://michaelkupietz.com/my-plugin/
      * Text Domain: mk-plugin
      * Domain Path: /languages
      */

      // first, some useful functions
      add_action( 'save_post', 'setLastUpdateTimestamp' );
      add_action( 'create_category', 'setLastUpdateTimestamp' );
      add_action( 'edited_category', 'setLastUpdateTimestamp' );
      add_action( 'delete_category', 'setLastUpdateTimestamp' );
      add_action( 'create_post_tag', 'setLastUpdateTimestamp', 10, 2 );
      add_action( 'edited_terms', 'setLastUpdateTimestamp', 10, 2 );
      add_action( 'delete_term', 'setLastUpdateTimestamp', 10, 3 );
      add_action( 'trash_post', 'setLastUpdateTimestamp' );

      function setLastUpdateTimestamp() {
      if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
      return;
      }
      // If this is a revision, don't do anything.
      if ( wp_is_post_revision( $post_id ) ) {
      return;
      }

      set_transient( 'mkFuncTransient_LastUpdateCheck',microtime(true),2592000);
      }

      function latestPostUpdate() {
      $theDate = get_transient( 'mkFuncTransient_LastUpdate_all');
      if ( ! (isset($theDate) && $theDate > 0)) {
      $theDate=microtime(true);
      set_transient( 'mkFuncTransient_LastUpdate_all',$theDate,2592000); //dont expire for 1 month
      }
      }
      return $theDate;
      }

      function getFunctionTransient ($functionName,$arguments=[]) {
      //call with: $transientData = getFunctionTransient(__FUNCTION__,$arguments);
      if ( $transientData != null) {return $transientData;}
      //the $arguments term in the call is optional. It's only for functions that return different values based on their arguments, so you can cache and retrieve the values of different calls with different arguments.
      $data = get_transient( 'mkFuncTransient'.$functionName . hash( 'sha512',json_encode( $arguments ) ) ); //per https://daext.com/blog/improve-the-shortcode-performance-with-the-wordpress-transients-api/
      $lastUpdate = get_transient( 'mkFuncTransient_LastUpdate' . $functionName . hash( 'sha512', json_encode( $arguments ) ) );
      $lastPostUpdate = latestPostUpdate();

      if ( $data !== false and $lastUpdate < $lastPostUpdate ) {
      return $data;
      } else {
      return null;
      }
      }

      function setFunctionTransient ($functionName, $value = null, $arguments=[]) {
      //call this right before returning value of function with as: setFunctionTransient(__FUNCTION__,$returnValue,$arguments);
      //you can leave out returnvalue, or set it to null, to delete the transient.
      //the $arguments term in the call is optional. It's only for functions that return different values based on their arguments, so you can cache and retrieve the values of different calls with different arguments.
      if ( $value === null) {
      delete_transient( 'mkFuncTransient'.$functionName . hash( 'sha512',json_encode( $arguments ) ) );
      delete_transient( 'mkFuncTransient_LastUpdate' . $functionName . hash( 'sha512', json_encode( $arguments ) ));
      return $value;
      } else {
      set_transient( 'mkFuncTransient'.$functionName . hash( 'sha512',json_encode( $arguments ) ) , $value , 2592000); //dont expire for 1 month
      set_transient( 'mkFuncTransient_LastUpdate' . $functionName . hash( 'sha512', json_encode( $arguments ) ), latestPostUpdate(), 2592000 );
      return $value;
      }
      }

      [ ... the rest of my shortcode routines follow here ... ]


      OK! Clear as mud, right? Here's all you need to know. I have a menu generation shortcode that originally looked something like this:

      function hierarchicalsitemap_shortcode_htmlmap($atts, $content = null) {
      .... a bunch of fancy PHP to generate the menu HTML from the categories goes here ....
      $out = [result of above code];
      return $out;
      }


      add_shortcode("htmlmap", "hierarchicalsitemap_shortcode_htmlmap" );

      So, once I had written the functions to handle transients, the only changes I had to make were to add three lines to the existing function, two right at the beginning and one before the "return" statement:

      function hierarchicalsitemap_shortcode_htmlmap($atts, $content = null) {
      $origAtts=array($atts,$content);
      $data = getFunctionTransient(__FUNCTION__, $origAtts);
      if ( $data != null) {return $data;}
      [.... a bunch of fancy PHP to generate the menu HTML from the categories goes here ....]
      setFunctionTransient(__FUNCTION__, $out, $origAtts);
      return $out;
      }


      And that's it. 75% faster load time, just like that.

      If you inspect this page code, before the first menu up top is an invisible <LI> for reasons I can't recall off the top of my head right this moment, but if you look at it, you will see output from the shortcode transient caching function.

      If this is the very first page load since I made a site update, that LI will look something like this:



      But if the menus have loaded at least once since my last update, you'll see this:



      and the page will have loaded in about a quarter of the time.

      If you're the sort of geek who likes this kind of stuff, come on over, we'll have a beer and I'll update the site in front of you and let you see this live. Wait, no, we should meet out in a coffee shop or something, my place is a mess.

      Obviously I could get much fancier with this. No doubt recreational programmers will immediately see further optimizations.

      I'm going to get this cleaned up, better documented, and posted to my github so it's easier to understand. For, you know, some people.
    • Non-render-blocking YouTube embeds in WordPress (488 words) On this site's Music Reviews page (itself built on-the-fly by a shortcode that sorts and displays posts from a "Music Review" custom post type), most of the reviews are accompanied by YouTube video embeds from the album in question.

      What I didn't realize in setting that up is that YouTube embeds use <iframe> tags to embed videos, and <iframe> tags block page rendering—the page's onload property, which signals that the page is fully ready to display to the viewer and to remove the spinning "page loader" image I use on this site as pages load—does not fire until every iframe on the page has fully loaded its contents. This caused that page, which currently only has a handful of videos on it, to load very slowly. What's worse, this site's custom menus parse all page contents every time the site is updated to display for word counts, meaning including that page in the menus drastically slowed down the first load of any page of the site after I update anything as the menus are rebuilt.

      So I hit upon an elegant solution. I created a shortcode as follows: [ytembed url="http://youtube.com/embedurlhere?blahblahblah"] with optional additional parameters "width" and "float" to set the css width and float properties.

      This shortcode creates a <div class="mk_YTembedDiv" data-url="http://youtube.com/embedurlhere?blahblahblah"> with the specified CSS properties. Then a script queued up in wordpress's page footers runs after the javascript event window.onload has fired to indicate the page has fully loaded.  That script checks if the page has any <div class="mk_YTembedDiv"> tag on it. If so, it scans the page to build a list of all of them, and grabbing the data-url attribute from each one, inserts an <iframe> tag into it with the proper attributes to display the video with that embed url.

      Therefore, the embedded <iframe> tags to show the videos are only added to the page, and begin loading, after the page has been built and displayed to the user. It does mean there can be a delay after the user starts interacting with the page before the videos appear, but I assume people will read a reviews before wanting to watch the video at the bottom of it, so I don't think this is a problem. And it's still much less inconvenient than the page not displaying at all until every video has loaded.

      The code that makes the magic happen resides in a custom plugin I wrote to hold all my shortcodes, and here's the README, code, and license, from my github repo at https://github.com/kupietools/wordpress-non-render-blocking-youtube-embed/:



      README.md

      ytembed_shortcode.php

      License

    • Easily embed this site's content on other sites (427 words) This site allows you to get the content of posts and pages by adding either /embed/ or ?embed to the URL, optionally including the post title, author, and/or tags.

      This was inspired by a discussion in an Indieweb Homebrew Website Club Europe/London online meetup. I want to say shadowy web standards advocate Tantek Çelik brought it up, so, as these things tend to happen, I coded it up here while we were talking. It's discussed on Indieweb's wiki at https://indieweb.org/embed.

      What do you mean, 'Embed'?



      Well, for instance, this post's permalink is https://michaelkupietz.com/?p=10887. You can see just the text of this post's content, without the sidebar and menus and other web page "furniture", at https://michaelkupietz.com/embed/?p=10887 or https://michaelkupietz.com/embed/?p=10887&embed, so you can theoretically include this page's content on your own site (long as you don't mind ugly, 1993-looking unstyled HTML.) These are linked in the header of every page, as on this page you see Embed link up there in the small row of article info under the title.

      You can append ?title, ?author or ?tags parameters to the embed URL too, to include the page title, author, and/or post tags at the top of the embed content, respectively. Just do something like "https://michaelkupietz.com/blah/blah/embed/?title&author&tags".

      How It Works Under The Hood (a Wordpress-centric view)



      This is done by adding the following code to the theme's functions.php file:

      // prevent stripping of /embed/ and embed parameters on redirects
      function preserve_embed_parts($redirect_url, $requested_url) {
      if ((strpos($requested_url, '/embed/') !== false ||
      isset($_GET['embed'])) &&
      $redirect_url !== $requested_url) {

      // If original had /embed/, add it to redirect
      if (strpos($requested_url, '/embed/') !== false && strpos($redirect_url, '/embed/') === false) {
      return rtrim($redirect_url, '/') . '/embed/';
      }

      // If original had ?embed or &embed, add it to redirect
      if (isset($_GET['embed']) && strpos($redirect_url, 'embed') === false) {
      $separator = (strpos($redirect_url, '?') !== false) ? '&' : '?';
      return $redirect_url . $separator . 'embed';
      }
      }
      return $redirect_url;
      }
      add_filter('redirect_canonical', 'preserve_embed_parts', 10, 2);

      // Apply template for embed patterns
      function check_embed_template($template) {
      $uri = $_SERVER['REQUEST_URI'];
      $is_embed = (
      strpos($uri, '/embed/') !== false ||
      isset($_GET['embed'])
      );

      if ($is_embed) {
      $new_template = locate_template(array('template-content-only.php'));
      if ($new_template) {
      return $new_template;
      }
      }
      return $template;
      }
      add_filter('template_include', 'check_embed_template');



      template-content-only.php



      Then in the theme folder, there's a template 'template-content-only.php', which is basically as follows:

      /*
      * Template Name: Content Only
      */
      ?>
      get_the_permalink() .
      '" />' .
      "\n"; ?>
      the_post();
      if (isset($_GET["title"])) {
      echo '

      ';
      echo get_the_title();
      echo "

      ";
      }
      if (isset($_GET["tags"])) {
      echo '';
      echo "Posted in ";
      /* [theme-specific code goes here to display post tags; omitted because you're not going to be able to use it, my theme is totally customized, so your theme's functions are different.] */
      echo "
      ";
      }
      if (isset($_GET['author'])) {
      echo '

      By ';
      echo get_the_author();
      echo'

      ';
      }
      the_content();
      $pl = get_the_permalink();
      echo '

      Content originally from ' .
      $pl .
      "
      . © " .
      ($modified_date =
      esc_html(get_the_modified_date("Y")) . " Michael E. Kupietz

      ");
      endwhile;
      // End of the loop.
      ?>


      But Wait, There's More! A caveat.



      If you're like me, and I think you are, you have Content Security Policy headers (I'd link to that, but there really aren't any references that don't make it sound much more complicated than it is) set up on your site to make sure nobody can inject scripts or wrap your site in an invisible iframe and clickjack your mom while she thinks she's looking directly at your site. Well, like all good security measures, this will trip you up, because if you have it set right, it may prevent the end goal of embedding from working at all, because nobody will be allowed to put your site in an iframe.

      This will happen if you have something like this in your .htaccess file:

      Header set Content-Security-Policy "[a bunch of rules & garbage here]; frame-ancestors 'self'". You may have none instead of .

      So we want to bend the rules, so if the url ends in /embed/ or has an ?embed url parameter, the frame-ancestors restrictions are lifted just for that page.

      We do this by wrapping that line with the following, wherever in your .htaccess file the security headers are defined:


      Header set Content-Security-Policy "[all your original rules and junk go here except for the frame-ancestors part]; frame-ancestors 'self' *"


      Header set Content-Security-Policy "[a bunch of rules & garbage here]; frame-ancestors 'self'" [this is just your original Header set Content-Security-Policy line, unchanged from what it was before because we want it to still apply to everything except the special Embed pages.]



      Huge web nerds will notice I used %{THE_REQUEST} instead of %{REQUEST_URI} like ChatGPT told you to when you wimped out and asked it how to do this. This is because ChatGPT doesn't know that there's a bug in Apache where %{REQUEST_URI} doesn't work right in conditionals and will always fail and cause you to spend an entire day going in circles, rending your clothes and wailing in despair at the dark gods who came up with this system. The sole reference I've seen to this is at https://processwire.com/talk/topic/23000-apache2-blocks-dont-work-with-regex-matches-against-the-request_uri-variable/, as was kindly pointed out to me by one of the helpful Indieweb folks, but sure enough, the suggestion there, to use THE_REQUEST instead, worked.

      So, Anyhoo



      This gives you a very easy way to pull my content by whatever embedding method you prefer,e.g.:



      Which, being this very page, is displayed on the front end as:



      I call this "The Infinity Iframe".