[BUG] Category pagination — cannot return to page 1 when SEF enabled

Started by Gingerweb, July 13, 2026, 16:53:15 PM

Previous topic - Next topic

Gingerweb

Category pagination — cannot return to page 1 when SEF enabled (limitstart lost, session retains last page)

Version: VirtueMart 4.6.8 (11258), Joomla 5.4.6, PHP 8.x, Joomla core SEF enabled (no third-party SEF/router)
Summary:

On a category with more than one page of products, navigating to page 2 and then clicking the "1" (or "Start"/"Prev") pagination link does not return the visitor to page 1. The page reloads still showing page 2.
Steps to reproduce:

Enable Joomla SEF URLs.
Open a category with 2+ pages of products (default limit, e.g. 12 per page).
Click "2" — URL becomes /category/results,13-24, page 2 displays correctly.
Click "1" (or « / ‹).
Observed: browser navigates to the bare category URL /category and page 2 is still shown.
Expected: page 1 is displayed.

Root cause (traced):
Two interacting pieces of core code:

In administrator/components/com_virtuemart/models/product.php (setPaginationLimits), when the visitor is already within the same category/manufacturer, limitStart is read via getUserStateFromRequest() against a per-category session key (com_virtuemart.<view>c<cateid>m<manid>.limitstart). This only overrides the stored session value if limitstart is explicitly present in the request.

In components/com_virtuemart/router.php, the build routine only emits a results,X-Y segment when limitstart > 0. For page 1 (limitstart = 0) no segment is added, so the page-1 pagination link is generated as the bare category URL. Joomla's SEF router additionally strips limitstart=0 as it equals the default.

Result: the page-1 link carries no page position. When it loads, getUserStateFromRequest() finds no limitstart in the request and falls back to the session value, which still holds the offset for page 2 — so the visitor is stuck on page 2.

Note: This only reproduces with an active session already advanced past page 1. A fresh/cookieless request to /category/results,1-12 returns page 1 correctly, which is why it can look intermittent.

Suggested fix direction:
Either (a) have the router emit an explicit results,1-N segment for limitstart = 0 so the page-1 link isn't collapsed to the bare URL, or (b) in the category model, treat a request with no limitstart/start parameter as page 1 rather than inheriting the stored session offset.

sirius

Hi Gingerweb,

I can confirm this bug, and your diagnosis is exactly right. I ran into the same issue on Joomla 5.4.6 / VirtueMart 4.6.8 / PHP 8.x and traced it to the same two interacting pieces:

  • vmpagination.php builds the "Start", "Previous" and "Page 1" links with limitstart=0&limit=N
  • the SEF router strips limitstart=0 because it is the default value, so the request arrives with no pagination parameter at all
  • the model then calls getUserStateFromRequest('limitstart', ...), which falls back to the value stored in the session, so you stay stuck on the current page

Two things I tried that do NOT work, in case it saves someone time:

  • A template override of the pagination chrome that appends start=0 to the href: the canonical SEF redirect strips the parameter server side anyway, so the effective URL is bare again and the bug persists.
  • Patching vmpagination.php directly: it works but core hacks get wiped on every VM update, so I ruled it out.

The fix that works reliably for me is a tiny system plugin acting on the incoming request instead of the generated URL. On onAfterRoute (after Joomla has parsed the URL and populated the input), if the request targets a paginated VirtueMart view and carries no limitstart at all, force it to 0:

public function onAfterRoute(Event $event): void
{
    $app = Factory::getApplication();
    if (!$app->isClient('site')) {
        return;
    }
    $input = $app->getInput();
    if ($input->getCmd('option') !== 'com_virtuemart') {
        return;
    }
    $view = $input->getCmd('view');
    if (!\in_array($view, ['category', 'virtuemart', 'manufacturer', 'search'], true)) {
        return;
    }
    // A real pagination request (page 2+) always carries limitstart in the URL.
    // A bare URL means "page 1", so do not let the session value win.
    if ($input->get('limitstart', null) !== null) {
        return;
    }
    $input->set('limitstart', 0);
}

This covers every path back to page 1: the Start / Previous / Page 1 pagination links, menu links, breadcrumbs and bookmarked category URLs.

One side effect to be aware of: any bare VirtueMart URL (without limitstart) now always lands on page 1 instead of restoring the page remembered in the session. In my opinion that is the expected behaviour anyway (it is what every visitor coming from a menu link or an external link would expect), but it is worth mentioning.

