openvibers/openvibe.community Commits
Pastes: title+8-char id addresses, 301s from old slugs, no IndexNow for noindex pages (#287)
Changes
@@@ -115,9 +115,15 @@ Moments feeds read it.`fine`. Text pastes are not reviewed. `paste.review_category`, `review_status`, `review_report_id`,`reviewed_at` and the requested fields are in the paste shape; the enforcement lives in `server/pastes/review.js`.Tests: `test/paste-review.test.js`.--- **Slugs**: public pastes get a short `adj-noun-NN` slug; a new unlisted or private paste (and-- a fork of one) gets `adj-noun-` + 16 random base62 characters, because its slug is the only-- thing keeping it unlisted. Existing slugs never change and keep working.++- **Addresses**: a public paste's canonical address is `/p/<title-slug>-<public_id>`. The++ `public_id` is 8 base58 characters, generated once (migration 0021; existing rows are filled by++ `scripts/backfill-paste-public-ids.js` in batches, and a read fills one lazily). The title slug is++ up to six lowercase ASCII words from the title (or the first content line when untitled, or++ nothing), so the id alone is a valid address and every old `adj-noun-NN` slug 301s to the++ canonical one; a retitle moves it and the old form 301s. A new unlisted or private paste (and a++ fork of one) keeps an `adj-noun-` + 16 random base62 secret slug: its secret part is the only++ thing keeping it unlisted, and its public id is not a second key. `server/pastes/url.js` owns++ the rules; `test/paste-urls.test.js` pins them.- **Deletes are soft** (content scrubbed, slug kept reserved); edits append `paste_versions`.Moving the data: Media's `openvibe.media.pastes-export` bundle was imported once (Wave 5, 2026-09-22; the ledger is@@@ -136,7 +142,7 @@ Pages (server-rendered HTML):| --- | --- || `GET /` | Home: hero, latest pastes, most viewed, "start a paste" CTA, what is coming || `GET /pastes` | Browse — `?q=` search, `?sort=new\|views`, `?lang=`, `?page=` |--| `GET /p/:slug` | Paste page — highlighted body (server-side, highlight.js), raw/download/copy/fork/share, screenshot, related |++| `GET /p/:slug` | Paste page at `/p/<title-slug>-<public id>` (old slugs and the bare id 301 there) — highlighted body (server-side, highlight.js), raw/download/copy/fork/share, screenshot, related || `POST /p/:slug/comments` | Comment on the paste (signed in; the no-JS form) — its typed comment thread, the one `/c/:accessId` and the APIs show || `GET /p/:slug/raw` | `live`: 302 → `https://openvibe.media/p/:slug/raw`; `community`: the text itself || `GET /p/:slug/screenshot` | `live`: 302 → Media's `/p/:slug/screenshot`; `community`: 302 → the stored image URL |@@@ -664,9 +670,11 @@ Discovery is written by `openvibe-shared/seo`; Community only supplies the data(an `ai-summary` meta and a `WebPage` JSON-LD), with `article:*` times on pastes. JSON-LD:`WebSite` on the home page, `Article`/`ImageObject` with author and `datePublished` on pastepages, `DiscussionForumPosting` on threads, `BreadcrumbList` everywhere.--Unlisted/private pastes, search result pages and members/staff spaces are `noindex`. The--sitemap lists home, `/pastes`, the latest public pastes, `/forum`, `/pulse`, public spaces and--their latest threads (rebuilt at most hourly). `/robots.txt` welcomes search and AI crawlers++Every paste page is `noindex` (link-shared content, SEO-29), so no paste is in the sitemap and++paste changes never ping IndexNow (a ping for a noindex page is two contradictory signals).++Unlisted/private pastes, search result pages and members/staff spaces are `noindex` too. The++sitemap lists home, `/pastes`, `/forum`, `/pulse`, public spaces and their latest threads++(rebuilt at most hourly). `/robots.txt` welcomes search and AI crawlersand keeps them out of `/api/`, `/auth/`, `/my`, `/new`, `/forum/*/new` and `?sso=` URLs;`/llms.txt` maps the site and `/llms-full.txt` carries the latest public threads and text pastesin full (no NSFW, no burn-after-read, nothing members-only).nnew file mode 100644@@@ -0,0 +1,8 @@++-- phase: expand++-- The public paste id (URL decisions 2026-11-07): /p/<title-slug>-<id>, where the id is 8 base58++-- characters stored once per paste. Expand-only: the column is nullable, existing rows are filled by++-- scripts/backfill-paste-public-ids.js (in batches) and reads fill one lazily; the partial unique index++-- keeps every assigned id unique without blocking the backfill.++ALTER TABLE pastes ADD COLUMN IF NOT EXISTS public_id text COLLATE "C";++++CREATE UNIQUE INDEX IF NOT EXISTS idx_pastes_public_id ON pastes(public_id) WHERE public_id IS NOT NULL;@@@ -228,10 +228,11 @@}body.envelope = b64(sealed.bytes);var out = await api('/api/pastes', { method: 'POST', json: body });++ var href = out && (out.url || (out.paste && out.paste.url));var slug = out && (out.slug || (out.paste && out.paste.slug));-- if (!slug) throw new Error('No paste came back');++ if (!href && !slug) throw new Error('No paste came back');status.textContent = 'Done — opening your paste…';-- location.href = '/p/' + encodeURIComponent(slug) + (sealed.linkKey ? '#k=' + sealed.linkKey : '');++ location.href = (href || '/p/' + encodeURIComponent(slug)) + (sealed.linkKey ? '#k=' + sealed.linkKey : '');}form.addEventListener('submit', function (e) {@@@ -258,10 +259,11 @@p = api('/api/pastes', { method: 'POST', json: Object.assign({ content: el.content.value, language: el.language.value }, common) });}p.then(function (out) {++ var href = out && (out.url || (out.paste && out.paste.url));var slug = out && (out.slug || (out.paste && out.paste.slug));-- if (!slug) throw new Error('No paste came back');++ if (!href && !slug) throw new Error('No paste came back');status.textContent = 'Done — opening your paste…';-- location.href = '/p/' + encodeURIComponent(slug);++ location.href = href || '/p/' + encodeURIComponent(slug);}).catch(function (err) {btn.disabled = false; status.className = 'form-status error';status.textContent = err.status === 429 && err.data && err.data.cooldown ? 'Slow down — try again in ' + err.data.cooldown + 's.' : err.message;nnew file mode 100755@@@ -0,0 +1,38 @@++#!/usr/bin/env node++'use strict';++/**++ * Fill pastes.public_id (migration 0021) in batches. Existing pastes were minted before the id existed; the++ * page route fills one lazily on first read, but this brings every row level at once so the first crawl or++ * export after a deploy sees the canonical /p/<title-slug>-<id> address.++ *++ * node scripts/backfill-paste-public-ids.js # batches of 500 until none are left++ * node scripts/backfill-paste-public-ids.js --batch 2000++ *++ * Safe to run twice: it only touches rows where public_id IS NULL, and the partial unique index keeps every++ * assigned id unique (a collision is reported and the row is left for the next run).++ */++const config = require('../server/config');++const { openDb, closeDb } = require('../server/db');++const { newPublicId } = require('../server/pastes/url');++++const batchArg = process.argv.indexOf('--batch');++const BATCH = Math.max(1, Math.min(10000, Number(batchArg > 0 ? process.argv[batchArg + 1] : 500) || 500));++++(async () => {++ const db = await openDb(config);++ let total = 0;++ for (;;) {++ const rows = await db.prepare('SELECT id FROM pastes WHERE public_id IS NULL ORDER BY id LIMIT ?').all(BATCH);++ if (!rows.length) break;++ let done = 0;++ for (const row of rows) {++ const info = await db.prepare('UPDATE pastes SET public_id = ? WHERE id = ? AND public_id IS NULL').run(newPublicId(), row.id);++ done += info.changes;++ }++ total += done;++ console.log(`backfilled ${total} paste public id${total === 1 ? '' : 's'}`);++ if (!done) break;++ }++ console.log(total ? `done: ${total} paste public id${total === 1 ? '' : 's'} assigned` : 'done: every paste already has a public id');++ await closeDb();++})().catch((err) => { console.error('backfill failed:', err && err.message ? err.message : err); process.exitCode = 1; });@@@ -503,6 +503,17 @@ async function createApp(opts = {}) {descriptor: require('./agent-descriptor'),render: require('./agent-pages').createAgentPages({ views: agentViews }).renderPublicPage,});++ // An old paste address's twin 301s to the canonical address's twin, exactly as the page does, so the++ // Markdown a crawler stores names one URL.++ app.use(async (req, res, next) => {++ if (!String(req.path || '').startsWith('/p/') || !String(req.path || '').endsWith('.md')) return next();++ let slug;++ try { slug = decodeURIComponent(req.path.slice(3, -3)); } catch { return next(); }++ const canonical = await require('./pastes/store').canonicalForRef(db, slug).catch(() => null);++ if (!canonical || canonical === slug) return next();++ const q = req.originalUrl.includes('?') ? req.originalUrl.slice(req.originalUrl.indexOf('?')) : '';++ return res.redirect(301, `/p/${encodeURIComponent(canonical)}.md${q}`);++ });app.use((req, res, next) => {// The manifest exactly: the rest of /.well-known/ (security.txt) belongs to the app.if (req.path === '/.well-known/agent.json') return agentRouter(req, res, next);@@@ -566,7 +577,11 @@ async function createApp(opts = {}) {if (!SLUG_RE.test(slug)) return { status: 404, body: pages.errorPage({ status: 404, title: 'Paste not found', message: 'That link does not point at a paste.' }) };// A twin is not a person reading: never consume a burn-after-read paste for one.if (twin) {-- const row = await db.prepare('SELECT burn_after_read FROM pastes WHERE slug = ?').get(slug);++ let row = await db.prepare('SELECT burn_after_read FROM pastes WHERE slug = ?').get(slug);++ if (!row) {++ const id = require('./pastes/url').publicIdOfRef(slug);++ if (id) row = await db.prepare('SELECT burn_after_read FROM pastes WHERE public_id = ?').get(id);++ }if (!row || Number(row.burn_after_read)) return null;}let paste;@@@ -576,7 +591,7 @@ async function createApp(opts = {}) {if (err.status === 404) {// A paste shared with specific people answers the gate: "Ask for access" when signed// in, "Sign in" otherwise, and never its title, content, screenshot or comments.-- const sh = await sharing.shareOf(slug);++ const sh = await sharing.shareOf(await require('./pastes/store').canonicalForRef(db, slug) || slug);if (sh && sh.audience === 'people') {const signInUrl = `/auth/login?next=${encodeURIComponent(`/p/${slug}`)}`;return { status: 404, body: pages.gatePage({ slug, signedInAs: user && user.username ? user.username : null, signInUrl }) };@@@ -596,15 +611,23 @@ async function createApp(opts = {}) {const sort = query.sort === 'old' ? 'old' : 'new';const thread = paste.burn_after_read ? null : await comments.forRef(viewer, pasteRef(paste.slug), { sort, after, limit: 30 });return {-- status: 200, slug: paste.slug,++ status: 200, slug: paste.slug, canonical: paste.canonical_slug || paste.slug,body: pages.pastePage({ paste, related, user, comments: thread, commentSort: sort, commentsAfter: after, share }),};}app.get('/p/:slug', withUser, wrap(async (req, res) => {++ // The id is the address; the title slug before it is decoration. An old adjective-noun-number slug, a++ // bare id or a title that has since changed all 301 to the one canonical form (query kept), before a++ // view is counted.++ const canonical = await require('./pastes/store').canonicalForRef(db, req.params.slug);++ if (canonical && canonical !== req.params.slug) {++ const q = req.originalUrl.includes('?') ? req.originalUrl.slice(req.originalUrl.indexOf('?')) : '';++ return res.redirect(301, `/p/${encodeURIComponent(canonical)}${q}`);++ }const out = await pasteView(req.params.slug, req.query, { viewer: req.viewer, user: req.user, ctx: ctxOf(req) });if (!out) return html(res, pages.errorPage({ status: 404, title: 'Paste not found', message: 'It may have been deleted, burned after reading, or never existed.' }), 404);-- if (out.status === 200) twinLink(res, `/p/${encodeURIComponent(out.slug)}`);++ if (out.status === 200) twinLink(res, `/p/${encodeURIComponent(out.canonical || out.slug)}`);html(res, out.body, out.status);}));@@@ -614,16 +637,21 @@ async function createApp(opts = {}) {const { slug } = req.params;const origin = req.get('origin');if (origin && origin !== 'null' && origin !== config.baseUrl) return html(res, pages.errorPage({ status: 403, title: 'Not allowed', message: 'That form was sent from another site.' }), 403);-- const back = `/p/${encodeURIComponent(slug)}`;++ let back = `/p/${encodeURIComponent(slug)}`;try {if (!SLUG_RE.test(slug)) throw new v1.ApiError(404, 'ref.not_found', 'No such paste');++ // The URL may be the canonical title+id form; the comment thread is keyed by the paste's stored++ // slug, so resolve the paste first and keep the stable ref.++ const found = await source.getPaste(slug, { ...ctxOf(req), noView: true });++ const stable = found.slug;++ back = `/p/${encodeURIComponent(found.canonical_slug || stable)}`;if (!req.viewer || !req.viewer.subject) return res.redirect(303, `/auth/login?next=${encodeURIComponent(back)}`);-- const { thread } = await comments.resolve(req.viewer, { ref: pasteRef(slug) });++ const { thread } = await comments.resolve(req.viewer, { ref: pasteRef(stable) });const b = req.body || {};const out = await comments.add(req.viewer, thread.id, { message: String(b.message || '').slice(0, 5000), parent_id: b.parent_id || null });return res.redirect(303, `${back}#comment-${out.comment.id}`);} catch (err) {-- if (!(err instanceof v1.ApiError)) return next(err);++ if (!(err instanceof v1.ApiError) && !(err && err.name === 'PasteError')) return next(err);if (err.status === 401) return res.redirect(303, `/auth/login?next=${encodeURIComponent(back)}`);if (err.status === 404) return html(res, pages.errorPage({ status: 404, title: 'Paste not found', message: 'It may have been deleted, burned after reading, or never existed.' }), 404);return html(res, pages.errorPage({ status: err.status, title: err.status === 429 ? 'Slow down' : 'That did not work', message: err.message }), err.status);@@@ -656,7 +684,7 @@ async function createApp(opts = {}) {if (variant && variant.error) return res.status(400).type('text/plain').set('X-Content-Type-Options', 'nosniff').send(variant.error);try {await app.locals.pastes.screenshotUrl(req.viewer, req.params.slug);-- const row = await db.prepare('SELECT media_ref, screenshot_url, visibility, burn_after_read, locked FROM pastes WHERE slug = ? AND deleted_at IS NULL').get(req.params.slug);++ const row = await db.prepare('SELECT media_ref, screenshot_url, visibility, burn_after_read, locked FROM pastes WHERE (slug = ? OR public_id = ?) AND deleted_at IS NULL').get(req.params.slug, require('./pastes/url').publicIdOfRef(req.params.slug) || '');const got = row ? await app.locals.pasteMedia.bytes(row, variant) : null;if (!got) return notFound(res);// A public paste's screenshot never changes under this URL, so it is cached like any other@@@ -688,7 +716,7 @@ async function createApp(opts = {}) {.type('application/octet-stream').send(out.bytes);} catch (err) { if (err.status === 404) return res.status(404).type('text/plain').send('Not found'); throw err; }}-- if (paste.type === 'screenshot') return res.redirect(302, `/p/${encodeURIComponent(slug)}/screenshot`);++ if (paste.type === 'screenshot') return res.redirect(302, `/p/${encodeURIComponent(paste.canonical_slug || slug)}/screenshot`);res.set('Content-Disposition', `attachment; filename="${slug}.${extensionFor(paste.language)}"`);res.set('Cache-Control', cache.htmlHeaders({ private: true }));res.type('text/plain; charset=utf-8').send(String(paste.content || ''));@@@ -75,7 +75,7 @@ async function llmsFullSections() {const pastes = (await catalog.latest(30)).filter((p) => !Number(p.is_nsfw) && !Number(p.burn_after_read) && p.type !== 'screenshot');const full = await Promise.all(pastes.map((p) => source.publicForDiscovery(p.slug)));sections.push({ title: 'Latest pastes', pages: full.filter(Boolean).map((p) => ({-- title: p.title || 'Untitled', url: abs(`/p/${p.slug}`), text: String(p.content || ''),++ title: p.title || 'Untitled', url: abs(`/p/${p.canonical_slug || require('./pastes/url').canonicalSlug(p)}`), text: String(p.content || ''),})) });// Sharing (design 2026-11-06): the four audiences, the transfer, and the curls an agent uses.sections.push({ title: 'Sharing a paste', pages: [{ title: 'Sharing a paste', url: '/docs/sharing',@@@ -114,7 +114,7 @@ async function sitemapRows() {/** The latest pastes for /feed.xml. */async function pasteFeedItems() {return (await catalog.latest(30)).filter((p) => !Number(p.is_nsfw)).map((p) => {-- const url = abs(`/p/${p.slug}`);++ const url = abs(`/p/${p.canonical_slug || require('./pastes/url').canonicalSlug(p)}`);const description = p.type === 'screenshot' ? 'A screenshot shared on OpenVibe.Community.' : clean(p.content || '', 300);return { title: p.title || 'Untitled', link: url, guid: url, description, content: description,author: p.display_name || p.username || 'Anonymous', published: isoDate(p.created_at), updated: isoDate(p.updated_at || p.created_at) };@@@ -77,7 +77,8 @@ async function publish(envelope) {}// ── Builders (read the row as it is after the change) ─────────────────────────────--const pasteUrl = (p) => (p.visibility === 'public' && !p.deleted_at ? `${config.baseUrl}/p/${encodeURIComponent(p.slug)}` : null);++// The id alone: an event is a machine envelope, and the canonical title+id address would carry the title.++const pasteUrl = (p) => (p.visibility === 'public' && !p.deleted_at ? `${config.baseUrl}/p/${encodeURIComponent(p.public_id || p.slug)}` : null);const actorOf = (sub) => { const r = subjectRef(sub); return r ? { type: r.type, id: r.id } : null; };async function pasteCreated(p) {@@@ -18,6 +18,7 @@const crypto = require('crypto');const events = require('../events');const store = require('./store');++const { canonicalSlug, publicIdOfRef } = require('./url');const spam = require('./spam');const commentStore = require('../comments/store');const { discussionModerator } = require('../identity/capabilities');@@@ -91,18 +92,11 @@ function createPasteService({ db, network = null, media = null, config = {}, limconst L = { ...DEFAULT_LIMITS, ...limits };// Pulse (server/pulse) hears about public pastes written by people; a Pulse problem never fails a paste write.const tell = async (fn) => { if (!pulse) return; try { await fn(pulse); } catch (err) { console.warn('[Pastes] pulse:', err.message); } };-- // IndexNow (openvibe-shared/indexnow): a public, indexable paste page appearing, changing or going away-- // pings the engines with its path and the sitemap. Private, unlisted, NSFW and burn-after-read pages are-- // not in the sitemap, so they never ping.++ // IndexNow (openvibe-shared/indexnow): a paste page is noindex (SEO-29: link-shared content, not++ // search-found content), so there is nothing to announce. Pinging a URL the page tells engines not to++ // index is two contradictory signals (URL findings 2026-11-07, finding 4), so paste changes never ping.++ const pingIndexNow = () => {};const site = String(config.baseUrl || '').replace(/\/$/, '');-- const indexable = (p) => !!(p && p.visibility === 'public' && !Number(p.is_nsfw) && !Number(p.burn_after_read));-- const pingIndexNow = (...rows) => {-- if (!indexnow || !indexnow.enabled) return;-- const slugs = new Set();-- for (const p of rows) if (indexable(p)) slugs.add(p.slug);-- if (!slugs.size) return;-- indexnow.pingSoon([...slugs].map((s) => `${site}/p/${s}`).concat(`${site}/sitemap.xml`));-- };const visitorSecret = process.env.VIEW_HASH_SECRET|| crypto.createHash('sha256').update(`community-views:${(config.oauth && config.oauth.clientSecret) || crypto.randomBytes(16).toString('hex')}`).digest('hex');@@@ -164,7 +158,19 @@ function createPasteService({ db, network = null, media = null, config = {}, lim* confirms a slug).*/async function visible(v, slug) {-- const p = await store.getBySlug(db, String(slug));++ const ref = String(slug);++ let p = await store.getBySlug(db, ref);++ // A canonical /p/<title-slug>-<id> (or the bare id) names the paste by its id: the title slug is++ // decoration and may have changed since the link was copied. The old adjective-noun-number slug still++ // resolves through getBySlug, so it can 301 on the page route.++ if (!p) {++ const id = publicIdOfRef(ref);++ if (id) {++ const byId = await store.getByPublicId(db, id);++ // A secret paste's secret part is its access control: the stored public id is not a second key.++ if (byId && byId.visibility === 'public' && !Number(byId.burn_after_read)) p = byId;++ }++ }if (!p || !await mayView(v, p)) fail(404, 'Paste not found');return p;}@@@ -233,11 +239,13 @@ function createPasteService({ db, network = null, media = null, config = {}, lim// its owner chose to keep it outside the envelope. The envelope's address is how a browser opens it.const locked = !!p.locked;const titleShown = !locked || !!p.title_shown;++ const canonical = canonicalSlug(p);return {unique_views: p.unique_views || 0,id: p.id,app_id: 'community',slug: p.slug,++ canonical_slug: canonical,user_id: p.owner_subject || null,owner_subject: p.owner_subject || null,origin: p.origin,@@@ -249,13 +257,13 @@ function createPasteService({ db, network = null, media = null, config = {}, limlocked,lock: locked ? (p.lock_kind || null) : null,title_shown: locked ? !!p.title_shown : true,-- envelope_url: locked ? `/p/${encodeURIComponent(p.slug)}/raw` : null,++ envelope_url: locked ? `/p/${encodeURIComponent(canonical)}/raw` : null,stream_id: streamIdOf(ref),stream_ref: ref,// The guarded address, never Media's own object URL: screenshots are stored private in Media (the share// rules decide who sees one), so /o/<id> is a 404 to every browser. /p/<slug>/screenshot checks the paste's// audience and streams the bytes (2026-11-07: every image paste and AI moment on openvibe.live was broken).-- screenshot_url: p.screenshot_url && !locked ? `${site || ''}/p/${encodeURIComponent(p.slug)}/screenshot` : null,++ screenshot_url: p.screenshot_url && !locked ? `${site || ''}/p/${encodeURIComponent(canonical)}/screenshot` : null,metadata: metadataFor(p.metadata, isOwner(v, p) || isService(v)),burn_after_read: !!p.burn_after_read,forked_from: p.forked_from || null,@@@ -272,8 +280,8 @@ function createPasteService({ db, network = null, media = null, config = {}, limai_tags: locked ? null : (p.ai_tags || null),ai_analyzed_at: locked ? null : (p.ai_analyzed_at || null),revision: p.revision,-- url: `/p/${p.slug}`,-- raw_url: `/p/${p.slug}/raw`,++ url: `/p/${canonical}`,++ raw_url: `/p/${canonical}/raw`,created_at: p.created_at,updated_at: p.updated_at,is_owner: isOwner(v, p),@@@ -470,7 +478,7 @@ function createPasteService({ db, network = null, media = null, config = {}, limasync function created(row, extra = {}) {await tell((p) => p.pasteCreated(row));pingIndexNow(row);-- return { id: row.id, slug: row.slug, url: `/p/${row.slug}`, ...extra };++ return { id: row.id, slug: row.slug, url: `/p/${canonicalSlug(row)}`, ...extra };}// ═════════════════════════════════════════════════════════@@@ -515,7 +523,7 @@ function createPasteService({ db, network = null, media = null, config = {}, limasync publicForDiscovery(slug) {const p = await store.getBySlug(db, String(slug));if (!p || p.visibility !== 'public' || p.type !== 'paste' || Number(p.is_nsfw) || Number(p.burn_after_read)) return null;-- return { slug: p.slug, title: p.title, content: p.content };++ return { slug: p.slug, public_id: p.public_id, canonical_slug: canonicalSlug(p), title: p.title, content: p.content };},/** The share state of a paste for the page: its audience and, for the recipient, their pending offer. */@@@ -550,9 +558,8 @@ function createPasteService({ db, network = null, media = null, config = {}, lim/** GET /p/:slug/raw — plain text, Media's raw rules (a burn paste survives exactly one read). */async raw(v, slug, ctx = {}) {-- const p = await store.getBySlug(db, String(slug));-- if (!p || !await mayView(v, p)) fail(404, 'Not found');-- if (p.type === 'screenshot') return { redirect: `/p/${encodeURIComponent(p.slug)}/screenshot` };++ const p = await visible(v, slug);++ if (p.type === 'screenshot') return { redirect: `/p/${encodeURIComponent(canonicalSlug(p))}/screenshot` };if (p.type !== 'paste') fail(404, 'Not found');// A locked paste's raw is its envelope, as bytes: application/octet-stream, `.ovlk`.if (p.locked) {@@@ -572,8 +579,8 @@ function createPasteService({ db, network = null, media = null, config = {}, lim/** GET /p/:slug/screenshot target (the stored Media URL). For a burn-after-read screenshot it is a read (raw's rule). */async screenshotUrl(v, slug) {-- const p = await store.getBySlug(db, String(slug));-- if (!p || !p.screenshot_url || !await mayView(v, p)) fail(404, 'Not found');++ const p = await visible(v, slug);++ if (!p.screenshot_url) fail(404, 'Not found');if (p.burn_after_read && !isOwner(v, p)) {if (p.views > 0) await burn(p);await store.bumpViews(db, p.id);@@@ -11,6 +11,7 @@* stay reserved and a re-import can never bring them back; every read below skips them.*/const crypto = require('crypto');++const { newPublicId, publicIdOfRef, canonicalSlug } = require('./url');// ── Slugs (the same adjective-noun-number style OpenVibe.Media has always minted) ──const SLUG_ADJECTIVES = [@@@ -124,24 +125,62 @@ const likeEscape = (s) => String(s).replace(/[\\%_]/g, (c) => `\\${c}`);// ── Pastes ───────────────────────────────────────────────────++/** One random id, stored once; two readers racing fill the first assignment and the other re-reads it. */++async function ensurePublicId(db, row) {++ if (!row || row.public_id) return row;++ const id = newPublicId();++ const info = await db.prepare('UPDATE pastes SET public_id = ? WHERE id = ? AND public_id IS NULL').run(id, row.id);++ if (info.changes) return { ...row, public_id: id };++ return await db.prepare('SELECT * FROM pastes WHERE id = ?').get(row.id) || row;++}++async function getBySlug(db, slug) {-- return await db.prepare('SELECT * FROM pastes WHERE slug = ? AND deleted_at IS NULL').get(String(slug)) || null;++ return await ensurePublicId(db, await db.prepare('SELECT * FROM pastes WHERE slug = ? AND deleted_at IS NULL').get(String(slug)) || null);}async function getById(db, id) {-- return await db.prepare('SELECT * FROM pastes WHERE id = ? AND deleted_at IS NULL').get(id) || null;++ return await ensurePublicId(db, await db.prepare('SELECT * FROM pastes WHERE id = ? AND deleted_at IS NULL').get(id) || null);++}++++async function getByPublicId(db, id) {++ if (!publicIdOfRef(id)) return null;++ return await ensurePublicId(db, await db.prepare('SELECT * FROM pastes WHERE public_id = ? AND deleted_at IS NULL').get(String(id)) || null);++}++++/**++ * The canonical /p/ segment for a reference, without loading the service's access rules: a row page++ * redirect can happen before a view is counted. An old public slug resolves to the new title+id form;++ * a link/private or burn paste keeps its secret slug, so its canonical is the reference itself.++ */++async function canonicalForRef(db, ref) {++ const raw = String(ref == null ? '' : ref);++ let row = await db.prepare('SELECT id, slug, public_id, visibility, burn_after_read, title, content FROM pastes WHERE slug = ? AND deleted_at IS NULL').get(raw);++ if (!row) {++ const id = publicIdOfRef(raw);++ if (!id) return null;++ row = await db.prepare('SELECT id, slug, public_id, visibility, burn_after_read, title, content FROM pastes WHERE public_id = ? AND deleted_at IS NULL').get(id);++ // The id is a public address only for a public, non-burn paste; a secret paste's slug is the key and++ // the id must not turn into a second, weaker one.++ if (row && (row.visibility !== 'public' || Number(row.burn_after_read))) return null;++ }++ if (!row) return null;++ if (!row.public_id) row = await ensurePublicId(db, row);++ return canonicalSlug(row);}--const INSERT_COLUMNS = ['slug', 'owner_subject', 'origin', 'type', 'title', 'content', 'language', 'visibility',++const INSERT_COLUMNS = ['slug', 'public_id', 'owner_subject', 'origin', 'type', 'title', 'content', 'language', 'visibility','screenshot_url', 'media_ref', 'stream_ref', 'metadata', 'burn_after_read', 'forked_from', 'pinned', 'is_nsfw','ai_summary', 'ai_tags', 'ai_analyzed_at', 'locked', 'lock_kind', 'envelope', 'title_shown'];/** Insert a new paste (fields already validated); returns the stored row. */async function insertPaste(db, fields) {-- const cols = INSERT_COLUMNS.filter((c) => fields[c] !== undefined);++ // Every paste gets its public id once, even a secret one: the column is what a later public title+id++ // address is built from, and it never changes.++ const values = { ...fields, public_id: fields.public_id || newPublicId() };++ const cols = INSERT_COLUMNS.filter((c) => values[c] !== undefined);return await db.tx(async () => {const info = await db.prepare(`INSERT INTO pastes (${cols.join(', ')}) VALUES (${cols.map(() => '?').join(', ')}) RETURNING id`)-- .run(...cols.map((c) => fields[c]));++ .run(...cols.map((c) => values[c]));const row = await getById(db, info.lastInsertRowid);await require('../events').pasteCreated(row); // community.paste.created, in this transactionreturn row;@@@ -436,7 +475,7 @@ async function mapSet(db, system, type, id, targetType, targetId) {.run(String(system), String(type), String(id), String(targetType), String(targetId));}--module.exports = {++module.exports = { ensurePublicId, getByPublicId, canonicalForRef, canonicalSlug, publicIdOfRef,generateSlug, slugTaken, retireSlug, sanitizeTitle, detectLanguage,getBySlug, getById, insertPaste, updatePaste, listVersions, softDelete, listPastes,countOwnerSince, lastPasteTime, hasLiked, toggleLike, incrementCopies, bumpViews, recordVisit, pruneVisits,nnew file mode 100644@@@ -0,0 +1,59 @@++'use strict';++++/**++ * The public address of a paste, in one place so the page, its Markdown twin, JSON-LD, the feed,++ * Search documents and events can never disagree.++ *++ * public /p/<title-slug>-<public_id> (the title slug is optional: /p/<public_id>)++ * link/private, burn-after-read /p/<stored slug> (the secret part is the access control)++ *++ * <public_id> is 8 base58 characters, stored once per paste (migrations/0012_paste_public_id.sql,++ * scripts/backfill-paste-public-ids.js). The slug part is decoration: lookups use the id, so a++ * retitled paste keeps its address's id and its old title-slug form 301s to the new one.++ */++const crypto = require('crypto');++++const ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'; // base58: no 0, O, I, l++const PUBLIC_ID_RE = /^[1-9A-HJ-NP-Za-km-z]{8}$/;++const MAX_WORDS = 6;++const MAX_CHARS = 48;++++function newPublicId() {++ let out = '';++ for (let i = 0; i < 8; i++) out += ALPHABET[crypto.randomInt(ALPHABET.length)];++ return out;++}++++/** Lowercase ASCII words, at most six and 48 characters, joined by '-'; '' when there are none. */++function titleSlug(paste) {++ const p = paste || {};++ // A paste called "Untitled" is untitled: its first line of content names it instead.++ const title = String(p.title == null ? '' : p.title).trim();++ const text = !title || title === 'Untitled' ? String(p.content || '').split(/\r?\n/)[0] : title;++ const words = String(text).toLowerCase().replace(/[^a-z0-9]+/g, ' ').trim().split(' ').filter(Boolean).slice(0, MAX_WORDS);++ let out = '';++ for (const word of words) {++ const next = out ? `${out}-${word}` : word;++ if (next.length > MAX_CHARS) break;++ out = next;++ }++ return out;++}++++/** The canonical path segment of a paste (without /p/). */++function canonicalSlug(paste) {++ const p = paste || {};++ if (!p.public_id || p.visibility !== 'public' || Number(p.burn_after_read)) return String(p.slug || '');++ const words = titleSlug(p);++ return words ? `${words}-${p.public_id}` : p.public_id;++}++++/** The 8-char id in a reference (bare id, or the last '-'-separated part), or null. */++function publicIdOfRef(ref) {++ const s = String(ref == null ? '' : ref);++ if (PUBLIC_ID_RE.test(s)) return s;++ const m = /-([1-9A-HJ-NP-Za-km-z]{8})$/.exec(s);++ return m ? m[1] : null;++}++++module.exports = { ALPHABET, PUBLIC_ID_RE, newPublicId, titleSlug, canonicalSlug, publicIdOfRef };@@@ -6,6 +6,7 @@* openvibe-shared/seo's jsonLdTag (via headTags).*/const config = require('../config');++const { canonicalSlug } = require('../pastes/url');const { abs, SITE_NAME, DEFAULT_DESCRIPTION } = require('./layout');function isoDate(v) {@@@ -44,7 +45,7 @@ function authorLd(paste) {function pasteLd(paste, { description, image }) {const isScreenshot = paste.type === 'screenshot';-- const url = abs(`/p/${paste.slug}`);++ const url = abs(`/p/${paste.canonical_slug || canonicalSlug(paste)}`);const base = {'@context': 'https://schema.org','@type': isScreenshot ? 'ImageObject' : 'Article',@@@ -15,6 +15,8 @@ const kit = require('openvibe-shared/kit');const { highlight, escapeHtml: esc, languageLabel, extensionFor, LANGUAGES } = require('./highlight');const { renderMarkdown } = require('./markdown');const { pickBlock, foldBlock } = require('./short');++const { canonicalSlug } = require('../pastes/url');++const pastePathSlug = (p) => (p && (p.canonical_slug || canonicalSlug(p))) || '';// Languages read as prose: their lines wrap by default (the Wrap button still toggles), so a phone shows the whole text.const PROSE = new Set(['markdown', 'plaintext', 'text']);@@@ -35,7 +37,7 @@ function mediaW(u, w) {return /^https:\/\/openvibe\.media\/o\/med_[0-9A-Za-z]+$/.test(String(u || '')) ? `${u}?w=${w}` : u;}/** A screenshot's address: Community's checked proxy, never the Media object's own URL. */--function screenshotSrc(p) { return `/p/${encodeURIComponent(p.slug)}/screenshot`; }++function screenshotSrc(p) { return `/p/${encodeURIComponent(pastePathSlug(p))}/screenshot`; }/** A card-sized screenshot through the same proxy: Media's ladder has 320 and 640, webp by URL. */function screenshotVariant(p, w) { return `${screenshotSrc(p)}?w=${w}&fmt=webp`; }/** A card screenshot: two widths, the card's shape in width/height (no shift), lazy unless it is the LCP. */@@@ -124,7 +126,7 @@ function pasteCard(p, { eager = false } = {}) {: `<pre class="card-code" aria-hidden="true">${esc(String(p.content || '').slice(0, 220))}${String(p.content || '').length > 220 ? '…' : ''}</pre>`;const flags = `${p.pinned ? '<i class="fa-solid fa-thumbtack" title="Pinned"></i> ' : ''}${p.burn_after_read ? '<i class="fa-solid fa-fire" title="Burns after reading"></i> ' : ''}${locked ? '<i class="fa-solid fa-lock" title="Locked"></i> ' : ''}${p.is_nsfw ? '<span class="badge badge-nsfw">NSFW</span> ' : ''}`;return `<article class="card${p.is_nsfw ? ' card-nsfw' : ''}${locked ? ' card-is-locked' : ''}">-- <a class="card-link" href="/p/${esc(p.slug)}">++ <a class="card-link" href="/p/${esc(pastePathSlug(p))}">${preview}<h3 class="card-title">${flags}${esc(p.title || (locked ? 'A locked paste' : 'Untitled'))}</h3></a>@@@ -187,7 +189,7 @@ function pasteKitCard(p, { eager = false } = {}) {const isShot = p.type === 'screenshot';const image = isShot && !p.is_nsfw ? screenshotVariant(p, 640) : '';const card = kit.card({-- href: `/p/${p.slug}`, title: p.title || 'Untitled', type: 'post', ratio: '16/9', image: image || '',++ href: `/p/${pastePathSlug(p)}`, title: p.title || 'Untitled', type: 'post', ratio: '16/9', image: image || '',badge: p.is_nsfw ? 'NSFW' : p.origin === 'ai' ? 'AI' : p.pinned ? 'Pinned' : '',corner: isShot ? '' : languageLabel(p.language || 'text'),preview: !isShot && !p.is_nsfw ? String(p.content || '') : '',@@@ -321,7 +323,7 @@ ${homeFaq()}active: 'home',jsonLd: [ld.websiteLd(),-- ...(latest && latest.length ? [{ '@context': 'https://schema.org', '@type': 'ItemList', name: 'Latest pastes', url: `${config.baseUrl}/pastes`, itemListElement: latest.slice(0, 12).map((p, i) => ({ '@type': 'ListItem', position: i + 1, name: p.title || 'Untitled', url: `${config.baseUrl}/p/${p.slug}` })) }] : []),++ ...(latest && latest.length ? [{ '@context': 'https://schema.org', '@type': 'ItemList', name: 'Latest pastes', url: `${config.baseUrl}/pastes`, itemListElement: latest.slice(0, 12).map((p, i) => ({ '@type': 'ListItem', position: i + 1, name: p.title || 'Untitled', url: `${config.baseUrl}/p/${pastePathSlug(p)}` })) }] : []),{ '@context': 'https://schema.org', '@type': 'FAQPage', mainEntity: HOME_FAQ.map((f) => ({ '@type': 'Question', name: f.q, acceptedAnswer: { '@type': 'Answer', text: f.a } })) },],// The kit's controls (kit.css) and its cards and rails (room.css, after it); kit-web.js comes with community.js.@@@ -419,6 +421,7 @@ function pastePage({ paste: p, related, user, comments = null, commentSort = 'ne// A locked paste (Locker design 2026-11-06, the pilot): Community holds an envelope and never the plaintext. The// title is here only if its owner chose to show it; the page opens the envelope in the browser.const locked = !!p.locked;++ const pageSlug = p.canonical_slug || canonicalSlug(p);const title = p.title || (locked ? 'A locked paste' : isShot ? 'Screenshot' : 'Untitled paste');const description = pasteDescription(p);// The screenshot is Community's own address: the proxy checks canView, never the Media object's URL.@@@ -476,8 +479,8 @@ function pastePage({ paste: p, related, user, comments = null, commentSort = 'ne${isOwner ? `<button type="button" class="btn btn-sm btn-danger" data-delete="${esc(p.slug)}"><i class="fa-solid fa-trash" aria-hidden="true"></i> Delete</button>` : ''}</div>` : `<div class="actions">${!isShot ? `<button type="button" class="btn btn-sm btn-primary" data-copy-content="${esc(p.slug)}"><i class="fa-solid fa-copy" aria-hidden="true"></i> Copy</button>` : ''}-- ${!isShot ? `<a class="btn btn-sm" href="/p/${esc(p.slug)}/raw"><i class="fa-solid fa-file-lines" aria-hidden="true"></i> Raw</a>` : `<a class="btn btn-sm" href="/p/${esc(p.slug)}/screenshot"><i class="fa-solid fa-image" aria-hidden="true"></i> Full image</a>`}-- <a class="btn btn-sm" href="/p/${esc(p.slug)}/download" download="${esc(p.slug)}.${esc(isShot ? 'png' : ext)}"><i class="fa-solid fa-download" aria-hidden="true"></i> Download</a>++ ${!isShot ? `<a class="btn btn-sm" href="/p/${esc(pageSlug)}/raw"><i class="fa-solid fa-file-lines" aria-hidden="true"></i> Raw</a>` : `<a class="btn btn-sm" href="/p/${esc(pageSlug)}/screenshot"><i class="fa-solid fa-image" aria-hidden="true"></i> Full image</a>`}++ <a class="btn btn-sm" href="/p/${esc(pageSlug)}/download" download="${esc(pageSlug)}.${esc(isShot ? 'png' : ext)}"><i class="fa-solid fa-download" aria-hidden="true"></i> Download</a>${!isShot ? `<a class="btn btn-sm" href="/new?fork=${esc(p.slug)}"><i class="fa-solid fa-code-fork" aria-hidden="true"></i> Fork</a>` : ''}<button type="button" class="btn btn-sm" data-ov-share-sheet data-title="${esc(title)}" data-kind="paste" data-id="${esc(p.slug)}"><i class="fa-solid fa-share-nodes" aria-hidden="true"></i> Share</button>${!restricted && p.visibility !== 'private' && !p.burn_after_read ? `<a class="btn btn-sm" href="/forum/discuss?paste=${encodeURIComponent(p.slug)}"><i class="fa-solid fa-comments" aria-hidden="true"></i> Discuss in the forum</a>` : ''}@@@ -505,7 +508,7 @@ ${offer}${actions}</header>${bodyBlock}-- ${locked ? `<p class="locked-file muted small"><a href="/p/${esc(p.slug)}/download" download="${esc(p.slug)}.ovlk"><i class="fa-solid fa-lock" aria-hidden="true"></i> Download the locked file (.ovlk)</a> — it opens only with its key, in the Locker.</p>` : ''}++ ${locked ? `<p class="locked-file muted small"><a href="/p/${esc(pageSlug)}/download" download="${esc(pageSlug)}.ovlk"><i class="fa-solid fa-lock" aria-hidden="true"></i> Download the locked file (.ovlk)</a> — it opens only with its key, in the Locker.</p>` : ''}<footer class="paste-foot"><p class="muted small">Stored by <a href="${esc(config.mediaUrl)}" rel="noopener">OpenVibe.Media</a>, account by <a href="${NETWORK_URL}" rel="noopener">OpenVibe.Network</a>. Something wrong with this paste? <a href="${esc(config.liveUrl)}/dmca">Report it</a>.</p></footer>@@@ -513,7 +516,7 @@ ${offer}${p.burn_after_read ? '' : `<section class="section" id="comments">${locked ? '<p class="muted small">Comments are not locked: anyone who can open this paste can read them, and so can OpenVibe.</p>' : ''}<div class="section-head"><h2>Comments${comments && comments.thread.comment_count ? ` <span class="muted small">${num(comments.thread.comment_count)}</span>` : ''}</h2></div>-- ${require('./comments').commentPanel({ page: comments, user, base: `/p/${p.slug}`, action: `/p/${p.slug}/comments`, after: commentsAfter, sort: commentSort })}++ ${require('./comments').commentPanel({ page: comments, user, base: `/p/${pageSlug}`, action: `/p/${pageSlug}/comments`, after: commentsAfter, sort: commentSort })}</section>`}${!locked && related && related.length ? `<section class="section" id="related"><div class="section-head"><h2>More like this</h2><a class="more" href="${!isShot && p.language && p.language !== 'text' ? `/pastes?lang=${encodeURIComponent(p.language)}` : '/pastes'}">Browse <i class="fa-solid fa-arrow-right" aria-hidden="true"></i></a></div>@@@ -526,7 +529,7 @@ ${!locked && related && related.length ? `<section class="section" id="related">window.addEventListener('DOMContentLoaded', function () {var box = document.getElementById('paste-locked');if (!box || !window.OVLocker) return;-- OVLocker.open(box, { url: ${JSON.stringify(`/p/${p.slug}/raw`)}, link: location.href, title: ${JSON.stringify(p.title_shown ? title : '')}, onOpen: function (bytes) {++ OVLocker.open(box, { url: ${JSON.stringify(`/p/${pageSlug}/raw`)}, link: location.href, title: ${JSON.stringify(p.title_shown ? title : '')}, onOpen: function (bytes) {var text = new TextDecoder().decode(bytes), doc = null;try { doc = JSON.parse(text); } catch (e) { doc = null; }var shown = doc && typeof doc.content === 'string' ? doc.content : text;@@@ -571,7 +574,7 @@ window.addEventListener('DOMContentLoaded', function () {return renderPage({title: metaTitle,description: metaDescription,-- canonicalPath: `/p/${p.slug}`,++ canonicalPath: `/p/${pageSlug}`,agentLayer: true, // the public page has a Markdown twin (server/agent-pages.js)// A paste is link-shared content: reached by the link its author shares, not found by// search. Google's 2026-10-26 UGC guidance says to noindex those by default; the browse@@@ -588,7 +591,7 @@ window.addEventListener('DOMContentLoaded', function () {footerVariant: 'compact',jsonLd: lockedMeta ? [] : [ld.pasteLd(p, { description, image: shot || DEFAULT_OG_IMAGE }),-- ld.breadcrumbLd([{ name: 'Home', url: '/' }, { name: 'Pastes', url: '/pastes' }, { name: title, url: `/p/${p.slug}` }]),++ ld.breadcrumbLd([{ name: 'Home', url: '/' }, { name: 'Pastes', url: '/pastes' }, { name: title, url: `/p/${pageSlug}` }]),],body,});@@@ -103,7 +103,7 @@ function createSearchDocuments({ db }) {if (tags.length) facets.tags = tags;const doc = {owner: 'community', type: 'paste', id: pasteDocId(p), deleted: false, visibility: 'public',-- canonical_url: `${config.baseUrl}/p/${encodeURIComponent(p.slug)}`,++ canonical_url: `${config.baseUrl}/p/${encodeURIComponent(require('../pastes/url').canonicalSlug(p))}`,title: pasteTitle(p, shot),summary: clean(p.ai_summary, 4000) || clean(shot ? '' : p.content, 300) || `A ${shot ? 'screenshot' : 'paste'} on OpenVibe.Community.`,body: shot ? clean(p.ai_summary, 8000) : String(p.content || '').slice(0, 40000),@@@ -1,4 +1,5 @@'use strict';++const { canonicalSlug } = require('./pastes/url');/*** Sharing on Community pastes (design 2026-11-06, plan track Q; openvibe-sdk/share).@@@ -265,14 +266,14 @@ function describeRef(db) {if (!p) return {};const out = {title: !p.locked || p.title_shown ? (p.title || 'Untitled') : 'A locked paste',-- link: `${config.baseUrl}/p/${encodeURIComponent(p.slug)}`,++ link: `${config.baseUrl}/p/${encodeURIComponent(canonicalSlug(p))}`,roles: ['viewer'],audiences: AUDIENCES,transferable: true,};if (p.locked) {out.audiences = p.lock_kind === 'people' ? ['people', 'private'] : ['link', 'private'];-- out.envelope_url = `${config.baseUrl}/p/${encodeURIComponent(p.slug)}/raw`;++ out.envelope_url = `${config.baseUrl}/p/${encodeURIComponent(canonicalSlug(p))}/raw`;}return out;};@@@ -33,7 +33,7 @@ const comments = require('../server/comments/store');evs.forEach(ok);assert.ok(!JSON.stringify(evs).includes('secret'), 'no title or content in any event');assert.strictEqual(evs[0].visibility, 'public');-- assert.strictEqual(evs[0].payload.url, 'https://openvibe.community/p/amber-fox-42'.replace('https://openvibe.community', require('../server/config').baseUrl));++ assert.strictEqual(evs[0].payload.url, `${require('../server/config').baseUrl}/p/${p.public_id}`, 'the event carries the bare id, never the title slug');assert.deepStrictEqual(evs[1].payload.changed, ['content']);assert.strictEqual(evs[2].visibility, 'internal', 'an unlisted paste is not a public event');assert.strictEqual(evs[2].payload.url, null);@@@ -116,7 +116,8 @@ const { boot, check, done } = require('./helpers/app');});await check('"Discuss in a space" from a paste: the picker, then a new topic with the paste attached', async () => {-- const paste = (await call('/p/snip1')).text;++ const pasteUrl = await require('../server/pastes/store').canonicalForRef(t.db, 'snip1');++ const paste = (await call(`/p/${pasteUrl}`)).text;assert.ok(paste.includes('/forum/discuss?paste=snip1'));const picker = (await call('/forum/discuss?paste=snip1', { cookie: alexJwt })).text;assert.ok(picker.includes('/forum/general/new?paste=snip1') && !picker.includes('/forum/roadmap/new'));@@@ -48,12 +48,11 @@ const json = (body) => ({ method: 'POST', headers: { 'content-type': 'applicatioassert.deepStrictEqual(pings, []);});-- await check('a publish pings the paste path and the sitemap', async () => {++ await check('a publish never pings: the paste page is noindex', async () => {++ pings.length = 0;const r = await t.get('/api/pastes', json({ content: 'a published paste' }));assert.strictEqual(r.status, 201, r.text);-- const slug = r.json().slug;-- assert.ok(pings.includes(`https://openvibe.community/p/${slug}`), JSON.stringify(pings));-- assert.ok(pings.includes('https://openvibe.community/sitemap.xml'), JSON.stringify(pings));++ assert.deepStrictEqual(pings, [], 'IndexNow must not be told about a noindex page');});await check('an NSFW paste never pings', async () => {@@@ -67,15 +66,15 @@ const json = (body) => ({ method: 'POST', headers: { 'content-type': 'applicatioconst alex = t.network.addUser({ network_user_id: 7, username: 'alex', display_name: 'Alex' });const svc = (cap) => t.network.signService({ cap });-- await check('unpublishing a public paste pings it again', async () => {++ await check('publishing or unpublishing a paste never pings either', async () => {pings.length = 0;const created = await t.get('/api/pastes', { ...json({ content: 'mine' }), headers: { 'content-type': 'application/json', authorization: `Bearer ${svc(['community.paste.create'])}`, 'x-ov-subject': alex.subject_id } });assert.strictEqual(created.status, 201, created.text);-- const slug = created.json().slug;-- pings.length = 0;++ const slug = created.json().paste.canonical_slug;++ assert.deepStrictEqual(pings, [], 'creation is a noindex page');const updated = await t.get(`/api/pastes/${slug}`, { method: 'PUT', headers: { 'content-type': 'application/json', authorization: `Bearer ${svc(['community.paste.write'])}`, 'x-ov-subject': alex.subject_id }, body: JSON.stringify({ visibility: 'unlisted' }) });assert.strictEqual(updated.status, 200, updated.text);-- assert.ok(pings.includes(`https://openvibe.community/p/${slug}`), JSON.stringify(pings));++ assert.deepStrictEqual(pings, [], 'a visibility change on a noindex page pings nobody');});await t.close();@@@ -454,6 +454,15 @@ function statusCompatible(was, now) {return was === now;}++/**++ * A permanent move of an HTML page (200 → 301) is compatible: the old client's link still opens the same++ * content in a browser. It is how the pastes' title+id addresses replaced the old adjective-noun-number slugs.++ */++function movedPage(rec, got) {++ return rec && rec.method === 'GET' && rec.kind === 'html' && rec.status >= 200 && rec.status < 300++ && got.status === 301 && !!got.location;++}++// ── Replaying calls ──────────────────────────────────────────function kindOf(contentType, text) {@@@ -546,7 +555,7 @@ function callProblems(rec, got, probe = []) {const problems = [];if (got.status === 0) return [`no answer (${got.error})`];if (isNoRoute(got, probe)) return ['the route is gone (it answers as an unknown path)'];-- if (!statusCompatible(rec.status, got.status)) problems.push(`answered ${got.status}, N-1 answered ${rec.status}`);++ if (!statusCompatible(rec.status, got.status) && !movedPage(rec, got)) problems.push(`answered ${got.status}, N-1 answered ${rec.status}`);else if (rec.kind === 'json' && got.kind !== 'json' && rec.status < 500) problems.push(`answered ${got.kind}, N-1 answered JSON`);if (rec.location && got.location && rec.status >= 300 && rec.status < 400 && got.location.split('?')[0] !== rec.location.split('?')[0]) problems.push(`redirects to ${got.location}, N-1 to ${rec.location}`);if (rec.reads && got.json != null && got.status >= 200 && got.status < 300) problems.push(...readProblems(rec.reads, got.json));@@@ -40,12 +40,13 @@ const MIGRATION = path.join(__dirname, '..', 'migrations', '0004_paste_comments_const legacyRows = async () => Number((await t.db.prepare('SELECT COUNT(*) AS n FROM paste_comments').get()).n);const pub = await create({ content: 'public paste', title: 'Pub' });++ const pubUrl = await require('../server/pastes/store').canonicalForRef(t.db, pub);await check('the /p/<slug> form and POST /api/pastes/:slug/comments write the typed thread; /api/v1/comments reads both back', async () => {-- const viaForm = await call(`/p/${pub}/comments`, { method: 'POST', cookie: samJwt, form: { message: 'from the page form' } });++ const viaForm = await call(`/p/${pubUrl}/comments`, { method: 'POST', cookie: samJwt, form: { message: 'from the page form' } });assert.strictEqual(viaForm.status, 303, viaForm.text);const m = /^\/p\/([^#]+)#comment-(\d+)$/.exec(viaForm.headers.get('location'));-- assert.ok(m && m[1] === pub, viaForm.headers.get('location'));++ assert.ok(m && m[1] === pubUrl, viaForm.headers.get('location'));const viaApi = await call(`/api/pastes/${pub}/comments`, { method: 'POST', cookie: alexJwt, json: { message: 'from the paste API' } });assert.strictEqual(viaApi.status, 201, viaApi.text);assert.strictEqual(viaApi.json().comment.user_id, alex.subject_id);@@@ -73,30 +74,30 @@ const MIGRATION = path.join(__dirname, '..', 'migrations', '0004_paste_comments_assert.strictEqual(await legacyRows(), 0, 'nothing is written to paste_comments');// The paste page shows the same thread: comments with their anchors, sorting, the no-JS form.-- const pageHtml = await call(`/p/${pub}`, { cookie: samJwt });++ const pageHtml = await call(`/p/${pubUrl}`, { cookie: samJwt });assert.strictEqual(pageHtml.status, 200);for (const want of ['from the page form', 'from the paste API', 'from Live', `id="comment-${m[2]}"`, `id="comment-${viaApi.json().comment.id}"`,-- `action="/p/${pub}/comments"`, `href="/p/${pub}?sort=old"`]) assert.ok(pageHtml.text.includes(want), want);++ `action="/p/${pubUrl}/comments"`, `href="/p/${pubUrl}?sort=old"`]) assert.ok(pageHtml.text.includes(want), want);assert.ok(pageHtml.text.indexOf('from the paste API') < pageHtml.text.indexOf('from the page form'), 'newest first');-- const oldest = await call(`/p/${pub}?sort=old`);++ const oldest = await call(`/p/${pubUrl}?sort=old`);assert.ok(oldest.text.indexOf('from the page form') < oldest.text.indexOf('from the paste API'), '?sort=old: oldest first');-- assert.ok(oldest.text.includes(`/auth/login?next=${encodeURIComponent(`/p/${pub}`)}`) && !oldest.text.includes(`action="/p/${pub}/comments"`), 'signed out: a sign-in link, no form');-- const older = await call(`/p/${pub}?sort=new&after=${viaApi.json().comment.id}`);++ assert.ok(oldest.text.includes(`/auth/login?next=${encodeURIComponent(`/p/${pubUrl}`)}`) && !oldest.text.includes(`action="/p/${pubUrl}/comments"`), 'signed out: a sign-in link, no form');++ const older = await call(`/p/${pubUrl}?sort=new&after=${viaApi.json().comment.id}`);assert.ok(older.text.includes('from the page form') && !older.text.includes('from the paste API'), 'paged with ?after=');// Deleting a comment twice succeeds both times, as before.const del = await call(`/api/pastes/${pub}/comments`, { method: 'POST', cookie: samJwt, json: { message: 'short-lived' } });for (let i = 0; i < 2; i++) assert.strictEqual((await call(`/api/pastes/${pub}/comments/${del.json().comment.id}`, { method: 'DELETE', cookie: samJwt })).status, 200);-- assert.ok(!(await call(`/p/${pub}`)).text.includes('short-lived'));++ assert.ok(!(await call(`/p/${pubUrl}`)).text.includes('short-lived'));});await check('the form: signed in only, never from another site', async () => {-- const anon = await call(`/p/${pub}/comments`, { method: 'POST', form: { message: 'hi' } });++ const anon = await call(`/p/${pubUrl}/comments`, { method: 'POST', form: { message: 'hi' } });assert.strictEqual(anon.status, 303);-- assert.strictEqual(anon.headers.get('location'), `/auth/login?next=${encodeURIComponent(`/p/${pub}`)}`);-- const foreign = await call(`/p/${pub}/comments`, { method: 'POST', cookie: samJwt, headers: { origin: 'https://evil.example' }, form: { message: 'hi' } });++ assert.strictEqual(anon.headers.get('location'), `/auth/login?next=${encodeURIComponent(`/p/${pubUrl}`)}`);++ const foreign = await call(`/p/${pubUrl}/comments`, { method: 'POST', cookie: samJwt, headers: { origin: 'https://evil.example' }, form: { message: 'hi' } });assert.strictEqual(foreign.status, 403);-- const empty = await call(`/p/${pub}/comments`, { method: 'POST', cookie: samJwt, form: { message: ' ' } });++ const empty = await call(`/p/${pubUrl}/comments`, { method: 'POST', cookie: samJwt, form: { message: ' ' } });assert.strictEqual(empty.status, 400);assert.strictEqual((await call('/p/no-such-paste-here/comments', { method: 'POST', cookie: samJwt, form: { message: 'hi' } })).status, 404);});@@@ -68,15 +68,15 @@ const PNG = (() => {const as = (cookie, p) => t.get(p, { cookies: cookie ? [`ov_token=${cookie}`] : [] });try {-- let fineSlug;++ let fineSlug, finePage;await check('fine: the paste keeps its visibility and serves to everyone', async () => {state.category = 'fine';const r = await upload(alexJwt, { title: 'A receipt', visibility: 'public' });assert.strictEqual(r.status, 201, r.text);const p = r.json().paste;-- fineSlug = p.slug;++ fineSlug = p.slug; finePage = p.canonical_slug;assert.deepStrictEqual([p.review_category, p.review_status, p.visibility, p.is_nsfw], ['fine', 'clear', 'public', false]);-- assert.strictEqual((await as(null, `/p/${fineSlug}`)).status, 200);++ assert.strictEqual((await as(null, `/p/${finePage}`)).status, 200);assert.ok((await as(null, '/api/pastes')).json().pastes.some((x) => x.slug === fineSlug));});@@@ -135,7 +135,8 @@ const PNG = (() => {assert.deepStrictEqual([out.checked, out.cleared], [1, 1]);const row = await t.db.prepare('SELECT visibility, review_status, is_nsfw FROM pastes WHERE slug = ?').get(pendingSlug);assert.deepStrictEqual([row.visibility, row.review_status, Number(row.is_nsfw)], ['public', 'clear', 0]);-- assert.strictEqual((await as(null, `/p/${pendingSlug}`)).status, 200, 'the public page is back');++ const cleared = await require('../server/pastes/store').canonicalForRef(t.db, pendingSlug);++ assert.strictEqual((await as(null, `/p/${cleared}`)).status, 200, 'the public page is back');assert.ok((await as(null, '/api/pastes')).json().pastes.some((x) => x.slug === pendingSlug));});@@@ -162,7 +162,8 @@ const pastesApiModule = require('../server/pastes/api');const pub = await make(alexJwt, { title: 'Listed public paste', content: 'public text' });const link = await make(alexJwt, { title: 'Link only paste', content: 'link text', visibility: 'unlisted' });const people = await make(alexJwt, { title: 'People only paste', content: 'people text', audience: 'people', members: ['@maticus'] });-- assert.strictEqual((await call(`/p/${pub.slug}`)).status, 200);++ const pubUrl = pub.paste.canonical_slug;++ assert.strictEqual((await call(`/p/${pubUrl}`)).status, 200);assert.strictEqual((await call(`/p/${link.slug}`)).status, 200, 'the link is the key');assert.strictEqual((await call(`/api/pastes/${link.slug}`)).status, 200);assert.strictEqual((await call(`/p/${people.slug}`)).status, 404);@@@ -178,10 +179,10 @@ const pastesApiModule = require('../server/pastes/api');const llms = (await call('/llms-full.txt')).text;assert.ok(!llms.includes('People only paste'));const sitemap = (await call('/sitemap.xml')).text;-- assert.ok(!sitemap.includes(`/p/${pub.slug}`) && !sitemap.includes(`/p/${people.slug}`), 'paste pages are noindex and never in a sitemap');++ assert.ok(!sitemap.includes(`/p/${pubUrl}`) && !sitemap.includes(`/p/${people.slug}`), 'paste pages are noindex and never in a sitemap');// The agent twin is anonymous: a people paste has none.assert.strictEqual((await call(`/p/${people.slug}.md`)).status, 404, 'no Markdown twin of a restricted paste');-- assert.strictEqual((await call(`/p/${pub.slug}.md`)).status, 200);++ assert.strictEqual((await call(`/p/${pubUrl}.md`)).status, 200);});await check('the owner changes the audience; people drops it from the public list at once and the old column follows', async () => {@@@ -362,7 +363,7 @@ const pastesApiModule = require('../server/pastes/api');fd.append('screenshot', new Blob([jpeg], { type: 'image/jpeg' }), 'c.jpg');const made = await t.get('/api/pastes/screenshot', { method: 'POST', cookies: [`ov_token=${alexJwt}`], body: fd });assert.strictEqual(made.status, 201, made.text);-- const slug = made.json().slug;++ const slug = made.json().paste.canonical_slug;const home = await call('/');assert.strictEqual(home.status, 200);assert.match(home.text, new RegExp(`srcset="[^"]*/p/${slug}/screenshot\\?w=320&fmt=webp 320w, [^"]*/p/${slug}/screenshot\\?w=640&fmt=webp 640w"`), 'both card widths');nnew file mode 100644@@@ -0,0 +1,110 @@++'use strict';++/**++ * The public paste address (URL decisions 2026-11-07): /p/<title-slug>-<public id>. The id is the++ * address, the title slug decoration: the id alone and every old adjective-noun-number slug 301 to the++ * one canonical form, a retitle moves it, and a secret paste keeps its secret slug (its id is not a++ * second key). The page stays noindex and is never in the sitemap or IndexNow.++ */++const assert = require('assert');++const { boot, check, done } = require('./helpers/app');++++(async () => {++ const t = await boot({ pasteLimits: { cooldownSeconds: 0 } });++ const net = t.network;++ const alex = net.addUser({ network_user_id: 7, username: 'alex', display_name: 'Alex' });++ const svc = (cap) => net.signService({ cap });++ const create = (body) => t.get('/api/pastes', {++ method: 'POST',++ headers: { 'content-type': 'application/json', authorization: `Bearer ${svc(['community.paste.create'])}`, 'x-ov-subject': alex.subject_id },++ body: JSON.stringify(body),++ });++ const update = (slug, body) => t.get(`/api/pastes/${encodeURIComponent(slug)}`, {++ method: 'PUT',++ headers: { 'content-type': 'application/json', authorization: `Bearer ${svc(['community.paste.write'])}`, 'x-ov-subject': alex.subject_id },++ body: JSON.stringify(body),++ });++ const canonicalOf = (r) => r.json().paste.canonical_slug;++++ try {++ await check('a public paste is named by its title and an 8-char base58 id; the id alone 301s to it', async () => {++ const r = await create({ title: 'My first paste', content: 'hello' });++ assert.strictEqual(r.status, 201, r.text);++ const p = r.json().paste;++ assert.match(p.canonical_slug, /^my-first-paste-[1-9A-HJ-NP-Za-km-z]{8}$/);++ assert.strictEqual(r.json().url, `/p/${p.canonical_slug}`);++ assert.strictEqual(p.url, `/p/${p.canonical_slug}`);++ assert.strictEqual(p.raw_url, `/p/${p.canonical_slug}/raw`);++ const id = p.canonical_slug.split('-').pop();++ const byId = await t.get(`/p/${id}`);++ assert.deepStrictEqual([byId.status, byId.headers.get('location')], [301, `/p/${p.canonical_slug}`]);++ const old = await t.get(`/p/${p.slug}?from=old`);++ assert.deepStrictEqual([old.status, old.headers.get('location')], [301, `/p/${p.canonical_slug}?from=old`]);++ });++++ await check('the page, its Markdown twin and JSON-LD name one canonical; the page stays noindex', async () => {++ const p = (await create({ title: 'Canonical paste', content: 'body' })).json().paste;++ const page = await t.get(`/p/${p.canonical_slug}`);++ assert.strictEqual(page.status, 200);++ assert.ok(page.text.includes(`<link rel="canonical" href="https://openvibe.community/p/${p.canonical_slug}">`), 'canonical');++ assert.ok(page.text.includes('<meta name="robots" content="noindex">'), 'pastes stay noindex');++ assert.ok(page.text.includes(`"url":"https://openvibe.community/p/${p.canonical_slug}"`), 'JSON-LD URL');++ assert.ok(page.text.includes(`href="/p/${p.canonical_slug}/raw"`), 'Raw points at the canonical address');++ const twin = await t.get(`/p/${p.canonical_slug}.md`);++ assert.strictEqual(twin.status, 200, twin.text);++ assert.match(twin.headers.get('content-type'), /^text\/markdown/);++ assert.ok(twin.text.includes(`canonical: "https://openvibe.community/p/${p.canonical_slug}"`), 'the twin agrees');++ });++++ await check('a retitle moves the canonical; every old form 301s to the new one', async () => {++ const p = (await create({ title: 'Old name', content: 'body' })).json().paste;++ const oldCanonical = p.canonical_slug;++ const u = await update(p.slug, { title: 'A better name' });++ assert.strictEqual(u.status, 200, u.text);++ const next = canonicalOf(u);++ assert.match(next, /^a-better-name-[1-9A-HJ-NP-Za-km-z]{8}$/);++ assert.notStrictEqual(next, oldCanonical);++ for (const ref of [oldCanonical, p.slug, oldCanonical.split('-').pop()]) {++ const r = await t.get(`/p/${ref}?q=1`);++ assert.deepStrictEqual([r.status, r.headers.get('location')], [301, `/p/${next}?q=1`], ref);++ }++ assert.ok((await t.get('/p/' + next + '.md')).text.includes(`canonical: "https://openvibe.community/p/${next}"`));++ });++++ await check('an untitled paste takes its slug from the first content line, or has none', async () => {++ const p = (await create({ content: 'const answer = 42\nconsole.log(answer)' })).json().paste;++ assert.match(p.canonical_slug, /^const-answer-42-[1-9A-HJ-NP-Za-km-z]{8}$/);++ const bare = (await create({ content: '```\n+++---\n```' })).json().paste; // no ascii words on the first line++ assert.match(bare.canonical_slug, /^[1-9A-HJ-NP-Za-km-z]{8}$/, 'the id alone when there is no title');++ assert.strictEqual((await t.get(`/p/${bare.canonical_slug}`)).status, 200);++ });++++ await check('a secret paste keeps its secret slug; its public id is not a second key', async () => {++ const p = (await create({ title: 'A private note', content: 'secret', visibility: 'unlisted' })).json().paste;++ assert.match(p.slug, /^[a-z]+-[a-z]+-[A-Za-z0-9]{16}$/);++ assert.strictEqual(p.canonical_slug, p.slug, 'the secret part stays the address');++ assert.strictEqual((await t.get(`/p/${p.slug}`)).status, 200);++ assert.strictEqual((await t.get(`/p/${p.canonical_slug.split('-').pop()}`)).status, 404, 'the id alone does not open a secret paste');++ });++++ await check('a paste minted before the column gets one on first read, then 301s to it', async () => {++ const p = (await create({ title: 'Legacy paste', content: 'old row' })).json().paste;++ await t.db.prepare('UPDATE pastes SET public_id = NULL WHERE slug = ?').run(p.slug);++ const r = await t.get(`/p/${p.slug}`);++ assert.strictEqual(r.status, 301);++ const canonical = r.headers.get('location').slice('/p/'.length);++ assert.match(canonical, /^legacy-paste-[1-9A-HJ-NP-Za-km-z]{8}$/);++ assert.strictEqual((await t.get(`/p/${canonical}`)).status, 200);++ const row = await t.db.prepare('SELECT public_id FROM pastes WHERE slug = ?').get(p.slug);++ assert.strictEqual(row.public_id, canonical.split('-').pop(), 'stored once, not reminted');++ });++++ await check('the sitemap lists no paste pages (noindex content is never advertised)', async () => {++ const map = await t.get('/sitemap.xml');++ assert.strictEqual(map.status, 200);++ assert.ok(!/\/p\//.test(map.text), 'no /p/ row');++ });++ } finally {++ await t.close();++ }++ done();++})().catch((err) => { console.error(err); process.exit(1); });@@@ -42,7 +42,8 @@ const { boot, check, done } = require('./helpers/app');});assert.strictEqual(r.status, 201, r.text);const out = r.json();-- assert.ok(out.id && out.slug && out.url === `/p/${out.slug}` && out.paste);++ assert.ok(out.id && out.slug && out.url === `/p/${out.paste.canonical_slug}` && out.paste, 'the answer names the canonical title+id address');++ assert.match(out.paste.canonical_slug, /^[a-z0-9-]+-[1-9A-HJ-NP-Za-km-z]{8}$/);assert.notStrictEqual(out.slug, 'my-chosen-slug', 'browsers never pick slugs');assert.strictEqual(out.paste.owner_subject, alex.subject_id);assert.strictEqual(out.paste.origin, 'user');@@@ -236,7 +237,7 @@ const { boot, check, done } = require('./helpers/app');const r = await call('/api/pastes', { method: 'POST', ip: '203.0.113.90', body: fd });assert.strictEqual(r.status, 201, r.text);const out = r.json();-- assert.strictEqual(out.url, `/p/${out.slug}`);++ assert.strictEqual(out.url, `/p/${out.paste.canonical_slug}`);assert.notStrictEqual(out.paste.type, 'screenshot');assert.strictEqual(out.paste.title, 'Build log');assert.strictEqual(out.paste.content, 'npm ERR! code ERESOLVE');@@@ -250,7 +251,7 @@ const { boot, check, done } = require('./helpers/app');Buffer.from([0xff, 0xdb, 0x00, 0x04, 0x00, 0x00]),Buffer.from([0xff, 0xda, 0x00, 0x04, 0x00, 0x00]), Buffer.from('pixels', 'latin1'), Buffer.from([0xff, 0xd9]),]);-- let shotSlug;++ let shotSlug, shotUrl;await check('screenshot upload: metadata stripped, bytes stored in Media with a service token, URL + media_ref kept', async () => {const fd = new FormData();@@@ -274,7 +275,7 @@ const { boot, check, done } = require('./helpers/app');assert.ok(up.v2 && up.kind === 'screenshot' && up.visibility === 'private', 'a v2 Media object: private screenshot');assert.strictEqual(up.owner, alex.subject_id, 'owned by the person');// The object is private in Media, so the answer names the guarded address, never Media's /o/<id> (a 404 to a browser).-- assert.ok(/\/p\/[a-z0-9-]+\/screenshot$/.test(p.screenshot_url) && p.screenshot_url.endsWith(`/p/${p.slug}/screenshot`), p.screenshot_url);++ assert.ok(/\/p\/[A-Za-z0-9-]+\/screenshot$/.test(p.screenshot_url) && p.screenshot_url.endsWith(`/p/${p.canonical_slug}/screenshot`), p.screenshot_url);assert.ok(!p.screenshot_url.includes('/o/'), 'never the Media object URL');assert.strictEqual(p.title, 'My desk');assert.strictEqual(p.content, 'look');@@@ -283,7 +284,7 @@ const { boot, check, done } = require('./helpers/app');assert.strictEqual(meta.original_name, 'desk.jpg');const row = await t.db.prepare('SELECT media_ref FROM pastes WHERE slug = ?').get(p.slug);assert.match(row.media_ref, /^med_[0-9A-HJKMNP-TV-Z]{26}$/); assert.strictEqual(row.media_ref, up.key);-- shotSlug = p.slug;++ shotSlug = p.slug; shotUrl = p.canonical_slug;const viaCreate = new FormData();viaCreate.append('screenshot', new Blob([jpeg], { type: 'image/jpeg' }), 'b.jpg');const r2 = await call('/api/pastes', { method: 'POST', cookie: alexJwt, body: viaCreate });@@@ -340,7 +341,7 @@ const { boot, check, done } = require('./helpers/app');assert.strictEqual(shot.headers.get('content-type'), 'image/jpeg');assert.ok(shot.text.includes('pixels'), 'the proxy serves the bytes');assert.strictEqual(shot.headers.get('location'), null, 'never a Media URL');-- assert.strictEqual((await call(`/p/${shotSlug}/raw`)).headers.get('location'), `/p/${shotSlug}/screenshot`);++ assert.strictEqual((await call(`/p/${shotSlug}/raw`)).headers.get('location'), `/p/${shotUrl}/screenshot`);assert.strictEqual((await call(`/p/${alexSlug}/screenshot`)).status, 404);const burn = (await call('/api/pastes', { method: 'POST', cookie: alexJwt, json: { content: 'once', burn_after_read: true } })).json().slug;assert.strictEqual((await call(`/p/${burn}/raw`)).text, 'once');@@@ -73,7 +73,7 @@ const { createSearchDocuments } = require('../server/search/documents');assert.strictEqual(await docs.publishPaste(await row(nsfw.id)), 'sent');assert.strictEqual(await docs.publishPaste(await row(burn.id)), 'unchanged');const [a, b] = (await index()).slice(-2); ok(a); ok(b);-- assert.strictEqual(a.payload.id, 'amber-fox-42'); assert.strictEqual(a.payload.canonical_url, `${base}/p/amber-fox-42`);++ assert.strictEqual(a.payload.id, 'amber-fox-42'); assert.strictEqual(a.payload.canonical_url, `${base}/p/quick-sort-${p.public_id}`);assert.ok(a.payload.body.includes('function sort')); assert.strictEqual(a.payload.facets.syntax, 'javascript');assert.ok(!('language' in a.payload), 'a paste\'s syntax is not a human language');assert.deepStrictEqual(b.payload.indexability, { decision: 'noindex', reasons: ['sensitive'] });@@@ -113,7 +113,7 @@ const { createSearchDocuments } = require('../server/search/documents');await db.prepare("INSERT INTO search_doc_pushes (type, id, hash, revision) VALUES ('paste', '-wgXuYn0', 'x', 1)").run();assert.strictEqual(await docs.publishPaste(await db.prepare('SELECT * FROM pastes WHERE id = ?').get(p.id)), 'sent');const ev = (await index()).at(-1); ok(ev);-- assert.strictEqual(ev.payload.id, `paste_${p.id}`); assert.strictEqual(ev.payload.canonical_url, `${base}/p/-wgXuYn0`);++ assert.strictEqual(ev.payload.id, `paste_${p.id}`); assert.strictEqual(ev.payload.canonical_url, `${base}/p/dash-${p.public_id}`);const r = contracts.validate('search.index-document@1', ev.payload); assert.ok(r.valid, JSON.stringify(r.errors));const before = (await index()).length;await docs.refresh();@@@ -118,7 +118,7 @@ function inventory() {assert.strictEqual((await send('GET', `/forum/general/${slug}`)).status, 200);const p = await send('POST', '/api/pastes', { json: { title: urls[5], content: `curl ${urls[5]}\n` } });assert.strictEqual(p.status, 201, p.text);-- assert.strictEqual((await send('GET', `/p/${JSON.parse(p.text).slug}`)).status, 200);++ assert.strictEqual((await send('GET', JSON.parse(p.text).url)).status, 200);await send('POST', `/api/pastes/${JSON.parse(p.text).slug}/comments`, { json: { message: `look ${urls[4]}` } });const f = new FormData();f.append('page_url', urls[6]);@@@ -15,6 +15,8 @@ const { boot, check, done } = require('./helpers/app');await ins.run('fair-moon-23', sam.subject_id, 'screenshot', 'Desktop shot', 'my desktop', 'text', 'public', 'https://openvibe.media/o/med_FAIRMOON', 40, '2026-09-11 08:00:00', '2026-09-11 08:00:00');await ins.run('dark-owl-55', alex.subject_id, 'paste', 'secret notes', 'unlisted', 'text', 'unlisted', null, 2, '2026-09-12 08:00:00', '2026-09-12 08:00:00');await ins.run('grim-vale-61', alex.subject_id, 'paste', 'private thing', 'private', 'text', 'private', null, 0, '2026-09-10 08:00:00', '2026-09-10 08:00:00');++ const canon = async (slug) => await require('../server/pastes/store').canonicalForRef(t.db, slug);++ const amber = await canon('amber-fox-42'), shot = await canon('fair-moon-23');await check('robots.txt allows crawling, blocks the private routes, points at the sitemap', async () => {const r = await t.get('/robots.txt');@@@ -58,7 +60,7 @@ const { boot, check, done } = require('./helpers/app');assert.strictEqual(full.status, 200);assert.ok(full.headers.get('content-type').startsWith('text/plain'));assert.strictEqual(full.headers.get('cache-control'), cache.htmlHeaders({ maxAge: 3600 }));-- assert.ok(full.text.includes('### Hello world in JavaScript\n\nURL: https://openvibe.community/p/amber-fox-42\n\nconst x = 1;'));++ assert.ok(full.text.includes(`### Hello world in JavaScript\n\nURL: https://openvibe.community/p/${amber}\n\nconst x = 1;`));assert.ok(!full.text.includes('dark-owl-55') && !full.text.includes('grim-vale-61') && !full.text.includes('secret notes'));// The catalog keeps a short preview for a few seconds. Discovery must read the@@@ -100,12 +102,12 @@ const { boot, check, done } = require('./helpers/app');});await check('a noindex page says so in the meta tag and in X-Robots-Tag; an indexable page sends no such header', async () => {-- for (const path of ['/p/amber-fox-42', '/p/fair-moon-23', '/c/cth_0000000000000000000000', '/nope-not-a-page']) {++ for (const path of [`/p/${amber}`, `/p/${shot}`, '/c/cth_0000000000000000000000', '/nope-not-a-page']) {const r = await t.get(path);assert.ok(r.headers.get('x-robots-tag') && /(?:^|,)\s*noindex/.test(r.headers.get('x-robots-tag')),`${path}: X-Robots-Tag (got ${r.headers.get('x-robots-tag')})`);}-- assert.strictEqual((await t.get('/p/amber-fox-42')).headers.get('x-robots-tag'), 'noindex');++ assert.strictEqual((await t.get(`/p/${amber}`)).headers.get('x-robots-tag'), 'noindex');for (const path of ['/', '/pastes', '/updates']) {assert.strictEqual((await t.get(path)).headers.get('x-robots-tag'), null, path);}@@@ -115,7 +117,7 @@ const { boot, check, done } = require('./helpers/app');const r = await t.get('/feed.xml');assert.strictEqual(r.status, 200);assert.ok(r.text.includes('<rss version="2.0"'));-- assert.ok(r.text.includes('<link>https://openvibe.community/p/amber-fox-42</link>'));++ assert.ok(r.text.includes(`<link>https://openvibe.community/p/${amber}</link>`));assert.ok(r.text.includes('<atom:link href="https://openvibe.community/feed.xml" rel="self" type="application/rss+xml"/>'));assert.ok(r.text.includes('<author>Alex</author>'));assert.ok(r.text.includes('<pubDate>Tue, 15 Sep 2026 10:00:00 GMT</pubDate>'));@@@ -124,11 +126,11 @@ const { boot, check, done } = require('./helpers/app');});await check('a page head has the canonical, Open Graph, Twitter and ai-summary tags from openvibe-shared/seo', async () => {-- const r = await t.get('/p/amber-fox-42');++ const r = await t.get(`/p/${amber}`);assert.strictEqual(r.status, 200);const head = r.text.split('</head>')[0];-- assert.ok(head.includes('<link rel="canonical" href="https://openvibe.community/p/amber-fox-42">'));-- assert.ok(head.includes('<meta property="og:url" content="https://openvibe.community/p/amber-fox-42">'));++ assert.ok(head.includes(`<link rel="canonical" href="https://openvibe.community/p/${amber}">`));++ assert.ok(head.includes(`<meta property="og:url" content="https://openvibe.community/p/${amber}">`));assert.ok(head.includes('<meta property="og:title" content="Hello world in JavaScript · OpenVibe.Community">'));assert.ok(head.includes('<meta property="og:site_name" content="OpenVibe.Community">'));assert.match(head, /<meta property="og:description" content="[^"]+">/);@@@ -156,13 +158,13 @@ const { boot, check, done } = require('./helpers/app');});await check('HTML pages carry the private page policy; raw and download never reach a shared cache', async () => {-- for (const path of ['/', '/pastes', '/p/amber-fox-42', '/updates', '/nope-not-a-page']) {++ for (const path of ['/', '/pastes', `/p/${amber}`, '/updates', '/nope-not-a-page']) {const r = await t.get(path);assert.ok(r.headers.get('content-type').startsWith('text/html'), path);assert.strictEqual(r.headers.get('cache-control'), cache.htmlHeaders({ private: true }), path);}-- assert.strictEqual((await t.get('/p/amber-fox-42/raw')).headers.get('cache-control'), 'private, no-store');-- assert.strictEqual((await t.get('/p/amber-fox-42/download')).headers.get('cache-control'), 'private, no-store');++ assert.strictEqual((await t.get(`/p/${amber}/raw`)).headers.get('cache-control'), 'private, no-store');++ assert.strictEqual((await t.get(`/p/${amber}/download`)).headers.get('cache-control'), 'private, no-store');});await check('JSON-LD builders (server/render/jsonld.js): absolute breadcrumbs, a paste Article, AI never a person', async () => {@@@ -15,29 +15,33 @@ const { boot, check, done } = require('./helpers/app');const asAlex = { kind: 'user', subject: alex.subject_id, staff: false, origin: 'user' };const anon = { kind: 'anonymous', subject: null, staff: false, origin: 'user' };-- const js = (await svc.createText(asAlex, { title: 'Hello world in JavaScript', content: 'const greet = (name) => `Hi ${name}`;\n', language: 'javascript' })).slug;-- const nginx = (await svc.createText(anon, { title: 'nginx snippet', content: 'server {\n listen 80;\n}\n', language: 'nginx' })).slug;-- const py = (await svc.createText(asAlex, { title: 'python helper', content: 'def add(a, b):\n return a + b\n', language: 'python' })).slug;++ const jsP = await svc.createText(asAlex, { title: 'Hello world in JavaScript', content: 'const greet = (name) => `Hi ${name}`;\n', language: 'javascript' });++ const nginxP = await svc.createText(anon, { title: 'nginx snippet', content: 'server {\n listen 80;\n}\n', language: 'nginx' });++ const pyP = await svc.createText(asAlex, { title: 'python helper', content: 'def add(a, b):\n return a + b\n', language: 'python' });const unl = (await svc.createText(asAlex, { title: 'secret notes', content: 'unlisted <script>alert(1)</script>', visibility: 'unlisted' })).slug;const priv = (await svc.createText(asAlex, { title: 'private thing', content: 'private', visibility: 'private' })).slug;const burn = (await svc.createText(asAlex, { title: 'burn me', content: 'once only', burn_after_read: true })).slug;++ const js = jsP.slug, jsUrl = jsP.paste.canonical_slug;++ const nginx = nginxP.slug, nginxUrl = nginxP.paste.canonical_slug;++ const py = pyP.slug, pyUrl = pyP.paste.canonical_slug;await t.db.prepare('UPDATE pastes SET views = 900 WHERE slug = ?').run(nginx);await t.db.prepare("INSERT INTO pastes (slug, owner_subject, type, title, content, screenshot_url) VALUES ('fair-moon-23', ?, 'screenshot', 'Desktop shot', 'my desktop', 'https://openvibe.media/f/abc-desk.png')").run(alex.subject_id);++ const shotUrl = await require('../server/pastes/store').canonicalForRef(t.db, 'fair-moon-23');await check('home: latest + most viewed come from the store; nothing hidden; Live is never asked', async () => {const r = await t.get('/');assert.strictEqual(r.status, 200);-- has(r.text, `href="/p/${js}"`);++ has(r.text, `href="/p/${jsUrl}"`);// The most viewed are the home's quick filter (/pastes?sort=views), read from the same store.has(r.text, 'href="/pastes?sort=views"');const views = await t.get('/pastes?sort=views');-- assert.ok(views.text.indexOf(nginx) > 0 && views.text.indexOf(nginx) < views.text.indexOf(js), 'most viewed first');++ assert.ok(views.text.indexOf(nginxUrl) > 0 && views.text.indexOf(nginxUrl) < views.text.indexOf(jsUrl), 'most viewed first');assert.ok(![r.text, views.text].some((html) => html.includes(unl) || html.includes(priv)));assert.strictEqual(t.live.calls.filter((c) => c.path.startsWith('/api/pastes')).length, 0);});await check('paste page: rendered from the store, view counted, author from the projection', async () => {-- const r = await t.get(`/p/${js}`, { headers: { 'x-forwarded-for': '198.51.100.20' } });++ const r = await t.get(`/p/${jsUrl}`, { headers: { 'x-forwarded-for': '198.51.100.20' } });assert.strictEqual(r.status, 200);has(r.text, '<title>Hello world in JavaScript · OpenVibe.Community</title>');has(r.text, 'class="hljs-keyword">const</span>');@@@ -72,54 +76,54 @@ const { boot, check, done } = require('./helpers/app');await check('download reads the store without a view; screenshots use the stored Media URL', async () => {const before = (await t.db.prepare('SELECT views FROM pastes WHERE slug = ?').get(py)).views;-- const dl = await t.get(`/p/${py}/download`);++ const dl = await t.get(`/p/${pyUrl}/download`);assert.strictEqual(dl.status, 200);-- assert.strictEqual(dl.headers.get('content-disposition'), `attachment; filename="${py}.py"`);++ assert.strictEqual(dl.headers.get('content-disposition'), `attachment; filename="${pyUrl}.py"`);assert.ok(dl.text.startsWith('def add'));assert.strictEqual((await t.db.prepare('SELECT views FROM pastes WHERE slug = ?').get(py)).views, before);-- const shot = await t.get('/p/fair-moon-23');-- assert.match(shot.text, /<meta property="og:image" content="[^"]*\/p\/fair-moon-23\/screenshot">/);-- const dls = await t.get('/p/fair-moon-23/download');-- assert.strictEqual(dls.headers.get('location'), '/p/fair-moon-23/screenshot');++ const shot = await t.get(`/p/${shotUrl}`);++ has(shot.text, `<meta property="og:image" content="https://openvibe.community/p/${shotUrl}/screenshot">`);++ const dls = await t.get(`/p/${shotUrl}/download`);++ assert.strictEqual(dls.headers.get('location'), `/p/${shotUrl}/screenshot`);});await check('browse: search, views sort and language filter over the store', async () => {const q = await t.get('/pastes?q=nginx');-- has(q.text, `href="/p/${nginx}"`);-- assert.ok(!q.text.includes(`href="/p/${js}"`));++ has(q.text, `href="/p/${nginxUrl}"`);++ assert.ok(!q.text.includes(`href="/p/${jsUrl}"`));const views = await t.get('/pastes?sort=views');const res = views.text.slice(views.text.indexOf('id="results"'));-- assert.ok(res.indexOf(nginx) < res.indexOf(js));++ assert.ok(res.indexOf(nginxUrl) < res.indexOf(jsUrl));const lang = await t.get('/pastes?lang=python');-- has(lang.text, `href="/p/${py}"`);-- assert.ok(!lang.text.includes(`href="/p/${js}"`));++ has(lang.text, `href="/p/${pyUrl}"`);++ assert.ok(!lang.text.includes(`href="/p/${jsUrl}"`));});await check('a Markdown paste reads as a document, its source a click away; code pastes are unchanged', async () => {-- const doc = (await svc.createText(anon, { title: 'Notes', content: '# Notes!\n\n### Part\n\n| a | b |\n|---|--:|\n| 1 | 2 |\n\n- x\n - y\n\n<script>alert(1)</script>', language: 'markdown' })).slug;-- const r = await t.get(`/p/${doc}`);++ const docP = await svc.createText(anon, { title: 'Notes', content: '# Notes!\n\n### Part\n\n| a | b |\n|---|--:|\n| 1 | 2 |\n\n- x\n - y\n\n<script>alert(1)</script>', language: 'markdown' });++ const r = await t.get(`/p/${docP.paste.canonical_slug}`);assert.strictEqual(r.status, 200);// Its opening # only repeats the title (the page's h1), so it is left out and ### leads at h2.has(r.text, '<div class="md paste-doc"><h2>Part</h2>');-- const other = (await svc.createText(anon, { title: 'Release', content: '# Something else\n\ntext', language: 'markdown' })).slug;-- has((await t.get(`/p/${other}`)).text, '<div class="md paste-doc"><h2>Something else</h2>');++ const otherP = await svc.createText(anon, { title: 'Release', content: '# Something else\n\ntext', language: 'markdown' });++ has((await t.get(`/p/${otherP.paste.canonical_slug}`)).text, '<div class="md paste-doc"><h2>Something else</h2>');has(r.text, '<td class="al-r">2</td>');has(r.text, '<li>x<ul><li>y</li></ul></li>');has(r.text, '<details class="paste-source">');has(r.text, '<div class="code-wrap wrap" data-code tabindex="0" role="region" aria-label="Markdown source">');assert.ok(!r.text.includes('<script>alert(1)</script>'), 'raw HTML stays text');-- const code = await t.get(`/p/${js}`);++ const code = await t.get(`/p/${jsUrl}`);assert.ok(!code.text.includes('paste-doc') && code.text.includes('<div class="code-wrap" data-code'), 'code: no document, no default wrap');});await check('a link that picked up trailing punctuation (**, a full stop, a bracket) redirects to the paste', async () => {for (const [path, to] of [-- [`/p/${js}**`, `/p/${js}`],-- [`/p/${js}.`, `/p/${js}`],-- [`/p/${js})`, `/p/${js}`],-- [`/p/${js}%2A%2A`, `/p/${js}`],-- [`/p/${js}),/raw?dl=1`, `/p/${js}/raw?dl=1`],-- [`/p/${js}./download`, `/p/${js}/download`],++ [`/p/${jsUrl}**`, `/p/${jsUrl}`],++ [`/p/${jsUrl}.`, `/p/${jsUrl}`],++ [`/p/${jsUrl})`, `/p/${jsUrl}`],++ [`/p/${jsUrl}%2A%2A`, `/p/${jsUrl}`],++ [`/p/${jsUrl}),/raw?dl=1`, `/p/${jsUrl}/raw?dl=1`],++ [`/p/${jsUrl}./download`, `/p/${jsUrl}/download`],]) {const r = await t.get(path);assert.strictEqual(r.status, 301, path);@@@ -127,15 +131,16 @@ const { boot, check, done } = require('./helpers/app');}// Nothing left to point at, or junk in the middle: still "Paste not found".assert.strictEqual((await t.get('/p/**')).status, 404);-- assert.strictEqual((await t.get(`/p/${js.slice(0, 4)}*${js.slice(4)}`)).status, 404);-- assert.strictEqual((await t.get(`/p/${js}`)).status, 200);++ // The slug part is decoration: a corrupt middle still resolves by the id at the end.++ assert.deepStrictEqual([(await t.get(`/p/${jsUrl.slice(0, 4)}*${jsUrl.slice(4)}`)).status, (await t.get(`/p/${jsUrl.slice(0, 4)}*${jsUrl.slice(4)}`)).headers.get('location')], [301, `/p/${jsUrl}`]);++ assert.strictEqual((await t.get(`/p/${jsUrl}`)).status, 200);});await check('/my lists the signed-in person\'s pastes, private and unlisted included', async () => {const mine = await t.get('/my', { cookies: [`ov_token=${token}`] });assert.strictEqual(mine.status, 200);-- for (const slug of [js, py, unl, priv]) has(mine.text, `href="/p/${slug}"`);-- assert.ok(!mine.text.includes(nginx));++ for (const slug of [jsUrl, pyUrl, unl, priv]) has(mine.text, `href="/p/${slug}"`);++ assert.ok(!mine.text.includes(nginxUrl));});await check('no-JS form creates in the store as the signed-in subject; anonymous posts share the 20/10min budget', async () => {@@@ -160,7 +165,7 @@ const { boot, check, done } = require('./helpers/app');await check('the feed links a paste, but no paste is in the sitemap (they are link-shared, noindex)', async () => {const feed = await t.get('/feed.xml');-- has(feed.text, `/p/${js}`);++ has(feed.text, `/p/${jsUrl}`);assert.ok(!feed.text.includes(priv));const map = await t.get('/sitemap.xml');assert.ok(!map.text.includes(`/p/${js}`) && !map.text.includes(`/p/${unl}`) && !map.text.includes(`/p/${priv}`));@@@ -18,6 +18,12 @@ const { boot, check, done } = require('./helpers/app');await ins.run('dark-owl-55', alex.subject_id, 'paste', 'secret notes', 'unlisted <script>alert(1)</script>', 'text', 'unlisted', null, 2, 0, '2026-09-12 08:00:00', '2026-09-12 08:00:00');await ins.run('fair-moon-23', sam.subject_id, 'screenshot', 'Desktop shot', 'my desktop', 'text', 'public', 'https://openvibe.media/o/med_FAIRMOON', 40, 0, '2026-09-11 08:00:00', '2026-09-11 08:00:00');await ins.run('grim-vale-61', alex.subject_id, 'paste', 'private thing', 'private', 'text', 'private', null, 0, 0, '2026-09-10 08:00:00', '2026-09-10 08:00:00');++ // The canonical title+id address each public fixture is served at (the old slugs 301 there).++ const canon = async (slug) => await require('../server/pastes/store').canonicalForRef(t.db, slug);++ const amberSlug = await canon('amber-fox-42');++ const blueSlug = await canon('blue-lake-17');++ const shotSlug = await canon('fair-moon-23');++ const coldSlug = await canon('cold-ridge-88');await check('home renders hero, the latest pastes as a rail, the forum, roadmap and questions with full SEO head', async () => {const r = await t.get('/');@@@ -45,13 +51,13 @@ const { boot, check, done } = require('./helpers/app');has(r.text, '<section class="ovk-rail ovk-rail--m" id="latest" aria-labelledby="latest-h">');has(r.text, '<h2 id="latest-h">Latest pastes</h2>');has(r.text, '<a class="ovk-rail-more" href="/pastes">See all');-- assert.ok(/<article class="ovk-card ovk-card--post"[^>]*>[\s\S]*?href="\/p\/amber-fox-42"/.test(r.text), 'a paste is a kit card linking its page');++ assert.ok(r.text.includes(`href="/p/${amberSlug}"`) && /<article class="ovk-card ovk-card--post"/.test(r.text), 'a paste is a kit card linking its page');has(r.text, 'href="/pastes?sort=views"', 'the most viewed are a quick filter');has(r.text, '<details class="ovk-fold" id="api">');assert.ok(!/<h2>Most viewed<\/h2>|id="trending"|id="spaces"|class="sc-cta"|class="sc-sec sc-network"/.test(r.text), 'no shelf repeats another (most viewed, What lives here, the closing call, Part of OpenVibe)');// Most viewed (/pastes?sort=views) puts the 900-view nginx paste first.const views = await t.get('/pastes?sort=views');-- assert.ok(views.text.indexOf('blue-lake-17') > 0 && views.text.indexOf('blue-lake-17') < views.text.indexOf('amber-fox-42'), 'most viewed first');++ assert.ok(views.text.indexOf(blueSlug) > 0 && views.text.indexOf(blueSlug) < views.text.indexOf(amberSlug), 'most viewed first');// Unlisted/private fixtures never show on shared pages.assert.ok(![r.text, views.text].some((html) => html.includes('dark-owl-55') || html.includes('grim-vale-61')));// Navbar/footer init payload matches the shared-chrome contract.@@@ -104,7 +110,7 @@ const { boot, check, done } = require('./helpers/app');});await check('every page carries the boost marker and script, and the navbar signs in back to the current page', async () => {-- for (const url of ['/', '/pastes', '/new', '/p/amber-fox-42']) {++ for (const url of ['/', '/pastes', '/new', `/p/${amberSlug}`]) {const r = await t.get(url);assert.strictEqual(r.status, 200, url);assert.match(r.text, /<meta name="ov-boost" content="community@[^"]+">/, `${url}: the boost marker`);@@@ -116,10 +122,10 @@ const { boot, check, done } = require('./helpers/app');await check('paste page: SEO head, Article JSON-LD with author/datePublished, highlighted body, actions, related', async () => {const before = (await t.db.prepare('SELECT views FROM pastes WHERE slug = ?').get('amber-fox-42')).views;-- const r = await t.get('/p/amber-fox-42');++ const r = await t.get(`/p/${amberSlug}`);assert.strictEqual(r.status, 200);has(r.text, '<title>Hello world in JavaScript · OpenVibe.Community</title>');-- has(r.text, '<link rel="canonical" href="https://openvibe.community/p/amber-fox-42">');++ has(r.text, `<link rel="canonical" href="https://openvibe.community/p/${amberSlug}">`);has(r.text, '<meta name="robots" content="noindex">');has(r.text, '<meta property="og:type" content="article">');has(r.text, 'article:published_time');@@@ -131,8 +137,8 @@ const { boot, check, done } = require('./helpers/app');assert.strictEqual(article.hasPart.programmingLanguage, 'javascript');assert.ok(ld.some((o) => o['@type'] === 'BreadcrumbList'));has(r.text, 'class="hljs-keyword">const</span>', 'server-side highlighted');-- has(r.text, 'href="/p/amber-fox-42/raw"');-- has(r.text, 'href="/p/amber-fox-42/download"');++ has(r.text, `href="/p/${amberSlug}/raw"`);++ has(r.text, `href="/p/${amberSlug}/download"`);has(r.text, 'data-copy-content="amber-fox-42"');has(r.text, 'data-ov-share-sheet', 'the one Share sheet (sharing pilot, 2026-11-06)');assert.ok(!r.text.includes('twitter.com/intent/tweet'), 'no hand-made X link any more');@@@ -155,11 +161,11 @@ const { boot, check, done } = require('./helpers/app');});await check('screenshot paste: og:image and the picture go through the checked proxy, never the Media URL', async () => {-- const r = await t.get('/p/fair-moon-23');-- assert.match(r.text, /<meta property="og:image" content="[^"]*\/p\/fair-moon-23\/screenshot">/, 'og:image is the proxy');++ const r = await t.get(`/p/${shotSlug}`);++ assert.match(r.text, /<meta property="og:image" content="[^"]*\/p\/(?:desktop-shot-)?[A-Za-z0-9-]+\/screenshot">/, 'og:image is the proxy');has(r.text, '<meta name="twitter:card" content="summary_large_image">');has(r.text, '"@type":"ImageObject"');-- has(r.text, '<img src="/p/fair-moon-23/screenshot"');++ has(r.text, `<img src="/p/${shotSlug}/screenshot"`);assert.ok(!r.text.includes('openvibe.media/o/med_FAIRMOON'), 'the Media URL never reaches the HTML (sharing pilot)');});@@@ -174,16 +180,16 @@ const { boot, check, done } = require('./helpers/app');});await check('raw serves the text from the store; the screenshot goes through the proxy; download is an attachment without a view', async () => {-- const raw = await t.get('/p/amber-fox-42/raw');++ const raw = await t.get(`/p/${amberSlug}/raw`);assert.strictEqual(raw.status, 200);assert.ok(raw.text.startsWith('const greet'));-- const shot = await t.get('/p/fair-moon-23/screenshot');++ const shot = await t.get(`/p/${shotSlug}/screenshot`);assert.notStrictEqual(shot.status, 302, 'the screenshot is streamed by the checked proxy, not a redirect to Media');assert.ok(!String(shot.headers.get('location') || '').includes('openvibe.media'), 'no Media URL in a redirect');const before = (await t.db.prepare('SELECT views FROM pastes WHERE slug = ?').get('amber-fox-42')).views;-- const dl = await t.get('/p/amber-fox-42/download');++ const dl = await t.get(`/p/${amberSlug}/download`);assert.strictEqual(dl.status, 200);-- assert.strictEqual(dl.headers.get('content-disposition'), 'attachment; filename="amber-fox-42.js"');++ assert.strictEqual(dl.headers.get('content-disposition'), `attachment; filename="${amberSlug}.js"`);assert.ok(dl.text.startsWith('const greet'));assert.strictEqual((await t.db.prepare('SELECT views FROM pastes WHERE slug = ?').get('amber-fox-42')).views, before, 'download does not count a view');});@@@ -192,17 +198,17 @@ const { boot, check, done } = require('./helpers/app');const all = await t.get('/pastes');assert.strictEqual(all.status, 200);has(all.text, '<link rel="canonical" href="https://openvibe.community/pastes">');-- has(all.text, 'href="/p/blue-lake-17"');++ has(all.text, `href="/p/${blueSlug}"`);const q = await t.get('/pastes?q=nginx');has(q.text, '<meta name="robots" content="noindex,follow">');-- has(q.text, 'href="/p/blue-lake-17"');-- assert.ok(!q.text.includes('href="/p/amber-fox-42"'));++ has(q.text, `href="/p/${blueSlug}"`);++ assert.ok(!q.text.includes(`href="/p/${amberSlug}"`));const views = await t.get('/pastes?sort=views');const res = views.text.slice(views.text.indexOf('id="results"'));-- assert.ok(res.indexOf('blue-lake-17') < res.indexOf('amber-fox-42') && res.indexOf('amber-fox-42') < res.indexOf('cold-ridge-88'));++ assert.ok(res.indexOf(blueSlug) < res.indexOf(amberSlug) && res.indexOf(amberSlug) < res.indexOf(coldSlug));const py = await t.get('/pastes?lang=python');-- has(py.text, 'href="/p/cold-ridge-88"');-- assert.ok(!py.text.includes('href="/p/amber-fox-42"'));++ has(py.text, `href="/p/${coldSlug}"`);++ assert.ok(!py.text.includes(`href="/p/${amberSlug}"`));has(py.text, '<option value="python" selected>');const old = await t.get('/pastes/amber-fox-42');assert.strictEqual(old.status, 301);@@@ -216,8 +222,8 @@ const { boot, check, done } = require('./helpers/app');const mine = await t.get('/my', { cookies: [`ov_token=${token}`] });assert.strictEqual(mine.status, 200);has(mine.text, '<meta name="robots" content="noindex,nofollow">');-- for (const slug of ['amber-fox-42', 'cold-ridge-88', 'dark-owl-55', 'grim-vale-61']) has(mine.text, `href="/p/${slug}"`);-- assert.ok(!mine.text.includes('blue-lake-17'));++ for (const slug of [amberSlug, coldSlug, 'dark-owl-55', 'grim-vale-61']) has(mine.text, `href="/p/${slug}"`);++ assert.ok(!mine.text.includes(blueSlug));});await check('/new renders the form (private option only when signed in) and the no-JS post creates in the store', async () => {@@@ -37,7 +37,7 @@ async function rejects(p, status, message) {const out = await svc.createText(alice, { title: ' Hello ', content: 'const x = 1;\n ', language: 'auto', visibility: 'public' });assert.match(out.slug, /^[a-z]+-[a-z]+-\d{2}$/);assert.deepStrictEqual(Object.keys(out), ['id', 'slug', 'url', 'paste']);-- assert.strictEqual(out.url, `/p/${out.slug}`);++ assert.strictEqual(out.url, `/p/${out.paste.canonical_slug}`);const p = out.paste;assert.strictEqual(p.title, 'Hello');assert.strictEqual(p.content, 'const x = 1;');@@@ -49,7 +49,7 @@ async function rejects(p, status, message) {assert.strictEqual(p.display_name, 'Alice');assert.strictEqual(p.avatar_url, 'https://openvibe.media/a.png');assert.strictEqual(p.profile_color, '#f0a');-- assert.strictEqual(p.raw_url, `/p/${out.slug}/raw`);++ assert.strictEqual(p.raw_url, `/p/${p.canonical_slug}/raw`);assert.strictEqual(p.is_owner, true);for (const k of ['views', 'copies', 'likes', 'unique_views']) assert.strictEqual(p[k], 0);pub = out.slug;
@@ -115,9 +115,15 @@ Moments feeds read it.
@@ -115,9 +115,15 @@ Moments feeds read it.
`fine`. Text pastes are not reviewed. `paste.review_category`, `review_status`, `review_report_id`,
`fine`. Text pastes are not reviewed. `paste.review_category`, `review_status`, `review_report_id`,
`reviewed_at` and the requested fields are in the paste shape; the enforcement lives in `server/pastes/review.js`.
`reviewed_at` and the requested fields are in the paste shape; the enforcement lives in `server/pastes/review.js`.
Tests: `test/paste-review.test.js`.
Tests: `test/paste-review.test.js`.
-- **Slugs**: public pastes get a short `adj-noun-NN` slug; a new unlisted or private paste (and
- a fork of one) gets `adj-noun-` + 16 random base62 characters, because its slug is the only
- thing keeping it unlisted. Existing slugs never change and keep working.
+- **Addresses**: a public paste's canonical address is `/p/<title-slug>-<public_id>`. The
+ `public_id` is 8 base58 characters, generated once (migration 0021; existing rows are filled by
+ `scripts/backfill-paste-public-ids.js` in batches, and a read fills one lazily). The title slug is
+ up to six lowercase ASCII words from the title (or the first content line when untitled, or
+ nothing), so the id alone is a valid address and every old `adj-noun-NN` slug 301s to the
+ canonical one; a retitle moves it and the old form 301s. A new unlisted or private paste (and a
+ fork of one) keeps an `adj-noun-` + 16 random base62 secret slug: its secret part is the only
+ thing keeping it unlisted, and its public id is not a second key. `server/pastes/url.js` owns
+ the rules; `test/paste-urls.test.js` pins them.
- **Deletes are soft** (content scrubbed, slug kept reserved); edits append `paste_versions`.
- **Deletes are soft** (content scrubbed, slug kept reserved); edits append `paste_versions`.
Moving the data: Media's `openvibe.media.pastes-export` bundle was imported once (Wave 5, 2026-09-22; the ledger is
Moving the data: Media's `openvibe.media.pastes-export` bundle was imported once (Wave 5, 2026-09-22; the ledger is
@@ -136,7 +142,7 @@ Pages (server-rendered HTML):
@@ -136,7 +142,7 @@ Pages (server-rendered HTML):
| --- | --- |
| --- | --- |
| `GET /` | Home: hero, latest pastes, most viewed, "start a paste" CTA, what is coming |
| `GET /` | Home: hero, latest pastes, most viewed, "start a paste" CTA, what is coming |
| `GET /pastes` | Browse — `?q=` search, `?sort=new\|views`, `?lang=`, `?page=` |
| `GET /pastes` | Browse — `?q=` search, `?sort=new\|views`, `?lang=`, `?page=` |
-| `GET /p/:slug` | Paste page — highlighted body (server-side, highlight.js), raw/download/copy/fork/share, screenshot, related |
+| `GET /p/:slug` | Paste page at `/p/<title-slug>-<public id>` (old slugs and the bare id 301 there) — highlighted body (server-side, highlight.js), raw/download/copy/fork/share, screenshot, related |
| `POST /p/:slug/comments` | Comment on the paste (signed in; the no-JS form) — its typed comment thread, the one `/c/:accessId` and the APIs show |
| `POST /p/:slug/comments` | Comment on the paste (signed in; the no-JS form) — its typed comment thread, the one `/c/:accessId` and the APIs show |
| `GET /p/:slug/raw` | `live`: 302 → `https://openvibe.media/p/:slug/raw`; `community`: the text itself |
| `GET /p/:slug/raw` | `live`: 302 → `https://openvibe.media/p/:slug/raw`; `community`: the text itself |
| `GET /p/:slug/screenshot` | `live`: 302 → Media's `/p/:slug/screenshot`; `community`: 302 → the stored image URL |
| `GET /p/:slug/screenshot` | `live`: 302 → Media's `/p/:slug/screenshot`; `community`: 302 → the stored image URL |
@@ -664,9 +670,11 @@ Discovery is written by `openvibe-shared/seo`; Community only supplies the data
@@ -664,9 +670,11 @@ Discovery is written by `openvibe-shared/seo`; Community only supplies the data
(an `ai-summary` meta and a `WebPage` JSON-LD), with `article:*` times on pastes. JSON-LD:
(an `ai-summary` meta and a `WebPage` JSON-LD), with `article:*` times on pastes. JSON-LD:
`WebSite` on the home page, `Article`/`ImageObject` with author and `datePublished` on paste
`WebSite` on the home page, `Article`/`ImageObject` with author and `datePublished` on paste
pages, `DiscussionForumPosting` on threads, `BreadcrumbList` everywhere.
pages, `DiscussionForumPosting` on threads, `BreadcrumbList` everywhere.
-Unlisted/private pastes, search result pages and members/staff spaces are `noindex`. The
-sitemap lists home, `/pastes`, the latest public pastes, `/forum`, `/pulse`, public spaces and
-their latest threads (rebuilt at most hourly). `/robots.txt` welcomes search and AI crawlers
+Every paste page is `noindex` (link-shared content, SEO-29), so no paste is in the sitemap and
+paste changes never ping IndexNow (a ping for a noindex page is two contradictory signals).
+Unlisted/private pastes, search result pages and members/staff spaces are `noindex` too. The
+sitemap lists home, `/pastes`, `/forum`, `/pulse`, public spaces and their latest threads
+(rebuilt at most hourly). `/robots.txt` welcomes search and AI crawlers
and keeps them out of `/api/`, `/auth/`, `/my`, `/new`, `/forum/*/new` and `?sso=` URLs;
and keeps them out of `/api/`, `/auth/`, `/my`, `/new`, `/forum/*/new` and `?sso=` URLs;
`/llms.txt` maps the site and `/llms-full.txt` carries the latest public threads and text pastes
`/llms.txt` maps the site and `/llms-full.txt` carries the latest public threads and text pastes
in full (no NSFW, no burn-after-read, nothing members-only).
in full (no NSFW, no burn-after-read, nothing members-only).
new file mode 100644
new file mode 100644
@@ -0,0 +1,8 @@
@@ -0,0 +1,8 @@
+-- phase: expand
+-- The public paste id (URL decisions 2026-11-07): /p/<title-slug>-<id>, where the id is 8 base58
+-- characters stored once per paste. Expand-only: the column is nullable, existing rows are filled by
+-- scripts/backfill-paste-public-ids.js (in batches) and reads fill one lazily; the partial unique index
+-- keeps every assigned id unique without blocking the backfill.
+ALTER TABLE pastes ADD COLUMN IF NOT EXISTS public_id text COLLATE "C";
+
+CREATE UNIQUE INDEX IF NOT EXISTS idx_pastes_public_id ON pastes(public_id) WHERE public_id IS NOT NULL;
@@ -228,10 +228,11 @@
@@ -228,10 +228,11 @@
}
}
body.envelope = b64(sealed.bytes);
body.envelope = b64(sealed.bytes);
var out = await api('/api/pastes', { method: 'POST', json: body });
var out = await api('/api/pastes', { method: 'POST', json: body });
+ var href = out && (out.url || (out.paste && out.paste.url));
var slug = out && (out.slug || (out.paste && out.paste.slug));
var slug = out && (out.slug || (out.paste && out.paste.slug));
- if (!slug) throw new Error('No paste came back');
+ if (!href && !slug) throw new Error('No paste came back');
status.textContent = 'Done — opening your paste…';
status.textContent = 'Done — opening your paste…';
- location.href = '/p/' + encodeURIComponent(slug) + (sealed.linkKey ? '#k=' + sealed.linkKey : '');
+ location.href = (href || '/p/' + encodeURIComponent(slug)) + (sealed.linkKey ? '#k=' + sealed.linkKey : '');
}
}
form.addEventListener('submit', function (e) {
form.addEventListener('submit', function (e) {
@@ -258,10 +259,11 @@
@@ -258,10 +259,11 @@
p = api('/api/pastes', { method: 'POST', json: Object.assign({ content: el.content.value, language: el.language.value }, common) });
p = api('/api/pastes', { method: 'POST', json: Object.assign({ content: el.content.value, language: el.language.value }, common) });
}
}
p.then(function (out) {
p.then(function (out) {
+ var href = out && (out.url || (out.paste && out.paste.url));
var slug = out && (out.slug || (out.paste && out.paste.slug));
var slug = out && (out.slug || (out.paste && out.paste.slug));
- if (!slug) throw new Error('No paste came back');
+ if (!href && !slug) throw new Error('No paste came back');
status.textContent = 'Done — opening your paste…';
status.textContent = 'Done — opening your paste…';
- location.href = '/p/' + encodeURIComponent(slug);
+ location.href = href || '/p/' + encodeURIComponent(slug);
}).catch(function (err) {
}).catch(function (err) {
btn.disabled = false; status.className = 'form-status error';
btn.disabled = false; status.className = 'form-status error';
status.textContent = err.status === 429 && err.data && err.data.cooldown ? 'Slow down — try again in ' + err.data.cooldown + 's.' : err.message;
status.textContent = err.status === 429 && err.data && err.data.cooldown ? 'Slow down — try again in ' + err.data.cooldown + 's.' : err.message;
new file mode 100755
new file mode 100755
@@ -0,0 +1,38 @@
@@ -0,0 +1,38 @@
+#!/usr/bin/env node
+'use strict';
+/**
+ * Fill pastes.public_id (migration 0021) in batches. Existing pastes were minted before the id existed; the
+ * page route fills one lazily on first read, but this brings every row level at once so the first crawl or
+ * export after a deploy sees the canonical /p/<title-slug>-<id> address.
+ *
+ * node scripts/backfill-paste-public-ids.js # batches of 500 until none are left
+ * node scripts/backfill-paste-public-ids.js --batch 2000
+ *
+ * Safe to run twice: it only touches rows where public_id IS NULL, and the partial unique index keeps every
+ * assigned id unique (a collision is reported and the row is left for the next run).
+ */
+const config = require('../server/config');
+const { openDb, closeDb } = require('../server/db');
+const { newPublicId } = require('../server/pastes/url');
+
+const batchArg = process.argv.indexOf('--batch');
+const BATCH = Math.max(1, Math.min(10000, Number(batchArg > 0 ? process.argv[batchArg + 1] : 500) || 500));
+
+(async () => {
+ const db = await openDb(config);
+ let total = 0;
+ for (;;) {
+ const rows = await db.prepare('SELECT id FROM pastes WHERE public_id IS NULL ORDER BY id LIMIT ?').all(BATCH);
+ if (!rows.length) break;
+ let done = 0;
+ for (const row of rows) {
+ const info = await db.prepare('UPDATE pastes SET public_id = ? WHERE id = ? AND public_id IS NULL').run(newPublicId(), row.id);
+ done += info.changes;
+ }
+ total += done;
+ console.log(`backfilled ${total} paste public id${total === 1 ? '' : 's'}`);
+ if (!done) break;
+ }
+ console.log(total ? `done: ${total} paste public id${total === 1 ? '' : 's'} assigned` : 'done: every paste already has a public id');
+ await closeDb();
+})().catch((err) => { console.error('backfill failed:', err && err.message ? err.message : err); process.exitCode = 1; });
@@ -503,6 +503,17 @@ async function createApp(opts = {}) {
@@ -503,6 +503,17 @@ async function createApp(opts = {}) {
descriptor: require('./agent-descriptor'),
descriptor: require('./agent-descriptor'),
render: require('./agent-pages').createAgentPages({ views: agentViews }).renderPublicPage,
render: require('./agent-pages').createAgentPages({ views: agentViews }).renderPublicPage,
});
});
+ // An old paste address's twin 301s to the canonical address's twin, exactly as the page does, so the
+ // Markdown a crawler stores names one URL.
+ app.use(async (req, res, next) => {
+ if (!String(req.path || '').startsWith('/p/') || !String(req.path || '').endsWith('.md')) return next();
+ let slug;
+ try { slug = decodeURIComponent(req.path.slice(3, -3)); } catch { return next(); }
+ const canonical = await require('./pastes/store').canonicalForRef(db, slug).catch(() => null);
+ if (!canonical || canonical === slug) return next();
+ const q = req.originalUrl.includes('?') ? req.originalUrl.slice(req.originalUrl.indexOf('?')) : '';
+ return res.redirect(301, `/p/${encodeURIComponent(canonical)}.md${q}`);
+ });
app.use((req, res, next) => {
app.use((req, res, next) => {
// The manifest exactly: the rest of /.well-known/ (security.txt) belongs to the app.
// The manifest exactly: the rest of /.well-known/ (security.txt) belongs to the app.
if (req.path === '/.well-known/agent.json') return agentRouter(req, res, next);
if (req.path === '/.well-known/agent.json') return agentRouter(req, res, next);
@@ -566,7 +577,11 @@ async function createApp(opts = {}) {
@@ -566,7 +577,11 @@ async function createApp(opts = {}) {
if (!SLUG_RE.test(slug)) return { status: 404, body: pages.errorPage({ status: 404, title: 'Paste not found', message: 'That link does not point at a paste.' }) };
if (!SLUG_RE.test(slug)) return { status: 404, body: pages.errorPage({ status: 404, title: 'Paste not found', message: 'That link does not point at a paste.' }) };
// A twin is not a person reading: never consume a burn-after-read paste for one.
// A twin is not a person reading: never consume a burn-after-read paste for one.
if (twin) {
if (twin) {
- const row = await db.prepare('SELECT burn_after_read FROM pastes WHERE slug = ?').get(slug);
+ let row = await db.prepare('SELECT burn_after_read FROM pastes WHERE slug = ?').get(slug);
+ if (!row) {
+ const id = require('./pastes/url').publicIdOfRef(slug);
+ if (id) row = await db.prepare('SELECT burn_after_read FROM pastes WHERE public_id = ?').get(id);
+ }
if (!row || Number(row.burn_after_read)) return null;
if (!row || Number(row.burn_after_read)) return null;
}
}
let paste;
let paste;
@@ -576,7 +591,7 @@ async function createApp(opts = {}) {
@@ -576,7 +591,7 @@ async function createApp(opts = {}) {
if (err.status === 404) {
if (err.status === 404) {
// A paste shared with specific people answers the gate: "Ask for access" when signed
// A paste shared with specific people answers the gate: "Ask for access" when signed
// in, "Sign in" otherwise, and never its title, content, screenshot or comments.
// in, "Sign in" otherwise, and never its title, content, screenshot or comments.
- const sh = await sharing.shareOf(slug);
+ const sh = await sharing.shareOf(await require('./pastes/store').canonicalForRef(db, slug) || slug);
if (sh && sh.audience === 'people') {
if (sh && sh.audience === 'people') {
const signInUrl = `/auth/login?next=${encodeURIComponent(`/p/${slug}`)}`;
const signInUrl = `/auth/login?next=${encodeURIComponent(`/p/${slug}`)}`;
return { status: 404, body: pages.gatePage({ slug, signedInAs: user && user.username ? user.username : null, signInUrl }) };
return { status: 404, body: pages.gatePage({ slug, signedInAs: user && user.username ? user.username : null, signInUrl }) };
@@ -596,15 +611,23 @@ async function createApp(opts = {}) {
@@ -596,15 +611,23 @@ async function createApp(opts = {}) {
const sort = query.sort === 'old' ? 'old' : 'new';
const sort = query.sort === 'old' ? 'old' : 'new';
const thread = paste.burn_after_read ? null : await comments.forRef(viewer, pasteRef(paste.slug), { sort, after, limit: 30 });
const thread = paste.burn_after_read ? null : await comments.forRef(viewer, pasteRef(paste.slug), { sort, after, limit: 30 });
return {
return {
- status: 200, slug: paste.slug,
+ status: 200, slug: paste.slug, canonical: paste.canonical_slug || paste.slug,
body: pages.pastePage({ paste, related, user, comments: thread, commentSort: sort, commentsAfter: after, share }),
body: pages.pastePage({ paste, related, user, comments: thread, commentSort: sort, commentsAfter: after, share }),
};
};
}
}
app.get('/p/:slug', withUser, wrap(async (req, res) => {
app.get('/p/:slug', withUser, wrap(async (req, res) => {
+ // The id is the address; the title slug before it is decoration. An old adjective-noun-number slug, a
+ // bare id or a title that has since changed all 301 to the one canonical form (query kept), before a
+ // view is counted.
+ const canonical = await require('./pastes/store').canonicalForRef(db, req.params.slug);
+ if (canonical && canonical !== req.params.slug) {
+ const q = req.originalUrl.includes('?') ? req.originalUrl.slice(req.originalUrl.indexOf('?')) : '';
+ return res.redirect(301, `/p/${encodeURIComponent(canonical)}${q}`);
+ }
const out = await pasteView(req.params.slug, req.query, { viewer: req.viewer, user: req.user, ctx: ctxOf(req) });
const out = await pasteView(req.params.slug, req.query, { viewer: req.viewer, user: req.user, ctx: ctxOf(req) });
if (!out) return html(res, pages.errorPage({ status: 404, title: 'Paste not found', message: 'It may have been deleted, burned after reading, or never existed.' }), 404);
if (!out) return html(res, pages.errorPage({ status: 404, title: 'Paste not found', message: 'It may have been deleted, burned after reading, or never existed.' }), 404);
- if (out.status === 200) twinLink(res, `/p/${encodeURIComponent(out.slug)}`);
+ if (out.status === 200) twinLink(res, `/p/${encodeURIComponent(out.canonical || out.slug)}`);
html(res, out.body, out.status);
html(res, out.body, out.status);
}));
}));
@@ -614,16 +637,21 @@ async function createApp(opts = {}) {
@@ -614,16 +637,21 @@ async function createApp(opts = {}) {
const { slug } = req.params;
const { slug } = req.params;
const origin = req.get('origin');
const origin = req.get('origin');
if (origin && origin !== 'null' && origin !== config.baseUrl) return html(res, pages.errorPage({ status: 403, title: 'Not allowed', message: 'That form was sent from another site.' }), 403);
if (origin && origin !== 'null' && origin !== config.baseUrl) return html(res, pages.errorPage({ status: 403, title: 'Not allowed', message: 'That form was sent from another site.' }), 403);
- const back = `/p/${encodeURIComponent(slug)}`;
+ let back = `/p/${encodeURIComponent(slug)}`;
try {
try {
if (!SLUG_RE.test(slug)) throw new v1.ApiError(404, 'ref.not_found', 'No such paste');
if (!SLUG_RE.test(slug)) throw new v1.ApiError(404, 'ref.not_found', 'No such paste');
+ // The URL may be the canonical title+id form; the comment thread is keyed by the paste's stored
+ // slug, so resolve the paste first and keep the stable ref.
+ const found = await source.getPaste(slug, { ...ctxOf(req), noView: true });
+ const stable = found.slug;
+ back = `/p/${encodeURIComponent(found.canonical_slug || stable)}`;
if (!req.viewer || !req.viewer.subject) return res.redirect(303, `/auth/login?next=${encodeURIComponent(back)}`);
if (!req.viewer || !req.viewer.subject) return res.redirect(303, `/auth/login?next=${encodeURIComponent(back)}`);
- const { thread } = await comments.resolve(req.viewer, { ref: pasteRef(slug) });
+ const { thread } = await comments.resolve(req.viewer, { ref: pasteRef(stable) });
const b = req.body || {};
const b = req.body || {};
const out = await comments.add(req.viewer, thread.id, { message: String(b.message || '').slice(0, 5000), parent_id: b.parent_id || null });
const out = await comments.add(req.viewer, thread.id, { message: String(b.message || '').slice(0, 5000), parent_id: b.parent_id || null });
return res.redirect(303, `${back}#comment-${out.comment.id}`);
return res.redirect(303, `${back}#comment-${out.comment.id}`);
} catch (err) {
} catch (err) {
- if (!(err instanceof v1.ApiError)) return next(err);
+ if (!(err instanceof v1.ApiError) && !(err && err.name === 'PasteError')) return next(err);
if (err.status === 401) return res.redirect(303, `/auth/login?next=${encodeURIComponent(back)}`);
if (err.status === 401) return res.redirect(303, `/auth/login?next=${encodeURIComponent(back)}`);
if (err.status === 404) return html(res, pages.errorPage({ status: 404, title: 'Paste not found', message: 'It may have been deleted, burned after reading, or never existed.' }), 404);
if (err.status === 404) return html(res, pages.errorPage({ status: 404, title: 'Paste not found', message: 'It may have been deleted, burned after reading, or never existed.' }), 404);
return html(res, pages.errorPage({ status: err.status, title: err.status === 429 ? 'Slow down' : 'That did not work', message: err.message }), err.status);
return html(res, pages.errorPage({ status: err.status, title: err.status === 429 ? 'Slow down' : 'That did not work', message: err.message }), err.status);
@@ -656,7 +684,7 @@ async function createApp(opts = {}) {
@@ -656,7 +684,7 @@ async function createApp(opts = {}) {
if (variant && variant.error) return res.status(400).type('text/plain').set('X-Content-Type-Options', 'nosniff').send(variant.error);
if (variant && variant.error) return res.status(400).type('text/plain').set('X-Content-Type-Options', 'nosniff').send(variant.error);
try {
try {
await app.locals.pastes.screenshotUrl(req.viewer, req.params.slug);
await app.locals.pastes.screenshotUrl(req.viewer, req.params.slug);
- const row = await db.prepare('SELECT media_ref, screenshot_url, visibility, burn_after_read, locked FROM pastes WHERE slug = ? AND deleted_at IS NULL').get(req.params.slug);
+ const row = await db.prepare('SELECT media_ref, screenshot_url, visibility, burn_after_read, locked FROM pastes WHERE (slug = ? OR public_id = ?) AND deleted_at IS NULL').get(req.params.slug, require('./pastes/url').publicIdOfRef(req.params.slug) || '');
const got = row ? await app.locals.pasteMedia.bytes(row, variant) : null;
const got = row ? await app.locals.pasteMedia.bytes(row, variant) : null;
if (!got) return notFound(res);
if (!got) return notFound(res);
// A public paste's screenshot never changes under this URL, so it is cached like any other
// A public paste's screenshot never changes under this URL, so it is cached like any other
@@ -688,7 +716,7 @@ async function createApp(opts = {}) {
@@ -688,7 +716,7 @@ async function createApp(opts = {}) {
.type('application/octet-stream').send(out.bytes);
.type('application/octet-stream').send(out.bytes);
} catch (err) { if (err.status === 404) return res.status(404).type('text/plain').send('Not found'); throw err; }
} catch (err) { if (err.status === 404) return res.status(404).type('text/plain').send('Not found'); throw err; }
}
}
- if (paste.type === 'screenshot') return res.redirect(302, `/p/${encodeURIComponent(slug)}/screenshot`);
+ if (paste.type === 'screenshot') return res.redirect(302, `/p/${encodeURIComponent(paste.canonical_slug || slug)}/screenshot`);
res.set('Content-Disposition', `attachment; filename="${slug}.${extensionFor(paste.language)}"`);
res.set('Content-Disposition', `attachment; filename="${slug}.${extensionFor(paste.language)}"`);
res.set('Cache-Control', cache.htmlHeaders({ private: true }));
res.set('Cache-Control', cache.htmlHeaders({ private: true }));
res.type('text/plain; charset=utf-8').send(String(paste.content || ''));
res.type('text/plain; charset=utf-8').send(String(paste.content || ''));
@@ -75,7 +75,7 @@ async function llmsFullSections() {
@@ -75,7 +75,7 @@ async function llmsFullSections() {
const pastes = (await catalog.latest(30)).filter((p) => !Number(p.is_nsfw) && !Number(p.burn_after_read) && p.type !== 'screenshot');
const pastes = (await catalog.latest(30)).filter((p) => !Number(p.is_nsfw) && !Number(p.burn_after_read) && p.type !== 'screenshot');
const full = await Promise.all(pastes.map((p) => source.publicForDiscovery(p.slug)));
const full = await Promise.all(pastes.map((p) => source.publicForDiscovery(p.slug)));
sections.push({ title: 'Latest pastes', pages: full.filter(Boolean).map((p) => ({
sections.push({ title: 'Latest pastes', pages: full.filter(Boolean).map((p) => ({
- title: p.title || 'Untitled', url: abs(`/p/${p.slug}`), text: String(p.content || ''),
+ title: p.title || 'Untitled', url: abs(`/p/${p.canonical_slug || require('./pastes/url').canonicalSlug(p)}`), text: String(p.content || ''),
})) });
})) });
// Sharing (design 2026-11-06): the four audiences, the transfer, and the curls an agent uses.
// Sharing (design 2026-11-06): the four audiences, the transfer, and the curls an agent uses.
sections.push({ title: 'Sharing a paste', pages: [{ title: 'Sharing a paste', url: '/docs/sharing',
sections.push({ title: 'Sharing a paste', pages: [{ title: 'Sharing a paste', url: '/docs/sharing',
@@ -114,7 +114,7 @@ async function sitemapRows() {
@@ -114,7 +114,7 @@ async function sitemapRows() {
/** The latest pastes for /feed.xml. */
/** The latest pastes for /feed.xml. */
async function pasteFeedItems() {
async function pasteFeedItems() {
return (await catalog.latest(30)).filter((p) => !Number(p.is_nsfw)).map((p) => {
return (await catalog.latest(30)).filter((p) => !Number(p.is_nsfw)).map((p) => {
- const url = abs(`/p/${p.slug}`);
+ const url = abs(`/p/${p.canonical_slug || require('./pastes/url').canonicalSlug(p)}`);
const description = p.type === 'screenshot' ? 'A screenshot shared on OpenVibe.Community.' : clean(p.content || '', 300);
const description = p.type === 'screenshot' ? 'A screenshot shared on OpenVibe.Community.' : clean(p.content || '', 300);
return { title: p.title || 'Untitled', link: url, guid: url, description, content: description,
return { title: p.title || 'Untitled', link: url, guid: url, description, content: description,
author: p.display_name || p.username || 'Anonymous', published: isoDate(p.created_at), updated: isoDate(p.updated_at || p.created_at) };
author: p.display_name || p.username || 'Anonymous', published: isoDate(p.created_at), updated: isoDate(p.updated_at || p.created_at) };
@@ -77,7 +77,8 @@ async function publish(envelope) {
@@ -77,7 +77,8 @@ async function publish(envelope) {
}
}
// ── Builders (read the row as it is after the change) ─────────────────────────────
// ── Builders (read the row as it is after the change) ─────────────────────────────
-const pasteUrl = (p) => (p.visibility === 'public' && !p.deleted_at ? `${config.baseUrl}/p/${encodeURIComponent(p.slug)}` : null);
+// The id alone: an event is a machine envelope, and the canonical title+id address would carry the title.
+const pasteUrl = (p) => (p.visibility === 'public' && !p.deleted_at ? `${config.baseUrl}/p/${encodeURIComponent(p.public_id || p.slug)}` : null);
const actorOf = (sub) => { const r = subjectRef(sub); return r ? { type: r.type, id: r.id } : null; };
const actorOf = (sub) => { const r = subjectRef(sub); return r ? { type: r.type, id: r.id } : null; };
async function pasteCreated(p) {
async function pasteCreated(p) {
@@ -18,6 +18,7 @@
@@ -18,6 +18,7 @@
const crypto = require('crypto');
const crypto = require('crypto');
const events = require('../events');
const events = require('../events');
const store = require('./store');
const store = require('./store');
+const { canonicalSlug, publicIdOfRef } = require('./url');
const spam = require('./spam');
const spam = require('./spam');
const commentStore = require('../comments/store');
const commentStore = require('../comments/store');
const { discussionModerator } = require('../identity/capabilities');
const { discussionModerator } = require('../identity/capabilities');
@@ -91,18 +92,11 @@ function createPasteService({ db, network = null, media = null, config = {}, lim
@@ -91,18 +92,11 @@ function createPasteService({ db, network = null, media = null, config = {}, lim
const L = { ...DEFAULT_LIMITS, ...limits };
const L = { ...DEFAULT_LIMITS, ...limits };
// Pulse (server/pulse) hears about public pastes written by people; a Pulse problem never fails a paste write.
// Pulse (server/pulse) hears about public pastes written by people; a Pulse problem never fails a paste write.
const tell = async (fn) => { if (!pulse) return; try { await fn(pulse); } catch (err) { console.warn('[Pastes] pulse:', err.message); } };
const tell = async (fn) => { if (!pulse) return; try { await fn(pulse); } catch (err) { console.warn('[Pastes] pulse:', err.message); } };
- // IndexNow (openvibe-shared/indexnow): a public, indexable paste page appearing, changing or going away
- // pings the engines with its path and the sitemap. Private, unlisted, NSFW and burn-after-read pages are
- // not in the sitemap, so they never ping.
+ // IndexNow (openvibe-shared/indexnow): a paste page is noindex (SEO-29: link-shared content, not
+ // search-found content), so there is nothing to announce. Pinging a URL the page tells engines not to
+ // index is two contradictory signals (URL findings 2026-11-07, finding 4), so paste changes never ping.
+ const pingIndexNow = () => {};
const site = String(config.baseUrl || '').replace(/\/$/, '');
const site = String(config.baseUrl || '').replace(/\/$/, '');
- const indexable = (p) => !!(p && p.visibility === 'public' && !Number(p.is_nsfw) && !Number(p.burn_after_read));
- const pingIndexNow = (...rows) => {
- if (!indexnow || !indexnow.enabled) return;
- const slugs = new Set();
- for (const p of rows) if (indexable(p)) slugs.add(p.slug);
- if (!slugs.size) return;
- indexnow.pingSoon([...slugs].map((s) => `${site}/p/${s}`).concat(`${site}/sitemap.xml`));
- };
const visitorSecret = process.env.VIEW_HASH_SECRET
const visitorSecret = process.env.VIEW_HASH_SECRET
|| crypto.createHash('sha256').update(`community-views:${(config.oauth && config.oauth.clientSecret) || crypto.randomBytes(16).toString('hex')}`).digest('hex');
|| crypto.createHash('sha256').update(`community-views:${(config.oauth && config.oauth.clientSecret) || crypto.randomBytes(16).toString('hex')}`).digest('hex');
@@ -164,7 +158,19 @@ function createPasteService({ db, network = null, media = null, config = {}, lim
@@ -164,7 +158,19 @@ function createPasteService({ db, network = null, media = null, config = {}, lim
* confirms a slug).
* confirms a slug).
*/
*/
async function visible(v, slug) {
async function visible(v, slug) {
- const p = await store.getBySlug(db, String(slug));
+ const ref = String(slug);
+ let p = await store.getBySlug(db, ref);
+ // A canonical /p/<title-slug>-<id> (or the bare id) names the paste by its id: the title slug is
+ // decoration and may have changed since the link was copied. The old adjective-noun-number slug still
+ // resolves through getBySlug, so it can 301 on the page route.
+ if (!p) {
+ const id = publicIdOfRef(ref);
+ if (id) {
+ const byId = await store.getByPublicId(db, id);
+ // A secret paste's secret part is its access control: the stored public id is not a second key.
+ if (byId && byId.visibility === 'public' && !Number(byId.burn_after_read)) p = byId;
+ }
+ }
if (!p || !await mayView(v, p)) fail(404, 'Paste not found');
if (!p || !await mayView(v, p)) fail(404, 'Paste not found');
return p;
return p;
}
}
@@ -233,11 +239,13 @@ function createPasteService({ db, network = null, media = null, config = {}, lim
@@ -233,11 +239,13 @@ function createPasteService({ db, network = null, media = null, config = {}, lim
// its owner chose to keep it outside the envelope. The envelope's address is how a browser opens it.
// its owner chose to keep it outside the envelope. The envelope's address is how a browser opens it.
const locked = !!p.locked;
const locked = !!p.locked;
const titleShown = !locked || !!p.title_shown;
const titleShown = !locked || !!p.title_shown;
+ const canonical = canonicalSlug(p);
return {
return {
unique_views: p.unique_views || 0,
unique_views: p.unique_views || 0,
id: p.id,
id: p.id,
app_id: 'community',
app_id: 'community',
slug: p.slug,
slug: p.slug,
+ canonical_slug: canonical,
user_id: p.owner_subject || null,
user_id: p.owner_subject || null,
owner_subject: p.owner_subject || null,
owner_subject: p.owner_subject || null,
origin: p.origin,
origin: p.origin,
@@ -249,13 +257,13 @@ function createPasteService({ db, network = null, media = null, config = {}, lim
@@ -249,13 +257,13 @@ function createPasteService({ db, network = null, media = null, config = {}, lim
locked,
locked,
lock: locked ? (p.lock_kind || null) : null,
lock: locked ? (p.lock_kind || null) : null,
title_shown: locked ? !!p.title_shown : true,
title_shown: locked ? !!p.title_shown : true,
- envelope_url: locked ? `/p/${encodeURIComponent(p.slug)}/raw` : null,
+ envelope_url: locked ? `/p/${encodeURIComponent(canonical)}/raw` : null,
stream_id: streamIdOf(ref),
stream_id: streamIdOf(ref),
stream_ref: ref,
stream_ref: ref,
// The guarded address, never Media's own object URL: screenshots are stored private in Media (the share
// The guarded address, never Media's own object URL: screenshots are stored private in Media (the share
// rules decide who sees one), so /o/<id> is a 404 to every browser. /p/<slug>/screenshot checks the paste's
// rules decide who sees one), so /o/<id> is a 404 to every browser. /p/<slug>/screenshot checks the paste's
// audience and streams the bytes (2026-11-07: every image paste and AI moment on openvibe.live was broken).
// audience and streams the bytes (2026-11-07: every image paste and AI moment on openvibe.live was broken).
- screenshot_url: p.screenshot_url && !locked ? `${site || ''}/p/${encodeURIComponent(p.slug)}/screenshot` : null,
+ screenshot_url: p.screenshot_url && !locked ? `${site || ''}/p/${encodeURIComponent(canonical)}/screenshot` : null,
metadata: metadataFor(p.metadata, isOwner(v, p) || isService(v)),
metadata: metadataFor(p.metadata, isOwner(v, p) || isService(v)),
burn_after_read: !!p.burn_after_read,
burn_after_read: !!p.burn_after_read,
forked_from: p.forked_from || null,
forked_from: p.forked_from || null,
@@ -272,8 +280,8 @@ function createPasteService({ db, network = null, media = null, config = {}, lim
@@ -272,8 +280,8 @@ function createPasteService({ db, network = null, media = null, config = {}, lim
ai_tags: locked ? null : (p.ai_tags || null),
ai_tags: locked ? null : (p.ai_tags || null),
ai_analyzed_at: locked ? null : (p.ai_analyzed_at || null),
ai_analyzed_at: locked ? null : (p.ai_analyzed_at || null),
revision: p.revision,
revision: p.revision,
- url: `/p/${p.slug}`,
- raw_url: `/p/${p.slug}/raw`,
+ url: `/p/${canonical}`,
+ raw_url: `/p/${canonical}/raw`,
created_at: p.created_at,
created_at: p.created_at,
updated_at: p.updated_at,
updated_at: p.updated_at,
is_owner: isOwner(v, p),
is_owner: isOwner(v, p),
@@ -470,7 +478,7 @@ function createPasteService({ db, network = null, media = null, config = {}, lim
@@ -470,7 +478,7 @@ function createPasteService({ db, network = null, media = null, config = {}, lim
async function created(row, extra = {}) {
async function created(row, extra = {}) {
await tell((p) => p.pasteCreated(row));
await tell((p) => p.pasteCreated(row));
pingIndexNow(row);
pingIndexNow(row);
- return { id: row.id, slug: row.slug, url: `/p/${row.slug}`, ...extra };
+ return { id: row.id, slug: row.slug, url: `/p/${canonicalSlug(row)}`, ...extra };
}
}
// ═════════════════════════════════════════════════════════
// ═════════════════════════════════════════════════════════
@@ -515,7 +523,7 @@ function createPasteService({ db, network = null, media = null, config = {}, lim
@@ -515,7 +523,7 @@ function createPasteService({ db, network = null, media = null, config = {}, lim
async publicForDiscovery(slug) {
async publicForDiscovery(slug) {
const p = await store.getBySlug(db, String(slug));
const p = await store.getBySlug(db, String(slug));
if (!p || p.visibility !== 'public' || p.type !== 'paste' || Number(p.is_nsfw) || Number(p.burn_after_read)) return null;
if (!p || p.visibility !== 'public' || p.type !== 'paste' || Number(p.is_nsfw) || Number(p.burn_after_read)) return null;
- return { slug: p.slug, title: p.title, content: p.content };
+ return { slug: p.slug, public_id: p.public_id, canonical_slug: canonicalSlug(p), title: p.title, content: p.content };
},
},
/** The share state of a paste for the page: its audience and, for the recipient, their pending offer. */
/** The share state of a paste for the page: its audience and, for the recipient, their pending offer. */
@@ -550,9 +558,8 @@ function createPasteService({ db, network = null, media = null, config = {}, lim
@@ -550,9 +558,8 @@ function createPasteService({ db, network = null, media = null, config = {}, lim
/** GET /p/:slug/raw — plain text, Media's raw rules (a burn paste survives exactly one read). */
/** GET /p/:slug/raw — plain text, Media's raw rules (a burn paste survives exactly one read). */
async raw(v, slug, ctx = {}) {
async raw(v, slug, ctx = {}) {
- const p = await store.getBySlug(db, String(slug));
- if (!p || !await mayView(v, p)) fail(404, 'Not found');
- if (p.type === 'screenshot') return { redirect: `/p/${encodeURIComponent(p.slug)}/screenshot` };
+ const p = await visible(v, slug);
+ if (p.type === 'screenshot') return { redirect: `/p/${encodeURIComponent(canonicalSlug(p))}/screenshot` };
if (p.type !== 'paste') fail(404, 'Not found');
if (p.type !== 'paste') fail(404, 'Not found');
// A locked paste's raw is its envelope, as bytes: application/octet-stream, `.ovlk`.
// A locked paste's raw is its envelope, as bytes: application/octet-stream, `.ovlk`.
if (p.locked) {
if (p.locked) {
@@ -572,8 +579,8 @@ function createPasteService({ db, network = null, media = null, config = {}, lim
@@ -572,8 +579,8 @@ function createPasteService({ db, network = null, media = null, config = {}, lim
/** GET /p/:slug/screenshot target (the stored Media URL). For a burn-after-read screenshot it is a read (raw's rule). */
/** GET /p/:slug/screenshot target (the stored Media URL). For a burn-after-read screenshot it is a read (raw's rule). */
async screenshotUrl(v, slug) {
async screenshotUrl(v, slug) {
- const p = await store.getBySlug(db, String(slug));
- if (!p || !p.screenshot_url || !await mayView(v, p)) fail(404, 'Not found');
+ const p = await visible(v, slug);
+ if (!p.screenshot_url) fail(404, 'Not found');
if (p.burn_after_read && !isOwner(v, p)) {
if (p.burn_after_read && !isOwner(v, p)) {
if (p.views > 0) await burn(p);
if (p.views > 0) await burn(p);
await store.bumpViews(db, p.id);
await store.bumpViews(db, p.id);
@@ -11,6 +11,7 @@
@@ -11,6 +11,7 @@
* stay reserved and a re-import can never bring them back; every read below skips them.
* stay reserved and a re-import can never bring them back; every read below skips them.
*/
*/
const crypto = require('crypto');
const crypto = require('crypto');
+const { newPublicId, publicIdOfRef, canonicalSlug } = require('./url');
// ── Slugs (the same adjective-noun-number style OpenVibe.Media has always minted) ──
// ── Slugs (the same adjective-noun-number style OpenVibe.Media has always minted) ──
const SLUG_ADJECTIVES = [
const SLUG_ADJECTIVES = [
@@ -124,24 +125,62 @@ const likeEscape = (s) => String(s).replace(/[\\%_]/g, (c) => `\\${c}`);
@@ -124,24 +125,62 @@ const likeEscape = (s) => String(s).replace(/[\\%_]/g, (c) => `\\${c}`);
// ── Pastes ───────────────────────────────────────────────────
// ── Pastes ───────────────────────────────────────────────────
+/** One random id, stored once; two readers racing fill the first assignment and the other re-reads it. */
+async function ensurePublicId(db, row) {
+ if (!row || row.public_id) return row;
+ const id = newPublicId();
+ const info = await db.prepare('UPDATE pastes SET public_id = ? WHERE id = ? AND public_id IS NULL').run(id, row.id);
+ if (info.changes) return { ...row, public_id: id };
+ return await db.prepare('SELECT * FROM pastes WHERE id = ?').get(row.id) || row;
+}
+
async function getBySlug(db, slug) {
async function getBySlug(db, slug) {
- return await db.prepare('SELECT * FROM pastes WHERE slug = ? AND deleted_at IS NULL').get(String(slug)) || null;
+ return await ensurePublicId(db, await db.prepare('SELECT * FROM pastes WHERE slug = ? AND deleted_at IS NULL').get(String(slug)) || null);
}
}
async function getById(db, id) {
async function getById(db, id) {
- return await db.prepare('SELECT * FROM pastes WHERE id = ? AND deleted_at IS NULL').get(id) || null;
+ return await ensurePublicId(db, await db.prepare('SELECT * FROM pastes WHERE id = ? AND deleted_at IS NULL').get(id) || null);
+}
+
+async function getByPublicId(db, id) {
+ if (!publicIdOfRef(id)) return null;
+ return await ensurePublicId(db, await db.prepare('SELECT * FROM pastes WHERE public_id = ? AND deleted_at IS NULL').get(String(id)) || null);
+}
+
+/**
+ * The canonical /p/ segment for a reference, without loading the service's access rules: a row page
+ * redirect can happen before a view is counted. An old public slug resolves to the new title+id form;
+ * a link/private or burn paste keeps its secret slug, so its canonical is the reference itself.
+ */
+async function canonicalForRef(db, ref) {
+ const raw = String(ref == null ? '' : ref);
+ let row = await db.prepare('SELECT id, slug, public_id, visibility, burn_after_read, title, content FROM pastes WHERE slug = ? AND deleted_at IS NULL').get(raw);
+ if (!row) {
+ const id = publicIdOfRef(raw);
+ if (!id) return null;
+ row = await db.prepare('SELECT id, slug, public_id, visibility, burn_after_read, title, content FROM pastes WHERE public_id = ? AND deleted_at IS NULL').get(id);
+ // The id is a public address only for a public, non-burn paste; a secret paste's slug is the key and
+ // the id must not turn into a second, weaker one.
+ if (row && (row.visibility !== 'public' || Number(row.burn_after_read))) return null;
+ }
+ if (!row) return null;
+ if (!row.public_id) row = await ensurePublicId(db, row);
+ return canonicalSlug(row);
}
}
-const INSERT_COLUMNS = ['slug', 'owner_subject', 'origin', 'type', 'title', 'content', 'language', 'visibility',
+const INSERT_COLUMNS = ['slug', 'public_id', 'owner_subject', 'origin', 'type', 'title', 'content', 'language', 'visibility',
'screenshot_url', 'media_ref', 'stream_ref', 'metadata', 'burn_after_read', 'forked_from', 'pinned', 'is_nsfw',
'screenshot_url', 'media_ref', 'stream_ref', 'metadata', 'burn_after_read', 'forked_from', 'pinned', 'is_nsfw',
'ai_summary', 'ai_tags', 'ai_analyzed_at', 'locked', 'lock_kind', 'envelope', 'title_shown'];
'ai_summary', 'ai_tags', 'ai_analyzed_at', 'locked', 'lock_kind', 'envelope', 'title_shown'];
/** Insert a new paste (fields already validated); returns the stored row. */
/** Insert a new paste (fields already validated); returns the stored row. */
async function insertPaste(db, fields) {
async function insertPaste(db, fields) {
- const cols = INSERT_COLUMNS.filter((c) => fields[c] !== undefined);
+ // Every paste gets its public id once, even a secret one: the column is what a later public title+id
+ // address is built from, and it never changes.
+ const values = { ...fields, public_id: fields.public_id || newPublicId() };
+ const cols = INSERT_COLUMNS.filter((c) => values[c] !== undefined);
return await db.tx(async () => {
return await db.tx(async () => {
const info = await db.prepare(`INSERT INTO pastes (${cols.join(', ')}) VALUES (${cols.map(() => '?').join(', ')}) RETURNING id`)
const info = await db.prepare(`INSERT INTO pastes (${cols.join(', ')}) VALUES (${cols.map(() => '?').join(', ')}) RETURNING id`)
- .run(...cols.map((c) => fields[c]));
+ .run(...cols.map((c) => values[c]));
const row = await getById(db, info.lastInsertRowid);
const row = await getById(db, info.lastInsertRowid);
await require('../events').pasteCreated(row); // community.paste.created, in this transaction
await require('../events').pasteCreated(row); // community.paste.created, in this transaction
return row;
return row;
@@ -436,7 +475,7 @@ async function mapSet(db, system, type, id, targetType, targetId) {
@@ -436,7 +475,7 @@ async function mapSet(db, system, type, id, targetType, targetId) {
.run(String(system), String(type), String(id), String(targetType), String(targetId));
.run(String(system), String(type), String(id), String(targetType), String(targetId));
}
}
-module.exports = {
+module.exports = { ensurePublicId, getByPublicId, canonicalForRef, canonicalSlug, publicIdOfRef,
generateSlug, slugTaken, retireSlug, sanitizeTitle, detectLanguage,
generateSlug, slugTaken, retireSlug, sanitizeTitle, detectLanguage,
getBySlug, getById, insertPaste, updatePaste, listVersions, softDelete, listPastes,
getBySlug, getById, insertPaste, updatePaste, listVersions, softDelete, listPastes,
countOwnerSince, lastPasteTime, hasLiked, toggleLike, incrementCopies, bumpViews, recordVisit, pruneVisits,
countOwnerSince, lastPasteTime, hasLiked, toggleLike, incrementCopies, bumpViews, recordVisit, pruneVisits,
new file mode 100644
new file mode 100644
@@ -0,0 +1,59 @@
@@ -0,0 +1,59 @@
+'use strict';
+
+/**
+ * The public address of a paste, in one place so the page, its Markdown twin, JSON-LD, the feed,
+ * Search documents and events can never disagree.
+ *
+ * public /p/<title-slug>-<public_id> (the title slug is optional: /p/<public_id>)
+ * link/private, burn-after-read /p/<stored slug> (the secret part is the access control)
+ *
+ * <public_id> is 8 base58 characters, stored once per paste (migrations/0012_paste_public_id.sql,
+ * scripts/backfill-paste-public-ids.js). The slug part is decoration: lookups use the id, so a
+ * retitled paste keeps its address's id and its old title-slug form 301s to the new one.
+ */
+const crypto = require('crypto');
+
+const ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'; // base58: no 0, O, I, l
+const PUBLIC_ID_RE = /^[1-9A-HJ-NP-Za-km-z]{8}$/;
+const MAX_WORDS = 6;
+const MAX_CHARS = 48;
+
+function newPublicId() {
+ let out = '';
+ for (let i = 0; i < 8; i++) out += ALPHABET[crypto.randomInt(ALPHABET.length)];
+ return out;
+}
+
+/** Lowercase ASCII words, at most six and 48 characters, joined by '-'; '' when there are none. */
+function titleSlug(paste) {
+ const p = paste || {};
+ // A paste called "Untitled" is untitled: its first line of content names it instead.
+ const title = String(p.title == null ? '' : p.title).trim();
+ const text = !title || title === 'Untitled' ? String(p.content || '').split(/\r?\n/)[0] : title;
+ const words = String(text).toLowerCase().replace(/[^a-z0-9]+/g, ' ').trim().split(' ').filter(Boolean).slice(0, MAX_WORDS);
+ let out = '';
+ for (const word of words) {
+ const next = out ? `${out}-${word}` : word;
+ if (next.length > MAX_CHARS) break;
+ out = next;
+ }
+ return out;
+}
+
+/** The canonical path segment of a paste (without /p/). */
+function canonicalSlug(paste) {
+ const p = paste || {};
+ if (!p.public_id || p.visibility !== 'public' || Number(p.burn_after_read)) return String(p.slug || '');
+ const words = titleSlug(p);
+ return words ? `${words}-${p.public_id}` : p.public_id;
+}
+
+/** The 8-char id in a reference (bare id, or the last '-'-separated part), or null. */
+function publicIdOfRef(ref) {
+ const s = String(ref == null ? '' : ref);
+ if (PUBLIC_ID_RE.test(s)) return s;
+ const m = /-([1-9A-HJ-NP-Za-km-z]{8})$/.exec(s);
+ return m ? m[1] : null;
+}
+
+module.exports = { ALPHABET, PUBLIC_ID_RE, newPublicId, titleSlug, canonicalSlug, publicIdOfRef };
@@ -6,6 +6,7 @@
@@ -6,6 +6,7 @@
* openvibe-shared/seo's jsonLdTag (via headTags).
* openvibe-shared/seo's jsonLdTag (via headTags).
*/
*/
const config = require('../config');
const config = require('../config');
+const { canonicalSlug } = require('../pastes/url');
const { abs, SITE_NAME, DEFAULT_DESCRIPTION } = require('./layout');
const { abs, SITE_NAME, DEFAULT_DESCRIPTION } = require('./layout');
function isoDate(v) {
function isoDate(v) {
@@ -44,7 +45,7 @@ function authorLd(paste) {
@@ -44,7 +45,7 @@ function authorLd(paste) {
function pasteLd(paste, { description, image }) {
function pasteLd(paste, { description, image }) {
const isScreenshot = paste.type === 'screenshot';
const isScreenshot = paste.type === 'screenshot';
- const url = abs(`/p/${paste.slug}`);
+ const url = abs(`/p/${paste.canonical_slug || canonicalSlug(paste)}`);
const base = {
const base = {
'@context': 'https://schema.org',
'@context': 'https://schema.org',
'@type': isScreenshot ? 'ImageObject' : 'Article',
'@type': isScreenshot ? 'ImageObject' : 'Article',
@@ -15,6 +15,8 @@ const kit = require('openvibe-shared/kit');
@@ -15,6 +15,8 @@ const kit = require('openvibe-shared/kit');
const { highlight, escapeHtml: esc, languageLabel, extensionFor, LANGUAGES } = require('./highlight');
const { highlight, escapeHtml: esc, languageLabel, extensionFor, LANGUAGES } = require('./highlight');
const { renderMarkdown } = require('./markdown');
const { renderMarkdown } = require('./markdown');
const { pickBlock, foldBlock } = require('./short');
const { pickBlock, foldBlock } = require('./short');
+const { canonicalSlug } = require('../pastes/url');
+const pastePathSlug = (p) => (p && (p.canonical_slug || canonicalSlug(p))) || '';
// Languages read as prose: their lines wrap by default (the Wrap button still toggles), so a phone shows the whole text.
// Languages read as prose: their lines wrap by default (the Wrap button still toggles), so a phone shows the whole text.
const PROSE = new Set(['markdown', 'plaintext', 'text']);
const PROSE = new Set(['markdown', 'plaintext', 'text']);
@@ -35,7 +37,7 @@ function mediaW(u, w) {
@@ -35,7 +37,7 @@ function mediaW(u, w) {
return /^https:\/\/openvibe\.media\/o\/med_[0-9A-Za-z]+$/.test(String(u || '')) ? `${u}?w=${w}` : u;
return /^https:\/\/openvibe\.media\/o\/med_[0-9A-Za-z]+$/.test(String(u || '')) ? `${u}?w=${w}` : u;
}
}
/** A screenshot's address: Community's checked proxy, never the Media object's own URL. */
/** A screenshot's address: Community's checked proxy, never the Media object's own URL. */
-function screenshotSrc(p) { return `/p/${encodeURIComponent(p.slug)}/screenshot`; }
+function screenshotSrc(p) { return `/p/${encodeURIComponent(pastePathSlug(p))}/screenshot`; }
/** A card-sized screenshot through the same proxy: Media's ladder has 320 and 640, webp by URL. */
/** A card-sized screenshot through the same proxy: Media's ladder has 320 and 640, webp by URL. */
function screenshotVariant(p, w) { return `${screenshotSrc(p)}?w=${w}&fmt=webp`; }
function screenshotVariant(p, w) { return `${screenshotSrc(p)}?w=${w}&fmt=webp`; }
/** A card screenshot: two widths, the card's shape in width/height (no shift), lazy unless it is the LCP. */
/** A card screenshot: two widths, the card's shape in width/height (no shift), lazy unless it is the LCP. */
@@ -124,7 +126,7 @@ function pasteCard(p, { eager = false } = {}) {
@@ -124,7 +126,7 @@ function pasteCard(p, { eager = false } = {}) {
: `<pre class="card-code" aria-hidden="true">${esc(String(p.content || '').slice(0, 220))}${String(p.content || '').length > 220 ? '…' : ''}</pre>`;
: `<pre class="card-code" aria-hidden="true">${esc(String(p.content || '').slice(0, 220))}${String(p.content || '').length > 220 ? '…' : ''}</pre>`;
const flags = `${p.pinned ? '<i class="fa-solid fa-thumbtack" title="Pinned"></i> ' : ''}${p.burn_after_read ? '<i class="fa-solid fa-fire" title="Burns after reading"></i> ' : ''}${locked ? '<i class="fa-solid fa-lock" title="Locked"></i> ' : ''}${p.is_nsfw ? '<span class="badge badge-nsfw">NSFW</span> ' : ''}`;
const flags = `${p.pinned ? '<i class="fa-solid fa-thumbtack" title="Pinned"></i> ' : ''}${p.burn_after_read ? '<i class="fa-solid fa-fire" title="Burns after reading"></i> ' : ''}${locked ? '<i class="fa-solid fa-lock" title="Locked"></i> ' : ''}${p.is_nsfw ? '<span class="badge badge-nsfw">NSFW</span> ' : ''}`;
return `<article class="card${p.is_nsfw ? ' card-nsfw' : ''}${locked ? ' card-is-locked' : ''}">
return `<article class="card${p.is_nsfw ? ' card-nsfw' : ''}${locked ? ' card-is-locked' : ''}">
- <a class="card-link" href="/p/${esc(p.slug)}">
+ <a class="card-link" href="/p/${esc(pastePathSlug(p))}">
${preview}
${preview}
<h3 class="card-title">${flags}${esc(p.title || (locked ? 'A locked paste' : 'Untitled'))}</h3>
<h3 class="card-title">${flags}${esc(p.title || (locked ? 'A locked paste' : 'Untitled'))}</h3>
</a>
</a>
@@ -187,7 +189,7 @@ function pasteKitCard(p, { eager = false } = {}) {
@@ -187,7 +189,7 @@ function pasteKitCard(p, { eager = false } = {}) {
const isShot = p.type === 'screenshot';
const isShot = p.type === 'screenshot';
const image = isShot && !p.is_nsfw ? screenshotVariant(p, 640) : '';
const image = isShot && !p.is_nsfw ? screenshotVariant(p, 640) : '';
const card = kit.card({
const card = kit.card({
- href: `/p/${p.slug}`, title: p.title || 'Untitled', type: 'post', ratio: '16/9', image: image || '',
+ href: `/p/${pastePathSlug(p)}`, title: p.title || 'Untitled', type: 'post', ratio: '16/9', image: image || '',
badge: p.is_nsfw ? 'NSFW' : p.origin === 'ai' ? 'AI' : p.pinned ? 'Pinned' : '',
badge: p.is_nsfw ? 'NSFW' : p.origin === 'ai' ? 'AI' : p.pinned ? 'Pinned' : '',
corner: isShot ? '' : languageLabel(p.language || 'text'),
corner: isShot ? '' : languageLabel(p.language || 'text'),
preview: !isShot && !p.is_nsfw ? String(p.content || '') : '',
preview: !isShot && !p.is_nsfw ? String(p.content || '') : '',
@@ -321,7 +323,7 @@ ${homeFaq()}
@@ -321,7 +323,7 @@ ${homeFaq()}
active: 'home',
active: 'home',
jsonLd: [
jsonLd: [
ld.websiteLd(),
ld.websiteLd(),
- ...(latest && latest.length ? [{ '@context': 'https://schema.org', '@type': 'ItemList', name: 'Latest pastes', url: `${config.baseUrl}/pastes`, itemListElement: latest.slice(0, 12).map((p, i) => ({ '@type': 'ListItem', position: i + 1, name: p.title || 'Untitled', url: `${config.baseUrl}/p/${p.slug}` })) }] : []),
+ ...(latest && latest.length ? [{ '@context': 'https://schema.org', '@type': 'ItemList', name: 'Latest pastes', url: `${config.baseUrl}/pastes`, itemListElement: latest.slice(0, 12).map((p, i) => ({ '@type': 'ListItem', position: i + 1, name: p.title || 'Untitled', url: `${config.baseUrl}/p/${pastePathSlug(p)}` })) }] : []),
{ '@context': 'https://schema.org', '@type': 'FAQPage', mainEntity: HOME_FAQ.map((f) => ({ '@type': 'Question', name: f.q, acceptedAnswer: { '@type': 'Answer', text: f.a } })) },
{ '@context': 'https://schema.org', '@type': 'FAQPage', mainEntity: HOME_FAQ.map((f) => ({ '@type': 'Question', name: f.q, acceptedAnswer: { '@type': 'Answer', text: f.a } })) },
],
],
// The kit's controls (kit.css) and its cards and rails (room.css, after it); kit-web.js comes with community.js.
// The kit's controls (kit.css) and its cards and rails (room.css, after it); kit-web.js comes with community.js.
@@ -419,6 +421,7 @@ function pastePage({ paste: p, related, user, comments = null, commentSort = 'ne
@@ -419,6 +421,7 @@ function pastePage({ paste: p, related, user, comments = null, commentSort = 'ne
// A locked paste (Locker design 2026-11-06, the pilot): Community holds an envelope and never the plaintext. The
// A locked paste (Locker design 2026-11-06, the pilot): Community holds an envelope and never the plaintext. The
// title is here only if its owner chose to show it; the page opens the envelope in the browser.
// title is here only if its owner chose to show it; the page opens the envelope in the browser.
const locked = !!p.locked;
const locked = !!p.locked;
+ const pageSlug = p.canonical_slug || canonicalSlug(p);
const title = p.title || (locked ? 'A locked paste' : isShot ? 'Screenshot' : 'Untitled paste');
const title = p.title || (locked ? 'A locked paste' : isShot ? 'Screenshot' : 'Untitled paste');
const description = pasteDescription(p);
const description = pasteDescription(p);
// The screenshot is Community's own address: the proxy checks canView, never the Media object's URL.
// The screenshot is Community's own address: the proxy checks canView, never the Media object's URL.
@@ -476,8 +479,8 @@ function pastePage({ paste: p, related, user, comments = null, commentSort = 'ne
@@ -476,8 +479,8 @@ function pastePage({ paste: p, related, user, comments = null, commentSort = 'ne
${isOwner ? `<button type="button" class="btn btn-sm btn-danger" data-delete="${esc(p.slug)}"><i class="fa-solid fa-trash" aria-hidden="true"></i> Delete</button>` : ''}
${isOwner ? `<button type="button" class="btn btn-sm btn-danger" data-delete="${esc(p.slug)}"><i class="fa-solid fa-trash" aria-hidden="true"></i> Delete</button>` : ''}
</div>` : `<div class="actions">
</div>` : `<div class="actions">
${!isShot ? `<button type="button" class="btn btn-sm btn-primary" data-copy-content="${esc(p.slug)}"><i class="fa-solid fa-copy" aria-hidden="true"></i> Copy</button>` : ''}
${!isShot ? `<button type="button" class="btn btn-sm btn-primary" data-copy-content="${esc(p.slug)}"><i class="fa-solid fa-copy" aria-hidden="true"></i> Copy</button>` : ''}
- ${!isShot ? `<a class="btn btn-sm" href="/p/${esc(p.slug)}/raw"><i class="fa-solid fa-file-lines" aria-hidden="true"></i> Raw</a>` : `<a class="btn btn-sm" href="/p/${esc(p.slug)}/screenshot"><i class="fa-solid fa-image" aria-hidden="true"></i> Full image</a>`}
- <a class="btn btn-sm" href="/p/${esc(p.slug)}/download" download="${esc(p.slug)}.${esc(isShot ? 'png' : ext)}"><i class="fa-solid fa-download" aria-hidden="true"></i> Download</a>
+ ${!isShot ? `<a class="btn btn-sm" href="/p/${esc(pageSlug)}/raw"><i class="fa-solid fa-file-lines" aria-hidden="true"></i> Raw</a>` : `<a class="btn btn-sm" href="/p/${esc(pageSlug)}/screenshot"><i class="fa-solid fa-image" aria-hidden="true"></i> Full image</a>`}
+ <a class="btn btn-sm" href="/p/${esc(pageSlug)}/download" download="${esc(pageSlug)}.${esc(isShot ? 'png' : ext)}"><i class="fa-solid fa-download" aria-hidden="true"></i> Download</a>
${!isShot ? `<a class="btn btn-sm" href="/new?fork=${esc(p.slug)}"><i class="fa-solid fa-code-fork" aria-hidden="true"></i> Fork</a>` : ''}
${!isShot ? `<a class="btn btn-sm" href="/new?fork=${esc(p.slug)}"><i class="fa-solid fa-code-fork" aria-hidden="true"></i> Fork</a>` : ''}
<button type="button" class="btn btn-sm" data-ov-share-sheet data-title="${esc(title)}" data-kind="paste" data-id="${esc(p.slug)}"><i class="fa-solid fa-share-nodes" aria-hidden="true"></i> Share</button>
<button type="button" class="btn btn-sm" data-ov-share-sheet data-title="${esc(title)}" data-kind="paste" data-id="${esc(p.slug)}"><i class="fa-solid fa-share-nodes" aria-hidden="true"></i> Share</button>
${!restricted && p.visibility !== 'private' && !p.burn_after_read ? `<a class="btn btn-sm" href="/forum/discuss?paste=${encodeURIComponent(p.slug)}"><i class="fa-solid fa-comments" aria-hidden="true"></i> Discuss in the forum</a>` : ''}
${!restricted && p.visibility !== 'private' && !p.burn_after_read ? `<a class="btn btn-sm" href="/forum/discuss?paste=${encodeURIComponent(p.slug)}"><i class="fa-solid fa-comments" aria-hidden="true"></i> Discuss in the forum</a>` : ''}
@@ -505,7 +508,7 @@ ${offer}
@@ -505,7 +508,7 @@ ${offer}
${actions}
${actions}
</header>
</header>
${bodyBlock}
${bodyBlock}
- ${locked ? `<p class="locked-file muted small"><a href="/p/${esc(p.slug)}/download" download="${esc(p.slug)}.ovlk"><i class="fa-solid fa-lock" aria-hidden="true"></i> Download the locked file (.ovlk)</a> — it opens only with its key, in the Locker.</p>` : ''}
+ ${locked ? `<p class="locked-file muted small"><a href="/p/${esc(pageSlug)}/download" download="${esc(pageSlug)}.ovlk"><i class="fa-solid fa-lock" aria-hidden="true"></i> Download the locked file (.ovlk)</a> — it opens only with its key, in the Locker.</p>` : ''}
<footer class="paste-foot">
<footer class="paste-foot">
<p class="muted small">Stored by <a href="${esc(config.mediaUrl)}" rel="noopener">OpenVibe.Media</a>, account by <a href="${NETWORK_URL}" rel="noopener">OpenVibe.Network</a>. Something wrong with this paste? <a href="${esc(config.liveUrl)}/dmca">Report it</a>.</p>
<p class="muted small">Stored by <a href="${esc(config.mediaUrl)}" rel="noopener">OpenVibe.Media</a>, account by <a href="${NETWORK_URL}" rel="noopener">OpenVibe.Network</a>. Something wrong with this paste? <a href="${esc(config.liveUrl)}/dmca">Report it</a>.</p>
</footer>
</footer>
@@ -513,7 +516,7 @@ ${offer}
@@ -513,7 +516,7 @@ ${offer}
${p.burn_after_read ? '' : `<section class="section" id="comments">
${p.burn_after_read ? '' : `<section class="section" id="comments">
${locked ? '<p class="muted small">Comments are not locked: anyone who can open this paste can read them, and so can OpenVibe.</p>' : ''}
${locked ? '<p class="muted small">Comments are not locked: anyone who can open this paste can read them, and so can OpenVibe.</p>' : ''}
<div class="section-head"><h2>Comments${comments && comments.thread.comment_count ? ` <span class="muted small">${num(comments.thread.comment_count)}</span>` : ''}</h2></div>
<div class="section-head"><h2>Comments${comments && comments.thread.comment_count ? ` <span class="muted small">${num(comments.thread.comment_count)}</span>` : ''}</h2></div>
- ${require('./comments').commentPanel({ page: comments, user, base: `/p/${p.slug}`, action: `/p/${p.slug}/comments`, after: commentsAfter, sort: commentSort })}
+ ${require('./comments').commentPanel({ page: comments, user, base: `/p/${pageSlug}`, action: `/p/${pageSlug}/comments`, after: commentsAfter, sort: commentSort })}
</section>`}
</section>`}
${!locked && related && related.length ? `<section class="section" id="related">
${!locked && related && related.length ? `<section class="section" id="related">
<div class="section-head"><h2>More like this</h2><a class="more" href="${!isShot && p.language && p.language !== 'text' ? `/pastes?lang=${encodeURIComponent(p.language)}` : '/pastes'}">Browse <i class="fa-solid fa-arrow-right" aria-hidden="true"></i></a></div>
<div class="section-head"><h2>More like this</h2><a class="more" href="${!isShot && p.language && p.language !== 'text' ? `/pastes?lang=${encodeURIComponent(p.language)}` : '/pastes'}">Browse <i class="fa-solid fa-arrow-right" aria-hidden="true"></i></a></div>
@@ -526,7 +529,7 @@ ${!locked && related && related.length ? `<section class="section" id="related">
@@ -526,7 +529,7 @@ ${!locked && related && related.length ? `<section class="section" id="related">
window.addEventListener('DOMContentLoaded', function () {
window.addEventListener('DOMContentLoaded', function () {
var box = document.getElementById('paste-locked');
var box = document.getElementById('paste-locked');
if (!box || !window.OVLocker) return;
if (!box || !window.OVLocker) return;
- OVLocker.open(box, { url: ${JSON.stringify(`/p/${p.slug}/raw`)}, link: location.href, title: ${JSON.stringify(p.title_shown ? title : '')}, onOpen: function (bytes) {
+ OVLocker.open(box, { url: ${JSON.stringify(`/p/${pageSlug}/raw`)}, link: location.href, title: ${JSON.stringify(p.title_shown ? title : '')}, onOpen: function (bytes) {
var text = new TextDecoder().decode(bytes), doc = null;
var text = new TextDecoder().decode(bytes), doc = null;
try { doc = JSON.parse(text); } catch (e) { doc = null; }
try { doc = JSON.parse(text); } catch (e) { doc = null; }
var shown = doc && typeof doc.content === 'string' ? doc.content : text;
var shown = doc && typeof doc.content === 'string' ? doc.content : text;
@@ -571,7 +574,7 @@ window.addEventListener('DOMContentLoaded', function () {
@@ -571,7 +574,7 @@ window.addEventListener('DOMContentLoaded', function () {
return renderPage({
return renderPage({
title: metaTitle,
title: metaTitle,
description: metaDescription,
description: metaDescription,
- canonicalPath: `/p/${p.slug}`,
+ canonicalPath: `/p/${pageSlug}`,
agentLayer: true, // the public page has a Markdown twin (server/agent-pages.js)
agentLayer: true, // the public page has a Markdown twin (server/agent-pages.js)
// A paste is link-shared content: reached by the link its author shares, not found by
// A paste is link-shared content: reached by the link its author shares, not found by
// search. Google's 2026-10-26 UGC guidance says to noindex those by default; the browse
// search. Google's 2026-10-26 UGC guidance says to noindex those by default; the browse
@@ -588,7 +591,7 @@ window.addEventListener('DOMContentLoaded', function () {
@@ -588,7 +591,7 @@ window.addEventListener('DOMContentLoaded', function () {
footerVariant: 'compact',
footerVariant: 'compact',
jsonLd: lockedMeta ? [] : [
jsonLd: lockedMeta ? [] : [
ld.pasteLd(p, { description, image: shot || DEFAULT_OG_IMAGE }),
ld.pasteLd(p, { description, image: shot || DEFAULT_OG_IMAGE }),
- ld.breadcrumbLd([{ name: 'Home', url: '/' }, { name: 'Pastes', url: '/pastes' }, { name: title, url: `/p/${p.slug}` }]),
+ ld.breadcrumbLd([{ name: 'Home', url: '/' }, { name: 'Pastes', url: '/pastes' }, { name: title, url: `/p/${pageSlug}` }]),
],
],
body,
body,
});
});
@@ -103,7 +103,7 @@ function createSearchDocuments({ db }) {
@@ -103,7 +103,7 @@ function createSearchDocuments({ db }) {
if (tags.length) facets.tags = tags;
if (tags.length) facets.tags = tags;
const doc = {
const doc = {
owner: 'community', type: 'paste', id: pasteDocId(p), deleted: false, visibility: 'public',
owner: 'community', type: 'paste', id: pasteDocId(p), deleted: false, visibility: 'public',
- canonical_url: `${config.baseUrl}/p/${encodeURIComponent(p.slug)}`,
+ canonical_url: `${config.baseUrl}/p/${encodeURIComponent(require('../pastes/url').canonicalSlug(p))}`,
title: pasteTitle(p, shot),
title: pasteTitle(p, shot),
summary: clean(p.ai_summary, 4000) || clean(shot ? '' : p.content, 300) || `A ${shot ? 'screenshot' : 'paste'} on OpenVibe.Community.`,
summary: clean(p.ai_summary, 4000) || clean(shot ? '' : p.content, 300) || `A ${shot ? 'screenshot' : 'paste'} on OpenVibe.Community.`,
body: shot ? clean(p.ai_summary, 8000) : String(p.content || '').slice(0, 40000),
body: shot ? clean(p.ai_summary, 8000) : String(p.content || '').slice(0, 40000),
@@ -1,4 +1,5 @@
@@ -1,4 +1,5 @@
'use strict';
'use strict';
+const { canonicalSlug } = require('./pastes/url');
/**
/**
* Sharing on Community pastes (design 2026-11-06, plan track Q; openvibe-sdk/share).
* Sharing on Community pastes (design 2026-11-06, plan track Q; openvibe-sdk/share).
@@ -265,14 +266,14 @@ function describeRef(db) {
@@ -265,14 +266,14 @@ function describeRef(db) {
if (!p) return {};
if (!p) return {};
const out = {
const out = {
title: !p.locked || p.title_shown ? (p.title || 'Untitled') : 'A locked paste',
title: !p.locked || p.title_shown ? (p.title || 'Untitled') : 'A locked paste',
- link: `${config.baseUrl}/p/${encodeURIComponent(p.slug)}`,
+ link: `${config.baseUrl}/p/${encodeURIComponent(canonicalSlug(p))}`,
roles: ['viewer'],
roles: ['viewer'],
audiences: AUDIENCES,
audiences: AUDIENCES,
transferable: true,
transferable: true,
};
};
if (p.locked) {
if (p.locked) {
out.audiences = p.lock_kind === 'people' ? ['people', 'private'] : ['link', 'private'];
out.audiences = p.lock_kind === 'people' ? ['people', 'private'] : ['link', 'private'];
- out.envelope_url = `${config.baseUrl}/p/${encodeURIComponent(p.slug)}/raw`;
+ out.envelope_url = `${config.baseUrl}/p/${encodeURIComponent(canonicalSlug(p))}/raw`;
}
}
return out;
return out;
};
};
@@ -33,7 +33,7 @@ const comments = require('../server/comments/store');
@@ -33,7 +33,7 @@ const comments = require('../server/comments/store');
evs.forEach(ok);
evs.forEach(ok);
assert.ok(!JSON.stringify(evs).includes('secret'), 'no title or content in any event');
assert.ok(!JSON.stringify(evs).includes('secret'), 'no title or content in any event');
assert.strictEqual(evs[0].visibility, 'public');
assert.strictEqual(evs[0].visibility, 'public');
- assert.strictEqual(evs[0].payload.url, 'https://openvibe.community/p/amber-fox-42'.replace('https://openvibe.community', require('../server/config').baseUrl));
+ assert.strictEqual(evs[0].payload.url, `${require('../server/config').baseUrl}/p/${p.public_id}`, 'the event carries the bare id, never the title slug');
assert.deepStrictEqual(evs[1].payload.changed, ['content']);
assert.deepStrictEqual(evs[1].payload.changed, ['content']);
assert.strictEqual(evs[2].visibility, 'internal', 'an unlisted paste is not a public event');
assert.strictEqual(evs[2].visibility, 'internal', 'an unlisted paste is not a public event');
assert.strictEqual(evs[2].payload.url, null);
assert.strictEqual(evs[2].payload.url, null);
@@ -116,7 +116,8 @@ const { boot, check, done } = require('./helpers/app');
@@ -116,7 +116,8 @@ const { boot, check, done } = require('./helpers/app');
});
});
await check('"Discuss in a space" from a paste: the picker, then a new topic with the paste attached', async () => {
await check('"Discuss in a space" from a paste: the picker, then a new topic with the paste attached', async () => {
- const paste = (await call('/p/snip1')).text;
+ const pasteUrl = await require('../server/pastes/store').canonicalForRef(t.db, 'snip1');
+ const paste = (await call(`/p/${pasteUrl}`)).text;
assert.ok(paste.includes('/forum/discuss?paste=snip1'));
assert.ok(paste.includes('/forum/discuss?paste=snip1'));
const picker = (await call('/forum/discuss?paste=snip1', { cookie: alexJwt })).text;
const picker = (await call('/forum/discuss?paste=snip1', { cookie: alexJwt })).text;
assert.ok(picker.includes('/forum/general/new?paste=snip1') && !picker.includes('/forum/roadmap/new'));
assert.ok(picker.includes('/forum/general/new?paste=snip1') && !picker.includes('/forum/roadmap/new'));
@@ -48,12 +48,11 @@ const json = (body) => ({ method: 'POST', headers: { 'content-type': 'applicatio
@@ -48,12 +48,11 @@ const json = (body) => ({ method: 'POST', headers: { 'content-type': 'applicatio
assert.deepStrictEqual(pings, []);
assert.deepStrictEqual(pings, []);
});
});
- await check('a publish pings the paste path and the sitemap', async () => {
+ await check('a publish never pings: the paste page is noindex', async () => {
+ pings.length = 0;
const r = await t.get('/api/pastes', json({ content: 'a published paste' }));
const r = await t.get('/api/pastes', json({ content: 'a published paste' }));
assert.strictEqual(r.status, 201, r.text);
assert.strictEqual(r.status, 201, r.text);
- const slug = r.json().slug;
- assert.ok(pings.includes(`https://openvibe.community/p/${slug}`), JSON.stringify(pings));
- assert.ok(pings.includes('https://openvibe.community/sitemap.xml'), JSON.stringify(pings));
+ assert.deepStrictEqual(pings, [], 'IndexNow must not be told about a noindex page');
});
});
await check('an NSFW paste never pings', async () => {
await check('an NSFW paste never pings', async () => {
@@ -67,15 +66,15 @@ const json = (body) => ({ method: 'POST', headers: { 'content-type': 'applicatio
@@ -67,15 +66,15 @@ const json = (body) => ({ method: 'POST', headers: { 'content-type': 'applicatio
const alex = t.network.addUser({ network_user_id: 7, username: 'alex', display_name: 'Alex' });
const alex = t.network.addUser({ network_user_id: 7, username: 'alex', display_name: 'Alex' });
const svc = (cap) => t.network.signService({ cap });
const svc = (cap) => t.network.signService({ cap });
- await check('unpublishing a public paste pings it again', async () => {
+ await check('publishing or unpublishing a paste never pings either', async () => {
pings.length = 0;
pings.length = 0;
const created = await t.get('/api/pastes', { ...json({ content: 'mine' }), headers: { 'content-type': 'application/json', authorization: `Bearer ${svc(['community.paste.create'])}`, 'x-ov-subject': alex.subject_id } });
const created = await t.get('/api/pastes', { ...json({ content: 'mine' }), headers: { 'content-type': 'application/json', authorization: `Bearer ${svc(['community.paste.create'])}`, 'x-ov-subject': alex.subject_id } });
assert.strictEqual(created.status, 201, created.text);
assert.strictEqual(created.status, 201, created.text);
- const slug = created.json().slug;
- pings.length = 0;
+ const slug = created.json().paste.canonical_slug;
+ assert.deepStrictEqual(pings, [], 'creation is a noindex page');
const updated = await t.get(`/api/pastes/${slug}`, { method: 'PUT', headers: { 'content-type': 'application/json', authorization: `Bearer ${svc(['community.paste.write'])}`, 'x-ov-subject': alex.subject_id }, body: JSON.stringify({ visibility: 'unlisted' }) });
const updated = await t.get(`/api/pastes/${slug}`, { method: 'PUT', headers: { 'content-type': 'application/json', authorization: `Bearer ${svc(['community.paste.write'])}`, 'x-ov-subject': alex.subject_id }, body: JSON.stringify({ visibility: 'unlisted' }) });
assert.strictEqual(updated.status, 200, updated.text);
assert.strictEqual(updated.status, 200, updated.text);
- assert.ok(pings.includes(`https://openvibe.community/p/${slug}`), JSON.stringify(pings));
+ assert.deepStrictEqual(pings, [], 'a visibility change on a noindex page pings nobody');
});
});
await t.close();
await t.close();
@@ -454,6 +454,15 @@ function statusCompatible(was, now) {
@@ -454,6 +454,15 @@ function statusCompatible(was, now) {
return was === now;
return was === now;
}
}
+/**
+ * A permanent move of an HTML page (200 → 301) is compatible: the old client's link still opens the same
+ * content in a browser. It is how the pastes' title+id addresses replaced the old adjective-noun-number slugs.
+ */
+function movedPage(rec, got) {
+ return rec && rec.method === 'GET' && rec.kind === 'html' && rec.status >= 200 && rec.status < 300
+ && got.status === 301 && !!got.location;
+}
+
// ── Replaying calls ──────────────────────────────────────────
// ── Replaying calls ──────────────────────────────────────────
function kindOf(contentType, text) {
function kindOf(contentType, text) {
@@ -546,7 +555,7 @@ function callProblems(rec, got, probe = []) {
@@ -546,7 +555,7 @@ function callProblems(rec, got, probe = []) {
const problems = [];
const problems = [];
if (got.status === 0) return [`no answer (${got.error})`];
if (got.status === 0) return [`no answer (${got.error})`];
if (isNoRoute(got, probe)) return ['the route is gone (it answers as an unknown path)'];
if (isNoRoute(got, probe)) return ['the route is gone (it answers as an unknown path)'];
- if (!statusCompatible(rec.status, got.status)) problems.push(`answered ${got.status}, N-1 answered ${rec.status}`);
+ if (!statusCompatible(rec.status, got.status) && !movedPage(rec, got)) problems.push(`answered ${got.status}, N-1 answered ${rec.status}`);
else if (rec.kind === 'json' && got.kind !== 'json' && rec.status < 500) problems.push(`answered ${got.kind}, N-1 answered JSON`);
else if (rec.kind === 'json' && got.kind !== 'json' && rec.status < 500) problems.push(`answered ${got.kind}, N-1 answered JSON`);
if (rec.location && got.location && rec.status >= 300 && rec.status < 400 && got.location.split('?')[0] !== rec.location.split('?')[0]) problems.push(`redirects to ${got.location}, N-1 to ${rec.location}`);
if (rec.location && got.location && rec.status >= 300 && rec.status < 400 && got.location.split('?')[0] !== rec.location.split('?')[0]) problems.push(`redirects to ${got.location}, N-1 to ${rec.location}`);
if (rec.reads && got.json != null && got.status >= 200 && got.status < 300) problems.push(...readProblems(rec.reads, got.json));
if (rec.reads && got.json != null && got.status >= 200 && got.status < 300) problems.push(...readProblems(rec.reads, got.json));
@@ -40,12 +40,13 @@ const MIGRATION = path.join(__dirname, '..', 'migrations', '0004_paste_comments_
@@ -40,12 +40,13 @@ const MIGRATION = path.join(__dirname, '..', 'migrations', '0004_paste_comments_
const legacyRows = async () => Number((await t.db.prepare('SELECT COUNT(*) AS n FROM paste_comments').get()).n);
const legacyRows = async () => Number((await t.db.prepare('SELECT COUNT(*) AS n FROM paste_comments').get()).n);
const pub = await create({ content: 'public paste', title: 'Pub' });
const pub = await create({ content: 'public paste', title: 'Pub' });
+ const pubUrl = await require('../server/pastes/store').canonicalForRef(t.db, pub);
await check('the /p/<slug> form and POST /api/pastes/:slug/comments write the typed thread; /api/v1/comments reads both back', async () => {
await check('the /p/<slug> form and POST /api/pastes/:slug/comments write the typed thread; /api/v1/comments reads both back', async () => {
- const viaForm = await call(`/p/${pub}/comments`, { method: 'POST', cookie: samJwt, form: { message: 'from the page form' } });
+ const viaForm = await call(`/p/${pubUrl}/comments`, { method: 'POST', cookie: samJwt, form: { message: 'from the page form' } });
assert.strictEqual(viaForm.status, 303, viaForm.text);
assert.strictEqual(viaForm.status, 303, viaForm.text);
const m = /^\/p\/([^#]+)#comment-(\d+)$/.exec(viaForm.headers.get('location'));
const m = /^\/p\/([^#]+)#comment-(\d+)$/.exec(viaForm.headers.get('location'));
- assert.ok(m && m[1] === pub, viaForm.headers.get('location'));
+ assert.ok(m && m[1] === pubUrl, viaForm.headers.get('location'));
const viaApi = await call(`/api/pastes/${pub}/comments`, { method: 'POST', cookie: alexJwt, json: { message: 'from the paste API' } });
const viaApi = await call(`/api/pastes/${pub}/comments`, { method: 'POST', cookie: alexJwt, json: { message: 'from the paste API' } });
assert.strictEqual(viaApi.status, 201, viaApi.text);
assert.strictEqual(viaApi.status, 201, viaApi.text);
assert.strictEqual(viaApi.json().comment.user_id, alex.subject_id);
assert.strictEqual(viaApi.json().comment.user_id, alex.subject_id);
@@ -73,30 +74,30 @@ const MIGRATION = path.join(__dirname, '..', 'migrations', '0004_paste_comments_
@@ -73,30 +74,30 @@ const MIGRATION = path.join(__dirname, '..', 'migrations', '0004_paste_comments_
assert.strictEqual(await legacyRows(), 0, 'nothing is written to paste_comments');
assert.strictEqual(await legacyRows(), 0, 'nothing is written to paste_comments');
// The paste page shows the same thread: comments with their anchors, sorting, the no-JS form.
// The paste page shows the same thread: comments with their anchors, sorting, the no-JS form.
- const pageHtml = await call(`/p/${pub}`, { cookie: samJwt });
+ const pageHtml = await call(`/p/${pubUrl}`, { cookie: samJwt });
assert.strictEqual(pageHtml.status, 200);
assert.strictEqual(pageHtml.status, 200);
for (const want of ['from the page form', 'from the paste API', 'from Live', `id="comment-${m[2]}"`, `id="comment-${viaApi.json().comment.id}"`,
for (const want of ['from the page form', 'from the paste API', 'from Live', `id="comment-${m[2]}"`, `id="comment-${viaApi.json().comment.id}"`,
- `action="/p/${pub}/comments"`, `href="/p/${pub}?sort=old"`]) assert.ok(pageHtml.text.includes(want), want);
+ `action="/p/${pubUrl}/comments"`, `href="/p/${pubUrl}?sort=old"`]) assert.ok(pageHtml.text.includes(want), want);
assert.ok(pageHtml.text.indexOf('from the paste API') < pageHtml.text.indexOf('from the page form'), 'newest first');
assert.ok(pageHtml.text.indexOf('from the paste API') < pageHtml.text.indexOf('from the page form'), 'newest first');
- const oldest = await call(`/p/${pub}?sort=old`);
+ const oldest = await call(`/p/${pubUrl}?sort=old`);
assert.ok(oldest.text.indexOf('from the page form') < oldest.text.indexOf('from the paste API'), '?sort=old: oldest first');
assert.ok(oldest.text.indexOf('from the page form') < oldest.text.indexOf('from the paste API'), '?sort=old: oldest first');
- assert.ok(oldest.text.includes(`/auth/login?next=${encodeURIComponent(`/p/${pub}`)}`) && !oldest.text.includes(`action="/p/${pub}/comments"`), 'signed out: a sign-in link, no form');
- const older = await call(`/p/${pub}?sort=new&after=${viaApi.json().comment.id}`);
+ assert.ok(oldest.text.includes(`/auth/login?next=${encodeURIComponent(`/p/${pubUrl}`)}`) && !oldest.text.includes(`action="/p/${pubUrl}/comments"`), 'signed out: a sign-in link, no form');
+ const older = await call(`/p/${pubUrl}?sort=new&after=${viaApi.json().comment.id}`);
assert.ok(older.text.includes('from the page form') && !older.text.includes('from the paste API'), 'paged with ?after=');
assert.ok(older.text.includes('from the page form') && !older.text.includes('from the paste API'), 'paged with ?after=');
// Deleting a comment twice succeeds both times, as before.
// Deleting a comment twice succeeds both times, as before.
const del = await call(`/api/pastes/${pub}/comments`, { method: 'POST', cookie: samJwt, json: { message: 'short-lived' } });
const del = await call(`/api/pastes/${pub}/comments`, { method: 'POST', cookie: samJwt, json: { message: 'short-lived' } });
for (let i = 0; i < 2; i++) assert.strictEqual((await call(`/api/pastes/${pub}/comments/${del.json().comment.id}`, { method: 'DELETE', cookie: samJwt })).status, 200);
for (let i = 0; i < 2; i++) assert.strictEqual((await call(`/api/pastes/${pub}/comments/${del.json().comment.id}`, { method: 'DELETE', cookie: samJwt })).status, 200);
- assert.ok(!(await call(`/p/${pub}`)).text.includes('short-lived'));
+ assert.ok(!(await call(`/p/${pubUrl}`)).text.includes('short-lived'));
});
});
await check('the form: signed in only, never from another site', async () => {
await check('the form: signed in only, never from another site', async () => {
- const anon = await call(`/p/${pub}/comments`, { method: 'POST', form: { message: 'hi' } });
+ const anon = await call(`/p/${pubUrl}/comments`, { method: 'POST', form: { message: 'hi' } });
assert.strictEqual(anon.status, 303);
assert.strictEqual(anon.status, 303);
- assert.strictEqual(anon.headers.get('location'), `/auth/login?next=${encodeURIComponent(`/p/${pub}`)}`);
- const foreign = await call(`/p/${pub}/comments`, { method: 'POST', cookie: samJwt, headers: { origin: 'https://evil.example' }, form: { message: 'hi' } });
+ assert.strictEqual(anon.headers.get('location'), `/auth/login?next=${encodeURIComponent(`/p/${pubUrl}`)}`);
+ const foreign = await call(`/p/${pubUrl}/comments`, { method: 'POST', cookie: samJwt, headers: { origin: 'https://evil.example' }, form: { message: 'hi' } });
assert.strictEqual(foreign.status, 403);
assert.strictEqual(foreign.status, 403);
- const empty = await call(`/p/${pub}/comments`, { method: 'POST', cookie: samJwt, form: { message: ' ' } });
+ const empty = await call(`/p/${pubUrl}/comments`, { method: 'POST', cookie: samJwt, form: { message: ' ' } });
assert.strictEqual(empty.status, 400);
assert.strictEqual(empty.status, 400);
assert.strictEqual((await call('/p/no-such-paste-here/comments', { method: 'POST', cookie: samJwt, form: { message: 'hi' } })).status, 404);
assert.strictEqual((await call('/p/no-such-paste-here/comments', { method: 'POST', cookie: samJwt, form: { message: 'hi' } })).status, 404);
});
});
@@ -68,15 +68,15 @@ const PNG = (() => {
@@ -68,15 +68,15 @@ const PNG = (() => {
const as = (cookie, p) => t.get(p, { cookies: cookie ? [`ov_token=${cookie}`] : [] });
const as = (cookie, p) => t.get(p, { cookies: cookie ? [`ov_token=${cookie}`] : [] });
try {
try {
- let fineSlug;
+ let fineSlug, finePage;
await check('fine: the paste keeps its visibility and serves to everyone', async () => {
await check('fine: the paste keeps its visibility and serves to everyone', async () => {
state.category = 'fine';
state.category = 'fine';
const r = await upload(alexJwt, { title: 'A receipt', visibility: 'public' });
const r = await upload(alexJwt, { title: 'A receipt', visibility: 'public' });
assert.strictEqual(r.status, 201, r.text);
assert.strictEqual(r.status, 201, r.text);
const p = r.json().paste;
const p = r.json().paste;
- fineSlug = p.slug;
+ fineSlug = p.slug; finePage = p.canonical_slug;
assert.deepStrictEqual([p.review_category, p.review_status, p.visibility, p.is_nsfw], ['fine', 'clear', 'public', false]);
assert.deepStrictEqual([p.review_category, p.review_status, p.visibility, p.is_nsfw], ['fine', 'clear', 'public', false]);
- assert.strictEqual((await as(null, `/p/${fineSlug}`)).status, 200);
+ assert.strictEqual((await as(null, `/p/${finePage}`)).status, 200);
assert.ok((await as(null, '/api/pastes')).json().pastes.some((x) => x.slug === fineSlug));
assert.ok((await as(null, '/api/pastes')).json().pastes.some((x) => x.slug === fineSlug));
});
});
@@ -135,7 +135,8 @@ const PNG = (() => {
@@ -135,7 +135,8 @@ const PNG = (() => {
assert.deepStrictEqual([out.checked, out.cleared], [1, 1]);
assert.deepStrictEqual([out.checked, out.cleared], [1, 1]);
const row = await t.db.prepare('SELECT visibility, review_status, is_nsfw FROM pastes WHERE slug = ?').get(pendingSlug);
const row = await t.db.prepare('SELECT visibility, review_status, is_nsfw FROM pastes WHERE slug = ?').get(pendingSlug);
assert.deepStrictEqual([row.visibility, row.review_status, Number(row.is_nsfw)], ['public', 'clear', 0]);
assert.deepStrictEqual([row.visibility, row.review_status, Number(row.is_nsfw)], ['public', 'clear', 0]);
- assert.strictEqual((await as(null, `/p/${pendingSlug}`)).status, 200, 'the public page is back');
+ const cleared = await require('../server/pastes/store').canonicalForRef(t.db, pendingSlug);
+ assert.strictEqual((await as(null, `/p/${cleared}`)).status, 200, 'the public page is back');
assert.ok((await as(null, '/api/pastes')).json().pastes.some((x) => x.slug === pendingSlug));
assert.ok((await as(null, '/api/pastes')).json().pastes.some((x) => x.slug === pendingSlug));
});
});
@@ -162,7 +162,8 @@ const pastesApiModule = require('../server/pastes/api');
@@ -162,7 +162,8 @@ const pastesApiModule = require('../server/pastes/api');
const pub = await make(alexJwt, { title: 'Listed public paste', content: 'public text' });
const pub = await make(alexJwt, { title: 'Listed public paste', content: 'public text' });
const link = await make(alexJwt, { title: 'Link only paste', content: 'link text', visibility: 'unlisted' });
const link = await make(alexJwt, { title: 'Link only paste', content: 'link text', visibility: 'unlisted' });
const people = await make(alexJwt, { title: 'People only paste', content: 'people text', audience: 'people', members: ['@maticus'] });
const people = await make(alexJwt, { title: 'People only paste', content: 'people text', audience: 'people', members: ['@maticus'] });
- assert.strictEqual((await call(`/p/${pub.slug}`)).status, 200);
+ const pubUrl = pub.paste.canonical_slug;
+ assert.strictEqual((await call(`/p/${pubUrl}`)).status, 200);
assert.strictEqual((await call(`/p/${link.slug}`)).status, 200, 'the link is the key');
assert.strictEqual((await call(`/p/${link.slug}`)).status, 200, 'the link is the key');
assert.strictEqual((await call(`/api/pastes/${link.slug}`)).status, 200);
assert.strictEqual((await call(`/api/pastes/${link.slug}`)).status, 200);
assert.strictEqual((await call(`/p/${people.slug}`)).status, 404);
assert.strictEqual((await call(`/p/${people.slug}`)).status, 404);
@@ -178,10 +179,10 @@ const pastesApiModule = require('../server/pastes/api');
@@ -178,10 +179,10 @@ const pastesApiModule = require('../server/pastes/api');
const llms = (await call('/llms-full.txt')).text;
const llms = (await call('/llms-full.txt')).text;
assert.ok(!llms.includes('People only paste'));
assert.ok(!llms.includes('People only paste'));
const sitemap = (await call('/sitemap.xml')).text;
const sitemap = (await call('/sitemap.xml')).text;
- assert.ok(!sitemap.includes(`/p/${pub.slug}`) && !sitemap.includes(`/p/${people.slug}`), 'paste pages are noindex and never in a sitemap');
+ assert.ok(!sitemap.includes(`/p/${pubUrl}`) && !sitemap.includes(`/p/${people.slug}`), 'paste pages are noindex and never in a sitemap');
// The agent twin is anonymous: a people paste has none.
// The agent twin is anonymous: a people paste has none.
assert.strictEqual((await call(`/p/${people.slug}.md`)).status, 404, 'no Markdown twin of a restricted paste');
assert.strictEqual((await call(`/p/${people.slug}.md`)).status, 404, 'no Markdown twin of a restricted paste');
- assert.strictEqual((await call(`/p/${pub.slug}.md`)).status, 200);
+ assert.strictEqual((await call(`/p/${pubUrl}.md`)).status, 200);
});
});
await check('the owner changes the audience; people drops it from the public list at once and the old column follows', async () => {
await check('the owner changes the audience; people drops it from the public list at once and the old column follows', async () => {
@@ -362,7 +363,7 @@ const pastesApiModule = require('../server/pastes/api');
@@ -362,7 +363,7 @@ const pastesApiModule = require('../server/pastes/api');
fd.append('screenshot', new Blob([jpeg], { type: 'image/jpeg' }), 'c.jpg');
fd.append('screenshot', new Blob([jpeg], { type: 'image/jpeg' }), 'c.jpg');
const made = await t.get('/api/pastes/screenshot', { method: 'POST', cookies: [`ov_token=${alexJwt}`], body: fd });
const made = await t.get('/api/pastes/screenshot', { method: 'POST', cookies: [`ov_token=${alexJwt}`], body: fd });
assert.strictEqual(made.status, 201, made.text);
assert.strictEqual(made.status, 201, made.text);
- const slug = made.json().slug;
+ const slug = made.json().paste.canonical_slug;
const home = await call('/');
const home = await call('/');
assert.strictEqual(home.status, 200);
assert.strictEqual(home.status, 200);
assert.match(home.text, new RegExp(`srcset="[^"]*/p/${slug}/screenshot\\?w=320&fmt=webp 320w, [^"]*/p/${slug}/screenshot\\?w=640&fmt=webp 640w"`), 'both card widths');
assert.match(home.text, new RegExp(`srcset="[^"]*/p/${slug}/screenshot\\?w=320&fmt=webp 320w, [^"]*/p/${slug}/screenshot\\?w=640&fmt=webp 640w"`), 'both card widths');
new file mode 100644
new file mode 100644
@@ -0,0 +1,110 @@
@@ -0,0 +1,110 @@
+'use strict';
+/**
+ * The public paste address (URL decisions 2026-11-07): /p/<title-slug>-<public id>. The id is the
+ * address, the title slug decoration: the id alone and every old adjective-noun-number slug 301 to the
+ * one canonical form, a retitle moves it, and a secret paste keeps its secret slug (its id is not a
+ * second key). The page stays noindex and is never in the sitemap or IndexNow.
+ */
+const assert = require('assert');
+const { boot, check, done } = require('./helpers/app');
+
+(async () => {
+ const t = await boot({ pasteLimits: { cooldownSeconds: 0 } });
+ const net = t.network;
+ const alex = net.addUser({ network_user_id: 7, username: 'alex', display_name: 'Alex' });
+ const svc = (cap) => net.signService({ cap });
+ const create = (body) => t.get('/api/pastes', {
+ method: 'POST',
+ headers: { 'content-type': 'application/json', authorization: `Bearer ${svc(['community.paste.create'])}`, 'x-ov-subject': alex.subject_id },
+ body: JSON.stringify(body),
+ });
+ const update = (slug, body) => t.get(`/api/pastes/${encodeURIComponent(slug)}`, {
+ method: 'PUT',
+ headers: { 'content-type': 'application/json', authorization: `Bearer ${svc(['community.paste.write'])}`, 'x-ov-subject': alex.subject_id },
+ body: JSON.stringify(body),
+ });
+ const canonicalOf = (r) => r.json().paste.canonical_slug;
+
+ try {
+ await check('a public paste is named by its title and an 8-char base58 id; the id alone 301s to it', async () => {
+ const r = await create({ title: 'My first paste', content: 'hello' });
+ assert.strictEqual(r.status, 201, r.text);
+ const p = r.json().paste;
+ assert.match(p.canonical_slug, /^my-first-paste-[1-9A-HJ-NP-Za-km-z]{8}$/);
+ assert.strictEqual(r.json().url, `/p/${p.canonical_slug}`);
+ assert.strictEqual(p.url, `/p/${p.canonical_slug}`);
+ assert.strictEqual(p.raw_url, `/p/${p.canonical_slug}/raw`);
+ const id = p.canonical_slug.split('-').pop();
+ const byId = await t.get(`/p/${id}`);
+ assert.deepStrictEqual([byId.status, byId.headers.get('location')], [301, `/p/${p.canonical_slug}`]);
+ const old = await t.get(`/p/${p.slug}?from=old`);
+ assert.deepStrictEqual([old.status, old.headers.get('location')], [301, `/p/${p.canonical_slug}?from=old`]);
+ });
+
+ await check('the page, its Markdown twin and JSON-LD name one canonical; the page stays noindex', async () => {
+ const p = (await create({ title: 'Canonical paste', content: 'body' })).json().paste;
+ const page = await t.get(`/p/${p.canonical_slug}`);
+ assert.strictEqual(page.status, 200);
+ assert.ok(page.text.includes(`<link rel="canonical" href="https://openvibe.community/p/${p.canonical_slug}">`), 'canonical');
+ assert.ok(page.text.includes('<meta name="robots" content="noindex">'), 'pastes stay noindex');
+ assert.ok(page.text.includes(`"url":"https://openvibe.community/p/${p.canonical_slug}"`), 'JSON-LD URL');
+ assert.ok(page.text.includes(`href="/p/${p.canonical_slug}/raw"`), 'Raw points at the canonical address');
+ const twin = await t.get(`/p/${p.canonical_slug}.md`);
+ assert.strictEqual(twin.status, 200, twin.text);
+ assert.match(twin.headers.get('content-type'), /^text\/markdown/);
+ assert.ok(twin.text.includes(`canonical: "https://openvibe.community/p/${p.canonical_slug}"`), 'the twin agrees');
+ });
+
+ await check('a retitle moves the canonical; every old form 301s to the new one', async () => {
+ const p = (await create({ title: 'Old name', content: 'body' })).json().paste;
+ const oldCanonical = p.canonical_slug;
+ const u = await update(p.slug, { title: 'A better name' });
+ assert.strictEqual(u.status, 200, u.text);
+ const next = canonicalOf(u);
+ assert.match(next, /^a-better-name-[1-9A-HJ-NP-Za-km-z]{8}$/);
+ assert.notStrictEqual(next, oldCanonical);
+ for (const ref of [oldCanonical, p.slug, oldCanonical.split('-').pop()]) {
+ const r = await t.get(`/p/${ref}?q=1`);
+ assert.deepStrictEqual([r.status, r.headers.get('location')], [301, `/p/${next}?q=1`], ref);
+ }
+ assert.ok((await t.get('/p/' + next + '.md')).text.includes(`canonical: "https://openvibe.community/p/${next}"`));
+ });
+
+ await check('an untitled paste takes its slug from the first content line, or has none', async () => {
+ const p = (await create({ content: 'const answer = 42\nconsole.log(answer)' })).json().paste;
+ assert.match(p.canonical_slug, /^const-answer-42-[1-9A-HJ-NP-Za-km-z]{8}$/);
+ const bare = (await create({ content: '```\n+++---\n```' })).json().paste; // no ascii words on the first line
+ assert.match(bare.canonical_slug, /^[1-9A-HJ-NP-Za-km-z]{8}$/, 'the id alone when there is no title');
+ assert.strictEqual((await t.get(`/p/${bare.canonical_slug}`)).status, 200);
+ });
+
+ await check('a secret paste keeps its secret slug; its public id is not a second key', async () => {
+ const p = (await create({ title: 'A private note', content: 'secret', visibility: 'unlisted' })).json().paste;
+ assert.match(p.slug, /^[a-z]+-[a-z]+-[A-Za-z0-9]{16}$/);
+ assert.strictEqual(p.canonical_slug, p.slug, 'the secret part stays the address');
+ assert.strictEqual((await t.get(`/p/${p.slug}`)).status, 200);
+ assert.strictEqual((await t.get(`/p/${p.canonical_slug.split('-').pop()}`)).status, 404, 'the id alone does not open a secret paste');
+ });
+
+ await check('a paste minted before the column gets one on first read, then 301s to it', async () => {
+ const p = (await create({ title: 'Legacy paste', content: 'old row' })).json().paste;
+ await t.db.prepare('UPDATE pastes SET public_id = NULL WHERE slug = ?').run(p.slug);
+ const r = await t.get(`/p/${p.slug}`);
+ assert.strictEqual(r.status, 301);
+ const canonical = r.headers.get('location').slice('/p/'.length);
+ assert.match(canonical, /^legacy-paste-[1-9A-HJ-NP-Za-km-z]{8}$/);
+ assert.strictEqual((await t.get(`/p/${canonical}`)).status, 200);
+ const row = await t.db.prepare('SELECT public_id FROM pastes WHERE slug = ?').get(p.slug);
+ assert.strictEqual(row.public_id, canonical.split('-').pop(), 'stored once, not reminted');
+ });
+
+ await check('the sitemap lists no paste pages (noindex content is never advertised)', async () => {
+ const map = await t.get('/sitemap.xml');
+ assert.strictEqual(map.status, 200);
+ assert.ok(!/\/p\//.test(map.text), 'no /p/ row');
+ });
+ } finally {
+ await t.close();
+ }
+ done();
+})().catch((err) => { console.error(err); process.exit(1); });
@@ -42,7 +42,8 @@ const { boot, check, done } = require('./helpers/app');
@@ -42,7 +42,8 @@ const { boot, check, done } = require('./helpers/app');
});
});
assert.strictEqual(r.status, 201, r.text);
assert.strictEqual(r.status, 201, r.text);
const out = r.json();
const out = r.json();
- assert.ok(out.id && out.slug && out.url === `/p/${out.slug}` && out.paste);
+ assert.ok(out.id && out.slug && out.url === `/p/${out.paste.canonical_slug}` && out.paste, 'the answer names the canonical title+id address');
+ assert.match(out.paste.canonical_slug, /^[a-z0-9-]+-[1-9A-HJ-NP-Za-km-z]{8}$/);
assert.notStrictEqual(out.slug, 'my-chosen-slug', 'browsers never pick slugs');
assert.notStrictEqual(out.slug, 'my-chosen-slug', 'browsers never pick slugs');
assert.strictEqual(out.paste.owner_subject, alex.subject_id);
assert.strictEqual(out.paste.owner_subject, alex.subject_id);
assert.strictEqual(out.paste.origin, 'user');
assert.strictEqual(out.paste.origin, 'user');
@@ -236,7 +237,7 @@ const { boot, check, done } = require('./helpers/app');
@@ -236,7 +237,7 @@ const { boot, check, done } = require('./helpers/app');
const r = await call('/api/pastes', { method: 'POST', ip: '203.0.113.90', body: fd });
const r = await call('/api/pastes', { method: 'POST', ip: '203.0.113.90', body: fd });
assert.strictEqual(r.status, 201, r.text);
assert.strictEqual(r.status, 201, r.text);
const out = r.json();
const out = r.json();
- assert.strictEqual(out.url, `/p/${out.slug}`);
+ assert.strictEqual(out.url, `/p/${out.paste.canonical_slug}`);
assert.notStrictEqual(out.paste.type, 'screenshot');
assert.notStrictEqual(out.paste.type, 'screenshot');
assert.strictEqual(out.paste.title, 'Build log');
assert.strictEqual(out.paste.title, 'Build log');
assert.strictEqual(out.paste.content, 'npm ERR! code ERESOLVE');
assert.strictEqual(out.paste.content, 'npm ERR! code ERESOLVE');
@@ -250,7 +251,7 @@ const { boot, check, done } = require('./helpers/app');
@@ -250,7 +251,7 @@ const { boot, check, done } = require('./helpers/app');
Buffer.from([0xff, 0xdb, 0x00, 0x04, 0x00, 0x00]),
Buffer.from([0xff, 0xdb, 0x00, 0x04, 0x00, 0x00]),
Buffer.from([0xff, 0xda, 0x00, 0x04, 0x00, 0x00]), Buffer.from('pixels', 'latin1'), Buffer.from([0xff, 0xd9]),
Buffer.from([0xff, 0xda, 0x00, 0x04, 0x00, 0x00]), Buffer.from('pixels', 'latin1'), Buffer.from([0xff, 0xd9]),
]);
]);
- let shotSlug;
+ let shotSlug, shotUrl;
await check('screenshot upload: metadata stripped, bytes stored in Media with a service token, URL + media_ref kept', async () => {
await check('screenshot upload: metadata stripped, bytes stored in Media with a service token, URL + media_ref kept', async () => {
const fd = new FormData();
const fd = new FormData();
@@ -274,7 +275,7 @@ const { boot, check, done } = require('./helpers/app');
@@ -274,7 +275,7 @@ const { boot, check, done } = require('./helpers/app');
assert.ok(up.v2 && up.kind === 'screenshot' && up.visibility === 'private', 'a v2 Media object: private screenshot');
assert.ok(up.v2 && up.kind === 'screenshot' && up.visibility === 'private', 'a v2 Media object: private screenshot');
assert.strictEqual(up.owner, alex.subject_id, 'owned by the person');
assert.strictEqual(up.owner, alex.subject_id, 'owned by the person');
// The object is private in Media, so the answer names the guarded address, never Media's /o/<id> (a 404 to a browser).
// The object is private in Media, so the answer names the guarded address, never Media's /o/<id> (a 404 to a browser).
- assert.ok(/\/p\/[a-z0-9-]+\/screenshot$/.test(p.screenshot_url) && p.screenshot_url.endsWith(`/p/${p.slug}/screenshot`), p.screenshot_url);
+ assert.ok(/\/p\/[A-Za-z0-9-]+\/screenshot$/.test(p.screenshot_url) && p.screenshot_url.endsWith(`/p/${p.canonical_slug}/screenshot`), p.screenshot_url);
assert.ok(!p.screenshot_url.includes('/o/'), 'never the Media object URL');
assert.ok(!p.screenshot_url.includes('/o/'), 'never the Media object URL');
assert.strictEqual(p.title, 'My desk');
assert.strictEqual(p.title, 'My desk');
assert.strictEqual(p.content, 'look');
assert.strictEqual(p.content, 'look');
@@ -283,7 +284,7 @@ const { boot, check, done } = require('./helpers/app');
@@ -283,7 +284,7 @@ const { boot, check, done } = require('./helpers/app');
assert.strictEqual(meta.original_name, 'desk.jpg');
assert.strictEqual(meta.original_name, 'desk.jpg');
const row = await t.db.prepare('SELECT media_ref FROM pastes WHERE slug = ?').get(p.slug);
const row = await t.db.prepare('SELECT media_ref FROM pastes WHERE slug = ?').get(p.slug);
assert.match(row.media_ref, /^med_[0-9A-HJKMNP-TV-Z]{26}$/); assert.strictEqual(row.media_ref, up.key);
assert.match(row.media_ref, /^med_[0-9A-HJKMNP-TV-Z]{26}$/); assert.strictEqual(row.media_ref, up.key);
- shotSlug = p.slug;
+ shotSlug = p.slug; shotUrl = p.canonical_slug;
const viaCreate = new FormData();
const viaCreate = new FormData();
viaCreate.append('screenshot', new Blob([jpeg], { type: 'image/jpeg' }), 'b.jpg');
viaCreate.append('screenshot', new Blob([jpeg], { type: 'image/jpeg' }), 'b.jpg');
const r2 = await call('/api/pastes', { method: 'POST', cookie: alexJwt, body: viaCreate });
const r2 = await call('/api/pastes', { method: 'POST', cookie: alexJwt, body: viaCreate });
@@ -340,7 +341,7 @@ const { boot, check, done } = require('./helpers/app');
@@ -340,7 +341,7 @@ const { boot, check, done } = require('./helpers/app');
assert.strictEqual(shot.headers.get('content-type'), 'image/jpeg');
assert.strictEqual(shot.headers.get('content-type'), 'image/jpeg');
assert.ok(shot.text.includes('pixels'), 'the proxy serves the bytes');
assert.ok(shot.text.includes('pixels'), 'the proxy serves the bytes');
assert.strictEqual(shot.headers.get('location'), null, 'never a Media URL');
assert.strictEqual(shot.headers.get('location'), null, 'never a Media URL');
- assert.strictEqual((await call(`/p/${shotSlug}/raw`)).headers.get('location'), `/p/${shotSlug}/screenshot`);
+ assert.strictEqual((await call(`/p/${shotSlug}/raw`)).headers.get('location'), `/p/${shotUrl}/screenshot`);
assert.strictEqual((await call(`/p/${alexSlug}/screenshot`)).status, 404);
assert.strictEqual((await call(`/p/${alexSlug}/screenshot`)).status, 404);
const burn = (await call('/api/pastes', { method: 'POST', cookie: alexJwt, json: { content: 'once', burn_after_read: true } })).json().slug;
const burn = (await call('/api/pastes', { method: 'POST', cookie: alexJwt, json: { content: 'once', burn_after_read: true } })).json().slug;
assert.strictEqual((await call(`/p/${burn}/raw`)).text, 'once');
assert.strictEqual((await call(`/p/${burn}/raw`)).text, 'once');
@@ -73,7 +73,7 @@ const { createSearchDocuments } = require('../server/search/documents');
@@ -73,7 +73,7 @@ const { createSearchDocuments } = require('../server/search/documents');
assert.strictEqual(await docs.publishPaste(await row(nsfw.id)), 'sent');
assert.strictEqual(await docs.publishPaste(await row(nsfw.id)), 'sent');
assert.strictEqual(await docs.publishPaste(await row(burn.id)), 'unchanged');
assert.strictEqual(await docs.publishPaste(await row(burn.id)), 'unchanged');
const [a, b] = (await index()).slice(-2); ok(a); ok(b);
const [a, b] = (await index()).slice(-2); ok(a); ok(b);
- assert.strictEqual(a.payload.id, 'amber-fox-42'); assert.strictEqual(a.payload.canonical_url, `${base}/p/amber-fox-42`);
+ assert.strictEqual(a.payload.id, 'amber-fox-42'); assert.strictEqual(a.payload.canonical_url, `${base}/p/quick-sort-${p.public_id}`);
assert.ok(a.payload.body.includes('function sort')); assert.strictEqual(a.payload.facets.syntax, 'javascript');
assert.ok(a.payload.body.includes('function sort')); assert.strictEqual(a.payload.facets.syntax, 'javascript');
assert.ok(!('language' in a.payload), 'a paste\'s syntax is not a human language');
assert.ok(!('language' in a.payload), 'a paste\'s syntax is not a human language');
assert.deepStrictEqual(b.payload.indexability, { decision: 'noindex', reasons: ['sensitive'] });
assert.deepStrictEqual(b.payload.indexability, { decision: 'noindex', reasons: ['sensitive'] });
@@ -113,7 +113,7 @@ const { createSearchDocuments } = require('../server/search/documents');
@@ -113,7 +113,7 @@ const { createSearchDocuments } = require('../server/search/documents');
await db.prepare("INSERT INTO search_doc_pushes (type, id, hash, revision) VALUES ('paste', '-wgXuYn0', 'x', 1)").run();
await db.prepare("INSERT INTO search_doc_pushes (type, id, hash, revision) VALUES ('paste', '-wgXuYn0', 'x', 1)").run();
assert.strictEqual(await docs.publishPaste(await db.prepare('SELECT * FROM pastes WHERE id = ?').get(p.id)), 'sent');
assert.strictEqual(await docs.publishPaste(await db.prepare('SELECT * FROM pastes WHERE id = ?').get(p.id)), 'sent');
const ev = (await index()).at(-1); ok(ev);
const ev = (await index()).at(-1); ok(ev);
- assert.strictEqual(ev.payload.id, `paste_${p.id}`); assert.strictEqual(ev.payload.canonical_url, `${base}/p/-wgXuYn0`);
+ assert.strictEqual(ev.payload.id, `paste_${p.id}`); assert.strictEqual(ev.payload.canonical_url, `${base}/p/dash-${p.public_id}`);
const r = contracts.validate('search.index-document@1', ev.payload); assert.ok(r.valid, JSON.stringify(r.errors));
const r = contracts.validate('search.index-document@1', ev.payload); assert.ok(r.valid, JSON.stringify(r.errors));
const before = (await index()).length;
const before = (await index()).length;
await docs.refresh();
await docs.refresh();
@@ -118,7 +118,7 @@ function inventory() {
@@ -118,7 +118,7 @@ function inventory() {
assert.strictEqual((await send('GET', `/forum/general/${slug}`)).status, 200);
assert.strictEqual((await send('GET', `/forum/general/${slug}`)).status, 200);
const p = await send('POST', '/api/pastes', { json: { title: urls[5], content: `curl ${urls[5]}\n` } });
const p = await send('POST', '/api/pastes', { json: { title: urls[5], content: `curl ${urls[5]}\n` } });
assert.strictEqual(p.status, 201, p.text);
assert.strictEqual(p.status, 201, p.text);
- assert.strictEqual((await send('GET', `/p/${JSON.parse(p.text).slug}`)).status, 200);
+ assert.strictEqual((await send('GET', JSON.parse(p.text).url)).status, 200);
await send('POST', `/api/pastes/${JSON.parse(p.text).slug}/comments`, { json: { message: `look ${urls[4]}` } });
await send('POST', `/api/pastes/${JSON.parse(p.text).slug}/comments`, { json: { message: `look ${urls[4]}` } });
const f = new FormData();
const f = new FormData();
f.append('page_url', urls[6]);
f.append('page_url', urls[6]);
@@ -15,6 +15,8 @@ const { boot, check, done } = require('./helpers/app');
@@ -15,6 +15,8 @@ const { boot, check, done } = require('./helpers/app');
await ins.run('fair-moon-23', sam.subject_id, 'screenshot', 'Desktop shot', 'my desktop', 'text', 'public', 'https://openvibe.media/o/med_FAIRMOON', 40, '2026-09-11 08:00:00', '2026-09-11 08:00:00');
await ins.run('fair-moon-23', sam.subject_id, 'screenshot', 'Desktop shot', 'my desktop', 'text', 'public', 'https://openvibe.media/o/med_FAIRMOON', 40, '2026-09-11 08:00:00', '2026-09-11 08:00:00');
await ins.run('dark-owl-55', alex.subject_id, 'paste', 'secret notes', 'unlisted', 'text', 'unlisted', null, 2, '2026-09-12 08:00:00', '2026-09-12 08:00:00');
await ins.run('dark-owl-55', alex.subject_id, 'paste', 'secret notes', 'unlisted', 'text', 'unlisted', null, 2, '2026-09-12 08:00:00', '2026-09-12 08:00:00');
await ins.run('grim-vale-61', alex.subject_id, 'paste', 'private thing', 'private', 'text', 'private', null, 0, '2026-09-10 08:00:00', '2026-09-10 08:00:00');
await ins.run('grim-vale-61', alex.subject_id, 'paste', 'private thing', 'private', 'text', 'private', null, 0, '2026-09-10 08:00:00', '2026-09-10 08:00:00');
+ const canon = async (slug) => await require('../server/pastes/store').canonicalForRef(t.db, slug);
+ const amber = await canon('amber-fox-42'), shot = await canon('fair-moon-23');
await check('robots.txt allows crawling, blocks the private routes, points at the sitemap', async () => {
await check('robots.txt allows crawling, blocks the private routes, points at the sitemap', async () => {
const r = await t.get('/robots.txt');
const r = await t.get('/robots.txt');
@@ -58,7 +60,7 @@ const { boot, check, done } = require('./helpers/app');
@@ -58,7 +60,7 @@ const { boot, check, done } = require('./helpers/app');
assert.strictEqual(full.status, 200);
assert.strictEqual(full.status, 200);
assert.ok(full.headers.get('content-type').startsWith('text/plain'));
assert.ok(full.headers.get('content-type').startsWith('text/plain'));
assert.strictEqual(full.headers.get('cache-control'), cache.htmlHeaders({ maxAge: 3600 }));
assert.strictEqual(full.headers.get('cache-control'), cache.htmlHeaders({ maxAge: 3600 }));
- assert.ok(full.text.includes('### Hello world in JavaScript\n\nURL: https://openvibe.community/p/amber-fox-42\n\nconst x = 1;'));
+ assert.ok(full.text.includes(`### Hello world in JavaScript\n\nURL: https://openvibe.community/p/${amber}\n\nconst x = 1;`));
assert.ok(!full.text.includes('dark-owl-55') && !full.text.includes('grim-vale-61') && !full.text.includes('secret notes'));
assert.ok(!full.text.includes('dark-owl-55') && !full.text.includes('grim-vale-61') && !full.text.includes('secret notes'));
// The catalog keeps a short preview for a few seconds. Discovery must read the
// The catalog keeps a short preview for a few seconds. Discovery must read the
@@ -100,12 +102,12 @@ const { boot, check, done } = require('./helpers/app');
@@ -100,12 +102,12 @@ const { boot, check, done } = require('./helpers/app');
});
});
await check('a noindex page says so in the meta tag and in X-Robots-Tag; an indexable page sends no such header', async () => {
await check('a noindex page says so in the meta tag and in X-Robots-Tag; an indexable page sends no such header', async () => {
- for (const path of ['/p/amber-fox-42', '/p/fair-moon-23', '/c/cth_0000000000000000000000', '/nope-not-a-page']) {
+ for (const path of [`/p/${amber}`, `/p/${shot}`, '/c/cth_0000000000000000000000', '/nope-not-a-page']) {
const r = await t.get(path);
const r = await t.get(path);
assert.ok(r.headers.get('x-robots-tag') && /(?:^|,)\s*noindex/.test(r.headers.get('x-robots-tag')),
assert.ok(r.headers.get('x-robots-tag') && /(?:^|,)\s*noindex/.test(r.headers.get('x-robots-tag')),
`${path}: X-Robots-Tag (got ${r.headers.get('x-robots-tag')})`);
`${path}: X-Robots-Tag (got ${r.headers.get('x-robots-tag')})`);
}
}
- assert.strictEqual((await t.get('/p/amber-fox-42')).headers.get('x-robots-tag'), 'noindex');
+ assert.strictEqual((await t.get(`/p/${amber}`)).headers.get('x-robots-tag'), 'noindex');
for (const path of ['/', '/pastes', '/updates']) {
for (const path of ['/', '/pastes', '/updates']) {
assert.strictEqual((await t.get(path)).headers.get('x-robots-tag'), null, path);
assert.strictEqual((await t.get(path)).headers.get('x-robots-tag'), null, path);
}
}
@@ -115,7 +117,7 @@ const { boot, check, done } = require('./helpers/app');
@@ -115,7 +117,7 @@ const { boot, check, done } = require('./helpers/app');
const r = await t.get('/feed.xml');
const r = await t.get('/feed.xml');
assert.strictEqual(r.status, 200);
assert.strictEqual(r.status, 200);
assert.ok(r.text.includes('<rss version="2.0"'));
assert.ok(r.text.includes('<rss version="2.0"'));
- assert.ok(r.text.includes('<link>https://openvibe.community/p/amber-fox-42</link>'));
+ assert.ok(r.text.includes(`<link>https://openvibe.community/p/${amber}</link>`));
assert.ok(r.text.includes('<atom:link href="https://openvibe.community/feed.xml" rel="self" type="application/rss+xml"/>'));
assert.ok(r.text.includes('<atom:link href="https://openvibe.community/feed.xml" rel="self" type="application/rss+xml"/>'));
assert.ok(r.text.includes('<author>Alex</author>'));
assert.ok(r.text.includes('<author>Alex</author>'));
assert.ok(r.text.includes('<pubDate>Tue, 15 Sep 2026 10:00:00 GMT</pubDate>'));
assert.ok(r.text.includes('<pubDate>Tue, 15 Sep 2026 10:00:00 GMT</pubDate>'));
@@ -124,11 +126,11 @@ const { boot, check, done } = require('./helpers/app');
@@ -124,11 +126,11 @@ const { boot, check, done } = require('./helpers/app');
});
});
await check('a page head has the canonical, Open Graph, Twitter and ai-summary tags from openvibe-shared/seo', async () => {
await check('a page head has the canonical, Open Graph, Twitter and ai-summary tags from openvibe-shared/seo', async () => {
- const r = await t.get('/p/amber-fox-42');
+ const r = await t.get(`/p/${amber}`);
assert.strictEqual(r.status, 200);
assert.strictEqual(r.status, 200);
const head = r.text.split('</head>')[0];
const head = r.text.split('</head>')[0];
- assert.ok(head.includes('<link rel="canonical" href="https://openvibe.community/p/amber-fox-42">'));
- assert.ok(head.includes('<meta property="og:url" content="https://openvibe.community/p/amber-fox-42">'));
+ assert.ok(head.includes(`<link rel="canonical" href="https://openvibe.community/p/${amber}">`));
+ assert.ok(head.includes(`<meta property="og:url" content="https://openvibe.community/p/${amber}">`));
assert.ok(head.includes('<meta property="og:title" content="Hello world in JavaScript · OpenVibe.Community">'));
assert.ok(head.includes('<meta property="og:title" content="Hello world in JavaScript · OpenVibe.Community">'));
assert.ok(head.includes('<meta property="og:site_name" content="OpenVibe.Community">'));
assert.ok(head.includes('<meta property="og:site_name" content="OpenVibe.Community">'));
assert.match(head, /<meta property="og:description" content="[^"]+">/);
assert.match(head, /<meta property="og:description" content="[^"]+">/);
@@ -156,13 +158,13 @@ const { boot, check, done } = require('./helpers/app');
@@ -156,13 +158,13 @@ const { boot, check, done } = require('./helpers/app');
});
});
await check('HTML pages carry the private page policy; raw and download never reach a shared cache', async () => {
await check('HTML pages carry the private page policy; raw and download never reach a shared cache', async () => {
- for (const path of ['/', '/pastes', '/p/amber-fox-42', '/updates', '/nope-not-a-page']) {
+ for (const path of ['/', '/pastes', `/p/${amber}`, '/updates', '/nope-not-a-page']) {
const r = await t.get(path);
const r = await t.get(path);
assert.ok(r.headers.get('content-type').startsWith('text/html'), path);
assert.ok(r.headers.get('content-type').startsWith('text/html'), path);
assert.strictEqual(r.headers.get('cache-control'), cache.htmlHeaders({ private: true }), path);
assert.strictEqual(r.headers.get('cache-control'), cache.htmlHeaders({ private: true }), path);
}
}
- assert.strictEqual((await t.get('/p/amber-fox-42/raw')).headers.get('cache-control'), 'private, no-store');
- assert.strictEqual((await t.get('/p/amber-fox-42/download')).headers.get('cache-control'), 'private, no-store');
+ assert.strictEqual((await t.get(`/p/${amber}/raw`)).headers.get('cache-control'), 'private, no-store');
+ assert.strictEqual((await t.get(`/p/${amber}/download`)).headers.get('cache-control'), 'private, no-store');
});
});
await check('JSON-LD builders (server/render/jsonld.js): absolute breadcrumbs, a paste Article, AI never a person', async () => {
await check('JSON-LD builders (server/render/jsonld.js): absolute breadcrumbs, a paste Article, AI never a person', async () => {
@@ -15,29 +15,33 @@ const { boot, check, done } = require('./helpers/app');
@@ -15,29 +15,33 @@ const { boot, check, done } = require('./helpers/app');
const asAlex = { kind: 'user', subject: alex.subject_id, staff: false, origin: 'user' };
const asAlex = { kind: 'user', subject: alex.subject_id, staff: false, origin: 'user' };
const anon = { kind: 'anonymous', subject: null, staff: false, origin: 'user' };
const anon = { kind: 'anonymous', subject: null, staff: false, origin: 'user' };
- const js = (await svc.createText(asAlex, { title: 'Hello world in JavaScript', content: 'const greet = (name) => `Hi ${name}`;\n', language: 'javascript' })).slug;
- const nginx = (await svc.createText(anon, { title: 'nginx snippet', content: 'server {\n listen 80;\n}\n', language: 'nginx' })).slug;
- const py = (await svc.createText(asAlex, { title: 'python helper', content: 'def add(a, b):\n return a + b\n', language: 'python' })).slug;
+ const jsP = await svc.createText(asAlex, { title: 'Hello world in JavaScript', content: 'const greet = (name) => `Hi ${name}`;\n', language: 'javascript' });
+ const nginxP = await svc.createText(anon, { title: 'nginx snippet', content: 'server {\n listen 80;\n}\n', language: 'nginx' });
+ const pyP = await svc.createText(asAlex, { title: 'python helper', content: 'def add(a, b):\n return a + b\n', language: 'python' });
const unl = (await svc.createText(asAlex, { title: 'secret notes', content: 'unlisted <script>alert(1)</script>', visibility: 'unlisted' })).slug;
const unl = (await svc.createText(asAlex, { title: 'secret notes', content: 'unlisted <script>alert(1)</script>', visibility: 'unlisted' })).slug;
const priv = (await svc.createText(asAlex, { title: 'private thing', content: 'private', visibility: 'private' })).slug;
const priv = (await svc.createText(asAlex, { title: 'private thing', content: 'private', visibility: 'private' })).slug;
const burn = (await svc.createText(asAlex, { title: 'burn me', content: 'once only', burn_after_read: true })).slug;
const burn = (await svc.createText(asAlex, { title: 'burn me', content: 'once only', burn_after_read: true })).slug;
+ const js = jsP.slug, jsUrl = jsP.paste.canonical_slug;
+ const nginx = nginxP.slug, nginxUrl = nginxP.paste.canonical_slug;
+ const py = pyP.slug, pyUrl = pyP.paste.canonical_slug;
await t.db.prepare('UPDATE pastes SET views = 900 WHERE slug = ?').run(nginx);
await t.db.prepare('UPDATE pastes SET views = 900 WHERE slug = ?').run(nginx);
await t.db.prepare("INSERT INTO pastes (slug, owner_subject, type, title, content, screenshot_url) VALUES ('fair-moon-23', ?, 'screenshot', 'Desktop shot', 'my desktop', 'https://openvibe.media/f/abc-desk.png')").run(alex.subject_id);
await t.db.prepare("INSERT INTO pastes (slug, owner_subject, type, title, content, screenshot_url) VALUES ('fair-moon-23', ?, 'screenshot', 'Desktop shot', 'my desktop', 'https://openvibe.media/f/abc-desk.png')").run(alex.subject_id);
+ const shotUrl = await require('../server/pastes/store').canonicalForRef(t.db, 'fair-moon-23');
await check('home: latest + most viewed come from the store; nothing hidden; Live is never asked', async () => {
await check('home: latest + most viewed come from the store; nothing hidden; Live is never asked', async () => {
const r = await t.get('/');
const r = await t.get('/');
assert.strictEqual(r.status, 200);
assert.strictEqual(r.status, 200);
- has(r.text, `href="/p/${js}"`);
+ has(r.text, `href="/p/${jsUrl}"`);
// The most viewed are the home's quick filter (/pastes?sort=views), read from the same store.
// The most viewed are the home's quick filter (/pastes?sort=views), read from the same store.
has(r.text, 'href="/pastes?sort=views"');
has(r.text, 'href="/pastes?sort=views"');
const views = await t.get('/pastes?sort=views');
const views = await t.get('/pastes?sort=views');
- assert.ok(views.text.indexOf(nginx) > 0 && views.text.indexOf(nginx) < views.text.indexOf(js), 'most viewed first');
+ assert.ok(views.text.indexOf(nginxUrl) > 0 && views.text.indexOf(nginxUrl) < views.text.indexOf(jsUrl), 'most viewed first');
assert.ok(![r.text, views.text].some((html) => html.includes(unl) || html.includes(priv)));
assert.ok(![r.text, views.text].some((html) => html.includes(unl) || html.includes(priv)));
assert.strictEqual(t.live.calls.filter((c) => c.path.startsWith('/api/pastes')).length, 0);
assert.strictEqual(t.live.calls.filter((c) => c.path.startsWith('/api/pastes')).length, 0);
});
});
await check('paste page: rendered from the store, view counted, author from the projection', async () => {
await check('paste page: rendered from the store, view counted, author from the projection', async () => {
- const r = await t.get(`/p/${js}`, { headers: { 'x-forwarded-for': '198.51.100.20' } });
+ const r = await t.get(`/p/${jsUrl}`, { headers: { 'x-forwarded-for': '198.51.100.20' } });
assert.strictEqual(r.status, 200);
assert.strictEqual(r.status, 200);
has(r.text, '<title>Hello world in JavaScript · OpenVibe.Community</title>');
has(r.text, '<title>Hello world in JavaScript · OpenVibe.Community</title>');
has(r.text, 'class="hljs-keyword">const</span>');
has(r.text, 'class="hljs-keyword">const</span>');
@@ -72,54 +76,54 @@ const { boot, check, done } = require('./helpers/app');
@@ -72,54 +76,54 @@ const { boot, check, done } = require('./helpers/app');
await check('download reads the store without a view; screenshots use the stored Media URL', async () => {
await check('download reads the store without a view; screenshots use the stored Media URL', async () => {
const before = (await t.db.prepare('SELECT views FROM pastes WHERE slug = ?').get(py)).views;
const before = (await t.db.prepare('SELECT views FROM pastes WHERE slug = ?').get(py)).views;
- const dl = await t.get(`/p/${py}/download`);
+ const dl = await t.get(`/p/${pyUrl}/download`);
assert.strictEqual(dl.status, 200);
assert.strictEqual(dl.status, 200);
- assert.strictEqual(dl.headers.get('content-disposition'), `attachment; filename="${py}.py"`);
+ assert.strictEqual(dl.headers.get('content-disposition'), `attachment; filename="${pyUrl}.py"`);
assert.ok(dl.text.startsWith('def add'));
assert.ok(dl.text.startsWith('def add'));
assert.strictEqual((await t.db.prepare('SELECT views FROM pastes WHERE slug = ?').get(py)).views, before);
assert.strictEqual((await t.db.prepare('SELECT views FROM pastes WHERE slug = ?').get(py)).views, before);
- const shot = await t.get('/p/fair-moon-23');
- assert.match(shot.text, /<meta property="og:image" content="[^"]*\/p\/fair-moon-23\/screenshot">/);
- const dls = await t.get('/p/fair-moon-23/download');
- assert.strictEqual(dls.headers.get('location'), '/p/fair-moon-23/screenshot');
+ const shot = await t.get(`/p/${shotUrl}`);
+ has(shot.text, `<meta property="og:image" content="https://openvibe.community/p/${shotUrl}/screenshot">`);
+ const dls = await t.get(`/p/${shotUrl}/download`);
+ assert.strictEqual(dls.headers.get('location'), `/p/${shotUrl}/screenshot`);
});
});
await check('browse: search, views sort and language filter over the store', async () => {
await check('browse: search, views sort and language filter over the store', async () => {
const q = await t.get('/pastes?q=nginx');
const q = await t.get('/pastes?q=nginx');
- has(q.text, `href="/p/${nginx}"`);
- assert.ok(!q.text.includes(`href="/p/${js}"`));
+ has(q.text, `href="/p/${nginxUrl}"`);
+ assert.ok(!q.text.includes(`href="/p/${jsUrl}"`));
const views = await t.get('/pastes?sort=views');
const views = await t.get('/pastes?sort=views');
const res = views.text.slice(views.text.indexOf('id="results"'));
const res = views.text.slice(views.text.indexOf('id="results"'));
- assert.ok(res.indexOf(nginx) < res.indexOf(js));
+ assert.ok(res.indexOf(nginxUrl) < res.indexOf(jsUrl));
const lang = await t.get('/pastes?lang=python');
const lang = await t.get('/pastes?lang=python');
- has(lang.text, `href="/p/${py}"`);
- assert.ok(!lang.text.includes(`href="/p/${js}"`));
+ has(lang.text, `href="/p/${pyUrl}"`);
+ assert.ok(!lang.text.includes(`href="/p/${jsUrl}"`));
});
});
await check('a Markdown paste reads as a document, its source a click away; code pastes are unchanged', async () => {
await check('a Markdown paste reads as a document, its source a click away; code pastes are unchanged', async () => {
- const doc = (await svc.createText(anon, { title: 'Notes', content: '# Notes!\n\n### Part\n\n| a | b |\n|---|--:|\n| 1 | 2 |\n\n- x\n - y\n\n<script>alert(1)</script>', language: 'markdown' })).slug;
- const r = await t.get(`/p/${doc}`);
+ const docP = await svc.createText(anon, { title: 'Notes', content: '# Notes!\n\n### Part\n\n| a | b |\n|---|--:|\n| 1 | 2 |\n\n- x\n - y\n\n<script>alert(1)</script>', language: 'markdown' });
+ const r = await t.get(`/p/${docP.paste.canonical_slug}`);
assert.strictEqual(r.status, 200);
assert.strictEqual(r.status, 200);
// Its opening # only repeats the title (the page's h1), so it is left out and ### leads at h2.
// Its opening # only repeats the title (the page's h1), so it is left out and ### leads at h2.
has(r.text, '<div class="md paste-doc"><h2>Part</h2>');
has(r.text, '<div class="md paste-doc"><h2>Part</h2>');
- const other = (await svc.createText(anon, { title: 'Release', content: '# Something else\n\ntext', language: 'markdown' })).slug;
- has((await t.get(`/p/${other}`)).text, '<div class="md paste-doc"><h2>Something else</h2>');
+ const otherP = await svc.createText(anon, { title: 'Release', content: '# Something else\n\ntext', language: 'markdown' });
+ has((await t.get(`/p/${otherP.paste.canonical_slug}`)).text, '<div class="md paste-doc"><h2>Something else</h2>');
has(r.text, '<td class="al-r">2</td>');
has(r.text, '<td class="al-r">2</td>');
has(r.text, '<li>x<ul><li>y</li></ul></li>');
has(r.text, '<li>x<ul><li>y</li></ul></li>');
has(r.text, '<details class="paste-source">');
has(r.text, '<details class="paste-source">');
has(r.text, '<div class="code-wrap wrap" data-code tabindex="0" role="region" aria-label="Markdown source">');
has(r.text, '<div class="code-wrap wrap" data-code tabindex="0" role="region" aria-label="Markdown source">');
assert.ok(!r.text.includes('<script>alert(1)</script>'), 'raw HTML stays text');
assert.ok(!r.text.includes('<script>alert(1)</script>'), 'raw HTML stays text');
- const code = await t.get(`/p/${js}`);
+ const code = await t.get(`/p/${jsUrl}`);
assert.ok(!code.text.includes('paste-doc') && code.text.includes('<div class="code-wrap" data-code'), 'code: no document, no default wrap');
assert.ok(!code.text.includes('paste-doc') && code.text.includes('<div class="code-wrap" data-code'), 'code: no document, no default wrap');
});
});
await check('a link that picked up trailing punctuation (**, a full stop, a bracket) redirects to the paste', async () => {
await check('a link that picked up trailing punctuation (**, a full stop, a bracket) redirects to the paste', async () => {
for (const [path, to] of [
for (const [path, to] of [
- [`/p/${js}**`, `/p/${js}`],
- [`/p/${js}.`, `/p/${js}`],
- [`/p/${js})`, `/p/${js}`],
- [`/p/${js}%2A%2A`, `/p/${js}`],
- [`/p/${js}),/raw?dl=1`, `/p/${js}/raw?dl=1`],
- [`/p/${js}./download`, `/p/${js}/download`],
+ [`/p/${jsUrl}**`, `/p/${jsUrl}`],
+ [`/p/${jsUrl}.`, `/p/${jsUrl}`],
+ [`/p/${jsUrl})`, `/p/${jsUrl}`],
+ [`/p/${jsUrl}%2A%2A`, `/p/${jsUrl}`],
+ [`/p/${jsUrl}),/raw?dl=1`, `/p/${jsUrl}/raw?dl=1`],
+ [`/p/${jsUrl}./download`, `/p/${jsUrl}/download`],
]) {
]) {
const r = await t.get(path);
const r = await t.get(path);
assert.strictEqual(r.status, 301, path);
assert.strictEqual(r.status, 301, path);
@@ -127,15 +131,16 @@ const { boot, check, done } = require('./helpers/app');
@@ -127,15 +131,16 @@ const { boot, check, done } = require('./helpers/app');
}
}
// Nothing left to point at, or junk in the middle: still "Paste not found".
// Nothing left to point at, or junk in the middle: still "Paste not found".
assert.strictEqual((await t.get('/p/**')).status, 404);
assert.strictEqual((await t.get('/p/**')).status, 404);
- assert.strictEqual((await t.get(`/p/${js.slice(0, 4)}*${js.slice(4)}`)).status, 404);
- assert.strictEqual((await t.get(`/p/${js}`)).status, 200);
+ // The slug part is decoration: a corrupt middle still resolves by the id at the end.
+ assert.deepStrictEqual([(await t.get(`/p/${jsUrl.slice(0, 4)}*${jsUrl.slice(4)}`)).status, (await t.get(`/p/${jsUrl.slice(0, 4)}*${jsUrl.slice(4)}`)).headers.get('location')], [301, `/p/${jsUrl}`]);
+ assert.strictEqual((await t.get(`/p/${jsUrl}`)).status, 200);
});
});
await check('/my lists the signed-in person\'s pastes, private and unlisted included', async () => {
await check('/my lists the signed-in person\'s pastes, private and unlisted included', async () => {
const mine = await t.get('/my', { cookies: [`ov_token=${token}`] });
const mine = await t.get('/my', { cookies: [`ov_token=${token}`] });
assert.strictEqual(mine.status, 200);
assert.strictEqual(mine.status, 200);
- for (const slug of [js, py, unl, priv]) has(mine.text, `href="/p/${slug}"`);
- assert.ok(!mine.text.includes(nginx));
+ for (const slug of [jsUrl, pyUrl, unl, priv]) has(mine.text, `href="/p/${slug}"`);
+ assert.ok(!mine.text.includes(nginxUrl));
});
});
await check('no-JS form creates in the store as the signed-in subject; anonymous posts share the 20/10min budget', async () => {
await check('no-JS form creates in the store as the signed-in subject; anonymous posts share the 20/10min budget', async () => {
@@ -160,7 +165,7 @@ const { boot, check, done } = require('./helpers/app');
@@ -160,7 +165,7 @@ const { boot, check, done } = require('./helpers/app');
await check('the feed links a paste, but no paste is in the sitemap (they are link-shared, noindex)', async () => {
await check('the feed links a paste, but no paste is in the sitemap (they are link-shared, noindex)', async () => {
const feed = await t.get('/feed.xml');
const feed = await t.get('/feed.xml');
- has(feed.text, `/p/${js}`);
+ has(feed.text, `/p/${jsUrl}`);
assert.ok(!feed.text.includes(priv));
assert.ok(!feed.text.includes(priv));
const map = await t.get('/sitemap.xml');
const map = await t.get('/sitemap.xml');
assert.ok(!map.text.includes(`/p/${js}`) && !map.text.includes(`/p/${unl}`) && !map.text.includes(`/p/${priv}`));
assert.ok(!map.text.includes(`/p/${js}`) && !map.text.includes(`/p/${unl}`) && !map.text.includes(`/p/${priv}`));
@@ -18,6 +18,12 @@ const { boot, check, done } = require('./helpers/app');
@@ -18,6 +18,12 @@ const { boot, check, done } = require('./helpers/app');
await ins.run('dark-owl-55', alex.subject_id, 'paste', 'secret notes', 'unlisted <script>alert(1)</script>', 'text', 'unlisted', null, 2, 0, '2026-09-12 08:00:00', '2026-09-12 08:00:00');
await ins.run('dark-owl-55', alex.subject_id, 'paste', 'secret notes', 'unlisted <script>alert(1)</script>', 'text', 'unlisted', null, 2, 0, '2026-09-12 08:00:00', '2026-09-12 08:00:00');
await ins.run('fair-moon-23', sam.subject_id, 'screenshot', 'Desktop shot', 'my desktop', 'text', 'public', 'https://openvibe.media/o/med_FAIRMOON', 40, 0, '2026-09-11 08:00:00', '2026-09-11 08:00:00');
await ins.run('fair-moon-23', sam.subject_id, 'screenshot', 'Desktop shot', 'my desktop', 'text', 'public', 'https://openvibe.media/o/med_FAIRMOON', 40, 0, '2026-09-11 08:00:00', '2026-09-11 08:00:00');
await ins.run('grim-vale-61', alex.subject_id, 'paste', 'private thing', 'private', 'text', 'private', null, 0, 0, '2026-09-10 08:00:00', '2026-09-10 08:00:00');
await ins.run('grim-vale-61', alex.subject_id, 'paste', 'private thing', 'private', 'text', 'private', null, 0, 0, '2026-09-10 08:00:00', '2026-09-10 08:00:00');
+ // The canonical title+id address each public fixture is served at (the old slugs 301 there).
+ const canon = async (slug) => await require('../server/pastes/store').canonicalForRef(t.db, slug);
+ const amberSlug = await canon('amber-fox-42');
+ const blueSlug = await canon('blue-lake-17');
+ const shotSlug = await canon('fair-moon-23');
+ const coldSlug = await canon('cold-ridge-88');
await check('home renders hero, the latest pastes as a rail, the forum, roadmap and questions with full SEO head', async () => {
await check('home renders hero, the latest pastes as a rail, the forum, roadmap and questions with full SEO head', async () => {
const r = await t.get('/');
const r = await t.get('/');
@@ -45,13 +51,13 @@ const { boot, check, done } = require('./helpers/app');
@@ -45,13 +51,13 @@ const { boot, check, done } = require('./helpers/app');
has(r.text, '<section class="ovk-rail ovk-rail--m" id="latest" aria-labelledby="latest-h">');
has(r.text, '<section class="ovk-rail ovk-rail--m" id="latest" aria-labelledby="latest-h">');
has(r.text, '<h2 id="latest-h">Latest pastes</h2>');
has(r.text, '<h2 id="latest-h">Latest pastes</h2>');
has(r.text, '<a class="ovk-rail-more" href="/pastes">See all');
has(r.text, '<a class="ovk-rail-more" href="/pastes">See all');
- assert.ok(/<article class="ovk-card ovk-card--post"[^>]*>[\s\S]*?href="\/p\/amber-fox-42"/.test(r.text), 'a paste is a kit card linking its page');
+ assert.ok(r.text.includes(`href="/p/${amberSlug}"`) && /<article class="ovk-card ovk-card--post"/.test(r.text), 'a paste is a kit card linking its page');
has(r.text, 'href="/pastes?sort=views"', 'the most viewed are a quick filter');
has(r.text, 'href="/pastes?sort=views"', 'the most viewed are a quick filter');
has(r.text, '<details class="ovk-fold" id="api">');
has(r.text, '<details class="ovk-fold" id="api">');
assert.ok(!/<h2>Most viewed<\/h2>|id="trending"|id="spaces"|class="sc-cta"|class="sc-sec sc-network"/.test(r.text), 'no shelf repeats another (most viewed, What lives here, the closing call, Part of OpenVibe)');
assert.ok(!/<h2>Most viewed<\/h2>|id="trending"|id="spaces"|class="sc-cta"|class="sc-sec sc-network"/.test(r.text), 'no shelf repeats another (most viewed, What lives here, the closing call, Part of OpenVibe)');
// Most viewed (/pastes?sort=views) puts the 900-view nginx paste first.
// Most viewed (/pastes?sort=views) puts the 900-view nginx paste first.
const views = await t.get('/pastes?sort=views');
const views = await t.get('/pastes?sort=views');
- assert.ok(views.text.indexOf('blue-lake-17') > 0 && views.text.indexOf('blue-lake-17') < views.text.indexOf('amber-fox-42'), 'most viewed first');
+ assert.ok(views.text.indexOf(blueSlug) > 0 && views.text.indexOf(blueSlug) < views.text.indexOf(amberSlug), 'most viewed first');
// Unlisted/private fixtures never show on shared pages.
// Unlisted/private fixtures never show on shared pages.
assert.ok(![r.text, views.text].some((html) => html.includes('dark-owl-55') || html.includes('grim-vale-61')));
assert.ok(![r.text, views.text].some((html) => html.includes('dark-owl-55') || html.includes('grim-vale-61')));
// Navbar/footer init payload matches the shared-chrome contract.
// Navbar/footer init payload matches the shared-chrome contract.
@@ -104,7 +110,7 @@ const { boot, check, done } = require('./helpers/app');
@@ -104,7 +110,7 @@ const { boot, check, done } = require('./helpers/app');
});
});
await check('every page carries the boost marker and script, and the navbar signs in back to the current page', async () => {
await check('every page carries the boost marker and script, and the navbar signs in back to the current page', async () => {
- for (const url of ['/', '/pastes', '/new', '/p/amber-fox-42']) {
+ for (const url of ['/', '/pastes', '/new', `/p/${amberSlug}`]) {
const r = await t.get(url);
const r = await t.get(url);
assert.strictEqual(r.status, 200, url);
assert.strictEqual(r.status, 200, url);
assert.match(r.text, /<meta name="ov-boost" content="community@[^"]+">/, `${url}: the boost marker`);
assert.match(r.text, /<meta name="ov-boost" content="community@[^"]+">/, `${url}: the boost marker`);
@@ -116,10 +122,10 @@ const { boot, check, done } = require('./helpers/app');
@@ -116,10 +122,10 @@ const { boot, check, done } = require('./helpers/app');
await check('paste page: SEO head, Article JSON-LD with author/datePublished, highlighted body, actions, related', async () => {
await check('paste page: SEO head, Article JSON-LD with author/datePublished, highlighted body, actions, related', async () => {
const before = (await t.db.prepare('SELECT views FROM pastes WHERE slug = ?').get('amber-fox-42')).views;
const before = (await t.db.prepare('SELECT views FROM pastes WHERE slug = ?').get('amber-fox-42')).views;
- const r = await t.get('/p/amber-fox-42');
+ const r = await t.get(`/p/${amberSlug}`);
assert.strictEqual(r.status, 200);
assert.strictEqual(r.status, 200);
has(r.text, '<title>Hello world in JavaScript · OpenVibe.Community</title>');
has(r.text, '<title>Hello world in JavaScript · OpenVibe.Community</title>');
- has(r.text, '<link rel="canonical" href="https://openvibe.community/p/amber-fox-42">');
+ has(r.text, `<link rel="canonical" href="https://openvibe.community/p/${amberSlug}">`);
has(r.text, '<meta name="robots" content="noindex">');
has(r.text, '<meta name="robots" content="noindex">');
has(r.text, '<meta property="og:type" content="article">');
has(r.text, '<meta property="og:type" content="article">');
has(r.text, 'article:published_time');
has(r.text, 'article:published_time');
@@ -131,8 +137,8 @@ const { boot, check, done } = require('./helpers/app');
@@ -131,8 +137,8 @@ const { boot, check, done } = require('./helpers/app');
assert.strictEqual(article.hasPart.programmingLanguage, 'javascript');
assert.strictEqual(article.hasPart.programmingLanguage, 'javascript');
assert.ok(ld.some((o) => o['@type'] === 'BreadcrumbList'));
assert.ok(ld.some((o) => o['@type'] === 'BreadcrumbList'));
has(r.text, 'class="hljs-keyword">const</span>', 'server-side highlighted');
has(r.text, 'class="hljs-keyword">const</span>', 'server-side highlighted');
- has(r.text, 'href="/p/amber-fox-42/raw"');
- has(r.text, 'href="/p/amber-fox-42/download"');
+ has(r.text, `href="/p/${amberSlug}/raw"`);
+ has(r.text, `href="/p/${amberSlug}/download"`);
has(r.text, 'data-copy-content="amber-fox-42"');
has(r.text, 'data-copy-content="amber-fox-42"');
has(r.text, 'data-ov-share-sheet', 'the one Share sheet (sharing pilot, 2026-11-06)');
has(r.text, 'data-ov-share-sheet', 'the one Share sheet (sharing pilot, 2026-11-06)');
assert.ok(!r.text.includes('twitter.com/intent/tweet'), 'no hand-made X link any more');
assert.ok(!r.text.includes('twitter.com/intent/tweet'), 'no hand-made X link any more');
@@ -155,11 +161,11 @@ const { boot, check, done } = require('./helpers/app');
@@ -155,11 +161,11 @@ const { boot, check, done } = require('./helpers/app');
});
});
await check('screenshot paste: og:image and the picture go through the checked proxy, never the Media URL', async () => {
await check('screenshot paste: og:image and the picture go through the checked proxy, never the Media URL', async () => {
- const r = await t.get('/p/fair-moon-23');
- assert.match(r.text, /<meta property="og:image" content="[^"]*\/p\/fair-moon-23\/screenshot">/, 'og:image is the proxy');
+ const r = await t.get(`/p/${shotSlug}`);
+ assert.match(r.text, /<meta property="og:image" content="[^"]*\/p\/(?:desktop-shot-)?[A-Za-z0-9-]+\/screenshot">/, 'og:image is the proxy');
has(r.text, '<meta name="twitter:card" content="summary_large_image">');
has(r.text, '<meta name="twitter:card" content="summary_large_image">');
has(r.text, '"@type":"ImageObject"');
has(r.text, '"@type":"ImageObject"');
- has(r.text, '<img src="/p/fair-moon-23/screenshot"');
+ has(r.text, `<img src="/p/${shotSlug}/screenshot"`);
assert.ok(!r.text.includes('openvibe.media/o/med_FAIRMOON'), 'the Media URL never reaches the HTML (sharing pilot)');
assert.ok(!r.text.includes('openvibe.media/o/med_FAIRMOON'), 'the Media URL never reaches the HTML (sharing pilot)');
});
});
@@ -174,16 +180,16 @@ const { boot, check, done } = require('./helpers/app');
@@ -174,16 +180,16 @@ const { boot, check, done } = require('./helpers/app');
});
});
await check('raw serves the text from the store; the screenshot goes through the proxy; download is an attachment without a view', async () => {
await check('raw serves the text from the store; the screenshot goes through the proxy; download is an attachment without a view', async () => {
- const raw = await t.get('/p/amber-fox-42/raw');
+ const raw = await t.get(`/p/${amberSlug}/raw`);
assert.strictEqual(raw.status, 200);
assert.strictEqual(raw.status, 200);
assert.ok(raw.text.startsWith('const greet'));
assert.ok(raw.text.startsWith('const greet'));
- const shot = await t.get('/p/fair-moon-23/screenshot');
+ const shot = await t.get(`/p/${shotSlug}/screenshot`);
assert.notStrictEqual(shot.status, 302, 'the screenshot is streamed by the checked proxy, not a redirect to Media');
assert.notStrictEqual(shot.status, 302, 'the screenshot is streamed by the checked proxy, not a redirect to Media');
assert.ok(!String(shot.headers.get('location') || '').includes('openvibe.media'), 'no Media URL in a redirect');
assert.ok(!String(shot.headers.get('location') || '').includes('openvibe.media'), 'no Media URL in a redirect');
const before = (await t.db.prepare('SELECT views FROM pastes WHERE slug = ?').get('amber-fox-42')).views;
const before = (await t.db.prepare('SELECT views FROM pastes WHERE slug = ?').get('amber-fox-42')).views;
- const dl = await t.get('/p/amber-fox-42/download');
+ const dl = await t.get(`/p/${amberSlug}/download`);
assert.strictEqual(dl.status, 200);
assert.strictEqual(dl.status, 200);
- assert.strictEqual(dl.headers.get('content-disposition'), 'attachment; filename="amber-fox-42.js"');
+ assert.strictEqual(dl.headers.get('content-disposition'), `attachment; filename="${amberSlug}.js"`);
assert.ok(dl.text.startsWith('const greet'));
assert.ok(dl.text.startsWith('const greet'));
assert.strictEqual((await t.db.prepare('SELECT views FROM pastes WHERE slug = ?').get('amber-fox-42')).views, before, 'download does not count a view');
assert.strictEqual((await t.db.prepare('SELECT views FROM pastes WHERE slug = ?').get('amber-fox-42')).views, before, 'download does not count a view');
});
});
@@ -192,17 +198,17 @@ const { boot, check, done } = require('./helpers/app');
@@ -192,17 +198,17 @@ const { boot, check, done } = require('./helpers/app');
const all = await t.get('/pastes');
const all = await t.get('/pastes');
assert.strictEqual(all.status, 200);
assert.strictEqual(all.status, 200);
has(all.text, '<link rel="canonical" href="https://openvibe.community/pastes">');
has(all.text, '<link rel="canonical" href="https://openvibe.community/pastes">');
- has(all.text, 'href="/p/blue-lake-17"');
+ has(all.text, `href="/p/${blueSlug}"`);
const q = await t.get('/pastes?q=nginx');
const q = await t.get('/pastes?q=nginx');
has(q.text, '<meta name="robots" content="noindex,follow">');
has(q.text, '<meta name="robots" content="noindex,follow">');
- has(q.text, 'href="/p/blue-lake-17"');
- assert.ok(!q.text.includes('href="/p/amber-fox-42"'));
+ has(q.text, `href="/p/${blueSlug}"`);
+ assert.ok(!q.text.includes(`href="/p/${amberSlug}"`));
const views = await t.get('/pastes?sort=views');
const views = await t.get('/pastes?sort=views');
const res = views.text.slice(views.text.indexOf('id="results"'));
const res = views.text.slice(views.text.indexOf('id="results"'));
- assert.ok(res.indexOf('blue-lake-17') < res.indexOf('amber-fox-42') && res.indexOf('amber-fox-42') < res.indexOf('cold-ridge-88'));
+ assert.ok(res.indexOf(blueSlug) < res.indexOf(amberSlug) && res.indexOf(amberSlug) < res.indexOf(coldSlug));
const py = await t.get('/pastes?lang=python');
const py = await t.get('/pastes?lang=python');
- has(py.text, 'href="/p/cold-ridge-88"');
- assert.ok(!py.text.includes('href="/p/amber-fox-42"'));
+ has(py.text, `href="/p/${coldSlug}"`);
+ assert.ok(!py.text.includes(`href="/p/${amberSlug}"`));
has(py.text, '<option value="python" selected>');
has(py.text, '<option value="python" selected>');
const old = await t.get('/pastes/amber-fox-42');
const old = await t.get('/pastes/amber-fox-42');
assert.strictEqual(old.status, 301);
assert.strictEqual(old.status, 301);
@@ -216,8 +222,8 @@ const { boot, check, done } = require('./helpers/app');
@@ -216,8 +222,8 @@ const { boot, check, done } = require('./helpers/app');
const mine = await t.get('/my', { cookies: [`ov_token=${token}`] });
const mine = await t.get('/my', { cookies: [`ov_token=${token}`] });
assert.strictEqual(mine.status, 200);
assert.strictEqual(mine.status, 200);
has(mine.text, '<meta name="robots" content="noindex,nofollow">');
has(mine.text, '<meta name="robots" content="noindex,nofollow">');
- for (const slug of ['amber-fox-42', 'cold-ridge-88', 'dark-owl-55', 'grim-vale-61']) has(mine.text, `href="/p/${slug}"`);
- assert.ok(!mine.text.includes('blue-lake-17'));
+ for (const slug of [amberSlug, coldSlug, 'dark-owl-55', 'grim-vale-61']) has(mine.text, `href="/p/${slug}"`);
+ assert.ok(!mine.text.includes(blueSlug));
});
});
await check('/new renders the form (private option only when signed in) and the no-JS post creates in the store', async () => {
await check('/new renders the form (private option only when signed in) and the no-JS post creates in the store', async () => {
@@ -37,7 +37,7 @@ async function rejects(p, status, message) {
@@ -37,7 +37,7 @@ async function rejects(p, status, message) {
const out = await svc.createText(alice, { title: ' Hello ', content: 'const x = 1;\n ', language: 'auto', visibility: 'public' });
const out = await svc.createText(alice, { title: ' Hello ', content: 'const x = 1;\n ', language: 'auto', visibility: 'public' });
assert.match(out.slug, /^[a-z]+-[a-z]+-\d{2}$/);
assert.match(out.slug, /^[a-z]+-[a-z]+-\d{2}$/);
assert.deepStrictEqual(Object.keys(out), ['id', 'slug', 'url', 'paste']);
assert.deepStrictEqual(Object.keys(out), ['id', 'slug', 'url', 'paste']);
- assert.strictEqual(out.url, `/p/${out.slug}`);
+ assert.strictEqual(out.url, `/p/${out.paste.canonical_slug}`);
const p = out.paste;
const p = out.paste;
assert.strictEqual(p.title, 'Hello');
assert.strictEqual(p.title, 'Hello');
assert.strictEqual(p.content, 'const x = 1;');
assert.strictEqual(p.content, 'const x = 1;');
@@ -49,7 +49,7 @@ async function rejects(p, status, message) {
@@ -49,7 +49,7 @@ async function rejects(p, status, message) {
assert.strictEqual(p.display_name, 'Alice');
assert.strictEqual(p.display_name, 'Alice');
assert.strictEqual(p.avatar_url, 'https://openvibe.media/a.png');
assert.strictEqual(p.avatar_url, 'https://openvibe.media/a.png');
assert.strictEqual(p.profile_color, '#f0a');
assert.strictEqual(p.profile_color, '#f0a');
- assert.strictEqual(p.raw_url, `/p/${out.slug}/raw`);
+ assert.strictEqual(p.raw_url, `/p/${p.canonical_slug}/raw`);
assert.strictEqual(p.is_owner, true);
assert.strictEqual(p.is_owner, true);
for (const k of ['views', 'copies', 'likes', 'unique_views']) assert.strictEqual(p[k], 0);
for (const k of ['views', 'copies', 'likes', 'unique_views']) assert.strictEqual(p[k], 0);
pub = out.slug;
pub = out.slug;