• Skip to main content
  • Skip to primary sidebar
Martin Taylor

Martin Taylor

PHP, CSS, MySQL and WordPress tips and techniques

WordPress

CSS-only menus in WordPress

Posted on May 26, 2020

I recently set myself the challenge of building a CSS-only menu (including sub-menu dropdowns) for a WordPress site. Why? Partly as an academic exercise, but partly in an attempt to reduce what appeared to be a too-busy combination of CSS, JS and PHP in the site and theme as it stood.

All this of course in contravention of the well-established KISS principle, but hey … we’re in lockdown and it’s good to have something to occupy the little grey cells.

One thing I soon discovered is that if you need a dropdown menu, you’ll be relying on the so-called “CSS Checkbox Hack”. The term “hack” isn’t quite fair, because the HTML and CSS used are perfectly legitimate and have valid use cases of their own. It’s just that there’s a no-doubt unintended way to use this particular feature of CSS.

There are scores of explanations of the checkbox hack on the web, so I’ll be brief. The idea is that you set up your HTML like this:

<label for="menu-toggle" class="menu-toggle-label">Menu</label>
<input type="checkbox" id="menu-toggle" class="menu-toggle">
<ul class="menu">
    <li>Blah</li>
</ul>

The label will be visible. The checkbox has to be hidden because it’s only there to trigger the action we need (and that’s the “hack”). The <ul> will initially be invisible.

Now, in HTML, if you click the label for a checkbox, it will toggle the checkbox’s “checked” state. And we can use that as a selector in CSS to drive a change in another element – for example to change the visibility of that <ul>. Here’s the relevant part of the CSS:

.menu, .menu-toggle {
    display: none;
}
.menu-toggle:checked + ul {
    display: block;
}

That second rule says that when menu-toggle is checked, find an adjacent <ul> and make it visible. Simple enough. So using the checkbox’s label effectively as a button causes the <ul> to appear and disappear.

The next challenge was to find a way to arrange WP’s menu HTML with the necessary labels and checkboxes. I think the only way to do this is to generate the HTML yourself, and the usual suggestion is to create an extension to the Walker_Nav_Menu class, where you can set up your own start/end HTML for the items and levels in the menu.

But that seemed like a tad too much complexity, so I went for a custom approach based on WP’s wp_get_nav_menu_items function. This brings back a sorted list of the menu items, each with a value defining its parent item, so you can work out what the submenu hierarchy is. By the way, I’d not realised until using this that the menu items are saved as posts in WP’s database.

The first bit of code here is where we grab the menu items and create an array of objects that reflect the hierarchy.

$menu_items = wp_get_nav_menu_items('primary-menu');
// Construct menu hierarchy
$menu = [];
foreach ($menu_items as $index=>$menu_item) {
	$parent = $menu_item->menu_item_parent;
	$menu_obj = new stdClass();
	$menu_obj->url = $menu_item->url;
	$menu_obj->title = $menu_item->title;
	if ($parent) { // Add submenu to parent object
		$menu[$lvl_index]->submenus[] = $menu_obj;		
	} else { // Make new parent object
		$menu[$index] = $menu_obj;
		$menu[$index]->submenus = [];
		$lvl_index = $index;
	}
}

We end up with an array of objects for the top-level menu items, each with an array of its submenu objects. Armed with this, we can generate the HTML. In my case, it looked like this. It’s a pretty straightforward iteration through the array and sub-menu arrays, shoving those labels and checkboxes in where necessary.

