openvibers/openvibe.wiki Commits
Wikis, not spaces: every word a reader sees says wiki (the nav, the home, start/settings/delete, titles, llms.txt), since openvibe.space is another product; the addresses stay, with /wikis and /new-wiki as aliases (#79)
Changes
@@@ -77,7 +77,7 @@ function createPages({ svc, viewers, platform, config, log = console, limits })const errorPage = (req, res, status, title, message) => send(req, res, status, views.errorBody({ status, title, message }), { title, robots: 'noindex, nofollow' });const notFound = (req, res) => errorPage(req, res, 404, 'Page not found', 'Nothing lives at that address, or it is not visible to you.');const gone = (req, res) => errorPage(req, res, 410, 'Gone', 'This page was deleted. Its history is kept, but it is no longer published.');-- // VIP (WS-K task 8): the join prompt for a VIP space or page the viewer is not admitted to (403, never cached).++ // VIP (WS-K task 8): the join prompt for a VIP wiki or page the viewer is not admitted to (403, never cached).const vipGate = (req, res, space, page) => {const r = svc.access.vipRefusal(space, page, req.actor);if (!r) return false;@@@ -123,7 +123,7 @@ function createPages({ svc, viewers, platform, config, log = console, limits })router.get('/', wrap(async (req, res) => {if (req.actor.subject) await svc.access.prepareVip(req.actor, (await svc.vipSpaces()).map((space) => ({ space }))); // VIP spaces the viewer may seeconst [spaces, recent] = await Promise.all([svc.listSpaces(req.actor), svc.recentChanges(10)]);-- // The official spaces' shelves: what is inside them, on the front page (only public spaces: the home is cached).++ // The official wikis' shelves: what is inside them, on the front page (only public spaces: the home is cached).const shelves = new Map(await Promise.all(spaces.filter((s) => s.kind === 'official' && s.visibility === 'public').slice(0, 3).map(async (s) => [s.id, await svc.shelf(s, req.actor, 48)])));// The site's one-line AI summary: the shell turns it into the ai-summary meta and the home's@@@ -142,19 +142,22 @@ function createPages({ svc, viewers, platform, config, log = console, limits })}));// ── Spaces ───────────────────────────────────────────────++ // Wikis were called spaces until 2026-10-17 (openvibe.space is another product); the addresses stay, these are aliases.++ router.get('/wikis', (req, res) => res.redirect(301, '/'));++ router.get('/new-wiki', (req, res) => res.redirect(301, '/new-space'));router.get('/new-space', (req, res) => {-- if (!req.actor.subject) return needSignIn(req, res, 'Sign in with your OpenVibe account to start a space.');-- send(req, res, 200, views.newSpacePage({ staff: req.actor.staff }), { title: 'Start a space', robots: 'noindex, nofollow' });++ if (!req.actor.subject) return needSignIn(req, res, 'Sign in with your OpenVibe account to start a wiki.');++ send(req, res, 200, views.newSpacePage({ staff: req.actor.staff }), { title: 'Start a wiki', robots: 'noindex, nofollow' });});router.post('/new-space', B('wiki.space.create'), form, wrap(async (req, res) => {-- if (!req.actor.subject) return needSignIn(req, res, 'Sign in with your OpenVibe account to start a space.');++ if (!req.actor.subject) return needSignIn(req, res, 'Sign in with your OpenVibe account to start a wiki.');const b = req.body || {};try {const space = await svc.createSpace({ name: b.name, slug: b.slug || null, description: b.description || null, visibility: b.visibility, kind: b.official === '1' && req.actor.staff ? 'official' : 'user' }, req.actor);res.redirect(303, `/s/${encodeURIComponent(space.slug)}`);} catch (err) {if (!err.status || err.status >= 500) throw err;-- send(req, res, err.status, views.newSpacePage({ values: b, error: err.message, staff: req.actor.staff }), { title: 'Start a space', robots: 'noindex, nofollow' });++ send(req, res, err.status, views.newSpacePage({ values: b, error: err.message, staff: req.actor.staff }), { title: 'Start a wiki', robots: 'noindex, nofollow' });}}));@@@ -165,7 +168,7 @@ function createPages({ svc, viewers, platform, config, log = console, limits })const proposals = canEdit ? (await svc.pendingProposals(req.actor)).filter((p) => p.space_id === space.id) : [];const open = space.visibility === 'public';send(req, res, 200, views.spacePage({ space, tree: await svc.tree(space, req.actor), canEdit, canManage: svc.access.canManage(space, req.actor), proposals }), {-- title: space.name, description: space.description || `${space.name}: a space on OpenVibe.Wiki.`, path: svc.spacePath(space),++ title: space.name, description: space.description || `${space.name}: a wiki on OpenVibe.Wiki.`, path: svc.spacePath(space),robots: open ? 'index, follow' : 'noindex, nofollow', cache: open ? 'public' : null,jsonLd: open ? seo.structuredData.breadcrumbs([{ name: 'Wiki', url: `${config.baseUrl}/` }, { name: space.name, url: `${config.baseUrl}${svc.spacePath(space)}` }]) : null,});@@@ -173,8 +176,8 @@ function createPages({ svc, viewers, platform, config, log = console, limits })function editorsOnly(req, res, space) {if (svc.access.canEdit(space, req.actor)) return true;-- if (!req.actor.subject) needSignIn(req, res, 'Sign in to edit. Only editors of this space can change it.');-- else errorPage(req, res, 403, 'Not an editor', 'Only editors of this space can do that. An owner of the space can give you a role.');++ if (!req.actor.subject) needSignIn(req, res, 'Sign in to edit. Only editors of this wiki can change it.');++ else errorPage(req, res, 403, 'Not an editor', 'Only editors of this wiki can do that. An owner of the wiki can give you a role.');return false;}@@@ -214,7 +217,7 @@ function createPages({ svc, viewers, platform, config, log = console, limits })router.get('/s/:space/settings', wrap(async (req, res) => {const space = await locateSpace(req, res);if (!space) return;-- if (!svc.access.canManage(space, req.actor)) return req.actor.subject ? errorPage(req, res, 403, 'Owners only', 'Only an owner of this space can change its settings.') : needSignIn(req, res, 'Sign in as an owner of this space.');++ if (!svc.access.canManage(space, req.actor)) return req.actor.subject ? errorPage(req, res, 403, 'Owners only', 'Only an owner of this wiki can change its settings.') : needSignIn(req, res, 'Sign in as an owner of this wiki.');send(req, res, 200, views.spaceSettingsPage({ space, roles: await svc.roles(space.id, req.actor), flash: req.query.saved ? 'Saved.' : null }), { title: `Settings of ${space.name}`, robots: 'noindex, nofollow' });}));@@@ -234,13 +237,13 @@ function createPages({ svc, viewers, platform, config, log = console, limits })}}));-- // Import pages into a space (owners): the same JSON bundle as POST /api/v1/spaces/:space/import,++ // Import pages into a wiki (owners): the same JSON bundle as POST /api/v1/spaces/:space/import,// pasted into a form; the options come from the form's controls.const importForm = express.urlencoded({ extended: false, limit: '6mb' });function ownersOnly(req, res, space) {if (svc.access.canManage(space, req.actor)) return true;-- if (!req.actor.subject) needSignIn(req, res, 'Sign in as an owner of this space to import pages.');-- else errorPage(req, res, 403, 'Owners only', 'Only an owner of this space imports pages into it.');++ if (!req.actor.subject) needSignIn(req, res, 'Sign in as an owner of this wiki to import pages.');++ else errorPage(req, res, 403, 'Owners only', 'Only an owner of this wiki imports pages into it.');return false;}router.get('/s/:space/import', wrap(async (req, res) => {@@@ -15,12 +15,12 @@ const { escapeHtml: esc } = require('openvibe-publishing/ssr');const SITE_NAME = 'OpenVibe.Wiki';const NETWORK_URL = 'https://openvibe.network';--const DEFAULT_DESCRIPTION = 'OpenVibe.Wiki: wiki spaces with page trees, revision history, citations and discussion, part of the OpenVibe network.';++const DEFAULT_DESCRIPTION = 'OpenVibe.Wiki: wikis with page trees, revision history, citations and discussion, part of the OpenVibe network.';// The one AI summary for the site: /llms.txt and /llms-full.txt publish it, and the home page carries it// as the shell's ai-summary meta / WebPage JSON-LD (openvibe-publishing/layout forwards `summary`).--const AI_SUMMARY = 'Wiki spaces of the OpenVibe network: page trees, immutable revisions, citations attached to the revision that used them, infoboxes and internal links.';++const AI_SUMMARY = 'Wikis of the OpenVibe network: page trees, immutable revisions, citations attached to the revision that used them, infoboxes and internal links.';const NAV_LINKS = [-- { label: 'Spaces', href: '/' },++ { label: 'Wikis', href: '/' },{ label: 'Recent changes', href: '/recent' },{ label: 'Search', href: '/search' },];@@@ -49,7 +49,7 @@ function navConfig(o, config) {service: 'wiki',apiBase: NETWORK_URL,links: [-- { label: 'Spaces', href: '/', active: o.active === 'home' },++ { label: 'Wikis', href: '/', active: o.active === 'home' },{ label: 'Recent changes', href: '/recent', active: o.active === 'recent' },{ label: 'Search', href: '/search', active: o.active === 'search' },],@@@ -84,7 +84,7 @@ function renderPage(o) {siteName: SITE_NAME,lang: 'en',// The front page's title says what the site is (search results show it); every other page is "<page> · OpenVibe.Wiki".-- title: o.title ? `${o.title} · ${SITE_NAME}` : o.path === '/' ? `${SITE_NAME}: wiki spaces with history and citations` : SITE_NAME,++ title: o.title ? `${o.title} · ${SITE_NAME}` : o.path === '/' ? `${SITE_NAME}: wikis with history and citations` : SITE_NAME,description: o.description || DEFAULT_DESCRIPTION,summary: o.summary,canonical: `${config.baseUrl}${o.path || '/'}`,@@@ -11,11 +11,11 @@ const { infoboxToText } = require('../wiki/content');const { html, raw } = ssr;const t = (v) => raw(ssr.timeTag(v));const e = encodeURIComponent;--const VIS_LABEL = { public: 'Public', members: 'Members (any signed-in OpenVibe account)', vip: 'VIP (the space owner\'s OpenVibe.VIP members)', private: 'Private (people with a role in this space)' };++const VIS_LABEL = { public: 'Public', members: 'Members (any signed-in OpenVibe account)', vip: 'VIP (the wiki owner\'s OpenVibe.VIP members)', private: 'Private (people with a role in this wiki)' };function wpath(space, page) { return `/w/${e(space.slug)}/${e(page.slug)}`; }--/** rel for outbound links (citations, infobox URLs): community spaces add ugc (see service.linkRel). */++/** rel for outbound links (citations, infobox URLs): community wikis add ugc (see service.linkRel). */function outRel(space) { return space && space.kind === 'official' ? 'noopener nofollow' : 'nofollow ugc noopener'; }function notice(kind, text) { return html`<p class="wk-notice wk-${kind}" role="status">${text}</p>`; }@@@ -26,7 +26,7 @@ function errorBody({ status, title, message }) {return html`<section class="wk-error"><h1>${title}</h1><p>${message}</p><p class="wk-muted">HTTP ${String(status)}</p><p><a href="/">Back to the wiki</a></p></section>`;}--/** A VIP space or page the viewer is not (yet) admitted to: the join prompt (WS-K task 8). */++/** A VIP wiki or page the viewer is not (yet) admitted to: the join prompt (WS-K task 8). */function vipGateBody({ title, spaceName, joinUrl, signedIn, signInUrl }) {return html`<section class="wk-error"><h1>For VIP members</h1><p>${title ? html`<strong>${title}</strong> is` : 'This is'} for the VIP members of ${spaceName ? html`<strong>${spaceName}</strong>'s owner` : 'its owner'}.</p><p>${signedIn ? '' : html`<a class="wk-btn" href="${signInUrl}">Sign in</a> `}${joinUrl ? html`<a class="wk-btn" href="${joinUrl}" rel="noopener">See their VIP plans</a>` : ''}</p><p><a href="/">Back to the wiki</a></p></section>`;@@@ -48,13 +48,13 @@ function home({ spaces, recent, shelves = new Map(), actor }) {const official = spaces.filter((s) => s.kind === 'official');const user = spaces.filter((s) => s.kind === 'user');const signedIn = !!(actor && actor.subject);-- const start = signedIn ? { label: 'Start a space', href: '/new-space', primary: true } : { label: 'Sign in to start a space', href: '/auth/login?next=%2Fnew-space', primary: true };++ const start = signedIn ? { label: 'Start a wiki', href: '/new-space', primary: true } : { label: 'Sign in to start a wiki', href: '/auth/login?next=%2Fnew-space', primary: true };const card = (s) => {const shelf = shelves.get(s.id);return html`<li><a class="wk-space" href="/s/${e(s.slug)}"><b>${s.name}</b>${s.visibility !== 'public' ? html` <span class="wk-tag">${s.visibility}</span>` : ''}${s.description ? html`<span class="wk-space-text">${s.description}</span>` : ''}${shelf && shelf.total ? html`<span class="wk-space-meta">${String(shelf.total)} page${shelf.total === 1 ? '' : 's'}${shelf.updated ? html` · updated ${t(shelf.updated)}` : ''}</span>` : ''}</a></li>`;};-- // Each official space with pages: what is inside it, its pages one level down as cards with their summaries. A space-- // without a shelf (not public, or empty) is a card under "Official spaces".++ // Each official wiki with pages: what is inside it, its pages one level down as cards with their summaries. A space++ // without a shelf (not public, or empty) is a card under "Official wikis".const shelved = official.filter((s) => shelves.get(s.id) && shelves.get(s.id).pages.length);const unshelved = official.filter((s) => !shelved.includes(s));const inside = shelved.map((s) => {@@@ -70,23 +70,23 @@ function home({ spaces, recent, shelves = new Map(), actor }) {return html`${raw(showcase.hero({eyebrow: 'OpenVibe.Wiki',title: 'Knowledge,', accent: 'with sources',-- lede: 'Wiki spaces with page trees, revision history, citations and discussion. Official spaces are edited by OpenVibe staff; anyone signed in with an OpenVibe account can start a space of their own.',-- actions: [start, { label: 'Browse the spaces', href: '#spaces' }],++ lede: 'Wikis with page trees, revision history, citations and discussion. Official wikis are edited by OpenVibe staff; anyone signed in with an OpenVibe account can start a wiki of their own.',++ actions: [start, { label: 'Browse the wikis', href: '#spaces' }],}))}<div id="spaces"></div>${raw(inside)}--${unshelved.length || !shelved.length ? html`<section class="sc-sec wk-home-sec" aria-labelledby="wk-official"><h2 id="wk-official">Official spaces</h2>${unshelved.length ? html`<ul class="wk-spaces">${unshelved.map(card)}</ul>` : html`<p class="wk-muted">None yet.</p>`}</section>` : ''}--<section class="sc-sec wk-home-sec" aria-labelledby="wk-community"><h2 id="wk-community">Community spaces</h2>${user.length ? html`<ul class="wk-spaces">${user.map(card)}</ul>`-- : html`<div class="wk-empty"><p><b>No community spaces yet.</b> Any OpenVibe account can start one: public, members-only, for its owner's VIP members, or private, with the editors it chooses.</p><p><a class="wk-button" href="${start.href}">${start.label}</a></p></div>`}</section>++${unshelved.length || !shelved.length ? html`<section class="sc-sec wk-home-sec" aria-labelledby="wk-official"><h2 id="wk-official">Official wikis</h2>${unshelved.length ? html`<ul class="wk-spaces">${unshelved.map(card)}</ul>` : html`<p class="wk-muted">None yet.</p>`}</section>` : ''}++<section class="sc-sec wk-home-sec" aria-labelledby="wk-community"><h2 id="wk-community">Community wikis</h2>${user.length ? html`<ul class="wk-spaces">${user.map(card)}</ul>`++ : html`<div class="wk-empty"><p><b>No community wikis yet.</b> Any OpenVibe account can start one: public, members-only, for its owner's VIP members, or private, with the editors it chooses.</p><p><a class="wk-button" href="${start.href}">${start.label}</a></p></div>`}</section>${raw(showcase.features({-- title: 'What a space gets',++ title: 'What a wiki gets',items: [-- { icon: 'ov:wiki', title: 'Page trees', text: 'Pages nest under pages, so a space reads like a book.' },++ { icon: 'ov:wiki', title: 'Page trees', text: 'Pages nest under pages, so a wiki reads like a book.' },{ icon: 'ov:history', title: 'Revision history', text: 'Every published revision is kept; compare or restore any of them.' },{ icon: 'ov:check', title: 'Citations', text: 'Claims carry their sources, listed with every page.' },{ icon: 'ov:community', title: 'Discussion', text: 'Each page has a thread on OpenVibe.Community.' },-- { icon: 'ov:account', title: 'Roles and visibility', text: 'Public, members-only, VIP or private spaces, with editors you choose.' },-- { icon: 'ov:search', title: 'Search and feeds', text: 'Full-text search across the spaces you can see, and Atom and JSON feeds.' },++ { icon: 'ov:account', title: 'Roles and visibility', text: 'Public, members-only, VIP or private wikis, with editors you choose.' },++ { icon: 'ov:search', title: 'Search and feeds', text: 'Full-text search across the wikis you can see, and Atom and JSON feeds.' },],}))}<section class="sc-sec wk-home-sec" aria-labelledby="wk-recent-h"><h2 id="wk-recent-h">Recently published</h2>${recent.length ? html`<ul class="wk-recent">${recent.map((r) => html`<li><span><a href="${wpath(r.space, r.page)}">${r.page.title}</a> <span class="wk-muted">in ${r.space.name}</span></span> <span class="wk-muted">${t(r.page.revision_published_at)}</span></li>`)}</ul>` : html`<p class="wk-muted">Nothing published yet.</p>`}@@@ -101,12 +101,12 @@ function treeHtml(nodes, space) {function spacePage({ space, tree, canEdit, canManage, proposals }) {return html`${crumbs([{ name: 'Wiki', url: '/' }, { name: space.name }])}<section><h1>${space.name}</h1>--<p class="wk-muted">${space.kind === 'official' ? 'Official space' : 'Community space'} · ${VIS_LABEL[space.visibility]}</p>++<p class="wk-muted">${space.kind === 'official' ? 'Official wiki' : 'Community wiki'} · ${VIS_LABEL[space.visibility]}</p>${space.description ? html`<p>${space.description}</p>` : ''}--${canEdit ? html`<p><a class="wk-button" href="/s/${e(space.slug)}/new">New page</a>${canManage ? html` <a href="/s/${e(space.slug)}/import">Import pages</a> <a href="/s/${e(space.slug)}/settings">Space settings</a>` : ''}</p>` : ''}++${canEdit ? html`<p><a class="wk-button" href="/s/${e(space.slug)}/new">New page</a>${canManage ? html` <a href="/s/${e(space.slug)}/import">Import pages</a> <a href="/s/${e(space.slug)}/settings">Wiki settings</a>` : ''}</p>` : ''}${proposals && proposals.length ? html`<p>${notice('info', `${proposals.length} AI proposal(s) wait for review.`)} <a href="/s/${e(space.slug)}/proposals">Review them</a></p>` : ''}<h2>Pages</h2>${tree.length ? raw(treeHtml(tree, space)) : html`<p class="wk-muted">No pages yet.</p>`}--${space.visibility === 'public' ? html`<p class="wk-muted">Follow this space: <a href="/s/${e(space.slug)}/feed.atom" type="application/atom+xml">Atom</a> · <a href="/s/${e(space.slug)}/feed.json" type="application/feed+json">JSON Feed</a></p>` : ''}++${space.visibility === 'public' ? html`<p class="wk-muted">Follow this wiki: <a href="/s/${e(space.slug)}/feed.atom" type="application/atom+xml">Atom</a> · <a href="/s/${e(space.slug)}/feed.json" type="application/feed+json">JSON Feed</a></p>` : ''}</section>`;}@@@ -190,7 +190,7 @@ function articlePage(v, { html: contentHtml, discussion, actor, mediaUrl, resolvtrail.push({ name: rev.fields.title || page.title });const banners = [];if (flash) banners.push(notice('ok', flash));-- if (page.state !== 'published') banners.push(notice('warn', `This page is ${page.state === 'scheduled' ? 'scheduled, not published yet' : page.state}. Only editors of the space can see it.`));++ if (page.state !== 'published') banners.push(notice('warn', `This page is ${page.state === 'scheduled' ? 'scheduled, not published yet' : page.state}. Only editors of the wiki can see it.`));else if (!v.isPublishedRevision) banners.push(notice('warn', `You are looking at revision ${rev.number}, not the published revision ${page.published_revision}.`));else if (v.canEdit && v.headNumber > rev.number) banners.push(html`<p class="wk-notice wk-info" role="status">Revision ${String(v.headNumber)} is newer than the published one and only editors can see it. <a href="${base}?rev=${String(v.headNumber)}">View it</a> · <a href="${base}/diff/${String(rev.number)}/${String(v.headNumber)}">compare</a></p>`);if (v.disclosure) banners.push(html`<p class="wk-notice wk-ai" role="note"><strong>${v.disclosure.short}.</strong> ${v.disclosure.long}</p>`);@@@ -292,15 +292,15 @@ function formValuesFromRevision(rev, page) {}function newSpacePage({ values = {}, error = null, staff = false }) {-- return html`${crumbs([{ name: 'Wiki', url: '/' }, { name: 'New space' }])}--<section><h1>Start a space</h1>${error ? notice('error', error) : ''}++ return html`${crumbs([{ name: 'Wiki', url: '/' }, { name: 'New wiki' }])}++<section><h1>Start a wiki</h1>${error ? notice('error', error) : ''}<form method="post" action="/new-space" class="wk-form"><label>Name <input type="text" name="name" required maxlength="120" value="${values.name || ''}"></label><label>Address (optional; lowercase letters, digits and dashes) <input type="text" name="slug" maxlength="63" pattern="[a-z0-9][a-z0-9-]{1,62}" value="${values.slug || ''}"></label><label>Description <input type="text" name="description" maxlength="1000" value="${values.description || ''}"></label><label>Visibility <select name="visibility">${['public', 'members', 'vip', 'private'].map((v) => html`<option value="${v}"${(values.visibility || 'public') === v ? raw(' selected') : ''}>${VIS_LABEL[v]}</option>`)}</select></label>--${staff ? html`<label class="wk-check"><input type="checkbox" name="official" value="1"> Official (editorial) space</label>` : ''}--<p><button type="submit">Create space</button></p></form></section>`;++${staff ? html`<label class="wk-check"><input type="checkbox" name="official" value="1"> Official (editorial) wiki</label>` : ''}++<p><button type="submit">Create wiki</button></p></form></section>`;}const IMPORT_EXAMPLE = JSON.stringify({ pages: [@@@ -334,7 +334,7 @@ function spaceSettingsPage({ space, roles, error = null, flash = null }) {<label>Name <input type="text" name="name" maxlength="120" value="${space.name}"></label><label>Description <input type="text" name="description" maxlength="1000" value="${space.description || ''}"></label><label>Visibility <select name="visibility">${['public', 'members', 'vip', 'private'].map((v) => html`<option value="${v}"${space.visibility === v ? raw(' selected') : ''}>${VIS_LABEL[v]}</option>`)}</select></label>--<p class="wk-muted">Making a space members-only or private removes its pages from sitemaps, feeds and OpenVibe.Search.</p>++<p class="wk-muted">Making a wiki members-only or private removes its pages from sitemaps, feeds and OpenVibe.Search.</p><p><button type="submit" name="op" value="settings">Save settings</button></p></form><h2>Roles</h2><table><thead><tr><th scope="col">Person</th><th scope="col">Role</th><th scope="col">Since</th><th scope="col"></th></tr></thead><tbody>@@@ -345,7 +345,7 @@ ${roles.map((r) => html`<tr><td><code>${r.subject}</code></td><td>${r.role}</td><label>Role <select name="role"><option value="editor">editor</option><option value="viewer">viewer</option><option value="owner">owner</option></select></label><p><button type="submit">Grant role</button></p></form><h2>Import</h2><p><a href="/s/${e(space.slug)}/import">Import pages from a bundle</a></p>--<h2>Delete</h2><form method="post" action="/s/${e(space.slug)}/settings" class="wk-form"><input type="hidden" name="op" value="delete"><label class="wk-check"><input type="checkbox" name="confirm" value="yes" required> Delete this space and take every page offline (addresses answer 410 Gone)</label><p><button type="submit">Delete space</button></p></form>++<h2>Delete</h2><form method="post" action="/s/${e(space.slug)}/settings" class="wk-form"><input type="hidden" name="op" value="delete"><label class="wk-check"><input type="checkbox" name="confirm" value="yes" required> Delete this wiki and take every page offline (addresses answer 410 Gone)</label><p><button type="submit">Delete wiki</button></p></form></section>`;}@@@ -353,7 +353,7 @@ function pageSettingsPage({ space, page, v, parents, error = null, flash = null,const base = wpath(space, page);return html`${crumbs([{ name: 'Wiki', url: '/' }, { name: space.name, url: `/s/${e(space.slug)}` }, { name: page.title, url: base }, { name: 'Settings' }])}<section><h1>Settings of ${page.title}</h1>${error ? notice('error', error) : ''}${flash ? notice('ok', flash) : ''}--<p>State: <strong>${page.state}</strong>${page.published_revision ? html` · published revision ${String(page.published_revision)}` : ''} · visibility ${page.visibility} (space: ${space.visibility})</p>++<p>State: <strong>${page.state}</strong>${page.published_revision ? html` · published revision ${String(page.published_revision)}` : ''} · visibility ${page.visibility} (wiki: ${space.visibility})</p><h2>Publish</h2><form method="post" action="${base}/settings" class="wk-form"><input type="hidden" name="op" value="publish"><label>Revision <input type="number" name="revision" min="1" value="${String(v.headNumber)}"></label>@@@ -52,7 +52,7 @@ const robotsOf = (head) => /<meta name="robots" content="([^"]*)">/.exec(head)[1// The home page: indexable by an explicit robots string, plus the showcase kit's stylesheet.r = await H.req(h, 'GET', '/');let { head } = frameOf(r.text, '/');-- assert.ok(head.includes('<title>OpenVibe.Wiki: wiki spaces with history and citations</title>'), `the home title says what the site is: ${(/<title>[^<]*<\/title>/.exec(head) || [''])[0]}`);++ assert.ok(head.includes('<title>OpenVibe.Wiki: wikis with history and citations</title>'), `the home title says what the site is: ${(/<title>[^<]*<\/title>/.exec(head) || [''])[0]}`);assert.ok(/<link rel="canonical" href="https?:\/\/[^"\/]+\/">/.test(head), 'the home canonical');assert.strictEqual(robotsOf(head), 'index, follow');assert.ok(/<link rel="stylesheet" href="\/shared\/[^"]*showcase[^"]*">/.test(head), 'the extra openvibe-shared stylesheet');@@@ -73,8 +73,8 @@ const data = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'seeds', 'opeassert.strictEqual(shelfCards, data.pages.length - roots.length, 'one card for each page under the root');assert.ok(shelf.includes('href="/w/openvibe/openvibe-live"'), 'cards link the pages');assert.ok(!/<small>[^<]{241,}<\/small>/.test(shelf), 'a card carries one short sentence');-- assert.ok(!homePage.text.includes('>Official spaces<'), 'a shelved space is not listed again');-- assert.ok(homePage.text.includes('No community spaces yet.'), 'the community section says how to start one');++ assert.ok(!homePage.text.includes('>Official wikis<'), 'a shelved space is not listed again');++ assert.ok(homePage.text.includes('No community wikis yet.'), 'the community section says how to start one');// Absent from sitemaps, feeds and Search until reviewed.assert.strictEqual(((await H.req(h, 'GET', '/sitemaps/pages-1.xml')).text.match(/<loc>/g) || []).length, 0);const atomFeed = await H.req(h, 'GET', '/feed.atom');@@@ -60,7 +60,7 @@ async function lastIndexEvent(h, pageId) {assert.strictEqual(r.status, 200);assert.match(r.headers.get('content-type'), /^text\/plain/);assert.strictEqual(r.headers.get('cache-control'), 'public, max-age=3600, stale-while-revalidate=3600');-- assert.ok(r.text.startsWith('# OpenVibe.Wiki') && r.text.includes('> Wiki spaces of the OpenVibe network'), 'the llms.txt header comes first');++ assert.ok(r.text.startsWith('# OpenVibe.Wiki') && r.text.includes('> Wikis of the OpenVibe network'), 'the llms.txt header comes first');assert.ok(r.text.includes('http://wiki.test/w/notes/open-page') && r.text.includes('http://wiki.test/w/notes/second-page'));assert.ok(!r.text.includes('unsourced-page'), 'only indexable pages, not merely listable ones');assert.ok(Buffer.byteLength(r.text) <= 512 * 1024, 'the page stays within maxBytes');
@@ -77,7 +77,7 @@ function createPages({ svc, viewers, platform, config, log = console, limits })
@@ -77,7 +77,7 @@ function createPages({ svc, viewers, platform, config, log = console, limits })
const errorPage = (req, res, status, title, message) => send(req, res, status, views.errorBody({ status, title, message }), { title, robots: 'noindex, nofollow' });
const errorPage = (req, res, status, title, message) => send(req, res, status, views.errorBody({ status, title, message }), { title, robots: 'noindex, nofollow' });
const notFound = (req, res) => errorPage(req, res, 404, 'Page not found', 'Nothing lives at that address, or it is not visible to you.');
const notFound = (req, res) => errorPage(req, res, 404, 'Page not found', 'Nothing lives at that address, or it is not visible to you.');
const gone = (req, res) => errorPage(req, res, 410, 'Gone', 'This page was deleted. Its history is kept, but it is no longer published.');
const gone = (req, res) => errorPage(req, res, 410, 'Gone', 'This page was deleted. Its history is kept, but it is no longer published.');
- // VIP (WS-K task 8): the join prompt for a VIP space or page the viewer is not admitted to (403, never cached).
+ // VIP (WS-K task 8): the join prompt for a VIP wiki or page the viewer is not admitted to (403, never cached).
const vipGate = (req, res, space, page) => {
const vipGate = (req, res, space, page) => {
const r = svc.access.vipRefusal(space, page, req.actor);
const r = svc.access.vipRefusal(space, page, req.actor);
if (!r) return false;
if (!r) return false;
@@ -123,7 +123,7 @@ function createPages({ svc, viewers, platform, config, log = console, limits })
@@ -123,7 +123,7 @@ function createPages({ svc, viewers, platform, config, log = console, limits })
router.get('/', wrap(async (req, res) => {
router.get('/', wrap(async (req, res) => {
if (req.actor.subject) await svc.access.prepareVip(req.actor, (await svc.vipSpaces()).map((space) => ({ space }))); // VIP spaces the viewer may see
if (req.actor.subject) await svc.access.prepareVip(req.actor, (await svc.vipSpaces()).map((space) => ({ space }))); // VIP spaces the viewer may see
const [spaces, recent] = await Promise.all([svc.listSpaces(req.actor), svc.recentChanges(10)]);
const [spaces, recent] = await Promise.all([svc.listSpaces(req.actor), svc.recentChanges(10)]);
- // The official spaces' shelves: what is inside them, on the front page (only public spaces: the home is cached).
+ // The official wikis' shelves: what is inside them, on the front page (only public spaces: the home is cached).
const shelves = new Map(await Promise.all(spaces.filter((s) => s.kind === 'official' && s.visibility === 'public').slice(0, 3)
const shelves = new Map(await Promise.all(spaces.filter((s) => s.kind === 'official' && s.visibility === 'public').slice(0, 3)
.map(async (s) => [s.id, await svc.shelf(s, req.actor, 48)])));
.map(async (s) => [s.id, await svc.shelf(s, req.actor, 48)])));
// The site's one-line AI summary: the shell turns it into the ai-summary meta and the home's
// The site's one-line AI summary: the shell turns it into the ai-summary meta and the home's
@@ -142,19 +142,22 @@ function createPages({ svc, viewers, platform, config, log = console, limits })
@@ -142,19 +142,22 @@ function createPages({ svc, viewers, platform, config, log = console, limits })
}));
}));
// ── Spaces ───────────────────────────────────────────────
// ── Spaces ───────────────────────────────────────────────
+ // Wikis were called spaces until 2026-10-17 (openvibe.space is another product); the addresses stay, these are aliases.
+ router.get('/wikis', (req, res) => res.redirect(301, '/'));
+ router.get('/new-wiki', (req, res) => res.redirect(301, '/new-space'));
router.get('/new-space', (req, res) => {
router.get('/new-space', (req, res) => {
- if (!req.actor.subject) return needSignIn(req, res, 'Sign in with your OpenVibe account to start a space.');
- send(req, res, 200, views.newSpacePage({ staff: req.actor.staff }), { title: 'Start a space', robots: 'noindex, nofollow' });
+ if (!req.actor.subject) return needSignIn(req, res, 'Sign in with your OpenVibe account to start a wiki.');
+ send(req, res, 200, views.newSpacePage({ staff: req.actor.staff }), { title: 'Start a wiki', robots: 'noindex, nofollow' });
});
});
router.post('/new-space', B('wiki.space.create'), form, wrap(async (req, res) => {
router.post('/new-space', B('wiki.space.create'), form, wrap(async (req, res) => {
- if (!req.actor.subject) return needSignIn(req, res, 'Sign in with your OpenVibe account to start a space.');
+ if (!req.actor.subject) return needSignIn(req, res, 'Sign in with your OpenVibe account to start a wiki.');
const b = req.body || {};
const b = req.body || {};
try {
try {
const space = await svc.createSpace({ name: b.name, slug: b.slug || null, description: b.description || null, visibility: b.visibility, kind: b.official === '1' && req.actor.staff ? 'official' : 'user' }, req.actor);
const space = await svc.createSpace({ name: b.name, slug: b.slug || null, description: b.description || null, visibility: b.visibility, kind: b.official === '1' && req.actor.staff ? 'official' : 'user' }, req.actor);
res.redirect(303, `/s/${encodeURIComponent(space.slug)}`);
res.redirect(303, `/s/${encodeURIComponent(space.slug)}`);
} catch (err) {
} catch (err) {
if (!err.status || err.status >= 500) throw err;
if (!err.status || err.status >= 500) throw err;
- send(req, res, err.status, views.newSpacePage({ values: b, error: err.message, staff: req.actor.staff }), { title: 'Start a space', robots: 'noindex, nofollow' });
+ send(req, res, err.status, views.newSpacePage({ values: b, error: err.message, staff: req.actor.staff }), { title: 'Start a wiki', robots: 'noindex, nofollow' });
}
}
}));
}));
@@ -165,7 +168,7 @@ function createPages({ svc, viewers, platform, config, log = console, limits })
@@ -165,7 +168,7 @@ function createPages({ svc, viewers, platform, config, log = console, limits })
const proposals = canEdit ? (await svc.pendingProposals(req.actor)).filter((p) => p.space_id === space.id) : [];
const proposals = canEdit ? (await svc.pendingProposals(req.actor)).filter((p) => p.space_id === space.id) : [];
const open = space.visibility === 'public';
const open = space.visibility === 'public';
send(req, res, 200, views.spacePage({ space, tree: await svc.tree(space, req.actor), canEdit, canManage: svc.access.canManage(space, req.actor), proposals }), {
send(req, res, 200, views.spacePage({ space, tree: await svc.tree(space, req.actor), canEdit, canManage: svc.access.canManage(space, req.actor), proposals }), {
- title: space.name, description: space.description || `${space.name}: a space on OpenVibe.Wiki.`, path: svc.spacePath(space),
+ title: space.name, description: space.description || `${space.name}: a wiki on OpenVibe.Wiki.`, path: svc.spacePath(space),
robots: open ? 'index, follow' : 'noindex, nofollow', cache: open ? 'public' : null,
robots: open ? 'index, follow' : 'noindex, nofollow', cache: open ? 'public' : null,
jsonLd: open ? seo.structuredData.breadcrumbs([{ name: 'Wiki', url: `${config.baseUrl}/` }, { name: space.name, url: `${config.baseUrl}${svc.spacePath(space)}` }]) : null,
jsonLd: open ? seo.structuredData.breadcrumbs([{ name: 'Wiki', url: `${config.baseUrl}/` }, { name: space.name, url: `${config.baseUrl}${svc.spacePath(space)}` }]) : null,
});
});
@@ -173,8 +176,8 @@ function createPages({ svc, viewers, platform, config, log = console, limits })
@@ -173,8 +176,8 @@ function createPages({ svc, viewers, platform, config, log = console, limits })
function editorsOnly(req, res, space) {
function editorsOnly(req, res, space) {
if (svc.access.canEdit(space, req.actor)) return true;
if (svc.access.canEdit(space, req.actor)) return true;
- if (!req.actor.subject) needSignIn(req, res, 'Sign in to edit. Only editors of this space can change it.');
- else errorPage(req, res, 403, 'Not an editor', 'Only editors of this space can do that. An owner of the space can give you a role.');
+ if (!req.actor.subject) needSignIn(req, res, 'Sign in to edit. Only editors of this wiki can change it.');
+ else errorPage(req, res, 403, 'Not an editor', 'Only editors of this wiki can do that. An owner of the wiki can give you a role.');
return false;
return false;
}
}
@@ -214,7 +217,7 @@ function createPages({ svc, viewers, platform, config, log = console, limits })
@@ -214,7 +217,7 @@ function createPages({ svc, viewers, platform, config, log = console, limits })
router.get('/s/:space/settings', wrap(async (req, res) => {
router.get('/s/:space/settings', wrap(async (req, res) => {
const space = await locateSpace(req, res);
const space = await locateSpace(req, res);
if (!space) return;
if (!space) return;
- if (!svc.access.canManage(space, req.actor)) return req.actor.subject ? errorPage(req, res, 403, 'Owners only', 'Only an owner of this space can change its settings.') : needSignIn(req, res, 'Sign in as an owner of this space.');
+ if (!svc.access.canManage(space, req.actor)) return req.actor.subject ? errorPage(req, res, 403, 'Owners only', 'Only an owner of this wiki can change its settings.') : needSignIn(req, res, 'Sign in as an owner of this wiki.');
send(req, res, 200, views.spaceSettingsPage({ space, roles: await svc.roles(space.id, req.actor), flash: req.query.saved ? 'Saved.' : null }), { title: `Settings of ${space.name}`, robots: 'noindex, nofollow' });
send(req, res, 200, views.spaceSettingsPage({ space, roles: await svc.roles(space.id, req.actor), flash: req.query.saved ? 'Saved.' : null }), { title: `Settings of ${space.name}`, robots: 'noindex, nofollow' });
}));
}));
@@ -234,13 +237,13 @@ function createPages({ svc, viewers, platform, config, log = console, limits })
@@ -234,13 +237,13 @@ function createPages({ svc, viewers, platform, config, log = console, limits })
}
}
}));
}));
- // Import pages into a space (owners): the same JSON bundle as POST /api/v1/spaces/:space/import,
+ // Import pages into a wiki (owners): the same JSON bundle as POST /api/v1/spaces/:space/import,
// pasted into a form; the options come from the form's controls.
// pasted into a form; the options come from the form's controls.
const importForm = express.urlencoded({ extended: false, limit: '6mb' });
const importForm = express.urlencoded({ extended: false, limit: '6mb' });
function ownersOnly(req, res, space) {
function ownersOnly(req, res, space) {
if (svc.access.canManage(space, req.actor)) return true;
if (svc.access.canManage(space, req.actor)) return true;
- if (!req.actor.subject) needSignIn(req, res, 'Sign in as an owner of this space to import pages.');
- else errorPage(req, res, 403, 'Owners only', 'Only an owner of this space imports pages into it.');
+ if (!req.actor.subject) needSignIn(req, res, 'Sign in as an owner of this wiki to import pages.');
+ else errorPage(req, res, 403, 'Owners only', 'Only an owner of this wiki imports pages into it.');
return false;
return false;
}
}
router.get('/s/:space/import', wrap(async (req, res) => {
router.get('/s/:space/import', wrap(async (req, res) => {
@@ -15,12 +15,12 @@ const { escapeHtml: esc } = require('openvibe-publishing/ssr');
@@ -15,12 +15,12 @@ const { escapeHtml: esc } = require('openvibe-publishing/ssr');
const SITE_NAME = 'OpenVibe.Wiki';
const SITE_NAME = 'OpenVibe.Wiki';
const NETWORK_URL = 'https://openvibe.network';
const NETWORK_URL = 'https://openvibe.network';
-const DEFAULT_DESCRIPTION = 'OpenVibe.Wiki: wiki spaces with page trees, revision history, citations and discussion, part of the OpenVibe network.';
+const DEFAULT_DESCRIPTION = 'OpenVibe.Wiki: wikis with page trees, revision history, citations and discussion, part of the OpenVibe network.';
// The one AI summary for the site: /llms.txt and /llms-full.txt publish it, and the home page carries it
// The one AI summary for the site: /llms.txt and /llms-full.txt publish it, and the home page carries it
// as the shell's ai-summary meta / WebPage JSON-LD (openvibe-publishing/layout forwards `summary`).
// as the shell's ai-summary meta / WebPage JSON-LD (openvibe-publishing/layout forwards `summary`).
-const AI_SUMMARY = 'Wiki spaces of the OpenVibe network: page trees, immutable revisions, citations attached to the revision that used them, infoboxes and internal links.';
+const AI_SUMMARY = 'Wikis of the OpenVibe network: page trees, immutable revisions, citations attached to the revision that used them, infoboxes and internal links.';
const NAV_LINKS = [
const NAV_LINKS = [
- { label: 'Spaces', href: '/' },
+ { label: 'Wikis', href: '/' },
{ label: 'Recent changes', href: '/recent' },
{ label: 'Recent changes', href: '/recent' },
{ label: 'Search', href: '/search' },
{ label: 'Search', href: '/search' },
];
];
@@ -49,7 +49,7 @@ function navConfig(o, config) {
@@ -49,7 +49,7 @@ function navConfig(o, config) {
service: 'wiki',
service: 'wiki',
apiBase: NETWORK_URL,
apiBase: NETWORK_URL,
links: [
links: [
- { label: 'Spaces', href: '/', active: o.active === 'home' },
+ { label: 'Wikis', href: '/', active: o.active === 'home' },
{ label: 'Recent changes', href: '/recent', active: o.active === 'recent' },
{ label: 'Recent changes', href: '/recent', active: o.active === 'recent' },
{ label: 'Search', href: '/search', active: o.active === 'search' },
{ label: 'Search', href: '/search', active: o.active === 'search' },
],
],
@@ -84,7 +84,7 @@ function renderPage(o) {
@@ -84,7 +84,7 @@ function renderPage(o) {
siteName: SITE_NAME,
siteName: SITE_NAME,
lang: 'en',
lang: 'en',
// The front page's title says what the site is (search results show it); every other page is "<page> · OpenVibe.Wiki".
// The front page's title says what the site is (search results show it); every other page is "<page> · OpenVibe.Wiki".
- title: o.title ? `${o.title} · ${SITE_NAME}` : o.path === '/' ? `${SITE_NAME}: wiki spaces with history and citations` : SITE_NAME,
+ title: o.title ? `${o.title} · ${SITE_NAME}` : o.path === '/' ? `${SITE_NAME}: wikis with history and citations` : SITE_NAME,
description: o.description || DEFAULT_DESCRIPTION,
description: o.description || DEFAULT_DESCRIPTION,
summary: o.summary,
summary: o.summary,
canonical: `${config.baseUrl}${o.path || '/'}`,
canonical: `${config.baseUrl}${o.path || '/'}`,
@@ -11,11 +11,11 @@ const { infoboxToText } = require('../wiki/content');
@@ -11,11 +11,11 @@ const { infoboxToText } = require('../wiki/content');
const { html, raw } = ssr;
const { html, raw } = ssr;
const t = (v) => raw(ssr.timeTag(v));
const t = (v) => raw(ssr.timeTag(v));
const e = encodeURIComponent;
const e = encodeURIComponent;
-const VIS_LABEL = { public: 'Public', members: 'Members (any signed-in OpenVibe account)', vip: 'VIP (the space owner\'s OpenVibe.VIP members)', private: 'Private (people with a role in this space)' };
+const VIS_LABEL = { public: 'Public', members: 'Members (any signed-in OpenVibe account)', vip: 'VIP (the wiki owner\'s OpenVibe.VIP members)', private: 'Private (people with a role in this wiki)' };
function wpath(space, page) { return `/w/${e(space.slug)}/${e(page.slug)}`; }
function wpath(space, page) { return `/w/${e(space.slug)}/${e(page.slug)}`; }
-/** rel for outbound links (citations, infobox URLs): community spaces add ugc (see service.linkRel). */
+/** rel for outbound links (citations, infobox URLs): community wikis add ugc (see service.linkRel). */
function outRel(space) { return space && space.kind === 'official' ? 'noopener nofollow' : 'nofollow ugc noopener'; }
function outRel(space) { return space && space.kind === 'official' ? 'noopener nofollow' : 'nofollow ugc noopener'; }
function notice(kind, text) { return html`<p class="wk-notice wk-${kind}" role="status">${text}</p>`; }
function notice(kind, text) { return html`<p class="wk-notice wk-${kind}" role="status">${text}</p>`; }
@@ -26,7 +26,7 @@ function errorBody({ status, title, message }) {
@@ -26,7 +26,7 @@ function errorBody({ status, title, message }) {
return html`<section class="wk-error"><h1>${title}</h1><p>${message}</p><p class="wk-muted">HTTP ${String(status)}</p><p><a href="/">Back to the wiki</a></p></section>`;
return html`<section class="wk-error"><h1>${title}</h1><p>${message}</p><p class="wk-muted">HTTP ${String(status)}</p><p><a href="/">Back to the wiki</a></p></section>`;
}
}
-/** A VIP space or page the viewer is not (yet) admitted to: the join prompt (WS-K task 8). */
+/** A VIP wiki or page the viewer is not (yet) admitted to: the join prompt (WS-K task 8). */
function vipGateBody({ title, spaceName, joinUrl, signedIn, signInUrl }) {
function vipGateBody({ title, spaceName, joinUrl, signedIn, signInUrl }) {
return html`<section class="wk-error"><h1>For VIP members</h1><p>${title ? html`<strong>${title}</strong> is` : 'This is'} for the VIP members of ${spaceName ? html`<strong>${spaceName}</strong>'s owner` : 'its owner'}.</p>
return html`<section class="wk-error"><h1>For VIP members</h1><p>${title ? html`<strong>${title}</strong> is` : 'This is'} for the VIP members of ${spaceName ? html`<strong>${spaceName}</strong>'s owner` : 'its owner'}.</p>
<p>${signedIn ? '' : html`<a class="wk-btn" href="${signInUrl}">Sign in</a> `}${joinUrl ? html`<a class="wk-btn" href="${joinUrl}" rel="noopener">See their VIP plans</a>` : ''}</p><p><a href="/">Back to the wiki</a></p></section>`;
<p>${signedIn ? '' : html`<a class="wk-btn" href="${signInUrl}">Sign in</a> `}${joinUrl ? html`<a class="wk-btn" href="${joinUrl}" rel="noopener">See their VIP plans</a>` : ''}</p><p><a href="/">Back to the wiki</a></p></section>`;
@@ -48,13 +48,13 @@ function home({ spaces, recent, shelves = new Map(), actor }) {
@@ -48,13 +48,13 @@ function home({ spaces, recent, shelves = new Map(), actor }) {
const official = spaces.filter((s) => s.kind === 'official');
const official = spaces.filter((s) => s.kind === 'official');
const user = spaces.filter((s) => s.kind === 'user');
const user = spaces.filter((s) => s.kind === 'user');
const signedIn = !!(actor && actor.subject);
const signedIn = !!(actor && actor.subject);
- const start = signedIn ? { label: 'Start a space', href: '/new-space', primary: true } : { label: 'Sign in to start a space', href: '/auth/login?next=%2Fnew-space', primary: true };
+ const start = signedIn ? { label: 'Start a wiki', href: '/new-space', primary: true } : { label: 'Sign in to start a wiki', href: '/auth/login?next=%2Fnew-space', primary: true };
const card = (s) => {
const card = (s) => {
const shelf = shelves.get(s.id);
const shelf = shelves.get(s.id);
return html`<li><a class="wk-space" href="/s/${e(s.slug)}"><b>${s.name}</b>${s.visibility !== 'public' ? html` <span class="wk-tag">${s.visibility}</span>` : ''}${s.description ? html`<span class="wk-space-text">${s.description}</span>` : ''}${shelf && shelf.total ? html`<span class="wk-space-meta">${String(shelf.total)} page${shelf.total === 1 ? '' : 's'}${shelf.updated ? html` · updated ${t(shelf.updated)}` : ''}</span>` : ''}</a></li>`;
return html`<li><a class="wk-space" href="/s/${e(s.slug)}"><b>${s.name}</b>${s.visibility !== 'public' ? html` <span class="wk-tag">${s.visibility}</span>` : ''}${s.description ? html`<span class="wk-space-text">${s.description}</span>` : ''}${shelf && shelf.total ? html`<span class="wk-space-meta">${String(shelf.total)} page${shelf.total === 1 ? '' : 's'}${shelf.updated ? html` · updated ${t(shelf.updated)}` : ''}</span>` : ''}</a></li>`;
};
};
- // Each official space with pages: what is inside it, its pages one level down as cards with their summaries. A space
- // without a shelf (not public, or empty) is a card under "Official spaces".
+ // Each official wiki with pages: what is inside it, its pages one level down as cards with their summaries. A space
+ // without a shelf (not public, or empty) is a card under "Official wikis".
const shelved = official.filter((s) => shelves.get(s.id) && shelves.get(s.id).pages.length);
const shelved = official.filter((s) => shelves.get(s.id) && shelves.get(s.id).pages.length);
const unshelved = official.filter((s) => !shelved.includes(s));
const unshelved = official.filter((s) => !shelved.includes(s));
const inside = shelved.map((s) => {
const inside = shelved.map((s) => {
@@ -70,23 +70,23 @@ function home({ spaces, recent, shelves = new Map(), actor }) {
@@ -70,23 +70,23 @@ function home({ spaces, recent, shelves = new Map(), actor }) {
return html`${raw(showcase.hero({
return html`${raw(showcase.hero({
eyebrow: 'OpenVibe.Wiki',
eyebrow: 'OpenVibe.Wiki',
title: 'Knowledge,', accent: 'with sources',
title: 'Knowledge,', accent: 'with sources',
- lede: 'Wiki spaces with page trees, revision history, citations and discussion. Official spaces are edited by OpenVibe staff; anyone signed in with an OpenVibe account can start a space of their own.',
- actions: [start, { label: 'Browse the spaces', href: '#spaces' }],
+ lede: 'Wikis with page trees, revision history, citations and discussion. Official wikis are edited by OpenVibe staff; anyone signed in with an OpenVibe account can start a wiki of their own.',
+ actions: [start, { label: 'Browse the wikis', href: '#spaces' }],
}))}
}))}
<div id="spaces"></div>
<div id="spaces"></div>
${raw(inside)}
${raw(inside)}
-${unshelved.length || !shelved.length ? html`<section class="sc-sec wk-home-sec" aria-labelledby="wk-official"><h2 id="wk-official">Official spaces</h2>${unshelved.length ? html`<ul class="wk-spaces">${unshelved.map(card)}</ul>` : html`<p class="wk-muted">None yet.</p>`}</section>` : ''}
-<section class="sc-sec wk-home-sec" aria-labelledby="wk-community"><h2 id="wk-community">Community spaces</h2>${user.length ? html`<ul class="wk-spaces">${user.map(card)}</ul>`
- : html`<div class="wk-empty"><p><b>No community spaces yet.</b> Any OpenVibe account can start one: public, members-only, for its owner's VIP members, or private, with the editors it chooses.</p><p><a class="wk-button" href="${start.href}">${start.label}</a></p></div>`}</section>
+${unshelved.length || !shelved.length ? html`<section class="sc-sec wk-home-sec" aria-labelledby="wk-official"><h2 id="wk-official">Official wikis</h2>${unshelved.length ? html`<ul class="wk-spaces">${unshelved.map(card)}</ul>` : html`<p class="wk-muted">None yet.</p>`}</section>` : ''}
+<section class="sc-sec wk-home-sec" aria-labelledby="wk-community"><h2 id="wk-community">Community wikis</h2>${user.length ? html`<ul class="wk-spaces">${user.map(card)}</ul>`
+ : html`<div class="wk-empty"><p><b>No community wikis yet.</b> Any OpenVibe account can start one: public, members-only, for its owner's VIP members, or private, with the editors it chooses.</p><p><a class="wk-button" href="${start.href}">${start.label}</a></p></div>`}</section>
${raw(showcase.features({
${raw(showcase.features({
- title: 'What a space gets',
+ title: 'What a wiki gets',
items: [
items: [
- { icon: 'ov:wiki', title: 'Page trees', text: 'Pages nest under pages, so a space reads like a book.' },
+ { icon: 'ov:wiki', title: 'Page trees', text: 'Pages nest under pages, so a wiki reads like a book.' },
{ icon: 'ov:history', title: 'Revision history', text: 'Every published revision is kept; compare or restore any of them.' },
{ icon: 'ov:history', title: 'Revision history', text: 'Every published revision is kept; compare or restore any of them.' },
{ icon: 'ov:check', title: 'Citations', text: 'Claims carry their sources, listed with every page.' },
{ icon: 'ov:check', title: 'Citations', text: 'Claims carry their sources, listed with every page.' },
{ icon: 'ov:community', title: 'Discussion', text: 'Each page has a thread on OpenVibe.Community.' },
{ icon: 'ov:community', title: 'Discussion', text: 'Each page has a thread on OpenVibe.Community.' },
- { icon: 'ov:account', title: 'Roles and visibility', text: 'Public, members-only, VIP or private spaces, with editors you choose.' },
- { icon: 'ov:search', title: 'Search and feeds', text: 'Full-text search across the spaces you can see, and Atom and JSON feeds.' },
+ { icon: 'ov:account', title: 'Roles and visibility', text: 'Public, members-only, VIP or private wikis, with editors you choose.' },
+ { icon: 'ov:search', title: 'Search and feeds', text: 'Full-text search across the wikis you can see, and Atom and JSON feeds.' },
],
],
}))}
}))}
<section class="sc-sec wk-home-sec" aria-labelledby="wk-recent-h"><h2 id="wk-recent-h">Recently published</h2>${recent.length ? html`<ul class="wk-recent">${recent.map((r) => html`<li><span><a href="${wpath(r.space, r.page)}">${r.page.title}</a> <span class="wk-muted">in ${r.space.name}</span></span> <span class="wk-muted">${t(r.page.revision_published_at)}</span></li>`)}</ul>` : html`<p class="wk-muted">Nothing published yet.</p>`}
<section class="sc-sec wk-home-sec" aria-labelledby="wk-recent-h"><h2 id="wk-recent-h">Recently published</h2>${recent.length ? html`<ul class="wk-recent">${recent.map((r) => html`<li><span><a href="${wpath(r.space, r.page)}">${r.page.title}</a> <span class="wk-muted">in ${r.space.name}</span></span> <span class="wk-muted">${t(r.page.revision_published_at)}</span></li>`)}</ul>` : html`<p class="wk-muted">Nothing published yet.</p>`}
@@ -101,12 +101,12 @@ function treeHtml(nodes, space) {
@@ -101,12 +101,12 @@ function treeHtml(nodes, space) {
function spacePage({ space, tree, canEdit, canManage, proposals }) {
function spacePage({ space, tree, canEdit, canManage, proposals }) {
return html`${crumbs([{ name: 'Wiki', url: '/' }, { name: space.name }])}
return html`${crumbs([{ name: 'Wiki', url: '/' }, { name: space.name }])}
<section><h1>${space.name}</h1>
<section><h1>${space.name}</h1>
-<p class="wk-muted">${space.kind === 'official' ? 'Official space' : 'Community space'} · ${VIS_LABEL[space.visibility]}</p>
+<p class="wk-muted">${space.kind === 'official' ? 'Official wiki' : 'Community wiki'} · ${VIS_LABEL[space.visibility]}</p>
${space.description ? html`<p>${space.description}</p>` : ''}
${space.description ? html`<p>${space.description}</p>` : ''}
-${canEdit ? html`<p><a class="wk-button" href="/s/${e(space.slug)}/new">New page</a>${canManage ? html` <a href="/s/${e(space.slug)}/import">Import pages</a> <a href="/s/${e(space.slug)}/settings">Space settings</a>` : ''}</p>` : ''}
+${canEdit ? html`<p><a class="wk-button" href="/s/${e(space.slug)}/new">New page</a>${canManage ? html` <a href="/s/${e(space.slug)}/import">Import pages</a> <a href="/s/${e(space.slug)}/settings">Wiki settings</a>` : ''}</p>` : ''}
${proposals && proposals.length ? html`<p>${notice('info', `${proposals.length} AI proposal(s) wait for review.`)} <a href="/s/${e(space.slug)}/proposals">Review them</a></p>` : ''}
${proposals && proposals.length ? html`<p>${notice('info', `${proposals.length} AI proposal(s) wait for review.`)} <a href="/s/${e(space.slug)}/proposals">Review them</a></p>` : ''}
<h2>Pages</h2>${tree.length ? raw(treeHtml(tree, space)) : html`<p class="wk-muted">No pages yet.</p>`}
<h2>Pages</h2>${tree.length ? raw(treeHtml(tree, space)) : html`<p class="wk-muted">No pages yet.</p>`}
-${space.visibility === 'public' ? html`<p class="wk-muted">Follow this space: <a href="/s/${e(space.slug)}/feed.atom" type="application/atom+xml">Atom</a> · <a href="/s/${e(space.slug)}/feed.json" type="application/feed+json">JSON Feed</a></p>` : ''}
+${space.visibility === 'public' ? html`<p class="wk-muted">Follow this wiki: <a href="/s/${e(space.slug)}/feed.atom" type="application/atom+xml">Atom</a> · <a href="/s/${e(space.slug)}/feed.json" type="application/feed+json">JSON Feed</a></p>` : ''}
</section>`;
</section>`;
}
}
@@ -190,7 +190,7 @@ function articlePage(v, { html: contentHtml, discussion, actor, mediaUrl, resolv
@@ -190,7 +190,7 @@ function articlePage(v, { html: contentHtml, discussion, actor, mediaUrl, resolv
trail.push({ name: rev.fields.title || page.title });
trail.push({ name: rev.fields.title || page.title });
const banners = [];
const banners = [];
if (flash) banners.push(notice('ok', flash));
if (flash) banners.push(notice('ok', flash));
- if (page.state !== 'published') banners.push(notice('warn', `This page is ${page.state === 'scheduled' ? 'scheduled, not published yet' : page.state}. Only editors of the space can see it.`));
+ if (page.state !== 'published') banners.push(notice('warn', `This page is ${page.state === 'scheduled' ? 'scheduled, not published yet' : page.state}. Only editors of the wiki can see it.`));
else if (!v.isPublishedRevision) banners.push(notice('warn', `You are looking at revision ${rev.number}, not the published revision ${page.published_revision}.`));
else if (!v.isPublishedRevision) banners.push(notice('warn', `You are looking at revision ${rev.number}, not the published revision ${page.published_revision}.`));
else if (v.canEdit && v.headNumber > rev.number) banners.push(html`<p class="wk-notice wk-info" role="status">Revision ${String(v.headNumber)} is newer than the published one and only editors can see it. <a href="${base}?rev=${String(v.headNumber)}">View it</a> · <a href="${base}/diff/${String(rev.number)}/${String(v.headNumber)}">compare</a></p>`);
else if (v.canEdit && v.headNumber > rev.number) banners.push(html`<p class="wk-notice wk-info" role="status">Revision ${String(v.headNumber)} is newer than the published one and only editors can see it. <a href="${base}?rev=${String(v.headNumber)}">View it</a> · <a href="${base}/diff/${String(rev.number)}/${String(v.headNumber)}">compare</a></p>`);
if (v.disclosure) banners.push(html`<p class="wk-notice wk-ai" role="note"><strong>${v.disclosure.short}.</strong> ${v.disclosure.long}</p>`);
if (v.disclosure) banners.push(html`<p class="wk-notice wk-ai" role="note"><strong>${v.disclosure.short}.</strong> ${v.disclosure.long}</p>`);
@@ -292,15 +292,15 @@ function formValuesFromRevision(rev, page) {
@@ -292,15 +292,15 @@ function formValuesFromRevision(rev, page) {
}
}
function newSpacePage({ values = {}, error = null, staff = false }) {
function newSpacePage({ values = {}, error = null, staff = false }) {
- return html`${crumbs([{ name: 'Wiki', url: '/' }, { name: 'New space' }])}
-<section><h1>Start a space</h1>${error ? notice('error', error) : ''}
+ return html`${crumbs([{ name: 'Wiki', url: '/' }, { name: 'New wiki' }])}
+<section><h1>Start a wiki</h1>${error ? notice('error', error) : ''}
<form method="post" action="/new-space" class="wk-form">
<form method="post" action="/new-space" class="wk-form">
<label>Name <input type="text" name="name" required maxlength="120" value="${values.name || ''}"></label>
<label>Name <input type="text" name="name" required maxlength="120" value="${values.name || ''}"></label>
<label>Address (optional; lowercase letters, digits and dashes) <input type="text" name="slug" maxlength="63" pattern="[a-z0-9][a-z0-9-]{1,62}" value="${values.slug || ''}"></label>
<label>Address (optional; lowercase letters, digits and dashes) <input type="text" name="slug" maxlength="63" pattern="[a-z0-9][a-z0-9-]{1,62}" value="${values.slug || ''}"></label>
<label>Description <input type="text" name="description" maxlength="1000" value="${values.description || ''}"></label>
<label>Description <input type="text" name="description" maxlength="1000" value="${values.description || ''}"></label>
<label>Visibility <select name="visibility">${['public', 'members', 'vip', 'private'].map((v) => html`<option value="${v}"${(values.visibility || 'public') === v ? raw(' selected') : ''}>${VIS_LABEL[v]}</option>`)}</select></label>
<label>Visibility <select name="visibility">${['public', 'members', 'vip', 'private'].map((v) => html`<option value="${v}"${(values.visibility || 'public') === v ? raw(' selected') : ''}>${VIS_LABEL[v]}</option>`)}</select></label>
-${staff ? html`<label class="wk-check"><input type="checkbox" name="official" value="1"> Official (editorial) space</label>` : ''}
-<p><button type="submit">Create space</button></p></form></section>`;
+${staff ? html`<label class="wk-check"><input type="checkbox" name="official" value="1"> Official (editorial) wiki</label>` : ''}
+<p><button type="submit">Create wiki</button></p></form></section>`;
}
}
const IMPORT_EXAMPLE = JSON.stringify({ pages: [
const IMPORT_EXAMPLE = JSON.stringify({ pages: [
@@ -334,7 +334,7 @@ function spaceSettingsPage({ space, roles, error = null, flash = null }) {
@@ -334,7 +334,7 @@ function spaceSettingsPage({ space, roles, error = null, flash = null }) {
<label>Name <input type="text" name="name" maxlength="120" value="${space.name}"></label>
<label>Name <input type="text" name="name" maxlength="120" value="${space.name}"></label>
<label>Description <input type="text" name="description" maxlength="1000" value="${space.description || ''}"></label>
<label>Description <input type="text" name="description" maxlength="1000" value="${space.description || ''}"></label>
<label>Visibility <select name="visibility">${['public', 'members', 'vip', 'private'].map((v) => html`<option value="${v}"${space.visibility === v ? raw(' selected') : ''}>${VIS_LABEL[v]}</option>`)}</select></label>
<label>Visibility <select name="visibility">${['public', 'members', 'vip', 'private'].map((v) => html`<option value="${v}"${space.visibility === v ? raw(' selected') : ''}>${VIS_LABEL[v]}</option>`)}</select></label>
-<p class="wk-muted">Making a space members-only or private removes its pages from sitemaps, feeds and OpenVibe.Search.</p>
+<p class="wk-muted">Making a wiki members-only or private removes its pages from sitemaps, feeds and OpenVibe.Search.</p>
<p><button type="submit" name="op" value="settings">Save settings</button></p></form>
<p><button type="submit" name="op" value="settings">Save settings</button></p></form>
<h2>Roles</h2>
<h2>Roles</h2>
<table><thead><tr><th scope="col">Person</th><th scope="col">Role</th><th scope="col">Since</th><th scope="col"></th></tr></thead><tbody>
<table><thead><tr><th scope="col">Person</th><th scope="col">Role</th><th scope="col">Since</th><th scope="col"></th></tr></thead><tbody>
@@ -345,7 +345,7 @@ ${roles.map((r) => html`<tr><td><code>${r.subject}</code></td><td>${r.role}</td>
@@ -345,7 +345,7 @@ ${roles.map((r) => html`<tr><td><code>${r.subject}</code></td><td>${r.role}</td>
<label>Role <select name="role"><option value="editor">editor</option><option value="viewer">viewer</option><option value="owner">owner</option></select></label>
<label>Role <select name="role"><option value="editor">editor</option><option value="viewer">viewer</option><option value="owner">owner</option></select></label>
<p><button type="submit">Grant role</button></p></form>
<p><button type="submit">Grant role</button></p></form>
<h2>Import</h2><p><a href="/s/${e(space.slug)}/import">Import pages from a bundle</a></p>
<h2>Import</h2><p><a href="/s/${e(space.slug)}/import">Import pages from a bundle</a></p>
-<h2>Delete</h2><form method="post" action="/s/${e(space.slug)}/settings" class="wk-form"><input type="hidden" name="op" value="delete"><label class="wk-check"><input type="checkbox" name="confirm" value="yes" required> Delete this space and take every page offline (addresses answer 410 Gone)</label><p><button type="submit">Delete space</button></p></form>
+<h2>Delete</h2><form method="post" action="/s/${e(space.slug)}/settings" class="wk-form"><input type="hidden" name="op" value="delete"><label class="wk-check"><input type="checkbox" name="confirm" value="yes" required> Delete this wiki and take every page offline (addresses answer 410 Gone)</label><p><button type="submit">Delete wiki</button></p></form>
</section>`;
</section>`;
}
}
@@ -353,7 +353,7 @@ function pageSettingsPage({ space, page, v, parents, error = null, flash = null,
@@ -353,7 +353,7 @@ function pageSettingsPage({ space, page, v, parents, error = null, flash = null,
const base = wpath(space, page);
const base = wpath(space, page);
return html`${crumbs([{ name: 'Wiki', url: '/' }, { name: space.name, url: `/s/${e(space.slug)}` }, { name: page.title, url: base }, { name: 'Settings' }])}
return html`${crumbs([{ name: 'Wiki', url: '/' }, { name: space.name, url: `/s/${e(space.slug)}` }, { name: page.title, url: base }, { name: 'Settings' }])}
<section><h1>Settings of ${page.title}</h1>${error ? notice('error', error) : ''}${flash ? notice('ok', flash) : ''}
<section><h1>Settings of ${page.title}</h1>${error ? notice('error', error) : ''}${flash ? notice('ok', flash) : ''}
-<p>State: <strong>${page.state}</strong>${page.published_revision ? html` · published revision ${String(page.published_revision)}` : ''} · visibility ${page.visibility} (space: ${space.visibility})</p>
+<p>State: <strong>${page.state}</strong>${page.published_revision ? html` · published revision ${String(page.published_revision)}` : ''} · visibility ${page.visibility} (wiki: ${space.visibility})</p>
<h2>Publish</h2>
<h2>Publish</h2>
<form method="post" action="${base}/settings" class="wk-form"><input type="hidden" name="op" value="publish">
<form method="post" action="${base}/settings" class="wk-form"><input type="hidden" name="op" value="publish">
<label>Revision <input type="number" name="revision" min="1" value="${String(v.headNumber)}"></label>
<label>Revision <input type="number" name="revision" min="1" value="${String(v.headNumber)}"></label>
@@ -52,7 +52,7 @@ const robotsOf = (head) => /<meta name="robots" content="([^"]*)">/.exec(head)[1
@@ -52,7 +52,7 @@ const robotsOf = (head) => /<meta name="robots" content="([^"]*)">/.exec(head)[1
// The home page: indexable by an explicit robots string, plus the showcase kit's stylesheet.
// The home page: indexable by an explicit robots string, plus the showcase kit's stylesheet.
r = await H.req(h, 'GET', '/');
r = await H.req(h, 'GET', '/');
let { head } = frameOf(r.text, '/');
let { head } = frameOf(r.text, '/');
- assert.ok(head.includes('<title>OpenVibe.Wiki: wiki spaces with history and citations</title>'), `the home title says what the site is: ${(/<title>[^<]*<\/title>/.exec(head) || [''])[0]}`);
+ assert.ok(head.includes('<title>OpenVibe.Wiki: wikis with history and citations</title>'), `the home title says what the site is: ${(/<title>[^<]*<\/title>/.exec(head) || [''])[0]}`);
assert.ok(/<link rel="canonical" href="https?:\/\/[^"\/]+\/">/.test(head), 'the home canonical');
assert.ok(/<link rel="canonical" href="https?:\/\/[^"\/]+\/">/.test(head), 'the home canonical');
assert.strictEqual(robotsOf(head), 'index, follow');
assert.strictEqual(robotsOf(head), 'index, follow');
assert.ok(/<link rel="stylesheet" href="\/shared\/[^"]*showcase[^"]*">/.test(head), 'the extra openvibe-shared stylesheet');
assert.ok(/<link rel="stylesheet" href="\/shared\/[^"]*showcase[^"]*">/.test(head), 'the extra openvibe-shared stylesheet');
@@ -73,8 +73,8 @@ const data = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'seeds', 'ope
@@ -73,8 +73,8 @@ const data = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'seeds', 'ope
assert.strictEqual(shelfCards, data.pages.length - roots.length, 'one card for each page under the root');
assert.strictEqual(shelfCards, data.pages.length - roots.length, 'one card for each page under the root');
assert.ok(shelf.includes('href="/w/openvibe/openvibe-live"'), 'cards link the pages');
assert.ok(shelf.includes('href="/w/openvibe/openvibe-live"'), 'cards link the pages');
assert.ok(!/<small>[^<]{241,}<\/small>/.test(shelf), 'a card carries one short sentence');
assert.ok(!/<small>[^<]{241,}<\/small>/.test(shelf), 'a card carries one short sentence');
- assert.ok(!homePage.text.includes('>Official spaces<'), 'a shelved space is not listed again');
- assert.ok(homePage.text.includes('No community spaces yet.'), 'the community section says how to start one');
+ assert.ok(!homePage.text.includes('>Official wikis<'), 'a shelved space is not listed again');
+ assert.ok(homePage.text.includes('No community wikis yet.'), 'the community section says how to start one');
// Absent from sitemaps, feeds and Search until reviewed.
// Absent from sitemaps, feeds and Search until reviewed.
assert.strictEqual(((await H.req(h, 'GET', '/sitemaps/pages-1.xml')).text.match(/<loc>/g) || []).length, 0);
assert.strictEqual(((await H.req(h, 'GET', '/sitemaps/pages-1.xml')).text.match(/<loc>/g) || []).length, 0);
const atomFeed = await H.req(h, 'GET', '/feed.atom');
const atomFeed = await H.req(h, 'GET', '/feed.atom');
@@ -60,7 +60,7 @@ async function lastIndexEvent(h, pageId) {
@@ -60,7 +60,7 @@ async function lastIndexEvent(h, pageId) {
assert.strictEqual(r.status, 200);
assert.strictEqual(r.status, 200);
assert.match(r.headers.get('content-type'), /^text\/plain/);
assert.match(r.headers.get('content-type'), /^text\/plain/);
assert.strictEqual(r.headers.get('cache-control'), 'public, max-age=3600, stale-while-revalidate=3600');
assert.strictEqual(r.headers.get('cache-control'), 'public, max-age=3600, stale-while-revalidate=3600');
- assert.ok(r.text.startsWith('# OpenVibe.Wiki') && r.text.includes('> Wiki spaces of the OpenVibe network'), 'the llms.txt header comes first');
+ assert.ok(r.text.startsWith('# OpenVibe.Wiki') && r.text.includes('> Wikis of the OpenVibe network'), 'the llms.txt header comes first');
assert.ok(r.text.includes('http://wiki.test/w/notes/open-page') && r.text.includes('http://wiki.test/w/notes/second-page'));
assert.ok(r.text.includes('http://wiki.test/w/notes/open-page') && r.text.includes('http://wiki.test/w/notes/second-page'));
assert.ok(!r.text.includes('unsourced-page'), 'only indexable pages, not merely listable ones');
assert.ok(!r.text.includes('unsourced-page'), 'only indexable pages, not merely listable ones');
assert.ok(Buffer.byteLength(r.text) <= 512 * 1024, 'the page stays within maxBytes');
assert.ok(Buffer.byteLength(r.text) <= 512 * 1024, 'the page stays within maxBytes');