Assume you’re working on a custom WordPress plugin that includes its own template for rendering data. The difference in this template is that it’s not one that you’ll apply to a page or custom post type in WordPress, but it will instead be its own page with access to the WordPress core functions.
For example, say you’re generating a report for a given user ID and you want the page to incorporate fonts from Google, perhaps a third-party library, and it look as if it’s not even part of the WordPress core application.
The head
element of the page may look something like this:
<!DOCTYPE html>
<html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title><?php echo getName($userId); ?></title> <link rel="stylesheet" href="<?php echo getAsset("/lib/reset.css"); ?>"> <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=PT+Sans"> </head>
But you there are two other requirements:
- You want to access the report through a pretty link that includes the
$userId
(perhaps something like/information/42
- You want to use core functions for getting user metadata through functions like
get_user_meta
.
To do this, you need to have custom templates with rewrite rules in your WordPress plugin. Again, this is not a custom template in the WordPress sense and you’ll only have as many rewrite rules as you do templates.
Table of Contents
Custom Templates with Rewrite Rules
1. The Plugin Structure
This can be structured however you want, but I find it easiest to have the rules set in their own file that’s included in the plugin’s bootstrap file or in the core plugin’s file.
I also think it’s easiest to keep the template in its own template
directory where you can also house other templates. The main reason for this being that if you have custom rewrite rules, each rule can map to its own template.
In the example I’m going to give throughout this article, I’m going to have the rules located in the plugin bootstrap file which I’m calling plugin.php
and I’m going to have the template in a directory called templates
. The template will be called information.php
so that it maps to the permalink structure I’m going to set up.
So aim to have:
plugin.php
in the root of your plugintemplates/information.php
in the root of your plugin
Note that the information.php
template does not need to include the standard WordPress template headers because this isn’t that type of template.
2. The Template
This template can be as involved and complicated or as simple as you like. For the purposes of this post and to keep the code easy to follow, I’m just going to render user’s first name and last name from the user metadata table.
For example:
<!DOCTYPE html>
<html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title><?php echo getName($userId); ?></title> <link rel="stylesheet" href="<?php echo getAsset("/lib/reset.css"); ?>"> <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=PT+Sans"> <link rel="stylesheet" href="<?php echo getAsset("index.css"); ?>"> </head> <body> <div id="container"> <header> <h1>Hello World</h1> </header> <main> <?php echo get_user_meta($userId, 'first_name', true) . ' ' . get_user_meta($userId, 'last_name', true); ?> </main> <footer> <p>© <?php echo date('Y'); ?> Tom McFarlin</p> </footer> </div><!-- #container --> </body>
</html>
Obviously, though, you can mark this up however you find necessary. You can even incorporate additional functions that are in plugin.php
or whatever file you want to incorporate additional logic to add functionality to the template.
You may see that I have a getAsset
function in the head
element of this page. This is what that function looks like in plugin.php
:
function getAsset(string $filename)
{ if (0 < stripos($filename, '.css')) { return trailingslashit(plugins_url('acme-info')) . "/assets/css/$filename"; } if (0 < stripos($filename, '.js')) { return trailingslashit(plugins_url('acme-info')) . "/assets/js/$filename"; }
}
This is useful for that I’m doing in a template like this but additional functionality and the reason behind doing this is outside the scope of this article. I’m showing that it can be done.
3. The Rewrite Rules
To set up the custom rewrite rules, you’ll need three functions:
First, set up the rewrite rule whenever the plugin is activated so that it persists as long as the plugin is activated. To do this, set up a function that looks like this:
register_activation_hook(__FILE__, function () { add_rewrite_rule( '^information/([0-9]+)/?', 'index.php?user_id=$matches[1]', 'top' ); flush_rewrite_rules();
});
This will add a rewrite rule whenever the plugin is activated to that you can use information/42
and have it passed to the WordPress core application with the digit representing the ID for the user account you want to retrieve.
We also flush the rewrite rules when the plugin is activated so the new rules take affect.
Next, set up the deactivation hook so the custom rewrite rule is removed and is flushed from the WordPress application so no lingering functionality from the plugin works:
register_deactivation_hook(__FILE__, function () { flush_rewrite_rules();
});
Finally, set up the functionality so that it does the following:
- Filters the
REQUEST_URI
and sanitizes the URL, - Separates the URL into parts by running
explode
on the URL - Verifying the URL is properly structured
- Redirecting to the custom
information.php
template - Handling errors if the file doesn’t exists, the user ID is invalid, or the URL is invalid.
It’s a lot of code for stronger functionality (hence my post on using ChatGPT for writing more secure code) but it should be clear enough given the notes above.
add_action('template_redirect', function () { $requestUri = filter_var($_SERVER['REQUEST_URI'], FILTER_SANITIZE_URL); $requestUriParts = array_values(array_filter(explode('/', $requestUri))); if ( count($requestUriParts) === 2 && strtolower($requestUriParts[0]) === 'report' && ctype_digit($requestUriParts[1]) ) { $userId = intval($requestUriParts[1]); // Validate $userId and ensure it's within an appropriate range if ($userId > 0 && $userId <= 1000000) { // Adjust the upper limit as needed $templatePath = plugin_dir_path(__FILE__) . 'templates/information.php'; // Check if the template file exists before including it if (file_exists($templatePath)) { include $templatePath; exit; } else { // Handle the case where the template file is missing die('Report template not found.'); } } else { // Handle invalid user IDs die('Invalid user ID.'); } } else { // Handle invalid URLs die('Invalid URL.'); }
});
At this point, assuming you’re trying to access https://yourEnvironment.com/information/42
then you’ll be redirected to the custom template outlined in the second step above.
Notice that you’re still able to read the userId
and you’re able to use built-in WordPress functions to render whatever information you deem necessary for your solution.