Skip to content

Choose page paths where the widget is active #5

Description

@ezzcodeezzlife

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 (buildParentEmbedScript in lib/widget-embed.ts) and sends pathname to the iframe.
  • POST /f stores pagePath (max 2048, forced to start with /) in app/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 /blog this 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.pathname only (ignore query/hash).
  • Prefix match: /blog matches /blog and /blog/post, not /blogging.
  • Normalize: ensure leading /, strip trailing / except /.
  • Max ~20 rules, each max 256 chars.
  • Empty allowlist in allowlist mode = 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:

  1. Inline config in the snippet — bad, requires re-copying the snippet on every path change.
  2. Tiny bootstrap: parent script already knows apiOrigin. Before mounting iframe, GET /h or /api/widget/hello with w + parentOrigin + pathname, cache in sessionStorage for a few minutes.
  3. Hello already exists (app/api/widget/hello/route.ts / lib/widget-hello.ts) but only returns { ok, message, widgetId }. Extend hello (or the existing /h route) 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 /admin is 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=ad counts 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 authorized localhost.

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 /f with a disallowed pagePath is 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.prisma RepositoryConfig
  • app/dashboard/[owner]/[repo]/configure/page.tsx + new fields component (mirror AuthorizedDomainsFields)
  • lib/widget-embed.ts parent bootstrap
  • lib/widget-hello.ts / app/h/route.ts / app/api/widget/hello/route.ts — pick one config endpoint and use it
  • app/f/route.ts shared matcher helper isPathAllowed(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)

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:dashboardDashboard, repo configure, accountarea:widgetEmbeddable widget, iframe, parent scriptenhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions