Advertise with Googlier.com Black Hills Web Works https://blackhillswebworks.com Websites for Churches, Nonprofits, and Small Businesses Fri, 27 Sep 2024 18:59:44 +0000 en-US hourly 1 https://media.blackhillswebworks.com/wp-content/uploads/2025/04/28140636/bhww-logo-for-favicon-2025.png Black Hills Web Works https://blackhillswebworks.com 32 32 Stop WordPress spam registrations on a LearnDash site using .htaccess https://blackhillswebworks.com/2024/09/27/stop-wordpress-spam-registrations-on-learndash-site/ Fri, 27 Sep 2024 18:59:44 +0000 https://blackhillswebworks.com/?p=5992 One of my website clients is a couple of Norwegian language college professors who came to me almost ten years ago for help with their existing LearnDash site that they were using as an online language learning tool that was used along with a textbook they had written. I did some custom LearnDash coding (i.e. custom reports) for them for a few years and eventually they had me build and host two new sites to replace their original site.

In mid-2023 they decided to convert one of their sites to an ecommerce site, charging a minimal fee for two years of access to the LearnDash course they had created. By this time LearnDash was more or less able to handle the course registrations and Stripe was tasked with collecting the fees.

Dealing with spam registrations – round one

Part of the ecommerce conversion process was cleaning out several thousand spam registrations that the site had accumulated over the years. The code I wrote for that purpose checked to see if a user had ever actually started the LearnDash course, and if they hadn’t, they were deleted from the database. Accounts and data for users who had started the course before the access fee was instituted were not deleted, but those users would still need to pay the fee to access and use the course in the future.

This code still runs whenever a WordPress admin page is loaded, and it allows a new registrant fifteen minutes to complete their payment or else it deletes their user account. The logic here is that spammers are not going to pay the access fee and therefore can be safely deleted.

That solution kept the number of spam registrations from accumulating, but it didn’t prevent them from happening in the first place.

Spammers are very efficient at registering fake accounts on virtually any WordPress site that allows user registrations, and this particular site was getting new spam registrations every few minutes. That may not seem like a problem since they’re deleted fifteen minutes later, but it does put an extra load on the server that doesn’t help matters when LearnDash already pushes a server’s limits further than most other types of websites.

A bigger issue is what this was doing to our email delivery. I use Postmark to handle all the emails that my client websites send out, both transactional and broadcast (i.e. newsletter) emails.

Transactional emails are emails such as password recovery emails, which the spambots trigger shortly after registering with either a fake email address or a stolen email address. Many of these emails bounced, and those that went to actual email addresses were often marked as “spam” by whoever owned those email addresses. Those bounces and spam complaints could or would eventually lead to services like Gmail or Yahoo sending our legitimate transactional emails into their users’ spam folders, which had already started happening. Or potentially even worse, being shut down by Postmark for too many bounces and spam complaints. That would be a significant problem.

So I set myself to working out a better solution.

Dealing with spam registrations – round two

Note: I’m going to use the actual URLs from my client’s site in the following explanations rather than create fake URLs such as https://yourdomain.com. It’s simpler that way.

LearnDash has an add-on plugin available called Integrity, which, among several other useful things, adds reCAPTCHA to the LearnDash login and/or registration forms.

That seemed like a logical first step so I installed that plugin and configured it to work with reCAPTCHA.

Unfortunately, what it ended up doing was preventing actual users from being able to log in, and it didn’t seem to prevent spam registrations because spammers are not using the LearnDash registration form.

What it does do, like most security approaches in life, is keep the honest people honest. With that in mind I have kept reCAPTCHA in place on my client’s LearnDash registration form, since so far it has not stopped any legitimate registrations that we know of. You can see that registration form at https://settigang.com/portal-access/?ld_register_id=580.

I should explain that URL. The page https://settigang.com/portal-access/ is a WordPress page. In the LearnDash Registration/Login settings there is a setting for the Registration/Login page. When you tell LearnDash which page to use as the registration and login page, LearnDash appends the parameter for a particular course ID to the end of that page’s URL. In this case, that parameter is ld_register_id=580 because the course ID is 580.

Spammers, on the other hand, go directly to the default WordPress registration form found on any WordPress site that has the setting “Anyone can register” enabled. That registration form is found at the URL https://settigang.com/wp-login.php?action=register.

If you were to click that link right now you would be emulating what a spambot does, and you would see the result of the code I’m going to explain next.

Dealing with spam registrations – round three

My next thought was to block access to https://settigang.com/wp-login.php?action=register using the .htaccess file included in any WordPress installation on an Apache server, normally found in the root directory where WordPress is typically located.

The text, or code, in the Gist below when added to your .htaccess file will block all access to that WordPress registration form.

Gist – Block all access to the WordPress default registration form

That worked, but it worked too well, as it also blocked legitimate registrations from real students. This puzzled me at first because we were using the LearnDash registration form, until I tried to register as a student and kept watching the address bar in my browser.

If you were to fill out and submit the registration form at https://settigang.com/portal-access/?ld_register_id=580 while watching the address bar, you would see the URL https://settigang.com/wp-login.php?action=register appear momentarily before being redirected to https://settigang.com/portal-access/?ld-registered=true&ld_register_id=580 where you could then complete your registration for that course by paying the course access fee.

I realized then that LearnDash is utilizing the default WordPress registration form as part of their registration process.

Dealing with spam registrations – round four

The next idea I had, and the one that worked and is still working, was to add an HTTP_REFERER condition to that .htaccess text.

