Teaser:
Last Published: 7/12/2026
Author: webresto
Category:
Tags: popup, modal, ads, sails, hook
Free Module
A Sails hook that shows a blocking full-screen popup over the site and lets admins configure several of them, ranked between each other. Only one popup is shown per visit — the active one with the lowest priority number wins.
The creative is a single image; everything else is behaviour settings. The admin editor uses Adminizer’s shared media manager, so an image can be selected from the existing library or uploaded without leaving the popup form. Per popup you configure:
sortOrder) — rank popups; the lowest active one shows.center, top, bottom, or any corner.workTime editor; a popup only shows within its schedule.Admin module ──► BlockingPopup rows ──► GET /blocking-popups ──► dist/module.js widget
(React/Inertia) (DB, workTime) (server picks winner) (renders the modal)
The server evaluates each popup’s workTime (in the configured timezone) and
returns the single popup to show. The client widget handles delay, position,
overlay, close/countdown and the localStorage cooldown.
The module registers itself as an admin-frontend snippet, so on any frontend
built through admin-frontend the widget <script> is injected automatically
(at the bottom of <body>). The operator sees it in the Snippets list as
blocking-popup and can enable/disable it there — no manual work needed. See
Snippet registration below.
To add the widget yourself, include the served script on any page. It auto-runs on load:
<script src="/blocking-popups/widget.js" defer></script>
Optional configuration — set a global before the script tag:
<script>
window.BLOCKING_POPUP = {
endpoint: "/blocking-popups", // REST endpoint (default)
auto: true, // auto-run on load (default)
zIndex: 2147483000 // base z-index (default)
};
</script>
<script src="/blocking-popups/widget.js" defer></script>
To trigger manually instead of on load, set auto: false and call:
window.BlockingPopupWidget.run();
The widget fails safe. If the REST backend is down, unreachable, times out
(8s), returns a non-2xx status, or sends malformed JSON, nothing is shown and
nothing is thrown — the widget only writes a single informational line to the
console (console.info, never console.error):
[blocking-popup] REST endpoint unavailable, no popup shown (network error)
The host page is never affected.
GET /blocking-popups → { ad: PublicPopup | null } — the single popup to show this visit.GET /blocking-popups?all=true → { ads: PublicPopup[] } — the full ordered queue (for custom clients).GET /blocking-popups/widget.js → the client widget script (application/javascript).Data responses are sent with Cache-Control: no-store; the widget script with max-age=3600.
On initialize the module subscribes to the core emitter event
admin-frontend:collect-snippets (subscriber id blocking-popup) and returns a
single ProvidedSnippet:
| Field | Value |
|---|---|
module |
blocking-popup |
key |
widget |
placement |
bodyBottom |
content |
<script src="/blocking-popups/widget.js" defer> |
enabledByDefault |
true |
admin-frontend polls this on every snippet listing and every recipe build, so the operator can toggle it on/off, and it drops out of the build automatically if the module stops answering (marked Unavailable). See src/lib/registerSnippet.ts.
config/blocking-popup.js (see config.example.js):
module.exports["blocking-popup"] = {
// Timezone used to evaluate each popup's workTime schedule.
timezone: "Europe/Moscow",
// Adminizer's registered default media manager used for popup images.
mediaManager: {
id: "default",
group: "blocking-popups",
accept: ["image/jpeg", "image/png"],
},
};
A custom React / Inertia admin module (under the Marketing section), built
with Vite exactly like the webresto core modules and served like admin-frontend.
It lists popups as cards, edits every field inline, and lets you toggle isActive
or delete without leaving the page. The image picker reuses the configured Adminizer
media manager (default, group blocking-popups); selection and multipart upload are
sent through Adminizer’s standard media-manager routes and access right. The selected public URL is stored as
{ url } JSON (kept compatible with the client widget); the workTime schedule is an optional
JSON array (a malformed schedule fails open, so the popup still shows).
${routePrefix}/blocking-popups-manager${routePrefix}/blocking-popups-manager/{list,upsert,toggle,delete}/blocking-popups-admin-assets/BlockingPopupsManager.jsFollowing the core convention (“separate view/edit rights for modules”), three tokens are registered in the Marketing department:
| Token | Grants |
|---|---|
blocking-popups-manager |
Legacy full access (view + manage) |
blocking-popups-view |
Read-only: open the page and list popups |
blocking-popups-manage |
Create / edit / toggle / delete popups |
The navbar link and page are gated by blocking-popups-view; every write endpoint
re-checks blocking-popups-manage server-side, and the UI hides write controls for
view-only users. blocking-popups-view is added to the default Marketer group.
npm install
npm run build # tsc → verify:build → browserify src/client/widget.js → dist/module.js
npm run build:adminizer # vite build → assets/blocking-popups-admin/BlockingPopupsManager.js
dist/module.js is the ready-to-serve client widget bundle. assets/blocking-popups-admin/
holds the admin module bundle served to the admin panel.
/blocking-popups-admin-assets is now mounted at hook-initialize time via
src/hook/bindAssets.ts (same as core’s hook/bindAssets). Previously it was
mounted inside the Adminpanel:afterHook:loaded handler — middleware added at
that point lands after the app’s frontendRoutes catch-all (config/http.js),
so the bundle URL returned the site index.html instead of JS.Blocking Popups under the Marketing section), built with Vite the same way
webresto core builds its modules and admin-frontend serves its bundle.blocking-popups-manager — legacy full accessblocking-popups-view — read-only (opens the page, lists popups)blocking-popups-manage — create / edit / toggle / delete
Every write endpoint re-checks the manage permission server-side; the UI hides
all write controls for view-only users.blocking-popups-view token is added to the default Marketer group.