Fix missing QR on every member page — blocked by our own CSP

The QR on /p/<id> and /p/<id>/<slug> was drawn by an inline <script>, but the
site sends `script-src 'self'` with no 'unsafe-inline'. The browser silently
refused to run it, so the QR box rendered as an empty white square on every
member page, not just Funnel Factory ones.

Moved the bootstrap to /page-qr.js and pass the link via a data attribute, so
no page data is interpolated into executable script and the CSP stays as
strict as it is. Loosening script-src to fix this would have traded a
site-wide security property for one widget.

This matters more than it looks: these pages go on printed flyers and ad
destinations, where an unscannable QR is discovered by the person holding the
paper.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
martbost
2026-08-28 11:28:48 -05:00
parent 7ca89f25ee
commit 1d52597c1c
2 changed files with 33 additions and 2 deletions
+31
View File
@@ -0,0 +1,31 @@
// Draws the QR on a member's hosted page (/p/<id> and /p/<id>/<slug>).
//
// This lives in its own file rather than inline in the rendered HTML for one
// reason: the site's CSP is `script-src 'self'` with no 'unsafe-inline', so an
// inline bootstrap is silently blocked and the QR box renders as an empty white
// square. Nothing errors visibly — the page just quietly loses its QR, which is
// exactly the sort of failure nobody notices until a printed flyer or an ad
// lands somewhere and cannot be scanned.
//
// The link comes from a data attribute so no page data is ever interpolated
// into executable script.
(function () {
'use strict';
function draw() {
var el = document.getElementById('ppQr');
if (!el) return;
var link = el.getAttribute('data-link');
if (!link || typeof qrcode !== 'function') return;
try {
var q = qrcode(0, 'M');
q.addData(link);
q.make();
el.innerHTML = q.createSvgTag({ cellSize: 4, margin: 2, scalable: true });
} catch (e) { /* leave the box empty rather than break the page */ }
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', draw);
} else {
draw();
}
})();
+2 -2
View File
@@ -157,11 +157,11 @@ function render(rec) {
(bullets ? '<ul>' + bullets + '</ul>' : '') + (bullets ? '<ul>' + bullets + '</ul>' : '') +
'<div class="cta"><p>' + esc(c.closing || 'Take a look and see what you think.') + '</p>' + '<div class="cta"><p>' + esc(c.closing || 'Take a look and see what you think.') + '</p>' +
'<a class="btn btn-primary" href="' + esc(link) + '">See how it works →</a>' + '<a class="btn btn-primary" href="' + esc(link) + '">See how it works →</a>' +
'<div class="qr" id="ppQr"></div><div class="micro" style="color:var(--muted)">or scan · ' + esc(link.replace(/^https:\/\//, '')) + '</div></div>' + '<div class="qr" id="ppQr" data-link="' + esc(link) + '"></div><div class="micro" style="color:var(--muted)">or scan · ' + esc(link.replace(/^https:\/\//, '')) + '</div></div>' +
'<div class="foot">Independent team resource shared by an individual member. Participation takes real effort and involves cryptocurrency risk, including risk of total loss. No income is guaranteed. ' + '<div class="foot">Independent team resource shared by an individual member. Participation takes real effort and involves cryptocurrency risk, including risk of total loss. No income is guaranteed. ' +
'<a href="/disclaimer" style="color:var(--teal)">Disclaimers</a> · <a href="/contract" style="color:var(--teal)">How the contract works</a></div>' + '<a href="/disclaimer" style="color:var(--teal)">Disclaimers</a> · <a href="/contract" style="color:var(--teal)">How the contract works</a></div>' +
'</main>' + '</main>' +
'<script src="/qrlib.js"></script><script>(function(){try{var q=qrcode(0,"M");q.addData(' + JSON.stringify(link) + ');q.make();document.getElementById("ppQr").innerHTML=q.createSvgTag({cellSize:4,margin:2,scalable:true});}catch(e){}})();</script>' + '<script src="/qrlib.js"></script><script src="/page-qr.js"></script>' +
'<script src="/chat.js" defer></script><script src="/translate.js" defer></script>' + '<script src="/chat.js" defer></script><script src="/translate.js" defer></script>' +
'</body></html>'; '</body></html>';
} }