Changing permalinksComments

Ever since I moved to WordPress I have had my permalink structure set as /%year%/%monthnum%/%day%/%postname%/ which served well enough. With the increase in microblogging and posts without titles, however, this structure looks a bit messy as the post permalinks also include date and time, such as: 13-12-2017-1127.

Not nice.

So I wanted to change the structure to just /%postname%/ which looks much nicer. The only problem is that changing it means links will no longer work unless redirection is in place.

I was looking for the best way to do this and came across the “Permalinks Migration Plugin for WordPress” by Dean Lee.

This is a great little plugin! All you do is copy your current permalink structure into the plugin settings page and click to update. Then you are free to change your permalink settings to any other option and all links will be redirected.

No messy link lists or .htaccess editing required and my post URLs are much cleaner.

Changing permalinks

A new blogrollComments

I have completed the first version of my blogroll plugin and you can view the results here. At present it includes ten people as I wanted to stay true to James Shelley’s notion about value.

Spoiler alert: he made the list.

As previously mentioned, the blogroll stores entries using a custom post type then displays them by way of a shortcode which can be placed on any page. Each time the shortcode is triggered the blogroll.opml file is recreated so it is always up to date.

I have updated the site footer to show links to both the directory and blogroll. The directory is still valuable for discovery purposes but nothing beats explicit recommendation.

The plugin is available on GitHub - usage and styling info is in the readme.

A new blogroll

Doing things properly

