openvibers/openvibe.community Commits

Pastes: title+8-char id addresses, 301s from old slugs, no IndexNow for noindex pages (#287)

OP OpenVibers committed 6 days ago · f5dea948e5 · 29 files, +505 −174

* Pastes: title+8-char id addresses, 301s from old slugs, no IndexNow for noindex pages

* Paste feeds and llms-full use the canonical address; N-1 accepts the permanent move

* The feed test follows the canonical paste address

Changes

ddiff --git a/README.md b/README.md
iindex 764cf31..859d1d2 100644
---- a/README.md
++++ b/README.md
@@@ -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 paste
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;
`/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).
ddiff --git a/migrations/0021_paste_public_id.sql b/migrations/0021_paste_public_id.sql
nnew file mode 100644
iindex 0000000..af1f9b0
---- /dev/null
++++ b/migrations/0021_paste_public_id.sql
@@@ -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;
ddiff --git a/public/js/community.js b/public/js/community.js
iindex 33f879f..4742054 100644
---- a/public/js/community.js
++++ b/public/js/community.js
@@@ -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;
ddiff --git a/scripts/backfill-paste-public-ids.js b/scripts/backfill-paste-public-ids.js
nnew file mode 100755
iindex 0000000..5580bbf
---- /dev/null
++++ b/scripts/backfill-paste-public-ids.js
@@@ -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; });
ddiff --git a/server/app.js b/server/app.js
iindex cf58d6e..2245d8e 100644
---- a/server/app.js
++++ b/server/app.js
@@@ -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 || ''));
ddiff --git a/server/discovery.js b/server/discovery.js
iindex 8afce2f..5e3530a 100644
---- a/server/discovery.js
++++ b/server/discovery.js
@@@ -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) };
ddiff --git a/server/events.js b/server/events.js
iindex 2443ff1..980b9bc 100644
---- a/server/events.js
++++ b/server/events.js
@@@ -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) {
ddiff --git a/server/pastes/service.js b/server/pastes/service.js
iindex a30c9b5..1782636 100644
---- a/server/pastes/service.js
++++ b/server/pastes/service.js
@@@ -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 = {}, lim
const 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 = {}, lim
locked,
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 = {}, lim
ai_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 = {}, lim
async 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 = {}, lim
async 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);
ddiff --git a/server/pastes/store.js b/server/pastes/store.js
iindex e261428..2d24413 100644
---- a/server/pastes/store.js
++++ b/server/pastes/store.js
@@@ -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 transaction
return 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,
ddiff --git a/server/pastes/url.js b/server/pastes/url.js
nnew file mode 100644
iindex 0000000..eedb311
---- /dev/null
++++ b/server/pastes/url.js
@@@ -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 };
ddiff --git a/server/render/jsonld.js b/server/render/jsonld.js
iindex 5327d3e..0efc3f0 100644
---- a/server/render/jsonld.js
++++ b/server/render/jsonld.js
@@@ -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',
ddiff --git a/server/render/pages.js b/server/render/pages.js
iindex 79b4ffa..ca597d6 100644
---- a/server/render/pages.js
++++ b/server/render/pages.js
@@@ -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,
});
ddiff --git a/server/search/documents.js b/server/search/documents.js
iindex 162a6bd..44e5e52 100644
---- a/server/search/documents.js
++++ b/server/search/documents.js
@@@ -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),
ddiff --git a/server/sharing.js b/server/sharing.js
iindex 7e36774..07d84a0 100644
---- a/server/sharing.js
++++ b/server/sharing.js
@@@ -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;
};
ddiff --git a/test/events.test.js b/test/events.test.js
iindex ebd5c0b..68b07f7 100644
---- a/test/events.test.js
++++ b/test/events.test.js
@@@ -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);
ddiff --git a/test/forum-style.test.js b/test/forum-style.test.js
iindex cacccad..645bea5 100644
---- a/test/forum-style.test.js
++++ b/test/forum-style.test.js
@@@ -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'));
ddiff --git a/test/indexnow.test.js b/test/indexnow.test.js
iindex ca3640f..25164de 100644
---- a/test/indexnow.test.js
++++ b/test/indexnow.test.js
@@@ -48,12 +48,11 @@ const json = (body) => ({ method: 'POST', headers: { 'content-type': 'applicatio
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' }));
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': 'applicatio
const 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();
ddiff --git a/test/n-1/harness.js b/test/n-1/harness.js
iindex ea9b899..58f94fb 100644
---- a/test/n-1/harness.js
++++ b/test/n-1/harness.js
@@@ -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));
ddiff --git a/test/paste-comments.test.js b/test/paste-comments.test.js
iindex 8db086f..fb05024 100644
---- a/test/paste-comments.test.js
++++ b/test/paste-comments.test.js
@@@ -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);
});
ddiff --git a/test/paste-review.test.js b/test/paste-review.test.js
iindex a1f1932..1f8dd0a 100644
---- a/test/paste-review.test.js
++++ b/test/paste-review.test.js
@@@ -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));
});
ddiff --git a/test/paste-sharing.test.js b/test/paste-sharing.test.js
iindex 56a6fd1..632c9af 100644
---- a/test/paste-sharing.test.js
++++ b/test/paste-sharing.test.js
@@@ -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&amp;fmt=webp 320w, [^"]*/p/${slug}/screenshot\\?w=640&amp;fmt=webp 640w"`), 'both card widths');
ddiff --git a/test/paste-urls.test.js b/test/paste-urls.test.js
nnew file mode 100644
iindex 0000000..74bab5f
---- /dev/null
++++ b/test/paste-urls.test.js
@@@ -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); });
ddiff --git a/test/pastes-api.test.js b/test/pastes-api.test.js
iindex 31b6be6..cfc55d9 100644
---- a/test/pastes-api.test.js
++++ b/test/pastes-api.test.js
@@@ -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');
ddiff --git a/test/search-documents.test.js b/test/search-documents.test.js
iindex 63b0797..6228f9c 100644
---- a/test/search-documents.test.js
++++ b/test/search-documents.test.js
@@@ -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();
ddiff --git a/test/security-ssrf.test.js b/test/security-ssrf.test.js
iindex 2d64293..b684e77 100644
---- a/test/security-ssrf.test.js
++++ b/test/security-ssrf.test.js
@@@ -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![x](${urls[2]})` } });
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]);
ddiff --git a/test/seo.test.js b/test/seo.test.js
iindex 231c11e..9418a9b 100644
---- a/test/seo.test.js
++++ b/test/seo.test.js
@@@ -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 () => {
ddiff --git a/test/ssr-community.test.js b/test/ssr-community.test.js
iindex 7a77eb9..49774a3 100644
---- a/test/ssr-community.test.js
++++ b/test/ssr-community.test.js
@@@ -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}`));
ddiff --git a/test/ssr.test.js b/test/ssr.test.js
iindex 1cbbe0a..9a795eb 100644
---- a/test/ssr.test.js
++++ b/test/ssr.test.js
@@@ -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 () => {
ddiff --git a/test/store.test.js b/test/store.test.js
iindex 992eab0..8e175b8 100644
---- a/test/store.test.js
++++ b/test/store.test.js
@@@ -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;