fendinger/kirby-visitor-ai-chat

AI site search as a chat widget for Kirby CMS.

Maintainers

Package info

github.com/fendinger/kirby-visitor-ai-chat

Type:kirby-plugin

pkg:composer/fendinger/kirby-visitor-ai-chat

Statistics

Installs: 0

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

1.0.0 2026-05-27 13:21 UTC

This package is auto-updated.

Last update: 2026-05-27 13:30:22 UTC


README

Kirby Visitor AI Chat

Kirby Visitor AI Chat

Intelligent site search as a chat widget for Kirby CMS. Content is indexed as embeddings and visitor questions are answered via RAG (Retrieval-Augmented Generation).

Five supported providers: OpenAI, Mistral, Google Gemini, IONOS AI Model Hub, Mittwald AI Hosting — freely configurable for embeddings and chat responses.

Using this plugin incurs costs. All supported providers charge for API usage (embeddings during indexing, chat completions per visitor question). In practice, costs are usually only a few cents per month for small to medium sites with moderate traffic — but this depends on site size, chosen models, and visitor volume. You are solely responsible for usage and billing with your provider. The plugin includes local token/cost tracking (see Cost and token tracking) and configurable rate limits, but does not enforce hard spending caps — set budget alerts and usage limits in your provider's dashboard.

Installation

Composer

composer require fendinger/kirby-visitor-ai-chat

Manual

Download and copy this repository to /site/plugins/kirby-visitor-ai-chat.

Configuration

In site/config/config.php:

return [
    'fendinger.visitor-ai-chat' => [
        'providers' => [
            'embedding' => 'openai',   // openai | mistral | gemini | ionos | mittwald
            'chat'      => 'openai',
        ],
        'openai'   => ['apiKey' => 'sk-...'],
        'mistral'  => ['apiKey' => '...'],
        'gemini'   => ['apiKey' => '...'],
        'ionos'    => ['apiKey' => '...'],
        'mittwald' => ['apiKey' => '...'],

        // Optional:
        'ui' => ['locale' => 'de'],    // null = auto
        'rateLimit' => ['perMinute' => 10, 'perHour' => 60],
        'chunk' => ['size' => 800, 'overlap' => 120],
        'topK' => 5,
        'maxQuery' => 500,
        'include' => [
            'templates' => [],  // empty = all templates; otherwise whitelist
        ],
        'exclude' => [
            'pages' => [],      // page IDs that are never indexed
        ],
        'crawler' => [
            'maxPages'   => 2000,    // safety upper bound
            'timeout'    => 10,      // seconds for sitemap fetch
            'useSitemap' => true,    // use sitemap.xml as additional seed
        ],
    ],
];

Getting API keys

You need an API key from at least one provider. Embedding and chat providers can be the same or different.

  • OpenAI — Sign up at platform.openai.com, then create a key at platform.openai.com/api-keys. Requires a funded billing account. Set budget alerts under Billing → Limits.
  • Mistral — Sign up at console.mistral.ai, then create a key at console.mistral.ai/api-keys. Free tier available for testing; paid plan required for production usage.
  • Google Gemini — Sign up at aistudio.google.com, then create a key at aistudio.google.com/apikey. Generous free tier for Gemini 1.5 Flash; paid plan available via Google Cloud.
  • IONOS AI Model Hub — Sign up at cloud.ionos.com, enable the AI Model Hub and create a token. Hosted in EU data centers (Frankfurt). Pay-as-you-go pricing per token; see the IONOS dashboard for current rates.
  • Mittwald AI Hosting — Available to mittwald customers via mStudio. Create an OpenAI-compatible API key per organization. All models are hosted in German data centers (GDPR-friendly, no training data retention). Billing is per provisioned model slot, not per token — see mStudio for current plans.

Paste the key into site/config/config.php as shown above. Make sure config.php is not committed to a public repository.

Initial reindex

Before the widget can answer, the site must be indexed once. In the Panel under Visitor AI Chat (menu entry), click „Run full reindex". Statistics (reachable pages, chunks, last crawl) are shown on the same page.