echo '<nav class="nav-primary">';
echo '<label for="menu-toggle" class="menu-toggle-label top-menu-toggle-label">Menu</label>';
echo '<input type="checkbox" id="menu-toggle" class="menu-toggle">';
echo '<ul class="menu menu-primary">';
foreach ($menu as $index=>$menu_item) {
	$has_submenu = count($menu_item->submenus);
	$classes = 'menu-item '.'menu-item'.$index;
	if ($has_submenu) $classes .= ' menu-item-has-children';
	echo '<li class="',$classes,'">';
	echo '<a class="menu-link" href="',$menu_item->url,'">';
	echo '<span>',$menu_item->title,'</span>';
	echo '</a>';
	if ($has_submenu) {
		echo '<label for="menu-toggle-',$index,'" class="menu-toggle-label">+</label>';
		echo '<input type="checkbox" id="menu-toggle-',$index,'" class="menu-toggle">';
		echo '<ul class="sub-menu">';
		foreach ($menu_item->submenus as $submenu_item) {
			echo '<li class="menu-item submenu-item">';
			echo '<a class="menu-link submenu-link" href="',$submenu_item->url,'">';
			echo '<span>',$submenu_item->title,'</span>';
			echo '</a>';
			echo '</li>';
		}
		echo '</ul>';
	}
	echo '</li>';
}
echo '</ul>';
echo '</nav>';

And hey, it works. But I decided against using it.

A few reasons for that.

First – it uses an unintended consequence of a CSS feature, and it puts checkboxes into a page that no user will ever see. That’s not what checkboxes are for.

Second – I still hold to the principle that CSS is for styling and JS is for functionality. This approach blurs that line.

Third – a JS approach allows me to do more with the HTML, like adding classes to dropdowns when they are visible … I can’t see a way to do this with CSS alone.

So all-in-all, a good exercise with some useful lessons.

Filed under: CSS, PHP, WordPress

How to cache a WordPress menu

Posted on May 26, 2020

I recently looked into creating a CSS-only navigation menu for WordPress, in an attempt to lighten the page payload. That’s a story that I’ll post separately, but it made me realise just how much effort WordPress goes to when creating the menu.

Of course, it’s all behind the scenes and it probably takes very little elapsed time, but it made me wonder whether its possible to cache the HTML. I’d used WP transients before to cache some custom site elements such as maps and galleries which don’t change frequently, so what about menus?

It turns out that it’s really straightforward to do. That said, there is a potentially major disadvantage, depending on the styles you’re applying to the menu items. I’ll come back to that shortly.

There are very few lines of code to add to your theme’s functions.php.

add_filter( 'wp_nav_menu', 'mt_cache_menu', 10, 2);
function mt_cache_menu($menu, $args) {
	set_transient ( 'mt_menu' , $menu, 3600 );
	return $menu;
}
add_filter( 'pre_wp_nav_menu', 'mt_cached_menu_check', 10, 2);
function mt_cached_menu_check($return, $args) {
	$menu = get_transient ( 'mt_menu');
	if ($menu) return $menu.'<!-- Cached -->';
	return null;
}
add_action ( 'wp_update_nav_menu', 'mt_delete_menu_cache', 10, 2);
function mt_delete_menu_cache($menu_id, $menu_data) {
	delete_transient ( 'mt_menu');
}

The first function, using the wp_nav_menu hook, saves the menu HTML when it’s just been created by WP. You need to decide on the key for the cache transient, and how long the cache will persist, in numbers of seconds. In the example above I’ve set it only for 3600 seconds, but in reality you’d probably choose a much higher value. If you leave this parameter out, it will default to “0”, which means “do not expire”.

The second function uses the pre_wp_nav_menu hook. WP calls this hook just before it sets about creating the menu. If you return a non-null value, WP will use that as the HTML, and save itself the bother of generating the menu afresh. So get_transient looks to see if the transient exists, and if so, whether it’s expired. If we get a value back, it must be the HTML we stored there, so we return it to WP. Otherwise, we return null so that WP will regenerate the HTML.

The final function here is to delete the cache when a menu is updated. It’s a simple hook into the wp_update_nav_menu action which fires in the admin back-end as a menu is changed. We simply delete the transient, with the result that next time round, WP will have to recreate the HTML and we can cache the new code.

Now, that major downside …

The menu HTML elements are dynamically assigned CSS classes. You can see the list at https://developer.wordpress.org/reference/functions/wp_nav_menu/#menu-item-css-classes. Classes such as .current-menu-item are designed for you to use to style (maybe highlight) the menu item for the current page. And if you cache the output, that class will be frozen in the HTML as it was when the menu was saved away into the transient. It won’t reflect the current page any more.

