Tags Archives: Wordpress

How To Reset WordPress Permalinks Using WP-CLI

When transferring a WordPress website from one location or domain to another, or changing from https: to http: your permalinks will probably stop working.

 

The standard solution for this from WordPress.org is to resave your permalinks as type: plain in the WordPress Admin Dashboard.

 

However, if you are transferring or replicating your website and databases non-manually, ie automatically using a shell script, then you may want to perform this action using the WP-CLI or WordPress WP Command Line Interface tool.

 

 

Here are the instructions for changing your permalinks using WP.

 

You need to install WP-CLI on your WordPress server first in order to use this method.

 

 

if your permalinks are set to plain, then you get this output:

 

 

root@asus:~# wp option get permalink_structure –path=/var/www/wordpress –allow-root

 

if they are set to postname, then you get this output:

 

root@asus:~# wp option get permalink_structure –path=/var/www/wordpress –allow-root

/%postname%/
root@asus:~#

 

so, you need to set the structure to nothing:

 

 

root@asus:~# wp option get permalink_structure –path=/var/www/wordpress –allow-root
/%postname%/
root@asus:~# wp option update permalink_structure ” ” –path=/var/www/wordpress –allow-root
Success: Updated ‘permalink_structure’ option.
root@asus:~# wp option get permalink_structure –path=/var/www/wordpress –allow-root

root@asus:~#

 

Note the result of 

 

wp option get permalink_structure

 

is now a blank line. This means that the permalinks have now been set to ” ” ie plain format.

 

So you need to add ” ” as the argument for the value in order to set the permalinks to plain.

 

I checked in wp dashboard – settings – permalinks

 

– and this is correct.

Continue Reading

How To Install WordPress WP-CLI (command line interface)

curl -O https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 6342k 100 6342k 0 0 2773k 0 0:00:02 0:00:02 –:–:– 2773k

 

root@asus:~# php wp-cli.phar –info
OS: Linux 5.8.0-63-generic #71-Ubuntu SMP Tue Jul 13 15:59:12 UTC 2021 x86_64
Shell: /bin/bash
PHP binary: /usr/bin/php7.4
PHP version: 7.4.9
php.ini used: /etc/php/7.4/cli/php.ini
MySQL binary: /usr/bin/mysql
MySQL version: mysql Ver 15.1 Distrib 10.3.29-MariaDB, for debian-linux-gnu (x86_64) using readline 5.2
SQL modes: STRICT_TRANS_TABLES,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION
WP-CLI root dir: phar://wp-cli.phar/vendor/wp-cli/wp-cli
WP-CLI vendor dir: phar://wp-cli.phar/vendor
WP_CLI phar path: /root
WP-CLI packages dir:
WP-CLI global config:
WP-CLI project config:
WP-CLI version: 2.6.0
root@asus:~#

 

root@asus:~# chmod +x wp-cli.phar
root@asus:~# sudo mv wp-cli.phar /usr/local/bin/wp
root@asus:~#

 

wp cli version

 

root@asus:~# wp cli version
Error: YIKES! It looks like you’re running this as root. You probably meant to run this as the user that your WordPress installation exists under.

 

If you REALLY mean to run this as root, we won’t stop you, but just bear in mind that any code on this site will then have full control of your server, making it quite DANGEROUS.

 

If you’d like to continue as root, please run this again, adding this flag: –allow-root

 

If you’d like to run it as the user that this site is under, you can run the following to become the respective user:

 

sudo -u USER -i — wp <command>

 

root@asus:~#

 

 

usage examples:

root@asus:~# wp plugin list –path=/var/www/wordpress –allow-root
+——————————————-+———-+———–+————–+
| name | status | update | version |
+——————————————-+———-+———–+————–+

… plug list follows….

 

 

root@asus:/usr/local/bin# wp core version –path=/var/www/wordpress –allow-root
5.9.3
root@asus:/usr/local/bin#

 

wp cache flush –path=/var/www/wordpress –allow-root

 

 

root@asus:~# wp cache flush –path=/var/www/wordpress –allow-root
Success: The cache was flushed.
root@asus:~#

 

 

Continue Reading

How To Automate the Replication of a WordPress System from an Online Server to an Offline  Localhost Domain

 

This article documents the procedure involved in creating a local working offline replica of an online WordPress website and the automation of this process by means of a shell script.

 

This exercise involves a Ubuntu 20 server and laptop, both running Apache2 and MySQL/Mariadb 10.

 

 

First I installed apache2, mariadb, php, and phpmyadmin on the laptop.

 

 

Copy the WordPress Directory

 

 

I then copied the current /var/www/wordpress directory to /home/kevin/DATA on the server.

 

This is then accessible via the NFS share DATA on the laptop.

Then on the laptop I copied this to /var/www/wordpress, having, first of all, renamed the existing wordpress directory instance.

 

then on laptop:

 

 

Create the Website MySQL Database and User

 

root@asus:~# mysql -u root -p
Enter password:
Welcome to the MariaDB monitor. Commands end with ; or \g.
Your MariaDB connection id is 90
Server version: 10.3.29-MariaDB-0ubuntu0.20.10.1 Ubuntu 20.10

Copyright (c) 2000, 2018, Oracle, MariaDB Corporation Ab and others.

Type ‘help;’ or ‘\h’ for help. Type ‘\c’ to clear the current input statement.

MariaDB [(none)]> show databases;
+——————–+
| Database |
+——————–+
| information_schema |
| mysql |
| performance_schema |
| phpmyadmin |
| wordpress |
+——————–+
5 rows in set (0.002 sec)

MariaDB [(none)]>

 

 

 

Our website WordPress database on the server is called kevwells, so we need to have a database with the same name present on our MariaDB on the laptop:

 

 

So create a database as follows on laptop with these parameters: 

 

MySQL settings – You can get this info from your web host ** //
/** The name of the database for WordPress */
define( ‘DB_NAME’, ‘kevwells’ );

/** MySQL database username */
define( ‘DB_USER’, ‘wordpressuser’ );

/** MySQL database password */
define( ‘DB_PASSWORD’, ‘**COMMENTED OUT***’ );

/** MySQL hostname */
define( ‘DB_HOST’, ‘localhost’ );

 

 

 MariaDB [(none)]> CREATE DATABASE kevwells DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci;

Query OK, 1 row affected (0.001 sec)

MariaDB [(none)]>

 

 

 