Alternatively via PHP CLI:

php -r 'require "kirby/bootstrap.php"; $kirby = new Kirby\Cms\App(); print_r((new Fendinger\VisitorAiChat\Indexer())->indexAll());'

Subsequent content changes are picked up automatically (see Automatic updates).

Embedding the widget

The plugin ships a snippet that renders the chat widget. Include it in your site template (typically site/snippets/footer.php or wherever you want the floating button to appear):

<?php snippet('visitor-ai-chat-widget') ?>

Without this line nothing is rendered on the frontend — the Panel and API still work, but visitors will not see the chat bubble.

Default models

Provider Embeddings Chat
OpenAI text-embedding-3-small gpt-4o-mini
Mistral mistral-embed mistral-small-latest
Gemini text-embedding-004 gemini-1.5-flash
IONOS BAAI/bge-m3 meta-llama/Llama-3.3-70B-Instruct
Mittwald Qwen3-Embedding-8B gpt-oss-120b

Models can be overridden per provider (openai.embeddingModel, openai.chatModel, …).

Switching providers requires a full reindex. Embedding vectors from different providers/models have different dimensions (e.g. OpenAI 1536, IONOS bge-m3 1024, Mittwald Qwen3 4096) and cannot be compared. After changing the embedding provider, run a full reindex from the Panel — otherwise the retriever will silently drop all hits and answers will be empty.

Overriding the system prompt

The default system prompt is suitable for most websites. To adjust tone, structure, length, or formatting rules, set your own prompt via config:

'fendinger.visitor-ai-chat' => [
    'systemPrompt' => <<<PROMPT
    You are the assistant for example.com. Answer strictly from the CONTEXT below.
    Keep answers under 3 sentences. Respond in {locale}.

    CONTEXT:
    {context}
    PROMPT,
],

Placeholders:

  • {context} — replaced with the RAG-retrieved content (must be included, otherwise the model has no grounding)
  • {locale} — current language code (e.g. de, en, fr)

Without a config override the plugin uses the built-in default, including rules for Markdown links, list formatting, and automatic response language based on the languages enabled in Kirby.

Widget texts and greeting

All visible texts of the frontend widget (title, greeting, placeholder, error messages) come from Kirby's translation system and can be overridden, e.g. in site/languages/en.php:

'translations' => [
    'visitor-ai-chat.title'    => 'Ask this site',
    'visitor-ai-chat.greeting' => 'Hi! How can I help?',
    // ...
],

The widget title can additionally be set via config (overrides the translation):

'ui' => ['title' => 'Chat Bot'],

Theme

The widget ships with a light and a dark theme. Light is the default. Set via config:

'ui' => ['theme' => 'dark'],   // 'light' (default) | 'dark'

License & activation

The plugin is commercial. Each license key is bound to a single production website (one activation per key). Local and development environments behave identically to production — activate a (test-mode) key there to verify the full flow before going live.

Activating a license

  1. Purchase a license at fendinger.lemonsqueezy.com — you receive the key by email.
  2. In the Kirby Panel, open System → Plugins and click Activate now in the row for Visitor AI Chat.
  3. Paste the key and click Activate License. The key is bound to your site's primary domain.

Once activated, the same row shows Licensed and clicking it opens a dialog with license details and a Deactivate license button.

Deployment via config

For automated deployments, you can set the key in site/config/config.php:

'fendinger.visitor-ai-chat' => [
    'license' => ['key' => 'XXXXXXXX-XXXXXXXX-XXXXXXXX-XXXXXXXX'],
],

Moving to a different website

Open the license dialog in System → Plugins and click Deactivate license before reusing the key on another site. The activation slot is released on the LemonSqueezy side and becomes available again.

Behaviour without a license

The plugin keeps working unconditionally, so a license server outage never breaks your site. The Panel displays a banner reminding you to register.

Indexing strategy