Hmm. Well, as long as you’re not styling anything like that, the cached version will be OK to use. But it could be a big disadvantage to caching. I’ve seen some blog posts where people suggest caching the menu separately for every page (every post?) on the site. Well maybe, but on a site with a big menu structure, maybe not.

I’ve one site where this will not be an issue – the menu styling doesn’t use current page highlighting. So I’ll go with this caching approach and see how we get on.

Filed under: WordPress

Using CSS Grid for layout

Posted on May 21, 2020

I have used a CSS Grid layout to display a Calendar, as described here. I was impressed by how easy it was to implement responsive behaviour with a grid, just by changing the ‘grid-template-columns’ value.

The other day, I decided to try CSS Grid as a way to lay out a WordPress page, and in this case one that uses a Genesis child theme. The layout of particular interest was in the body of the page, containing the content and sidebar.

The sidebar can be placed to the left or right of the content, according to theme customisation options in the admin UI. The stylesheets I’ve seen generally use “float: left” and/or “float: right” to achieve the layout.

Thing is, I’m not hugely fond of “float”, apart from quite limited contexts such as images in text blocks. I’ve often had problems with other page elements being affected by the float, which leads me to look for alternatives.

So the technique I’ve used is, first, to associate a grid-area with each of the two elements. Note that in Genesis at least, the elements are of the types <main> and <aside>.

main {
	grid-area: main;
}
aside {
	grid-area: aside;
}

Once these grid area names have been defined, they can be used to define the layout of the grid columns as follows:

.content-sidebar-wrap {
    display: grid;
}
.content-sidebar .content-sidebar-wrap {
    grid-template-columns: 66% 33%;
    grid-template-areas: "main aside";
}
.sidebar-content .content-sidebar-wrap {
    grid-template-columns: 33% 66%;
    grid-template-areas: "aside main";
}

Note that Genesis handily assigns a class to the <body> element, to define the layout that the admin has selected. It’s either ‘content-sidebar’ or ‘sidebar-content’ in this case, but this approach would be easy enough to adapt for other scenarios.

This page itself uses the technique – take a look at the HTML/CSS sources for more detail.

For small / mobile devices, it’s straightforward enough – just apply “display: block” or something like that to the wrapping container.

Now, I must be honest and say that while this works really well, I have a niggling reservation. Many many years ago, we used HTML tables to apply structure and layout to web pages. It’s certainly not deemed best practice these days … use tables where you have tabular data, and nowhere else. Could the same be said of CSS Grids?

Filed under: CSS, Genesis, WordPress

PHP Calendar class

Posted on May 10, 2020

Note – I corrected this code in July 2021, because the original was unaware that some months span six Monday-to-Sunday weeks. The setDates() function is the one that needed to be corrected.

Further note – I decided to use PHP’s relative date calculations to simplify the code – once again, the setDates() function has been updated below.

I needed to build a monthly events calendar to display on one of my WP sites. I’d been using The Events Calendar plugin, which had served me well, but a recent new version had caused some issues, so I started looking at alternatives. To be fair, even with its problems, The Events Calendar was still better than the others for what I needed.

But the plugin was a heavyweight solution for my requirement, so I set about seeing if I could develop something of my own. One of the primary challenges was to generate a monthly calendar, ideally one with responsive markup.

There were many examples on the web, which were interesting enough, but most of them used HTML tables, so I tried a slightly different approach, and I’m happy with the solution I devised.

See a working example

Here’s the PHP class for the calendar:

class myCalendar {

private $curr_date, $prev_date, $next_date;
private $first_cal_day, $last_cal_day, $num_weeks, $has_events;

public function __construct($year, $month) {
    $this->setDates($year, $month);
}

public function setDates($year, $month) {

    $this->curr_date = new DateTime();
    $this->curr_date->setDate($year, $month, 1);

    // Last day of previous month
    $this->prev_date = new DateTime();
    $this->prev_date->setDate($year, $month, 0);

    // First day of next month
    $this->next_date = new DateTime();
    $this->next_date->setDate($year, $month + 1, 1);

    // Get first Monday to appear on calendar
    $this->first_cal_day = new DateTime();
    $this->first_cal_day->setDate($year, $month, 2)->modify('previous Monday');

    // Get last Sunday to appear on calendar
    $this->last_cal_day = new DateTime();
    $this->last_cal_day->setDate($year, $month + 1, -1)->modify('first Sunday');

    // Calculate number of weeks
    $diff = $this->first_cal_day->diff($this->last_cal_day);
    $this->num_weeks = ceil ($diff->days / 7);
}

public function get_date() {
    return $this->curr_date;
}
public function get_prevdate() {
    return $this->prev_date;
}
public function get_nextdate() {
    return $this->next_date;
}

public function day_names($format = 'abbr') {
    switch ($format) {
        case 'abbr':
            return ['Mon','Tue','Wed','Thu','Fri','Sat','Sun'];
            break;
        case 'full':
            return ['Monday','Tuesday','Wednesday','Thursday','Friday','Saturday','Sunday'];
            break;
        default:
            return ['M','T','W','T','F','S','S'];
            break;
    }
}

public function getCalendar($with_events = true) {

    if ($with_events) {
        $events = $this->getEvents();
    } else {
        $events = [];
    }
    $calendar = [];

    $oneday = new DateInterval('P1D');
    $calday = $this->first_cal_day;

    for ($w = 1; $w <= $this->num_weeks; $w++) {
        for ($d = 1; $d < 8; $d++) {
            $day = new stdClass();
            $event_date = $calday->format("Y-m-d");
            $day->date = clone $calday;
            $day->has_event = array_key_exists($event_date, $events);
            if ($day->has_event) {
                $day->events = $events[$event_date];
                $this->has_events = true;
            } else {
                $day->events = [];
            }
            $calendar[] = $day;
            $calday->add($oneday);
        }
    }
    
    return $calendar;

}

public function getEvents() {

    $events = [];

    $events['2020-04-01'][] = ['text' => 'Event1', 'slug' => 'event-one'];
    $events['2020-04-01'][] = ['text' => 'Event2', 'slug' => 'event-two'];
    $events['2020-04-05'][] = ['text' => 'Event3', 'slug' => 'event-three'];
    $events['2020-04-22'][] = ['text' => 'Event4', 'slug' => 'event-four'];

    $events['2020-05-01'][] = ['text' => 'Event5', 'slug' => 'event-five'];
    $events['2020-05-11'][] = ['text' => 'Event6', 'slug' => 'event-six'];
    $events['2020-05-25'][] = ['text' => 'Event7', 'slug' => 'event-seven'];
    $events['2020-05-27'][] = ['text' => 'Event8', 'slug' => 'event-eight'];   

    return $events;
    
}

public function has_events() {
    return $this->has_events;
}

}

After instantiating the object with a year and month, I can ask it for an array of days, covering the full weeks that span the start and end of the month. For example, the calendar for April 2020 runs from Monday March 30 to Sunday May 3.

Each element in the array is an object, with properties:
date = PHP DateTime object for the day
has_event = true / false showing whether there are events on this day
events = array of events occurring on this day

In my site, the events are a custom post type that are queried in the getEvents method, so I’ve hard-coded some examples in the code above. The events are presented as an array of arrays so that each date can have more than one event. I use the text property to display in the calendar, and the slug property becomes a link to the page for that specific event.

There are a few helper methods such as get_prevdate and get_nextdate, which can be used to create Previous / Next month links above the calendar on the web page.

Now, for presentation, rather than using HTML tables, I’m using “display: grid” for the calendar’s container div. For a wide screen, the CSS is:

.calendar-grid {
  display: grid;
  grid-template-columns: 14% 14% 14% 14% 14% 14% 14%;  
}