MariaDB [(none)]> show databases;
+——————–+
| Database |
+——————–+
| information_schema |
| kevwells |
| mysql |
| performance_schema |
| phpmyadmin |
| wordpress |
+——————–+
6 rows in set (0.001 sec)

MariaDB [(none)]>

 

 

Next, create a user for the database:

 

 

 

MariaDB [(none)]> CREATE USER wordpressuser@localhost;
Query OK, 0 rows affected (0,001 sec)

MariaDB [(none)]>

NOTE no quote marks

 

to set the password: 

ALTER USER ‘wordpressuser’@’localhost’ IDENTIFIED BY ‘passwordcommentedout’;

pay attention to the inverted commas, these have to be the correct type else it wont work with mariadb!

To list all existing users in your database server, you need to query the user table in your mysql database.

 

SELECT the user and host column from the table as follows:

 

SELECT user, host FROM mysql.user;

 

MariaDB [(none)]> SELECT user, host FROM mysql.user;
+——————+———–+
| user | host |
+——————+———–+
| phpmyadmin | localhost |
| root | localhost |
| wordpressuser | localhost |
| ‘phpmyadmin’ | localhost |
+——————+———–+
4 rows in set (0.000 sec)

 

Along the way, I noticed a wrongly entered username above for phpmyadmin, ie ‘phpmyadmin’ , so I deleted this:

 

MariaDB [(none)]> drop user ‘‘phpmyadmin’’@localhost ;
Query OK, 0 rows affected (0.001 sec)

MariaDB [(none)]>
MariaDB [(none)]> SELECT user, host FROM mysql.user;
+—————+———–+
| user | host |
+—————+———–+
| phpmyadmin | localhost |
| root | localhost |
| wordpressuser | localhost |
+—————+———–+
3 rows in set (0.000 sec)

MariaDB [(none)]>

 

we got rid of the ‘phpmyadmin’ as this was an error with the apostrophes as part of the user name.

 

We already have the wordpressuser existing.

 

So all is well. we just need to make sure the password for our user is the correct one.

 

 

 

Just to be certain, I also then went into http://localhost/phpmyadmin and changed the password for user wordpressuser to **COMMENTED OUT** as above.

 

to set the password: 

ALTER USER ‘wordpressuser’@’localhost’ IDENTIFIED BY ‘passwordcommentedout’;

pay attention to the inverted commas, these have to be the correct type else it wont work with mariadb!

 

It returned the message:

 

 

The password for ‘wordpressuser’@’localhost’ was changed successfully.

 

 

 

So on our laptop have created a kevwells database (as yet empty)  and a user called wordpressuser as on the online server.

 

 

I then made following changes to the /var/www/wordpress/wp-config.php:

/* define(‘FORCE_SSL_ADMIN’, true);
*/

 

/*
define( ‘WP_HOME’, ‘https://kevwells.com’ );
define( ‘WP_SITEURL’, ‘https://kevwells.com’ ); */

 

