PagePathHistory

Tracks historical URLs for pages and automatically performs 301 (permanent) redirects when a past URL is requested

Whenever a page is moved or renamed, PagePathHistory records the previous URL and redirects visitors to the new location — preventing broken links and preserving SEO value.

// The module is autoloaded and available via $modules:
$pathHistory = $modules->get('PagePathHistory');

// Or from any Wire-derived object:
$pathHistory = $wire->modules->get('PagePathHistory');

PagePathHistory hooks into Pages::moved, Pages::renamed, and ProcessPageView::pageNotFound automatically. Most of the time you don't need to call it directly — it works silently in the background. Use the API methods when you need to query history, manually record paths, or integrate custom redirect logic.

Expand all      API reference

Properties
PropertyTypeDefaultDescription
minimumAgeint120Minimum age in seconds a page must have before its previous path is recorded.
rootSegmentsarray|boolfalseCached list of all root-level path segments ever recorded. Automatically rebuilt.
Constants
ConstantValueDescription
dbTableName'page_path_history'Name of the database table used by this module
minimumAge120Default minimum age (seconds) before tracking a page's path
maxSegments10Maximum path segments for recursive lookups
Retrieval

getPathHistory($page, $options = [])

Returns an array of all paths a page has previously had, ordered oldest to newest.

// Get all historical paths for a page as strings
$paths = $pathHistory->getPathHistory($page);
// ['/old-blog/my-post/', '/blog/old-slug/', '/blog/my-post/']

// Limit to a specific language
$paths = $pathHistory->getPathHistory($page, $languages->get('de'));

// Get verbose info (dates, languages)
$paths = $pathHistory->getPathHistory($page, ['verbose' => true]);
foreach($paths as $p) {
    echo $p['path'] . ' — ' . $p['date'] . "\n";
}

// Shortcut: boolean true enables verbose mode
$paths = $pathHistory->getPathHistory($page, true);

Options array:

OptionTypeDefaultDescription
languageLanguage|int|stringnullLimit results to this language. null = all languages.
verboseboolfalseReturn associative array with path, date, and language keys.
virtualbooltrueInclude auto-determined virtual entries from parent history.

When virtual is true (default), the method also includes paths resulting from parent page moves — URLs that would have been valid for the page even though the page itself didn't move. This gives a complete picture of all URLs that once led to the page.

getPathInfo($path, array $options = [])

Returns detailed information about a path if it exists in the history. Returns an associative array with the following keys:

KeyTypeDescription
idintID of the matched page, or 0 if no match
pathstringThe matched path
language_idintLanguage ID, if applicable
templates_idintTemplate ID of the matched page
parent_idintParent ID of the matched page
statusintStatus flags of the matched page
createdstringISO-8601 date string when the entry was created
namestringPage name (in default language)
matchTypestring'exact' for exact match, 'partial' for URL segments match, or '' for no match
urlSegmentStrstringURL segments portion of the path (only for partial matches)
$info = $pathHistory->getPathInfo('/old-url/');

if($info['id']) {
    echo "Found page ID {$info['id']} at {$info['path']}\n";
}

// Allow partial matches with URL segments
$info = $pathHistory->getPathInfo('/old-url/segment1/segment2/', [
    'allowUrlSegments' => true
]);
if($info['matchType'] === 'partial') {
    echo "URL segments: {$info['urlSegmentStr']}\n";
}

Only pages with templates that allow URL segments are considered for partial matches.

getPage($path, $level = 0)

Given a historical path, returns the Page object that once lived there, or a NullPage if no match is found. If the path is language-specific, the returned page will have a _language property set to the corresponding Language object.

$page = $pathHistory->getPage('/old-blog/my-post/');

if($page->id) {
    // Check if this redirect is for a specific language
    $language = $page->get('_language');
    if($language) {
        echo "Redirecting to {$language->title} version: {$page->url}\n";
    } else {
        echo "Redirecting to: {$page->url}\n";
    }
}

This method traverses path segments recursively (up to maxSegments levels) to find matches even when parent paths have changed multiple times.

getRootSegments()

Returns an array of all root-level segments (first path component after /) that have ever appeared in the path history. Useful for efficient preliminary checks before performing deeper lookups.

$segments = $pathHistory->getRootSegments();
// ['blog', 'about', 'products', 'old-site', …]

isRootSegment($segment)

Returns true if the given segment has ever existed as a root-level path component in the history. Accepts either a single segment or a full path (the root segment is extracted automatically).

if($pathHistory->isRootSegment('blog')) {
    // 'blog' has existed at the root level before
}

// Also accepts full paths:
if($pathHistory->isRootSegment('/old-site/some/page')) {
    // '/old-site/' is a known root segment
}
Manipulation

setPathHistory(Page $page, $path, $language = null)

Records a historical path for a page and removes any existing entries for the page's current path (since those are no longer applicable). Returns true on success, or false if the path is already consumed in history.

// Record a path for the default language
$pathHistory->setPathHistory($page, '/old-path/');

// Record a language-specific path
$pathHistory->setPathHistory($page, '/de/alter-pfad/', $languages->get('de'));

This is the preferred method for recording history. It calls addPathHistory() internally and then cleans up overlapping current-path entries.

addPathHistory(Page $page, $path, $language = null)

