COVESA runs a news and blog page that has to serve dozens of categories and hundreds of tags, and it still needs to page through years of posts without falling over. The build that solved it is a blog filter with pagination that runs on top of Divi, filters by category and tag over AJAX, and keeps numbered pagination working at the same time. That last part is where most attempts break.
This is the case study version of that work. Not a line-by-line code dump, but the decisions: why I skipped the plugins, how filtering and paging share one query, and the three things that bit me on the way. If you build on Divi for clients who care how the page looks, this is the pattern I reach for now.
What COVESA Actually Needed
COVESA is a standards organization for connected-vehicle software, and they publish often. Their news and blog page carries six top-level categories and well over a hundred tags. A visitor lands there to find one specific thing: posts about AOSP, or telematics, or a working group they follow.
So the page had two jobs that usually fight each other. Let a reader filter the list down to a category or a tag without a full page reload. And still let them walk through page 2, page 3, page 8 of the results, because there are far more posts than fit on one screen. A filter that forgets about pagination is half a feature. Pagination that forgets the active filter is worse, because it silently shows the wrong posts.
The site is a Divi child theme, so whatever I built had to sit inside the existing layout and not fight the designer's work. That ruled out the heavy filter plugins right away. Most of them want to own the loop and the markup, and the result rarely looks like the rest of a Divi page.
The Filter Bar That Lives Inside Divi
What shipped is a filter bar across the top of the blog page with two rows of controls. A row of category buttons, and a row of tag buttons below it. Click a category and the post grid below refreshes over AJAX to show only that category. Click a tag and it narrows again. The numbered pagination sits at the bottom and updates with every filter change.
The whole thing is custom code loaded through the snippet manager, not a plugin. About 200 lines of PHP, 80 lines of vanilla JavaScript, and 60 lines of CSS. No jQuery dependency I added, no premium tier, no third-party script between the visitor and the posts. It registers two shortcodes, one for the filter bar and one for the results grid, plus a single AJAX endpoint that does the actual querying.
I went with custom code here for the same reason I lean on the snippet manager over a child theme for most site customizations. It is portable, it is easy to read six months later, and it does exactly one job. If you want the longer argument, I wrote it up in Code Snippets vs Child Themes. For a feature like this, a tightly scoped snippet beats both a bloated plugin and a tangle of child-theme files.
How Filtering and Pagination Share One Query
The trick to a blog filter with pagination that does not break is to treat the filter and the page number as inputs to the same query, not two separate systems. Every AJAX request carries three things: the active category, the active tag, and the requested page number. The server builds one WP_Query from all three and returns both the posts and a fresh block of pagination links.
One WP_Query, every input
On the server, the AJAX handler reads the category, the tag, and the page off the request, then assembles the query arguments. The category and tag map to a taxonomy query. The page number maps to the paged argument, which is the WordPress-native way to ask for page N of a result set. According to the WordPress developer reference for WP_Query, paged is the parameter that drives which slice of posts you get back, so the page number has to overwrite it on every request.
$args = array(
'post_type' => 'post',
'posts_per_page' => 9,
'paged' => max( 1, absint( $_POST['paged'] ) ),
);
if ( ! empty( $_POST['category'] ) ) {
$args['category_name'] = sanitize_title( $_POST['category'] );
}
if ( ! empty( $_POST['tag'] ) ) {
$args['tag'] = sanitize_title( $_POST['tag'] );
}
$query = new WP_Query( $args );Because the filter and the page number flow through one query, the two features can never disagree. Page 3 of the AOSP category is just page 3 with the AOSP filter set. There is no second code path that can forget the active category, which is exactly the bug that plagues bolt-on pagination.
Pagination links the browser can trust
The same handler builds the pagination markup with paginate_links() and hands it back inside the AJAX response. The live page renders numbered links with a current-page marker and standard previous and next controls. When a reader clicks page 4, the JavaScript fires another AJAX call that carries the current filter plus the new page, and the grid and the pagination both refresh together.
This is the part that makes it feel native. The Divi blog area below the filter bar keeps its styling, the grid swaps in place, and the page never does a hard reload. A reader filtering by a working group can move through every page of that group's coverage without losing their place.
Why AJAX over a full page load
I could have done this with plain query-string links and full reloads. The live page even keeps real ?paged= URLs as a fallback. But on a list this dense, a full reload on every filter click is a jarring experience. AJAX keeps the header, the filter bar, and the scroll position steady while only the result grid changes. The reader stays oriented. That orientation matters more on a research-heavy site than on a five-post personal blog.
The Three Things That Bit Me
No real build goes clean. Three problems showed up, and all three are the kind that pass in testing and fail in the wild.
The nonce that expires overnight
Every AJAX request is protected by a WordPress nonce, passed to the JavaScript with wp_localize_script(). The catch is that nonces do not live forever. According to the WordPress nonces documentation, a nonce is only valid for a window of roughly 12 to 24 hours. Leave the blog page open in a tab overnight, come back, click a filter, and the request fails because the nonce is stale.
The fix is to localize the nonce fresh on every page render rather than caching it anywhere, and to fail gracefully on the client if a request comes back rejected. The nonce ships with the page, so a normal visit always carries a current one. The only people who hit the wall are the ones who leave the tab parked for a day, and for them a reload mints a new nonce.
Page caching that freezes the first paint
Page caching is great until it serves a stale snapshot of an interactive component. The blog page gets cached with whatever filter state it had when the cache was built. A visitor could land on a cached page that looks like it is already filtered, which is confusing.
The answer is to let JavaScript own the state after the first paint. The cached HTML renders an unfiltered, page-one view as the safe default. As soon as the script runs, it reads the real state and takes over. The cache can serve the shell all day long, and the live behavior is always driven by the client, not by whatever was frozen into the snapshot.
Hundreds of tags as buttons is a wall of noise
COVESA has over a hundred tags. My first pass rendered every one as a button. The live page still carries that full set, and it is a lot. A filter bar with that many controls stops being a filter and starts being a wall. The reader cannot find the tag they want in the noise.
The lesson I took from it is to cap what you show. Lead with the categories, which are few and meaningful, and treat the long tag list as a secondary control that can be capped, collapsed, or made searchable rather than dumped on screen all at once. Rendering everything is the easy choice and the wrong one. The right move is to respect the reader's attention and show the handful of filters that actually drive most of the clicks.
What We Gave Up by Skipping the Plugin
Custom code is not free. By writing this instead of installing a filter plugin, I took on the maintenance. If WordPress changes how paginate_links() or taxonomy queries behave, that is mine to fix, not a vendor's. A plugin would also have shipped niceties I did not build, like saved filter presets or a search box wired into the same query.
The other tradeoff is that the snippet lives in the snippet manager, so anyone maintaining the site has to know it is there. It is not a plugin card you can spot in a glance at the plugins list. I document that in the project notes so the next person, including future me, does not go looking for a plugin that was never installed.
I made that trade on purpose. The plugin overhead, the layout fight with Divi, and the premium upsell for features COVESA did not need all cost more than the maintenance of 340 well-scoped lines. For a client who cares how the page looks, keeping the build inside the Divi layout was worth owning the code.
What the Page Does Today
The news and blog page is live and doing its job. The filter bar sits at the top with category and tag controls. The post grid refreshes over AJAX when a filter changes. Numbered pagination runs underneath, carrying the active filter through every page, with the pages I counted on the live view running well past page eight of results.
No plugin appears in the plugins list for any of it. There is no premium license to renew, no layout override fighting the Divi design, and no third-party script logging visitors. The feature is just part of the site. That is the outcome I want from work like this: something that looks built in, not bolted on.
The Divi context matters here too. Building on top of the existing Divi page builder meant the blog area kept its styling for free, so the custom query slotted in behind a layout the designer already approved.
Would I Build It This Way Again
Yes, with one change. The pattern is right: one query, every input, AJAX over the grid, pagination that shares the filter state. I would not go back to a plugin for this. The single-query approach is what keeps filtering and paging from ever disagreeing, and that is the whole game.
The one thing I would do differently is cap the tags from the start instead of rendering all of them and learning the hard way. That was an hour I did not need to spend. Everything else, I would build the same way tomorrow.
Here is the thing about custom blog features. They are not about showing off code. They are about giving a reader a page that gets out of the way and lets them find what they came for. If you want the broader take on making blog content findable, I covered it in my guide to creating blog posts that actually get found online. A good filter is part of that same job: helping the right reader reach the right post.
Frequently Asked Questions
How do you keep WordPress filtering and pagination from breaking each other?
Treat the filter and the page number as inputs to one query instead of two systems. Every AJAX request carries the active category, the active tag, and the requested page number, and the server builds a single WP_Query from all three. Because there is only one code path, the page number can never forget the active filter, which is the bug that breaks most bolt-on pagination.
Do you need a plugin to build a blog filter with pagination on Divi?
No. The COVESA build is custom code loaded through the snippet manager, about 200 lines of PHP, 80 lines of vanilla JavaScript, and 60 lines of CSS. It registers two shortcodes and one AJAX endpoint. Skipping the plugin kept the feature inside the existing Divi layout instead of letting a plugin take over the markup and styling.
Why does an AJAX filter stop working after the page sits open overnight?
The AJAX requests are protected by a WordPress nonce, and a nonce is only valid for roughly 12 to 24 hours. If a visitor leaves the page open past that window and then clicks a filter, the request fails because the nonce is stale. The fix is to localize a fresh nonce on every page render and fail gracefully on the client, so a reload mints a new valid nonce.
How does page caching interfere with an AJAX blog filter?
Page caching can serve a frozen snapshot that looks like it is already filtered, which confuses visitors. The fix is to let the cached HTML render an unfiltered, page-one view as the safe default, then let JavaScript read the real state and take over after the first paint. The cache serves the shell and the client drives the live behavior.
The Complete Code
That is the walkthrough. Here is the entire snippet in one block so you can take it and use it. This is the code as it is actually installed, not a cleaned-up version written for the article. It is long because the filter bar, the query, the pagination and the styling all live in the same snippet on purpose. Paste it into Code Snippets as a PHP snippet, set it to run everywhere, and put the shortcode on your archive page.
Copy the block below with the Copy button in its top right corner.
function truncate_string_by_word_pag($string, $length) {
if (strlen($string) <= $length) {
return $string;
}
$truncated = substr($string, 0, $length);
$last_space = strrpos($truncated, ' ');
if ($last_space !== false && $last_space > 0) {
$truncated = substr($truncated, 0, $last_space);
}
return $truncated . '...';
}
add_shortcode('custom_blog_archive_pag', 'custom_blog_archive_shortcode_pag');
function custom_blog_archive_shortcode_pag() {
ob_start();
$querystr = "";
$search = isset($_GET['search']) ? $_GET['search'] : '';
$category = isset($_GET['category']) ? urldecode($_GET['category']) : '';
$tag = isset($_GET['tag']) ? urldecode($_GET['tag']) : '';
$paged = (get_query_var('paged')) ? get_query_var('paged') : 1;
$base_url = get_permalink();
$posts_per_page = 12;
$offset = ($paged - 1) * $posts_per_page;
if (empty($search) && empty($category) && empty($tag)) {
$querystr = "Categories NOT LIKE 'Spotlight'";
} else {
$querystr = "
(Title LIKE '%$search%' OR Excerpt LIKE '%$search%')
AND (Categories LIKE '%$category%' OR '$category' = '')
AND (Tags LIKE '%$tag%' OR '$tag' = '')
";
}
?>
<style>
.blog-container {
display: flex;
max-width: 1080px;
margin: auto;
}
.blog-sidebar {
width: 28%;
box-sizing: border-box;
padding-right: 50px;
}
.blog-posts {
width: 72%;
}
@media (max-width: 981px) {
.blog-sidebar {
width: 33%;
}
.blog-posts {
width: 67%;
}
}
@media (max-width: 768px) {
.blog-container {
flex-direction: column;
}
.blog-sidebar,
.blog-posts {
width: 100%;
padding-right: 0; /* Remove padding, as it's no longer needed */
}
.post {
flex-direction: column; /* Ensure elements stack vertically */
height: auto !important; /* Adjust height for stacking */
}
.post-image {
flex-basis: 100%; /* Take full width */
padding-right: 0 !important; /* Remove padding */
}
.post-content {
flex-basis: 100%; /* Take full width */
padding-right: 0 !important; /* Remove padding */
padding-top: 20px;
padding-bottom: 20px;
}
}
.post {
display: flex;
flex-wrap: wrap; /* Allows items to wrap if needed */
align-items: flex-start; /* Aligns items at the top of the flex container */
margin-bottom: 20px;
padding: 15px;
border-radius: 10px;
height: 235px;
}
.post-image {
flex-basis: 30%; /* Adjust width as needed */
display: flex;
flex-direction: column;
padding-right: 20px;
height: 100%;
}
.post-content {
flex-basis: 70%; /* Adjust width as needed */
display: flex;
flex-direction: column;
justify-content: space-between; /* Pushes the last item (tags) to the bottom */
height: 205px; /* Ensures the container takes full height */
}
form {
padding: 20px;
background-color: white;
border-radius: 10px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.form-element {
margin-bottom: 10px;
}
.form-titles {
font-family: 'Open Sans';
font-size: 18px;
font-weight: 700;
color: #1d1d1d;
margin-bottom: 10px;
}
input[type="text"] {
padding: 8px;
width: calc(100% - 85px); /* Adjust width to fit the search button */
display: inline-block;
}
.search-button {
padding: 8px 16px;
color: #FFFFFF!important;
border-color: #00aab7;
border-radius: 7px;
font-weight: 600!important;
background-color: #00aab7;
}
.category-link {
display: block; /* Make each link block level for better control */
color: #1d1d1d;
text-decoration: none;
font-weight: 600;
padding: 4px;
border: 0px solid #007BFF;
margin-bottom: 2px; /* Space between categories */
}
.tag-button {
padding: 4px 8px;
background-color: #1d1d1d;
border: none;
border-radius: 20px; /* Rounded corners */
color: white;
cursor: pointer;
display: inline-block;
line-height: normal;
text-align: center;
margin: 5px; /* Space between tags */
}
.reset-button {
padding: 8px 15px;
text-align: center;
font-weight: 600;
border: none;
color: #1d1d1d;
cursor: pointer;
display: block;
text-decoration: none;
}
.img-style{
margin-top: auto;
margin-bottom: auto;
//max-width: 100%;
height: auto;
}
.date-style{
font-size: 1em;
color: grey;
}
.h2-style{
font-size: 20px;
color: #1d1d1d;
font-weight: 800;
line-height: 1em;
padding-bottom: 5px;
}
.excerpt-style{
font-size: 18px;
color: #1d1d1d;
font-weight: 600;
line-height: 1.4em;
}
.category-style, .tag-style{
font-size: 1em;
color: #1d1d1d;
font-weight: 600;
//line-height: 1.6em;
}
.tag-style {
margin-top: auto !important;
}
.pagination {
display: flex;
justify-content: center;
align-items: center;
margin-top: 20px;
}
.pagination a {
margin: 0 5px;
padding: 8px 12px;
text-decoration: none;
border: 1px solid #ddd;
color: #0c71c3;
border-radius: 4px;
}
.pagination a:hover {
background-color: #0c71c3;
color: white;
}
.pagination-current {
margin: 0 5px;
padding: 8px 12px;
border: 1px solid #0c71c3;
background-color: #0c71c3;
color: white;
border-radius: 4px;
}
.pagination-prev, .pagination-next {
font-weight: bold;
}
@media (max-width: 768px) {
.hide-on-mobile {
display: none !important;
}
}
</style>
<div class="blog-container">
<div class="blog-sidebar">
<form action="" method="GET">
<div class="form-element">
<p class="form-titles">SEARCH BLOG ARTICLES</P>
</div>
<div class="form-element">
<input style="width:100%; border-radius: 7px;" type="text" name="search" value="<?php echo htmlspecialchars($search); ?>" placeholder="Enter keywords...">
</div>
<div class="form-element">
<button type="submit" class="search-button">Search</button>
</div>
<div class="form-element" style="display: flex; align-items: center; margin-top: 30px;">
<img loading="lazy" decoding="async" style="height: 15px;" src="/wp-content/uploads/2024/05/category_icon.png" alt="" title="category_icon" >
<p class="form-titles" style="margin-left: 10px; margin-bottom: 0px;">CATEGORIES</P>
</div>
<!-- Dynamically generate category links -->
<?php
//foreach (get_categories() as $cat):
$spotlight = get_term_by('name', 'Spotlight', 'category');
$spotlight = get_term_by('name', 'Spotlight', 'category');
if ($spotlight) {
$categories = get_terms(array(
'taxonomy' => 'category',
'exclude' => array($spotlight->term_id),
'hide_empty' => true,
));
foreach ($categories as $cat) {
$url = $base_url . '?' . http_build_query(array_filter(array(
'category' => urlencode($cat->name),
'search' => !empty($search) ? urlencode($search) : null,
'tag' => !empty($tag) ? urlencode($tag) : null
)));
$style = ($category == $cat->name) ? 'color: #00aab7;' : '';
$stylecheck = ($category == $cat->name) ? 'style="color: #00aab7;"' : 'style="color: #fff;"';
?>
<div class="form-element" style="display: flex; align-items: center;">
<svg <?php echo $stylecheck; ?> width="15" height="15" viewBox="0 0 448 512" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"><path fill="currentColor" d="M438.6 105.4c12.5 12.5 12.5 32.8 0 45.3l-256 256c-12.5 12.5-32.8 12.5-45.3 0l-128-128c-12.5-12.5-12.5-32.8 0-45.3s32.8-12.5 45.3 0L160 338.7 393.4 105.4c12.5-12.5 32.8-12.5 45.3 0z"/></svg>
<a href="<?php echo $url; ?>" class="category-link" style="margin-left: 10px;<?php echo $style; ?>"><?php echo strtoupper($cat->name); ?></a>
</div>
<?php
}
}
?>
<!--
<div class="form-element hide-on-mobile" style="display: flex; align-items: center; margin-top: 20px;">
<img loading="lazy" decoding="async" style="height: 15px;" src="/wp-content/uploads/2024/05/tag_icon-1.png" alt="" title="tag_icon" >
<p class="form-titles" style="margin-left: 10px; margin-bottom: 0px;">TAGS</P>
</div>
<div class="form-element hide-on-mobile">
-->
<!-- Dynamically generate tag buttons -->
<?php
$args = array(
'posts_per_page' => -1,
'fields' => 'ids',
'tax_query' => array(
array(
'taxonomy' => 'category',
'field' => 'slug',
'terms' => 'spotlight',
'operator' => 'NOT IN'
)
)
);
$post_ids = get_posts($args);
if (!empty($post_ids)) {
$tags = get_terms(array(
'taxonomy' => 'post_tag',
'object_ids' => $post_ids, // Use the post IDs from the previous query
'hide_empty' => false // Optionally hide tags not assigned to any posts
));
foreach ($tags as $tag_item) {
$url = $base_url . '?' . http_build_query(array_filter(array(
'tag' => urlencode($tag_item->name),
'search' => !empty($search) ? urlencode($search) : null,
'category' => !empty($category) ? urlencode($category) : null
)));
$style = ($tag == $tag_item->name) ? 'background-color: #00aab7;' : '';
?>
<!-- <a href="<?php echo $url; ?>" class="tag-button" style="<?php echo $style; ?>"><?php echo $tag_item->name; ?></a> -->
<?php
}
} else {
echo 'No posts found outside of the Spotlight category.';
}
?>
<!-- </div> -->
<div class="form-element">
<a href="<?php echo get_permalink(); ?>" class="reset-button"><svg width="12" height="12" viewBox="0 0 384 512" xmlns="http://www.w3.org/2000/svg" style="vertical-align: middle;" aria-hidden="true"><path fill="currentColor" d="M342.6 150.6c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0L192 210.7 86.6 105.4c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3L146.7 256 41.4 361.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0L192 301.3l105.4 105.3c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L237.3 256l105.3-105.4z"/></svg> RESET ALL FILTERS</a>
</div>
</form>
</div>
<div class="blog-posts">
<?php
global $wpdb;
$query = "
SELECT SQL_CALC_FOUND_ROWS * FROM (
SELECT
p.ID,
DATE_FORMAT(p.post_date, '%M %d, %Y') as Date,
p.post_title AS Title,
p.post_excerpt AS Excerpt,
p.post_name as slug,
MAX(wp.guid) AS Featured,
GROUP_CONCAT(DISTINCT CASE WHEN tax.taxonomy = 'category' THEN cat.name END ORDER BY cat.name ASC SEPARATOR ', ') AS Categories,
GROUP_CONCAT(DISTINCT CASE WHEN tax.taxonomy = 'post_tag' THEN tag.name END ORDER BY tag.name ASC SEPARATOR ', ') AS Tags
FROM
wp_posts AS p
LEFT JOIN
wp_term_relationships AS rel ON rel.object_id = p.ID
LEFT JOIN
wp_term_taxonomy AS tax ON tax.term_taxonomy_id = rel.term_taxonomy_id
LEFT JOIN
wp_terms AS cat ON cat.term_id = tax.term_id AND tax.taxonomy = 'category'
LEFT JOIN
wp_terms AS tag ON tag.term_id = tax.term_id AND tax.taxonomy = 'post_tag'
LEFT JOIN
wp_postmeta AS pm ON p.ID = pm.post_id AND pm.meta_key = '_thumbnail_id'
LEFT JOIN
wp_posts AS wp ON wp.ID = pm.meta_value
WHERE
p.post_status = 'publish' AND
p.post_type = 'post'
GROUP BY
p.ID
ORDER BY
p.post_date DESC
) as t4 WHERE 1 AND {$querystr}
LIMIT $offset, $posts_per_page
";
$posts = $wpdb->get_results($query);
$total_posts = $wpdb->get_var("SELECT FOUND_ROWS()");
if (!empty($posts)) {
$last_title = '';
$i = 0;
foreach ($posts as $post) {
$truncated_excerpt = truncate_string_by_word_pag($post->Excerpt, 200);
if ($last_title != $post->Title) {
$i++;
$class = ($i % 2 == 0) ? '#fff' : 'rgba(29, 29, 29, 0.05)';
?>
<div class='post' style='background-color: <?php echo $class; ?>;'>
<div class='post-image'>
<?php if (!empty($post->Featured)): ?>
<div class="img-style"><a href="/<?php echo $post->slug; ?>" class=""><img src='<?php echo $post->Featured; ?>' alt='<?php echo $post->Title; ?>'></a></div>
<?php endif; ?>
</div>
<div class='post-content'>
<div class="date-style"><?php echo $post->Date; ?></div>
<div><h2 class="h2-style"><a href="/<?php echo $post->slug; ?>" class=""><?php echo $post->Title; ?></a></h2></div>
<div class="excerpt-style"><?php echo $truncated_excerpt; ?></div>
<?php if (!empty($post->Categories)): ?>
<!--<div class="category-style"><strong>Categories:</strong> <?php echo $post->Categories; ?></div>-->
<?php endif; ?>
<?php if (!empty($post->Tags)): ?>
<div class="tag-style" style="display: flex; align-items: center;"><img loading="lazy" decoding="async" style="height: 15px;" src="/wp-content/uploads/2024/05/tag_icon-1.png" alt="" title="tag_icon" ><span style="margin-left: 10px;"><?php echo $post->Tags; ?></span></div>
<?php endif; ?>
</div>
</div>
<?php
$last_title = $post->Title;
}
}
} else {
echo "<p>No posts found.</p>";
}
?>
<!-- Pagination controls -->
<div class="pagination">
<?php
$total_pages = ceil($total_posts / $posts_per_page);
if ($total_pages > 1) {
// Previous page link
if ($paged > 1) {
$prev_url = add_query_arg(array_filter(array(
'paged' => $paged - 1,
'search' => !empty($search) ? urlencode($search) : null,
'category' => !empty($category) ? urlencode($category) : null,
'tag' => !empty($tag) ? urlencode($tag) : null
)), $base_url);
echo "<a href='$prev_url' class='pagination-prev'>« Previous</a>";
}
// Page numbers
for ($i = 1; $i <= $total_pages; $i++) {
$url = add_query_arg(array_filter(array(
'paged' => $i,
'search' => !empty($search) ? urlencode($search) : null,
'category' => !empty($category) ? urlencode($category) : null,
'tag' => !empty($tag) ? urlencode($tag) : null
)), $base_url);
if ($i == $paged) {
echo "<span class='pagination-current'>$i</span>";
} else {
echo "<a href='$url' class='pagination-link'>$i</a>";
}
}
// Next page link
if ($paged < $total_pages) {
$next_url = add_query_arg(array_filter(array(
'paged' => $paged + 1,
'search' => !empty($search) ? urlencode($search) : null,
'category' => !empty($category) ? urlencode($category) : null,
'tag' => !empty($tag) ? urlencode($tag) : null
)), $base_url);
echo "<a href='$next_url' class='pagination-next'>Next »</a>";
}
}
?>
</div>
</div>
</div>
<?php
return ob_get_clean();
}



0 Comments