The indexer crawls the site starting from the homepage (BFS over internal <a href> links, optionally seeded by sitemap.xml) and indexes the rendered HTML output of each reachable page. This captures static template text (headings, buttons, labels) that is not stored in content fields.

Per page, the content from <main> (fallback <body>) is extracted; <nav>, <footer>, <script>, <style>, <noscript>, <svg> and <template> are removed. Image metadata (Alt, Caption, Title, Photographer) from each page's Kirby files is appended to the rendered text — even if the template does not output it.

Fragment pages that are not reachable via links are not indexed. Pages with <meta name="robots" content="noindex"> are also ignored. For multi-language sites, each active language is crawled separately.

Escape hatch: forceIndex

For pages that are not linked (e.g. only reachable via newsletter/ads, SPA routing), the plugin field can be embedded:

# site/blueprints/pages/mypage.yml
fields:
  forceIndex:
    extends: fields/visitor-ai-chat-force-index

With the toggle enabled, the page is always indexed — regardless of the crawl result.

Excluding individual pages

Conversely, a page can be explicitly excluded from the index:

fields:
  excludefromindex:
    extends: fields/visitor-ai-chat-exclude-index

Alternatively globally via config (exclude.pages) as a list of page IDs.

On-demand reindex per page

To let editors trigger an immediate reindex of a single page from the Panel (without running a full reindex), embed the button field in the page blueprint:

fields:
  reindexNow:
    extends: fields/visitor-ai-chat-reindex-now

Clicking the button calls the per-page reindex API and shows a Panel notification on success or failure.

Alternatively (or additionally), the same action is available as a view button in the page header bar (top right, next to Status / Settings). Enable it by adding page.reindex to the page view button list — either globally in site/config/config.php:

return [
    'panel' => [
        'viewButtons' => [
            'page' => ['page.preview', 'page.reindex', 'page.settings'],
        ],
    ],
];

…or per page blueprint:

buttons:
  - page.preview
  - page.reindex
  - page.settings

Panel

The plugin registers a menu entry Visitor AI Chat with four tabs:

  • Index — statistics (reachable pages, chunks, last crawl), button for a full reindex, list of indexed chunks
  • Chats — chat log analytics (see Chat logs)
  • Usage — token consumption and costs per provider/model
  • Test — interactive chat to test the prompt directly in the Panel. Requests from this tab are marked as [Test] in the log and aggregated under „Panel test".

If no provider is configured or an API key is missing, the Panel shows a setup notice instead of the tabs, with a specific error message and config example.

Cost and token tracking

The plugin logs every embed and chat request locally in site/cache/fendinger.visitor-ai-chat/usage.json:

  • Token consumption (prompt, completion, total) and request count
  • Aggregated per day, month, and overall — broken down by provider, model, and type

Costs are calculated from tokens using a static price list (USD per 1M tokens). Defaults for OpenAI, Mistral, and Gemini are included in the plugin. Override in site/config/config.php:

'fendinger.visitor-ai-chat' => [
    'pricing' => [
        'openai' => [
            'gpt-4o-mini' => ['input' => 0.15, 'output' => 0.60],
            // ...
        ],
    ],
],

Notes:

  • Prices are calculated locally — no actual billing against providers.
  • OpenAI and Mistral return token counts for both embedding and chat APIs. Gemini returns tokens only for the chat API — embeddings are counted as requests but without a token count (and therefore without cost).
  • IONOS and Mittwald ship with default prices set to $0. IONOS bills per token but rates vary by model and region — fill in your current rates via the pricing override. Mittwald bills per provisioned model slot (monthly), not per token, so token-based cost figures don't apply; track actual costs in mStudio.
  • If a model is not in the price list, tokens are still tracked, but the cost stays at $0.

Chat logs

The plugin logs by default every visitor request along with answer, sources, tokens, and latency — as a JSONL file per month under site/cache/fendinger.visitor-ai-chat/logs/. Disable in config.php:

'fendinger.visitor-ai-chat' => [
    'logging' => [
        'enabled'       => false,  // default: true
        'retentionDays' => 90,     // 0 = unlimited
    ],
],