This next Gist is the final working product. What it’s doing is checking to see if the previous URL is the page with the LearnDash registration form on it. If it isn’t, as in a spambot going directly to that WordPress registration page, the server returns a 403 status code (403 Forbidden).

The result? Zero spam registrations since implementing this seemingly simple fix, while still allowing all legitimate registrations by real people. And as a bonus, the bounce rate and spam reputation for this particular site is now trending back down to zero, where it should be.

Gist – Block all access to the WordPress default registration form unless referred there by a specific page

Feel free to adapt that .htaccess code for your situation, changing the URL accordingly.

Note: I’ve left the first RewriteCond line in that code but commented out (with an #) as a reminder to me and to you that leaving the LearnDash parameter appended to the referrer URL does not work.

A word about .htaccess

Your .htaccess file is powerful. What you do there affects your entire website. Before making any changes to that file you should have a backup copy, and you should probably even backup your entire website and database.

Having said that, the .htaccess file is not voodoo or black magic. You don’t have to be afraid of it. Experiment and learn!

]]>
Automatically clean Action Scheduler database tables and improve your WordPress website https://blackhillswebworks.com/2024/08/29/automatically-clean-action-scheduler-database-tables/ https://blackhillswebworks.com/2024/08/29/automatically-clean-action-scheduler-database-tables/#comments Thu, 29 Aug 2024 18:05:47 +0000 https://blackhillswebworks.com/?p=5950 Keeping the Action Scheduler database tables cleaned out and free of excess unnecessary data can help your WordPress website run better and faster. You could remove the unnecessary data manually, but why not do it automatically by implementing the code in this blog post?

What is Action Scheduler?

Action Scheduler is a tool developed and maintained by Automattic. It is typically included as a library with plugins such as WooCommerce, MailPoet, Solid Backups (formerly known as BackupBuddy), and many others. It can also be installed as a plugin, but that’s not as common.

Plugins use Action Scheduler to schedule all kinds of background processing tasks, from backups to recurring payments to general housekeeping tasks. Your WordPress website is probably using Action Scheduler for something, and if it is you can find it in the WordPress menu under Tools -> Scheduled Actions.

Action Scheduler and your database

Action Scheduler adds four tables to your database:

  1. actionscheduler_actions
  2. actionscheduler_claims
  3. actionscheduler_groups
  4. actionscheduler_logs

Left to its own devices, Action Scheduler cleans the actionscheduler_actions and actionscheduler_logs tables every minute, removing any actions and their associated logs that have been in those tables longer than thirty-one days, and it does this in batches of twenty actions at a time.

The problem I had (and you may also have) is that by default it only cleans out actions (and their associated logs) that have the status “complete” or “canceled.” Actions marked as “failed” remain in the tables indefinitely. I suppose that makes some sense, as it gives the site admin a chance to look into why those actions failed. However, some or many of those failed actions are often corrected when their associated plugins are updated, and therefore they no longer need to be kept in the database.

The ever-zealous MailPoet plugin

An issue I had with several websites is that MailPoet, the email newsletter and marketing plugin, has two actions that run approximately every two minutes, mailpoet/cron/daemon-trigger and mailpoet/cron/daemon-run.

As I understand it, mailpoet/cron/daemon-trigger tells mailpoet/cron/daemon-run to run, and when mailpoet/cron/daemon-run is finished, which may be a few seconds to a few minutes later, mailpoet/cron/daemon-trigger is again scheduled for two minutes after that.

This isn’t exact due to the variance in run times for mailpoet/cron/daemon-run, but somewhere in the neighborhood of one thousand completed actions are added to the database every day, and if the scheduler retains completed actions for thirty-one days, which it does by default, then that amounts to thousands of completed actions and their logs continually sitting in your database. Each one of those MailPoet actions adds three additional rows to the actionscheduler_logs table.

This screenshot of an Action Scheduler admin page shows the two MailPoet actions, their associated log entries, and their overall effect on the database with 23,006 actions perpetually taking up space in the database for this particular website example.

Screenshot of Action Scheduler admin page before cleaning the database

Screenshot of Action Scheduler admin page before cleaning the database, with 23,006 actions just sitting there, taking up space.

This all adds up to MiBs of useless data sitting in your database taking up space, making your database bloated and potentially slowing down your website. (A mebibyte (MiB) is a multiple of 1,024 and a megabyte (MB) is a multiple of 1,000. Databases, at least the MySQL databases I use, use MiBs instead of MBs.)

The two screenshots below from PhpMyAdmin show the Action Scheduler database tables before and after implementing the code I’ve documented in the code section further down this post.

Screenshot of Action Scheduler database tables BEFORE cleaning

Screenshot of Action Scheduler database tables BEFORE cleaning

Screenshot of Action Scheduler database tables AFTER cleaning

Screenshot of Action Scheduler database tables AFTER cleaning

Why does this matter?

As websites continue to get more and more complex, their databases tend to grow along with that complexity. The size and complexity of your database can and does affect the speed of your website for your users.

There are support threads on the WordPress forums and elsewhere of people dealing with Action Scheduler database tables filled with GBs of unnecessary data.

One issue I ran into was with backups made by Solid Backups (formerly BackupBuddy) scheduled to run in the background. There are times when Solid Backups fails to complete a scheduled or manual backup due to the size of a database. Keeping the size of the database as small as possible can only help in that regard.

