Current subscription system is fragmented:
pdf-prompt-modal in bookpost.html triggers push subscribe for PDF accessemail_gate.js has its own modal for email gatingpush_subscribe.js has its own card/overlaysubscribe-modal.js with no page-specific logic_data/subscribe_paths.yml)paths:
- match: "/bookprogrammer/**"
mode: overlay
autoShow: true
delay: 3000
incentive:
type: book
bookId: programming-zero-hero
label_ar: "اشترك للحصول على الكتاب كاملاً"
label_en: "Subscribe to unlock the full book"
image: /bookprogrammer/cover.webp
onSubscribe:
route: book-gate
action: unlock
bookId: programming-zero-hero
- match: "/pybook/**"
mode: overlay
autoShow: true
delay: 0 # show immediately on PDF click, not on page load
trigger: pdf-click # only show when PDF button clicked, not on timer
incentive:
type: book
bookId: python-book
label_ar: "اشترك للتحميل"
onSubscribe:
route: book-gate
action: unlock
bookId: python-book
- match: "/techquiz/**"
mode: sheet
autoShow: false # manual trigger only (e.g. after score)
incentive:
type: app
appId: techquiz
label_ar: "اشترك لحفظ نتائجك"
onSubscribe:
route: app-gate
action: unlock
appId: techquiz
- match: "/**" # global fallback
mode: sheet
autoShow: true
delay: 8000
incentive:
type: newsletter
label_ar: "اشترك لتصلك المقالات الجديدة"
onSubscribe:
route: push
action: subscribed
secondaryOffer:
label_ar: "هل تريد أيضاً الوصول إلى الكتب؟"
route: library
Jekyll inlines the matched config at build time into each layout’s AppCoreConfig.subscribeModal. Path matching at build time = zero JS overhead for matching.
Alternative (runtime matching): Store paths in JS, match at runtime with glob. Simpler to update without rebuild, but adds ~2KB of matching logic and runs on every page load. Prefer build-time for performance; runtime for dynamic paths (user-specific).
assets/js/subscribe-modal.js)Single responsibility: show UI, collect subscription, broadcast event.
(function () {
var config = window.AppCoreConfig?.subscribeModal;
if (!config?.enabled) return;
// Display modes
var MODES = { overlay: renderOverlay, sheet: renderSheet };
function show(triggerSource) {
var mode = MODES[config.mode] || MODES.sheet;
mode(config.incentive, triggerSource);
}
function renderSheet(incentive) { /* bottom panel, swipe-dismiss */ }
function renderOverlay(incentive) { /* full backdrop, centered card */ }
function onSubscribeComplete(type, scope) {
// Broadcast — this is the only thing subscribe-modal.js cares about after subscribe
window.dispatchEvent(new CustomEvent('subscribeModal:complete', {
bubbles: true,
detail: {
type: type, // 'push' | 'email'
scope: scope, // 'page' | 'site'
path: location.pathname,
site: location.hostname,
incentive: config.incentive, // { type, bookId, appId, ... }
route: config.onSubscribe // { route, action, bookId, ... }
}
}));
}
// Triggers
if (config.autoShow) {
setTimeout(function () {
if (!alreadySubscribed()) show('timer');
}, config.delay || 5000);
}
// Expose for manual trigger: SubscribeModal.show('pdf-click')
window.SubscribeModal = { show: show };
})();
What subscribe-modal.js does NOT do:
BookGate.unlock() — that’s the router’s jobassets/js/subscribe-router.js or inline in app-core.js)Listens for subscribeModal:complete, reads the route instruction, calls the right gate.
window.addEventListener('subscribeModal:complete', function (e) {
var d = e.detail;
var r = d.route; // { route, action, bookId, appId }
switch (r.route) {
case 'book-gate':
if (window.BookGate) BookGate.unlock(r.bookId);
break;
case 'email-gate':
if (window.EmailGate) EmailGate.unlock();
break;
case 'app-gate':
if (window.AppGate) AppGate.unlock(r.appId);
break;
case 'library':
window.location.href = '/library/';
break;
case 'push':
// already subscribed via push — nothing extra to do
break;
}
});
Adding a new gate = one new case in this switch. No changes to subscribe-modal.js.
The router lives in app-core.js (loads on every page) or as a separate subscribe-router.js loaded whenever subscribeModal is enabled.
Each gate can also listen for the event, independent of the router. This creates two unlock paths:
BookGate.unlock(bookId) directlydetail.route.bookId === this.bookId → self-unlocksThe second path is the resilient one: even if the router isn’t loaded on a page, the gate can unlock itself. Belt and suspenders.
// Inside book-gate.js or bookpost.html inline script
window.addEventListener('subscribeModal:complete', function (e) {
var d = e.detail;
if (d.route?.route === 'book-gate' && d.route?.bookId === MY_BOOK_ID) {
unlockChapter();
}
});
After a successful page-specific subscribe, the modal can offer a secondary action:
function showSecondaryOffer(primaryComplete) {
if (!config.secondaryOffer) return;
// Show a smaller follow-on card: "هل تريد أيضاً...?"
// User can dismiss or accept
// On accept: fire a second subscribeModal:complete with scope:'site'
}
This is always optional (secondaryOffer key in config). No secondary offer = nothing shown after subscribe.
window.AppCoreConfig = {
subscribeModal: {
enabled: true,
mode: 'sheet', // 'overlay' | 'sheet'
autoShow: true,
delay: 5000,
trigger: 'timer', // 'timer' | 'manual' | 'pdf-click' | 'scroll-50'
incentive: {
type: 'newsletter', // 'newsletter' | 'book' | 'app' | 'course' | null
bookId: null,
appId: null,
label_ar: 'اشترك للمتابعة',
label_en: 'Subscribe to follow',
image: null // optional cover image shown in modal
},
onSubscribe: {
route: 'push', // 'push' | 'book-gate' | 'email-gate' | 'app-gate' | 'library'
action: 'subscribed',
bookId: null,
appId: null
},
secondaryOffer: null // or { label_ar, route }
}
};
_data/subscribe_paths.yml — define path configs (no code yet)assets/js/subscribe-modal.js — UI only: sheet + overlay, incentive display, subscribe call, event broadcastapp-core.js — subscribeModal:complete listener, route → gate dispatchenabled: false in each layoutsubscribe_paths.ymlsubscribe-modal.js triggers it directly using PushSubscribe.subscribe() again with scope: 'site' — no extra module needed.window.PushSubscribe before showing. If not loaded, fall back to email subscribe or don’t show. Guard pattern already handles this.