Personalized social previews for shared referral links

/my/:id and /join/:id are now served with server-injected, per-member
Open Graph + Twitter Card tags (social scrapers don't run JS, so the
per-ID meta must be in the static HTML). Strips any baked-in og/twitter
tags and injects a fresh ID-specific set — title/description reference
the member #, branded og-card.jpg image, canonical per-ID og:url. Bare
/my and /join paths unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
martbost
2026-08-15 08:30:52 -05:00
parent db8261be6e
commit 7b33e28d0c
+33
View File
@@ -497,6 +497,35 @@ async function handleApi(req,res,pathname){
return json(res,404,{error:'API endpoint not found'});
}
// Serve /my/:id and /join/:id with personalized Open Graph / Twitter tags so a
// shared personal referral link previews nicely (social scrapers don't run JS,
// so the per-ID meta must be in the static HTML). Strips any baked-in og/twitter
// tags and injects a fresh, ID-specific set using the branded card image.
function serveMemberPage(req,res,file,kind,id){
let html; try{ html=fs.readFileSync(file,'utf8'); }catch(e){ return staticFile(req,res,path.join(PUBLIC_DIR,'404.html'),404); }
const base='https://rmcircle.saasy.top';
const url=kind==='join'?`${base}/join/${id}`:`${base}/my/${id}`;
const title=kind==='join'
? `You're invited to the RM Circle team by Member #${id}`
: `Member #${id}'s RM Circle team — live on the blockchain`;
const desc=kind==='join'
? `A team-first crypto build where every payout is verified on Polygon. See Member #${id}'s real on-chain position and join their team. No income is guaranteed.`
: `Member #${id}'s live position, team, and payments — read straight from the blockchain. Get your 2, help your 2. No income is guaranteed.`;
const img=`${base}/og-card.jpg`;
const og=[
`<meta property="og:type" content="website">`,
`<meta property="og:site_name" content="Crypto Team Build Network">`,
`<meta property="og:title" content="${title}">`,
`<meta property="og:description" content="${desc}">`,
`<meta property="og:url" content="${url}">`,
`<meta property="og:image" content="${img}"><meta property="og:image:width" content="1200"><meta property="og:image:height" content="630">`,
`<meta name="twitter:card" content="summary_large_image"><meta name="twitter:title" content="${title}"><meta name="twitter:description" content="${desc}"><meta name="twitter:image" content="${img}">`
].join('');
html=html.replace(/<meta\s+(?:property="og:[^"]*"|name="twitter:[^"]*")[^>]*>/gi,'');
html=html.replace(/(<head[^>]*>)/i,`$1${og}`);
res.writeHead(200,securityHeaders({'Content-Type':'text/html; charset=utf-8','Cache-Control':'no-cache'}));
res.end(html);
}
const server=http.createServer(async(req,res)=>{
try{
const u=new URL(req.url,`http://${req.headers.host||'localhost'}`),pathname=decodeURIComponent(u.pathname);
@@ -510,6 +539,10 @@ const server=http.createServer(async(req,res)=>{
const body=`<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="robots" content="noindex"><title>${ok?'Unsubscribed':'Invalid link'}</title><link rel="stylesheet" href="/styles.css"></head><body class="admin-bg"><div class="login-panel" style="margin:12vh auto;text-align:center"><h1>${ok?'You are unsubscribed ✓':'Invalid or expired link'}</h1><p style="color:var(--muted)">${ok?`Position #${safeId} will no longer receive alert emails. You can turn them back on anytime from your dashboard.`:'We could not process that unsubscribe link. You can manage alerts from your dashboard.'}</p><a class="btn btn-primary" href="/my/${safeId}">Open dashboard</a></div></body></html>`;
res.writeHead(200,securityHeaders({'Content-Type':'text/html; charset=utf-8','Cache-Control':'no-store'}));return res.end(body);
}
{ let mj;
if((mj=pathname.match(/^\/my\/(\d{1,15})$/)))return serveMemberPage(req,res,path.join(PUBLIC_DIR,'my.html'),'my',mj[1]);
if((mj=pathname.match(/^\/join\/(\d{1,15})$/)))return serveMemberPage(req,res,path.join(PUBLIC_DIR,'join.html'),'join',mj[1]);
}
let file;
if(pathname==='/')file=path.join(PUBLIC_DIR,'index.html');else if(pathname==='/start'||pathname==='/start/')file=path.join(PUBLIC_DIR,'start.html');else if(pathname==='/training'||pathname==='/training/')file=path.join(PUBLIC_DIR,'training.html');else if(pathname==='/admin'||pathname==='/admin/')file=path.join(PUBLIC_DIR,'admin.html');else if(pathname==='/my'||pathname==='/my/'||/^\/my\/\d{1,15}$/.test(pathname))file=path.join(PUBLIC_DIR,'my.html');else if(pathname==='/contract'||pathname==='/contract/')file=path.join(PUBLIC_DIR,'contract.html');else if(pathname==='/disclaimer'||pathname==='/disclaimer/')file=path.join(PUBLIC_DIR,'disclaimer.html');else if(/^\/join\/\d{1,15}$/.test(pathname))file=path.join(PUBLIC_DIR,'join.html');else if(pathname==='/join'||pathname==='/join/'){res.writeHead(302,{Location:'/start'});return res.end();}else{
const safe=path.normalize(pathname).replace(/^([.][.][/\\])+/, '').replace(/^[/\\]+/,'');file=path.join(PUBLIC_DIR,safe);if(!file.startsWith(PUBLIC_DIR))file='';