Repository navigation
Choose page paths where the widget is active #5
Description
Summary
Let the repo owner choose which URL paths the widget is active on. Today the script is all-or-nothing: if it is in the layout, every page gets the FAB — including /admin, checkout, preview deploys, and client “please don’t show this to users yet” routes.
Path is already captured (pagePath on WidgetFeedback, sent to the agent). It is not gated. Pro marketing even lists “Page path context tracking” (lib/plan-features.ts) — that is context-for-the-agent, not an allowlist. This issue is the allowlist/blocklist.
Current behavior
- Parent embed always mounts the iframe (
buildParentEmbedScriptinlib/widget-embed.ts) and sendspathnameto the iframe. POST /fstorespagePath(max 2048, forced to start with/) inapp/f/route.ts.- Authorize is domain-only (
RepositoryConfig.authorizedDomains,lib/widget-resolve.ts/lib/widget-origin.ts). No path check. - Configure UI (
configure/page.tsx) has domains, embed snippet, email, agent instructions — no paths.
pagePath is client-supplied. Anyone who can load the widget can POST an arbitrary path. Domain allowlist is the real security boundary; path rules are a product control (where the FAB appears + where we accept submissions).
Problem
Agencies embed one snippet in a shared layout (Next.js app/layout.tsx, WordPress footer, etc.). They still need:
- Widget on marketing pages, not on
/wp-admin,/checkout,/app/* - Staging vs production is already handled by domains; paths handle the rest
- “Launch the widget only on
/blogthis week” without a code deploy
If we only hide the FAB in CSS but still accept POST /f from those paths, a curious user (or scraped widget id) can still submit. Hide in the parent script and reject on the server.
Proposed design
Config (per RepositoryConfig)
widgetPathMode: all | allowlist | denylist // default all (today’s behavior)
widgetPathRules: string[] // path prefixes, e.g. /blog, /pricing
Matching rules (keep them dumb so owners cannot shoot themselves):
- Compare against
location.pathnameonly (ignore query/hash). - Prefix match:
/blogmatches/blogand/blog/post, not/blogging. - Normalize: ensure leading
/, strip trailing/except/. - Max ~20 rules, each max 256 chars.
- Empty allowlist in
allowlistmode = widget never shows (fail closed) + configure warning.
UI on configure, next to Authorized Domains:
- Default: “All pages on authorized domains”
- “Only these paths…” / “Everywhere except…”
- Textarea or tag input, one prefix per line, with live examples.
Runtime
Parent script (best UX, no iframe flash):
The parent currently has no config besides widgetId. Options:
- Inline config in the snippet — bad, requires re-copying the snippet on every path change.
- Tiny bootstrap: parent script already knows
apiOrigin. Before mounting iframe,GET /hor/api/widget/hellowithw+parentOrigin+pathname, cache insessionStoragefor a few minutes. - Hello already exists (
app/api/widget/hello/route.ts/lib/widget-hello.ts) but only returns{ ok, message, widgetId }. Extend hello (or the existing/hroute) to return{ mount: boolean, theme, … }.
Recommendation: (3) — one authenticated-by-widget-id call from the parent. If hello fails, fail closed (do not mount) when we know the owner configured restrictions; if hello 5xx, current behavior (mount) is OK for mode=all to avoid a total outage.
app/h/route.ts is already a short widget endpoint — check it when implementing.
Server POST /f: if path is not allowed, 403 with a generic message (do not leak the rule list).
Caching
/widget/{wid} is max-age=300. Path rules must not be baked into that JS, or owners wait 5 minutes after saving. Put rules behind hello with Cache-Control: private, max-age=60 or no-store.
Assumptions
- This is not a substitute for authorized domains. Paths are evaluated only after origin checks.
- Free tier included. Hiding the widget on
/adminis hygiene, not a Pro upsell. (We can still upsell “Page path context tracking” as agent context — already implemented.) - Query strings do not participate (
/pricing?ref=adcounts as/pricing). - Glob stars (
/app/**) are v2; prefixes are enough. - Localhost page URLs are already stripped from storage (
isLocalDevPageUrl); path rules still apply on localhost if they authorizedlocalhost.
Acceptance criteria
- Configure: mode + prefix list; save with the existing configure form.
- Default for existing repos: all pages (no behavior change).
- Parent does not mount iframe on disallowed paths (no FAB flash if hello returns in time; acceptable brief delay).
-
POST /fwith a disallowedpagePathis rejected even if someone calls the API directly. - Allowlist empty → widget off + explicit warning in UI.
- Changing rules does not require re-copying the embed snippet.
Out of scope
- Per-path different widgets / different repos.
- Time-based schedules (“only weekdays”).
- Matching on hash routers (
/#/settings) — document that SPA hash routes need the host to pass a logical path later if demanded.
Implementation notes
prisma/schema.prismaRepositoryConfigapp/dashboard/[owner]/[repo]/configure/page.tsx+ new fields component (mirrorAuthorizedDomainsFields)lib/widget-embed.tsparent bootstraplib/widget-hello.ts/app/h/route.ts/app/api/widget/hello/route.ts— pick one config endpoint and use itapp/f/route.tsshared matcher helperisPathAllowed(pathname, mode, rules)
See also
- Widget theme (same hello/config channel)
- Element picker (must not start pick mode if widget is not mounted)
- Use-case landings (agencies care about this in sales copy)
Activity
ezzcodeezzlife commentedon Aug 18, 2026
Related: widget theme #8 (shared hello/config API), element picker #1, landings #4