When logging is disabled, the Chats tab is hidden in the Panel.

In the Panel, the Chats tab shows:

  • Totals, answered/unanswered ratios
  • Top questions (normalized) — ideas for FAQs or content gaps
  • Unanswered questions — detected by empty sources or „I don't know"-style phrases
  • Recent requests (clickable for detail view)

GDPR notes

Logs contain plain-text questions from visitors. Depending on content these may qualify as personal data:

  • Update your privacy policy (mention storage of chat logs, purpose, duration)
  • Set retention restrictively (90 days is often appropriate, unlimited is risky)
  • If not needed, disable logging with 'enabled' => false

The plugin stores no IP addresses and no user agent. Only an anonymous session ID from the browser's sessionStorage (lives only while the tab is open).

Automatic updates

Panel changes (page.create/update/delete) trigger incremental reindexing. A page is only reindexed if it was reachable during the last full crawl or if forceIndex is enabled. A full reindex refreshes the reachability list.

Two reindex modes: full vs. delta

The Panel offers two buttons in the Index tab header:

  • „Run full reindex" — re-embeds every eligible page from scratch. Use this after switching the embedding provider or model, after changing chunk.size / chunk.overlap, or whenever you want a guaranteed clean rebuild. Costs one embedding call per chunk.
  • „Update index" (delta) — crawls everything and embeds new pages plus pages whose rendered output changed (compared via sha1 of the rendered+extracted text against the previous run). Unchanged pages keep their existing chunk vectors and skip the embedding API call entirely. Because the hash is taken on the rendered output (not on page.modified()), it also catches snippet, plugin, translation or site-content changes that ripple through many pages.

The per-page hook (page.update:after) uses the same hash check — a page edit that doesn't change the rendered output won't trigger an embedding call.

A reindex run reports both counts so you can see the savings:

{ "pages": 872, "embedded": 35, "reused": 837, "chunks": 4120, "ms": 18420 }

A provider/model change is auto-detected and forces a full re-embed even if you click Update index (different vector dimensions are not comparable). When in doubt, use the full reindex.

API equivalents:

POST /api/visitor-ai-chat/reindex          # full reindex (default; ?force=0 falls back to delta)
POST /api/visitor-ai-chat/reindex-delta    # delta (skip-unchanged)

What gets indexed?

  • The visible page content — everything the template outputs inside <main> (headings, body text, blocks, lists, tables, static template strings)
  • Per-page image metadata from Kirby files: filename, alt text, caption, title, photographer — regardless of whether the template displays them

Excluded are <nav>, <footer>, <script>, <style>, <noscript> and <svg>. This keeps navigation and footer out of retrieval across all pages, so they don't dilute results.

API

  • POST /visitor-ai-chat/query — public, rate-limited
    • Body: { "query": "...", "history": [{"role":"user|assistant","content":"..."}] }
    • Response: { "answer": "...", "sources": [{"url","title"}], "latencyMs": 234 }
  • POST /api/visitor-ai-chat/reindex — Panel-user protected
    • Response: { "ok": true, "stats": {"pages": N, "chunks": M, "ms": ...} }

Internally the Panel uses additional protected routes under /api/visitor-ai-chat/* (chunks, per-page reindex, test-query, conversation fetch, log reset, page exclude, URL index). These are not intended as a public interface.

Storage

Index: site/cache/fendinger.visitor-ai-chat/index.json — portable, can be deleted for a full rebuild.

Security

  • Rate limiting per IP (Kirby cache, IP SHA1-hashed)
  • Query length limit (default 500 characters)
  • API keys are read only via option() — never exposed to the frontend

Requirements

  • Kirby CMS
  • PHP 8.2+
  • A valid API key for at least one of the supported providers (OpenAI, Mistral, Gemini, IONOS, or Mittwald)

License

Commercial license. See LICENSE for full terms.

One license covers unlimited projects as long as you retain ownership of the code. If you hand a project over to a client, that client needs their own license (same model as Kirby CMS itself).

Author

Jens Fendinger