Another issue with Solid Backups is that the default advanced settings for that plugin have it running a PHP runtime test and a PHP memory test on a weekly basis. By design these two tests will always result in a “failed” status in Action Scheduler—and by default, Action Scheduler does not remove “failed” actions.

How to improve Action Scheduler – the code

Gist – Automatically clean Action Scheduler database tables and improve your WordPress website

The code explained

The first filter adds the “failed” status to the default Action Scheduler statuses of “complete” and “canceled” that are removed or purged during the routine cleanup process.

The second filter changes the number of actions that are purged each time the cleanup process runs. The default batch size is 20, but I’ve changed that to 100. You could use any number you want, but the larger the number the longer it will take, and you risk running out of server memory if you go too big. You could also use a larger number for the initial cleanup process and then change it back to a number like 100 when you have matters under control.

There’s another filter available that I’ve opted not to use that allows you to change the retention period from thirty-one days to whatever interval you prefer. I decided to leave that interval alone except for actions related to specific plugins, which is what the next bit of code does.

The rest of the code consists of two functions that work together to let you customize which actions are purged and how often, if you don’t want to wait for Action Scheduler’s default thirty-one day retention period.

The default retention period in my code is one day (86400 seconds), and in my working example I’ve left that default retention period for Rank Math SEO actions and set the retention period for MailPoet actions to three days (259200 seconds). Both the actionscheduler_actions and actionscheduler_logs tables are purged of the actions and their logs in one operation.

The functions are hooked to the wp_loaded action hook, which fires after WordPress is fully loaded. Anytime a page loads, either on the front end or in the admin, the code will run. So the code runs “automatically” as long as your website is getting some traffic, or you or another user is accessing the back end of the website.

Where to put this code

Add the code to a core functionality plugin (the best choice), its own plugin, or your theme’s functions.php file.

Modify the code to fit your needs and desires, as well as any other plugins you may be using that are filling up your Action Scheduler database tables with unnecessary data.

And finally, make a backup of your website and its database before introducing any new code or significant changes.

]]>
https://blackhillswebworks.com/2024/08/29/automatically-clean-action-scheduler-database-tables/feed/ 6
Automatic bounce and complaint management using Postmark to send MailPoet (and other) emails https://blackhillswebworks.com/2023/04/25/automatic-bounce-complaint-management-using-postmark-to-send-mailpoet-emails/ Tue, 25 Apr 2023 20:21:07 +0000 https://blackhillswebworks.com/?p=5737 I see that it’s been eight years since my last blog post on this website, a tutorial post which also happened to be about MailPoet. I have eleven other blog posts that I never actually started, or started and did not finish, that are still in the draft stage. All that to say that I am still building WordPress websites, but life got too busy for regular and consistent blogging.

Anyhow, while MailPoet has its quirks and issues, I still think it’s the best option for sending newsletters, post notifications, and other similar emails from WordPress.

A few months ago I was getting several client websites set up for sending emails and thought I’d try the MailPoet Sending Service instead of Amazon SES, which I’d used for years. The main draw to MailPoet’s sending service for me was the automatic bounce and complaint handling in the MailPoet back end of your WordPress website. Not having to manually update multiple bounced subscribers each time a newsletter goes out is a big plus.

However, for one of those clients we were sending out a newsletter letting their users know about their redesigned website, and when we started sending the emails we were quickly shut down by MailPoet because of too many bounces. Now granted, I should have used an email list cleaning service such as emailable before we started sending, but that didn’t happen for whatever reason.

The problem, which is what inspired this solution and blog post, was that there is no readily accessible way to know which emails were sent, which emails were not sent, or any other detailed information about the emails. I did contact MailPoet and someone there did eventually send me two CSV files, one that listed “discarded” emails, and another that listed “not delivered” emails. That information helped, but I had to wait three days to get it.

Enter Postmark

I’ve been using Postmark to send transactional emails from all my client websites since 2019. The price is right ($10 per month for up to 10,000 emails), they have a WordPress plugin, and the detailed information for each email that is sent can be very helpful. That detailed information includes events such as processed, delivered, opened, and clicked.

In late 2020 they added broadcast emails to their available offerings, but I didn’t start using that feature until a few months ago, as mentioned earlier in this article. The challenge or hurdle that I really wanted to overcome was linking MailPoet with Postmark so that bounces and complaints were handled automatically in MailPoet.

Why do this?

There are lots of good reasons. Here are three:

  1. Automatic bounce, complaint, and unsubscribe handling within MailPoet, without using the MailPoet sending service. (Sorry MailPoet!)
  2. Let’s say you have a WooCommerce (or other ecommerce) site, and a customer enters their email address incorrectly and their order confirmation email bounces. You will be notified by email and you will have the opportunity to edit the order details and potentially fix the email address they entered by mistake.
  3. I have a client whose Google Account email stopped working due to the payment credit card expiring that Google had on file. When her website tried to send her new order notification emails they bounced, and I was notified. I was then able to let her know via phone that her email was no longer working.

One caveat with Postmark

Postmark automatically includes an unsubscribe link to each Broadcast email that suppresses that email address in their system. So if you include a MailPoet unsubscribe link in the footer of your email, there will be a second unsubscribe link at the very bottom of the email. No big deal, and if someone clicks that Postmark link rather than the MailPoet link, they will still be unsubscribed in MailPoet if you implement the code and process described below.

Not just for MailPoet emails

I’m using this code and process on all my client websites that send emails for any reason, not just MailPoet emails. As I mentioned above in the “Why do this?” section, this lets you be alerted to any email delivery issues.

The code

