Badge emblems, profile social links, and SEO/OG social previews
- Achievement badge share-image now draws a milestone emblem (⚡🎯⭐🏆) in the medal - Profile: Facebook/X/YouTube/Instagram/TikTok/Telegram/LinkedIn/Website links, shown on the public bio page - OG + Twitter Card tags on homepage/ledger/contract (hero banner as share image) - Per-member OG tags server-injected into /wall/<username> so shared bio links preview with name/bio/avatar - robots.txt + sitemap.xml; .txt/.xml MIME types Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -151,7 +151,7 @@ function siteConfig() {
|
||||
const MIME = { '.html': 'text/html; charset=utf-8', '.css': 'text/css', '.js': 'text/javascript',
|
||||
'.png': 'image/png', '.jpg': 'image/jpeg', '.svg': 'image/svg+xml', '.webp': 'image/webp',
|
||||
'.ico': 'image/x-icon', '.json': 'application/json', '.mp4': 'video/mp4', '.woff2': 'font/woff2',
|
||||
'.gif': 'image/gif', '.webm': 'video/webm' };
|
||||
'.gif': 'image/gif', '.webm': 'video/webm', '.txt': 'text/plain; charset=utf-8', '.xml': 'application/xml; charset=utf-8' };
|
||||
const CSP = "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; img-src 'self' data: https:; media-src 'self' https: blob:; connect-src 'self'; font-src 'self' data: https://fonts.gstatic.com; form-action 'self'; frame-src https: http:";
|
||||
function baseHeaders(extra) {
|
||||
return Object.assign({ 'Content-Security-Policy': CSP, 'X-Content-Type-Options': 'nosniff',
|
||||
@@ -684,7 +684,18 @@ const server = http.createServer(async (req, res) => {
|
||||
if (avatar && !/^(\/uploads\/[a-z0-9]{24}\.(png|jpg|webp|gif)|https:\/\/[^\s]+)$/i.test(avatar))
|
||||
return json(res, 400, { error: 'Avatar must be an uploaded image or an https image URL.' });
|
||||
const bio = b.bio === undefined ? undefined : String(b.bio || '').trim().slice(0, 600);
|
||||
const r = await accounts.setProfile(s.email, avatar, bio);
|
||||
// socials: {platform: url}; keep only known platforms with valid https urls
|
||||
let socials;
|
||||
if (b.socials !== undefined) {
|
||||
const PLAT = ['facebook', 'twitter', 'youtube', 'instagram', 'tiktok', 'telegram', 'linkedin', 'website'];
|
||||
const clean = {};
|
||||
for (const p of PLAT) {
|
||||
const v = String((b.socials && b.socials[p]) || '').trim();
|
||||
if (v && /^https:\/\/[^\s]+$/i.test(v) && v.length <= 200) clean[p] = v;
|
||||
}
|
||||
socials = Object.keys(clean).length ? JSON.stringify(clean) : null;
|
||||
}
|
||||
const r = await accounts.setProfile(s.email, avatar, bio, socials);
|
||||
return json(res, r.error ? 400 : 200, r);
|
||||
}
|
||||
// -- line banner: the member's viral slot on welcome tours + their wall
|
||||
@@ -737,8 +748,9 @@ const server = http.createServer(async (req, res) => {
|
||||
.map(x => ({ name: x.username ? '@' + x.username : x.memberId ? 'member #' + x.memberId : 'a member',
|
||||
bannerUrl: x.lineBannerUrl || null, targetUrl: x.lineTargetUrl || null }));
|
||||
const joinPath = '/join/' + (a.username || a.code);
|
||||
let socials = null; try { socials = a.socials ? JSON.parse(a.socials) : null; } catch (e) {}
|
||||
return json(res, 200, { name: a.username ? '@' + a.username : 'member #' + (a.memberId || 0),
|
||||
avatarUrl: a.avatarUrl || null, bio: a.bio || null,
|
||||
avatarUrl: a.avatarUrl || null, bio: a.bio || null, socials,
|
||||
joinUrl: joinPath, qrUrl: '/api/qr?d=' + encodeURIComponent('https://instantadpay.com' + joinPath), ladder });
|
||||
}
|
||||
// -- watch-to-earn video ads: serve one, then reward a server-clock-verified watch
|
||||
@@ -996,7 +1008,39 @@ const server = http.createServer(async (req, res) => {
|
||||
m = /^\/uploads\/([a-z0-9]{24}\.(?:png|jpg|webp|gif|mp4|webm))$/.exec(p);
|
||||
if (m) return sendFile(res, path.join(UPLOADS_DIR, m[1]));
|
||||
if (/^\/tx\/0x[0-9a-fA-F]{64}$/.test(p)) return sendFile(res, path.join(PUBLIC_DIR, 'tx.html'));
|
||||
if (/^\/wall\/[A-Za-z0-9_]{1,20}$/.test(p)) return sendFile(res, path.join(PUBLIC_DIR, 'wall.html'));
|
||||
m = /^\/wall\/([A-Za-z0-9_]{1,20})$/.exec(p);
|
||||
if (m) { // server-inject per-member OG tags so shared bio links preview correctly (crawlers don't run JS)
|
||||
try {
|
||||
const tok = m[1].toLowerCase();
|
||||
let a = await accounts.byUsername(tok); if (!a) a = await accounts.byCode(tok);
|
||||
let html = fs.readFileSync(path.join(PUBLIC_DIR, 'wall.html'), 'utf8');
|
||||
if (a) {
|
||||
const nm = a.username ? '@' + a.username : 'member #' + (a.memberId || 0);
|
||||
const esc = t => String(t || '').replace(/[&<>"]/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c]));
|
||||
const title = nm + ' on InstantAdPay';
|
||||
const desc = a.bio ? esc(a.bio).slice(0, 200) : 'Join ' + nm + '’s line on InstantAdPay — instant on-chain ad payouts, free to join.';
|
||||
const img = a.avatarUrl && /^https:/.test(a.avatarUrl) ? a.avatarUrl : 'https://instantadpay.com/banners/iap-hero-1200x630.png';
|
||||
const url = 'https://instantadpay.com/wall/' + (a.username || a.code);
|
||||
const og = [
|
||||
'<meta property="og:type" content="profile">',
|
||||
'<meta property="og:site_name" content="InstantAdPay">',
|
||||
'<meta property="og:url" content="' + url + '">',
|
||||
'<meta property="og:title" content="' + esc(title) + '">',
|
||||
'<meta property="og:description" content="' + desc + '">',
|
||||
'<meta property="og:image" content="' + esc(img) + '">',
|
||||
'<meta name="twitter:card" content="summary_large_image">',
|
||||
'<meta name="twitter:title" content="' + esc(title) + '">',
|
||||
'<meta name="twitter:description" content="' + desc + '">',
|
||||
'<meta name="twitter:image" content="' + esc(img) + '">',
|
||||
'<meta name="description" content="' + desc + '">',
|
||||
'<link rel="canonical" href="' + url + '">'
|
||||
].join('\n');
|
||||
html = html.replace('<title>Banner wall | InstantAdPay</title>', '<title>' + esc(title) + '</title>').replace('<!--OG-->', og);
|
||||
}
|
||||
res.writeHead(200, baseHeaders({ 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-cache' }));
|
||||
return res.end(html);
|
||||
} catch (e) { return sendFile(res, path.join(PUBLIC_DIR, 'wall.html')); }
|
||||
}
|
||||
const safe = path.normalize(p).replace(/^([.\\/])+/, '');
|
||||
const file = path.join(PUBLIC_DIR, safe);
|
||||
if (file.startsWith(PUBLIC_DIR) && fs.existsSync(file) && fs.statSync(file).isFile()) return sendFile(res, file);
|
||||
|
||||
Reference in New Issue
Block a user