Then with a media query for smaller displays, it falls back to a simple vertical list. Neat, and with some accompanying use of display: none; I can easily hide any days with no events when it’s in this format.

@media screen and (max-width: 600px) {

.calendar-grid {
  grid-template-columns: auto;  
}
.no-event {
    display: none;
}
...

Here’s an example of using the class on the front end:

$calendar = new myCalendar($year, $month);
    
echo '<div class="calendar-container">';
    
echo '<div class="calendar-headers">';
echo '<div class="calendar-nav-prev"><a class="prev-month" href="?caldate=',$calendar->get_prevdate()->format('Y-m'),'"><< ',$calendar->get_prevdate()->format('F'),'</a></div>';
echo '<div class="calendar-title">',$calendar->get_date()->format('F Y'),'</div>';
echo '<div class="calendar-nav-next"><a class="next-month" href="?caldate=',$calendar->get_nextdate()->format('Y-m'),'">',$calendar->get_nextdate()->format('F'),' >></a></div>';
echo '</div>';

$caldays = $calendar->getCalendar();

if (!$calendar->has_events()) {
    echo '<div class="calendar-empty">No events scheduled for this month</div>';
}
    
echo '<div class="calendar-grid">';
     
foreach ($calendar->day_names() as $dayname) {
    echo '<div class="day-name">',$dayname,'</div>';
}
    
foreach ($caldays as $calday) {
    $calmonth = $calday->date->format("m");
    $cellclass = ($calmonth == $month) ? 'current-month' : 'adjacent-month';
    $cellclass .= $calday->has_event ? " has-event" : " no-event";
    echo '<div class="',$cellclass,'">';
    echo '<div class="dayinfull">',$calday->date->format("l j M"),'</div>';
    echo '<div class="daynumber">',$calday->date->format("d"),'</div>';
    if ($calday->has_event) {
      foreach ($calday->events as $day_event) {
        echo '<div class="calendar-event">';
        echo '<a href="/events/',$day_event['slug'],'">',$day_event['text'],'</a>';
        echo '</div>';
      }
    }
    echo '</div>';
}

// End the grid
echo '</div>';
// End the container
echo '</div>';

Filed under: PHP, WordPress

Overriding site title in a Genesis theme

Posted on May 8, 2020

I wanted to make the whole site title and tagline area into a link to the home page, on my Genesis-themed site. By default, only the site title is hyperlinked by Genesis. To make matters a tad more complicated, the area containing the two pieces of text has a background image, offset to the left of the text.

The HTML for the area in question, as output by Genesis, goes like this:

<div class="title-area">
<h1 class="site-title" itemprop="headline">
<a href="http://mysite.com/">Site name</a>
</h1>
<p class="site-description" itemprop="description">Site tagline</p>
</div>

Essentially, then, I wanted to add a hyperlink around the whole of this block, and to remove the link you can see on the site name.

Reading up about Genesis filters, I found that there are a number of likely candidates. They all belong to the genesis_markup family, and you can be quite specific in targeting a particular context.

For example, Genesis precedes the title area with this code:

genesis_markup(
	[
		'open'    => '<div %s>',
		'context' => 'title-area',
	]
);

It looked like it should be possible to apply a filter to this at the hook named genesis_markup_title-area, or even genesis_markup_title-area_open.

Suffice to say that I tried for some time to use one or other of these filters but was unsuccessful. Maybe one day I’ll work it out.

In the end I took a more radical approach, and completely over-rode the function that creates this area of the page. I added this code to the functions.php file in my child theme.

remove_action ('genesis_header', 'genesis_do_header');
add_action ('genesis_header', 'custom_do_header');

function custom_do_header() {
	echo '<a class="title-link" href="',get_home_url(),'">';
	echo '<div class="title-area">';	
	echo '<h1 class="site-title" itemprop="headline">',get_bloginfo( 'name' ),'</h1>';
	do_action( 'genesis_site_description' );
	echo '</div>';
	echo '</a>';
}

This works, of course, and i suppose it has the advantage of being simple. It always concerns me, though, that by overriding a parent theme function, I run the risk of missing out on a theme update / improvement. Using hooks and filters should ideally be the way to customise a parent theme.

Filed under: Genesis, WordPress

Extra checks on WordPress login

Posted on May 7, 2020

The WordPress login is quite possibly secure enough, but on a couple of my sites I’ve added a piece of custom authentication. This also acts as a bot-filter, given that the required input (in theory) isn’t obvious.

I figured it might be useful to describe the process of adding a field to the login form, and subsequently validating the user’s input.

A note – the algorithm I’ve used on my other sites isn’t the same as this, and doesn’t depend on a session variable. Sessions are by default unavailable in WordPress, which strives to be stateless, but you can set up a session quite easily, as it turns out.

Another note – there are plugins that will do something similar, but I like to have more control over my sites, and it’s not difficult to code.

The idea is that I create a string containing pairs of characters and digits. For example, this might be p4s1g2d4b6c7m4t3. I display this on the login page. My users know the secret, which is that they must enter the digits following ‘c’ and ‘p’ respectively. In this case, they would enter 74.

The code is regenerated on every login form, in an attempt to make it tricky to see what’s going on. It’s not utterly fool proof but hopefully obscure enough to ward off casual intruders.

Step 1 – create and store a random ‘captcha’ value, and add a field to the login form.

This code would go in your theme’s functions.php or in a plugin. The PHP tags at start and end are only shown here to enable syntax highlighting.

<?php
add_action( 'login_form', 'my_login_form');
function my_login_form () {
  session_start();
  $chars = ['b','c','d','g','m','p','s','t'];
  for ($i = 0; $i < 8; $i++) {
    $chars[$i] .= rand(1,9);
  }
  shuffle($chars);
  $_SESSION['captcha'] = join('',$chars);
?>
<div style="height: 100px;">
  <label>
    <p><?php echo $_SESSION['captcha']; ?></p>
    <p>Enter security code numbers below</p>
    <input type="text" name="captchacode" value="">
  </label>
</div>
<?php }

So we hook into WP’s login_form, in which you are simply adding to the standard form. We start a session, create the random string, and store it in the session. Then we display the string, and add an input field to the form.

Step 2 – validate the user’s input against the stored string.

Again, this code would go in your theme’s functions.php or in a plugin.

<?php
add_filter('authenticate', 'check_captcha', 90, 3);
function check_captcha($user, $login, $password) {
  session_start();
  $session_code = $_SESSION['captcha'];
  session_destroy();
  if ($login == '' or $password == '') return $user;
  if (is_wp_error($user)) {
    $error = '<strong>ERROR</strong>: Login details not recognised';
    return new WP_Error( 'invalidcombo', $error );
  }
  $captcha = '';
  $chars = ['c','p'];
  foreach ($chars as $char) {
    $i = strpos($session_code, $char);
    $captcha .= $session_code[$i + 1];
  }
  $input = sanitize_text_field($_POST['captchacode']);
  if ($captcha != $input) {
    $error = '<strong>ERROR</strong>: Login details not recognised';
    return new WP_Error( 'invalidcombo', $error );		
  }
  return $user;
}
?>

We hook into WP’s authenticate filter, and pick up the session variable we stored earlier. Then just to be tidy, we destroy the session.

Next I have some code to check if WP has already have rejected the user, so there’s no point in continuing beyond there; we just return an error.

And then on to the check for the extra input field. The two characters to check are set up in an array (‘c’ and ‘p’ in this case). If you wanted to add more characters this is where you’d do it.

Then we concatenate the digits to the right of each of the key characters, and check the user’s input against the result. If it’s no good, we return an error, but if it looks good, we return the value that was originally supplied to the filter and let WP do the rest.

Filed under: WordPress

Primary Sidebar

Recent Posts

  • PHP Calendar Class using relative dates
  • PHP Calendar Class revisited
  • Wildcard subdomains in XAMPP
  • MySQL – retrieving rows with one column as key
  • CSS-only menus in WordPress

Copyright Martin Taylor © 2023