Gist – Automatic bounce and complaint management using Postmark to send MailPoet (and other) emails

Where to put this code

Add the code to a core functionality plugin (best choice), its own plugin, or functions.php.

Steps to implement this process

  1. Modify the above code as follows:
    1. Change website URL in the function documentation block
    2. Change the $site variable in email information
    3. Change the $to variable in email information
    4. Temporarily uncomment error_log() in four places
  2. Optional: In wp-config.php temporarily enable (by setting to “true”)
    1. define( ‘WP_DEBUG’, true );
    2. define( ‘WP_DEBUG_LOG’, true );
    3. define( ‘WP_DEBUG_DISPLAY’, false ); // If this is set to true then the errors will be visible on the front end to everyone
  3. Go to https://account.postmarkapp.com/servers and click on the Postmark server that you’ll be connecting to.
  4. Click on Broadcast emails, then Webhooks, and then the Add webhook button.
    1. (You may need to create the Broadcast emails message stream if you don’t see it.)
  5. Add the webhook URL. For example:
    1. https://blackhillshoneyfarm.com/wp-admin/admin-post.php?action=process_postmark_events
  6. Check the event boxes for:
    1. Bounce
    2. Spam Complaint
    3. Subscription Change
  7. Test all three events using the “Send test” button and feature on that page.
  8. Optional: If debugging is set to true (see step 2 above) check the debug.log in the wp-content directory.
    1. If there’s nothing there, you probably have an admin redirect in place. If so,  add this to the redirect conditional:
    2. && ‘/wp-admin/admin-post.php’ != $_SERVER[‘PHP_SELF’]
  9. Now repeat steps 4 through 8 for Transactional emails.
]]>
MailPoet responsive newsletter template quick fix https://blackhillswebworks.com/2015/03/27/mailpoet-responsive-newsletter-template-quick-fix/ https://blackhillswebworks.com/2015/03/27/mailpoet-responsive-newsletter-template-quick-fix/#comments Fri, 27 Mar 2015 23:03:44 +0000 http://blackhillswebworks.com/?p=5648 Email newsletters fall into the “necessary evil” category, in my opinion. I use them here on this blog, and I have clients who want and/or need them for their websites, but what a pain to get working right, especially with WordPress.

Yeah, I know, I could use, and have used, MailChimp or one of the other heavy hitters, but I’m stubborn enough to want this to work with WordPress, and not have to depend on an email service like MailChimp.

MailPoet, SendPress, or Sendy?

I’m going to stick my neck out and say that the only real contenders in the WordPress email newsletter arena are MailPoet (formerly Wysija) and SendPress. You don’t have to agree with me on that one, but this is my blog. 🙂

I currently use MailPoet on this website, though their security snafu in mid-2014 didn’t make me too happy. Everybody makes mistakes, and to their credit they fixed that problem in a timely manner.

I purchased, tried, and still want to like SendPress. Neither MailPoet nor SendPress follows the WordPress admin API very well, but I think SendPress does a better job than MailPoet with their admin. The guys behind SendPress seem motivated, though their version iterations come and go in waves, which is to be expected with a group of guys who also have day jobs – can’t blame them for that! My main problem with SendPress is that their version releases tend to be buggy, and while they usually push out a fix in a short time, I need something I can use with paying clients that’s more dependably professional.

Sendy seemed like a good stand-alone, separate-from-WordPress option. I purchased and installed it on my server, set it up for one of my ecommerce clients, worked up a responsive template, and sent out an email campaign. I have no complaints with how Sendy performs, but it doesn’t integrate (yet?) with WordPress, and that is still a problem.

Needed to make a decision

My current client project is for a large corporation that sends out press releases, SEC filings, and other items on a regular (monthly?) basis, and they’ve asked about notifying their investors via email when these items are published. That project is quickly drawing to a close so I needed to make a decision, and after weighing all the pros and cons, I finally chose MailPoet for this project.

MailPoet does a good job of setting up and sending automatic emails (newsletters) for posts and other custom post types. In this case I set up three different email lists, one each for regular posts and two custom post types. Nice!

Then I started testing the emails and, as I expected, they were not “responsive,” meaning they didn’t look so good on my Android phone, and they looked even worse on my wife’s iPhone 6. MailPoet has been saying for a year or two now that responsive templates are coming, though I can’t imagine why it would take so long to integrate. Time to dig in to the code and see what we can do about this.

Easier than I expected it to be

There is (was) a thread on the MailPoet UserVoice forums that has to do with the lack of a responsive email template, and one of the commenters listed all the files she had to modify to achieve a responsive template. The problem with this approach, as you probably know, is that your modifications won’t survive a plugin update. As I write this post MailPoet is currently at 2.6.15.

I wanted to make this easy on my future self, so after looking through the plugin and reading some code, I selected email_template.html, made a few changes, sent out a test email, and there it was, a mostly responsive email. I say mostly because if you want to do a better responsive job with images you should probably modify the css.html file too.

What needs to change

MailPoet thinks, or thought, that an email should be 600 px wide, no matter what device the email is being viewed on. I, and many others, disagree. So what you’ll need to change are six instances where they set the table width at 600, and change those to 100%. You’ll also want to change style=”min-width:600px;” to style=”max-width:600px;width:100%;” in the td tags, and add max-width:600px; to the main table tag inline style.

Images can be dealt with, more or less, by adding max-width:100%; to the img rule in css.html.

I’ll include the modified email_template.html and css.html files in their respective Gists in this post. You can just copy my code over to your files, though it wouldn’t hurt to read through the code, see what changed, and file it away in your memory.

