Visibility & page scope
Decide where the widget appears, who can act on it, and which pages share their pins.
Ways to limit the widget
| Setting | Effect |
|---|---|
Pinned comments / Bug reports (Widget tab, modes.comment / modes.bug) | Turn off comments or bug reports; the menu, shortcuts and Pinwren.open() follow. |
Read-only (Widget tab, readOnly) | Visitors see pins and threads but can't comment, reply, report or resolve. |
Show existing pins to visitors (Privacy & data tab, publicRead) | Off: visitors see no existing feedback — no pins, no panel — but can still send it. |
Hide the launcher (Widget tab, launcher.hidden) | No launcher; open the widget from your own UI with the JavaScript API. |
| Allowed sites (Install tab) | The widget only works on the sites you list; anywhere else it removes itself. |
Where it shows (Widget tab, visibility) (below) | Show the widget only outside production, or only on wide enough screens. |
Who gets it (Widget tab, visibility.mode) (below) | Everyone, signed-in users only, people with a link, some pages, or only when you say so. |
You can also simply not load the script — for example render the snippet only for signed-in staff, or only on your staging deployment.
Environments and screen width
Set these on the project's Widget tab, in the Where it shows card: Environments (Everywhere / Not in production) and Minimum screen width. The rules run in the browser only; the server just stores the setting.
Prop
Type
With environments: "non-production", a host counts as non-production — and the widget shows — when it is:
localhostor*.localhost, a loopback address (127.x.x.x,::1) or0.0.0.0;- a private LAN address (
10.x,192.168.x,172.16–172.31.x); - under
.local,.test,.localhostor.internal; - on a
staging,stage,previewordevsubdomain, also numbered or suffixed (dev2.example.com,preview-42.example.com) — any label except the last, sopinwren.xride-labs.initself is production; - not a network host at all (a
file:page).
Every other host is production. There the widget doesn't start at all — no launcher, no console or network
wrappers, no replay — and logs [pinwren] hidden on this production host to the console.
Below minViewportWidth the widget closes and hides, and comes back when the window is widened again;
Pinwren.open() does nothing while it is hidden.
Force it on
data-force-show (or the forceShow init option) overrides both rules — useful to let one person review
production, or to show the widget on a phone despite minViewportWidth:
<script src="https://feedback.example.com/widget.js" data-project="pk_…" data-force-show async></script>Pinwren.init({ projectKey: "pk_…", endpoint: "https://feedback.example.com", forceShow: true });Who gets the widget
visibility.mode decides who sees the widget at all. Like the rules above it runs in the browser; while a mode
hides the widget, it makes no requests for pins.
| Mode | The widget shows |
|---|---|
always | For everyone (the default). |
identified | After your code calls Pinwren.identify() with a user id or email — e.g. only for signed-in customers. |
queryParam | When the page was opened with ?<queryParam> (default ?pinwren); remembered until the browser tab closes. |
urlPatterns | On pages whose path matches one of urlPatterns (up to 50, the page scope syntax: /app/*, /p/:id). |
manual | Only after your code calls Pinwren.show(). |
Whatever the mode, Pinwren.show() and Pinwren.hide() override it until the other one is called, forceShow
shows the widget, and so do a valid review link (?pinwren_review=<token>)
and a signed "Open on page" link. A review link is remembered until the browser tab closes, asks the guest for
their name before they send anything, and marks what they send with the link's name.
// mode "manual": show the widget to your own staff only
if (user.isStaff) Pinwren.show();Page scope
By default pins belong to the page they were left on. Page addresses are grouped as described in
Page review: no hash, no trailing slash, no tracking
parameters, the query string sorted — so ?a=1&b=2, ?b=2&a=1&utm_source=x and the same with a trailing slash
are one page. Pages built from one template — /product/1,
/product/2, … — therefore each have their own pins.
To review the template once and see the same pins on every instance, give the page a URL pattern:
<script
src="https://feedback.example.com/widget.js"
data-project="pk_…"
data-url-pattern="/product/:id"
async
></script>Pinwren.setPageScope({ urlPattern: "/product/:id" });
Pinwren.setPageScope(null); // back to exact-page pinsNew feedback carries the pattern (page.urlPattern), and the widget asks the server for pins by pattern
instead of exact URL.
| Pattern | Matches |
|---|---|
/product/:id | /product/42, /product/blue-shoe — one path segment per :name |
/blog/* | anything under /blog/, across slashes |
/docs/*/intro | /docs/v1/intro, /docs/guides/start/intro |
Patterns match the path only — origin, query string and hash are ignored — and a trailing slash is optional.
A pattern written as a full URL (https://example.com/product/:id) uses its path. Up to 500 characters.
Single-page apps
In an SPA, call Pinwren.setPageScope() from your router whenever the route template changes, the same way
you would set the page title.