sayamaapps / kirby-hikkoshi
Checked redirects for Kirby CMS: old addresses, tolerant matching and 410 for removed pages.
Package info
github.com/sayamaapps/kirby-hikkoshi
Type:kirby-plugin
pkg:composer/sayamaapps/kirby-hikkoshi
Requires
- php: ^8.2
- ext-mbstring: *
- getkirby/composer-installer: ^1.2
Requires (Dev)
- ergebnis/composer-normalize: ^2.54
- getkirby/cms: ^5.0
- icanhazstring/composer-unused: ^0.9.7
- maglnet/composer-require-checker: ^4.0
- phpmd/phpmd: ^2.15
- phpunit/phpunit: 11.5.55
- vimeo/psalm: ^6.20
Suggests
- ext-intl: Unicode normalisation (Normalizer) for umlauts in paths; without it the paths are compared as written.
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-10-31 17:57:31 UTC
README
Checked redirects for Kirby CMS after a site has moved.
Hikkoshi (引っ越し) means moving house. It maps a site's old addresses to their new pages and checks that map before the site goes live.
Features
- A checked list: loops, duplicate old paths and foreign targets are errors; chains are warnings.
- Tolerant matching: case, trailing slash, percent-encoding, umlauts,
.htmlandindex.phpendings. - Query-string addresses such as
/?page_id=82, per-entry status and query handling, and 410 for removed pages. - Command-line check, an
.htaccessconverter and a test helper for sites. - Server-side only: no cookies, no browser storage, no outbound requests. English and German messages.
Requirements
Kirby 5 · PHP 8.2 to 8.5 · mbstring extension
The intl extension is optional: with it, umlauts in paths are compared in their normalised form.
Installation
Pick one of three ways. Each puts the plugin into site/plugins/hikkoshi/.
Composer (the folder is then managed by Composer; add it to .gitignore and deploy it like vendor/):
composer require sayamaapps/kirby-hikkoshi
ZIP: download the release archive from the GitHub releases page and unpack it to site/plugins/hikkoshi/. The archive contains the runtime files only.
Git submodule:
git submodule add https://github.com/sayamaapps/kirby-hikkoshi.git site/plugins/hikkoshi
Usage
Hikkoshi is off until you set 'sayamaapps.hikkoshi.active' => true in site/config/config.php. Then create site/config/hikkoshi.php, which maps old address to new address (the package ships a template as config/hikkoshi.example.php):
<?php return [ 'ueber-uns' => 'team', 'impressum.html' => 'impressum', '?page_id=82' => 'impressum', 'aktion' => ['to' => 'produkte', 'status' => 302, 'query' => 'drop'], 'gewinnspiel-2019' => 410, 'partner' => 'https://partner.example/start', // needs sayamaapps.hikkoshi.allowedHosts ];
Keys and values
- Keys are old paths such as
'ueber-uns'or'downloads/karte.pdf'. Query conditions follow the?:'?page_id=82', or'?feed'(parameter present, any value). The home page needs a query condition. - Values are a page id (
'team','home'),410, an array withto,statusandquery, or an external URL whose host is listed insayamaapps.hikkoshi.allowedHosts.410answers with the snippethikkoshi/gone; to restyle it, createsite/snippets/hikkoshi/gone.php, which receives$titleand$text(escape them). - Targets are lowercase page ids:
'/kontakt'and'kontakt.html'are errors. Numeric keys such as2024are errors too; write'archiv/2024'.
Matching
The exact pass comes first. If nothing matches, one suffix (html, htm, php) or index file (index.php, index.html, index.htm) is removed from the last segment and the lookup runs again. The first hit wins.
- Requests and keys are normalised: case, repeated slashes, percent-encoding and umlauts (
/Über-Uns/matches'ueber-uns'). The second pass runs one way:/impressum.htmlfinds'impressum', but'impressum.html'does not catch/impressum. - Only
GETandHEADare redirected, and only to published pages. Otherwise Kirby answers as usual. - Not matched: host names (keep those rules in the web server), files in
public/, other suffixes such as.pdf, and old media URLs. A key that is also a real page address redirects, so that page becomes unreachable there. - Matched condition parameters are removed from the target URL. Other parameters are kept (
querykeep, the default) or dropped (drop). A 410 never carries a query.
Site and page methods
$site->hikkoshiRedirects(): the validated entries;[]while off.$site->hikkoshiResolve($path, $query = []): the decision for a path and query (status,location,page), ornull.$site->hikkoshiCheck($render = true): findings for the site test;[]when all is well.$page->hikkoshiSources(): the old paths that redirect to this page.Sayamaapps\Hikkoshi\Options::get(kirby(), 'language')reads anysayamaapps.hikkoshi.*option in your templates.t('hikkoshi.gone.title')andt('sayamaapps.hikkoshi.gone.title')read the plugin's strings in your templates.
Options
Every option is sayamaapps.hikkoshi.<name>, written flat: 'sayamaapps.hikkoshi.active' => true. The nested form 'hikkoshi' => ['active' => true] works for the 1.x name only (see Deprecated). null means "not set". Booleans take a real true or false. A broken value is logged once and the default is used; with Kirby's debug option on, it throws PHP's InvalidArgumentException. That is PHP's class and not a Kirby exception on purpose: a site that catches InvalidArgumentException keeps working, and Kirby's own exception classes do not extend it. Kirby exceptions are planned for 2.0 (see below).
The 1.x name hikkoshi.<name> still works as a fallback until 2.0, flat or nested, and is read without a warning. If both names are set, the new one wins.
| Option | Default | Description |
|---|---|---|
active |
false |
Master switch. Only true enables Hikkoshi. |
redirects |
null |
The list as an array, or a Closure returning one. Takes precedence over file. |
file |
null |
List file, absolute or relative to site/config/. Unset: site/config/hikkoshi.php, then site/config/weiterleitungen.php. |
status |
301 |
Status of entries without their own: 301, 302, 307 or 308. |
query |
'keep' |
keep passes the remaining query parameters on; drop sends none. |
allowedHosts |
[] |
Hosts (string or list) that may be external redirect targets. |
resolveChains |
false |
true replaces a chain's target with the final one, reported as info. |
lowercase |
true |
Lowercase both sides before matching. false matches exact case. |
transliterate |
true |
ä→ae, ö→oe, ü→ue, ß→ss on both sides. |
suffixes |
['html', 'htm', 'php'] |
Suffixes removed in the second pass. [] turns it off. |
indexFiles |
['index.php', 'index.html', 'index.htm'] |
Last segments removed in the second pass (blog/index.php becomes blog). [] turns it off. |
language |
site language | en or de, for messages and the 410 page. Default: site language, then locale, then English. |
Deprecated
Deprecated in 1.x and removed in 2.0. Each still works. Nothing is raised at runtime: the deprecations are listed here and in the code (@deprecated).
| 1.x | Use instead |
|---|---|
hikkoshi_option($key, $default) |
Sayamaapps\Hikkoshi\Options::get($kirby, $key, $default) |
hikkoshi.<option> in config.php |
sayamaapps.hikkoshi.<option>. Still read as fallback. Rename the keys in your site config. |
Hikkoshi\* classes (Hikkoshi\Validator, Hikkoshi\ConfigException, …) |
Sayamaapps\Hikkoshi\* (the same classes under both names; the exception keeps the name Hikkoshi\ConfigException at runtime) |
Planned for 2.0
These change behaviour, so they wait for the next major version. In 1.x they are not done:
- Broken options throw a Kirby exception instead of PHP's
InvalidArgumentException. - The translation keys
hikkoshi.<key>are removed.sayamaapps.hikkoshi.<key>stays. hikkoshi_option()and theHikkoshi\*aliases are removed.- Error messages name the option as
sayamaapps.hikkoshi.<name>.
Hikkoshi\ConfigException keeps extending LogicException, as in 1.x, so a site's own catch keeps working.
Validation
- Errors (loops, duplicate old paths, invalid keys or targets, foreign hosts, self-redirects, invalid status or query mode) stop the site: every request fails with HTTP 500. The first test shows them.
- Warnings (chains such as
a → b → c) are logged and the site keeps working. Enter the final target directly, or setresolveChains. The chain check ignores entries with a query condition.
CLI
vendor/bin/hikkoshi check [FILE] [--lang=en|de] [--resolve-chains] [--allow-host=HOST] [--strict] vendor/bin/hikkoshi convert FILE [--lang=en|de]
checkvalidates the list without booting Kirby.FILEdefaults to the list file.--allow-hostaccepts an external host and can be repeated;--strictalso fails on warnings.convertturns ApacheRedirect,RedirectMatchandRewriteRulelines into entries, prints PHP to standard output and writes no file. Regular expressions are skipped, and prefix rules convert for the exact path only. Review the result, then runcheck:
vendor/bin/hikkoshi convert public/.htaccess > site/config/hikkoshi.php
- Exit codes:
0no errors (forconvert: all lines converted),1errors (with--strictalso warnings; forconvert: lines skipped or partly converted),2usage error or file not found. - Once the list passes
checkand the site test, delete the converted rules from.htaccessin the same deploy. Apache answers before PHP does, so a leftover rule wins over the list. Keep what the list cannot express: the host-name rule (wwwto the bare domain),DirectoryIndex, Kirby's rules that pass requests toindex.php, and header settings.
Test helper for sites
A test in tests/HikkoshiTest.php boots the site and fails on any finding. hikkoshiCheck() adds what only the running site shows: missing or draft targets, wrong answers and targets that redirect again.
<?php $base = dirname(__DIR__); require $base . '/vendor/autoload.php'; try { $kirby = new Kirby\Cms\App(['roots' => [ 'index' => $base . '/public', 'base' => $base, 'content' => $base . '/content', 'site' => $base . '/site', ]]); $findings = $kirby->site()->hikkoshiCheck(); // hikkoshiCheck(false) skips rendering } catch (Hikkoshi\ConfigException $e) { // a broken list stops Kirby while it starts $findings = $e->findings(); } foreach ($findings as $f) { fwrite(STDERR, $f['level'] . ': ' . $f['message'] . PHP_EOL); } exit($findings === [] ? 0 : 1);
Without Kirby, register the class map of autoload.php and call Sayamaapps\Hikkoshi\Validator::validate() with the list and the same allowedHosts:
$root = __DIR__ . '/site/plugins/hikkoshi'; $map = require $root . '/autoload.php'; spl_autoload_register(static function (string $class) use ($map, $root): void { if (isset($map[strtolower($class)])) { require $root . '/' . $map[strtolower($class)]; } }); $findings = Sayamaapps\Hikkoshi\Validator::validate($list, ['allowedHosts' => ['partner.example']])['findings'];
The 1.x names (Hikkoshi\Validator and the others) are aliases in each class file, so code written for 1.x that loads src/ directly keeps working.
Design notes
- The plugin's strings come from its own Kirby-free table (
src/I18n.php), so a missing Kirby translation never shows English for a German message. The same strings are also registered as Kirby translations. - The 410 page takes its text from the same table, not from Kirby's
t(). Kirby picks the translation by its own locale; Hikkoshi picks the language from thelanguageoption, then the site language, then the locale. The CLI and the validator need the same strings without Kirby. A site that wants its own 410 text usest('hikkoshi.gone.title')in its own snippet. - Defaults are passed at each option call, never in an
optionsblock of the plugin: Kirby would file them under the plugin name, and the options would no longer be the 1.x names.
Privacy
Hikkoshi stores nothing and processes no visitor data: no cookies, no browser storage, no requests to third parties. It therefore has no place in a site's privacy policy.
Tests: composer test. Issues and pull requests are welcome, without a support promise. Report security problems privately through GitHub's vulnerability reporting. Changes: CHANGELOG.md.
License
MIT, see LICENSE.md.