It would be nice to have this handled in VM core, for example by having vmpagination.php emit the page 1 links with a parameter the SEF layer does not strip, or by not falling back to the session state when the request is a plain category URL.

Hope this helps.
J5.4.7 | PHP 8.4.10 + Redis + APC + Opcode
Litespeed | MariaDB 10.6.22
VM Prod : 4.6.8 11258 | VM Test : 4.9.3 11254

wyder

I can add another real-world confirmation of this issue.

On a production VirtueMart store, we had exactly the same behaviour:

  • page 2 and subsequent pages worked correctly, for example:

/category/results,81-160.html

  • clicking page 1 changed the URL back to the bare category URL:

/category.html

However, the products from page 2 were still displayed.

The problem appeared only in some categories, which initially made it look inconsistent.

After comparing the Joomla menu items assigned to the affected and unaffected VirtueMart categories, we found an important difference.

Some category menu items contained only a short link such as:

index.php?option=com_virtuemart&view=category&virtuemart_category_id=2

After opening and saving the same menu item in Joomla, its link was expanded with additional parameters:

index.php?option=com_virtuemart&view=category&virtuemart_category_id=2&virtuemart_manufacturer_id=0&virtuemart_manufacturercategories_id=0&clearCart=0&limitstart=0

After updating the affected menu items, returning from page 2 to page 1 started working correctly.

The most relevant parameter for this pagination issue is:

&limitstart=0

Even though limitstart=0 is not visible in the final SEF URL, it is stored in the active Joomla menu item.

This appears to be consistent with Joomla routing behaviour. When parsing a URL, Joomla merges the active menu item's query variables with the variables parsed from the route:

$active = $this->router->menu->getActive();

if ($active) {
$vars = array_merge($active->query, $vars);
}

Therefore, even if the SEF router removes limitstart=0 from the visible URL, the value can still be supplied through the active menu item and made available to VirtueMart during the request.

This may also explain why the issue appears only in some categories or installations:

  • some VirtueMart category menu items already contain limitstart=0,
  • older or incomplete menu items may contain only the shortened link,
  • categories without a matching menu item cannot benefit from this workaround.

In our case, the store contained approximately 600 VirtueMart category menu items, so opening and saving them manually was not practical.

We applied the workaround directly in the Joomla menu table.

First, we checked which menu items would be affected:

SELECT
`id`,
`title`,
`link`
FROM `yourprefix_menu`
WHERE `link` LIKE 'index.php?option=com_virtuemart&view=category%';

Then we appended the missing parameters only when they were not already present:

UPDATE `yourprefix_menu`
SET `link` = CONCAT(
`link`,
IF(
`link` NOT LIKE '%virtuemart_manufacturer_id=%',
'&virtuemart_manufacturer_id=0',
''
),
IF(
`link` NOT LIKE '%virtuemart_manufacturercategories_id=%',
'&virtuemart_manufacturercategories_id=0',
''
),
IF(
`link` NOT LIKE '%clearCart=%',
'&clearCart=0',
''
),
IF(
`link` NOT LIKE '%limitstart=%',
'&limitstart=0',
''
)
)
WHERE `link` LIKE 'index.php?option=com_virtuemart&view=category%'
AND `published` = 1;

yourprefix_menu must be replaced with the actual Joomla menu table name, for example:

abc_menu

The Joomla #__menu placeholder cannot be used directly in phpMyAdmin.

A database backup should of course be made before running the UPDATE query.

After running the query and clearing the Joomla and VirtueMart caches, the affected categories correctly returned visitors from page 2 or later to page 1.

I did not test whether all the additional zero-value parameters are required individually. For this particular pagination problem, limitstart=0 is clearly the relevant parameter.

The other parameters were added because they matched the complete link generated after opening and saving a VirtueMart category menu item in Joomla.

This database change should still be treated only as a workaround, not as a core fix.

It depends on the request being associated with a matching Joomla menu item. It may not fully cover:

  • category URLs opened from external websites,
  • bookmarked bare category URLs,
  • categories without their own Joomla menu item,
  • breadcrumbs using a different menu context,
  • manufacturer or search result pagination.

The system plugin proposed above is therefore a broader workaround because it acts directly on the incoming request.

However, the underlying issue should still be fixed in VirtueMart core. A bare category URL should reliably open page 1 instead of inheriting the previous limitstart value stored in the visitor's session.