The two files that need to be modified are:
wp-content/plugins/wysija-newsletters/tools/templates/newsletter/email/css.html
wp-content/plugins/wysija-newsletters/tools/templates/newsletter/email/email_template.html

The Caveat: There had to be one, and it is that I’m using the default email theme. If you’re using something else, you may need to alter these directions. In other words, your mileage may vary.

Bonus: What about HTML tables in the emails?

My client occasionally sends out financial data in their press releases, using HTML tables, and those really needed to be in the emails also. MailPoet was stripping out the table, tr, td, and th tags, and fortunately they provide a filter to alter what is stripped out of your emails. That filter is mpoet_strip_tags_ignored, and you can find it in wysija-newsletters/helpers/articles.php. As a bonus I’ll include a Gist for that too.

The HTML tables aren’t responsive, but I’m okay with that at this point since the majority of the time there won’t be tables in the emails.

Add the filter code to a core functionality plugin (best choice), its own plugin, or functions.php.

The Code

Gist – Modified pseudo-responsive MailPoet email_template.html


Gist – Modified MailPoet css.html

Gist – Filter MailPoet tags not being stripped

]]>
https://blackhillswebworks.com/2015/03/27/mailpoet-responsive-newsletter-template-quick-fix/feed/ 53
Comparing LearnDash and Sensei – WordPress LMS lessons learned the hard way https://blackhillswebworks.com/2015/01/23/comparing-learndash-and-sensei-wordpress-lms-lessons-learned-the-hard-way/ https://blackhillswebworks.com/2015/01/23/comparing-learndash-and-sensei-wordpress-lms-lessons-learned-the-hard-way/#comments Fri, 23 Jan 2015 22:21:01 +0000 http://blackhillswebworks.com/?p=5592 Note: Edited September 16, 2015

In late 2014 I built a WordPress Learning Management System (LMS) website for a client using the LearnDash LMS plugin. Two months later I rebuilt the site using the Sensei LMS plugin from WooThemes.

Read on to find out why…

Comparing LearnDash and Sensei - WordPress LMS lessons learned the hard way

A little background to this project

The client in this story is a creative agency that designs and manages projects for, among others, nonprofits who are funded by a foundation. In early 2014 they redesigned and I developed a site that we had originally built a few years earlier for one of those nonprofits. This time, however, we also built a matching Moodle site to go along with the WordPress site, which the nonprofit uses to educate its members.

This post isn’t about Moodle, but let me say this: My experience building and working with Moodle leaves me convinced that whatever Moodle can do, WordPress can do it better and more efficiently. That’s a strong statement, and I know people are going to disagree with me, but I’ve worked with both and that is where I stand. (Interesting tidbit: I just now noticed that our local high school is using Moodle.)

Fast forward a few months and my client asked me to research and find the best WordPress LMS plugin to use for an upcoming project – the project I described in the opening paragraph of this post.

Research

As I researched what was available for WordPress LMS plugins it became obvious that there are (were?) three major players in this field: LearnDash, Sensei, and WP Courseware.

During this process I leaned heavily on Chris Lema and his LMS research. Chris has written a number of posts on the topic, including eLearning on WordPress: Comparing WP Courseware and LearnDash. In that post he explained how he had observed that these three LMS plugin providers had different approaches to creating their plugins.

According to Chris, WooThemes are, “WordPress guys first, plugin guys second, and eLearning guys last,” while LearnDash are, “eLearning first, plugin second, and WordPress third.” Then he said, and this is important: “And that will have an impact on how you see, experience and value what they’ve done.” Looking back, I wish I had read between those lines a little harder. But if I had, I wouldn’t be writing this post. 🙂

Research conclusions

For a variety of reasons, LearnDash is the LMS I decided to go with for this project. It had a more mature feature set, they were actively developing and releasing new features, they have an active user community and forum, and their client list is impressive. On top of that, the license is good for life. (At least it was then, if I’m remembering correctly. I see now that their license is good for one year – that’s a smart move!)

Sensei at that time was having some performance issues with its analyzing and reporting, its feature set wasn’t quite up to LearnDash, and they were still relatively new.

Looking back, I’m not sure why I didn’t pick WP Courseware. I think the guys at Fly Plugins are working hard to produce a good product, but for a reason that escapes me now, I didn’t pick them.

Unique project requirements and custom coding

One unique requirement for this project was that they wanted and needed to use the LMS for both online and in-person courses, and they had to be able to track their users’ progress through different custom stages as they completed various courses. I have yet to build a WordPress project that didn’t involve some custom coding, and it seems that my client projects are becoming more complex, with more custom coding to go with that trend. This doesn’t bother me at all…in fact, I enjoy challenging projects and the learning that comes with them.

The WordPress long-standing motto is “Code is poetry.” I like this motto, and I like to think that the code I write contributes to the global body of WordPress code poetry. This is going to sound harsh, and I think this may be what Chris was hinting at between the lines that he couldn’t just come out and say: the LearnDash code is not poetry. It’s effective, mostly, which I’ll get to later, but it’s not poetry. My impression of the LearnDash code is that it was written in a hurry, probably by more than one person, and they weren’t overly concerned with clean or consistent code.

Note: As of LearnDash version 2.1, released on September 16, 2015, LearnDash has a completely rewritten code base, and it’s looking good! 🙂

