openvibers/openvibe.space Commits
Forum links follow Community's move to /forum (#222)
Changes
@@@ -2,6 +2,10 @@## Unreleased++- **The forum's links follow Community's move to `/forum`** (design 2026-11-07): Space's forum redirect now++ maps `/s` and `/s/*` straight to `/forum` and `/forum/<space>/<thread>` (the `/t/` segment is gone) instead++ of passing the old path on, so a bookmark lands in one hop. The home page, both navbars and `llms.txt` link++ `https://openvibe.community/forum`; the forum API paths still answer 308 unchanged.- **Official example spaces** (plan §4 Space: "Next: official example spaces"): three published examples owned by the official OpenVibe account — a link page (`ov-links`), a portfolio (`ov-portfolio`) and a monthly community night with a live RSVP list (`ov-night`) — adapted from the Space kit's `site-links`, `site-portfolio` and `site-event` templates and kept in `seeds/official-spaces/`. `node scripts/seed-official-spaces.js` creates and publishes them through the service's own modules (`server/spaces/official.js`); it is idempotent, `--revise` repairs drift, and a slug somebody else holds is reported and never taken. `spaces.create` now accepts an explicit `slug` (same checks as a generated one). `test/official-spaces.test.js`.- **Share:** a space's page carries the network's one Share button (`data-ov-share-sheet`) in its actions; it shares the published space's own address (or the page's address for a draft), with its name and description. `test/spaces.test.js`.- **Guest drafts** (openvibe.website, plan 5.9: "making websites and code without needing OpenVibe logins"): a visitor who is not signed in makes a draft on openvibe.space (`GET/POST /spaces/guest`) from a website template or a sentence, and edits it with a secret link (`/spaces/<id>/edit?key=…`; the cookie only remembers it, the link alone works). The draft has no owner and of the secret only its sha256 is kept. It is static: never published, no visitor's data, no server functions; in the preview the SDK answers "sign in to use this" where a visitor's account would be needed. A signed-in person who holds the secret claims it (`POST /spaces/<id>/claim`): it becomes theirs, the secret is cleared, and `space.app.claimed` is logged. The limits (per address, per draft, per service) and the seven-day expiry are in `server/hosting-config.js`; the abuse screen (`server/spaces/content-check.js`) refuses a phishing page and any form in a guest draft that posts to another origin.@@@ -55,16 +55,16 @@ The forum lived here from 3 October 2026 (it moved off Community then). On **202moved it back**: OpenVibe.Community owns the forum (spaces, threads, posts, votes, categories,per-space moderators, the Discord relay) again, and Space became the network's hosting (static sites now; dynamic pages and spaces next).--So Space keeps no forum code and **every forum URL answers a permanent redirect to the same path and--query on https://openvibe.community**:++So Space keeps no forum code and **every forum URL answers a permanent redirect to the forum's++address on https://openvibe.community** (which moved from `/s` to `/forum` on 2026-11-07):--| Path | Methods | Status |--| --- | --- | --- |--| `/s`, `/s/*` (pages, feeds) | GET, HEAD | `301` |--| `/s`, `/s/*` | anything else | `308` (the method and body are preserved) |--| `/api/v1/spaces*`, `/api/v1/posts/*`, `/api/v1/space-groups*`, `/api/v1/relay*` | all | `308` |++| Path | Methods | Status | Goes to |++| --- | --- | --- | --- |++| `/s`, `/s/*` (pages, feeds) | GET, HEAD | `301` | `/forum/<space>/<thread>` (the `/t/` segment is gone) |++| `/s`, `/s/*` | anything else | `308` (the method and body are preserved) | the same `/forum` path |++| `/api/v1/spaces*`, `/api/v1/posts/*`, `/api/v1/space-groups*`, `/api/v1/relay*` | all | `308` | the same API path |--A redirected bookmark, crawler or API client therefore lands on Community's copy of the same page.++A redirected bookmark, crawler or API client therefore lands on Community's copy in one hop.Nothing that is not a forum path redirects.## Routes@@@ -12,9 +12,10 @@* GET /robots.txt, /llms.txt, /llms-full.txt, /sitemap.xml crawler endpoints (the sitemap is / alone)* GET /api/health, /api/ready, /release.json, /metrics (loopback)*-- * The forum is OpenVibe.Community's again (owner decision, 2026-10-08): every forum URL here answers-- * a permanent redirect to the same path and query there — /s and /s/* (301 for GET and HEAD, 308-- * otherwise), and /api/v1/spaces*, /api/v1/posts/*, /api/v1/space-groups* and /api/v1/relay* (308).++ * The forum is OpenVibe.Community's again (owner decision, 2026-10-08), and it lives at /forum there++ * (2026-11-07): every forum URL here answers a permanent redirect to it — /s and /s/* (301 for GET and++ * HEAD, 308 otherwise), mapped to /forum/<space>/<thread>, and /api/v1/spaces*, /api/v1/posts/*,++ * /api/v1/space-groups* and /api/v1/relay* (308, path unchanged).** Space holds hosted content and emits hosting lifecycle events. The historical forum rows belong* to Community; the forum redirects below stay intact. See README.md.@@@ -44,16 +45,29 @@ const FORUM_PAGE = /^\/s(\/|$)/;const FORUM_API = /^\/api\/v1\/(spaces|posts|space-groups|relay)(\/|$)/;/**-- * Permanent redirect to the same path and query on Community. A GET or HEAD of a page keeps its-- * method with a 301; anything else (a form post, a vote) must not be replayed as a GET, and the-- * APIs are called with bodies, so they always answer 308.++ * Permanent redirect to the forum's address on Community. The forum moved from /s (spaces) to /forum++ * and dropped a thread's /t/ segment (2026-11-07), so /s/general/t/123 becomes /forum/general/123 —++ * one hop, never a second redirect. A GET or HEAD of a page keeps its method with a 301; anything else++ * (a form post, a vote) must not be replayed as a GET, and the APIs are called with bodies, so they++ * always answer 308.*/++function forumTarget(url) {++ const qi = url.indexOf('?');++ const pathname = qi === -1 ? url : url.slice(0, qi);++ const query = qi === -1 ? '' : url.slice(qi);++ if (pathname === '/s') return `/forum${query}`;++ let rest = pathname.replace(/\/+$/, '');++ if (rest === '/s/new-space') return `/forum/new${query}`;++ rest = rest.replace(/^\/s\/([^/]+)\/t(?=\/|$)/, '/forum/$1').replace(/^\/s(?=\/|$)/, '/forum');++ return `${rest}${query}`;++}++function forumRedirect(req, res, next) {const isPage = FORUM_PAGE.test(req.path);if (!isPage && !FORUM_API.test(req.path)) return next();const method = String(req.method || 'GET').toUpperCase();const status = isPage && (method === 'GET' || method === 'HEAD') ? 301 : 308;-- res.redirect(status, `${config.communityUrl}${req.originalUrl}`);++ res.redirect(status, `${config.communityUrl}${isPage ? forumTarget(req.originalUrl) : req.originalUrl}`);}async function createApp(opts = {}) {@@@ -28,7 +28,7 @@ function llmsSections() {{ title: 'Make a space', url: `${u}/spaces/new`, html: true, note: 'describe it to the AI builder, or start from a template (sign-in required)' },] },{ title: 'The network', links: [-- { title: 'The forum', url: `${config.communityUrl}/s`, note: 'OpenVibe.Community hosts the forum' },++ { title: 'The forum', url: `${config.communityUrl}/forum`, note: 'OpenVibe.Community hosts the forum' },{ title: 'OpenVibe.Network', url: config.networkUrl, note: 'identity, accounts and sign-in' },] },];@@@ -70,7 +70,7 @@ function navbarInit(opts) {{ label: 'Home', href: '/', active: opts.active === 'home' },{ label: 'Spaces', href: '/spaces', active: opts.active === 'spaces' },{ label: 'Hosting', href: '/dashboard' },-- { label: 'The forum', href: `${config.communityUrl}/s` },++ { label: 'The forum', href: `${config.communityUrl}/forum` },],menu: { after: [{ label: 'OpenVibe.Network', href: NETWORK_URL, icon: 'fa-globe' }] },history: { type: opts.historyType || 'page', title: opts.historyTitle || opts.title },@@@ -96,7 +96,7 @@ function footerInit(opts) {heading: 'Space',items: [{ name: 'Home', url: '/' },-- { name: 'The forum', url: `${config.communityUrl}/s` },++ { name: 'The forum', url: `${config.communityUrl}/forum` },{ name: 'OpenVibe.Community', url: config.communityUrl },{ name: 'Source code', url: 'https://github.com/OpenVibers/OpenVibe.Space' },],@@@ -12,7 +12,7 @@ const sc = require('openvibe-shared/showcase');const { renderPage, escapeHtml: esc } = require('./layout');const { pickBlock, foldBlock } = require('./short');--const FORUM_URL = `${config.communityUrl}/s`;++const FORUM_URL = `${config.communityUrl}/forum`;const FORUM_LABEL = FORUM_URL.replace(/^https?:\/\//, '');const SERVICES_URL = 'https://openvibe.services/';const TEMPLATES = require('../spaces/templates').list();@@@ -70,16 +70,16 @@ async function check(name, fn) {assert.match(html, /<title>OpenVibe\.Space — spaces and static hosting<\/title>/);assert.match(html, /<h1>Make a space\.<span class="sc-accent"> An app on your OpenVibe account\.<\/span><\/h1>/);assert.match(html, /The forum moved to OpenVibe\.Community/);-- assert.match(html, /href="https:\/\/openvibe\.community\/s"/);++ assert.match(html, /href="https:\/\/openvibe\.community\/forum"/);});await check('every forum URL is a permanent redirect to OpenVibe.Community', async () => {const s = await fetch(`${base}/s`, { redirect: 'manual' });assert.strictEqual(s.status, 301);-- assert.strictEqual(s.headers.get('location'), 'https://openvibe.community/s');++ assert.strictEqual(s.headers.get('location'), 'https://openvibe.community/forum');const nested = await fetch(`${base}/s/general/t/1?x=2`, { redirect: 'manual' });assert.strictEqual(nested.status, 301);-- assert.strictEqual(nested.headers.get('location'), 'https://openvibe.community/s/general/t/1?x=2');++ assert.strictEqual(nested.headers.get('location'), 'https://openvibe.community/forum/general/1?x=2');const api = await fetch(`${base}/api/v1/spaces/general/threads`, { redirect: 'manual' });assert.strictEqual(api.status, 308);assert.strictEqual(api.headers.get('location'), 'https://openvibe.community/api/v1/spaces/general/threads');@@@ -32,7 +32,7 @@ const { boot, check, done } = require('./helpers/app');assert.match(r.text, /What works now/);assert.match(r.text, /showcase\.css/, 'built from the network\'s showcase sections');assert.match(r.text, /The forum moved to OpenVibe\.Community/);-- assert.match(r.text, /href="https:\/\/openvibe\.community\/s"/, 'the forum link points at Community');++ assert.match(r.text, /href="https:\/\/openvibe\.community\/forum"/, 'the forum link points at Community');assert.match(r.text, /<link rel="canonical" href="https:\/\/openvibe\.space\/">/);assert.doesNotMatch(r.text, /href="\/s"/, 'nothing links a /s that only redirects');});@@@ -74,7 +74,7 @@ const { boot, check, done } = require('./helpers/app');const r = await t.get('/llms.txt');assert.strictEqual(r.status, 200);assert.match(r.text, /OpenVibe\.Space/);-- assert.match(r.text, /https:\/\/openvibe\.community\/s/);++ assert.match(r.text, /https:\/\/openvibe\.community\/forum/);assert.doesNotMatch(r.text, /\/s\/feed\.xml/, 'no forum feed is advertised');});@@@ -13,24 +13,33 @@ const COMMUNITY = 'https://openvibe.community';(async () => {const t = await boot();-- await check('GET and HEAD of /s and its nested paths answer 301 to Community, path and query intact', async () => {-- for (const path of ['/s', '/s?sort=new', '/s/general', '/s/general/t/123?x=1&y=two', '/s/feed.xml', '/s/general/feed.xml']) {++ await check('GET and HEAD of /s and its nested paths answer 301 to the forum on Community, mapped to /forum', async () => {++ const map = [++ ['/s', '/forum'],++ ['/s?sort=new', '/forum?sort=new'],++ ['/s/general', '/forum/general'],++ ['/s/general/t/123?x=1&y=two', '/forum/general/123?x=1&y=two'],++ ['/s/feed.xml', '/forum/feed.xml'],++ ['/s/general/feed.xml', '/forum/general/feed.xml'],++ ['/s/new-space', '/forum/new'],++ ];++ for (const [path, to] of map) {const r = await t.get(path);assert.strictEqual(r.status, 301, path);-- assert.strictEqual(r.headers.get('location'), `${COMMUNITY}${path}`, path);++ assert.strictEqual(r.headers.get('location'), `${COMMUNITY}${to}`, path);}const head = await t.get('/s?sort=top', { method: 'HEAD' });assert.strictEqual(head.status, 301);-- assert.strictEqual(head.headers.get('location'), `${COMMUNITY}/s?sort=top`);++ assert.strictEqual(head.headers.get('location'), `${COMMUNITY}/forum?sort=top`);});await check('other methods of /s and /s/* answer 308 (the method and body are preserved)', async () => {const post = await t.get('/s/general/threads?q=1', { method: 'POST', body: 'title=x', headers: { 'content-type': 'application/x-www-form-urlencoded' } });assert.strictEqual(post.status, 308);-- assert.strictEqual(post.headers.get('location'), `${COMMUNITY}/s/general/threads?q=1`);++ assert.strictEqual(post.headers.get('location'), `${COMMUNITY}/forum/general/threads?q=1`);const del = await t.get('/s/general/t/123/posts/9', { method: 'DELETE' });assert.strictEqual(del.status, 308);-- assert.strictEqual(del.headers.get('location'), `${COMMUNITY}/s/general/t/123/posts/9`);++ assert.strictEqual(del.headers.get('location'), `${COMMUNITY}/forum/general/123/posts/9`);});await check('the forum APIs answer 308 to Community, path and query intact', async () => {
@@ -2,6 +2,10 @@
@@ -2,6 +2,10 @@
## Unreleased
## Unreleased
+- **The forum's links follow Community's move to `/forum`** (design 2026-11-07): Space's forum redirect now
+ maps `/s` and `/s/*` straight to `/forum` and `/forum/<space>/<thread>` (the `/t/` segment is gone) instead
+ of passing the old path on, so a bookmark lands in one hop. The home page, both navbars and `llms.txt` link
+ `https://openvibe.community/forum`; the forum API paths still answer 308 unchanged.
- **Official example spaces** (plan §4 Space: "Next: official example spaces"): three published examples owned by the official OpenVibe account — a link page (`ov-links`), a portfolio (`ov-portfolio`) and a monthly community night with a live RSVP list (`ov-night`) — adapted from the Space kit's `site-links`, `site-portfolio` and `site-event` templates and kept in `seeds/official-spaces/`. `node scripts/seed-official-spaces.js` creates and publishes them through the service's own modules (`server/spaces/official.js`); it is idempotent, `--revise` repairs drift, and a slug somebody else holds is reported and never taken. `spaces.create` now accepts an explicit `slug` (same checks as a generated one). `test/official-spaces.test.js`.
- **Official example spaces** (plan §4 Space: "Next: official example spaces"): three published examples owned by the official OpenVibe account — a link page (`ov-links`), a portfolio (`ov-portfolio`) and a monthly community night with a live RSVP list (`ov-night`) — adapted from the Space kit's `site-links`, `site-portfolio` and `site-event` templates and kept in `seeds/official-spaces/`. `node scripts/seed-official-spaces.js` creates and publishes them through the service's own modules (`server/spaces/official.js`); it is idempotent, `--revise` repairs drift, and a slug somebody else holds is reported and never taken. `spaces.create` now accepts an explicit `slug` (same checks as a generated one). `test/official-spaces.test.js`.
- **Share:** a space's page carries the network's one Share button (`data-ov-share-sheet`) in its actions; it shares the published space's own address (or the page's address for a draft), with its name and description. `test/spaces.test.js`.
- **Share:** a space's page carries the network's one Share button (`data-ov-share-sheet`) in its actions; it shares the published space's own address (or the page's address for a draft), with its name and description. `test/spaces.test.js`.
- **Guest drafts** (openvibe.website, plan 5.9: "making websites and code without needing OpenVibe logins"): a visitor who is not signed in makes a draft on openvibe.space (`GET/POST /spaces/guest`) from a website template or a sentence, and edits it with a secret link (`/spaces/<id>/edit?key=…`; the cookie only remembers it, the link alone works). The draft has no owner and of the secret only its sha256 is kept. It is static: never published, no visitor's data, no server functions; in the preview the SDK answers "sign in to use this" where a visitor's account would be needed. A signed-in person who holds the secret claims it (`POST /spaces/<id>/claim`): it becomes theirs, the secret is cleared, and `space.app.claimed` is logged. The limits (per address, per draft, per service) and the seven-day expiry are in `server/hosting-config.js`; the abuse screen (`server/spaces/content-check.js`) refuses a phishing page and any form in a guest draft that posts to another origin.
- **Guest drafts** (openvibe.website, plan 5.9: "making websites and code without needing OpenVibe logins"): a visitor who is not signed in makes a draft on openvibe.space (`GET/POST /spaces/guest`) from a website template or a sentence, and edits it with a secret link (`/spaces/<id>/edit?key=…`; the cookie only remembers it, the link alone works). The draft has no owner and of the secret only its sha256 is kept. It is static: never published, no visitor's data, no server functions; in the preview the SDK answers "sign in to use this" where a visitor's account would be needed. A signed-in person who holds the secret claims it (`POST /spaces/<id>/claim`): it becomes theirs, the secret is cleared, and `space.app.claimed` is logged. The limits (per address, per draft, per service) and the seven-day expiry are in `server/hosting-config.js`; the abuse screen (`server/spaces/content-check.js`) refuses a phishing page and any form in a guest draft that posts to another origin.
@@ -55,16 +55,16 @@ The forum lived here from 3 October 2026 (it moved off Community then). On **202
@@ -55,16 +55,16 @@ The forum lived here from 3 October 2026 (it moved off Community then). On **202
moved it back**: OpenVibe.Community owns the forum (spaces, threads, posts, votes, categories,
moved it back**: OpenVibe.Community owns the forum (spaces, threads, posts, votes, categories,
per-space moderators, the Discord relay) again, and Space became the network's hosting (static sites now; dynamic pages and spaces next).
per-space moderators, the Discord relay) again, and Space became the network's hosting (static sites now; dynamic pages and spaces next).
-So Space keeps no forum code and **every forum URL answers a permanent redirect to the same path and
-query on https://openvibe.community**:
+So Space keeps no forum code and **every forum URL answers a permanent redirect to the forum's
+address on https://openvibe.community** (which moved from `/s` to `/forum` on 2026-11-07):
-| Path | Methods | Status |
-| --- | --- | --- |
-| `/s`, `/s/*` (pages, feeds) | GET, HEAD | `301` |
-| `/s`, `/s/*` | anything else | `308` (the method and body are preserved) |
-| `/api/v1/spaces*`, `/api/v1/posts/*`, `/api/v1/space-groups*`, `/api/v1/relay*` | all | `308` |
+| Path | Methods | Status | Goes to |
+| --- | --- | --- | --- |
+| `/s`, `/s/*` (pages, feeds) | GET, HEAD | `301` | `/forum/<space>/<thread>` (the `/t/` segment is gone) |
+| `/s`, `/s/*` | anything else | `308` (the method and body are preserved) | the same `/forum` path |
+| `/api/v1/spaces*`, `/api/v1/posts/*`, `/api/v1/space-groups*`, `/api/v1/relay*` | all | `308` | the same API path |
-A redirected bookmark, crawler or API client therefore lands on Community's copy of the same page.
+A redirected bookmark, crawler or API client therefore lands on Community's copy in one hop.
Nothing that is not a forum path redirects.
Nothing that is not a forum path redirects.
## Routes
## Routes
@@ -12,9 +12,10 @@
@@ -12,9 +12,10 @@
* GET /robots.txt, /llms.txt, /llms-full.txt, /sitemap.xml crawler endpoints (the sitemap is / alone)
* GET /robots.txt, /llms.txt, /llms-full.txt, /sitemap.xml crawler endpoints (the sitemap is / alone)
* GET /api/health, /api/ready, /release.json, /metrics (loopback)
* GET /api/health, /api/ready, /release.json, /metrics (loopback)
*
*
- * The forum is OpenVibe.Community's again (owner decision, 2026-10-08): every forum URL here answers
- * a permanent redirect to the same path and query there — /s and /s/* (301 for GET and HEAD, 308
- * otherwise), and /api/v1/spaces*, /api/v1/posts/*, /api/v1/space-groups* and /api/v1/relay* (308).
+ * The forum is OpenVibe.Community's again (owner decision, 2026-10-08), and it lives at /forum there
+ * (2026-11-07): every forum URL here answers a permanent redirect to it — /s and /s/* (301 for GET and
+ * HEAD, 308 otherwise), mapped to /forum/<space>/<thread>, and /api/v1/spaces*, /api/v1/posts/*,
+ * /api/v1/space-groups* and /api/v1/relay* (308, path unchanged).
*
*
* Space holds hosted content and emits hosting lifecycle events. The historical forum rows belong
* Space holds hosted content and emits hosting lifecycle events. The historical forum rows belong
* to Community; the forum redirects below stay intact. See README.md.
* to Community; the forum redirects below stay intact. See README.md.
@@ -44,16 +45,29 @@ const FORUM_PAGE = /^\/s(\/|$)/;
@@ -44,16 +45,29 @@ const FORUM_PAGE = /^\/s(\/|$)/;
const FORUM_API = /^\/api\/v1\/(spaces|posts|space-groups|relay)(\/|$)/;
const FORUM_API = /^\/api\/v1\/(spaces|posts|space-groups|relay)(\/|$)/;
/**
/**
- * Permanent redirect to the same path and query on Community. A GET or HEAD of a page keeps its
- * method with a 301; anything else (a form post, a vote) must not be replayed as a GET, and the
- * APIs are called with bodies, so they always answer 308.
+ * Permanent redirect to the forum's address on Community. The forum moved from /s (spaces) to /forum
+ * and dropped a thread's /t/ segment (2026-11-07), so /s/general/t/123 becomes /forum/general/123 —
+ * one hop, never a second redirect. A GET or HEAD of a page keeps its method with a 301; anything else
+ * (a form post, a vote) must not be replayed as a GET, and the APIs are called with bodies, so they
+ * always answer 308.
*/
*/
+function forumTarget(url) {
+ const qi = url.indexOf('?');
+ const pathname = qi === -1 ? url : url.slice(0, qi);
+ const query = qi === -1 ? '' : url.slice(qi);
+ if (pathname === '/s') return `/forum${query}`;
+ let rest = pathname.replace(/\/+$/, '');
+ if (rest === '/s/new-space') return `/forum/new${query}`;
+ rest = rest.replace(/^\/s\/([^/]+)\/t(?=\/|$)/, '/forum/$1').replace(/^\/s(?=\/|$)/, '/forum');
+ return `${rest}${query}`;
+}
+
function forumRedirect(req, res, next) {
function forumRedirect(req, res, next) {
const isPage = FORUM_PAGE.test(req.path);
const isPage = FORUM_PAGE.test(req.path);
if (!isPage && !FORUM_API.test(req.path)) return next();
if (!isPage && !FORUM_API.test(req.path)) return next();
const method = String(req.method || 'GET').toUpperCase();
const method = String(req.method || 'GET').toUpperCase();
const status = isPage && (method === 'GET' || method === 'HEAD') ? 301 : 308;
const status = isPage && (method === 'GET' || method === 'HEAD') ? 301 : 308;
- res.redirect(status, `${config.communityUrl}${req.originalUrl}`);
+ res.redirect(status, `${config.communityUrl}${isPage ? forumTarget(req.originalUrl) : req.originalUrl}`);
}
}
async function createApp(opts = {}) {
async function createApp(opts = {}) {
@@ -28,7 +28,7 @@ function llmsSections() {
@@ -28,7 +28,7 @@ function llmsSections() {
{ title: 'Make a space', url: `${u}/spaces/new`, html: true, note: 'describe it to the AI builder, or start from a template (sign-in required)' },
{ title: 'Make a space', url: `${u}/spaces/new`, html: true, note: 'describe it to the AI builder, or start from a template (sign-in required)' },
] },
] },
{ title: 'The network', links: [
{ title: 'The network', links: [
- { title: 'The forum', url: `${config.communityUrl}/s`, note: 'OpenVibe.Community hosts the forum' },
+ { title: 'The forum', url: `${config.communityUrl}/forum`, note: 'OpenVibe.Community hosts the forum' },
{ title: 'OpenVibe.Network', url: config.networkUrl, note: 'identity, accounts and sign-in' },
{ title: 'OpenVibe.Network', url: config.networkUrl, note: 'identity, accounts and sign-in' },
] },
] },
];
];
@@ -70,7 +70,7 @@ function navbarInit(opts) {
@@ -70,7 +70,7 @@ function navbarInit(opts) {
{ label: 'Home', href: '/', active: opts.active === 'home' },
{ label: 'Home', href: '/', active: opts.active === 'home' },
{ label: 'Spaces', href: '/spaces', active: opts.active === 'spaces' },
{ label: 'Spaces', href: '/spaces', active: opts.active === 'spaces' },
{ label: 'Hosting', href: '/dashboard' },
{ label: 'Hosting', href: '/dashboard' },
- { label: 'The forum', href: `${config.communityUrl}/s` },
+ { label: 'The forum', href: `${config.communityUrl}/forum` },
],
],
menu: { after: [{ label: 'OpenVibe.Network', href: NETWORK_URL, icon: 'fa-globe' }] },
menu: { after: [{ label: 'OpenVibe.Network', href: NETWORK_URL, icon: 'fa-globe' }] },
history: { type: opts.historyType || 'page', title: opts.historyTitle || opts.title },
history: { type: opts.historyType || 'page', title: opts.historyTitle || opts.title },
@@ -96,7 +96,7 @@ function footerInit(opts) {
@@ -96,7 +96,7 @@ function footerInit(opts) {
heading: 'Space',
heading: 'Space',
items: [
items: [
{ name: 'Home', url: '/' },
{ name: 'Home', url: '/' },
- { name: 'The forum', url: `${config.communityUrl}/s` },
+ { name: 'The forum', url: `${config.communityUrl}/forum` },
{ name: 'OpenVibe.Community', url: config.communityUrl },
{ name: 'OpenVibe.Community', url: config.communityUrl },
{ name: 'Source code', url: 'https://github.com/OpenVibers/OpenVibe.Space' },
{ name: 'Source code', url: 'https://github.com/OpenVibers/OpenVibe.Space' },
],
],
@@ -12,7 +12,7 @@ const sc = require('openvibe-shared/showcase');
@@ -12,7 +12,7 @@ const sc = require('openvibe-shared/showcase');
const { renderPage, escapeHtml: esc } = require('./layout');
const { renderPage, escapeHtml: esc } = require('./layout');
const { pickBlock, foldBlock } = require('./short');
const { pickBlock, foldBlock } = require('./short');
-const FORUM_URL = `${config.communityUrl}/s`;
+const FORUM_URL = `${config.communityUrl}/forum`;
const FORUM_LABEL = FORUM_URL.replace(/^https?:\/\//, '');
const FORUM_LABEL = FORUM_URL.replace(/^https?:\/\//, '');
const SERVICES_URL = 'https://openvibe.services/';
const SERVICES_URL = 'https://openvibe.services/';
const TEMPLATES = require('../spaces/templates').list();
const TEMPLATES = require('../spaces/templates').list();
@@ -70,16 +70,16 @@ async function check(name, fn) {
@@ -70,16 +70,16 @@ async function check(name, fn) {
assert.match(html, /<title>OpenVibe\.Space — spaces and static hosting<\/title>/);
assert.match(html, /<title>OpenVibe\.Space — spaces and static hosting<\/title>/);
assert.match(html, /<h1>Make a space\.<span class="sc-accent"> An app on your OpenVibe account\.<\/span><\/h1>/);
assert.match(html, /<h1>Make a space\.<span class="sc-accent"> An app on your OpenVibe account\.<\/span><\/h1>/);
assert.match(html, /The forum moved to OpenVibe\.Community/);
assert.match(html, /The forum moved to OpenVibe\.Community/);
- assert.match(html, /href="https:\/\/openvibe\.community\/s"/);
+ assert.match(html, /href="https:\/\/openvibe\.community\/forum"/);
});
});
await check('every forum URL is a permanent redirect to OpenVibe.Community', async () => {
await check('every forum URL is a permanent redirect to OpenVibe.Community', async () => {
const s = await fetch(`${base}/s`, { redirect: 'manual' });
const s = await fetch(`${base}/s`, { redirect: 'manual' });
assert.strictEqual(s.status, 301);
assert.strictEqual(s.status, 301);
- assert.strictEqual(s.headers.get('location'), 'https://openvibe.community/s');
+ assert.strictEqual(s.headers.get('location'), 'https://openvibe.community/forum');
const nested = await fetch(`${base}/s/general/t/1?x=2`, { redirect: 'manual' });
const nested = await fetch(`${base}/s/general/t/1?x=2`, { redirect: 'manual' });
assert.strictEqual(nested.status, 301);
assert.strictEqual(nested.status, 301);
- assert.strictEqual(nested.headers.get('location'), 'https://openvibe.community/s/general/t/1?x=2');
+ assert.strictEqual(nested.headers.get('location'), 'https://openvibe.community/forum/general/1?x=2');
const api = await fetch(`${base}/api/v1/spaces/general/threads`, { redirect: 'manual' });
const api = await fetch(`${base}/api/v1/spaces/general/threads`, { redirect: 'manual' });
assert.strictEqual(api.status, 308);
assert.strictEqual(api.status, 308);
assert.strictEqual(api.headers.get('location'), 'https://openvibe.community/api/v1/spaces/general/threads');
assert.strictEqual(api.headers.get('location'), 'https://openvibe.community/api/v1/spaces/general/threads');
@@ -32,7 +32,7 @@ const { boot, check, done } = require('./helpers/app');
@@ -32,7 +32,7 @@ const { boot, check, done } = require('./helpers/app');
assert.match(r.text, /What works now/);
assert.match(r.text, /What works now/);
assert.match(r.text, /showcase\.css/, 'built from the network\'s showcase sections');
assert.match(r.text, /showcase\.css/, 'built from the network\'s showcase sections');
assert.match(r.text, /The forum moved to OpenVibe\.Community/);
assert.match(r.text, /The forum moved to OpenVibe\.Community/);
- assert.match(r.text, /href="https:\/\/openvibe\.community\/s"/, 'the forum link points at Community');
+ assert.match(r.text, /href="https:\/\/openvibe\.community\/forum"/, 'the forum link points at Community');
assert.match(r.text, /<link rel="canonical" href="https:\/\/openvibe\.space\/">/);
assert.match(r.text, /<link rel="canonical" href="https:\/\/openvibe\.space\/">/);
assert.doesNotMatch(r.text, /href="\/s"/, 'nothing links a /s that only redirects');
assert.doesNotMatch(r.text, /href="\/s"/, 'nothing links a /s that only redirects');
});
});
@@ -74,7 +74,7 @@ const { boot, check, done } = require('./helpers/app');
@@ -74,7 +74,7 @@ const { boot, check, done } = require('./helpers/app');
const r = await t.get('/llms.txt');
const r = await t.get('/llms.txt');
assert.strictEqual(r.status, 200);
assert.strictEqual(r.status, 200);
assert.match(r.text, /OpenVibe\.Space/);
assert.match(r.text, /OpenVibe\.Space/);
- assert.match(r.text, /https:\/\/openvibe\.community\/s/);
+ assert.match(r.text, /https:\/\/openvibe\.community\/forum/);
assert.doesNotMatch(r.text, /\/s\/feed\.xml/, 'no forum feed is advertised');
assert.doesNotMatch(r.text, /\/s\/feed\.xml/, 'no forum feed is advertised');
});
});
@@ -13,24 +13,33 @@ const COMMUNITY = 'https://openvibe.community';
@@ -13,24 +13,33 @@ const COMMUNITY = 'https://openvibe.community';
(async () => {
(async () => {
const t = await boot();
const t = await boot();
- await check('GET and HEAD of /s and its nested paths answer 301 to Community, path and query intact', async () => {
- for (const path of ['/s', '/s?sort=new', '/s/general', '/s/general/t/123?x=1&y=two', '/s/feed.xml', '/s/general/feed.xml']) {
+ await check('GET and HEAD of /s and its nested paths answer 301 to the forum on Community, mapped to /forum', async () => {
+ const map = [
+ ['/s', '/forum'],
+ ['/s?sort=new', '/forum?sort=new'],
+ ['/s/general', '/forum/general'],
+ ['/s/general/t/123?x=1&y=two', '/forum/general/123?x=1&y=two'],
+ ['/s/feed.xml', '/forum/feed.xml'],
+ ['/s/general/feed.xml', '/forum/general/feed.xml'],
+ ['/s/new-space', '/forum/new'],
+ ];
+ for (const [path, to] of map) {
const r = await t.get(path);
const r = await t.get(path);
assert.strictEqual(r.status, 301, path);
assert.strictEqual(r.status, 301, path);
- assert.strictEqual(r.headers.get('location'), `${COMMUNITY}${path}`, path);
+ assert.strictEqual(r.headers.get('location'), `${COMMUNITY}${to}`, path);
}
}
const head = await t.get('/s?sort=top', { method: 'HEAD' });
const head = await t.get('/s?sort=top', { method: 'HEAD' });
assert.strictEqual(head.status, 301);
assert.strictEqual(head.status, 301);
- assert.strictEqual(head.headers.get('location'), `${COMMUNITY}/s?sort=top`);
+ assert.strictEqual(head.headers.get('location'), `${COMMUNITY}/forum?sort=top`);
});
});
await check('other methods of /s and /s/* answer 308 (the method and body are preserved)', async () => {
await check('other methods of /s and /s/* answer 308 (the method and body are preserved)', async () => {
const post = await t.get('/s/general/threads?q=1', { method: 'POST', body: 'title=x', headers: { 'content-type': 'application/x-www-form-urlencoded' } });
const post = await t.get('/s/general/threads?q=1', { method: 'POST', body: 'title=x', headers: { 'content-type': 'application/x-www-form-urlencoded' } });
assert.strictEqual(post.status, 308);
assert.strictEqual(post.status, 308);
- assert.strictEqual(post.headers.get('location'), `${COMMUNITY}/s/general/threads?q=1`);
+ assert.strictEqual(post.headers.get('location'), `${COMMUNITY}/forum/general/threads?q=1`);
const del = await t.get('/s/general/t/123/posts/9', { method: 'DELETE' });
const del = await t.get('/s/general/t/123/posts/9', { method: 'DELETE' });
assert.strictEqual(del.status, 308);
assert.strictEqual(del.status, 308);
- assert.strictEqual(del.headers.get('location'), `${COMMUNITY}/s/general/t/123/posts/9`);
+ assert.strictEqual(del.headers.get('location'), `${COMMUNITY}/forum/general/123/posts/9`);
});
});
await check('the forum APIs answer 308 to Community, path and query intact', async () => {
await check('the forum APIs answer 308 to Community, path and query intact', async () => {