sayamaapps / kirby-higawari
Daily menu (Tageskarte) for Kirby CMS 5: dishes with a validity date and window, a Panel structure, a plain snippet and a Fude marker for changes by chat. No scripts, no third-party requests, no chat or write endpoint.
Package info
github.com/sayamaapps/kirby-higawari
Type:kirby-plugin
pkg:composer/sayamaapps/kirby-higawari
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
- sayamaapps/kirby-fude: Changes the daily menu by chat (the fude: marker in the Tageskarte field).
Provides
None
Conflicts
- sayamaapps/kirby-fude: <1.4
Replaces
None
This package is auto-updated.
Last update: 2026-10-31 11:19:55 UTC
README
Kirby 5 · PHP 8.2 to 8.5 · MIT
Daily menu for Kirby CMS 5. Higawari (日替わり) is Japanese for "changes every day": a menu of dishes, each menu valid on a date you set, shown on that day and nothing else.
- A date, a window. A menu is valid on its date, or on as many days as the site sets, counted from that date. Without a date nothing is shown, so an old card cannot linger.
- The Panel. A tab with the date and up to five dishes (name, further lines, price, an optional variant with its own price, markers). Dish names are the only required text.
- Plain output. One snippet with semantic HTML and
higawari-*classes. No JavaScript, no request to another server, no cookie. - Optional text when nothing is valid. Either nothing, or a sentence of your choice.
- Nothing that writes from outside. Higawari has no chat, bot or write endpoint. Changes by chat belong to the separate package Fude, which reads the marker in the dishes blueprint (see below).
Code, comments and this README are in English. The Panel texts and the snippet texts are in German and English.
Requirements
- Kirby 5 (
getkirby/cms^5.0) - PHP 8.2 to 8.5, with the
mbstringextension
Installation
composer require sayamaapps/kirby-higawari
Composer installs the plugin into site/plugins/higawari/. Until the package is on Packagist, add the repository first:
"repositories": [{ "type": "vcs", "url": "https://github.com/sayamaapps/kirby-higawari" }]
ZIP: download the release from GitHub and unpack it to site/plugins/higawari/.
Git submodule: from the Kirby project root:
git submodule add https://github.com/sayamaapps/kirby-higawari site/plugins/higawari
Off by default. Switch it on in site/config/config.php:
return [ 'sayamaapps.higawari.active' => true, ];
Configuration
All options are read as sayamaapps.higawari.<name>, flat or nested. The short spelling higawari.<name> is not read (see Deprecations). null counts as "not set". A broken value is reported to the PHP error log once and the default is used; with Kirby's debug option on, it throws an InvalidArgumentException instead.
| Option | Default | Meaning |
|---|---|---|
active |
false |
Master switch. Only a real true switches it on. |
days |
1 |
How many days a menu is shown, counted from its date: 1 is the day itself, 3 is the date and the two days after. From 1 to 366. Only in mode from; mode until ignores it. |
dateMode |
from |
What the date means, see Two date modes. from: the first day, shown for days days. until: the last day ("Anzeigen bis"). Any other value is reported and from is used. |
timezone |
Europe/Berlin |
The zone in which "today" is decided. |
currency |
€ |
Shown after a price, with a space: 7,50 €. An empty string shows bare numbers: 7,50. |
emptyText |
null |
Shown in place of the menu when none is valid. null shows nothing. |
note |
null |
A line under the dishes, for example about prices or allergens. |
language |
site language | de or en, for the snippet and Panel texts. Otherwise the default language, then the first two letters of locale, then English. |
Content field contract
The Panel tab writes two fields. Their names are a compatibility promise: sites that already store these field names keep working unchanged, so their content needs no migration.
| Field | Type | Meaning |
|---|---|---|
tageskarteDatum |
date | "Gilt am", in mode until "Anzeigen bis": the day the menu is valid on, or with dateMode until the last day it is shown. Empty: nothing is shown. |
tagesgerichte |
structure, max. 5 | The dishes (see below). |
Dish fields:
| Field | Type | Meaning |
|---|---|---|
gericht |
text, required, max. 160 | The dish. |
beschreibung |
textarea, max. 300 | Further lines, one per line break. |
preis |
number, 0 to 999, step 0.01 | Optional. Empty shows the dish without a price. |
variante |
text, max. 60 | Optional. A second serving with its own price, such as „mit Wiener“ or „kleine Portion“, shown as its own line under the dish. preis is then the price without it. |
variantenpreis |
number, 0 to 999, step 0.01 | Optional. The price of the variant; shown only with variante. |
kennzeichnungen |
text, max. 40 | Optional markers, shown as written. |
Reading rules, the same for the Panel content and imported data: a dish without a name is dropped; a price that cannot be read (abc, 1000, 12.345) drops the dish rather than showing a wrong amount; a decimal comma and a decimal point both work; at most five dishes are shown, in order. A variant needs a name: a variant price alone is ignored, and a variant price that cannot be read drops the variant line, not the dish.
Two date modes
from(default): the date is the first day. The menu is shown on it and on thedays− 1 days after it.until: the date is the last day. The menu is shown on every day up to and including it, with no first day, so a card entered "until Friday" is visible at once and disappears after Friday.daysdoes not apply.
Switching the mode changes the meaning of the stored date, not the content: a site that moves from from to until has to re-enter the dates that should now mean "until".
Why these fields
gericht,preis,beschreibung: the same price check applies to every price. The extra text can be kept as one string or split into lines; Higawari splits it, because a line break in a Panel field is the way editors write a list of sauces or sides.variante,variantenpreis: one dish, two prices („Linseneintopf 9,90 €, mit Wiener 11,50 €“) is common on a daily menu. Written into the description, the second price loses the price column and the price check. One variant covers the usual case; a dish with more servings is better entered as two dishes.kennzeichnungen: free text, because an allergen or marker list is a legal matter that the site should decide, not the package. A structured list (for example EU allergen numbers) would be a guess at that decision.tageskarteDatum: the day the menu is served, plus the window (days), so a menu can be shown for more than one day. An undated menu that stays visible until it is removed is deliberately not supported: a daily menu without a date would show last week's dishes.- Five dishes: the cap is five, both in the code and in the blueprint, so a larger blueprint cannot show more dishes than the code does.
- Price format:
7,50 €or7,50, depending on whether the Panel shows the unit. Thecurrencyoption covers both.
Left out on purpose
- Weekend and closing-day rules. The menu logic has none. Opening hours are another package's job (Noren).
- Chat, bots and write endpoints. Changes by chat are Fude's job.
- "Tageskarte leeren" as a Panel button. The clearing itself is here (see below). The button needs a dialog in the site's own Panel area, which only the site can define.
- The picture beside the menu, and its layout. Those are site design.
Blueprints
Add the tab to a page blueprint with extends:
# site/blueprints/pages/home.yml tabs: tageskarte: extends: higawari/tabs/daily-menu
Pieces for your own blueprints: higawari/tabs/daily-menu (the tab), higawari/fields/date and higawari/fields/dishes. The tab and the date show the texts of the configured dateMode, so the same blueprint works in both modes.
Snippet
// The page that renders the template: snippet('higawari'); // Another page, and a fixed clock (a DateTimeImmutable, for tests and previews): snippet('higawari', ['source' => $site->homePage(), 'now' => $customNow]);
Markup, with classes only and no inline style:
<section class="higawari" aria-labelledby="higawari-titel"> <h2 class="higawari-titel" id="higawari-titel">Tageskarte</h2> <p class="higawari-gilt"><time datetime="2026-10-10">Samstag, 10.10.2026</time></p> <ul class="higawari-gerichte"> <li class="higawari-gericht"> <span class="higawari-name">Kartoffelsuppe</span> <span class="higawari-preis">6,90 €</span> <span class="higawari-variante">mit Wiener</span> <!-- only with a variant --> <span class="higawari-variantenpreis">8,40 €</span> <!-- only with a variant price --> <p class="higawari-beschreibung">mit Brot<br>und Kräutern</p> <p class="higawari-kennzeichnungen">Kennzeichnungen: V, L</p> </li> </ul> <p class="higawari-hinweis">…</p> <!-- only with the note option --> </section>
With dateMode until, the line reads Bis einschließlich Samstag, 10.10.2026 (English: Until Saturday, 10 October 2026), and its datetime is the last day.
With nothing valid, the snippet prints nothing, or only <p class="higawari-leer"> with the emptyText option. Every value is escaped. The page is never cached by Kirby, because the menu depends on the clock. To change the markup, create site/snippets/higawari.php; Kirby uses it instead.
No CSS is shipped. The classes are the hooks for your own stylesheet.
Page method
$card = $page->higawariCard(); // now, in the configured zone // [ // 'from' => '2026-10-10', 'to' => '2026-10-10', // mode until: from is today, to the date // 'dishes' => [ // ['name' => 'Kartoffelsuppe', 'lines' => ['mit Brot', 'und Kräutern'], 'price' => '6,90 €', 'markers' => 'V, L', // 'variant' => ['label' => 'mit Wiener', 'price' => '8,40 €']], // or null // ], // ] // or null: plugin off, not valid that day, or no usable dish
Clearing
use Sayamaapps\Higawari\Menu; $page->update(Menu::CLEARED); // all dishes and the date, in one step
Higawari\Menu::CLEARED (1.x) is the same constant.
CLEARED empties both fields together. Keeping the date would hide a menu entered the next day, because the old date has already passed.
Fude
The dishes field carries a fude: marker, so a site with Fude 1.4 or later gets the menu as a chat collection without a line in site/config/fude.php: a bot may replace the whole menu, update or delete a dish (at most five), and every change sets tageskarteDatum. That is today, unless the chat names another day ("the menu for tomorrow"); the marker sets editable, Fude refuses a past day. The bot learns what the date means from the field's label and help text. The marker sets readImages, so the chat side may read a photo of the blackboard as the new menu; the dishes on it still arrive as a draft that someone confirms. touch stamps the day in Fude's fude.timezone (default Europe/Berlin). Without Fude, Kirby ignores the marker.
| Dish | |
|---|---|
| Required | gericht |
| Asked | — (the menu comes as one message or a photo; asking every day would slow it down) |
| Free | beschreibung, preis, variante, variantenpreis, kennzeichnungen |
- In mode
untila change by chat without a named day sets the date to today, so the menu is shown until today only. The bot names the last day ("bis Freitag"). - The collection is named
<page id>-tagesgerichte, for examplehome-tagesgerichte. - The page needs both fields, as the tab brings them. A blueprint that extends only
higawari/fields/dishesmakes every Fude route fail, because the date to stamp is missing. - To switch it off, or to change it, use Fude's overrides in
site/config/fude.php:'home-tagesgerichte' => falseremoves the collection, an array under the same name replaces it.
Privacy
Higawari processes no visitor data: it sets no cookies, uses no browser storage and makes no request to a third party. There is nothing to add to a site's privacy policy.
Deprecations
Kept until 2.0. Nothing breaks; a site moves at its own pace. Nothing is reported at run time; the replacements are marked @deprecated in the code.
| Old name | Now | Note |
|---|---|---|
Classes Higawari\Menu, Higawari\Options, Higawari\I18n, Higawari\Daily, Higawari\Panel |
Sayamaapps\Higawari\… with the same names |
Class aliases in legacy.php; no code change needed. |
Higawari\I18n::t() |
Sayamaapps\Higawari\I18n::translate() |
Wrapper with the same result. Only code that calls the class directly is affected. |
The short option spelling higawari.<name> was never read and is not read now. Use sayamaapps.higawari.<name>.
Also unchanged in 1.x: the snippet name, the blueprint names, the content field names, the page method higawariCard() and the options.
Planned for 2.0
These would change what a site sees or catches, so they wait for the next major version:
- A broken option throws
Kirby\Exception\InvalidArgumentExceptioninstead ofInvalidArgumentException. That class does not extendInvalidArgumentException, so acatch (InvalidArgumentException)around the plugin stops catching it. Debug mode only. - Run-time deprecation notices for the class aliases and for
I18n::t(). - Removal of the class aliases and of
I18n::t().
Development
composer install composer ci # the checks below, except the code style composer test # PHPUnit composer analyze:psalm # Psalm, errorLevel 3 composer analyze:phpmd # PHP Mess Detector composer analyze:unused # unused Composer packages composer analyze:require-checker # undeclared dependencies composer analyze:phpcpd # copy and paste detection, needs the global phpcpd composer normalize:check # composer.json layout
Two tools are installed globally: php-cs needs PHP 8.3 or newer, and phpcpd 6 conflicts with the PHPUnit dependencies.
composer global require getkirby/php-cs:0.2.1 sebastian/phpcpd:6.0.3
kirby-cs fix --dry-run --diff # code style; getkirby/php-cs needs PHP 8.3 or newer
The Kirby tests boot a real Kirby 5 on a throwaway site and need no network connection. CI runs the tests on PHP 8.2 to 8.5 with the lowest and the highest dependency versions, and against the Kirby 6 pre-release as a warning.
Changelog
See CHANGELOG.md.
License
MIT, see LICENSE.