While quickly discussing an issue (where native comments from others were posted as from me on Aaron Parecki pointed out on the #indieweb slack that the microformats markup was incomplete in that the comments were missing h-cards.

The h-card gives machine readable information about the author of an on page element so it was wondered if this was the cause of the issue.

Manton has confirmed it's not but that doesn't mean I shouldn't fix things.

So, hopefully, I’ve made some changes to the theme that resolve this and correctly identify the author (name and URL) for each comment, although I did have to force every comment to have the class h-as-reply to ensure child comments (replies to replies) are distinguished from their parents.

To ensure correct parsing I also had to add a filter to the comment_text() function so that the comment body was marked up with the e-content class:

function add_class_comment_text($content) 
    return "<div class='e-content'>" . $content . "</div>";
add_filter('commenttext', 'add_class_comment_text');
Doing things properly

Whilst double-checking settings I was greeted by the notification “Your API key is no longer valid” for Akismet. I didn’t know they expired, maybe something changed with the last JetPack update.

This would explain why I’ve suddenly been getting more spam comments which Akismet really should be blocking. I know they try to “encourage” people to pay on the Basic (read “old free”) tier now - when going to get a new API key the cost slider was set at $27 by default.


More on comment spam

I’ve been getting more comment spam that’s not automatically nixed by Akismet. Under Settings > Discussion you can add terms to a blacklist so that comments are automatically placed in the bin but I wanted to go one step further.

I found a code snippet described as automatically deleting spam comments but it actually prevents them from being submitted in the first place using wp_die to kill the process on detection of specified terms.

It didn’t seem to work properly throwing “empty needle” errors when the found term was not the first in the list. Not only that but to edit the list of prohibited terms you would have to edit the code itself.

So I reworked it, fixed the empty needle errors by changing the detection method and created a simple plugin which lets you specify banned terms via a settings page in wp-admin.

When a banned term is found the comment is prevented from being submitted and wp_die kicks in displaying a reason and the prohibited term:

I’ll see how it goes and will be adding more items as I come across them, but let me know if you have any problems submitting comments and what term is specified.

You can check out the plugin on GitHub.

More on comment spam

I’m not entirely sure why but I completely separated the newer blog content from the archive to even the search level.

If searching when on a newer page you only get results since the post-archive date. Search from within the archive, however, and you’ll get results from anywhere.

Creating the archive itself makes obvious sense but I’m not so sure about extending this separation to search. I think I’m going to remove that restriction.


If you had visited the Today page between last night and this morning you might have noticed a bit of a change - or things acting a bit weird while I was trying to make that change.

I wanted to further enhance the Today page so considered having the header fix itself to the top of the browser when scrolling up and the page contents would then slide underneath it like this:

It was working to a point but the section sizing wasn't behaving itself when fixed, acting differently across platforms and screen sizes.

I've removed the change again but just wanted to let you know you weren't going mad.


Rethinking the feedsComments

Colin Devroe made a good point.

He is subscribed to my main feed and wondered why he didn't see my Watch follow-up post about the woman on the train.

The answer was that it was a microblog post and, therefore, in the /feed/microblog feed instead.

Emailing back and forth about it made realise that this is a bit pointless.

I was originally going to keep the microblog separate from the longer posts and, because doesn't want post titles for micro posts, set up a custom feed with no item title element.

It was my intention to not include the shorter posts in the main feed as it had always been for more essay-type posts but, ever since I decided not to separate micro posts on the site, I have been using standard and status posts interchangeably - the only difference is whether they have a title or not.

So, for common sense to prevail (and for related posts not to be ignored based on length) I am going to remove the exclusion for micro posts from the main feed.

The microblog feed will still exist, as will the separate podcast feed, but everything will now be in the main feed.

It has been quite a moot point anyway. A status post of 281 characters without a title would have already been included in the feed being not be added to the microblog category.

Arguing the toss over character count is stupid.

Rethinking the feeds

Life without WorkflowComments

From originally not knowing how I would ever use it, Workflow has become such a big part of how I do things that I'd be pretty lost without it. So, just like a lot of others I had a moment of panic when the app was bought by Apple.

Acquisitions of that type tend to be for specific functionality or for the team with the app itself often falling by the wayside or being removed altogether.

Luckily, that doesn't seem to be the case so far but doesn't rule it out in future. I, therefore, started thinking about what I do and how I could do it differently.

First step: looking through my 'flows' to see what I use Workflow for

  • microblog posts to WordPress
  • longer status type posts to WordPress
  • Likes and Replies
  • Posting microcast episodes (including a flow to check the latest episode uploaded to iCloud Drive)
  • rename, convert and resize photos
  • joining images

Second step: how could I do things without Workflow?

Images can be processed by other means so that's not an issue. I have a few image apps I use on a regular basis (Enlight, Phoenix, Photo Joiner) so that is covered.

I use Ulysses for posting long form pieces with titles, and posts with images regardless of length so could use it for any normal post - it just feels a bit heavy for microposts.

I've never really liked the WordPress app for iOS (or Android for that matter) as it always seemed unreliable and messy. Still, I've not used it for ages so quickly installed it. It has definitely improved but still feels a bit clunky - maybe it's just something you have to get used to.

I could post using Drafts using an action to go via the WordPress app (without actually having to use it) but would then need to switch to wp-admin in some cases.

The app supports the core functionality, including post formats, so 'status' can be chosen for microblog and title-less posts. It doesn't, however, appear to support custom fields which is a problem for me.

Both 'Likes and Replies' and microcast posts rely on being able to add data to custom fields. Being unable to do this kills automation for me and forces me to post via the WordPress backend - which I hate doing.

Ulysses also doesn't support custom fields and, according to the last feedback I had from the team, doesn't intend to.

Maybe the WordPress app can support them via the wordpress:// URL scheme although I doubt it, or another app, otherwise I'd be stuck. But I don't really want to buy yet another writing app just for that feature.

Conclusion: where would I be?

Without Workflow I would be stuck in various multi-staged processes. It's perfectly doable but very fiddly compared to the ease of automation that we just take for granted.

It's good to know where you stand.

Life without Workflow

Getting plugged in – epilogue

Since raising the version of the 'Likes and Replies' plugin to 0.9.0 and calling it a release candidate I don't think I've come across any problems. It's hard to believe that the last commit logged on GitHub was 17 days ago.

Where does the time go?

There might be some tidying up I could do to the code but, on the whole, I think it's achieved exactly what I was after and reached its natural conclusion.

That's not to say there won't be future updates if I think of new functionality, find problems or want to do things differently. But, for now, I think it's safe to put down a marker and bump it to a 1.0 release.

Although the plugin is reasonably simple I've learnt some good techniques which can be applied again and again while also learning from mistakes that, hopefully, won't be repeated.

It's been fun to go through the process of documenting this publicly, with nowhere to hide, and am glad I could contribute something to those who may use it.

Getting plugged in – epilogue

Getting plugged in – part 6: includes

There are times when I feel like a bit of an idiot. This is one of those times!

As you will no doubt recall, I was trying to separate out the plugin actions into various included files. The relevant code triggered correctly when posting via the REST API (I.e. from Workflow) but the action that should be run when posting natively failed.

I couldn't work out why.

So, I started disabling the safety checks to see if I could find out why it wasn't working. I soon discovered that it was the nonce check that was failing.

And then it dawned on me...

I had copied the code directly from the main plugin file to an include but not altered the condition for the nonce check.

Why did this matter?

Well, the original condition was basename( __FILE__ ) - used when you want to check that the action is being triggered within the same file. Quite obviously it now wasn't, so the check failed and the code never ran.

Changing the condition to a manually entered string meant the nonce verification would now pass and the code be triggered.

(At least we know that nonce verification works.)

I have now separated all functions into includes called from the main plugin file and everything appears to be working nicely.

I have, therefore, bumped the plugin version to 0.9.0 and called it a release candidate. Barring any glitches or obvious problems this will be the final code.

As always, the latest version is available in the GitHub repository.

Getting plugged in – part 6: includes

Getting plugged in – part 5: settings

What started as a quick update to split the plugin into parts (so that it wasn't all one monolithic file) became quite a major one.

My original plan was to move both hooks for updating the post content to separate included files - it hasn't quite gone according to plan.

The hook for wp_insert_post_data (used when posting via Workflow) has worked fine. When the save_post hook and function were moved to a separate file, however, I couldn't get it to trigger.

I have had to move it back into the main plugin file until I find a solution but this setback gave me an excuse to look at adding a settings page and adding the code for that to an include instead.

As when creating the meta box, this was very much a case of doing it in stages, ensuring each worked before moving on.

Settings page

First up we need to create an admin menu entry using the admin_menu hook and add_menu_page() function:

add_action('admin_menu', 'landr_menu');

function landr_menu() {
    add_menu_page('Likes and Replies Settings', 'Likes and Replies', 'administrator', 'landr-settings', 'landr_settings_page', 'dashicons-admin-generic', 3 );

The parameters for the function are as follows:

  • the settings page title (in the header title tag)
  • the text for the menu item
  • who can see the menu item (capability)
  • the URL slug for the page
  • the callback function used to populate the page
  • the menu item icon
  • the position in the menu (if not entered it will be at the bottom)

That's all you need to create a settings page, it'll be empty but it's surprising how simple it is to create. Now we just need to add to it.

Form and options

We use the callback function mentioned above to populate the settings page - whatever you include within the function gets display. Essentially you are just building a HTML form but with a few special features.

I wanted to have the option to change the text used by the plugin to precede the like and reply links so that's what the form here will set.

The form fields need to be identified so their values can be saved so we first have to make WordPress aware of the options we are going to be saving to the database. They must be contained in a settings group which we can think of as like a HTML fieldset.

function landr_settings() {
    register_setting( 'landr-settings-group', 'like_text' );
    register_setting( 'landr-settings-group', 'reply_text' );

The form itself is simple and pretty standard but should include at least the below:

  • the post method points to WordPress' built in options.php for saving any settings
  • calling the settings group for the form using settings_fields() - it must match the group registered above
  • the fields will be named the same as the registered settings.
  • the standard WordPress submit_button();

Registering the settings we intend to use, calling the settings group and naming the fields correctly means that WordPress takes care of everything for us. Using options.php in the post method does exactly as you would imagine: saves the settings to the options table in the database.

Using the values

We use the saved settings in a couple of ways: firstly, the form itself needs to show the current values when loaded, and the code to update the post needs to pull them in.

Luckily, reading the values is simple because of where they are stored - we just use the get_option() function with the setting name as a parameter. Then, just to be on the safe side, we escape the values to remove any invalid characters:

esc_attr( get_option('reply_text') );

We just echo this wherever the option is needed.

Defaults, activation and deactivation

Whenever options are being written to the database it is good practice to remove them if the plugin is deactivated or uninstalled - we don't want to leave unnecessary data lying around.

We also need to set some defaults in place when the plugin is activated. Yep, you guessed it, we're using hooks again:

register_activation_hook( __FILE__, 'landr_activate' );
register_deactivation_hook(__FILE__, 'landr_deactivate');

function landr_activate() {
    add_option('like_text', 'Liked:');
    add_option('reply_text', 'In reply to:');

function landr_deactivate() {

__FILE__ is used in the hooks because they are included in the main plugin file.

And that's it, we're done.

I will try to get the save_post hook working from an include in a future revision but, for now, the updates have been pushed to the GitHub repository.

Getting plugged in – part 5: settings

Getting plugged in – part 4.5

After getting the meta box working I realised that the code hooked into save_post wasn't being triggered when posting via the Workflow app.

The app is probably using the WordPress REST API to create the post which doesn't behave in the same way as native posting and bypasses the hook.

John Johnston suggested hooking into wp_insert_post_data instead for posts made in this way.

I didn't know anything about this method so had to do a bit of research but discovered that it was actually pretty simple to use.

The main caveat was that, as I needed to access the post ID to work with the meta data, I had to pass the second, optional parameter $postarr to my function or it wouldn't work.

add_filter( 'wp_insert_post_data', 'filter_post_data', '99', 2 );

function filter_post_data( $content, $postarr )

It was then largely a case of repurposing the same code from the save_post hook to build the updated post content and return it from the function:

$content['post_content'] = /* new content value here */;

return $content;

It seemed to be working when viewing the post preview but when going to edit the post draft in wp-admin only the original post content was showing.

And I couldn't work out why.

I broke the site a few (a lot) times trying to figure it out before, eventually, realising that the content displayed by the front end and the edit post page are actually saved in different places.

The post as seen by the reader takes its content from the post_content field but the edit post screen pulls its version from post_content_filtered - I had been updating the former but not the latter, hence the confusion.

With this accounted for everything works as planned either locally within WordPress or when posted from Workflow.

Thanks John.

Getting plugged in – part 4.5

Getting plugged in – part 4: meta boxes

Numerous tutorials exist for adding and using meta boxes; some manage to make it seem like a dark art by rushing through too much in one go without explaining exactly what is going on.

This isn't going to be a guide, more a detailing of the steps I have taken to get a meta box in place for adding the 'liked' and 'reply' custom fields on posts then converting those into #indieweb webmention links.

Display a simple meta box

This is the easy bit, and I actually mean easy.

WordPress provides the add_meta_boxes hook and add_meta_box() function to easily register a custom box. We first need to create a function which calls add_meta_box() then add that function as an action to the add_meta_boxes hook:

function landr_custom_meta() {
    add_meta_box( 'landr_meta', 'Like and Replies', 'landr_meta_callback', 'post' );

add_action( 'add_meta_boxes', 'landr_custom_meta' );

The add_meta_box() function needs four parameters although more can be used to control where the meta box sits on the page, these are: ID, the box title, a callback function which actually does the work, and the type of post we want it to be used with.

Just to prove everything is set up properly a callback function can be created:

function landr_meta_callback() {
    echo 'Likes and replies go here.';

And, just like that we have a meta box.

It doesn't do anything yet, but it was easy to create. Now comes the fun stuff.

Adding fields and saving values

Getting the required fields in is just a case of adding some standard HTML (so I won't go in to that here) ensuring that the fields are named properly so they can be referenced later.

With the fields added we need to be able to write their values to the database if they have been populated. Another function coming up added as an action to the save_post hook.

As we saw before, security is paramount when dealing with WordPress plugins. Because we are dealing with something that can write to the WordPress database we need to ensure that this is handled securely.

There are two steps to take: firstly, adding a nonce (number used once) field to the meta box which will prevent improper access, and double-checking that the person trying to save the data is allowed to do so:

//create nonce

wp_nonce_field( basename( <strong>FILE</strong> ), 'landr_nonce' );

//check if nonce exists and is verified

if ( ! isset( $_POST[ 'landr_nonce'] ) ) {

if ( ! wp_verify_nonce( $_POST['landr_nonce'], basename( <strong>FILE</strong> ) ) ) {

//check permission to edit post

if ( ! current_user_can( 'edit_post', $post_id ) ) {

The 'liked' or 'reply' values can then be written to the postmeta table using add_post_meta() ready to be converted into webmention links.

if ( isset( $_POST['liked-url'] ) ) {
    $liked_url = sanitize_text_field( $_POST['liked-url'] );
    add_post_meta( $post_id, 'Liked', $liked_url );

if ( isset( $_POST['reply-url'] ) ) {
    $reply_url = sanitize_text_field( $_POST['reply-url'] );
    add_post_meta( $post_id, 'Reply', $reply_url );


Previously, the plugin used the content_save_pre hook to add the webmention link to the start of the post content. With the new code linked to save_post it wasn't going to work as it fires after content_save_pre.

The original code to write the link was modified slightly and moved into the same function so everything is processed at the same time.

It needs some tidying up but the updated plugin can be viewed in the GitHub repository.

Getting plugged in – part 4: meta boxes

Improving the webmentions directoryComments

When creating my webmentions author directory I originally wanted to avoid any reliance on the #indieweb Semantic Linkbacks plugin, as not everyone will be using it, and I wanted to keep things fairly self-contained.

It also meant that if you are not using webmentions you could just remove that argument from the initial query and just use the template for any comments.

Unfortunately, WordPress doesn't provide proper support for comment types as it does post types.

So, when a webmention reply is received (as opposed to likes or RSVPs etc.) WordPress the plugin converts it to a normal comment, removing its 'comment_type' value. This means it doesn't get caught if filtering for only webmentions. (Thanks to Michael for the full lowdown.)

In order to catch these replies I am forced to add a check against the 'semantic_linkbacks_type' for each comment to see if it is actually a reply.

$wmreply = get_comment_meta( $comment->comment_ID, 'semantic_linkbacks_type', true );

  then check if

$wmreply == 'reply'

The original query included 'type' => 'webmention' but this, obviously, had to be removed so that the linkbacks check could be performed against all comments.

Combining the two type checks gives the desired result but, for my purposes, there is a caveat.

I wanted the directory to list those engaging via directly from their own sites but the above also lists replies send from as the service supports webmentions. I have, therefore, added it to the list of exceptions not to return authors for - I had already excluded my own domains and blank urls.

The current (sanitised) version of the directory page template is available on GitHub.

Lightbulb moment

After making these changes I realised there should actually be an easier way, or a more streamlined one at least.

What if when a comment is received we immediately perform the linkbacks type check and, if true, rewrite the comment_type value back to the comments table in the database?

But that's a project for another time.

Improving the webmentions directory

Getting plugged in – part 3: getting it wrongComments

A big part of learning by doing is getting things wrong. You're never going to get it right first time, every time and just have to accept that.

It's what prompted me to write that I needed a second WordPress installation to test against rather than keep breaking the blog.

But, as well as getting it wrong, we need to be willing to admit that we got it wrong.

And that's where I am at the moment.

The basic plugin is in and working, and hopefully a little bit more secure than it was, so I now I need to set priorities for the next steps.

The two main to-dos on my original list are:

  • register the 'liked' and 'reply' custom fields so always available
  • add a settings page

i think i was looking at the custom fields the wrong way as I haven't found a way to make an unpopulated entry be always available in the drop-down. It appears that items only exist in the custom fields drop-down if they are currently part of a key:value pair in the database.

As the plugin removes the target URL when the post is updated the options are no longer available.

I could be wrong. (Again.)

Custom field vs meta box

Off in search of another option I wondered about using meta boxes. Surprise, surprise, I was looking at them the wrong way.

I'm detecting a pattern here.

I initially thought that meta boxes were a separate, self contained entity and wouldn't play nice when posting from somewhere like WorkFlow.

But, a bit of research later, and it turns out that meta boxes are actually just a way of displaying the mechanism to add and store post metadata - a custom version of custom fields.

Creation of a metabox may require a little change to the base code but that'll be easy to manage and the next step in the process has essentially been decided for me.

So, no coding in this part, but there are advantages to being wrong.

Getting plugged in – part 3: getting it wrong

On comments and webmentionsComments

In reply to: Depending on how they show up, I'll sometimes take webmentions which show up as "XXX mentioned this on YYY" and change the metadata in wp_comments...

There seems to be a lot of inconsistency in the way webmentions and microformats are handled or implemented between different platforms, especially Known and WordPress.

I've had to dive into the database on a number of occasions to add webmention as the comment type because it has not been detected properly.

While we can do this we certainly shouldn't have to.

On comments and webmentions

I added a filter to functions.php to truncate posts in the RSS feed of type 'status' that were longer than 280 characters, then insert a permalink at the end, so that they would play nicer with

On the one hand it's good for it to be obvious they are longer posts without titles but, at the same time, it comes back to the issues of distribution and whether what we do on our own sites should be dictated by the means of distribution.

I'm torn between thinking "it's a tough call" and "what am I playing at?"


I currently have webmentions enabled for posts from my own site; it serves as a means to highlight relevant posts or, maybe, parts of a thread.

But it almost feels like I'm flying in the face of convention and that most wouldn't do this.

Maybe I'll filter them into their own "relevant posts" section to differentiate them from external mentions.

But, as Tantek Çelik suggested on the Indieweb Slack, they could possibly be used as much more than just relevant posts.

This, however, leads me on to other ideas. A link from one post to another just shows as "mentioned this post" in the list of comments which isn't that illustrative. I could post it as a reply (using my custom fields solution) but what if I wanted the reply link within the body rather than at the start of the post?

Markdown let's you include inline HTML so you could add the u-in-reply-to class to the link but switching to HTML when writing in markdown always feels awkward.

What if Markdown was extended to allow for microformats attributes to be added links?

Instead of just

[link_text_here](url_here "Title")

We could have

[link_text_here](url_here "Title" c:'class_names, comma_separated' r:'rel_type' )

A quick search reveals Markdown supersets like Maruku and Kramdown but these would require a different toolset.


Removing post titles from WordPress RSS feedsComments

I previously detailed a method of automatically replacing blank post titles so that I didn't have multiple items (posted from the app) listed as '(no title)' in the WordPress back end. wants posts to not have titles but will treat certain text as blank, such as the date, hence the above.

If a status coming from a blog (via RSS) has a title, or one not in a recognised format, will treat it as a long form post and just display it as a title and link. Not ideal.

Jimmy Baum (@jimmymansaray) wanted to have just the date as status titles but this falls foul of these limitations.

So, how to allow different title formats on your own blog but then pass data to in such a way it displays those statuses correctly?


WordPress allows you to customise your RSS feed on the fly by accessing different aspects of it and applying a filter. Just like modifying post contents prior to saving or display.

The post title in an RSS feed is logically called using the_title_rss() so we need to find a way to modify this based on the specific set of requirements.

Just as I do, Jimmy uses a category called 'microblog' with a dedicated feed so this is what we need to filter on. After a few tests it seemed the most reliable way was to check against the_category_rss() which lists the categories for the current post as would be displayed in the feed itself, like the below for my previous post:


We can then simply check for the presence of the required category within that string and return an empty title.

The code to do so should be as follows:

function remove_post_title_rss ( $title ) {
  $categories = get_the_category_rss();
  $pos = strpos($categories, 'microblog');
  if ( $pos != '' ) {
    $title = '';
  return $title;

add_filter( 'the_title_rss', 'remove_post_title_rss');
Removing post titles from WordPress RSS feeds