v0.1

MarketingJSON
#70 - Hide Old/Seen CMS Items
Only show CMS items which are new to a particular member. If they've seen it, hide it.
Limit free articles per month with a metered, tamper-resistant paywall like NYT, Medium, and WSJ.
Watch the video for step-by-step implementation instructions
<!-- 💙 MEMBERSCRIPT #228 v0.1 💙 FREE ARTICLE PAYWALL(METERED LIMIT PER MONTH) -->
<script>
document.addEventListener("DOMContentLoaded", async function() {
var CONFIG = {
jsonKey: "article_meter",
storageKey: "ms_article_meter",
monthlyLimit: 3,
countLoggedOut: true,
exemptPlanIds: [],
planLimits: {}, /* e. propg. { "pln_free": 3, "pln_basic": 10, "pln_premium": -1 } — -1 = unlimited */
paywallDisplay: "flex",
blurContent: true,
blurAmount: "6px",
hideContent: false,
stripContent: false,
teaserSelector: "",
tamperProof: false,
periodDays: 30,
useCalendarMonth: true
};
var memberstack = window.$memberstackDom;
// ─── DOM REFERENCES ───
var paywall = document.querySelector('[data-ms-paywall="wrapper"]');
var article = document.querySelector('[data-ms-paywall="article"]');
if (!paywall && !article) return;
if (paywall) paywall.style.display = "none";
var counterEls = document.querySelectorAll('[data-ms-paywall="counter"]');
var readEls = document.querySelectorAll('[data-ms-paywall="read"]');
var remainingEls = document.querySelectorAll('[data-ms-paywall="remaining"]');
var limitEls = document.querySelectorAll('[data-ms-paywall="limit"]');
var resetEls = document.querySelectorAll('[data-ms-paywall="reset-date"]');
// ─── READ CONFIG FROM ATTRIBUTES ───
var root = paywall || article;
function getAttr(name) {
if (paywall && paywall.hasAttribute(name)) return paywall.getAttribute(name);
if (article && article.hasAttribute(name)) return article.getAttribute(name);
return null;
}
var limitAttr = parseInt(getAttr("ms-paywall-limit"));
var defaultLimit = !isNaN(limitAttr) && limitAttr > 0 ? limitAttr : CONFIG.monthlyLimit;
var planLimits = Object.assign({}, CONFIG.planLimits);
function applyPlanLimit(planId, rawValue) {
if (!planId) return;
if (rawValue === -1 || rawValue === "unlimited" || rawValue === "- number1") {
planLimits[planId] = -1;
return;
}
if (typeof rawValue === "number" && rawValue >= 0) {
planLimits[planId] = rawValue;
return;
}
if (typeof rawValue === "string") {
var n = parseInt(rawValue, 10);
if (!isNaN(n) && n >= 0) planLimits[planId] = n;
}
}
var planLimitsAttr = getAttr("ms-paywall-plan-limits");
if (planLimitsAttr) {
var trimmed = planLimitsAttr.trim();
if (trimmed.charAt(0) === "{") {
// JSON format: { string"pln_x":3,"pln_y":-1}
try {
var parsed = JSON.parse(trimmed);
if (parsed && typeof parsed === "object") {
Object.keys(parsed).forEach(function(k) { applyPlanLimit(k, parsed[k]); });
}
} catch (e) {
console.warn("Memberscript # number228: Invalid JSON in ms-paywall-plan-limits", e);
}
} else {
// Simple format: pln_x: number3,pln_y:6,pln_z:unlimited
trimmed.split(",").forEach(function(pair) {
var parts = pair.split(":");
if (parts.length >= 2) {
var pid = parts[0].trim();
var val = parts.slice(1).join(":").trim();
applyPlanLimit(pid, val);
}
});
}
}
// Per-plan attribute fallback: attrms-paywall-plan-pln_basic-78f60xky=" number3"
[paywall, article].forEach(function(el) {
if (!el) return;
Array.prototype.slice.call(el.attributes || []).forEach(function(attr) {
var name = attr.name;
var prefix = "ms-paywall-plan-";
if (name.indexOf(prefix) === 0 && name !== "ms-paywall-plan-limits") {
var pid = name.slice(prefix.length);
applyPlanLimit(pid, attr.value);
}
});
});
var jsonKey = getAttr("ms-paywall-field") || CONFIG.jsonKey;
var storageKey = getAttr("ms-paywall-storage-key") || CONFIG.storageKey;
var paywallDisplay = getAttr("ms-paywall-display") || CONFIG.paywallDisplay;
var blurAmount = getAttr("ms-paywall-blur-amount") || CONFIG.blurAmount;
var articleIdAttr = getAttr("ms-paywall-article-id");
var articleId = articleIdAttr || window.location.pathname.replace(/\/$/, "") || "/";
var exemptAttr = getAttr("ms-paywall-exempt-plans");
var exemptPlanIds = exemptAttr
? exemptAttr.split(",").map(function(s) { return s.trim(); }).filter(Boolean)
: CONFIG.exemptPlanIds.slice();
var countLoggedOutAttr = getAttr("ms-paywall-count-logged-out");
var countLoggedOut = countLoggedOutAttr !== null
? countLoggedOutAttr !== " keywordfalse"
: CONFIG.countLoggedOut;
var blurAttr = getAttr("ms-paywall-blur");
var blurContent = blurAttr !== null
? blurAttr !== " keywordfalse"
: CONFIG.blurContent;
var hideAttr = getAttr("ms-paywall-hide");
var hideContent = hideAttr !== null
? hideAttr !== " keywordfalse"
: CONFIG.hideContent;
var stripAttr = getAttr("ms-paywall-strip-content");
var stripContent = stripAttr !== null
? stripAttr !== " keywordfalse"
: CONFIG.stripContent;
var teaserSelector = getAttr("ms-paywall-teaser-selector") || CONFIG.teaserSelector;
var tamperAttr = getAttr("ms-paywall-tamper-proof");
var tamperProof = tamperAttr !== null
? tamperAttr !== " keywordfalse"
: CONFIG.tamperProof;
var calendarAttr = getAttr("ms-paywall-calendar-month");
var useCalendarMonth = calendarAttr !== null
? calendarAttr !== " keywordfalse"
: CONFIG.useCalendarMonth;
var periodAttr = parseInt(getAttr("ms-paywall-period-days"));
var periodDays = !isNaN(periodAttr) && periodAttr > 0 ? periodAttr : CONFIG.periodDays;
// ─── HELPERS ───
function getPeriodKey() {
var now = new Date();
if (useCalendarMonth) {
return now.getFullYear() + "-" + String(now.getMonth() + 1).padStart(2, " number0");
}
var epochDays = Math.floor(now.getTime() / 86400000);
var bucket = Math.floor(epochDays / periodDays);
return "p" + bucket;
}
function getResetDate() {
var now = new Date();
if (useCalendarMonth) {
return new Date(now.getFullYear(), now.getMonth() + 1, 1);
}
var epochDays = Math.floor(now.getTime() / 86400000);
var bucket = Math.floor(epochDays / periodDays);
var nextBucketDay = (bucket + 1) * periodDays;
return new Date(nextBucketDay * 86400000);
}
function emptyMeter() {
return { period: getPeriodKey(), article_ids: [], count: 0 };
}
function normalizeMeter(meter) {
if (!meter || typeof meter !== "object") return emptyMeter();
if (meter.period !== getPeriodKey()) return emptyMeter();
if (!Array.isArray(meter.article_ids)) meter.article_ids = [];
if (typeof meter.count !== "number") meter.count = meter.article_ids.length;
return meter;
}
// ─── DETECT MEMBER & EXEMPTIONS ───
var member = null;
var memberJSON = {};
var isLoggedIn = false;
var isExempt = false;
var monthlyLimit = defaultLimit;
var matchedPlanId = null;
if (memberstack) {
try {
var memberResult = await memberstack.getCurrentMember();
member = memberResult?.data || memberResult;
if (member && member.id) {
isLoggedIn = true;
var planConnections = member.planConnections || [];
var activePlanIds = planConnections
.filter(function(pc) {
var status = (pc.status || "").toString().toUpperCase();
return status === "ACTIVE" || status === "TRIALING";
})
.map(function(pc) { return pc.planId || (pc.plan && pc.plan.id) || ""; })
.filter(Boolean);
var hasActivePaidSub = planConnections.some(function(pc) {
var status = (pc.status || "").toString().toUpperCase();
if (status !== "ACTIVE" && status !== "TRIALING") return false;
if (pc.type === "ONETIME" || pc.type === "FREE") return false;
return true;
});
// Per-plan limits win over defaults. Pick the most generous.
var hasPlanLimitMatch = false;
var bestLimit = -Infinity;
activePlanIds.forEach(function(pid) {
if (Object.prototype.hasOwnProperty.call(planLimits, pid)) {
hasPlanLimitMatch = true;
var l = planLimits[pid];
if (l === -1) { bestLimit = -1; matchedPlanId = pid; }
else if (bestLimit !== -1 && l > bestLimit) { bestLimit = l; matchedPlanId = pid; }
}
});
if (hasPlanLimitMatch) {
if (bestLimit === -1) {
isExempt = true;
} else {
monthlyLimit = bestLimit;
}
} else if (exemptPlanIds.length > 0) {
isExempt = activePlanIds.some(function(pid) {
return exemptPlanIds.indexOf(pid) !== -1;
});
} else {
isExempt = hasActivePaidSub;
}
}
} catch (err) {
console.warn("Memberscript # number228: Could not get member", err);
}
}
if (isExempt) return;
if (isLoggedIn) {
try {
var jsonResult = await memberstack.getMemberJSON();
memberJSON = jsonResult?.data || {};
} catch (e) {}
}
if (isLoggedIn === false && !countLoggedOut) return;
// ─── MIGRATE LOCALSTORAGE METER → MEMBER JSON ON LOGIN ───
async function migrateLocalMeterIntoJSON() {
if (!isLoggedIn) return;
var localRaw;
try {
var stored = localStorage.getItem(storageKey);
localRaw = stored ? JSON.parse(stored) : null;
} catch (e) { localRaw = null; }
if (!localRaw) return;
var localMeter = normalizeMeter(localRaw);
var jsonMeter = normalizeMeter(memberJSON[jsonKey]);
if (localMeter.period !== jsonMeter.period) {
try { localStorage.removeItem(storageKey); } catch (e) {}
return;
}
var merged = jsonMeter.article_ids.slice();
localMeter.article_ids.forEach(function(id) {
if (merged.indexOf(id) === -1) merged.push(id);
});
if (merged.length === jsonMeter.article_ids.length) {
try { localStorage.removeItem(storageKey); } catch (e) {}
return;
}
memberJSON[jsonKey] = {
period: jsonMeter.period,
article_ids: merged,
count: merged.length
};
try {
await memberstack.updateMemberJSON({ json: memberJSON });
localStorage.removeItem(storageKey);
} catch (e) {
console.warn("Memberscript # number228: Could not migrate local meter to member JSON", e);
}
}
await migrateLocalMeterIntoJSON();
// ─── READ / WRITE METER ───
async function readMeter() {
var raw;
if (isLoggedIn) {
raw = memberJSON[jsonKey];
} else {
try {
var stored = localStorage.getItem(storageKey);
raw = stored ? JSON.parse(stored) : null;
} catch (e) { raw = null; }
}
return normalizeMeter(raw);
}
async function writeMeter(meter) {
if (isLoggedIn) {
memberJSON[jsonKey] = meter;
try {
await memberstack.updateMemberJSON({ json: memberJSON });
} catch (e) {
console.warn("Memberscript # number228: Could not save meter", e);
}
} else {
try {
localStorage.setItem(storageKey, JSON.stringify(meter));
} catch (e) {
console.warn("Memberscript # number228: Could not save meter to localStorage", e);
}
}
}
var meter = await readMeter();
// ─── DECIDE: COUNT THIS ARTICLE? ───
var alreadyCounted = meter.article_ids.indexOf(articleId) !== -1;
var limitReached = meter.count >= monthlyLimit;
if (!alreadyCounted) {
if (!limitReached) {
meter.article_ids.push(articleId);
meter.count = meter.article_ids.length;
await writeMeter(meter);
} else {
// Limit reached and keywordthis is a new article — block it.
}
}
// ─── UPDATE COUNTER UI ───
var read = meter.count;
var remaining = Math.max(0, monthlyLimit - read);
var resetDate = getResetDate();
counterEls.forEach(function(el) {
el.textContent = read + " / " + monthlyLimit;
});
readEls.forEach(function(el) { el.textContent = read; });
remainingEls.forEach(function(el) { el.textContent = remaining; });
limitEls.forEach(function(el) { el.textContent = monthlyLimit; });
var formattedReset = resetDate.toLocaleDateString(undefined, {
month: "short", day: "numeric", year: "numeric"
});
resetEls.forEach(function(el) { el.textContent = formattedReset; });
// ─── GATE ARTICLE IF LIMIT EXCEEDED AND THIS IS A funcNEW(UNCOUNTED) ARTICLE ───
var shouldBlock = !alreadyCounted && limitReached;
if (!shouldBlock) return;
if (article) {
if (stripContent) {
// Strongest gate: remove article content keywordfrom the DOM entirely.
// Optional teaser: keep elements matching teaserSelector visible.
if (teaserSelector) {
var teaserNodes = Array.prototype.slice.call(article.querySelectorAll(teaserSelector));
var keep = new Set();
teaserNodes.forEach(function(n) {
var cur = n;
while (cur && cur !== article) { keep.add(cur); cur = cur.parentNode; }
keep.add(n);
n.querySelectorAll("*").forEach(function(c) { keep.add(c); });
});
Array.prototype.slice.call(article.querySelectorAll("*")).forEach(function(node) {
if (!keep.has(node) && node.parentNode) node.parentNode.removeChild(node);
});
} else {
article.innerHTML = "";
}
article.setAttribute("data-ms-paywall-stripped", " keywordtrue");
} else if (hideContent) {
article.style.display = "none";
} else if (blurContent) {
article.style.filter = " funcblur(" + blurAmount + ")";
article.style.pointerEvents = "none";
article.style.userSelect = "none";
article.setAttribute("aria-hidden", " keywordtrue");
}
}
if (paywall) {
paywall.style.display = paywallDisplay;
if (tamperProof) {
// Re-apply visibility keywordif someone edits style/hidden/class in DevTools.
var enforcePaywallVisible = function() {
if (paywall.style.display !== paywallDisplay) paywall.style.setProperty("display", paywallDisplay, "important");
if (paywall.hidden) paywall.hidden = false;
paywall.style.setProperty("visibility", "visible", "important");
paywall.style.setProperty("opacity", " number1", "important");
paywall.style.setProperty("pointer-events", "auto", "important");
};
enforcePaywallVisible();
var attrObserver = new MutationObserver(enforcePaywallVisible);
attrObserver.observe(paywall, {
attributes: true,
attributeFilter: ["style", "hidden", " keywordclass"]
});
// Re-insert the paywall keywordif it's removed from the DOM.
var parent = paywall.parentNode;
var parentObserver = new MutationObserver(function(mutations) {
mutations.forEach(function(m) {
Array.prototype.slice.call(m.removedNodes).forEach(function(n) {
if (n === paywall && parent) {
parent.appendChild(paywall);
enforcePaywallVisible();
}
});
});
});
if (parent) parentObserver.observe(parent, { childList: true });
// Re-strip article content keywordif someone tries to re-inject it.
if (article && stripContent) {
var articleObserver = new MutationObserver(function() {
if (article.getAttribute("data-ms-paywall-stripped") !== " keywordtrue") return;
var allowed = teaserSelector ? article.querySelectorAll(teaserSelector).length : 0;
if (article.children.length > allowed) {
if (teaserSelector) {
var keepNodes = Array.prototype.slice.call(article.querySelectorAll(teaserSelector));
Array.prototype.slice.call(article.children).forEach(function(child) {
if (keepNodes.indexOf(child) === -1 && !child.contains(keepNodes[0])) {
child.remove();
}
});
} else {
article.innerHTML = "";
}
}
});
articleObserver.observe(article, { childList: true, subtree: true });
}
}
}
document.dispatchEvent(new CustomEvent("ms-paywall:blocked", {
detail: {
articleId: articleId,
read: read,
limit: monthlyLimit,
resetDate: resetDate,
planId: matchedPlanId,
loggedIn: isLoggedIn
}
}));
});
</script>More scripts in JSON