{"id":51023078,"url":"https://github.com/lemmon/kirby-plugin-reactions","last_synced_at":"2026-06-21T17:30:55.911Z","repository":{"id":353628037,"uuid":"1218677072","full_name":"lemmon/kirby-plugin-reactions","owner":"lemmon","description":"Emoji reactions for Kirby CMS via HTML POST or HTMX. Features append-only JSONL storage and privacy-first   visitor anonymization.","archived":false,"fork":false,"pushed_at":"2026-04-24T17:45:23.000Z","size":20,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-04-24T19:31:10.795Z","etag":null,"topics":["emoji","feedback","htmx","kirby","kirby-cms","kirby-plugin","php","privacy","widget"],"latest_commit_sha":null,"homepage":"","language":"PHP","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/lemmon.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null}},"created_at":"2026-04-23T05:33:25.000Z","updated_at":"2026-04-24T17:45:28.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/lemmon/kirby-plugin-reactions","commit_stats":null,"previous_names":["lemmon/kirby-plugin-reactions"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/lemmon/kirby-plugin-reactions","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lemmon%2Fkirby-plugin-reactions","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lemmon%2Fkirby-plugin-reactions/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lemmon%2Fkirby-plugin-reactions/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lemmon%2Fkirby-plugin-reactions/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/lemmon","download_url":"https://codeload.github.com/lemmon/kirby-plugin-reactions/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lemmon%2Fkirby-plugin-reactions/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34620358,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-21T02:00:05.568Z","response_time":54,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["emoji","feedback","htmx","kirby","kirby-cms","kirby-plugin","php","privacy","widget"],"created_at":"2026-06-21T17:30:52.764Z","updated_at":"2026-06-21T17:30:55.902Z","avatar_url":"https://github.com/lemmon.png","language":"PHP","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Reactions for Kirby\n\nA privacy-friendly emoji reactions widget for Kirby CMS. Plain HTML POST, no JavaScript required. HTMX progressively enhances if present.\n\nVisitors can cast multiple reactions on a page and click an active reaction again to remove it. Counts are always visible and represent the current active state, not the number of raw clicks.\n\n## Installation\n\n```bash\ncomposer require lemmon/kirby-reactions\n# or\ngit submodule add git@github.com:lemmon/kirby-plugin-reactions.git site/plugins/reactions\n```\n\n## Usage\n\n```php\n\u003c?php snippet('reactions') ?\u003e\n```\n\nOverride the prompt per call:\n\n```php\n\u003c?php snippet('reactions', [\n    'question' =\u003e 'How did this land?',\n]) ?\u003e\n```\n\nLabels read from `t('reactions.*')` with English fallbacks:\n\n```yaml\nreactions.question: React to this page\nreactions.confirmation: Reaction saved.\n```\n\nBEM-like classes for styling: `reactions`, `reactions__question`, `reactions__actions`, `reactions__button`, `reactions__button--active`, `reactions__emoji`, `reactions__label`, `reactions__count`, `reactions__status`. No CSS ships with the plugin.\n\n## Reaction pool\n\nThe plugin has one global reaction pool:\n\n```php\nreturn [\n    'lemmon.reactions' =\u003e [\n        'pool' =\u003e [\n            'up' =\u003e [\n                'emoji' =\u003e '👍',\n                'label' =\u003e 'Thumbs up',\n            ],\n            'down' =\u003e [\n                'emoji' =\u003e '👎',\n                'label' =\u003e 'Thumbs down',\n            ],\n            'heart' =\u003e [\n                'emoji' =\u003e '❤️',\n                'label' =\u003e 'Love it',\n            ],\n            'mindblown' =\u003e [\n                'emoji' =\u003e '🤯',\n                'label' =\u003e 'Mind blown',\n            ],\n        ],\n    ],\n];\n```\n\nKeys are stable ids and must match `[a-z0-9][a-z0-9_-]{0,63}`. Store and compare the key, not the emoji, so labels and emoji can change later without losing old data.\n\nFor very small configs, a string value is accepted as shorthand:\n\n```php\n'lemmon.reactions' =\u003e [\n    'pool' =\u003e [\n        'up' =\u003e '👍',\n        'down' =\u003e '👎',\n    ],\n],\n```\n\n## Storage\n\nEvery valid click is appended to `{storage-root}/reactions/events.jsonl` as one JSON object per line:\n\n```json\n{\n  \"page\": \"page://...\",\n  \"reaction\": \"up\",\n  \"action\": \"on\",\n  \"timestamp\": 1710000000,\n  \"visitorHash\": \"...\"\n}\n```\n\nClicking an active reaction appends an `off` event. Counts replay the log and keep the final `on` / `off` state for each visitor + reaction pair.\n\nVotes key on `$page-\u003euuid()-\u003etoString()`, so data survives renames. Requires UUIDs (Kirby's default).\n\nThe log path resolves in this order:\n\n1. `lemmon.reactions.storage.dir` override -- wins if set.\n2. `{storage-root}/reactions/` if the site registers a `storage` root.\n3. `{site-root}/storage/reactions/` as the zero-config fallback.\n\n### Register a shared `storage` root (recommended)\n\nRegister a Git-ignored `storage` root once in `public/index.php`:\n\n```php\n$kirby = new Kirby([\n    'roots' =\u003e [\n        'index'   =\u003e __DIR__,\n        'base'    =\u003e $base = dirname(__DIR__),\n        'site'    =\u003e $base . '/site',\n        'content' =\u003e $base . '/content',\n        'storage' =\u003e $base . '/storage',\n    ],\n]);\n```\n\n## Programmatic API\n\nThree static helpers on `Lemmon\\Reactions\\Reactions`, all expecting `$page-\u003euuid()-\u003etoString()`:\n\n```php\nuse Lemmon\\Reactions\\Reactions;\n\n$pageUri = $page-\u003euuid()-\u003etoString();\n\nReactions::counts($pageUri); // ['up' =\u003e 42, 'down' =\u003e 3]\nReactions::active($pageUri); // ['up' =\u003e true]\nReactions::pool();           // ['up' =\u003e ['emoji' =\u003e '...', 'label' =\u003e '...'], ...]\n```\n\n## HTMX (optional)\n\nThe form always renders with `hx-post`, `hx-target=\"this\"`, and `hx-swap=\"outerHTML\"` attributes. They are ignored when HTMX is not loaded; when it is, clicking a reaction swaps the form in place with updated counts and pressed states.\n\n## Configuration\n\n```php\nreturn [\n    'lemmon.reactions' =\u003e [\n        'secret' =\u003e null, // HMAC secret; falls back to Kirby's content token\n        'storage' =\u003e [\n            'dir' =\u003e null, // absolute path override\n        ],\n        'pool' =\u003e [\n            'up' =\u003e [\n                'emoji' =\u003e '👍',\n                'label' =\u003e 'Thumbs up',\n            ],\n            'down' =\u003e [\n                'emoji' =\u003e '👎',\n                'label' =\u003e 'Thumbs down',\n            ],\n        ],\n        'cache' =\u003e [\n            'active' =\u003e true,\n            'type' =\u003e 'file',\n            'prefix' =\u003e 'lemmon/reactions',\n        ],\n    ],\n];\n```\n\n`cache` is a standard Kirby cache config; switch `type` for Redis/memcached. The explicit `prefix` keeps HTTP and CLI invocations sharing one cache directory.\n\nTo hide the widget, remove `snippet('reactions')` from your templates.\n\n### Baked-in defaults\n\n| Behavior           | Value                                      |\n| ------------------ | ------------------------------------------ |\n| Token TTL          | 30 min                                     |\n| Rate limit (IP)    | 120 events / 10 min                        |\n| Rate limit (page)  | 80 events per IP + page / 24 h             |\n| Counts cache TTL   | 5 min                                      |\n| Active cache TTL   | 1 min                                      |\n| IPv4 anonymization | /24 (last octet zeroed)                    |\n| IPv6 anonymization | /64 (last 8 bytes zeroed)                  |\n| Visitor identity   | random session id, page-scoped HMAC in log |\n| IP hash            | ephemeral cache buckets only, not in JSONL |\n| Log filename       | `events.jsonl`                             |\n| User agent         | not stored                                 |\n\n## Security\n\nSet a strong secret in production. Generate one with `openssl rand -hex 32` (or `php -r 'echo bin2hex(random_bytes(32)), PHP_EOL;'`) and paste the resulting hex string as a literal value, or load it from env:\n\n```php\n'lemmon.reactions' =\u003e [\n    'secret' =\u003e $_ENV['REACTIONS_SECRET'] ?? null,\n],\n```\n\nDo not embed `bin2hex(random_bytes(32))` directly in the config -- it would re-run per request and rotate the secret every time. Rotating the secret invalidates outstanding tokens and changes the HMAC for every visitor going forward.\n\nVote events store a page-scoped HMAC of a random session visitor id, keeping reaction state separate per page. IPs are anonymized to /24 (IPv4) or /64 (IPv6), then HMAC-hashed for ephemeral rate-limit buckets only -- never persisted.\n\n## License\n\nMIT. See `LICENSE`.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flemmon%2Fkirby-plugin-reactions","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Flemmon%2Fkirby-plugin-reactions","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flemmon%2Fkirby-plugin-reactions/lists"}