define(‘WP_HOME’,’http://localhost’);
define(‘WP_SITEURL’,’http://localhost’);

 

 

 

Had to give wordpressuser privilege to database kevwells because we are using the database name kevwells and not wordpress for our website wordpress database!

 

MariaDB [(none)]> GRANT ALL ON kevwells.* TO wordpressuser@localhost;
Query OK, 0 rows affected (0,001 sec)

MariaDB [(none)]>

 

MariaDB [(none)]> flush privileges ;
Query OK, 0 rows affected (0.000 sec)

MariaDB [(none)]>

 

 

Note that the reply of mariadb says

 

Query OK, 0 rows affected (0.000 sec)

but it has processed and implemented the change when you do flush privileges.

user wordpressuser then has all privileges granted to kevwells database.

 

Launch the WordPress Duplicator Plugin

Next, start the Duplicator WordPress Plugin procedure on the server to prepare the copy of the site for download from the server to the laptop.

 

You need to download both the zip file and the installer.php for duplicator – you carry out both these actions from within the duplicator dashboard in the plugin on WordPress on the server.

 

Then, on localhost ie laptop, we activate the duplicator installer.php in the WordPress folder on laptop.

 

This installs the instance onto WordPress and apache on localhost.

 

 

Finally, then do the following:

 

Admin Login Login to the WordPress Admin to finalize this install.

 

Auto delete installer files after login (recommended)

IMPORTANT FINAL STEPS: Login into the WordPress Admin to remove all installation files and finalize the install process.

This install is NOT complete until all installer files have been completely removed. Leaving any of the installer files on this server can lead to security issues.

 

 

Review Install Report
The install report is designed to give you a synopsis of the possible errors and warnings that may exist after the installation is completed.

 

 

Test Site
After the install is complete run through your entire site and test all pages and posts.

 

 

Final Security Cleanup
When completed with the installation please delete all installation files. Leaving these files on your server can be a security risk! You can remove all these files by logging into your WordPress admin and following the remove notification links or by deleting these file manually. Be sure these files/directories are removed. Optionally it is also recommended to remove the archive.zip/daf file.

 

 

dup-installer
installer.php
installer-backup.php
dup-installer-bootlog__[HASH].txt
archive.zip/daf

 

 

 

I moved all the above out of /var/www/wordpress to a directory called /var/www/wordpress_duplicator_myfiles/

 

 

NOTE: wordpress admin dashboard username, password, authcode etc is the same on http://localhost/wp-admin as on http://3.222.27.169/wp-admin

 

 

Assemble the MySQL Command To Modify the Website URL References

 

To execute statements from the command line without an interactive prompt, use the -e option:

 

 

mysql mydb -e ‘select * from foo’

 

 

 

We need the following command set:

 

 

UPDATE wp_options SET option_value = replace(option_value, ‘http://3.222.27.169’, ‘http://localhost’) WHERE option_name = ‘home’ OR option_name = ‘siteurl’;
UPDATE wp_posts SET guid = replace(guid, ‘http://3.222.27.169′,’http://localhost’);
UPDATE wp_posts SET post_content = replace(post_content, ‘http://3.222.27.169’, ‘http://localhost’);
UPDATE wp_postmeta SET meta_value = replace(meta_value,’http://3.222.27.169′,’http://localhost’);
FLUSH PRIVILEGES;

 

 

The mysql client utility can take a password on the command line with either the -p or –password= options.

 

 

If you use -p, there must not be any blank space after the option letter.

 

 

BUT: this is insecure because it means a user could view the password – either directly via /proc/$pid/cmdline or via the ps command.

 

 

The safest way to do this would be to create a new config file and pass it to mysql using either the –defaults-file= option.

 

 

to do this,  create a file ~/.my.cnf, make it only accessible by yourself, permission 600.

 

 

[client]
user=myuser
password=mypassword

 

Then you don’t need type password any more. Make sure that you secure this file.

 

 

nano /root/.my.cnf

 

 

[client]
user=root
password=<yourmysqlrootpassword>

 

 

 

chmod 640 /root/.my.cnf
chown root.root /root/.my.cnf

 

-rw-r—– 1 root root 38 Apr 19 18:44 .my.cnf

 

 

 

Then run:

 

mysql –defaults-file=

 

 

mysql –defaults-file=/root/.my.cnf -e “show databases ;”

 

root@asus:~# mysql –defaults-file=/root/.my.cnf -e “show databases ;”
+——————–+
| Database |
+——————–+
| information_schema |
| kevwells |
| mysql |
| performance_schema |
| phpmyadmin |
| wordpress |
+——————–+
root@asus:~#

 

It works. So, now let’s try the full command:

 

/usr/bin/mysql –defaults-file=/root/.my.cnf -e “USE kevwells ; UPDATE wp_options SET option_value = replace(option_value, ‘https://kevwells.com’, ‘http://localhost’) WHERE option_name = ‘home’ OR option_name = ‘siteurl’; UPDATE wp_posts SET guid = replace(guid, ‘https://kevwells.com’,’http://localhost’); UPDATE wp_posts SET post_content = replace(post_content, ‘https://kevwells.com’, ‘http://localhost’); UPDATE wp_postmeta SET meta_value = replace(meta_value,’http://kevwells.com’,’http://localhost’); FLUSH PRIVILEGES;”

 

 

Build a Shell Script to Automate the Replication Process

 

Next we want to put this and other commands in an executable script under /usr/local/bin as the second part of our backup script routine which downloads the databases from kevwells.com to localhost.

 

Easiest way to do the downloading would be via the /home/kevin/ NFS Share, this also provides a regular database backup on the share.

 

Then call up the script using crontab.

 

Finally, make sure you have the two definitions to the wp-config.php and also the FORCE_SSL_ADMIN directive (we disable this as we are not using SSL on the laptop localhost apache2).

 

 

define(‘FORCE_SSL_ADMIN’, false);

define( ‘WP_HOME’, ‘http://localhost’ );
define( ‘WP_SITEURL’, ‘http://localhost’ );

 

We need a script to download the MySQL WordPress database from the online server at kevwells.com to the laptop, and then to make the necessary URL adjustments.

 

Do not simply copy database files from one machine to the other. This can lead to database inconsistencies. In any case with a dynamic online database with real-time changes this would be a recipe for disaster.

 

The correct way to export and import a mysql database between two machines is to use mysqldump and mysqlimport.

 

On an online machine with continual write actions, you would also need to pause the database while performing the export-import action. However as this is simply a database for a  WordPress website with few write actions taking place, we can ignore this and go straight to the export routine.

 

To do this, we use mysqldump.

 

Use mysqldump –help to see what options are available.

 

The basic structure of our command will be, on the server:

 

mysqldump –quick db_name | gzip > db_name.gz 

 

 

and to restore, ie import on the other machine:

 

mysqladmin create db_name 

 

gunzip < db_name.gz | mysql db_name

 

 

 

using the mysql cli client the command would be (but we will use mysqladmin in our case)

 

root@asus:/usr/local/bin# mysql -p -uwordpressuser kevwells < /home/kevin/DATA/KEVWELLS.COM/kevwells.sql
Enter password:
root@asus:/usr/local/bin#

 

on the server we use:

 

mysqldump –quick db_name | gzip > db_name.gz
mysqldump –defaults-file=/root/.my.cnf –quick kevwells | gzip > /home/kevin/DATA/KEVWELLS.COM/kevwells.sql.gz

 

So,

 

root@gemini:~#
root@gemini:~#
root@gemini:~# mysqldump –defaults-file=/root/.my.cnf –quick kevwells | gzip > /home/kevin/DATA/KEVWELLS.COM/kevwells.sql.gz
root@gemini:~# mysqldump –defaults-file=/root/.my.cnf –quick mysql | gzip > /home/kevin/DATA/KEVWELLS.COM/mysql.sql.gz
root@gemini:~#

 

then on laptop:

 

Transfer both .sql files with database contents to the target machine and run these commands there:

 

mysqladmin create db_name 

gunzip < db_name.gz | mysql db_name

 

You need to delete the old database .sql files first, else the mysqlimport command will fail:

 

root@asus:~# rm /home/kevin/DATA/KEVWELLS.COM/kevwells.sql
root@asus:~# rm /home/kevin/DATA/KEVWELLS.COM/mysql.sql
root@asus:~#

 

then perform the imports:

 

root@asus:~# mysqlimport kevwells | gunzip /home/kevin/DATA/KEVWELLS.COM/kevwells.sql.gz
root@asus:~#

root@asus:~# mysqlimport mysql | gunzip /home/kevin/DATA/KEVWELLS.COM/mysql.sql.gz
root@asus:~#

 

After you import the mysql database onto the laptop, execute mysqladmin flush-privileges so the laptop mysql server reloads the grant table info:

 

mysqladmin flush-privileges

 

root@asus:~# mysqladmin flush-privileges
root@asus:~#

 

Some other commands:

 

root@asus:/home/kevin/DATA/KEVWELLS.COM# mysqladmin status
Uptime: 32382 Threads: 7 Questions: 7651 Slow queries: 0 Opens: 155 Flush tables: 1 Open tables: 148 Queries per second avg: 0.236
root@asus:/home/kevin/DATA/KEVWELLS.COM#

 

We can implement the automation in various ways.

 

One way would be to have two scripts, one on server which executes first via crontab, and the other on the laptop which executes later via crontab.

 

The server script would perform the mysqldump, the laptop script would do the mysqlimport.

 

Another way would be remote ssh – but we would have to give root ssh key permission on the server from the laptop for this. Then we would do everything from the one script on the laptop.

 

We will use this method to keep things simpler and means there is just one script that controls the entire process.

 

Set up the SSH for Remote Login without Password Prompt

 

First, copy the ssh key for root to geminivpn

 

use geminivpn and not gemini as wifi routers do not always permit outgoing ssh connections!

 

on gemini:

 

In the /etc/ssh/sshd_config you must have the directive:

 

PermitRootLogin yes

 

then restart sshd:

 

systemctl restart sshd

 

Next, assemble the script on the laptop:

 

 

Check Mysql Server Command Functionality

 

 

First let’s check the functionality on the server:

 

root@gemini:/etc/ssh# mysqladmin status
Uptime: 1458129 Threads: 7 Questions: 4569013 Slow queries: 0 Opens: 358 Flush tables: 1 Open tables: 337 Queries per second avg: 3.133
root@gemini:/etc/ssh#

 

and let’s see if we have our ssh command set correctly and if it works for mysqladmin commands:

root@asus:/home/kevin# ssh root@geminivpn “mysqladmin status”
Uptime: 1458134 Threads: 7 Questions: 4569014 Slow queries: 0 Opens: 358 Flush tables: 1 Open tables: 337 Queries per second avg: 3.133
root@asus:/home/kevin#

 

everything good.

 

Check the Functions.php: 

there was also a problem with /var/www/wordpress# nano wp-content/themes/canvas/functions.php

 

This contained:

 

update_option( ‘siteurl’, ‘http://3.222.27.169’ );
update_option( ‘home’, ‘http://3.222.27.169’ );

 

These directives were changing the site name in the WordPress database back to kevwells.com from localhost.

 

So we have to make sure this is modified when we copied the WordPress files across. This is not dependent on the MySQL databases far as I can gather at this stage, it is solely in the functions.php of the canvas theme used for the site.

 

I have now commented this out on both kevwells.com and localhost.

 

Assemble the Script

 

So, next step is to assemble our script on the laptop:

 

root@asus:/usr/local/bin# cat kevwells.com_db_replication.sh

 

#!/bin/bash
# Script: /usr/local/bin/kevwells.com_db_replication.sh
# Date Created: 19.4.2022
# Last Modified 25.02.2024
# Author: Kevin Wells
# Purpose: export wordpress database for kevwells.com website from NFS share on kevinvm1vpn to laptop
# and modify database domain url from http://kevwells.com to http://localhost
# How called: via crontab. Can also be called manually
# Requires: mysql/mariadb, mysqladmin, mysql client, ssh, ssh keys installed on server for passwordless remote command execution
# /root/.my.cnf containing mysql password for root, gzip/gunzip on both machines
# Location: asus laptop /usr/local/bin

set -x

SERVER=”kevinvm1vpn”
WEBSITESOURCE=”/home/kevin/NFSVOLUME/DATAVOLUME/KEVWELLS.COM”
LOCALSOURCE=”/media/kevin/KEVINVM1VPN/srv/nfs4/NFSSHARE/DATAVOLUME/KEVWELLS.COM”

## not needed, doing it on the server locally:
## scp -r root@$SERVER:/var/www/wordpress/* root@$SERVER:/home/kevin/NFSVOLUME/DATAVOLUME/KEVWELLS.COM/

 

# first, make sure the wordpress cache is flushed to disk:
ssh root@$SERVER “wp cache flush –path=/var/www/wordpress –allow-root “

# first delete the old .sql database file from last time:
##dont need to do this remote, is a problem with the rm command in any case, but delete it locally
##ssh root@$SERVER runuser -l kevin -c ‘rm -f $WEBSITESOURCE/all_databases.sql’
# delete locally:
rm -f $LOCALSOURCE/all_databases.sql

 

# create the database export from server kevwells.com:

#ssh -i /root/KevinVM1.pem ubuntu@kevinvm1vpn “mysqldump –all-databases > /home/kevin/DATAVOLUME/KEVWELLS.COM/all_databases.sql”

ssh root@$SERVER “mysqldump –all-databases > $WEBSITESOURCE/all_databases.sql”

 

# next import the databases into the laptop mysql:
# no username or password needed as this is defined in the /root/.my.cnf file:

mysql < $LOCALSOURCE/all_databases.sql

# should not be necessary, but to be on safe side:
chown -R mysql.mysql /var/lib/mysql/kevwells

/usr/bin/mysql -e “USE kevwells ; UPDATE wp_options SET option_value = replace(option_value, ‘http://kevwells.com’, ‘http://localhost’) WHERE option_name = ‘home’ OR option_name = ‘siteurl’; UPDATE wp_posts SET guid = replace(guid, ‘https://kevwells.com’,’http://localhost’); UPDATE wp_posts SET post_content = replace(post_content, ‘https://kevwells.com’, ‘http://localhost’); UPDATE wp_postmeta SET meta_value = replace(meta_value,’https://kevwells.com’,’http://localhost’); FLUSH PRIVILEGES;”

/usr/local/bin/wp cache flush –path=/var/www/wordpress –allow-root

# then execute mysqladmin flush-privileges so the laptop mysql server reloads the grant table info:

mysqladmin flush-privileges

# finally, we can reset the permalinks to plain, we do this using the wordpress wp-cli tool:

#wp option get permalink_structure –path=/var/www/wordpress –allow-root
#this should return the following output – or similar tag category:
#/%postname%/

#wp option update permalink_structure ” ” –path=/var/www/wordpress –allow-root

/usr/bin/php -f /usr/local/bin/wp option update permalink_structure ‘ ‘ –path=/var/www/wordpress –allow-root

 

#this should return the following output:
#Success: Updated ‘permalink_structure’ option.

# wp option get permalink_structure –path=/var/www/wordpress –allow-root
#this should return the following output: (ie empty line)

# END OF SCRIPT

 

  

Testing the script with bash option set -x:

  

root@asus:/home/kevin/shellscripts# ./kevwells.com_db_replication.sh
+ SERVER=kevinvm1vpn
+ WEBSITESOURCE=/home/kevin/NFSVOLUME/DATAVOLUME/KEVWELLS.COM
+ LOCALSOURCE=/media/kevin/KEVINVM1VPN/srv/nfs4/NFSSHARE/DATAVOLUME/KEVWELLS.COM
+ ssh root@kevinvm1vpn ‘wp cache flush –path=/var/www/wordpress –allow-root ‘
PHP Warning: Undefined array key “HTTP_HOST” in /var/www/wordpress/wp-content/plugins/force-https-littlebizzy.disabled/core/redirect.php on line 91
Warning: Some code is trying to do a URL redirect. Backtrace:
#0 /var/www/wordpress/wp-includes/class-wp-hook.php(312): WP_CLI\Utils\wp_redirect_handler(‘…’)
#1 /var/www/wordpress/wp-includes/plugin.php(205): WP_Hook->apply_filters(‘…’, Array)
#2 /var/www/wordpress/wp-includes/pluggable.php(1396): apply_filters(‘…’, ‘…’, 301)
#3 /var/www/wordpress/wp-content/plugins/force-https-littlebizzy.disabled/core/redirect.php(91): wp_redirect(‘…’, 301)
#4 /var/www/wordpress/wp-content/plugins/force-https-littlebizzy.disabled/core/redirect.php(68): FHTTPS_Core_Redirect->redirect()
#5 /var/www/wordpress/wp-includes/class-wp-hook.php(310): FHTTPS_Core_Redirect->start(”)
#6 /var/www/wordpress/wp-includes/class-wp-hook.php(334): WP_Hook->apply_filters(NULL, Array)
#7 /var/www/wordpress/wp-includes/plugin.php(517): WP_Hook->do_action(Array)
#8 /var/www/wordpress/wp-settings.php(495): do_action(‘…’)
#9 phar:///usr/local/bin/wp/vendor/wp-cli/wp-cli/php/WP_CLI/Runner.php(1317): require(‘…’)
#10 phar:///usr/local/bin/wp/vendor/wp-cli/wp-cli/php/WP_CLI/Runner.php(1235): WP_CLI\Runner->load_wordpress()
#11 phar:///usr/local/bin/wp/vendor/wp-cli/wp-cli/php/WP_CLI/Bootstrap/LaunchRunner.php(28): WP_CLI\Runner->start()
#12 phar:///usr/local/bin/wp/vendor/wp-cli/wp-cli/php/bootstrap.php(78): WP_CLI\Bootstrap\LaunchRunner->process(Object(WP_CLI\Bootstrap\BootstrapState))
#13 phar:///usr/local/bin/wp/vendor/wp-cli/wp-cli/php/wp-cli.php(27): WP_CLI\bootstrap()
#14 phar:///usr/local/bin/wp/php/boot-phar.php(11): include(‘…’)
#15 /usr/local/bin/wp(4): include(‘…’)
+ rm -f /media/kevin/KEVINVM1VPN/srv/nfs4/NFSSHARE/DATAVOLUME/KEVWELLS.COM/all_databases.sql
+ ssh root@kevinvm1vpn ‘mysqldump –all-databases > /home/kevin/NFSVOLUME/DATAVOLUME/KEVWELLS.COM/all_databases.sql’
+ mysql
./kevwells.com_db_replication.sh: line 50: /media/kevin/KEVINVM1VPN/srv/nfs4/NFSSHARE/DATAVOLUME/KEVWELLS.COM/all_databases.sql: No such file or directory
+ chown -R mysql.mysql /var/lib/mysql/kevwells
+ /usr/bin/mysql -e ‘USE kevwells ; UPDATE wp_options SET option_value = replace(option_value, ‘\”http://kevwells.com’\”, ‘\”http://localhost’\”) WHERE option_name = ‘\”home’\” OR option_name = ‘\”siteurl’\”; UPDATE wp_posts SET guid = replace(guid, ‘\”https://kevwells.com’\”,’\”http://localhost’\”); UPDATE wp_posts SET post_content = replace(post_content, ‘\”https://kevwells.com’\”, ‘\”http://localhost’\”); UPDATE wp_postmeta SET meta_value = replace(meta_value,’\”https://kevwells.com’\”,’\”http://localhost’\”); FLUSH PRIVILEGES;’
+ /usr/local/bin/wp cache flush –path=/var/www/wordpress –allow-root
Success: The cache was flushed.
+ mysqladmin flush-privileges
+ /usr/bin/php -f /usr/local/bin/wp option update permalink_structure ‘ ‘ –path=/var/www/wordpress –allow-root
Success: Value passed for ‘permalink_structure’ option is unchanged.
root@asus:/home/kevin/shellscripts#

 

root@asus:/usr/local/bin#

 

 

Make sure the script is executable!

root@asus:/usr/local/bin# chmod 750 kevwells.com_db_replication.sh
root@asus:/usr/local/bin# ls -l kevwells.com_db_replication.sh
-rwxr-x— 1 root root 2007 Apr 20 10:57 kevwells.com_db_replication.sh
root@asus:/usr/local/bin#

 

And finally, schedule the script to run using crontab on the laptop: 

 

eg

 

9 2 * * * /usr/local/bin/kevwells.com_db_replication.sh

 

 

Additional Notes on the Replication Process

 

Some WordPress plugins can interfere with the replication and correct rendition of the website from https://kevwells.com to http://localhost.

 

In particular, check that:

 

Really Simple SSL is “deactivated, but still using https” (select this option in the plugin settings in the WP Dashboard).

 

Also I have disabled Permalinks Moved Permanently plugin.

 

Provided these plugins are disabled and the replication script runs correctly with the MySQL DB Update URL commands executed to change siteurl links from https://kevwells.com to http://localhost then the replication should be error-free and there is no need for the permalinks to be saved in the WP Dashboard as is otherwise usually the case with WordPress site transfers and domain name changes.

 

Continue Reading

How To Create an HTML Flatfile Instance of a Website

To create an html flatfile replica of the WordPress website http://kevwells.com the challenge was to get the output from the php content files as html.

 

 

This problem was resolved in the following way:

 

 

Launch an instance of the php webserver process on port 8080

 

 

NOTE this has to be executed from the top of the website document folder tree ie in /var/www/html: 

 

 

root@gemini:/var/www/html# php -S kevwells.com:8080
[Thu Jan 6 22:36:44 2022] PHP 7.4.3 Development Server (http://kevwells.com:8080) started

 

 

 

Then in another terminal window execute the wget command:

 

 

wget -r –mirror –page-requisites –convert-links -U mozilla -F http://kevwells.com:8080

 

 

NOTE: We do not use the “span hosts” wget option  – else this will download all the external sites that are linked as well.

 

this then generates and downloads a static html flatfile instance of the website.

 

this can then be accessed from a web-browser by using the URL file:///<filesystem location of the wget output>

 

 

eg

 

 

file:///home/kevin/DATA/KEVWELLS.COM/kevwells.com_FlatHTMLfiles/kevwells.com/

 

The location folder must be made available first in NFS for clients to connect to.

 

Alternatively the html folder tree for the downloaded site can be  copied to any other machine and accessed locally from there.

 

This displays a file system based instance of the downloaded website.

 

 

 

 

 

 

 

 

this method downloads the website as flat html files, you access the site instance via the file:/// reference in the browser URL field.

in other words, it does not convert the internal website links to pages and posts into localhost referenced links.

but it means you do have a local instance of the site containing all the content as flat html files.

Continue Reading

How To Replicate A WordPress Website From Server To Localhost

Replicating Website from http://kevwells.com to http://localhost on laptop.

 

First install Apache, MySQL/MariaDB and PHP on laptop.

 

Then create a database with the same name and connection/login credentials as on the kevwells.com server.

 

 

Next, install the WordPress Duplicator Plugin on kevwells.com on the server. We are using the Duplicator Lite (free version) not the paid for pro-version.

 

This has one drawback: the links within the website to https:/kevwells.com cannot be changed using Duplicator, so we have to use other means to do this.

 

A further problem (not Duplicator related) is the use of https SSL/TLS on the kevwells.com server, while the localhost on the laptop uses http (http://localhost).

 

 

 

We were not able to convert the localhost instance to http.

 

 

This meant that when calling up http:/localhost in the web-browser, the link would automatically default to https and connect to the https:/kevwells.com external public url. This also meant we could not access the WordPress admin account on the localhost.

 

The only way around this problem was to temporarily change the configuration of kevwells.com from https to http, then perform the Duplicator export.

 

 

We would then be exporting http://kevwells.com to http:/localhost and not https:/kevwells.com.

 

 

This was possible.

 

 

This is done by modifying the sites-enabled file /etc/apache2/sites-enabled/kevwells.com.conf on the kevwells.com server:

 

the lines with double hash sign ie ## have been temporarily commented out for this purpose:

root@gemini:/etc/apache2/sites-enabled# cat kevwells.com.conf

 

<IfModule mod_ssl.c>
##<VirtualHost *:444>

 

<VirtualHost *:80>
# The ServerName directive sets the request scheme, hostname and port that
# the server uses to identify itself. This is used when creating
# redirection URLs. In the context of virtual hosts, the ServerName
# specifies what hostname must appear in the request’s Host: header to
# match this virtual host. For the default virtual host (this file) this
# value is not decisive as it is used as a last resort host regardless.
# However, you must set it for any further virtual host explicitly.

 

ServerName kevwells.com

 

ServerAdmin webmaster@localhost
DocumentRoot /var/www/html

# Available loglevels: trace8, …, trace1, debug, info, notice, warn,
# error, crit, alert, emerg.
# It is also possible to configure the loglevel for particular
# modules, e.g.
#LogLevel info ssl:warn

 

ErrorLog ${APACHE_LOG_DIR}/error.log
CustomLog ${APACHE_LOG_DIR}/access.log combined

 

# For most configuration files from conf-available/, which are
# enabled or disabled at a global level, it is possible to
# include a line for only one particular virtual host. For example the
# following line enables the CGI configuration for this host only
# after it has been globally disabled with “a2disconf”.
#Include conf-available/serve-cgi-bin.conf

 

##Include /etc/letsencrypt/options-ssl-apache.conf

 

ServerAlias www.kevwells.com

 

SSLCertificateFile /etc/letsencrypt/live/kevwells.com/fullchain.pem
##SSLCertificateKeyFile /etc/letsencrypt/live/kevwells.com/privkey.pem

 

</VirtualHost>
##</IfModule>

 

Then execute:

 

a2dissite kevwells.com.conf
systemctl reload apache2

 

and

 

a2ensite kevwells.com.conf
systemctl reload apache2

 

The site should now be operating in http mode instead of html.

 

After exporting the site using Duplicator, return the config to the original state and perform the a2dissite and a2ensite steps again. https service will then be restored.

 

 

The export file was 2.4GB in size. There is also an installer.php provided by the plugin specifically for this export file. You manually copy these two files to the laptop destination machine.

 

From there, these two files are moved to the root document folder of the website on the localhost, in this case /var/www/html.

 

The installer.php from Duplicator is then executed from a webbrowser:

 

http://localhost/installer.php

 

Then follow the instructions from Duplicator installer.php in the browser display.

 

The process takes several minutes.

 

 

We then still had a problem with the links. Entering http://localhost would only display the home page of the site. All other pages were not accessible.

 

Further configuration was necessary.

 

In phpmyadmin we changed the kevwells database table wp_options entries for siteurl and blogname from http://kevwells.com and kevwells.com to http://localhost and localhost respectively.

 

We then added

 

define(‘WP_HOME’,’http://localhost’);
define(‘WP_SITEURL’,’http://localhost’);

 

 

 

to wp-config.php

 

this file also has to contain the definitions for the database connections:

 

// ** MySQL settings – You can get this info from your web host ** //

/** The name of the database for WordPress */

define( ‘DB_NAME’, “kevwells” );

/** MySQL database username */

define( ‘DB_USER’, “wordpressuser” );

/** MySQL database password */

define( ‘DB_PASSWORD’, “*****” );

/* password is commented out here for security reasons */

/** MySQL hostname */

define( ‘DB_HOST’, “localhost” );

 

 

the .htaccess file is empty:

 

kevin@asus:/var/www/html$ cat .htaccess
# BEGIN WordPress
# The directives (lines) between “BEGIN WordPress” and “END WordPress” are
# dynamically generated, and should only be modified via WordPress filters.
# Any changes to the directives between these markers will be overwritten.

# END WordPresskevin@asus:/var/www/html$

 

 

We then installed a further WordPress plugin: Go Live Update Urls

 

this is to change the Url site references within the website from http://kevwells.com to http://localhost

 

However the content would still not display, although we could open the WordPress Admin for localhost successfully.

 

The solution was to execute the Permalinks settings in WordPress Admin, changing them from custom to plain, saving the settings.

 

The website then displayed correctly.

 

The only part of the content which gives a not found error are the pages (eg terms and conditions, disclaimer etc), posts are ok.

 

We now have a static replica of the http://kevwells.com instance running on the laptop as http://localhost

 

Continue Reading

How To Secure WordPress Against Brute Force Password Attacks

There’s a worldwide hacker attack going on against many WordPress websites.

The attack is a so-called “brute force attack” which aims to test different password and admin user id combinations to try and gain access to your WordPress administration dashboard.

Then the attack installs a bot which is then used to launch attacks on other servers or else perform other illicit activity.

How To Secure WordPress Against Brute Force Password Attacks

To help protect your site against this attack, you can add a secondary layer of administrator login security to WordPress sites.

To do this you basically create a .wpadmin file in the top home directory of your server system or in your server account home user area if you are on a shared server.

You then add a username and password pair, encrypt the password and then activate the security in the .htaccess file in your server space.

Here are the steps in detail:

(The following instructions are for server accounts that use cPanel, which is the most commonly used web-hosting account admin interface).
1. Create the .wpadmin file

Create a file with the name .wpadmin in your home directory. It’s important to name the file with the . (dot) before the file name, ie: .wpadmin.

eg. /home/username/.wpadmin
(“username” is your cPanel admin account username)

2. Create an encrypted password for a new user name/password combination.

The easiest way to do this is to use the htpassword generator at htaccesstools.

Go to www.htaccesstools.com/htpasswd-generator and enter the user name you wish to use and the password.

Note this combination should NOT be the same as the ones you currently use, neither for your cPanel login, nor for any of your WordPress sites. It should be a totally new user name and password combination.

Make sure you note down temporarily (and securely) or at least don’t forget what you have entered. You’ll need to know this in order to login each time later.

The password generator will then output an encrypted version of the password you entered for the user name that you entered. Copy and paste this into the .wpadmin file you have just created. That’s you need in the .wpadmin file.

Note that the .wpadmin file ONLY contains the user name (non-encrypted) and the encrypted version of your password. You do not add the non-encrypted version of the password.

Make sure you don’t forget the non-encrypted version of the password, else you won’t be able to log in!

For example:

user name: steve
non-encrypted password: abcdefg (by the way, a very poor password, so don’t ever use it. I’m just using it for the sake of this example)

The htpassword generator will create something like this:

steve:gjodWDQ8944qfr

The field after the colon, in this case: gjodWDQ8944qfr is the encrypted version of the password abcdefg.

You enter this line into the .wpadmin.php file. Then save and close the file.

3. Finally, update the .htaccess file

Cut and paste the following lines into your /home/username/.htaccess file.
:
ErrorDocument 401 “Unauthorized Access”
ErrorDocument 403 “Forbidden”
<FilesMatch “wp-login.php”>
AuthName “Authorized Only”
AuthType Basic
AuthUserFile /home/username/.wpadmin
require valid-user

Make sure you substitute your own username int the line AuthUserFile /home/username/.wpadmin.

Your secondary login wall of defence is now complete.

From now on, when you want to access a WordPress Admin dashboard on your web server, it will first prompt you for the username and the (non-encrypted) password combination that you configured as above.

For example, in this case, that will be:

login: steve
password: abcdefg

It will then let you pass and direct you to the normal standard admin login for the WordPress site you requested. You then log yourself in on the WordPress dashboard as normal.

Note that if you have WordPress pages that are password-secured using the standard WordPress password protect functionality, then the above procedure will require you to perform the double login using your secondary username/password for these pages as well.

This may be a feature you are happy to live with – or it might be an irritation and complication that you’d rather not have.

In this case, you’ll have to weigh up the pros and cons of implementing this additional security versus the extra login overhead involved.

 

 

Continue Reading

A Practical Guide To Basic WordPress Security

If you have a website for your business, then there’s a good chance it will be a WordPress website.

WordPress is a mature and secure Website Content Management System or CMS which is used by millions of websites all around the world.
 
But like all websites and webservers, WordPress can also be hacked and compromised by intruders if you don’t pay attention to basic security aspects. 

A Practical Guide To Basic WordPress Security

Website security is a complex area and to discuss all the aspects of web server security I would end up filling a whole book (perhaps I’ll write it one day).
 
What I’m going to do here is provide you with the essential and most important basics of WordPress security which will go a long way in providing you with an acceptable level of security for your website and which involve relatively low overhead from yourself to implement. 

The most common problem websites around the world face are attacks launched by so-called “script kiddies”. 

Script kiddies are the most common – and fortunately also the least competent, types of computer hackers. Script kiddies are people – they may indeed be “kids”, but are actually often adults, who rely on running freely available hacking tools and program scripts to try and identify and break into websites which have lax basic security.
 
These scripts and hacker tools look for websites which have weak administrator accounts and especially passwords, unpatched, bug-ridden or outdated WordPress plugins or databases, or web-hosting providers that have security holes in their systems.
 
The majority of successful break-ins occur simply through script-kiddie hackers finding and exploiting these weaknesses. 

So, what you need to do first of all is to make sure you eliminate these weaknesses from your website. 

Take A Look At Your Web-Hosting

 
First of all, take a look at your web-hosting. 

Make sure you are hosting your site with a web-hosting provider who takes web server security seriously. They should ensure that proper security measures are taken at all times and that their system and those of their customers are backed up properly and are properly protected against intruders. 

It’s especially important that operating system and web server system software is updated whenever new versions are released. This can usually close most security holes straight away.
 
The good news is that most web-hosting providers do look after their systems fairly well, but there are still some out there who are lax in this area.
 
The best advice is to check the reviews of your web-hosting provider to gauge what their level of reliability is like in practice. 

WordPress and Web-Hosting User Accounts

 
Always use secure passwords for both your Web-Hosting and your WordPress accounts.
 
There’s a lot that can be said about what makes for a secure password.
 
But basically a secure password follows these fundamental rules: 

  • The longer the password the better.
  • Use a combination of lower and uppercase letters, alphanumeric, and other characters such as hyphens, dots, dollar, hash, percentage signs and so on.
  • Use “nonsense” words – NEVER use a word from a dictionary.
  • Never reuse a password.
  • Never use the same password on more than one site.
  • Never write your passwords down on paper – and be careful about where you store them on your computer or online. DON’T store passwords in an email inbox.
  • Never use any password obviously based on some aspect of yourself or your business. That’s too easy to guess.
  • Use separate editor and administrator accounts for WordPress – with different passwords and user names for each.
  • Do not use obvious login names for your WordPress user accounts. Do not use “admin” or “administrator” names for your root or admin accounts for WordPress.
  • You can randomize your WordPress user account names for both administrator and editor accounts just as you can with the passwords. You can set the displayed editor name in your pages and posts to the one you want the public to see. Make sure your chosen randomized user names are not displayed as page or post authors.
  • Use a password storage and retrieval tool such as LastPass or Roboform. These tools also generate random, long and complex passwords for you on demand which are then encrypted and stored for you. They provide a local and an online instance of your own password database. Make sure you always remember your master password for your password database – and keep it safe.
  • If you can accept the extra inconvenience involved, add two-factor authentication to your login systems. These tend to involve an email or mobile phone check – sometimes even both.

 

WordPress Security Plugins

 
Install a couple of reputable WordPress security plugins on your site. There are a number of these available, but it’s best to stick to the most popular, proven, tried and tested security plugins. 

The two security plugins I recommend in most cases for WordPress websites are Bulletproof Security and WordFence. 

You can also install the Stealth Login Page Plugin which will add a second tier of security to your login procedure, requiring you to enter a previously set Authentication Code along with your user name and password when you want to login to your WordPress Dashboard. 

WordPress Themes

 
Only install WordPress themes from reliable theme design providers. I recommend taking a look at Woo Themes but there are also many other quality theme publishers. . 

Make sure you apply updates to the themes promptly as and when they become available.

WordPress Core Updates

 
Make sure that you also apply all WordPress Core Platform version updates immediately they become available. This can be crucial in ensuring that any new security exploit is prevented. 

WordPress Plugin Policy

 
Be careful when choosing your WordPress plugins. Plugins can contain bugs and vulnerabilities. It’s important that the plugin should be actively maintained by the developer so that bugs and security weaknesses can be resolved quickly. 

The best rule to follow with plugins is to use only as many as necessary and as few as possible. 

Access Policy

 
It’s best not to access your website’s WordPress dashboard through a public wi-fi system, because your user name and password can be intercepted by anyone using Internet sniffer software. A safer way to do this on a public wi-fi network is to use a trusted VPN service. 

WordPress and Server Backups

 
Finally, make sure you backup your website regularly. Both your web server and your WordPress website, including the database should all be separately backed up on a regular basis. 

Always maintain more than one copy of your backups – and keep these backups on a separate machine and location to your web server. 

By following these basic security rules you will be able to thwart many of the attempts of hackers to attack and compromise your web server and your website. 

 

Continue Reading

How To Install WordPress Plugins On Your Website

seogoogleipad-605440_960_720WordPress plugins are add-on modules which provide functions and features for your website.

Plugins are easy to install and most of them are free of charge.

How To Install WordPress Plugins On Your Website

Installing WordPress plugins is simple. Once you’ve downloaded your plugin, login to your WordPress Dashboard and click on the Plug-in -> Add New

The plugin then uploads to your site. For most plugins you also need to click on “activate” to switch on the plugin.

However, it’s very easy to get carried away and install a whole load of plugins, not all of which actually add much value to your site.

I recommend you don’t install too many plugins because this can lead to configuration conflicts and strange effects on your site which can be time-consuming and troublesome to debug.

In practice, there’s probably only a couple of dozen or so plugins at most that you will really need.

If you have problems with your site’s display or strange effects as a result of installing a plugin, then you may need to disable the plugin or remove it completely.

The best advice is to install the plugins one by one and check everything is working ok before you go on to install the next one.

Here Are Some Great “Must Have” WordPress Plugins

So, here’s a list of some of the plugins I use, some of which you you might also find useful for your site.

All of these plugins are available free of charge via the WordPress.org site.

Akismet This is a comment spam protector that comes already built into WordPress. To activate it you need to obtain an “API key” from the WordPress.com site.

About Me 3000 This displays an info bio box on your sidebar with a photo.

AdRotate If your site carries advertising, then Adrotate is a really useful plugin which helps you manage and rotate your display and banner ads.

Bad Behavior This plugin denies automated spambots access to your site.

BulletProof Security This is a very robust plugin which protects your website against literally thousands of different hacking attempts.

Colorful Text Widgets This plugin displays a text box on your sidebar which you can use for whatever purpose you wish. To display an ad, a newsletter sign up box, information about your business, whatever.

EU Cookie Law This is a very useful plugin for websites based in the EU. It displays a popup bar on your site to inform users about the EU cookie law and so ensures that your website is compliant with EU cookie law

Fast Secure Contact Form This is a very powerful form builder that enables your readers to send you email.

Find Me On The Find Me On sidebar widget displays icons for all of your social network profiles.

Google XML Sitemaps This plugin will generate a special XML sitemap which will help search engines to better index your site.

Open Web Analytics This is a traffic statistics plugin which is a viable alternative to using Google Analytics.

Permalinks Moved Permanently When permalink isn’t found, this checks if a post with the requested slug exists somewhere else on your site.

Select Post Ender A simple plugin to allow you to add a message footer at the end of every post.

Select Posts in Sidebar This plugin displays a list of posts in your sidebar

ShareThis This plugin enables your visitors to share a post or page with others via e-mail and social media sites.

Select Smart Youtube PRO This plugin enables you to insert YouTube videos in your posts and pages.

Select Table of Contents Plus This plugin automatically creates a table of contents at the top of each of your posts.

Select TinyMCE Advanced Enables advanced features and plugins in TinyMCE, the visual editor in WordPress.

Select TinyMCE Spellcheck This adds a contextual spell, style, and grammar checker to WordPress

Social Networks Auto-Poster This plugin automatically publishes posts from your site to your accounts on Facebook, Twitter, and Google+ profiles

W3 Total Cache This plugin dramatically improve the speed and user experience of your site.

WordPress Database Backup This creates an on-demand backup of your WordPress database.

WordPress Editorial Calendar This plugin displays your posts in a calendar for easy management of your publishing schedule.

WordPress SEO This is about the best all-in-one SEO plugin for WordPress.

WP-SpamFree An extremely powerful anti-spam plugin that virtually eliminates comment spam.

WP125 This plugin enables you to manage 125×125 size ads from your WordPress Dashboard.

wp Time Machine (for Backups) Creates an archive of all your WordPress Data & Files and then stores them on Dropbox, Amazon S3, or your FTP host.

Yet Another Related Posts Plugin Adds related posts to your site and in RSS feeds, based on a powerful, customizable algorithm.

Where To Find WordPress Plugins

You can find all the plugins I’ve listed above at the official WordPress plugins page at wordpress.org/extend/plugins.

All the WordPress plugins mentioned are available free of charge.

 

Continue Reading