I’m speculating here, but I think one reason for that style is that they aren’t weren’t charging enough for LearnDash. I may be wrong, but a lifetime plugin license doesn’t bode well for sustainability. Sure, it’s more affordable for the end user, but I’d rather pay for something that’s making enough of a profit to ensure that it will be supported and available for as long as I or my client is going to need it.

Project: Phase 2

A couple months after we launched this project, the number of users was over 600, and my client’s client wanted to make some enhancements and improvements. They wanted:

  1. better and more reporting options,
  2. a way for their site admins to mark courses complete for users in the WordPress admin,
  3. and emails sent to users upon course registration and a reminder email sent one week before a course starts.

LearnDash has the ProPanel add-on which has a cool UI (user interface), but it also has some problems. As I was moving data for all those users from LearnDash into Sensei, I realized that the ProPanel reports were not accurate. I was comparing each course’s list of user IDs, the ProPanel dashboard report, and a database export, and what I found was that there were users listed in the reports who were not in the courses, and there were users in the courses who weren’t listed in the reports. That’s a problem that no amount of fancy UI can compensate for.

A feature that had been requested more than once in the LearnDash forums was being able to mark courses complete in the admin. Each time it was requested LearnDash stated that this was not available, and it wasn’t on their feature calendar.

Marking courses complete in LearnDash

Emails were limited also, and I knew that I’d be doing a lot of custom coding to make all of these requests a reality.

Which leads me (back) to WooThemes. Six years ago when I started building websites I used Joomla!. WordPress was still largely a blogging platform then, but it was beginning to mature into the CMS it is now. After a year or so of Joomla! I switched over to WordPress and have never, ever regretted that move.

I discovered WooThemes at that time and modifying their themes is how I got started in PHP. I eventually switched to the Genesis framework, and again, I have no regrets about that move. Having said that, I know that the guys at WooThemes do write good code, and after my experience with the LearnDash code I decided to purchase Sensei and give that a try for this project – even if it meant rebuilding part of the site and moving a lot of data from one LMS to another.

The Sensei knows

I set up a staging site using a copy of the project’s live site, installed Sensei, and got started. What a relief when I dived into the code and realized what I already suspected: the Sensei code is clean, well-written, well-organized, and developer-friendly. Yes!

Working with clean code made my job easier, and I finished the custom requirements for this phase of the project and had a good time doing it.

Apples to apples

This won’t be as thorough as it could be, but for the rest of this post, I’ll make some side-by-side comparisons of LearnDash and Sensei in key areas so you can see how they compare. These are areas that involved this project directly, and they may or may not matter for your project.

Creating Courses, Lessons, and Quizzes

LearnDash

Creating your course content (lessons, quizzes, questions) in LearnDash is going to take some clicking around. Especially the quizzes and questions. Creating a quiz is going to involve scrolling through a long page of options. LearnDash does have more types of questions available than Sensei does, but they also utilize WP Pro Quiz, a third-party vendor, to handle the quizzes.

Sensei

Creating a course with lessons and quizzes in Sensei is easy. Each course admin page has a list of its lessons at the bottom of the page, and each lesson has its quiz, if there is one, on that lesson’s admin page. Sensei does utilize a question bank that, in theory, lets you re-use questions on different quizzes. In my experience, re-using questions leads to question answers showing up in other quizzes that also use those questions. I’ve filed a bug report with Woo, but haven’t heard back yet that this has been fixed.

Reporting

LearnDash

Without the ProPanel add-on, LearnDash reports are limited. In fact, there’s very little data available in your browser, and you’ll need to use their export feature to see and work with your data.

LearnDash report options

With the ProPanel add-on, you have more reporting options. The UI looks good, but as I mentioned earlier, the reports are not accurate. I did not go into the code to understand why, because by this time I had already decided to switch to Sensei.

Sensei

Sensei uses (extends) the WP List Table class, which was a good move, in my opinion. Using a WordPress core class for something like this means that end-users and/or developers can build on what they have done. As my client requested, I was able to build custom reports by extending Sensei’s extension of WP List Table, and add a custom jQuery date filter as well.

Sensei custom reports filtered by date

Users and their courses

LearnDash

One thing we noticed right away was that Editors and Administrators are automatically registered for every course on a LearnDash site. Our project had, at that time, 95 courses. Every course a user is registered for shows up in their profile, which makes for some long, awkward profile pages for the Editors and Admins. This also skews the ProPanel course reports, since those Editors and Admins are included in the data for each report.

I have no idea why they decided to work it this way, but they explain on the LearnDash forums that this is how it is. Maybe a work-around for course viewing permissions?

Sensei

No issues here. If you haven’t registered for a course, it does not appear on your profile, no matter what user role you have.

Documentation

LearnDash

There is documentation, but it could be more complete. I noticed that there are some questions asked by users on the LearnDash forums over and over, and I wondered why they didn’t just address those questions in the documentation. Maybe they’re too busy? Very possible – I know how that can be!

Sensei

Sensei’s docs are thorough, and probably cover more topics than most end-users will need. Still, I did find some areas that need updating to match the current version of the plugin.

Support

LearnDash

LearnDash uses a combination of FAQs, forums, and a ticket system. The link to the ticket system can be hard to find – maybe so user’s utilize the forum first? Justin, one of the the co-founders, is making a heroic effort to keep forum questions answered, but as LearnDash grows into the future, they may not be able to sustain this support model.

Sensei

If you want true WooThemes support, you’ll need to use their ticket system. There is a community forum, and the Sensei devs and product lead do venture in occasionally, but they don’t guarantee that your forum question will be answered by them, or anyone else for that matter.

