openvibers/openvibe.network Commits
Profiles live on openvibe.network/@name; openvibe.my keeps managing them (#682)
Changes
@@@ -223,7 +223,7 @@ function renderProfile(user) {let places = {}; try { places = typeof user.public_page_places === 'string' ? JSON.parse(user.public_page_places) : (user.public_page_places || {}); } catch {}for (const key of ['live', 'media', 'space', 'community']) document.getElementById(`public-place-${key}`).checked = places[key] !== false;const pageUrl = document.getElementById('public-page-url');-- if (user.username && !user.is_anon) { pageUrl.href = `https://openvibe.my/@${encodeURIComponent(user.username)}`; pageUrl.textContent = `openvibe.my/@${user.username}`; }++ if (user.username && !user.is_anon) { pageUrl.href = `https://openvibe.network/@${encodeURIComponent(user.username)}`; pageUrl.textContent = `openvibe.network/@${user.username}`; }for (const id of ['public-page-pronouns', 'public-page-bio', 'public-page-links', 'public-place-live', 'public-place-media', 'public-place-space', 'public-place-community', 'public-page-preview-toggle', 'public-page-save']) document.getElementById(id).disabled = unavailable;if (unavailable) document.getElementById('public-page-msg').textContent = 'Sign in to edit your public page.';}@@@ -213,7 +213,7 @@</div><div class="card" id="public-page-card"><h3><span class="icon"><i class="fa-solid fa-address-card"></i></span> Your public page</h3>-- <p class="muted">A link-in-bio page at <a id="public-page-url" href="https://openvibe.my/" target="_blank" rel="noopener">openvibe.my/@you</a>. Only information already public while signed out can appear here.</p>++ <p class="muted">A link-in-bio page at <a id="public-page-url" href="https://openvibe.network/" target="_blank" rel="noopener">openvibe.network/@you</a>. Only information already public while signed out can appear here.</p><label for="public-page-pronouns">Pronouns <small>(40 characters)</small></label><input id="public-page-pronouns" maxlength="40" autocomplete="off"><label for="public-page-bio">Bio <small>(280 characters)</small></label><textarea id="public-page-bio" maxlength="280" rows="3"></textarea><label for="public-page-links">Links <small>(up to 8; one per line: label | https://…)</small></label><textarea id="public-page-links" rows="5" placeholder="My website | https://example.com"></textarea>@@@ -496,8 +496,7 @@ router.put('/profile', requireAuth, async (req, res) => {if (!Array.isArray(links)) links = [];const indexable = Number(user.profile_public) && (!!String(user.public_page_bio || '').trim() || links.length > 0);if (changed && indexable) {-- const origin = String(getConfig(req).my?.baseUrl || 'https://openvibe.my').replace(/\/+$/, '');-- req.app.locals.publicPageIndexNow?.pingSoon(`${origin}/@${encodeURIComponent(user.username)}`);++ req.app.locals.publicPageIndexNow?.pingSoon(`https://openvibe.network/@${encodeURIComponent(user.username)}`);}}res.json({ user: sanitizeUser(user) });@@@ -498,7 +498,7 @@ function llmsTxt() {// The JSON an agent came for is a main section, not something to skip (AGT-5 review).{ title: 'Machine-readable', links: [{ title: 'Open stats (JSON): every site\'s numbers', url: 'https://openvibe.network/api/v1/stats' },-- { title: 'Public person page (JSON)', url: 'https://openvibe.network/api/v1/people/@handle/page', note: 'the signed-out profile at https://openvibe.my/@handle, including its public OpenVibe places' },++ { title: 'Public person page (JSON)', url: 'https://openvibe.network/api/v1/people/@handle/page', note: 'the signed-out profile at https://openvibe.network/@handle, including its public OpenVibe places' },{ title: 'OpenVibe Connect MCP', url: 'https://connect.openvibe.network/mcp', note: 'includes network.person_page.read for a public person page' },{ title: 'Tool catalog (JSON)', url: 'https://openvibe.tools/api/catalog.json' },{ title: 'Tool domains (JSON)', url: 'https://openvibe.network/api/domains' },@@@ -25,7 +25,6 @@ const { siteForHost, fullName } = require('../frame/sites');const esc = seo.esc;const SITE = 'https://openvibe.network';--const MY_SITE = 'https://openvibe.my';const SPACE_SITE = 'https://openvibe.space';const COMMUNITY_SITE = 'https://openvibe.community';const INVENTORY_SITE = 'https://openvibe.my/inventory';@@@ -296,7 +295,7 @@ function createPublicProfiles({ db, selfToken = () => null, inventoryUrl = 'httptry { links = typeof u.public_page_links === 'string' ? JSON.parse(u.public_page_links) : (u.public_page_links || []); } catch {}try { choices = { ...choices, ...(typeof u.public_page_places === 'string' ? JSON.parse(u.public_page_places) : (u.public_page_places || {})) }; } catch {}const places = Number(u.profile_public) ? await placesOf(u.username, choices) : { spaces: [], community: [], videos: [] };-- return { username: u.username, display_name: u.display_name || u.username, pronouns: Number(u.profile_public) ? String(u.public_page_pronouns || '').slice(0, 40) : '', avatar_url: avatarUrl(u.username), profile_url: `${MY_SITE}/@${encodeURIComponent(u.username)}`, bio: Number(u.profile_public) ? String(u.public_page_bio || '').slice(0, 280) : '', links: Number(u.profile_public) && Array.isArray(links) ? links.slice(0, 8) : [], places };++ return { username: u.username, display_name: u.display_name || u.username, pronouns: Number(u.profile_public) ? String(u.public_page_pronouns || '').slice(0, 40) : '', avatar_url: avatarUrl(u.username), profile_url: `${SITE}/@${encodeURIComponent(u.username)}`, bio: Number(u.profile_public) ? String(u.public_page_bio || '').slice(0, 280) : '', links: Number(u.profile_public) && Array.isArray(links) ? links.slice(0, 8) : [], places };}/** profile@1: what every site may show about a person. */@@@ -328,6 +327,9 @@ function createPublicProfiles({ db, selfToken = () => null, inventoryUrl = 'http/** GET /@:username (server/index.js mounts it with its own rate limit). */async function pageHandler(req, res, next) {try {++ if (String(req.hostname || '').toLowerCase() === 'openvibe.my') {++ return res.redirect(301, `${SITE}/@${encodeURIComponent(String(req.params.username || ''))}`);++ }const found = await find(req.params.username);if (!found) return next(); // the shared 404 pageif (found.moved) return res.redirect(301, `/@${encodeURIComponent(found.moved)}`);@@@ -336,8 +338,9 @@ function createPublicProfiles({ db, selfToken = () => null, inventoryUrl = 'httpconst [inv, page] = await Promise.all([Number(u.profile_public) ? wearsOf(u.subject_id) : null, pageOf(u)]);res.set('Content-Type', 'text/html; charset=utf-8');res.set('Cache-Control', cache.htmlHeaders({ maxAge: 60 }));-- const origin = String(req.hostname || '').toLowerCase() === 'openvibe.my' ? 'https://openvibe.my' : SITE;-- res.send(origin === MY_SITE ? renderPersonPage(u, page, origin) : renderPage(u, inv, origin, page.places));++ // A person's public profile lives on openvibe.network (the owner, 11-08: "OpenVibe.My is for users' stuff,++ // the profiles should be on .Network"). openvibe.my is the person's own hub: /@name there moves here.++ res.send(renderPage(u, inv, SITE, page.places, page.pronouns));} catch (err) { next(err); }}@@@ -450,32 +453,7 @@ ${sorted.map((g) => {<p class="pf-note"><a href="${esc(all)}">${inv.more ? 'Every item' : 'This inventory'} on OpenVibe.Inventory</a> · <a href="${INVENTORY_SITE}/items">Every item there is</a></p></section>`;}--function renderPersonPage(u, page, origin = MY_SITE) {-- const name = page.display_name || page.username;-- const url = `${origin}/@${encodeURIComponent(page.username)}`;-- const indexable = !!(page.bio || page.links.length);-- const ld = seo.jsonLdTag({-- '@context': 'https://schema.org', '@type': 'ProfilePage', url, name: `${name} (@${page.username}) · OpenVibe`,-- mainEntity: { '@type': 'Person', name, alternateName: `@${page.username}`, image: page.avatar_url, url, ...(page.bio ? { description: page.bio } : {}) },-- });-- const links = page.links.map((l) => {-- try { const parsed = new URL(l.url); return parsed.protocol === 'https:' && !parsed.username && !parsed.password ? `<li><a href="${esc(parsed.href)}" rel="me noopener noreferrer">${esc(l.label)}</a></li>` : ''; } catch { return ''; }-- }).join('');-- const places = publicPlacesHtml(page.places);-- return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">--<title>${esc(name)} (@${esc(page.username)}) · OpenVibe</title><meta name="description" content="${esc(page.bio || `${name} on OpenVibe.`)}">--<link rel="canonical" href="${esc(url)}">${indexable ? '' : '<meta name="robots" content="noindex">'}--<meta property="og:type" content="profile"><meta property="og:title" content="${esc(`${name} (@${page.username})`)}"><meta property="og:url" content="${esc(url)}"><meta property="og:image" content="${esc(page.avatar_url)}"><meta property="profile:username" content="${esc(page.username)}">--${ld}<style>--:root{color-scheme:light dark}*{box-sizing:border-box}body{margin:0;background:var(--bg-primary,#f5f6fa);color:var(--text-primary,#171923);font:16px/1.55 system-ui,sans-serif}.person-page{width:min(100% - 32px,600px);margin:clamp(24px,7vw,80px) auto}.person-card{padding:clamp(22px,5vw,40px);border:1px solid var(--border,#d8dbe4);border-radius:24px;background:var(--bg-card,var(--bg-secondary,#fff));box-shadow:0 18px 54px #0002}.person-head{display:flex;flex-direction:column;align-items:center;gap:14px;text-align:center}.person-avatar{width:104px;height:104px;border-radius:50%;object-fit:cover;background:var(--bg-secondary,#eee)}h1{font-size:clamp(1.7rem,5vw,2.5rem);line-height:1.1;margin:0;overflow-wrap:anywhere}.person-handle{margin:6px 0 0;color:var(--text-secondary,#5c6170)}.person-pronouns{display:inline-block;margin:8px 0 0;padding:2px 10px;border:1px solid var(--border,#d8dbe4);border-radius:999px;font-size:.85rem;color:var(--text-secondary,#5c6170)}.person-bio{margin:18px auto 0;max-width:46ch;text-align:center;white-space:pre-wrap;overflow-wrap:anywhere}.person-links{display:grid;gap:10px;list-style:none;margin:24px 0 0;padding:0}.person-links a{display:flex;min-height:52px;align-items:center;justify-content:center;padding:10px 18px;border:1px solid var(--border,#d8dbe4);border-radius:14px;background:var(--bg-secondary,#f6f7fa);color:inherit;font-weight:650;text-align:center;text-decoration:none;overflow-wrap:anywhere;transition:border-color .15s,transform .15s}.person-links a:hover{border-color:var(--accent,#6d5dfc)}.person-links a:active{transform:scale(.99)}.person-places{margin-top:30px}.person-places h2{margin:0 0 10px;font-size:.78rem;font-weight:800;letter-spacing:.06em;text-transform:uppercase;color:var(--text-secondary,#5c6170)}.pf-places{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(230px,100%),1fr));gap:12px;list-style:none;padding:0;margin:0}.pf-places li{min-width:0}.pf-places a{display:flex;flex-direction:column;justify-content:center;gap:5px;min-height:72px;padding:12px 16px;border:1px solid var(--border,#d8dbe4);border-radius:14px;background:var(--bg-secondary,#f6f7fa);color:inherit;text-decoration:none;overflow-wrap:anywhere}.pf-places a span{font-size:.82rem;color:var(--text-secondary,#5c6170)}.pf-places a .pf-live{display:inline-flex;align-items:center;gap:6px;color:var(--live-red,#e5484d);font-weight:700}.pf-live::before{content:'';width:8px;height:8px;border-radius:50%;background:currentColor}@media(max-width:520px){.person-avatar{width:88px;height:88px}}@media(prefers-color-scheme:dark){body{background:var(--bg-primary,#101218);color:var(--text-primary,#f5f6fa)}.person-card{border-color:var(--border,#30333d);background:var(--bg-card,var(--bg-secondary,#171a22))}.person-avatar{background:var(--bg-secondary,#222)}.person-handle,.person-pronouns,.pf-places a span{color:var(--text-secondary,#a8adba)}.person-links a,.pf-places a{border-color:var(--border,#30333d)}.person-links a,.pf-places a{background:var(--bg-secondary,#20232d)}}--</style></head><body><div id="navbar-mount"></div><main class="person-page"><article class="person-card" itemscope itemtype="https://schema.org/Person">--<div class="person-head"><img class="person-avatar" src="${esc(page.avatar_url)}" alt="" width="104" height="104" itemprop="image"><div><h1 itemprop="name">${esc(name)}</h1><p class="person-handle">@${esc(page.username)}</p>${page.pronouns ? `<p class="person-pronouns">${esc(page.pronouns)}</p>` : ''}</div></div>--${page.bio ? `<p class="person-bio" itemprop="description">${esc(page.bio)}</p>` : ''}${links ? `<nav aria-label="${esc(name)}'s links"><ul class="person-links">${links}</ul></nav>` : ''}--${places ? `<div class="person-places">${places}</div>` : ''}</article></main>${require('openvibe-shared/footer').ssr({ service: 'network', variant: 'compact' })}--<script src="/shared/navbar.js" defer></script><script src="/js/account-safety.js" defer></script><script>document.addEventListener('DOMContentLoaded',function(){try{if(window.OpenVibeNavbar)OpenVibeNavbar.init({service:'network',apiBase:location.origin});}catch(e){}});</script></body></html>`;--}----function renderPage(u, inv, origin = SITE, places = { spaces: [], community: [], videos: [] }) {++function renderPage(u, inv, origin = SITE, places = { spaces: [], community: [], videos: [] }, pronouns = '') {const canonicalUrl = `${origin}/@${encodeURIComponent(u.username)}`;const name = u.display_name || u.username;const isPublic = !!inv;@@@ -546,7 +524,7 @@ a{color:var(--accent)}.pf-who{flex:1;min-width:200px;padding-top:56px}.pf-who h1{margin:0;font-size:clamp(1.5rem,4vw,2.1rem);line-height:1.15;overflow-wrap:anywhere}.pf-name-badge{width:.85em;height:.85em;margin-right:.25em;vertical-align:-.08em;border-radius:6px;object-fit:contain}--.pf-handle{margin:.2em 0 0;color:var(--text-secondary)}++.pf-handle{margin:.2em 0 0;color:var(--text-secondary)}.pf-pronouns{display:inline-block;margin-left:6px;padding:1px 9px;border:1px solid var(--border);border-radius:999px;font-size:.82em;vertical-align:1px}.pf-places a .pf-live{display:inline-flex;align-items:center;gap:6px;color:var(--live-red,#e5484d);font-weight:700}.pf-live::before{content:'';width:8px;height:8px;border-radius:50%;background:currentColor}.pf-bio{margin:0;padding:0 24px 18px;color:var(--text-primary);line-height:1.6;white-space:pre-line;overflow-wrap:anywhere;max-width:72ch}.pf-stats{list-style:none;display:flex;flex-wrap:wrap;gap:8px;margin:0;padding:0 24px 22px}.pf-stats li{display:flex;flex-direction:column;gap:2px;padding:8px 14px;border:1px solid var(--border);border-radius:12px;background:var(--bg-card);min-width:110px}@@@ -586,7 +564,8 @@ a{color:var(--accent)}.pf-ach li{overflow-wrap:anywhere}.pf-ach small{color:var(--text-muted);font-size:.75rem}.pf-empty{color:var(--text-secondary);padding:18px;border:1px dashed var(--border);border-radius:12px;margin:0}.pf-note{color:var(--text-secondary);font-size:.85rem;margin:12px 0 0}--.pf-links{display:flex;flex-wrap:wrap;gap:10px;list-style:none;margin:12px 0;padding:0}.pf-links a{display:inline-flex;min-height:44px;align-items:center;padding:8px 14px;border:1px solid var(--border);border-radius:999px;color:var(--text-primary);text-decoration:none}++.pf-links{display:flex;flex-wrap:wrap;gap:10px;list-style:none;margin:0 0 18px;padding:0 24px}.pf-links a{display:inline-flex;min-height:44px;align-items:center;padding:8px 14px;border:1px solid var(--border);border-radius:999px;color:var(--text-primary);text-decoration:none}++@media(max-width:600px){.pf-links{display:grid;padding:0 24px}.pf-links a{justify-content:center;min-height:50px;border-radius:14px;font-weight:650}}.pf-places{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(230px,100%),1fr));gap:12px;list-style:none;padding:0;margin:0}.pf-places li{min-width:0}.pf-places a{display:flex;flex-direction:column;justify-content:center;gap:5px;min-height:72px;padding:12px 16px;border:1px solid var(--border);border-radius:14px;background:var(--bg-secondary);color:var(--text-primary);text-decoration:none;overflow-wrap:anywhere}.pf-places a span{font-size:.82rem;color:var(--text-secondary)}@media (max-width:560px){.pf-head{padding:0 16px 16px;gap:14px}.pf-avatar{width:92px;height:92px}.pf-hat{font-size:32px;top:-18px}.pf-who{padding-top:0;min-width:100%}.pf-bio,.pf-stats,.pf-private{padding-left:16px;padding-right:16px}.pf-grid{grid-template-columns:repeat(auto-fill,minmax(112px,1fr))}.pf-badge small{display:none}.pf-badge{padding:6px 12px 6px 9px}}</style>@@@ -599,7 +578,7 @@ ${require('openvibe-shared/frame').noscriptNav({ name: 'OpenVibe.Network', links<div class="pf-banner" aria-hidden="true"></div><div class="pf-head"><div class="pf-pic"><img class="pf-avatar" src="${esc(avatarUrl(u.username))}" alt="" width="112" height="112">${hatGlyph ? `<span class="pf-hat" title="${esc(hat.name)}" aria-hidden="true">${esc(hatGlyph)}</span>` : ''}</div>--<div class="pf-who"><h1>${badgeId ? `<img class="pf-name-badge" src="https://openvibe.media/o/${badgeId}" alt="" title="${esc(badge.name)}" width="32" height="32">` : ''}${fxClass ? `<span class="ov-fx ${fxClass}" title="${esc(fx.name)}">${esc(name)}</span>` : esc(name)}</h1><p class="pf-handle">@${esc(u.username)}</p></div>++<div class="pf-who"><h1>${badgeId ? `<img class="pf-name-badge" src="https://openvibe.media/o/${badgeId}" alt="" title="${esc(badge.name)}" width="32" height="32">` : ''}${fxClass ? `<span class="ov-fx ${fxClass}" title="${esc(fx.name)}">${esc(name)}</span>` : esc(name)}</h1><p class="pf-handle">@${esc(u.username)}${isPublic && pronouns ? ` <span class="pf-pronouns">${esc(pronouns)}</span>` : ''}</p></div></div>${bio ? `<p class="pf-bio">${esc(bio)}</p>` : ''}${isPublic && Array.isArray(publicLinks) && publicLinks.length ? `<nav aria-label="${esc(name)}'s links"><ul class="pf-links">${publicLinks.slice(0, 8).map((link) => { let href = ''; try { const parsed = new URL(link.url); if (parsed.protocol === 'https:' && !parsed.username && !parsed.password) href = parsed.href; } catch {} return href ? `<li><a rel="me noopener noreferrer" href="${esc(href)}">${esc(String(link.label || '').slice(0, 40))}</a></li>` : ''; }).join('')}</ul></nav>` : ''}@@@ -616,4 +595,4 @@ ${require('openvibe-shared/footer').ssr({ service: 'network', variant: 'compact'</html>`;}--module.exports = { createPublicProfiles, renderPage, renderPersonPage, monthOf, SUBJECT_RE, PUBLIC_PLACE_TIMEOUT_MS, PUBLIC_PLACE_TTL_MS };++module.exports = { createPublicProfiles, renderPage, monthOf, SUBJECT_RE, PUBLIC_PLACE_TIMEOUT_MS, PUBLIC_PLACE_TTL_MS };@@@ -32,7 +32,7 @@ function sitemapXml({ release = null, profiles = [] } = {}) {const day = String(lastmod).slice(0, 10);return seo.sitemapXml([...PAGES.map((page) => ({ loc: `${HOST}${page.path}`, lastmod: day, changefreq: page.changefreq, priority: page.priority })),-- ...profiles.map((username) => ({ loc: `https://openvibe.my/@${encodeURIComponent(username)}`, lastmod: day, changefreq: 'weekly', priority: 0.5 })),++ ...profiles.map((username) => ({ loc: `https://openvibe.network/@${encodeURIComponent(username)}`, lastmod: day, changefreq: 'weekly', priority: 0.5 })),]);}@@@ -11,7 +11,7 @@ const fs = require('fs');const path = require('path');const express = require('express');const { getDb } = require('../server/db/database');--const { createPublicProfiles, renderPersonPage, monthOf, PUBLIC_PLACE_TIMEOUT_MS, PUBLIC_PLACE_TTL_MS } = require('../server/profile/public');++const { createPublicProfiles, monthOf, PUBLIC_PLACE_TIMEOUT_MS, PUBLIC_PLACE_TTL_MS } = require('../server/profile/public');const publicCors = require('../server/public-cors');const A = 'usr_01JZ0000000000000000000AAA';@@@ -106,7 +106,7 @@ const INSTANCES = [assert.ok(r.text.includes('Member since') && r.text.includes('October 2026'));assert.ok(/<span class="pf-stat-k">Rarest<\/span><span class="pf-stat-v">Legendary<\/span>/.test(r.text));assert.ok(r.text.includes('<span class="pf-count">4</span>'), 'four items');-- assert.ok(r.text.includes('"@type":"ProfilePage"') && r.text.includes('<link rel="canonical" href="https://openvibe.network/@Ana">'));++ assert.ok(r.text.includes('"@type":"ProfilePage"') && r.text.includes('<link rel="canonical" href="https://openvibe.network/@Ana">'), 'profiles live on openvibe.network');assert.ok(r.text.includes('--pf-color:#ff0066'));assert.ok(r.text.includes('https://openvibe.my/inventory/itd_01JZ00000000000000000001R1'), 'items link to their Inventory page');assert.ok(!/name="robots" content="noindex"/.test(r.text));@@@ -158,7 +158,7 @@ const INSTANCES = [assert.strictEqual(r.status, 200);assert.deepStrictEqual(JSON.parse(r.text).person.links, [{ label: 'My site', url: 'https://example.com/' }]);assert.strictEqual(JSON.parse(r.text).person.bio, 'Hello there');-- assert.strictEqual(JSON.parse(r.text).person.profile_url, 'https://openvibe.my/@Ana');++ assert.strictEqual(JSON.parse(r.text).person.profile_url, 'https://openvibe.network/@Ana');assert.strictEqual(JSON.parse(r.text).person.places.live.is_live, true);assert.deepStrictEqual(JSON.parse(r.text).person.places.videos.map((v) => v.title), ['First video']);assert.deepStrictEqual(JSON.parse(r.text).person.places.videos.map((v) => v.url), ['https://openvibe.video/watch?v=tdLVs5wad4A'], 'a video links to its watch page');@@@ -167,9 +167,14 @@ const INSTANCES = [const person = JSON.parse(r.text).person;r = await get('/api/v1/people/@ana/page');assert.deepStrictEqual([r.status, r.headers.get('location')], [301, '/api/v1/people/@Ana/page']);-- const newPage = renderPersonPage({ username: 'Ana' }, person, 'https://openvibe.my');-- assert.ok(newPage.includes('<link rel="canonical" href="https://openvibe.my/@Ana">') && newPage.includes('First video') && newPage.includes('My site'));-- assert.ok(!newPage.includes('pf-grid') && !newPage.includes('inventory'), 'the new page shows public places without private account inventory');++ // openvibe.my is the person's own hub; a profile address there moves to openvibe.network (the owner, 11-08).++ // fetch cannot set Host, so this one request goes out raw.++ const onMy = await new Promise((resolve, reject) => {++ const u = new URL(b);++ require('http').get({ host: u.hostname, port: u.port, path: '/@Ana', headers: { Host: 'openvibe.my' } }, (res) => { res.resume(); resolve({ status: res.statusCode, location: res.headers.location }); }).on('error', reject);++ });++ assert.deepStrictEqual([onMy.status, onMy.location], [301, 'https://openvibe.network/@Ana']);++ assert.strictEqual(person.username, 'Ana');// A hidden profile: name and picture, nothing else, not indexed.r = await get('/@hidden');
@@ -223,7 +223,7 @@ function renderProfile(user) {
@@ -223,7 +223,7 @@ function renderProfile(user) {
let places = {}; try { places = typeof user.public_page_places === 'string' ? JSON.parse(user.public_page_places) : (user.public_page_places || {}); } catch {}
let places = {}; try { places = typeof user.public_page_places === 'string' ? JSON.parse(user.public_page_places) : (user.public_page_places || {}); } catch {}
for (const key of ['live', 'media', 'space', 'community']) document.getElementById(`public-place-${key}`).checked = places[key] !== false;
for (const key of ['live', 'media', 'space', 'community']) document.getElementById(`public-place-${key}`).checked = places[key] !== false;
const pageUrl = document.getElementById('public-page-url');
const pageUrl = document.getElementById('public-page-url');
- if (user.username && !user.is_anon) { pageUrl.href = `https://openvibe.my/@${encodeURIComponent(user.username)}`; pageUrl.textContent = `openvibe.my/@${user.username}`; }
+ if (user.username && !user.is_anon) { pageUrl.href = `https://openvibe.network/@${encodeURIComponent(user.username)}`; pageUrl.textContent = `openvibe.network/@${user.username}`; }
for (const id of ['public-page-pronouns', 'public-page-bio', 'public-page-links', 'public-place-live', 'public-place-media', 'public-place-space', 'public-place-community', 'public-page-preview-toggle', 'public-page-save']) document.getElementById(id).disabled = unavailable;
for (const id of ['public-page-pronouns', 'public-page-bio', 'public-page-links', 'public-place-live', 'public-place-media', 'public-place-space', 'public-place-community', 'public-page-preview-toggle', 'public-page-save']) document.getElementById(id).disabled = unavailable;
if (unavailable) document.getElementById('public-page-msg').textContent = 'Sign in to edit your public page.';
if (unavailable) document.getElementById('public-page-msg').textContent = 'Sign in to edit your public page.';
}
}
@@ -213,7 +213,7 @@
@@ -213,7 +213,7 @@
</div>
</div>
<div class="card" id="public-page-card">
<div class="card" id="public-page-card">
<h3><span class="icon"><i class="fa-solid fa-address-card"></i></span> Your public page</h3>
<h3><span class="icon"><i class="fa-solid fa-address-card"></i></span> Your public page</h3>
- <p class="muted">A link-in-bio page at <a id="public-page-url" href="https://openvibe.my/" target="_blank" rel="noopener">openvibe.my/@you</a>. Only information already public while signed out can appear here.</p>
+ <p class="muted">A link-in-bio page at <a id="public-page-url" href="https://openvibe.network/" target="_blank" rel="noopener">openvibe.network/@you</a>. Only information already public while signed out can appear here.</p>
<label for="public-page-pronouns">Pronouns <small>(40 characters)</small></label><input id="public-page-pronouns" maxlength="40" autocomplete="off">
<label for="public-page-pronouns">Pronouns <small>(40 characters)</small></label><input id="public-page-pronouns" maxlength="40" autocomplete="off">
<label for="public-page-bio">Bio <small>(280 characters)</small></label><textarea id="public-page-bio" maxlength="280" rows="3"></textarea>
<label for="public-page-bio">Bio <small>(280 characters)</small></label><textarea id="public-page-bio" maxlength="280" rows="3"></textarea>
<label for="public-page-links">Links <small>(up to 8; one per line: label | https://…)</small></label><textarea id="public-page-links" rows="5" placeholder="My website | https://example.com"></textarea>
<label for="public-page-links">Links <small>(up to 8; one per line: label | https://…)</small></label><textarea id="public-page-links" rows="5" placeholder="My website | https://example.com"></textarea>
@@ -496,8 +496,7 @@ router.put('/profile', requireAuth, async (req, res) => {
@@ -496,8 +496,7 @@ router.put('/profile', requireAuth, async (req, res) => {
if (!Array.isArray(links)) links = [];
if (!Array.isArray(links)) links = [];
const indexable = Number(user.profile_public) && (!!String(user.public_page_bio || '').trim() || links.length > 0);
const indexable = Number(user.profile_public) && (!!String(user.public_page_bio || '').trim() || links.length > 0);
if (changed && indexable) {
if (changed && indexable) {
- const origin = String(getConfig(req).my?.baseUrl || 'https://openvibe.my').replace(/\/+$/, '');
- req.app.locals.publicPageIndexNow?.pingSoon(`${origin}/@${encodeURIComponent(user.username)}`);
+ req.app.locals.publicPageIndexNow?.pingSoon(`https://openvibe.network/@${encodeURIComponent(user.username)}`);
}
}
}
}
res.json({ user: sanitizeUser(user) });
res.json({ user: sanitizeUser(user) });
@@ -498,7 +498,7 @@ function llmsTxt() {
@@ -498,7 +498,7 @@ function llmsTxt() {
// The JSON an agent came for is a main section, not something to skip (AGT-5 review).
// The JSON an agent came for is a main section, not something to skip (AGT-5 review).
{ title: 'Machine-readable', links: [
{ title: 'Machine-readable', links: [
{ title: 'Open stats (JSON): every site\'s numbers', url: 'https://openvibe.network/api/v1/stats' },
{ title: 'Open stats (JSON): every site\'s numbers', url: 'https://openvibe.network/api/v1/stats' },
- { title: 'Public person page (JSON)', url: 'https://openvibe.network/api/v1/people/@handle/page', note: 'the signed-out profile at https://openvibe.my/@handle, including its public OpenVibe places' },
+ { title: 'Public person page (JSON)', url: 'https://openvibe.network/api/v1/people/@handle/page', note: 'the signed-out profile at https://openvibe.network/@handle, including its public OpenVibe places' },
{ title: 'OpenVibe Connect MCP', url: 'https://connect.openvibe.network/mcp', note: 'includes network.person_page.read for a public person page' },
{ title: 'OpenVibe Connect MCP', url: 'https://connect.openvibe.network/mcp', note: 'includes network.person_page.read for a public person page' },
{ title: 'Tool catalog (JSON)', url: 'https://openvibe.tools/api/catalog.json' },
{ title: 'Tool catalog (JSON)', url: 'https://openvibe.tools/api/catalog.json' },
{ title: 'Tool domains (JSON)', url: 'https://openvibe.network/api/domains' },
{ title: 'Tool domains (JSON)', url: 'https://openvibe.network/api/domains' },
@@ -25,7 +25,6 @@ const { siteForHost, fullName } = require('../frame/sites');
@@ -25,7 +25,6 @@ const { siteForHost, fullName } = require('../frame/sites');
const esc = seo.esc;
const esc = seo.esc;
const SITE = 'https://openvibe.network';
const SITE = 'https://openvibe.network';
-const MY_SITE = 'https://openvibe.my';
const SPACE_SITE = 'https://openvibe.space';
const SPACE_SITE = 'https://openvibe.space';
const COMMUNITY_SITE = 'https://openvibe.community';
const COMMUNITY_SITE = 'https://openvibe.community';
const INVENTORY_SITE = 'https://openvibe.my/inventory';
const INVENTORY_SITE = 'https://openvibe.my/inventory';
@@ -296,7 +295,7 @@ function createPublicProfiles({ db, selfToken = () => null, inventoryUrl = 'http
@@ -296,7 +295,7 @@ function createPublicProfiles({ db, selfToken = () => null, inventoryUrl = 'http
try { links = typeof u.public_page_links === 'string' ? JSON.parse(u.public_page_links) : (u.public_page_links || []); } catch {}
try { links = typeof u.public_page_links === 'string' ? JSON.parse(u.public_page_links) : (u.public_page_links || []); } catch {}
try { choices = { ...choices, ...(typeof u.public_page_places === 'string' ? JSON.parse(u.public_page_places) : (u.public_page_places || {})) }; } catch {}
try { choices = { ...choices, ...(typeof u.public_page_places === 'string' ? JSON.parse(u.public_page_places) : (u.public_page_places || {})) }; } catch {}
const places = Number(u.profile_public) ? await placesOf(u.username, choices) : { spaces: [], community: [], videos: [] };
const places = Number(u.profile_public) ? await placesOf(u.username, choices) : { spaces: [], community: [], videos: [] };
- return { username: u.username, display_name: u.display_name || u.username, pronouns: Number(u.profile_public) ? String(u.public_page_pronouns || '').slice(0, 40) : '', avatar_url: avatarUrl(u.username), profile_url: `${MY_SITE}/@${encodeURIComponent(u.username)}`, bio: Number(u.profile_public) ? String(u.public_page_bio || '').slice(0, 280) : '', links: Number(u.profile_public) && Array.isArray(links) ? links.slice(0, 8) : [], places };
+ return { username: u.username, display_name: u.display_name || u.username, pronouns: Number(u.profile_public) ? String(u.public_page_pronouns || '').slice(0, 40) : '', avatar_url: avatarUrl(u.username), profile_url: `${SITE}/@${encodeURIComponent(u.username)}`, bio: Number(u.profile_public) ? String(u.public_page_bio || '').slice(0, 280) : '', links: Number(u.profile_public) && Array.isArray(links) ? links.slice(0, 8) : [], places };
}
}
/** profile@1: what every site may show about a person. */
/** profile@1: what every site may show about a person. */
@@ -328,6 +327,9 @@ function createPublicProfiles({ db, selfToken = () => null, inventoryUrl = 'http
@@ -328,6 +327,9 @@ function createPublicProfiles({ db, selfToken = () => null, inventoryUrl = 'http
/** GET /@:username (server/index.js mounts it with its own rate limit). */
/** GET /@:username (server/index.js mounts it with its own rate limit). */
async function pageHandler(req, res, next) {
async function pageHandler(req, res, next) {
try {
try {
+ if (String(req.hostname || '').toLowerCase() === 'openvibe.my') {
+ return res.redirect(301, `${SITE}/@${encodeURIComponent(String(req.params.username || ''))}`);
+ }
const found = await find(req.params.username);
const found = await find(req.params.username);
if (!found) return next(); // the shared 404 page
if (!found) return next(); // the shared 404 page
if (found.moved) return res.redirect(301, `/@${encodeURIComponent(found.moved)}`);
if (found.moved) return res.redirect(301, `/@${encodeURIComponent(found.moved)}`);
@@ -336,8 +338,9 @@ function createPublicProfiles({ db, selfToken = () => null, inventoryUrl = 'http
@@ -336,8 +338,9 @@ function createPublicProfiles({ db, selfToken = () => null, inventoryUrl = 'http
const [inv, page] = await Promise.all([Number(u.profile_public) ? wearsOf(u.subject_id) : null, pageOf(u)]);
const [inv, page] = await Promise.all([Number(u.profile_public) ? wearsOf(u.subject_id) : null, pageOf(u)]);
res.set('Content-Type', 'text/html; charset=utf-8');
res.set('Content-Type', 'text/html; charset=utf-8');
res.set('Cache-Control', cache.htmlHeaders({ maxAge: 60 }));
res.set('Cache-Control', cache.htmlHeaders({ maxAge: 60 }));
- const origin = String(req.hostname || '').toLowerCase() === 'openvibe.my' ? 'https://openvibe.my' : SITE;
- res.send(origin === MY_SITE ? renderPersonPage(u, page, origin) : renderPage(u, inv, origin, page.places));
+ // A person's public profile lives on openvibe.network (the owner, 11-08: "OpenVibe.My is for users' stuff,
+ // the profiles should be on .Network"). openvibe.my is the person's own hub: /@name there moves here.
+ res.send(renderPage(u, inv, SITE, page.places, page.pronouns));
} catch (err) { next(err); }
} catch (err) { next(err); }
}
}
@@ -450,32 +453,7 @@ ${sorted.map((g) => {
@@ -450,32 +453,7 @@ ${sorted.map((g) => {
<p class="pf-note"><a href="${esc(all)}">${inv.more ? 'Every item' : 'This inventory'} on OpenVibe.Inventory</a> · <a href="${INVENTORY_SITE}/items">Every item there is</a></p></section>`;
<p class="pf-note"><a href="${esc(all)}">${inv.more ? 'Every item' : 'This inventory'} on OpenVibe.Inventory</a> · <a href="${INVENTORY_SITE}/items">Every item there is</a></p></section>`;
}
}
-function renderPersonPage(u, page, origin = MY_SITE) {
- const name = page.display_name || page.username;
- const url = `${origin}/@${encodeURIComponent(page.username)}`;
- const indexable = !!(page.bio || page.links.length);
- const ld = seo.jsonLdTag({
- '@context': 'https://schema.org', '@type': 'ProfilePage', url, name: `${name} (@${page.username}) · OpenVibe`,
- mainEntity: { '@type': 'Person', name, alternateName: `@${page.username}`, image: page.avatar_url, url, ...(page.bio ? { description: page.bio } : {}) },
- });
- const links = page.links.map((l) => {
- try { const parsed = new URL(l.url); return parsed.protocol === 'https:' && !parsed.username && !parsed.password ? `<li><a href="${esc(parsed.href)}" rel="me noopener noreferrer">${esc(l.label)}</a></li>` : ''; } catch { return ''; }
- }).join('');
- const places = publicPlacesHtml(page.places);
- return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
-<title>${esc(name)} (@${esc(page.username)}) · OpenVibe</title><meta name="description" content="${esc(page.bio || `${name} on OpenVibe.`)}">
-<link rel="canonical" href="${esc(url)}">${indexable ? '' : '<meta name="robots" content="noindex">'}
-<meta property="og:type" content="profile"><meta property="og:title" content="${esc(`${name} (@${page.username})`)}"><meta property="og:url" content="${esc(url)}"><meta property="og:image" content="${esc(page.avatar_url)}"><meta property="profile:username" content="${esc(page.username)}">
-${ld}<style>
-:root{color-scheme:light dark}*{box-sizing:border-box}body{margin:0;background:var(--bg-primary,#f5f6fa);color:var(--text-primary,#171923);font:16px/1.55 system-ui,sans-serif}.person-page{width:min(100% - 32px,600px);margin:clamp(24px,7vw,80px) auto}.person-card{padding:clamp(22px,5vw,40px);border:1px solid var(--border,#d8dbe4);border-radius:24px;background:var(--bg-card,var(--bg-secondary,#fff));box-shadow:0 18px 54px #0002}.person-head{display:flex;flex-direction:column;align-items:center;gap:14px;text-align:center}.person-avatar{width:104px;height:104px;border-radius:50%;object-fit:cover;background:var(--bg-secondary,#eee)}h1{font-size:clamp(1.7rem,5vw,2.5rem);line-height:1.1;margin:0;overflow-wrap:anywhere}.person-handle{margin:6px 0 0;color:var(--text-secondary,#5c6170)}.person-pronouns{display:inline-block;margin:8px 0 0;padding:2px 10px;border:1px solid var(--border,#d8dbe4);border-radius:999px;font-size:.85rem;color:var(--text-secondary,#5c6170)}.person-bio{margin:18px auto 0;max-width:46ch;text-align:center;white-space:pre-wrap;overflow-wrap:anywhere}.person-links{display:grid;gap:10px;list-style:none;margin:24px 0 0;padding:0}.person-links a{display:flex;min-height:52px;align-items:center;justify-content:center;padding:10px 18px;border:1px solid var(--border,#d8dbe4);border-radius:14px;background:var(--bg-secondary,#f6f7fa);color:inherit;font-weight:650;text-align:center;text-decoration:none;overflow-wrap:anywhere;transition:border-color .15s,transform .15s}.person-links a:hover{border-color:var(--accent,#6d5dfc)}.person-links a:active{transform:scale(.99)}.person-places{margin-top:30px}.person-places h2{margin:0 0 10px;font-size:.78rem;font-weight:800;letter-spacing:.06em;text-transform:uppercase;color:var(--text-secondary,#5c6170)}.pf-places{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(230px,100%),1fr));gap:12px;list-style:none;padding:0;margin:0}.pf-places li{min-width:0}.pf-places a{display:flex;flex-direction:column;justify-content:center;gap:5px;min-height:72px;padding:12px 16px;border:1px solid var(--border,#d8dbe4);border-radius:14px;background:var(--bg-secondary,#f6f7fa);color:inherit;text-decoration:none;overflow-wrap:anywhere}.pf-places a span{font-size:.82rem;color:var(--text-secondary,#5c6170)}.pf-places a .pf-live{display:inline-flex;align-items:center;gap:6px;color:var(--live-red,#e5484d);font-weight:700}.pf-live::before{content:'';width:8px;height:8px;border-radius:50%;background:currentColor}@media(max-width:520px){.person-avatar{width:88px;height:88px}}@media(prefers-color-scheme:dark){body{background:var(--bg-primary,#101218);color:var(--text-primary,#f5f6fa)}.person-card{border-color:var(--border,#30333d);background:var(--bg-card,var(--bg-secondary,#171a22))}.person-avatar{background:var(--bg-secondary,#222)}.person-handle,.person-pronouns,.pf-places a span{color:var(--text-secondary,#a8adba)}.person-links a,.pf-places a{border-color:var(--border,#30333d)}.person-links a,.pf-places a{background:var(--bg-secondary,#20232d)}}
-</style></head><body><div id="navbar-mount"></div><main class="person-page"><article class="person-card" itemscope itemtype="https://schema.org/Person">
-<div class="person-head"><img class="person-avatar" src="${esc(page.avatar_url)}" alt="" width="104" height="104" itemprop="image"><div><h1 itemprop="name">${esc(name)}</h1><p class="person-handle">@${esc(page.username)}</p>${page.pronouns ? `<p class="person-pronouns">${esc(page.pronouns)}</p>` : ''}</div></div>
-${page.bio ? `<p class="person-bio" itemprop="description">${esc(page.bio)}</p>` : ''}${links ? `<nav aria-label="${esc(name)}'s links"><ul class="person-links">${links}</ul></nav>` : ''}
-${places ? `<div class="person-places">${places}</div>` : ''}</article></main>${require('openvibe-shared/footer').ssr({ service: 'network', variant: 'compact' })}
-<script src="/shared/navbar.js" defer></script><script src="/js/account-safety.js" defer></script><script>document.addEventListener('DOMContentLoaded',function(){try{if(window.OpenVibeNavbar)OpenVibeNavbar.init({service:'network',apiBase:location.origin});}catch(e){}});</script></body></html>`;
-}
-
-function renderPage(u, inv, origin = SITE, places = { spaces: [], community: [], videos: [] }) {
+function renderPage(u, inv, origin = SITE, places = { spaces: [], community: [], videos: [] }, pronouns = '') {
const canonicalUrl = `${origin}/@${encodeURIComponent(u.username)}`;
const canonicalUrl = `${origin}/@${encodeURIComponent(u.username)}`;
const name = u.display_name || u.username;
const name = u.display_name || u.username;
const isPublic = !!inv;
const isPublic = !!inv;
@@ -546,7 +524,7 @@ a{color:var(--accent)}
@@ -546,7 +524,7 @@ a{color:var(--accent)}
.pf-who{flex:1;min-width:200px;padding-top:56px}
.pf-who{flex:1;min-width:200px;padding-top:56px}
.pf-who h1{margin:0;font-size:clamp(1.5rem,4vw,2.1rem);line-height:1.15;overflow-wrap:anywhere}
.pf-who h1{margin:0;font-size:clamp(1.5rem,4vw,2.1rem);line-height:1.15;overflow-wrap:anywhere}
.pf-name-badge{width:.85em;height:.85em;margin-right:.25em;vertical-align:-.08em;border-radius:6px;object-fit:contain}
.pf-name-badge{width:.85em;height:.85em;margin-right:.25em;vertical-align:-.08em;border-radius:6px;object-fit:contain}
-.pf-handle{margin:.2em 0 0;color:var(--text-secondary)}
+.pf-handle{margin:.2em 0 0;color:var(--text-secondary)}.pf-pronouns{display:inline-block;margin-left:6px;padding:1px 9px;border:1px solid var(--border);border-radius:999px;font-size:.82em;vertical-align:1px}.pf-places a .pf-live{display:inline-flex;align-items:center;gap:6px;color:var(--live-red,#e5484d);font-weight:700}.pf-live::before{content:'';width:8px;height:8px;border-radius:50%;background:currentColor}
.pf-bio{margin:0;padding:0 24px 18px;color:var(--text-primary);line-height:1.6;white-space:pre-line;overflow-wrap:anywhere;max-width:72ch}
.pf-bio{margin:0;padding:0 24px 18px;color:var(--text-primary);line-height:1.6;white-space:pre-line;overflow-wrap:anywhere;max-width:72ch}
.pf-stats{list-style:none;display:flex;flex-wrap:wrap;gap:8px;margin:0;padding:0 24px 22px}
.pf-stats{list-style:none;display:flex;flex-wrap:wrap;gap:8px;margin:0;padding:0 24px 22px}
.pf-stats li{display:flex;flex-direction:column;gap:2px;padding:8px 14px;border:1px solid var(--border);border-radius:12px;background:var(--bg-card);min-width:110px}
.pf-stats li{display:flex;flex-direction:column;gap:2px;padding:8px 14px;border:1px solid var(--border);border-radius:12px;background:var(--bg-card);min-width:110px}
@@ -586,7 +564,8 @@ a{color:var(--accent)}
@@ -586,7 +564,8 @@ a{color:var(--accent)}
.pf-ach li{overflow-wrap:anywhere}.pf-ach small{color:var(--text-muted);font-size:.75rem}
.pf-ach li{overflow-wrap:anywhere}.pf-ach small{color:var(--text-muted);font-size:.75rem}
.pf-empty{color:var(--text-secondary);padding:18px;border:1px dashed var(--border);border-radius:12px;margin:0}
.pf-empty{color:var(--text-secondary);padding:18px;border:1px dashed var(--border);border-radius:12px;margin:0}
.pf-note{color:var(--text-secondary);font-size:.85rem;margin:12px 0 0}
.pf-note{color:var(--text-secondary);font-size:.85rem;margin:12px 0 0}
-.pf-links{display:flex;flex-wrap:wrap;gap:10px;list-style:none;margin:12px 0;padding:0}.pf-links a{display:inline-flex;min-height:44px;align-items:center;padding:8px 14px;border:1px solid var(--border);border-radius:999px;color:var(--text-primary);text-decoration:none}
+.pf-links{display:flex;flex-wrap:wrap;gap:10px;list-style:none;margin:0 0 18px;padding:0 24px}.pf-links a{display:inline-flex;min-height:44px;align-items:center;padding:8px 14px;border:1px solid var(--border);border-radius:999px;color:var(--text-primary);text-decoration:none}
+@media(max-width:600px){.pf-links{display:grid;padding:0 24px}.pf-links a{justify-content:center;min-height:50px;border-radius:14px;font-weight:650}}
.pf-places{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(230px,100%),1fr));gap:12px;list-style:none;padding:0;margin:0}.pf-places li{min-width:0}.pf-places a{display:flex;flex-direction:column;justify-content:center;gap:5px;min-height:72px;padding:12px 16px;border:1px solid var(--border);border-radius:14px;background:var(--bg-secondary);color:var(--text-primary);text-decoration:none;overflow-wrap:anywhere}.pf-places a span{font-size:.82rem;color:var(--text-secondary)}
.pf-places{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(230px,100%),1fr));gap:12px;list-style:none;padding:0;margin:0}.pf-places li{min-width:0}.pf-places a{display:flex;flex-direction:column;justify-content:center;gap:5px;min-height:72px;padding:12px 16px;border:1px solid var(--border);border-radius:14px;background:var(--bg-secondary);color:var(--text-primary);text-decoration:none;overflow-wrap:anywhere}.pf-places a span{font-size:.82rem;color:var(--text-secondary)}
@media (max-width:560px){.pf-head{padding:0 16px 16px;gap:14px}.pf-avatar{width:92px;height:92px}.pf-hat{font-size:32px;top:-18px}.pf-who{padding-top:0;min-width:100%}.pf-bio,.pf-stats,.pf-private{padding-left:16px;padding-right:16px}.pf-grid{grid-template-columns:repeat(auto-fill,minmax(112px,1fr))}.pf-badge small{display:none}.pf-badge{padding:6px 12px 6px 9px}}
@media (max-width:560px){.pf-head{padding:0 16px 16px;gap:14px}.pf-avatar{width:92px;height:92px}.pf-hat{font-size:32px;top:-18px}.pf-who{padding-top:0;min-width:100%}.pf-bio,.pf-stats,.pf-private{padding-left:16px;padding-right:16px}.pf-grid{grid-template-columns:repeat(auto-fill,minmax(112px,1fr))}.pf-badge small{display:none}.pf-badge{padding:6px 12px 6px 9px}}
</style>
</style>
@@ -599,7 +578,7 @@ ${require('openvibe-shared/frame').noscriptNav({ name: 'OpenVibe.Network', links
@@ -599,7 +578,7 @@ ${require('openvibe-shared/frame').noscriptNav({ name: 'OpenVibe.Network', links
<div class="pf-banner" aria-hidden="true"></div>
<div class="pf-banner" aria-hidden="true"></div>
<div class="pf-head">
<div class="pf-head">
<div class="pf-pic"><img class="pf-avatar" src="${esc(avatarUrl(u.username))}" alt="" width="112" height="112">${hatGlyph ? `<span class="pf-hat" title="${esc(hat.name)}" aria-hidden="true">${esc(hatGlyph)}</span>` : ''}</div>
<div class="pf-pic"><img class="pf-avatar" src="${esc(avatarUrl(u.username))}" alt="" width="112" height="112">${hatGlyph ? `<span class="pf-hat" title="${esc(hat.name)}" aria-hidden="true">${esc(hatGlyph)}</span>` : ''}</div>
-<div class="pf-who"><h1>${badgeId ? `<img class="pf-name-badge" src="https://openvibe.media/o/${badgeId}" alt="" title="${esc(badge.name)}" width="32" height="32">` : ''}${fxClass ? `<span class="ov-fx ${fxClass}" title="${esc(fx.name)}">${esc(name)}</span>` : esc(name)}</h1><p class="pf-handle">@${esc(u.username)}</p></div>
+<div class="pf-who"><h1>${badgeId ? `<img class="pf-name-badge" src="https://openvibe.media/o/${badgeId}" alt="" title="${esc(badge.name)}" width="32" height="32">` : ''}${fxClass ? `<span class="ov-fx ${fxClass}" title="${esc(fx.name)}">${esc(name)}</span>` : esc(name)}</h1><p class="pf-handle">@${esc(u.username)}${isPublic && pronouns ? ` <span class="pf-pronouns">${esc(pronouns)}</span>` : ''}</p></div>
</div>
</div>
${bio ? `<p class="pf-bio">${esc(bio)}</p>` : ''}
${bio ? `<p class="pf-bio">${esc(bio)}</p>` : ''}
${isPublic && Array.isArray(publicLinks) && publicLinks.length ? `<nav aria-label="${esc(name)}'s links"><ul class="pf-links">${publicLinks.slice(0, 8).map((link) => { let href = ''; try { const parsed = new URL(link.url); if (parsed.protocol === 'https:' && !parsed.username && !parsed.password) href = parsed.href; } catch {} return href ? `<li><a rel="me noopener noreferrer" href="${esc(href)}">${esc(String(link.label || '').slice(0, 40))}</a></li>` : ''; }).join('')}</ul></nav>` : ''}
${isPublic && Array.isArray(publicLinks) && publicLinks.length ? `<nav aria-label="${esc(name)}'s links"><ul class="pf-links">${publicLinks.slice(0, 8).map((link) => { let href = ''; try { const parsed = new URL(link.url); if (parsed.protocol === 'https:' && !parsed.username && !parsed.password) href = parsed.href; } catch {} return href ? `<li><a rel="me noopener noreferrer" href="${esc(href)}">${esc(String(link.label || '').slice(0, 40))}</a></li>` : ''; }).join('')}</ul></nav>` : ''}
@@ -616,4 +595,4 @@ ${require('openvibe-shared/footer').ssr({ service: 'network', variant: 'compact'
@@ -616,4 +595,4 @@ ${require('openvibe-shared/footer').ssr({ service: 'network', variant: 'compact'
</html>`;
</html>`;
}
}
-module.exports = { createPublicProfiles, renderPage, renderPersonPage, monthOf, SUBJECT_RE, PUBLIC_PLACE_TIMEOUT_MS, PUBLIC_PLACE_TTL_MS };
+module.exports = { createPublicProfiles, renderPage, monthOf, SUBJECT_RE, PUBLIC_PLACE_TIMEOUT_MS, PUBLIC_PLACE_TTL_MS };
@@ -32,7 +32,7 @@ function sitemapXml({ release = null, profiles = [] } = {}) {
@@ -32,7 +32,7 @@ function sitemapXml({ release = null, profiles = [] } = {}) {
const day = String(lastmod).slice(0, 10);
const day = String(lastmod).slice(0, 10);
return seo.sitemapXml([
return seo.sitemapXml([
...PAGES.map((page) => ({ loc: `${HOST}${page.path}`, lastmod: day, changefreq: page.changefreq, priority: page.priority })),
...PAGES.map((page) => ({ loc: `${HOST}${page.path}`, lastmod: day, changefreq: page.changefreq, priority: page.priority })),
- ...profiles.map((username) => ({ loc: `https://openvibe.my/@${encodeURIComponent(username)}`, lastmod: day, changefreq: 'weekly', priority: 0.5 })),
+ ...profiles.map((username) => ({ loc: `https://openvibe.network/@${encodeURIComponent(username)}`, lastmod: day, changefreq: 'weekly', priority: 0.5 })),
]);
]);
}
}
@@ -11,7 +11,7 @@ const fs = require('fs');
@@ -11,7 +11,7 @@ const fs = require('fs');
const path = require('path');
const path = require('path');
const express = require('express');
const express = require('express');
const { getDb } = require('../server/db/database');
const { getDb } = require('../server/db/database');
-const { createPublicProfiles, renderPersonPage, monthOf, PUBLIC_PLACE_TIMEOUT_MS, PUBLIC_PLACE_TTL_MS } = require('../server/profile/public');
+const { createPublicProfiles, monthOf, PUBLIC_PLACE_TIMEOUT_MS, PUBLIC_PLACE_TTL_MS } = require('../server/profile/public');
const publicCors = require('../server/public-cors');
const publicCors = require('../server/public-cors');
const A = 'usr_01JZ0000000000000000000AAA';
const A = 'usr_01JZ0000000000000000000AAA';
@@ -106,7 +106,7 @@ const INSTANCES = [
@@ -106,7 +106,7 @@ const INSTANCES = [
assert.ok(r.text.includes('Member since') && r.text.includes('October 2026'));
assert.ok(r.text.includes('Member since') && r.text.includes('October 2026'));
assert.ok(/<span class="pf-stat-k">Rarest<\/span><span class="pf-stat-v">Legendary<\/span>/.test(r.text));
assert.ok(/<span class="pf-stat-k">Rarest<\/span><span class="pf-stat-v">Legendary<\/span>/.test(r.text));
assert.ok(r.text.includes('<span class="pf-count">4</span>'), 'four items');
assert.ok(r.text.includes('<span class="pf-count">4</span>'), 'four items');
- assert.ok(r.text.includes('"@type":"ProfilePage"') && r.text.includes('<link rel="canonical" href="https://openvibe.network/@Ana">'));
+ assert.ok(r.text.includes('"@type":"ProfilePage"') && r.text.includes('<link rel="canonical" href="https://openvibe.network/@Ana">'), 'profiles live on openvibe.network');
assert.ok(r.text.includes('--pf-color:#ff0066'));
assert.ok(r.text.includes('--pf-color:#ff0066'));
assert.ok(r.text.includes('https://openvibe.my/inventory/itd_01JZ00000000000000000001R1'), 'items link to their Inventory page');
assert.ok(r.text.includes('https://openvibe.my/inventory/itd_01JZ00000000000000000001R1'), 'items link to their Inventory page');
assert.ok(!/name="robots" content="noindex"/.test(r.text));
assert.ok(!/name="robots" content="noindex"/.test(r.text));
@@ -158,7 +158,7 @@ const INSTANCES = [
@@ -158,7 +158,7 @@ const INSTANCES = [
assert.strictEqual(r.status, 200);
assert.strictEqual(r.status, 200);
assert.deepStrictEqual(JSON.parse(r.text).person.links, [{ label: 'My site', url: 'https://example.com/' }]);
assert.deepStrictEqual(JSON.parse(r.text).person.links, [{ label: 'My site', url: 'https://example.com/' }]);
assert.strictEqual(JSON.parse(r.text).person.bio, 'Hello there');
assert.strictEqual(JSON.parse(r.text).person.bio, 'Hello there');
- assert.strictEqual(JSON.parse(r.text).person.profile_url, 'https://openvibe.my/@Ana');
+ assert.strictEqual(JSON.parse(r.text).person.profile_url, 'https://openvibe.network/@Ana');
assert.strictEqual(JSON.parse(r.text).person.places.live.is_live, true);
assert.strictEqual(JSON.parse(r.text).person.places.live.is_live, true);
assert.deepStrictEqual(JSON.parse(r.text).person.places.videos.map((v) => v.title), ['First video']);
assert.deepStrictEqual(JSON.parse(r.text).person.places.videos.map((v) => v.title), ['First video']);
assert.deepStrictEqual(JSON.parse(r.text).person.places.videos.map((v) => v.url), ['https://openvibe.video/watch?v=tdLVs5wad4A'], 'a video links to its watch page');
assert.deepStrictEqual(JSON.parse(r.text).person.places.videos.map((v) => v.url), ['https://openvibe.video/watch?v=tdLVs5wad4A'], 'a video links to its watch page');
@@ -167,9 +167,14 @@ const INSTANCES = [
@@ -167,9 +167,14 @@ const INSTANCES = [
const person = JSON.parse(r.text).person;
const person = JSON.parse(r.text).person;
r = await get('/api/v1/people/@ana/page');
r = await get('/api/v1/people/@ana/page');
assert.deepStrictEqual([r.status, r.headers.get('location')], [301, '/api/v1/people/@Ana/page']);
assert.deepStrictEqual([r.status, r.headers.get('location')], [301, '/api/v1/people/@Ana/page']);
- const newPage = renderPersonPage({ username: 'Ana' }, person, 'https://openvibe.my');
- assert.ok(newPage.includes('<link rel="canonical" href="https://openvibe.my/@Ana">') && newPage.includes('First video') && newPage.includes('My site'));
- assert.ok(!newPage.includes('pf-grid') && !newPage.includes('inventory'), 'the new page shows public places without private account inventory');
+ // openvibe.my is the person's own hub; a profile address there moves to openvibe.network (the owner, 11-08).
+ // fetch cannot set Host, so this one request goes out raw.
+ const onMy = await new Promise((resolve, reject) => {
+ const u = new URL(b);
+ require('http').get({ host: u.hostname, port: u.port, path: '/@Ana', headers: { Host: 'openvibe.my' } }, (res) => { res.resume(); resolve({ status: res.statusCode, location: res.headers.location }); }).on('error', reject);
+ });
+ assert.deepStrictEqual([onMy.status, onMy.location], [301, 'https://openvibe.network/@Ana']);
+ assert.strictEqual(person.username, 'Ana');
// A hidden profile: name and picture, nothing else, not indexed.
// A hidden profile: name and picture, nothing else, not indexed.
r = await get('/@hidden');
r = await get('/@hidden');