citewidget / laravel
Add the CiteWidget chat widget to a Laravel site with one Blade tag. Answers come from your own datasheets, with citations.
Requires
- php: ^8.2
- illuminate/console: ^12.0|^13.0
- illuminate/contracts: ^12.0|^13.0
- illuminate/log: ^12.0|^13.0
- illuminate/support: ^12.0|^13.0
- illuminate/view: ^12.0|^13.0
Requires (Dev)
- larastan/larastan: ^3.0
- laravel/pint: ^1.18
- orchestra/testbench: ^10.0|^11.0
- pestphp/pest: ^3.8|^4.0|^5.0
- pestphp/pest-plugin-laravel: ^3.0|^4.0|^5.0
Suggests
- spatie/laravel-csp: Lets nonce 'auto' pick up the nonce from app('csp-nonce').
Provides
None
Conflicts
None
Replaces
None
README
Add the CiteWidget chat widget to a Laravel site with one Blade tag. The widget answers buyers' questions from your own datasheets and links every answer to its source page.
<x-citewidget />
The package prints a single external <script ... defer> tag. It adds no inline JavaScript,
needs no build step and runs no JavaScript of its own.
- Requirements: PHP 8.2 or later, Laravel 12 or 13 (Laravel 13 needs PHP 8.3 or later).
Contents
- Install
- Blade usage
- Configuration
- Options
- Content Security Policy
- Livewire
- Inertia (React or Vue)
- Octane and page caches
- Troubleshooting
- Security
- Development
Install
composer require citewidget/laravel
Laravel discovers the package automatically. Add your public workspace key to .env. Copy it from
the Widget page (Embed snippet) or from Settings in your CiteWidget dashboard. It starts with pk_.
CITEWIDGET_KEY=pk_your_public_key
Then put the tag in your layout, just before </body>:
<x-citewidget /> </body>
The tag can also go in <head>, because the script loads with defer.
Optional: the install command
php artisan citewidget:install
The command does three things:
- It publishes
config/citewidget.php. An existing file is kept unless you pass--force. - It adds
CITEWIDGET_KEY=andCITEWIDGET_ENABLED=trueto.env.exampleif they are missing. - It prints the Blade line to add.
It never reads or writes .env. To publish only the config file, run:
php artisan vendor:publish --tag=citewidget-config
Blade usage
The component and the directive render exactly the same tag.
{{-- Settings from config/citewidget.php and your dashboard --}} <x-citewidget /> @citewidget {{-- Override options for one page --}} <x-citewidget position="bottom-left" title="Ask our datasheets" :suggestions="['Max pressure of DN50?', 'Which seal for steam?']" :show-launcher="false" /> @citewidget(['position' => 'bottom-left', 'title' => 'Ask our datasheets'])
- Naming. Blade attributes use kebab-case (
primary-color). Directive arrays and the config file use snake_case (primary_color). - Binding. To pass booleans, numbers and arrays, use
:binding (:show-launcher="false"). - Overrides. Attributes override the config file, and the config file overrides your dashboard. An attribute you leave out keeps the config value.
- Misspelt names throw. A misspelt attribute or key, such as
positon, throws an exception instead of being ignored.
The rendered tag looks like this:
<script src="https://api.citewidget.com/widget/v1/citewidget.js" data-workspace-key="pk_..." data-api-url="https://api.citewidget.com" data-server-url="https://app.citewidget.com" data-position="bottom-left" data-navigate-once nonce="..." defer></script>
Opening the chat from your own button
The widget exposes window.CiteWidget with open(), close(), toggle() and newChat().
Call these from your JavaScript bundle rather than from an inline onclick, so a strict CSP is not affected:
document.querySelector('#ask-us')?.addEventListener('click', () => window.CiteWidget.open());
The widget also dispatches citewidget-open, citewidget-close and citewidget-handoff events
on document. Their detail is always null.
Configuration
.env |
Config key | Default | Notes |
|---|---|---|---|
CITEWIDGET_KEY |
key |
none | Your public pk_ key. Anything else renders nothing and logs a warning. |
CITEWIDGET_ENABLED |
enabled |
true |
Set it to false to render nothing, for example on staging. |
CITEWIDGET_SCRIPT_URL |
script_url |
https://api.citewidget.com/widget/v1/citewidget.js |
https only. |
CITEWIDGET_API_URL |
api_url |
https://api.citewidget.com |
Answers API. https only. |
CITEWIDGET_SERVER_URL |
server_url |
https://app.citewidget.com |
Account API. Without it, the widget gets no dashboard settings, no lead capture and no "Talk to a person". https only. |
| — | nonce |
'auto' |
See Content Security Policy. |
| — | options.* |
all null |
Widget options; see below. null means "not printed". |
env(). It is called only inconfig/citewidget.php, sophp artisan config:cacheworks. After you change.envon a server with cached config, runphp artisan config:cacheagain.- Precedence. Lowest to highest: the widget's built-in defaults, then your dashboard settings, then the options set through this package (config file, then tag).
- Managing from the dashboard. Leave an option
nullto manage it from the dashboard. An option set here overrides the dashboard on every page where it is set.
Options
These are all the options the widget's script tag reads.
| Config / directive key | Blade attribute | Printed as | Accepts | Widget default |
|---|---|---|---|---|
key (config) / workspace_key (tag) |
workspace-key |
data-workspace-key |
Public key: pk_ plus 5–125 of A-Z a-z 0-9 _ - |
— |
api_url |
api-url |
data-api-url |
Absolute https URL | https://api.citewidget.com (via this package) |
server_url |
server-url |
data-server-url |
Absolute https URL | https://app.citewidget.com (via this package) |
title |
title |
data-title |
Text, up to 80 characters | "Ask our docs" |
greeting |
greeting |
data-greeting |
Text, up to 200; the headline of the empty chat | "How can I help?" |
bot_name |
bot-name |
data-bot-name |
Text, up to 60 | "Docs Assistant" |
primary_color |
primary-color |
data-primary-color |
Hex colour: #rgb, #rgba, #rrggbb or #rrggbbaa |
#0F766E |
surface_color |
surface-color |
data-surface-color |
Hex colour; panel background in the light scheme | #ffffff |
color_scheme |
color-scheme |
data-color-scheme |
light, dark or auto (follows the visitor's system) |
light |
position |
position |
data-position |
bottom-right or bottom-left |
bottom-right |
show_launcher |
show-launcher |
data-launcher |
Boolean; false hides the round button (open the chat with CiteWidget.open()) |
true |
launcher_label |
launcher-label |
data-launcher-label |
Text, up to 40 | "Ask AI" |
launcher_icon |
launcher-icon |
data-launcher-icon |
chat, sparkles, headset or book |
chat |
launcher_icon_url |
launcher-icon-url |
data-launcher-icon-url |
Absolute https image URL, up to 2048 characters; falls back to launcher_icon if it fails to load |
none |
soft_refusal |
soft-refusal |
data-soft-refusal |
Text, up to 500; the reply when your documents have no answer | "I don't have this answer in our documents yet." |
goodbye |
goodbye |
data-goodbye |
Text, up to 500; shown when the chat ends | "Thanks for chatting — goodbye! …" |
suggestions |
suggestions |
data-suggestions |
Up to 3 starter questions of up to 200 characters: an array, "Q1|Q2" or a JSON array |
Used only when the workspace has no suggested questions of its own |
handoff |
handoff |
data-handoff |
Boolean; "Talk to a person" (needs server_url) |
true |
handoff_reply_time |
handoff-reply-time |
data-handoff-reply-time |
Text, up to 80, e.g. "usually within a few hours" | "usually within a few hours" |
lead_capture |
lead-capture |
data-lead-capture |
Boolean; asks for name and email after an unanswered question or at the end of the chat (needs server_url) |
true |
chat_duration_minutes |
chat-duration-minutes |
data-chat-duration |
Whole minutes from 0 to 120; 0 means no limit |
0 |
nonce (tag only) |
nonce |
nonce |
CSP nonce for this one tag | From the nonce setting |
How values are checked before printing:
- Booleans.
true/false,1/0,yes/noandon/offare accepted and printed astrueorfalse. - Text. Text is trimmed and shortened to the limit. Blank values count as not set.
- Starter questions. They are printed as a JSON array, with
<,>,&,'and"hex-escaped, inside an HTML-escaped attribute. - Colours. This package accepts hex colours only, although the widget itself also understands
rgb(),hsl()and colour names. - Escaping. Every value is HTML-escaped. Option values are never compiled as Blade, so
{{ }}or@phpinside a title is printed as text. - Invalid values. An invalid value, such as
position="top-left", is left out and logged as a warning, and the rest of the tag still renders.
Content Security Policy
| Directive | Allow | Why |
|---|---|---|
script-src |
https://api.citewidget.com (or the nonce) |
The widget script |
style-src |
The same nonce (or 'unsafe-inline') |
The widget copies the script tag's nonce to its own <style> |
connect-src |
https://api.citewidget.com https://app.citewidget.com |
Answers, dashboard settings, lead capture, "Talk to a person" |
img-src |
blob:, plus the host of launcher_icon_url if you set one |
Cited PDF pages; your launcher image |
The widget loads no frames and no fonts.
If you change script_url, api_url or server_url, allow those hosts instead.
The nonce setting decides which nonce goes on the tag:
nonce setting |
Nonce on the tag |
|---|---|
'auto' (default) |
Vite::cspNonce() if your app called Vite::useCspNonce(); else app('csp-nonce') from spatie/laravel-csp if it is installed; else none |
null |
None |
| Any other string | That string |
A tag can also set its own: <x-citewidget :nonce="$nonce" />. A nonce must be 1–256 characters of
A-Z a-z 0-9 + / = _ -; anything else is left out and logged.
Livewire
The tag carries data-navigate-once, so with wire:navigate the script runs once. When Livewire
replaces <body>, the widget puts its element back, and an open conversation survives the navigation.
Keep <x-citewidget /> in the layout, as usual.
If you prefer Livewire to keep the widget's element itself across navigations, place an empty element
for it inside @persist. The widget uses an existing empty <citewidget-chat> instead of creating one.
@persist('citewidget') <citewidget-chat></citewidget-chat> @endpersist <x-citewidget />
Inertia (React or Vue)
Put <x-citewidget /> in the root template, resources/views/app.blade.php. That template is
rendered on the first visit only. The widget adds its element to <body>, outside the Inertia app
root, so Inertia page visits leave it in place.
To pass options from your React or Vue code instead, use the @citewidget/react or @citewidget/vue
package inside a persistent layout. Use one or the other on a page, not both.
Octane and page caches
The package is safe under Laravel Octane:
- Config and the nonce are read each time the tag renders.
- The renderer is never a singleton.
- No class keeps static state.
Full-page or response caches store the rendered tag, including its nonce. Exclude those pages from the cache, or use a CSP setup whose nonce matches the cached page.
Troubleshooting
| Symptom | Check |
|---|---|
| Nothing renders | php artisan about --only=citewidget shows whether the widget is enabled and whether the key is accepted. The key must start with pk_. A missing key renders nothing, with a debug log entry. A refused key or invalid option logs a warning that starts with "CiteWidget". |
.env change has no effect |
Config is cached: run php artisan config:cache (or config:clear). |
| Dashboard changes don't show | An option set in config/citewidget.php or on the tag overrides the dashboard. Set it back to null or remove the attribute. Also check that server_url is not blank. |
| No lead capture or "Talk to a person" | Both need server_url. |
| Script or styles blocked | See Content Security Policy. The browser console names the blocked directive. |
Unknown CiteWidget option / Unknown <x-citewidget> attribute(s) |
A misspelt name; the message lists the valid ones. |
Security
- Public key only. Only a public
pk_key belongs in a page. The package refuses anything else, such as a secretsk_key: it renders nothing, and its log message never repeats the refused key. - The key is visible. A public key can be read from your page source, like any embed key. Restricting a key to your own domains is planned for the CiteWidget dashboard. Set your allowed domains there once that ships.
- Validation and escaping. URLs must be absolute https URLs without credentials. Every printed value is validated and HTML-escaped.
Development
composer install composer test # Pest composer analyse # PHPStan (Larastan), level max composer lint # Pint --test
tests/Fixtures/embed-attributes.json is a copy of the CiteWidget embed contract. The tests fail when
the option map drifts from it.
Changelog
See CHANGELOG.md.