My conclusion

Both LearnDash and Sensei are heavy hitters in the WordPress LMS arena. If you’re okay with using a plugin “out of the box” and you know you won’t need or want to do any customizing, then either one will probably work for you – but do your homework regarding the specific features offered by each plugin, and take their demos for a spin.

Even though I ended up doing a lot more work than I originally intended, I am glad that I used LearnDash first, as exposure to different ways of solving a problem can only have good results in the long term. Adding future features on their current code base is going to be a challenge, and I wouldn’t be surprised if at some point in their development progress they do a complete rewrite of the LearnDash code. I would if I were them.

Note: (I added this earlier in the post, but I’ll add it here too.) As of LearnDash version 2.1, released on September 16, 2015, LearnDash has a completely rewritten code base, and it’s looking good! 🙂

Sensei is the relative newcomer, but from what I’ve seen so far, and knowing what WooThemes is capable of, I think this plugin is going to go far in an industry that is growing by leaps and bounds.

So what should you take away from this? When you’re choosing a plugin that plays a fundamental part in your website’s functionality, do your homework, and make sure you look under the hood!

]]>
https://blackhillswebworks.com/2015/01/23/comparing-learndash-and-sensei-wordpress-lms-lessons-learned-the-hard-way/feed/ 92
How to add multiple product tabs to WooCommerce 2.x https://blackhillswebworks.com/2014/06/14/add-multiple-product-tabs-to-woocommerce-2-x/ https://blackhillswebworks.com/2014/06/14/add-multiple-product-tabs-to-woocommerce-2-x/#comments Sat, 14 Jun 2014 18:34:07 +0000 http://blackhillswebworks.com/?p=5453 WooThemes and WooCommerce ninjaI’m currently wrapping up a new WooCommerce-powered ecommerce website for a client, and thought that I’d share the code I’m using to add multiple new tabs to the product pages.

When I originally went looking for a solution to this challenge I found a couple blog posts by Sean Barton that pointed me in the right direction: How to remove the WooCommerce 2.0+ Reviews tab and Adding your own tabs to Woocommerce and Woothemes. His posts explain in detail how the WooCommerce tab system works, so I won’t try to duplicate that here in this post.

Unless I missed it, his posts didn’t explain how to add multiple tabs, which is what I needed to do for this project. After coming up with my solution and while writing this post I did notice that he has released a plugin for managing multiple tabs. I haven’t tried it, but it’s there if you’d rather use a plugin instead of working directly with the code.

“Ingredients” and “Benefits” tabs

For this project, the tabs I needed to add were “Ingredients” and “Benefits,” as you can see in the screenshot below. The code I’m posting here uses those two labels, and you’ll need to make adjustments to the code as necessary to suit your project.

WooCommerce product with multiple tabs

Add your metaboxes or custom fields first

There’s more involved here than just adding a tab or two, since you also need a way to add content to those tabs. To do that I’m using Custom Metaboxes and Fields for WordPress. I’m not going to go into detail about how to use CMB as there is documentation for that already available, but I will post the specific code that I’m using along with CMB to add custom metaboxes to the WooCommerce product post types.

The first Gist posted below shows how I add the metaboxes to the product post type. For the sake of making it simpler for my client to create and edit the content for those tabs, I’m using the WYSIWYG metabox. This screenshot shows the metaboxes in place on a product edit page.

WooCommerce custom metaboxes

Now add the product tabs

The second Gist posted below shows the code to add the tabs to your products on the front end. Some things to note:

  1. If the custom metabox that populates a tab is empty, then that tab will not be displayed on the front end.
  2. You can rearrange the tabs by changing their priority.
  3. I’m using several functions to sanitize, format, and display the tab content: esc_textarea, htmlspecialchars_decode, and wpautop. If you know of a better or simpler way to do this that still works on the front end, feel free to let me know in the comments below.
  4. After a comment and a question (see below), I’ve updated the code to use apply_filters( 'the_content', $product_ingredients ) instead of the previous combination of functions described in step 3.

The Code

Gist – Add custom metaboxes to WooCommerce products

Gist – Add multiple product tabs to WooCommerce 2.x

Where to put this code

Add the code to a core functionality plugin (best choice), its own plugin, or functions.php.

]]>
https://blackhillswebworks.com/2014/06/14/add-multiple-product-tabs-to-woocommerce-2-x/feed/ 47
Notes to self on January 24, 2014 https://blackhillswebworks.com/2014/01/24/notes-to-self-on-january-24-2014/ Fri, 24 Jan 2014 16:39:12 +0000 http://blackhillswebworks.com/?p=5309 Notes to selfThree new and/or new-to-me plugins: Custom Post Widget, Stream, and WP Document Revisions. Adding Dashicons to your custom post types.

Plugins

Custom Post Widget – I’ve been looking for this plugin for several years, though I didn’t know it. This plugin solves the problem of using a variety of content in your text widgets, and giving your clients the ability to edit that content without being admins and having access to the Widgets area. Awesome.

Stream – Tracks and monitors everything that happens on your WordPress website. I’ve installed this on several sites and am giving it a test. I like what I see so far compared to other logging plugins I’ve tried in the past. I like it a lot, actually.

WP Document Revisions – I’m currently building an intranet for a large corporation and they needed a secure way to manage their downloadable files. I originally started using Premise to do that, but this plugin runs circles around Premise when it comes to how secure documents are handled. Some folks on this plugin’s support forum are saying it doesn’t work with WordPress 3.7 or 3.8, but I haven’t had any problems with it yet. Highly, highly recommended.

