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:
martbost
2026-09-06 12:21:53 -05:00
parent b001df61b1
commit 3c0e94f20f
16 changed files with 178 additions and 52 deletions
+7 -5
View File
@@ -35,7 +35,7 @@ function newCode(taken) {
const pub = a => a ? { email: a.email, sponsorRef: a.sponsorRef || '', code: a.code || null,
username: a.username || null, memberId: a.memberId || 0,
lineBannerUrl: a.lineBannerUrl || null, lineTargetUrl: a.lineTargetUrl || null,
avatarUrl: a.avatarUrl || null, bio: a.bio || null,
avatarUrl: a.avatarUrl || null, bio: a.bio || null, socials: a.socials || null,
address: a.address || null, created: a.created } : null;
const USER_RE = /^[a-zA-Z0-9_]{3,20}$/;
const normUser = u => String(u || '').trim().toLowerCase();
@@ -108,11 +108,12 @@ const J = {
this.save();
return { ok: true, account: pub(acct) };
},
async setProfile(e, avatarUrl, bio) {
async setProfile(e, avatarUrl, bio, socials) {
const acct = this.db.byEmail[e];
if (!acct) return { error: 'No such account.' };
if (avatarUrl !== undefined) acct.avatarUrl = avatarUrl || null;
if (bio !== undefined) acct.bio = bio || null;
if (socials !== undefined) acct.socials = socials || null;
this.save();
return { ok: true, account: pub(acct) };
},
@@ -155,7 +156,7 @@ const J = {
const rowPub = r => r ? pub({ email: r.email, sponsorRef: r.sponsor_ref, code: r.code,
username: r.username, memberId: r.member_id || 0,
lineBannerUrl: r.line_banner_url, lineTargetUrl: r.line_target_url,
avatarUrl: r.avatar_url, bio: r.bio,
avatarUrl: r.avatar_url, bio: r.bio, socials: r.socials,
address: r.address, created: Number(r.created) }) : null;
const D = {
async signup(e, password, ref) {
@@ -205,9 +206,10 @@ const D = {
await db.q('UPDATE accounts SET line_banner_url=?, line_target_url=? WHERE email=?', [bannerUrl || null, targetUrl || null, e]);
return { ok: true, account: await this.byEmail(e) };
},
async setProfile(e, avatarUrl, bio) {
async setProfile(e, avatarUrl, bio, socials) {
if (avatarUrl !== undefined) await db.q('UPDATE accounts SET avatar_url=? WHERE email=?', [avatarUrl || null, e]);
if (bio !== undefined) await db.q('UPDATE accounts SET bio=? WHERE email=?', [bio || null, e]);
if (socials !== undefined) await db.q('UPDATE accounts SET socials=? WHERE email=?', [socials || null, e]);
return { ok: true, account: await this.byEmail(e) };
},
async byMemberId(id) {
@@ -310,5 +312,5 @@ async function count() { return impl().count(); }
module.exports = { init, signup, login, ensure, byEmail, byAddress, byCode, byUsername,
setUsername, setMemberId, namesForMembers, listByReferrer, downline, linkWallet, count,
setLineBanner: (e, b, t) => impl().setLineBanner(String(e || '').toLowerCase(), b, t),
setProfile: (e, a, bio) => impl().setProfile(String(e || '').toLowerCase(), a, bio),
setProfile: (e, a, bio, socials) => impl().setProfile(String(e || '').toLowerCase(), a, bio, socials),
byMemberId: id => impl().byMemberId(id) };