Search by

sayamaapps / kirby-hikkoshi

simsimmal

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

Statistics

Installs: 163

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v1.3.0 2026-10-31 17:57 UTC

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, .html and index.php endings.
  • Query-string addresses such as /?page_id=82, per-entry status and query handling, and 410 for removed pages.
  • Command-line check, an .htaccess converter 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 with to, status and query, or an external URL whose host is listed in sayamaapps.hikkoshi.allowedHosts. 410 answers with the snippet hikkoshi/gone; to restyle it, create site/snippets/hikkoshi/gone.php, which receives $title and $text (escape them).
  • Targets are lowercase page ids: '/kontakt' and 'kontakt.html' are errors. Numeric keys such as 2024 are 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.html finds 'impressum', but 'impressum.html' does not catch /impressum.
  • Only GET and HEAD are 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 (query keep, 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), or null.
  • $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 any sayamaapps.hikkoshi.* option in your templates.
  • t('hikkoshi.gone.title') and t('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 the Hikkoshi\* 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 set resolveChains. 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]
  • check validates the list without booting Kirby. FILE defaults to the list file. --allow-host accepts an external host and can be repeated; --strict also fails on warnings.
  • convert turns Apache Redirect, RedirectMatch and RewriteRule lines 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 run check:
vendor/bin/hikkoshi convert public/.htaccess > site/config/hikkoshi.php
  • Exit codes: 0 no errors (for convert: all lines converted), 1 errors (with --strict also warnings; for convert: lines skipped or partly converted), 2 usage error or file not found.
  • Once the list passes check and the site test, delete the converted rules from .htaccess in 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 (www to the bare domain), DirectoryIndex, Kirby's rules that pass requests to index.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 the language option, 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 uses t('hikkoshi.gone.title') in its own snippet.
  • Defaults are passed at each option call, never in an options block 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.