1/** 2 * KanseiLink Paywall â server-enforced premium content loader. 3 * 4 * Articles contain EMPTY placeholders: 5 * <div data-tier="pro" data-premium-id="insights/some-article"></div> 6 * The premium HTML is NOT in the static page (the repo and Pages are public); 7 * it lives in the KanseiLink API DB and is fetched here for subscribers: 8 * GET /api/premium?article=<id>&email=<email>&token=<access-token> 9 * 10 * Credentials (localStorage): 11 * kl_email â subscriber email 12 * kl_token â per-email access token, issued ONLY by: 13 * - /api/access-token after Stripe checkout (subscription/success.html), or 14 * - magic-link sign-in (subscription/login.html) 15 * 16 * Adding new gated articles: wrap the section with data-tier, then run 17 * node scripts/extract-premium-sections.mjs && node scripts/upload-premium-content.mjs 18 */ 19(function () { 20 "use strict"; 21 22 var API_BASE = window.KANSEI_API_BASE || "https://kansei-link-mcp-production.up.railway.app"; 23 var STORAGE_KEY = "kl_email"; 24 var TOKEN_KEY = "kl_token"; 25 26 // Detect language from <html lang="..."> 27 var lang = (document.documentElement.lang || "ja").startsWith("en") ? "en" : "ja"; 28 var LOGIN_PATH = lang === "en" ? "/en/subscription/login.html" : "/subscription/login.html"; 29 var PRICING_PATH = lang === "en" ? "/en/pricing.html" : "/pricing.html"; 30 31 var TIER_LABELS = { 32 ja: { 33 pro: { badge: "Pro", title: "Pro ãã©ã³ã§å ¨æãèªã", desc: "Agent Voice詳細ãã¬ã·ãæåçãgotchasãGPT/Claude/Geminiæ¯è¼ãè¦æ¾é¡ã", cta: "Pro ãã©ã³ã«ç»é²ï¼$19/æï¼", login: "ç»é²æ¸ã¿ã®æ¹ã¯ãã°ã¤ã³" }, 34 team: { badge: "Team", title: "Team ãã©ã³ã§è©³ç´°ã¬ãã¼ããè¦ã", desc: "æå®ãµã¼ãã¹ã®è©³ç´°ã¬ãã¼ããç«¶åæ¯è¼ãAXRæ¨ç§»ãAgent Voiceçãã¼ã¿ã«ã¢ã¯ã»ã¹ã", cta: "Team ãã©ã³ã«ç»é²ï¼$149/æï¼", login: "ç»é²æ¸ã¿ã®æ¹ã¯ãã°ã¤ã³" }, 35 upgrade: "ãã©ã³ã®ã¢ããã°ã¬ã¼ããå¿ è¦ã§ã", 36 error: "ã³ã³ãã³ããèªã¿è¾¼ãã¾ããã§ãããåèªã¿è¾¼ã¿ãã¦ãã ããã" 37 }, 38 en: { 39 pro: { badge: "Pro", title: "Read the full article with Pro", desc: "Unlock Agent Voice details, recipe success rates, gotchas, and GPT/Claude/Gemini comparison.", cta: "Subscribe to Pro ($19/mo)", login: "Already subscribed? Sign in" }, 40 team: { badge: "Team", title: "Access detailed reports with Team", desc: "Per-service detailed reports, competitive analysis, AXR trends, and raw Agent Voice data.", cta: "Subscribe to Team ($149/mo)", login: "Already subscribed? Sign in" }, 41 upgrade: "A plan upgrade is required for this section", 42 error: "Could not load this section. Please reload the page." 43 } 44 }; 45 46 function getEmail() { 47 return localStorage.getItem(STORAGE_KEY) || ""; 48 } 49 function getToken() { 50 return localStorage.getItem(TOKEN_KEY) || ""; 51 } 52 53 // âââ CTA card (rendered INSIDE the empty placeholder) âââ 54 function renderCTA(section, requiredTier, opts) { 55 var labels = TIER_LABELS[lang][requiredTier] || TIER_LABELS[lang].pro; 56 var note = (opts && opts.note) || ""; 57 section.innerHTML = 58 '<div class="kl-paywall-cta" style="text-align:center;padding:36px 24px;margin:24px 0;background:linear-gradient(135deg,#F4F5FD 0%,#EEF2FF 100%);border:1px solid #E0E7FF;border-radius:16px;">' + 59 '<span style="display:inline-block;padding:4px 12px;background:var(--teal,#00bfa5);color:#fff;border-radius:12px;font-size:12px;font-weight:700;margin-bottom:12px;">' + labels.badge + '</span>' + 60 '<h3 style="margin:0 0 8px;font-size:20px;">' + labels.title + '</h3>' + 61 '<p style="color:#666;margin:0 auto 20px;font-size:15px;line-height:1.6;max-width:480px;">' + labels.desc + '</p>' + 62 (note ? '<p style="color:#b45309;margin:0 0 16px;font-size:13.5px;">' + note + '</p>' : '') + 63 '<button class="kl-checkout-btn" data-plan="' + requiredTier + '" style="display:inline-block;padding:12px 32px;background:var(--teal,#00bfa5);color:#fff;border:none;border-radius:8px;font-size:16px;font-weight:700;cursor:pointer;margin-bottom:12px;">' + labels.cta + '</button>' + 64 '<br><a href="' + LOGIN_PATH + '" class="kl-login-link" style="color:var(--teal,#00bfa5);font-size:14px;text-decoration:none;">' + labels.login + '</a>' + 65 '</div>'; 66 } 67 68 // âââ Premium fetch âââ 69 function loadPremium(section, articleId, email, token) { 70 var requiredTier = section.getAttribute("data-tier") || "pro"; 71 var url = API_BASE + "/api/premium?article=" + encodeURIComponent(articleId) + 72 "&email=" + encodeURIComponent(email) + 73 "&token=" + encodeURIComponent(token); 74 var xhr = new XMLHttpRequest(); 75 xhr.open("GET", url); 76 xhr.onload = function () { 77 if (xhr.status === 200) { 78 try { 79 var data = JSON.parse(xhr.responseText); 80 section.innerHTML = data.html; 81 section.setAttribute("data-premium-loaded", "1"); 82 return; 83 } catch (e) { /* fall through to CTA */ } 84 renderCTA(section, requiredTier, { note: TIER_LABELS[lang].error }); 85 } else if (xhr.status === 403) { 86 // Signed in, but the plan is too low for this section. 87 renderCTA(section, requiredTier, { note: TIER_LABELS[lang].upgrade }); 88 } else if (xhr.status === 401) { 89 // Token missing/stale (e.g. secret rotation) â sign in again. 90 renderCTA(section, requiredTier); 91 } else { 92 renderCTA(section, requiredTier, { note: TIER_LABELS[lang].error }); 93 } 94 }; 95 xhr.onerror = function () { 96 renderCTA(section, requiredTier, { note: TIER_LABELS[lang].error }); 97 }; 98 xhr.send(); 99 } 100 101 // âââ Checkout (price IDs come from /api/config at click time) âââ 102 function startCheckout(plan) { 103 var xhr = new XMLHttpRequest(); 104 xhr.open("GET", API_BASE + "/api/config"); 105 xhr.onload = function () { 106 var priceId = ""; 107 if (xhr.status === 200) { 108 try { 109 var prices = (JSON.parse(xhr.responseText).prices || {}); 110 priceId = plan === "team" ? prices.team : prices.proMonthly; 111 } catch (e) { /* ignore */ } 112 } 113 if (!priceId) { 114 window.location.href = PRICING_PATH; 115 return; 116 } 117 var body = { priceId: priceId }; 118 var email = getEmail(); 119 if (email) body.email = email; 120 var checkout = new XMLHttpRequest(); 121 checkout.open("POST", API_BASE + "/api/checkout"); 122 checkout.setRequestHeader("Content-Type", "application/json"); 123 checkout.onload = function () { 124 if (checkout.status === 200) { 125 try { 126 var data = JSON.parse(checkout.responseText); 127 if (data.url) { window.location.href = data.url; return; } 128 } catch (e) { /* ignore */ } 129 } 130 window.location.href = PRICING_PATH; 131 }; 132 checkout.onerror = function () { window.location.href = PRICING_PATH; }; 133 checkout.send(JSON.stringify(body)); 134 }; 135 xhr.onerror = function () { window.location.href = PRICING_PATH; }; 136 xhr.send(); 137 } 138 139 // âââ Main âââ
140 function init() { 141 var sections = document.querySelectorAll("[data-premium-id]"); 142 if (sections.length) { 143 var email = getEmail(); 144 var token = getToken(); 145 for (var i = 0; i < sections.length; i++) { 146 var section = sections[i]; 147 var articleId = section.getAttribute("data-premium-id"); 148 if (email && token) { 149 loadPremium(section, articleId, email, token); 150 } else { 151 renderCTA(section, section.getAttribute("data-tier") || "pro"); 152 } 153 } 154 } 155 156 // Event delegation for CTA checkout buttons. Acts ONLY on buttons with 157 // data-plan (our CTA cards) or data-price (legacy explicit price ID) â 158 // pricing.html has its own .kl-checkout-btn handler using 159 // data-price-monthly/annual, which must not double-fire here. 160 document.addEventListener("click", function (e) { 161 var btn = e.target.closest(".kl-checkout-btn"); 162 if (btn && (btn.hasAttribute("data-plan") || btn.getAttribute("data-price"))) { 163 e.preventDefault(); 164 var explicitPrice = btn.getAttribute("data-price"); 165 if (explicitPrice) { 166 // Legacy path: a concrete price ID was provided on the button. 167 var body = { priceId: explicitPrice }; 168 var email = getEmail(); 169 if (email) body.email = email; 170 var xhr = new XMLHttpRequest(); 171 xhr.open("POST", API_BASE + "/api/checkout"); 172 xhr.setRequestHeader("Content-Type", "application/json"); 173 xhr.onload = function () { 174 if (xhr.status === 200) { 175 try { var d = JSON.parse(xhr.responseText); if (d.url) window.location.href = d.url; } catch (err) { /* ignore */ } 176 } 177 }; 178 xhr.send(JSON.stringify(body)); 179 } else { 180 startCheckout(btn.getAttribute("data-plan") || "pro"); 181 } 182 } 183 }); 184 } 185 186 if (document.readyState === "loading") { 187 document.addEventListener("DOMContentLoaded", init); 188 } else { 189 init(); 190 } 191})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.