openvibers/openvibe.tools Commits

Remove the retired paste page and the unused hand-written sitemap code (both replaced by the registry and Community)

OP OpenVibers committed 18 Sept 2026 · fd0f1a4ab9 · 3 files, +2 −670


Changes

ddiff --git a/apps/gateway/public/paste.html b/apps/gateway/public/paste.html
ddeleted file mode 100644
iindex a985e48..0000000
---- a/apps/gateway/public/paste.html
++++ /dev/null
@@@ -1,638 +0,0 @@
--<!DOCTYPE html>
--<html lang="en">
--<head>
-- <meta charset="UTF-8">
-- <meta name="viewport" content="width=device-width, initial-scale=1.0">
-- <title>Pastes.OpenVibe.Tools — Code & Text Sharing</title>
--<link rel="icon" type="image/svg+xml" data-ov-icon="tools" href="data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%22-32%20-32%2064%2064%22%3E%3Cdefs%3E%3ClinearGradient%20id%3D%22ring%22%20x1%3D%220%22%20y1%3D%220%22%20x2%3D%221%22%20y2%3D%221%22%3E%3Cstop%20offset%3D%220%22%20stop-color%3D%22%2338bdf8%22%2F%3E%3Cstop%20offset%3D%221%22%20stop-color%3D%22%232563eb%22%2F%3E%3C%2FlinearGradient%3E%0A%3CradialGradient%20id%3D%22glow%22%3E%3Cstop%20offset%3D%220%22%20stop-color%3D%22%2338bdf8%22%20stop-opacity%3D%22.34%22%2F%3E%3Cstop%20offset%3D%221%22%20stop-color%3D%22%2338bdf8%22%20stop-opacity%3D%220%22%2F%3E%3C%2FradialGradient%3E%0A%3CradialGradient%20id%3D%22bg%22%20cx%3D%22.5%22%20cy%3D%22.38%22%20r%3D%22.8%22%3E%3Cstop%20offset%3D%220%22%20stop-color%3D%22%2316223a%22%2F%3E%3Cstop%20offset%3D%22.6%22%20stop-color%3D%22%230a0f1c%22%2F%3E%3Cstop%20offset%3D%221%22%20stop-color%3D%22%23060912%22%2F%3E%3C%2FradialGradient%3E%3C%2Fdefs%3E%3Crect%20x%3D%22-32%22%20y%3D%22-32%22%20width%3D%2264%22%20height%3D%2264%22%20rx%3D%2215%22%20fill%3D%22url(%23bg)%22%2F%3E%3Cg%20transform%3D%22scale(1.22)%22%3E%0A%3Ccircle%20r%3D%2221%22%20fill%3D%22url(%23glow)%22%2F%3E%0A%3Ccircle%20r%3D%2218%22%20fill%3D%22none%22%20stroke%3D%22url(%23ring)%22%20stroke-width%3D%224%22%20opacity%3D%22.38%22%2F%3E%0A%3Cpath%20d%3D%22M0%2C-18%20A18%2C18%200%200%2C1%2017.4%2C4.6%22%20fill%3D%22none%22%20stroke%3D%22url(%23ring)%22%20stroke-width%3D%224.4%22%20stroke-linecap%3D%22round%22%2F%3E%0A%3Cpath%20d%3D%22M-9.5%2C-7%20L0%2C10%20L9.5%2C-7%22%20fill%3D%22none%22%20stroke%3D%22%23fff%22%20stroke-width%3D%224.8%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%2F%3E%0A%3Ccircle%20cx%3D%2217.4%22%20cy%3D%224.6%22%20r%3D%223.2%22%20fill%3D%22%23ffffff%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E">
--<meta name="theme-color" content="#0d131d">
--<meta name="color-scheme" content="dark light">
--<link rel="apple-touch-icon" href="/assets/logo-192.png">
--<link rel="manifest" href="/manifest.webmanifest">
-- <meta name="description" content="Paste / snippet sharing with syntax highlighting, screenshots, forks, likes, and comments. Part of the OpenVibe.">
-- <meta property="og:title" content="Pastes.OpenVibe.Tools — Code & Text Sharing">
-- <meta property="og:description" content="Paste / snippet sharing with syntax highlighting, screenshots, forks, likes, and comments.">
-- <meta property="og:type" content="website">
-- <meta property="og:url" content="https://pastes.openvibe.tools">
-- <link rel="canonical" href="https://pastes.openvibe.tools">
-- <link rel="preconnect" href="https://fonts.googleapis.com">
-- <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
--<!-- Critical theme bootstrap (ov-theme-boot) — MUST stay inline and above the stylesheets.
-- Paints the cached theme synchronously so the default palette never flashes. Reads
-- localStorage only: no network, nothing to block on. theme-loader.js still runs after
-- and reconciles with the account. -->
--<script>
--(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
--</script>
-- <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
-- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
-- <style>
-- :root {
-- --bg-primary: #0b0c12;
-- --bg-secondary: #12131c;
-- --bg-tertiary: #1a1b28;
-- --bg-card: rgba(22,24,36,0.85);
-- --bg-hover: #252738;
-- --bg-input: #181926;
-- --text-primary: #f1f4fb;
-- --text-secondary: #a0a8c4;
-- --text-muted: #5e6580;
-- --accent: #8b5cf6;
-- --accent-light: #a78bfa;
-- --accent-dark: #6d28d9;
-- --green: #22c55e;
-- --red: #ef4444;
-- --yellow: #f59e0b;
-- --blue: #3b82f6;
-- --cyan: #06b6d4;
-- --border: rgba(255,255,255,0.07);
-- --border-light: rgba(255,255,255,0.12);
-- --shadow: 0 24px 64px rgba(0,0,0,0.4);
-- --radius: 16px;
-- --radius-sm: 10px;
-- --mono: 'JetBrains Mono', 'Fira Code', monospace;
-- }
--
-- *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
-- html { background: var(--bg-primary); scroll-behavior: smooth; }
-- body {
-- min-height: 100vh;
-- font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
-- background: var(--bg-primary);
-- color: var(--text-primary);
-- line-height: 1.6;
-- }
--
-- .bg-glow {
-- position: fixed; top: -40%; left: -20%;
-- width: 140vw; height: 140vh;
-- background: radial-gradient(ellipse at 30% 20%, rgba(139,92,246,0.04) 0%, transparent 60%),
-- radial-gradient(ellipse at 70% 80%, rgba(59,130,246,0.03) 0%, transparent 50%);
-- pointer-events: none; z-index: 0;
-- }
--
-- .page { position: relative; z-index: 1; }
-- .nav-placeholder { height: 56px; }
-- .shell { width: min(960px, calc(100% - 32px)); margin: 0 auto; padding: 24px 0 60px; }
--
-- /* ── Hero ─── */
-- .hero { text-align: center; padding: 32px 0 24px; }
-- .hero h1 { font-size: 2rem; font-weight: 800; letter-spacing: -0.02em; }
-- .hero h1 i { color: var(--accent); margin-right: 8px; }
-- .hero p { color: var(--text-secondary); margin-top: 4px; font-size: 0.95rem; }
--
-- /* ── Buttons ─── */
-- .btn {
-- display: inline-flex; align-items: center; gap: 6px;
-- padding: 8px 16px; border-radius: var(--radius-sm);
-- font-weight: 600; font-size: 0.85rem; cursor: pointer;
-- border: 1px solid var(--border-light); background: var(--bg-tertiary);
-- color: var(--text-primary); transition: background 0.15s;
-- }
-- .btn:hover { background: var(--bg-hover); }
-- .btn-accent { background: var(--accent); color: #000; border-color: var(--accent); }
-- .btn-accent:hover { background: var(--accent-light); }
-- .btn-small { padding: 5px 12px; font-size: 0.8rem; }
-- .btn-icon { padding: 6px 10px; }
--
-- /* ── Tabs ─── */
-- .tabs { display: flex; gap: 4px; margin-bottom: 16px; border-bottom: 1px solid var(--border); padding-bottom: 8px; }
-- .tab {
-- padding: 6px 14px; border-radius: var(--radius-sm); cursor: pointer;
-- font-size: 0.85rem; color: var(--text-secondary); transition: all 0.15s;
-- border: none; background: none;
-- }
-- .tab:hover { color: var(--text-primary); background: var(--bg-hover); }
-- .tab.active { color: var(--accent); background: rgba(139,92,246,0.1); font-weight: 600; }
--
-- /* ── Card ─── */
-- .card {
-- background: var(--bg-card); border: 1px solid var(--border);
-- border-radius: var(--radius); padding: 20px;
-- backdrop-filter: blur(12px);
-- }
--
-- /* ── New Paste Form ─── */
-- .paste-editor { display: flex; flex-direction: column; gap: 12px; }
-- .paste-editor input, .paste-editor textarea, .paste-editor select {
-- background: var(--bg-input); border: 1px solid var(--border);
-- border-radius: var(--radius-sm); padding: 10px 14px;
-- color: var(--text-primary); font-family: inherit; font-size: 0.9rem;
-- outline: none; transition: border 0.15s;
-- }
-- .paste-editor input:focus, .paste-editor textarea:focus { border-color: var(--accent); }
-- .paste-editor textarea {
-- font-family: var(--mono); font-size: 0.85rem;
-- min-height: 260px; resize: vertical; line-height: 1.5;
-- tab-size: 4;
-- }
-- .paste-editor .row { display: flex; gap: 10px; flex-wrap: wrap; }
-- .paste-editor .row > * { flex: 1; min-width: 120px; }
--
-- /* ── Paste List ─── */
-- .paste-list { display: flex; flex-direction: column; gap: 8px; }
-- .paste-item {
-- display: flex; align-items: flex-start; gap: 14px;
-- padding: 14px 16px; border-radius: var(--radius-sm);
-- background: var(--bg-secondary); border: 1px solid var(--border);
-- cursor: pointer; transition: all 0.15s;
-- }
-- .paste-item:hover { background: var(--bg-hover); border-color: var(--border-light); }
-- .paste-item-icon {
-- width: 36px; height: 36px; display: flex; align-items: center; justify-content: center;
-- background: rgba(139,92,246,0.1); color: var(--accent);
-- border-radius: 8px; font-size: 0.9rem; flex-shrink: 0;
-- }
-- .paste-item-icon.screenshot { background: rgba(59,130,246,0.1); color: var(--blue); }
-- .paste-item-body { flex: 1; min-width: 0; }
-- .paste-item-title { font-weight: 600; font-size: 0.9rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
-- .paste-item-meta { font-size: 0.75rem; color: var(--text-muted); margin-top: 2px; display: flex; gap: 12px; flex-wrap: wrap; }
-- .paste-item-snippet {
-- font-family: var(--mono); font-size: 0.75rem; color: var(--text-secondary);
-- margin-top: 6px; max-height: 3.2em; overflow: hidden;
-- white-space: pre-wrap; word-break: break-all;
-- }
--
-- /* ── Single Paste View ─── */
-- .paste-view-header { display: flex; justify-content: space-between; align-items: flex-start; gap: 12px; flex-wrap: wrap; margin-bottom: 16px; }
-- .paste-view-title { font-size: 1.3rem; font-weight: 700; }
-- .paste-view-meta { font-size: 0.8rem; color: var(--text-muted); margin-top: 4px; }
-- .paste-view-actions { display: flex; gap: 6px; flex-wrap: wrap; }
-- .paste-content-wrap {
-- position: relative; background: var(--bg-input); border: 1px solid var(--border);
-- border-radius: var(--radius-sm); overflow: hidden;
-- }
-- .paste-content {
-- padding: 16px; font-family: var(--mono); font-size: 0.82rem;
-- line-height: 1.65; white-space: pre-wrap; word-break: break-all;
-- max-height: 600px; overflow-y: auto;
-- }
-- .paste-content-wrap .copy-btn {
-- position: absolute; top: 8px; right: 8px;
-- }
-- .paste-screenshot img {
-- max-width: 100%; border-radius: var(--radius-sm);
-- }
--
-- /* ── Comments ─── */
-- .comments { margin-top: 20px; }
-- .comments h3 { font-size: 0.95rem; font-weight: 600; margin-bottom: 10px; }
-- .comment {
-- padding: 10px 14px; margin-bottom: 6px;
-- background: var(--bg-secondary); border-radius: var(--radius-sm);
-- border: 1px solid var(--border);
-- }
-- .comment-header { font-size: 0.78rem; color: var(--text-muted); margin-bottom: 4px; }
-- .comment-header strong { color: var(--text-secondary); }
-- .comment-body { font-size: 0.85rem; }
-- .comment-form { display: flex; gap: 8px; margin-top: 10px; }
-- .comment-form input {
-- flex: 1; background: var(--bg-input); border: 1px solid var(--border);
-- border-radius: var(--radius-sm); padding: 8px 12px; color: var(--text-primary);
-- font-size: 0.85rem; outline: none;
-- }
-- .comment-form input:focus { border-color: var(--accent); }
--
-- /* ── Pagination ─── */
-- .pagination { display: flex; gap: 6px; justify-content: center; margin-top: 16px; }
--
-- /* ── States ─── */
-- .empty { text-align: center; padding: 40px; color: var(--text-muted); }
-- .loading { text-align: center; padding: 40px; color: var(--text-muted); }
-- .hidden { display: none !important; }
--
-- /* ── Toast ─── */
-- .toast-container { position: fixed; bottom: 20px; right: 20px; z-index: 9999; display: flex; flex-direction: column; gap: 6px; }
-- .toast {
-- padding: 10px 16px; border-radius: var(--radius-sm);
-- font-size: 0.82rem; font-weight: 500;
-- background: var(--bg-tertiary); border: 1px solid var(--border-light);
-- color: var(--text-primary); animation: slideIn 0.2s;
-- }
-- .toast.error { border-color: var(--red); color: var(--red); }
-- .toast.success { border-color: var(--green); color: var(--green); }
-- @keyframes slideIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: none; } }
--
-- @media (max-width: 600px) {
-- .hero h1 { font-size: 1.4rem; }
-- .shell { padding: 16px 0 40px; }
-- .paste-view-header { flex-direction: column; }
-- }
-- </style>
--</head>
--<body>
-- <div class="bg-glow"></div>
-- <div class="page">
-- <div class="nav-placeholder" id="navbar-mount"></div>
-- <div class="shell">
-- <div class="hero">
-- <h1><i class="fa-solid fa-paste"></i> Paste.OpenVibe</h1>
-- <p>Share code, text, and screenshots. No signup required.</p>
-- </div>
--
-- <!-- Tabs -->
-- <div class="tabs">
-- <button class="tab active" data-view="browse" onclick="showView('browse')"><i class="fa-solid fa-list"></i> Browse</button>
-- <button class="tab" data-view="new" onclick="showView('new')"><i class="fa-solid fa-plus"></i> New Paste</button>
-- </div>
--
-- <!-- Browse View -->
-- <div id="view-browse">
-- <div style="display:flex;gap:8px;margin-bottom:14px;flex-wrap:wrap;">
-- <input type="text" id="paste-search" placeholder="Search pastes…" style="flex:1;min-width:200px;background:var(--bg-input);border:1px solid var(--border);border-radius:var(--radius-sm);padding:8px 12px;color:var(--text-primary);font-size:0.85rem;outline:none;" oninput="debouncedSearch()">
-- <select id="paste-type-filter" onchange="loadPastes()" style="background:var(--bg-input);border:1px solid var(--border);border-radius:var(--radius-sm);padding:8px 12px;color:var(--text-primary);font-size:0.85rem;">
-- <option value="">All Types</option>
-- <option value="paste">Text</option>
-- <option value="screenshot">Screenshots</option>
-- </select>
-- </div>
-- <div id="paste-list" class="paste-list">
-- <div class="loading"><i class="fa-solid fa-spinner fa-spin"></i> Loading…</div>
-- </div>
-- <div id="paste-pagination" class="pagination"></div>
-- </div>
--
-- <!-- New Paste View -->
-- <div id="view-new" class="hidden">
-- <div class="card">
-- <form class="paste-editor" onsubmit="createPaste(event)">
-- <input type="text" id="np-title" placeholder="Title (optional)" maxlength="200">
-- <textarea id="np-content" placeholder="Paste your code or text here…" required></textarea>
-- <div class="row">
-- <select id="np-language">
-- <option value="text">Plain Text</option>
-- <option value="javascript">JavaScript</option>
-- <option value="python">Python</option>
-- <option value="html">HTML</option>
-- <option value="css">CSS</option>
-- <option value="json">JSON</option>
-- <option value="bash">Bash</option>
-- <option value="sql">SQL</option>
-- <option value="markdown">Markdown</option>
-- <option value="c">C</option>
-- <option value="cpp">C++</option>
-- <option value="java">Java</option>
-- <option value="rust">Rust</option>
-- <option value="go">Go</option>
-- <option value="ruby">Ruby</option>
-- <option value="php">PHP</option>
-- <option value="yaml">YAML</option>
-- <option value="xml">XML</option>
-- </select>
-- <select id="np-visibility">
-- <option value="public">Public</option>
-- <option value="unlisted">Unlisted</option>
-- </select>
-- </div>
-- <div style="display:flex;gap:10px;align-items:center;">
-- <label style="font-size:0.82rem;color:var(--text-secondary);display:flex;align-items:center;gap:6px;">
-- <input type="checkbox" id="np-burn"> Burn after read
-- </label>
-- <label style="font-size:0.82rem;color:var(--text-secondary);display:flex;align-items:center;gap:6px;">
-- <input type="checkbox" id="np-nsfw"> NSFW
-- </label>
-- </div>
-- <button type="submit" class="btn btn-accent"><i class="fa-solid fa-paper-plane"></i> Create Paste</button>
-- </form>
-- </div>
-- </div>
--
-- <!-- Single Paste View -->
-- <div id="view-paste" class="hidden">
-- <div id="paste-detail"></div>
-- </div>
-- </div>
-- </div>
--
-- <div id="ov-footer"></div>
-- <div class="toast-container" id="toasts"></div>
--
-- <script src="https://openvibe.network/shared/navbar.js"></script>
-- <script src="https://openvibe.network/shared/footer.js"></script>
-- <script src="https://openvibe.network/shared/theme-loader.js" defer></script>
-- <script>
-- 'use strict';
--
-- const API = '/api/pastes';
-- const PAGE_SIZE = 20;
-- let currentOffset = 0;
-- let token = null;
--
-- function esc(s) { const d = document.createElement('div'); d.textContent = s; return d.innerHTML; }
--
-- function timeAgo(ts) {
-- const d = new Date(ts);
-- const diff = (Date.now() - d.getTime()) / 1000;
-- if (diff < 60) return 'just now';
-- if (diff < 3600) return `${Math.floor(diff / 60)}m ago`;
-- if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`;
-- if (diff < 604800) return `${Math.floor(diff / 86400)}d ago`;
-- return d.toLocaleDateString();
-- }
--
-- function getCookie(n) {
-- const m = document.cookie.match(new RegExp('(?:^|; )' + n + '=([^;]*)'));
-- return m ? decodeURIComponent(m[1]) : null;
-- }
--
-- async function api(path, opts = {}) {
-- const headers = { 'Content-Type': 'application/json' };
-- if (token) headers['Authorization'] = `Bearer ${token}`;
-- const res = await fetch(`${API}${path}`, { ...opts, headers: { ...headers, ...opts.headers } });
-- const data = await res.json().catch(() => ({}));
-- if (!res.ok) throw new Error(data.error || `Request failed (${res.status})`);
-- return data;
-- }
--
-- // ── Views ──────────────────────────────────────────────
-- function showView(view) {
-- document.querySelectorAll('.tab').forEach(t => t.classList.toggle('active', t.dataset.view === view));
-- document.getElementById('view-browse').classList.toggle('hidden', view !== 'browse');
-- document.getElementById('view-new').classList.toggle('hidden', view !== 'new');
-- document.getElementById('view-paste').classList.toggle('hidden', view !== 'paste');
-- if (view === 'browse') { loadPastes(); history.pushState(null, '', '/'); }
-- if (view === 'new') history.pushState(null, '', '/new');
-- }
--
-- // ── Browse ─────────────────────────────────────────────
-- let _searchTimer;
-- function debouncedSearch() { clearTimeout(_searchTimer); _searchTimer = setTimeout(() => { currentOffset = 0; loadPastes(); }, 300); }
--
-- async function loadPastes() {
-- const list = document.getElementById('paste-list');
-- const search = document.getElementById('paste-search').value.trim();
-- const type = document.getElementById('paste-type-filter').value;
-- list.innerHTML = '<div class="loading"><i class="fa-solid fa-spinner fa-spin"></i> Loading…</div>';
--
-- try {
-- const params = new URLSearchParams({ limit: PAGE_SIZE, offset: currentOffset });
-- if (search) params.set('search', search);
-- if (type) params.set('type', type);
-- const data = await api(`/?${params}`);
-- renderPasteList(data.pastes || [], data.total || 0);
-- } catch (err) {
-- list.innerHTML = `<div class="empty"><i class="fa-solid fa-triangle-exclamation"></i> ${esc(err.message)}</div>`;
-- }
-- }
--
-- function renderPasteList(pastes, total) {
-- const list = document.getElementById('paste-list');
-- if (!pastes.length) {
-- list.innerHTML = '<div class="empty"><i class="fa-solid fa-clipboard-question"></i> No pastes found</div>';
-- document.getElementById('paste-pagination').innerHTML = '';
-- return;
-- }
--
-- list.innerHTML = pastes.map(p => {
-- const isScreenshot = p.type === 'screenshot';
-- const iconClass = isScreenshot ? 'screenshot' : '';
-- const icon = isScreenshot ? 'fa-image' : 'fa-code';
-- const snippet = !isScreenshot && p.content ? `<div class="paste-item-snippet">${esc(p.content.slice(0, 200))}</div>` : '';
-- return `<div class="paste-item" onclick="viewPaste('${esc(p.slug)}')">
-- <div class="paste-item-icon ${iconClass}"><i class="fa-solid ${icon}"></i></div>
-- <div class="paste-item-body">
-- <div class="paste-item-title">${esc(p.title || 'Untitled')}</div>
-- <div class="paste-item-meta">
-- <span>${esc(p.display_name || p.username || 'Anonymous')}</span>
-- <span>${timeAgo(p.created_at)}</span>
-- ${p.language && p.language !== 'text' ? `<span>${esc(p.language)}</span>` : ''}
-- <span><i class="fa-solid fa-eye"></i> ${p.views || 0}</span>
-- <span><i class="fa-solid fa-heart"></i> ${p.likes || 0}</span>
-- </div>
-- ${snippet}
-- </div>
-- </div>`;
-- }).join('');
--
-- // Pagination
-- const pages = Math.ceil(total / PAGE_SIZE);
-- const currentPage = Math.floor(currentOffset / PAGE_SIZE);
-- const pag = document.getElementById('paste-pagination');
-- if (pages <= 1) { pag.innerHTML = ''; return; }
-- let html = '';
-- for (let i = 0; i < pages && i < 10; i++) {
-- html += `<button class="btn btn-small ${i === currentPage ? 'btn-accent' : ''}" onclick="goPage(${i})">${i + 1}</button>`;
-- }
-- pag.innerHTML = html;
-- }
--
-- function goPage(page) {
-- currentOffset = page * PAGE_SIZE;
-- loadPastes();
-- }
--
-- // ── View Single Paste ──────────────────────────────────
-- async function viewPaste(slug) {
-- showViewDirect('paste');
-- const detail = document.getElementById('paste-detail');
-- detail.innerHTML = '<div class="loading"><i class="fa-solid fa-spinner fa-spin"></i> Loading…</div>';
-- history.pushState(null, '', `/p/${slug}`);
--
-- try {
-- const data = await api(`/${slug}`);
-- const p = data.paste;
-- renderPasteView(p);
-- initChrome({ type: 'paste', title: p.title || slug });
-- loadComments(slug);
-- } catch (err) {
-- initChrome();
-- detail.innerHTML = `<div class="empty">${esc(err.message)}</div>`;
-- }
-- }
--
-- function renderPasteView(p) {
-- const detail = document.getElementById('paste-detail');
-- const isScreenshot = p.type === 'screenshot';
--
-- const contentHtml = isScreenshot
-- ? `<div class="paste-screenshot"><img src="${esc(p.screenshot_url)}" alt="Screenshot"></div>`
-- : `<div class="paste-content-wrap">
-- <button class="btn btn-small btn-icon copy-btn" onclick="copyPaste('${esc(p.slug)}')"><i class="fa-solid fa-copy"></i></button>
-- <pre class="paste-content">${esc(p.content || '')}</pre>
-- </div>`;
--
-- detail.innerHTML = `
-- <div class="card">
-- <div class="paste-view-header">
-- <div>
-- <div class="paste-view-title">${esc(p.title || 'Untitled')}</div>
-- <div class="paste-view-meta">
-- ${esc(p.display_name || p.username || 'Anonymous')} &middot; ${timeAgo(p.created_at)}
-- ${p.language && p.language !== 'text' ? ` &middot; ${esc(p.language)}` : ''}
-- &middot; <i class="fa-solid fa-eye"></i> ${p.views || 0}
-- ${p.burn_after_read ? ' &middot; <span style="color:var(--red)"><i class="fa-solid fa-fire"></i> Burn after read</span>' : ''}
-- ${p.is_nsfw ? ' &middot; <span style="color:var(--yellow)">NSFW</span>' : ''}
-- </div>
-- </div>
-- <div class="paste-view-actions">
-- <button class="btn btn-small" onclick="likePaste('${esc(p.slug)}')">
-- <i class="fa-${p.liked ? 'solid' : 'regular'} fa-heart"></i> <span id="like-count">${p.likes || 0}</span>
-- </button>
-- <button class="btn btn-small" onclick="forkPaste('${esc(p.slug)}')"><i class="fa-solid fa-code-fork"></i> Fork</button>
-- ${!isScreenshot ? `<button class="btn btn-small" onclick="copyPaste('${esc(p.slug)}')"><i class="fa-solid fa-copy"></i> Copy</button>` : ''}
-- <a class="btn btn-small" href="${API}/${esc(p.slug)}/raw" target="_blank"><i class="fa-solid fa-download"></i> Raw</a>
-- <button class="btn btn-small" onclick="showView('browse')"><i class="fa-solid fa-arrow-left"></i> Back</button>
-- </div>
-- </div>
-- ${contentHtml}
-- <div class="comments" id="comments-section"></div>
-- </div>`;
-- }
--
-- // ── Actions ────────────────────────────────────────────
-- async function createPaste(e) {
-- e.preventDefault();
-- const content = document.getElementById('np-content').value;
-- if (!content.trim()) return;
-- try {
-- const data = await api('/', {
-- method: 'POST',
-- body: JSON.stringify({
-- title: document.getElementById('np-title').value.trim() || undefined,
-- content,
-- language: document.getElementById('np-language').value,
-- visibility: document.getElementById('np-visibility').value,
-- burn_after_read: document.getElementById('np-burn').checked,
-- is_nsfw: document.getElementById('np-nsfw').checked,
-- }),
-- });
-- toast('Paste created!', 'success');
-- viewPaste(data.paste.slug);
-- document.querySelector('.paste-editor').reset();
-- } catch (err) {
-- toast(err.message, 'error');
-- }
-- }
--
-- async function likePaste(slug) {
-- try {
-- const data = await api(`/${slug}/like`, { method: 'POST' });
-- document.getElementById('like-count').textContent = data.likes;
-- } catch (err) {
-- toast(err.message, 'error');
-- }
-- }
--
-- async function forkPaste(slug) {
-- try {
-- const data = await api(`/${slug}/fork`, { method: 'POST' });
-- toast('Forked!', 'success');
-- viewPaste(data.paste.slug);
-- } catch (err) {
-- toast(err.message, 'error');
-- }
-- }
--
-- async function copyPaste(slug) {
-- try {
-- const data = await api(`/${slug}`);
-- await navigator.clipboard.writeText(data.paste.content || '');
-- api(`/${slug}/copy`, { method: 'POST' }).catch(() => {});
-- toast('Copied!', 'success');
-- } catch {
-- toast('Copy failed', 'error');
-- }
-- }
--
-- // ── Comments ───────────────────────────────────────────
-- async function loadComments(slug) {
-- const section = document.getElementById('comments-section');
-- try {
-- const data = await api(`/${slug}/comments`);
-- const comments = data.comments || [];
-- section.innerHTML = `
-- <h3><i class="fa-solid fa-comments"></i> Comments (${comments.length})</h3>
-- ${comments.map(c => `
-- <div class="comment">
-- <div class="comment-header"><strong>${esc(c.display_name || c.username || c.anon_name || 'Anonymous')}</strong> &middot; ${timeAgo(c.created_at)}</div>
-- <div class="comment-body">${esc(c.message)}</div>
-- </div>
-- `).join('')}
-- <form class="comment-form" onsubmit="postComment(event, '${esc(slug)}')">
-- <input type="text" id="comment-input" placeholder="Add a comment…" maxlength="500" required>
-- <button type="submit" class="btn btn-small btn-accent"><i class="fa-solid fa-paper-plane"></i></button>
-- </form>`;
-- } catch { /* non-critical */ }
-- }
--
-- async function postComment(e, slug) {
-- e.preventDefault();
-- const input = document.getElementById('comment-input');
-- const message = input.value.trim();
-- if (!message) return;
-- try {
-- await api(`/${slug}/comments`, { method: 'POST', body: JSON.stringify({ message }) });
-- input.value = '';
-- loadComments(slug);
-- } catch (err) {
-- toast(err.message, 'error');
-- }
-- }
--
-- // ── Routing ────────────────────────────────────────────
-- function showViewDirect(view) {
-- document.querySelectorAll('.tab').forEach(t => t.classList.toggle('active', t.dataset.view === view));
-- document.getElementById('view-browse').classList.toggle('hidden', view !== 'browse');
-- document.getElementById('view-new').classList.toggle('hidden', view !== 'new');
-- document.getElementById('view-paste').classList.toggle('hidden', view !== 'paste');
-- }
--
-- // ── Shared chrome ──────────────────────────────────────
-- // The navbar records this visit in the account's cross-network history, so on a paste
-- // page it waits for the paste's title (viewPaste calls it); everywhere else it boots at once.
-- let chromeReady = false;
-- function initChrome(history) {
-- if (chromeReady) return;
-- chromeReady = true;
-- try {
-- OpenVibeNavbar.init({
-- service: 'paste', apiBase: 'https://openvibe.network', token,
-- history: history || { type: 'tool', title: 'Paste.OpenVibe' },
-- silentLogin: 'https://openvibe.tools/auth/login?silent=1&next={url}',
-- menu: { after: [{ id: 'my-pastes', label: 'My pastes', href: 'https://openvibe.community/my', icon: 'fa-clipboard-list' }] },
-- });
-- } catch {}
-- try {
-- OpenVibeFooter.init({
-- service: 'paste', variant: 'compact', mount: '#ov-footer', apiBase: 'https://openvibe.network',
-- links: [{ heading: 'Pastes', items: [
-- { name: 'New paste', url: '/new' },
-- { name: 'Browse pastes', url: '/' },
-- { name: 'My pastes', url: 'https://openvibe.community/my' },
-- ] }],
-- });
-- } catch {}
-- }
--
-- function route() {
-- const path = location.pathname;
-- if (path.startsWith('/p/')) {
-- const slug = path.split('/')[2];
-- if (slug) { viewPaste(slug); return; }
-- }
-- initChrome();
-- if (path === '/new') { showView('new'); return; }
-- showView('browse');
-- }
--
-- window.addEventListener('popstate', route);
--
-- // ── Toast ──────────────────────────────────────────────
-- function toast(msg, type = 'info') {
-- const container = document.getElementById('toasts');
-- const t = document.createElement('div');
-- t.className = `toast ${type}`;
-- t.textContent = msg;
-- container.appendChild(t);
-- setTimeout(() => t.remove(), 4000);
-- }
--
-- // ── Init ───────────────────────────────────────────────
-- document.addEventListener('DOMContentLoaded', () => {
-- token = localStorage.getItem('ov_token') || getCookie('ov_token');
-- route();
-- });
-- </script>
-- <script src="https://openvibe.live/live-notify.js" async></script>
--</body>
--</html>
ddiff --git a/apps/gateway/server/index.js b/apps/gateway/server/index.js
iindex e87dd82..8ffef58 100644
---- a/apps/gateway/server/index.js
++++ b/apps/gateway/server/index.js
@@@ -256,7 +256,7 @@ app.use((req, res, next) => {
return sendTool(req, res, NET_HTML);
});
--// Paste subdomain → paste.html SPA
++// Paste subdomain → hands over to openvibe.community
app.use((req, res, next) => {
if (getRequestHost(req) !== 'pastes.openvibe.tools') return next();
if (req.path.startsWith('/api/') || req.path.startsWith('/auth/')) return next();
ddiff --git a/apps/gateway/server/seo/render.js b/apps/gateway/server/seo/render.js
iindex a9c6f0a..15a237c 100644
---- a/apps/gateway/server/seo/render.js
++++ b/apps/gateway/server/seo/render.js
@@@ -139,34 +139,4 @@ function renderTool(file, sub, canonicalHost) {
return html;
}
--/** Hub pages list every tool, which gives crawlers a clean path into all of them. */
--function toolUrls() {
-- const { CATALOG } = require('./catalog');
-- const urls = [];
-- for (const [sub, rec] of CATALOG) {
-- urls.push({ loc: `https://${sub}.openvibe.tools/`, priority: rec.hub ? '0.9' : '0.8', changefreq: rec.hub ? 'weekly' : 'monthly' });
-- }
-- return urls;
--}
--
--/** Full sitemap: the hubs, every tool subdomain, and the satellite apps. */
--function buildSitemap(extraUrls = []) {
-- const today = new Date().toISOString().slice(0, 10);
-- const seen = new Set();
-- const all = [
-- { loc: `${SITE}/`, priority: '1.0', changefreq: 'weekly' },
-- ...toolUrls(),
-- ...extraUrls,
-- ].filter(u => { if (seen.has(u.loc)) return false; seen.add(u.loc); return true; });
-- const body = all.map(u => ` <url>\n <loc>${u.loc}</loc>\n <lastmod>${u.lastmod || today}</lastmod>\n <changefreq>${u.changefreq || 'monthly'}</changefreq>\n <priority>${u.priority || '0.7'}</priority>\n </url>`).join('\n');
-- return `<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n${body}\n</urlset>\n`;
--}
--
--/** Satellite apps that live outside the dev/net catalogs but belong in the sitemap. */
--const SATELLITES = [
-- 'img', 'audio', 'docs', 'food', 'maps', 'yt', 'text', 'logo', 'pastes',
-- 'case', 'braille', 'binary', 'fancy', 'json', 'markdown', 'escape', 'diff', 'slug',
-- 'wordcount', 'reverse', 'reversetext',
--].map(sub => ({ loc: `https://${sub}.openvibe.tools/`, priority: '0.8', changefreq: 'weekly' }));
--
--module.exports = { renderTool, buildSitemap, SATELLITES, toolUrls, SITE };
++module.exports = { renderTool, SITE };