How the product works
Blue Machines Chat Preview lets sales and customer-success teams see how the BlueMachines chat widget looks on a client's website before it is embedded in production.
A user provides three inputs on the home page:
- Client URL — the website to preview (e.g.
https://bluemachines.ai) - Chat widget code — the
<script>embed snippet from BlueMachines - Backdrop mode — how to render the site behind the widget:
auto,iframe, orthumbnail
The app opens a full-screen preview. The client site appears as a live iframe or a screenshot, and the chat widget is overlaid on top in a separate layer. The widget always renders — even when the site backdrop fails to load.
Live iframe
Embeds the real client site. Best when the site allows framing (no X-Frame-Options block).
Thumbnail screenshot
Server-generated PNG via Microlink (optional Firecrawl fallback). Cookie-banner hiding, retries, and static logo fallback on failure.
Auto mode (default)
Checks iframe headers first, then tries live iframe with a short timeout before falling back to thumbnail.
Short links
Save a preview config as /s/{id}. Share the link — recipients land directly in preview.
API logs
Every public API call is logged with type, provider, success status, client URL, and full request/response JSON.
User journeys
| Journey | Entry | Result |
|---|---|---|
| Open preview | Home → Open Preview | New browser tab at /preview?… |
| Copy short link | Home → Copy short link | Clipboard gets {origin}/s/{id} |
| Share short link | Recipient visits /s/{id} |
Redirect to /preview?… with saved params |
| History | Home → History, or /history |
Paginated table of past short links (20 per page); search by client URL substring |
| API logs | Home → API Logs, or /api-logs |
Paginated log of screenshot, iframe-check, and shorten API calls |
| Docs | Home → Docs, or /docs |
This product and API guide |
Home form processing
When the user submits, the app:
- Normalizes the URL (adds
https://if missing) - Validates URL format and non-empty widget code
- Extracts widget attributes:
data-widget-token,data-agent-id,data-agent-profile-id,data-base-url,src - Builds query params (includes
backdroponly when notauto) - Stores
preview:urlandpreview:widgetin sessionStorage as a same-tab fallback
The Additional params section lets you attach arbitrary key/value pairs
(e.g. utm_source, ref) that are carried through to the preview URL
and persisted on the short link. Reserved keys (url, backdrop,
src, and the data-* widget attributes) are blocked.
Preview page
The preview page (/preview) is the core of the product. It stacks three layers
inside a device frame:
Backdrop modes
| URL param | Behavior |
|---|---|
auto (default) |
Server header check + iframe probe → fallback to thumbnail |
iframe |
Live site iframe only; user can switch to thumbnail via settings |
thumbnail |
Screenshot only via screenshot API |
Auto mode resolution
In auto mode the app shows a loading logo while checking headers — the site iframe is
not mounted until iframe embedding is likely allowed. This avoids stuck states and long waits.
Timeouts
| Timeout | Duration | Effect |
|---|---|---|
| iframe-check fetch | 4s | Server header probe; on timeout → not iframeable |
| Header check hang (auto) | 4s | If check stalls, try iframe anyway |
| Iframe reveal | 5s | Fall back to thumbnail if iframe never becomes visible |
| Screenshot per attempt | 12s | Client safety timeout before retry or static logo |
| Screenshot upstream | 28s | Server fetch to Microlink (2 strategies) + optional Firecrawl |
Thumbnail retries
The screenshot API retries Microlink with escalating wait strategies and can fall back to
Firecrawl when FIRECRAWL_API_KEY is configured. The preview page
retries up to 3 times (attempts 0–2) with fresh=1 before showing
the static logo. Cookie consent banners are suppressed via Microlink adblock, injected CSS,
and an accept-button click script.
Logo states
| State | Visual |
|---|---|
| Loading backdrop | Animated BlueMachines logo with shine effect |
| Both iframe and thumbnail failed | Static centered logo, no animation |
| Backdrop loaded | Logo hidden; site iframe or thumbnail visible |
Device frames & settings
Users can switch between desktop (full viewport), tablet (820×1180), and mobile (390×844) frames via the settings gear in the bottom-left corner. On real mobile browsers, the preview renders full-viewport without a simulated bezel.
The settings panel also lets users manually toggle between iframe and thumbnail backdrop modes.
Short links
Short links persist a preview configuration so it can be shared without re-entering the form.
Each link is a 7-character ID stored in the short_links database table.
Stored parameters
Each short link stores a JSON object with these keys:
url— normalized client site URLdata-widget-token,data-agent-id,data-agent-profile-iddata-base-url,src— widget script configurationbackdrop— only when notauto
Deduplication: Identical URL + widget + backdrop always returns the same short ID. No duplicate rows are created.
Redirect flow
- User visits
/s/{id} - App loads stored params from the database
- On miss → redirect to home page
- On hit → redirect to
/preview?{params}
History search
The history page supports client URL search via /history?q=salesforce.
Matching is case-insensitive substring (ILIKE) on the stored params.url field.
Search is debounced in the UI and resets pagination to page 1.
System architecture
App routes
| Path | Purpose |
|---|---|
/ | Home form |
/preview | Live preview with widget overlay |
/history | Paginated short-link history with client URL search |
/api-logs | Paginated public API request logs |
/docs | This guide (served as HTML) |
/s/{id} | Short-link redirect to preview |
Tech stack
| Layer | Technology |
|---|---|
| Framework | TanStack Start (React 19, SSR) |
| Routing | TanStack Router |
| Styling | Tailwind CSS v4 + shadcn/ui |
| Database | Supabase |
| Deploy | Cloudflare Workers |
Public API reference
Three HTTP endpoints are exposed under /api/public/. All support CORS
(Access-Control-Allow-Origin: *) for cross-origin use.
GET /api/public/iframe-check
No auth
Checks whether a target URL can be embedded in an iframe by the preview app.
Query parameters
| Param | Required | Description |
|---|---|---|
url | Yes | Target site URL to check |
How it works
- Validates the URL
- Fetches the target with a browser User-Agent (4 second timeout)
- Reads
X-Frame-Optionsand CSPframe-ancestorsheaders - Compares allowed origins against the preview app's origin
Response
{
"iframeable": true,
"status": 200,
"xFrameOptions": null,
"csp": null
}
Returns iframeable: false when headers block embedding, or on network/timeout errors.
Cached for 1 hour (max-age=3600). Each call is logged with provider
embeddable or non-embeddable.
GET /api/public/screenshot
No auth
Returns a PNG/JPEG/GIF screenshot of a target URL. Proxies Microlink.io server-side with
optional Firecrawl fallback when FIRECRAWL_API_KEY is set.
Query parameters
| Param | Required | Default | Description |
|---|---|---|---|
url | Yes | — | Valid HTTP(S) URL |
width | No | 1440 | 320–2560 viewport width |
height | No | 900 | 480–2560 viewport height |
fresh | No | — | Set to 1 to bypass cache and force re-capture |
attempt | No | — | Retry attempt number (used with fresh=1) |
Upstream behavior
- Microlink (primary):
waitUntil=auto,waitForTimeout5–8s, adblock, cookie-banner CSS/scripts, 2 server-side strategies - Firecrawl (fallback): used when Microlink fails and API key is configured
- Provider logged as
microlinkorfirecrawl
Response
Raw image bytes with appropriate Content-Type. Returns 502 JSON on failure (28s server timeout). Cached for 24 hours unless fresh=1.
Example
GET /api/public/screenshot?url=https%3A%2F%2Fexample.com&width=1440&height=900
POST /api/public/shorten
Bearer token
Programmatically create a short link for a preview configuration. Used by integrations and automation.
Authentication
Authorization: Bearer <your-token>
Returns 401 Unauthorized if the token is missing or invalid.
Request body
{
"url": "https://example.com",
"widget": "<script src=\"...\" data-widget-token=\"...\" ...></script>",
"backdrop": "auto",
"params": { "utm_source": "newsletter", "ref": "abc" }
}
| Field | Required | Description |
|---|---|---|
url | Yes | Client site URL (max 2000 chars) |
widget | Yes | Full widget embed script (max 20000 chars) |
backdrop | No | auto, iframe, thumbnail, or screenshot |
params | No | Extra query params merged into the preview URL (max 20 keys, key ≤ 100 chars, value ≤ 2000 chars). Reserved keys (url, backdrop, src, data-widget-token, data-agent-id, data-agent-profile-id, data-base-url) return 400. |
Response
{
"id": "abc1234",
"shortUrl": "https://preview.example.com/s/abc1234",
"previewUrl": "https://preview.example.com/preview?url=...",
"deduped": false
}
Same URL + widget + backdrop + extra params returns the existing ID (deduplicated, with
deduped: true). Different extras produce a new short link. The app normalizes
URLs and extracts widget attributes automatically from the script string.
Example
curl -X POST https://preview.example.com/api/public/shorten \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <your-token>" \
-d '{
"url": "https://bluemachines.ai",
"widget": "<script src=\"...\" data-widget-token=\"...\" data-agent-id=\"...\" data-agent-profile-id=\"...\"></script>",
"backdrop": "auto"
}'
ALL /api/public/widget-proxy/*
No auth
Transparent reverse proxy to https://api.bluemachines.ai. Used by the preview
page so the embedded widget can call the BlueMachines API from the preview origin without
CORS issues. Forwards method, path, query, headers (minus Authorization,
Cookie, Set-Cookie), and body; copies the upstream
Content-Type back.
GET /api/public/widget-proxy/v2/agents/<id>
POST /api/public/widget-proxy/v2/chat/messages
All requests are recorded in api_logs with api_type = "widget-proxy".
Binary upstream bodies are summarized as [binary N bytes] in the log.
API summary
| Endpoint | Method | Auth | Purpose |
|---|---|---|---|
/api/public/iframe-check |
GET | None | Check if URL is iframe-embeddable |
/api/public/screenshot |
GET | None | Get page screenshot as image |
/api/public/shorten |
POST | Bearer token | Create shareable short link |
/api/public/widget-proxy/* |
ALL | None | Reverse proxy to api.bluemachines.ai |
All endpoints support OPTIONS for CORS preflight.
Error responses return JSON: { "error": "description" } with appropriate HTTP status codes
(400, 401, 502, etc.).
Every request is recorded in the api_logs table — see API logs.
API logs
All public API endpoints write a row to the api_logs Supabase table on every request.
View them at /api-logs (linked from the home page header).
Logged fields
| Field | Description |
|---|---|
api_type | screenshot, iframe-check, shorten, or widget-proxy |
provider | microlink / firecrawl for screenshots; embeddable / non-embeddable for iframe checks; widget-proxy for proxy calls; empty for shorten |
success | Whether the API returned a successful result |
status_code | HTTP status returned to the client |
duration_ms | Server-side request duration |
request | JSON payload — includes client URL, dimensions, etc. |
response | JSON summary of the response (not raw image bytes) |
UI features
- Paginated table (20 per page) sorted newest first
- Client URL column extracted from
request.url - Success / failed badge and HTTP status code
- View JSON dialog with full request and response objects
Apply the api_logs migration in supabase/migrations/ before using this feature.
Logging is fire-and-forget — a logging failure never blocks the API response.
Error handling & edge cases
| Scenario | Behavior |
|---|---|
| Site blocks iframe (X-Frame-Options / CSP) | Auto mode falls back to thumbnail screenshot |
| Thumbnail screenshot fails (WAF, timeout) | Up to 3 client retries with fresh=1, then static BlueMachines logo; widget still works |
| Auto mode iframe slow or blocked | Falls back to thumbnail within ~5–9s (header check + iframe reveal caps) |
| Cookie consent banners in screenshots | Microlink adblock + injected CSS/click script; fresh=1 bypasses bad cached captures |
| Invalid or missing preview params | "No preview data" message with link back to home |
| Short link ID not found | Redirect to home page |
| Widget renders regardless | Chat widget overlay always appears on top of any backdrop state |