Resources

Dashicons – Need a WordPress 3.8-flavored icon for your custom post type? Check out the Dashicons website for the icons and their names, and then add that to your code when you register your custom post type.

]]>
Notes to self on January 10, 2014 https://blackhillswebworks.com/2014/01/10/notes-to-self-on-january-10-2014/ https://blackhillswebworks.com/2014/01/10/notes-to-self-on-january-10-2014/#comments Fri, 10 Jan 2014 16:19:43 +0000 http://blackhillswebworks.com/?p=5290 Notes to selfA WP Elevation interview with Cory Miller, and StatusCake as an alternative to Pingdom. The past few weeks have been really busy around here, so not much time to find stuff for this post series. 🙂

Inspiration

WP Elevation Episode #14 – Cory Miller – Great interview by Troy Dean with Cory Miller of iThemes and BackupBuddy fame. If your business involves the internet, websites, plugins, WordPress, etc., then this is a highly recommended interview to listen to or watch. BackupBuddy is the single most important plugin on any website I build, and hearing the history behind it was enlightening!

Resources

StatusCake – Earlier this week I was looking for a possible alternative to Pingdom and came across StatusCake. While Pingdom only allows you to monitor one website for free before moving to a paid plan, StatusCake has an unlimited free plan, and their paid plans are more affordable than Pingdom. Beyond the pricing differences, they claim to have significant advantages over Pingdom.

]]>
https://blackhillswebworks.com/2014/01/10/notes-to-self-on-january-10-2014/feed/ 1
Notes to self on December 20, 2013 https://blackhillswebworks.com/2013/12/20/notes-to-self-on-december-20-2013/ Fri, 20 Dec 2013 16:18:28 +0000 http://blackhillswebworks.com/?p=5221 Notes to selfA way for me to remember some of the better things I found this week on the web. Feel free to follow along.

Inspiration

I work alone, in my home office, and other than the normal sounds of my wife and kids doing their thing around the house, it can get pretty quiet around here. Music can help, but much of what I do requires full-on concentration, and at times music can be too distracting. Enter the ambient noise collection of websites:

Coffitivity – I first heard about Coffitivity not long after they got started, and I use it from time to time when I’m not listening to music. They now have three different recordings to choose from, along with a collection of apps to take Coffitivity with you.

Noisli – Lots of variety here: rain, thunder, wind, forest, leaves falling, a water stream, ocean waves, water, bonfire, summer night, coffee shop, white noise, pink noise, and blue noise. Play one, some, or all at the same time. And if that collection of sounds wasn’t enough, the background color of this site is constantly changing, creating an interesting chromotherapy effect.

Rainy Cafe – Two options with Rainy Cafe, a cafe and rain (a thunderstorm, actually). You can play one or both and mix the volume levels of each. Really like the thunderstorm on this site.

Plugins

Video User Manuals – I mentioned this plugin last week, but they’re currently running a big Christmas sale that ends 15 hours from now – $97 per year instead of the usual $240. I’ve just started using it and am now setting it up in my client sites. The videos aren’t perfect, but nobody’s video tutorials are. A nice feature is being able to set a master profile that you can then use every time you install it, saving you the time to set it up and choose which videos to display.

]]>
Notes to self on December 13, 2013 https://blackhillswebworks.com/2013/12/13/notes-to-self-on-december-13-2013/ Fri, 13 Dec 2013 16:00:16 +0000 http://blackhillswebworks.com/?p=5199 Notes to selfA way for me to remember some of the better things I found this week on the web. Feel free to follow along.

Inspiration

Basecamp – I realize there are a lot of Basecamp “haters” out there, and I had sort of given up on Basecamp for a while myself, but I’ve recently been flooded with client work and realized that I needed what Basecamp has to offer, which is an incredible communication and collaboration platform that’s really intuitive, especially for non-technical clients. I’m also using Trello, which I really like for my own internal use, and I’m giving Asana an honest try, but Basecamp is still the king in this arena.

Resources

WordPress 3.8 – The big news in the WordPress world is that WordPress 3.8 “Parker” was released on Thursday, December 12. 3.8 comes with a huge admin overhaul that, in my opinion, looks great. Another big improvement is how the widget management works.

Soliloquy – Soliloquy is my default go-to slider when I have a website that needs a slider, and Thomas (the developer) has posted this Gist as a way to dequeue the Soliloquy CSS if you’re so inclined to roll your own slider CSS.

Plugins

Barley for WordPress – An idea and a plugin with lots of promise for being able to edit your posts and pages on the front end of your website instead of in the admin. I had started editing this very post with my copy of the plugin installed here, but then I realized that I couldn’t save the post as a draft, and then I clicked on a link for editing advanced options and lost the changes I had made. As it turns out I’ve got a plugin conflicting with Barley on this site that I haven’t taken the time to hunt down yet, but I will. I then tried it on another site and it worked great.

Video User Manuals – I’ve had my eye on this plugin for a while now as a way to painlessly incorporate training videos into the WordPress admin for my client sites. I’ve been using another plugin for this, but Video User Manuals takes a slightly different approach with more options available.

One important difference is that these videos are shot using an Editor user profile, and not an Administrator profile, which makes perfect sense for client sites. The plugin also “knows” which videos to show based on the version of WordPress your site is using. Nice!

As icing on the cake, I just received a promo email from Troy Dean, founder of VUM, that a big promotion is coming next week. Big as in $97 per year instead of $240. That’s big!

]]>