Adds a historical path without cleaning up current-path entries. Returns true if the path was added, or false if it overlaps with an existing live path (checked via PagePaths when available).

$pathHistory->addPathHistory($page, '/some-historical-path/');

Note: The path cannot collide with an existing page's current URL. If a live page already occupies that path, the method returns false and does not record the entry.

deletePathHistory(Page $page, $path)

Deletes a single historical path entry for the given page. Returns the number of rows deleted (0 or 1).

$deleted = $pathHistory->deletePathHistory($page, '/old-url/');
if($deleted) echo "Path removed from history\n";

deleteAllPathHistory($page)

Deletes all historical paths. Pass a Page object to delete history for one page, or true to delete all history for all pages.

// Delete all history for one page
$pathHistory->deleteAllPathHistory($page);

// Delete all history for every page
$pathHistory->deleteAllPathHistory(true);
Hooks

PagePathHistory listens to several core hooks and also provides hookable methods for its own lifecycle.

Hooks added by the module

HookWhen
Pages::movedAfter a page is moved to a different parent
Pages::renamedAfter a page is renamed
Pages::deletedAfter a page is deleted (cleans up history)
ProcessPageView::pageNotFoundWhen a 404 occurs (performs automatic redirect)
Page::addUrl($url, $language)When adding a custom URL via $page->addUrl()
Page::removeUrl($url, $language)When removing a custom URL via $page->removeUrl()

After installation, $page->addUrl() and $page->removeUrl() become available as methods on every Page object:

// Add a custom historical URL for the page
$page->addUrl('/custom-redirect/', $languages->get('de'));

// Remove a previously added URL
$page->removeUrl('/custom-redirect/');

Hookable module methods

HookWhenArguments
PagePathHistory::installModule is installed
PagePathHistory::uninstallModule is uninstalled
PagePathHistory::upgradeModule is upgraded$fromVersion, $toVersion
// Hook after install to seed initial history
$wire->addHookAfter('PagePathHistory::install', function(HookEvent $event) {
    $pathHistory = $event->object;
    // Populate history from existing pages...
});
Configuration

The module provides one configurable setting via the admin UI:

Minimum age (seconds) — Pages must exist for at least this many seconds before their previous paths are recorded. Default is 120 seconds (2 minutes). This prevents recording history for pages that are immediately moved or renamed during initial setup.

// Set via API
$pathHistory = $modules->get('PagePathHistory');
$pathHistory->minimumAge = 300; // 5 minutes
$modules->saveConfig('PagePathHistory', 'minimumAge', 300);

The module configuration screen also allows deleting all recorded historical paths.

Notes
  • Autoload: This is an autoload module — it initializes on every request.
  • Automatic redirects: When a 404 occurs, the module looks up the requested path in its history. If found, it issues a 301 Moved Permanently redirect to the page's current URL, including the correct language when applicable.
  • Trash exclusion: Paths involving pages in the trash are not recorded.
  • Recovery format exclusion: Paths matching the trash recovery format (e.g. /blog/5134.3096.83_page-name) are excluded.
  • Multi-language: Supports LanguageSupportPageNames and records language-specific historical paths. When a language-specific redirect is triggered, the visitor is redirected to the correct language version of the page.
  • URL segments: getPathInfo() can match paths with additional URL segments, but only when the matched page's template has URL segments enabled.
  • $page->addUrl() and $page->removeUrl() are added to every Page object by this module, enabling programmatic URL history management.
  • Related: See PagePaths for the runtime path cache that PagePathHistory queries to avoid recording paths that are currently in use.
  • Source file: wire/modules/Page/PagePathHistory/PagePathHistory.module

API reference: methods, properties, hooks

to the new location whenever the past URL is accessed.


Click any linked item for full usage details and examples. Hookable methods are indicated with the icon. In addition to those shown below, the PagePathHistory class also inherits all the methods and properties of: WireData and Wire.

Show class?     Show args?       Only hookable?    

Common

NameReturnSummary 
PagePathHistory::addPathHistory(Page $page, string $path)
bool

Add a history path for a page

 
PagePathHistory::deleteAllPathHistory($page)
None

Delete all path history for a given Page or for all pages

 
PagePathHistory::deletePathHistory(Page $page, string $path)
int

Delete path entry for given page and path

 
PagePathHistory::getPage(string $path)
Page NullPage

Given a previously existing path, return the matching Page object or NullPage if not found.

 
PagePathHistory::getPathHistory(Page $page)
array

Get an array of all paths the given page has previously had, oldest to newest

 
PagePathHistory::getPathInfo(string $path)
array

Get array of info about a path if it is in history

 
PagePathHistory::getRootSegments()
array

Get all root segments

 
PagePathHistory::isRootSegment(string $segment)
bool

Is/was given segment ever a root segment?

 
PagePathHistory::setPathHistory(Page $page, string $path)
bool

Set a history path for a page and delete any existing entries for page’s current path

 
PagePathHistory::upgrade(int $fromVersion, int $toVersion)
None

Upgrade PagePathHistory module schema

Properties

NameReturnSummary 
PagePathHistory::minimumAge int 
PagePathHistory::rootSegments array bool 

Additional methods and properties

In addition to the methods and properties above, PagePathHistory also inherits the methods and properties of these classes:

API reference based on ProcessWire core version 3.0.269