openvibers/openvibe.community Commits
Pastes browse: Show pastes, images or all (?type=), carried through paging (lang=image still means images); the home page's 'What is coming' roadmap block is replaced by what is live (spaces, threads, Pulse); /llms.txt says what Community is and how AI pastes are labelled
Changes
@@@ -192,6 +192,7 @@ function createApp(opts = {}) {// ── Machine endpoints ────────────────────────────────────app.get('/robots.txt', (_req, res) => res.type('text/plain').set('Cache-Control', 'public, max-age=3600').send(seo.robotsTxt()));++ app.get('/llms.txt', (_req, res) => res.type('text/plain').set('Cache-Control', 'public, max-age=3600').send(seo.llmsTxt()));app.get('/sitemap.xml', seo.sitemapHandler);app.get('/feed.xml', wrap(seo.feedHandler));@@@ -66,8 +66,11 @@ function normalizeBrowseQuery(query = {}) {const sort = query.sort === 'views' ? 'views' : 'new';const lang = String(query.lang || '').trim().toLowerCase().replace(/[^a-z0-9+#.-]/g, '').slice(0, 32);const page = Math.max(1, Math.min(parseInt(query.page, 10) || 1, 500));-- return { q, sort, lang, page };++ // What to list: text pastes (default), images (screenshots) or all of them.++ const type = ['all', 'pastes', 'images'].includes(query.type) ? query.type : (lang === 'image' ? 'images' : 'pastes');++ return { q, sort, lang: type === 'images' && lang === 'image' ? '' : lang, page, type };}++const _ofType = (type) => (p) => (type === 'all' ? true : type === 'images' ? p.type === 'screenshot' : p.type !== 'screenshot');/*** Browse: { q, sort: 'new'|'views', lang, page } → { pastes, total, page, pages, perPage, ... }.@@@ -75,11 +78,11 @@ function normalizeBrowseQuery(query = {}) {* sort and the language filter are answered from the recent window.*/async function browse(query = {}, ctx = {}) {-- const { q, sort, lang, page } = normalizeBrowseQuery(query);++ const { q, sort, lang, page, type } = normalizeBrowseQuery(query);const offset = (page - 1) * PER_PAGE;let pastes, total, windowed = false;-- if (sort === 'new' && !lang) {++ if (sort === 'new' && !lang && type === 'pastes') {const out = await source.listPastes({ limit: PER_PAGE, offset, search: q || undefined, type: 'paste' }, ctx);pastes = _textOnlyPublic(out && out.pastes);total = Number(out && out.total) || pastes.length;@@@ -91,14 +94,15 @@ async function browse(query = {}, ctx = {}) {} else {rows = await recent();}-- if (lang) rows = rows.filter((p) => (lang === 'image' ? p.type === 'screenshot' : (p.language || 'text') === lang));++ rows = rows.filter(_ofType(type));++ if (lang) rows = rows.filter((p) => (p.language || 'text') === lang);rows = rows.slice().sort(sort === 'views' ? byViews : byNewest);total = rows.length;pastes = rows.slice(offset, offset + PER_PAGE);windowed = true;}const pages = Math.max(1, Math.ceil(total / PER_PAGE));-- return { pastes, total, page, pages, perPage: PER_PAGE, q, sort, lang, windowed, windowSize: WINDOW };++ return { pastes, total, page, pages, perPage: PER_PAGE, q, sort, lang, type, windowed, windowSize: WINDOW };}/** For tests and a hot reload after a write. */@@@ -60,6 +60,7 @@ function pagerHref(state, page) {if (state.q) q.set('q', state.q);if (state.sort && state.sort !== 'new') q.set('sort', state.sort);if (state.lang) q.set('lang', state.lang);++ if (state.type && state.type !== 'pastes') q.set('type', state.type);if (page > 1) q.set('page', String(page));const s = q.toString();return `/pastes${s ? `?${s}` : ''}`;@@@ -127,14 +128,13 @@ function homePage({ latest, trending, languages, user }) {</div></section>--<section class="section coming" id="coming">-- <h2>What is coming</h2>++<section class="section coming" id="spaces">++ <h2>Spaces and threads</h2><div class="coming-grid">-- <div class="coming-item"><i class="fa-solid fa-layer-group" aria-hidden="true"></i><h3><a href="/s">Spaces</a></h3><p>Open now: General, Feedback and Showcase. Places for the communities around each streamer, game and project come next — run by the people in them.</p></div>-- <div class="coming-item"><i class="fa-solid fa-comments" aria-hidden="true"></i><h3><a href="/s/general">Threads</a></h3><p>Long-form discussion that outlives a chat scrollback, readable without JavaScript. Forums, the way they should have stayed.</p></div>-- <div class="coming-item"><i class="fa-solid fa-inbox" aria-hidden="true"></i><h3>Submissions</h3><p>Send in clips, art, ideas and reports; the community reviews and features the best of it.</p></div>++ <div class="coming-item"><i class="fa-solid fa-layer-group" aria-hidden="true"></i><h3><a href="/s">Spaces</a></h3><p>General, Feedback and Showcase: places for the community to talk, share and get help.</p></div>++ <div class="coming-item"><i class="fa-solid fa-comments" aria-hidden="true"></i><h3><a href="/s/general">Threads</a></h3><p>Long-form discussion that outlives a chat scrollback: start one in any open space.</p></div>++ <div class="coming-item"><i class="fa-solid fa-wave-square" aria-hidden="true"></i><h3><a href="/pulse">Pulse</a></h3><p>What is happening across the OpenVibe network, in one feed.</p></div></div>-- <p class="muted">Pastes, spaces, threads and the <a href="/pulse">Pulse</a> of the network are live today. The rest is being built in the open — follow along on <a href="https://github.com/OpenVibers" rel="noopener">GitHub</a> and <a href="https://discord.gg/M6MuRUaeJj" rel="noopener">Discord</a>.</p></section>`;return renderPage({title: null,@@@ -148,21 +148,21 @@ function homePage({ latest, trending, languages, user }) {// ── Browse ───────────────────────────────────────────────────function browsePage(result, languages) {-- const { pastes, total, page, pages, q, sort, lang, windowed, windowSize } = result;++ const { pastes, total, page, pages, q, sort, lang, type = 'pastes', windowed, windowSize } = result;const titleBits = [];if (q) titleBits.push(`“${q}”`);if (lang) titleBits.push(languageLabel(lang));titleBits.push(sort === 'views' ? 'most viewed' : 'newest');const title = `Pastes — ${titleBits.join(', ')}${page > 1 ? ` (page ${page})` : ''}`;-- const canonical = pagerHref({ q, sort, lang }, page);++ const canonical = pagerHref({ q, sort, lang, type }, page);const opt = (id, label) => `<option value="${esc(id)}"${id === (lang || '') ? ' selected' : ''}>${esc(label)}</option>`;const langOptions = [opt('', 'All languages')].concat((languages || []).map((l) => opt(l.language, `${languageLabel(l.language)} (${l.count})`)));if (lang && !(languages || []).some((l) => l.language === lang)) langOptions.push(opt(lang, languageLabel(lang)));const pager = pages > 1 ? `<nav class="pager" aria-label="Pages">-- ${page > 1 ? `<a rel="prev" href="${esc(pagerHref({ q, sort, lang }, page - 1))}"><i class="fa-solid fa-chevron-left" aria-hidden="true"></i> Newer</a>` : '<span></span>'}++ ${page > 1 ? `<a rel="prev" href="${esc(pagerHref({ q, sort, lang, type }, page - 1))}"><i class="fa-solid fa-chevron-left" aria-hidden="true"></i> Newer</a>` : '<span></span>'}<span class="pager-info">Page ${page} of ${pages}</span>-- ${page < pages ? `<a rel="next" href="${esc(pagerHref({ q, sort, lang }, page + 1))}">Older <i class="fa-solid fa-chevron-right" aria-hidden="true"></i></a>` : '<span></span>'}++ ${page < pages ? `<a rel="next" href="${esc(pagerHref({ q, sort, lang, type }, page + 1))}">Older <i class="fa-solid fa-chevron-right" aria-hidden="true"></i></a>` : '<span></span>'}</nav>` : '';const body = `@@@ -172,6 +172,7 @@ function browsePage(result, languages) {</header><form class="filters" method="get" action="/pastes" role="search" data-browse><label class="search"><i class="fa-solid fa-magnifying-glass" aria-hidden="true"></i><input type="search" name="q" value="${esc(q)}" placeholder="Search titles and content" aria-label="Search pastes"></label>++ <label>Show <select name="type"><option value="pastes"${type === 'pastes' ? ' selected' : ''}>Pastes</option><option value="images"${type === 'images' ? ' selected' : ''}>Images</option><option value="all"${type === 'all' ? ' selected' : ''}>All</option></select></label><label>Sort <select name="sort"><option value="new"${sort === 'new' ? ' selected' : ''}>Newest</option><option value="views"${sort === 'views' ? ' selected' : ''}>Most viewed</option></select></label><label>Language <select name="lang">${langOptions.join('')}</select></label><button class="btn" type="submit">Apply</button>@@@ -118,6 +118,30 @@ function threadLd({ space, thread, posts, opening, description }) {};}++// ── llms.txt: what this site is, for language-model crawlers (llmstxt.org) ────────────++function llmsTxt() {++ const u = config.baseUrl;++ return [++ '# OpenVibe.Community',++ '',++ '> The discussion side of the OpenVibe network: pastes (text and screenshots), spaces with threads, and the Pulse feed of what is happening across the network.',++ '',++ '## Browse',++ `- [Pastes](${u}/pastes): public pastes, newest first (?type=pastes|images|all, ?sort=views, ?lang=<language>)`,++ `- [Spaces](${u}/s): open discussion spaces and their threads`,++ `- [Pulse](${u}/pulse): network activity`,++ '',++ '## Machine-readable',++ `- [Sitemap](${u}/sitemap.xml)`,++ `- [Latest pastes (RSS)](${u}/feed.xml)`,++ `- [Latest threads (RSS)](${u}/s/feed.xml)`,++ '',++ '## Content labels',++ '- Pastes made by the OpenVibe AI (moments caught from live streams) carry origin "ai" and are labelled as AI-generated; everything else was written by people.',++ '',++ ].join('\n');++}++// ── robots.txt ───────────────────────────────────────────────function robotsTxt() {return [@@@ -226,4 +250,4 @@ ${items.join('\n')}function resetCaches() { _sitemap = null; _sitemapAt = 0; }--module.exports = { isoDate, clean, websiteLd, breadcrumbLd, pasteLd, threadLd, discussionAuthorLd, robotsTxt, buildSitemap, sitemapHandler, feedHandler, threadFeed, useForum, resetCaches };++module.exports = { llmsTxt, isoDate, clean, websiteLd, breadcrumbLd, pasteLd, threadLd, discussionAuthorLd, robotsTxt, buildSitemap, sitemapHandler, feedHandler, threadFeed, useForum, resetCaches };@@@ -22,7 +22,7 @@ const { boot, check, done } = require('./helpers/app');has(r.text, '<h1>Share it with a link. Talk about it here.</h1>');has(r.text, '<h2>Latest pastes</h2>');has(r.text, '<h2>Most viewed</h2>');-- has(r.text, 'id="coming"');++ has(r.text, 'id="spaces"');has(r.text, 'href="/p/amber-fox-42"');// Most-viewed puts the 900-view nginx paste first.const trendingAt = r.text.indexOf('id="trending"');
@@ -192,6 +192,7 @@ function createApp(opts = {}) {
@@ -192,6 +192,7 @@ function createApp(opts = {}) {
// ── Machine endpoints ────────────────────────────────────
// ── Machine endpoints ────────────────────────────────────
app.get('/robots.txt', (_req, res) => res.type('text/plain').set('Cache-Control', 'public, max-age=3600').send(seo.robotsTxt()));
app.get('/robots.txt', (_req, res) => res.type('text/plain').set('Cache-Control', 'public, max-age=3600').send(seo.robotsTxt()));
+ app.get('/llms.txt', (_req, res) => res.type('text/plain').set('Cache-Control', 'public, max-age=3600').send(seo.llmsTxt()));
app.get('/sitemap.xml', seo.sitemapHandler);
app.get('/sitemap.xml', seo.sitemapHandler);
app.get('/feed.xml', wrap(seo.feedHandler));
app.get('/feed.xml', wrap(seo.feedHandler));
@@ -66,8 +66,11 @@ function normalizeBrowseQuery(query = {}) {
@@ -66,8 +66,11 @@ function normalizeBrowseQuery(query = {}) {
const sort = query.sort === 'views' ? 'views' : 'new';
const sort = query.sort === 'views' ? 'views' : 'new';
const lang = String(query.lang || '').trim().toLowerCase().replace(/[^a-z0-9+#.-]/g, '').slice(0, 32);
const lang = String(query.lang || '').trim().toLowerCase().replace(/[^a-z0-9+#.-]/g, '').slice(0, 32);
const page = Math.max(1, Math.min(parseInt(query.page, 10) || 1, 500));
const page = Math.max(1, Math.min(parseInt(query.page, 10) || 1, 500));
- return { q, sort, lang, page };
+ // What to list: text pastes (default), images (screenshots) or all of them.
+ const type = ['all', 'pastes', 'images'].includes(query.type) ? query.type : (lang === 'image' ? 'images' : 'pastes');
+ return { q, sort, lang: type === 'images' && lang === 'image' ? '' : lang, page, type };
}
}
+const _ofType = (type) => (p) => (type === 'all' ? true : type === 'images' ? p.type === 'screenshot' : p.type !== 'screenshot');
/**
/**
* Browse: { q, sort: 'new'|'views', lang, page } → { pastes, total, page, pages, perPage, ... }.
* Browse: { q, sort: 'new'|'views', lang, page } → { pastes, total, page, pages, perPage, ... }.
@@ -75,11 +78,11 @@ function normalizeBrowseQuery(query = {}) {
@@ -75,11 +78,11 @@ function normalizeBrowseQuery(query = {}) {
* sort and the language filter are answered from the recent window.
* sort and the language filter are answered from the recent window.
*/
*/
async function browse(query = {}, ctx = {}) {
async function browse(query = {}, ctx = {}) {
- const { q, sort, lang, page } = normalizeBrowseQuery(query);
+ const { q, sort, lang, page, type } = normalizeBrowseQuery(query);
const offset = (page - 1) * PER_PAGE;
const offset = (page - 1) * PER_PAGE;
let pastes, total, windowed = false;
let pastes, total, windowed = false;
- if (sort === 'new' && !lang) {
+ if (sort === 'new' && !lang && type === 'pastes') {
const out = await source.listPastes({ limit: PER_PAGE, offset, search: q || undefined, type: 'paste' }, ctx);
const out = await source.listPastes({ limit: PER_PAGE, offset, search: q || undefined, type: 'paste' }, ctx);
pastes = _textOnlyPublic(out && out.pastes);
pastes = _textOnlyPublic(out && out.pastes);
total = Number(out && out.total) || pastes.length;
total = Number(out && out.total) || pastes.length;
@@ -91,14 +94,15 @@ async function browse(query = {}, ctx = {}) {
@@ -91,14 +94,15 @@ async function browse(query = {}, ctx = {}) {
} else {
} else {
rows = await recent();
rows = await recent();
}
}
- if (lang) rows = rows.filter((p) => (lang === 'image' ? p.type === 'screenshot' : (p.language || 'text') === lang));
+ rows = rows.filter(_ofType(type));
+ if (lang) rows = rows.filter((p) => (p.language || 'text') === lang);
rows = rows.slice().sort(sort === 'views' ? byViews : byNewest);
rows = rows.slice().sort(sort === 'views' ? byViews : byNewest);
total = rows.length;
total = rows.length;
pastes = rows.slice(offset, offset + PER_PAGE);
pastes = rows.slice(offset, offset + PER_PAGE);
windowed = true;
windowed = true;
}
}
const pages = Math.max(1, Math.ceil(total / PER_PAGE));
const pages = Math.max(1, Math.ceil(total / PER_PAGE));
- return { pastes, total, page, pages, perPage: PER_PAGE, q, sort, lang, windowed, windowSize: WINDOW };
+ return { pastes, total, page, pages, perPage: PER_PAGE, q, sort, lang, type, windowed, windowSize: WINDOW };
}
}
/** For tests and a hot reload after a write. */
/** For tests and a hot reload after a write. */
@@ -60,6 +60,7 @@ function pagerHref(state, page) {
@@ -60,6 +60,7 @@ function pagerHref(state, page) {
if (state.q) q.set('q', state.q);
if (state.q) q.set('q', state.q);
if (state.sort && state.sort !== 'new') q.set('sort', state.sort);
if (state.sort && state.sort !== 'new') q.set('sort', state.sort);
if (state.lang) q.set('lang', state.lang);
if (state.lang) q.set('lang', state.lang);
+ if (state.type && state.type !== 'pastes') q.set('type', state.type);
if (page > 1) q.set('page', String(page));
if (page > 1) q.set('page', String(page));
const s = q.toString();
const s = q.toString();
return `/pastes${s ? `?${s}` : ''}`;
return `/pastes${s ? `?${s}` : ''}`;
@@ -127,14 +128,13 @@ function homePage({ latest, trending, languages, user }) {
@@ -127,14 +128,13 @@ function homePage({ latest, trending, languages, user }) {
</div>
</div>
</section>
</section>
-<section class="section coming" id="coming">
- <h2>What is coming</h2>
+<section class="section coming" id="spaces">
+ <h2>Spaces and threads</h2>
<div class="coming-grid">
<div class="coming-grid">
- <div class="coming-item"><i class="fa-solid fa-layer-group" aria-hidden="true"></i><h3><a href="/s">Spaces</a></h3><p>Open now: General, Feedback and Showcase. Places for the communities around each streamer, game and project come next — run by the people in them.</p></div>
- <div class="coming-item"><i class="fa-solid fa-comments" aria-hidden="true"></i><h3><a href="/s/general">Threads</a></h3><p>Long-form discussion that outlives a chat scrollback, readable without JavaScript. Forums, the way they should have stayed.</p></div>
- <div class="coming-item"><i class="fa-solid fa-inbox" aria-hidden="true"></i><h3>Submissions</h3><p>Send in clips, art, ideas and reports; the community reviews and features the best of it.</p></div>
+ <div class="coming-item"><i class="fa-solid fa-layer-group" aria-hidden="true"></i><h3><a href="/s">Spaces</a></h3><p>General, Feedback and Showcase: places for the community to talk, share and get help.</p></div>
+ <div class="coming-item"><i class="fa-solid fa-comments" aria-hidden="true"></i><h3><a href="/s/general">Threads</a></h3><p>Long-form discussion that outlives a chat scrollback: start one in any open space.</p></div>
+ <div class="coming-item"><i class="fa-solid fa-wave-square" aria-hidden="true"></i><h3><a href="/pulse">Pulse</a></h3><p>What is happening across the OpenVibe network, in one feed.</p></div>
</div>
</div>
- <p class="muted">Pastes, spaces, threads and the <a href="/pulse">Pulse</a> of the network are live today. The rest is being built in the open — follow along on <a href="https://github.com/OpenVibers" rel="noopener">GitHub</a> and <a href="https://discord.gg/M6MuRUaeJj" rel="noopener">Discord</a>.</p>
</section>`;
</section>`;
return renderPage({
return renderPage({
title: null,
title: null,
@@ -148,21 +148,21 @@ function homePage({ latest, trending, languages, user }) {
@@ -148,21 +148,21 @@ function homePage({ latest, trending, languages, user }) {
// ── Browse ───────────────────────────────────────────────────
// ── Browse ───────────────────────────────────────────────────
function browsePage(result, languages) {
function browsePage(result, languages) {
- const { pastes, total, page, pages, q, sort, lang, windowed, windowSize } = result;
+ const { pastes, total, page, pages, q, sort, lang, type = 'pastes', windowed, windowSize } = result;
const titleBits = [];
const titleBits = [];
if (q) titleBits.push(`“${q}”`);
if (q) titleBits.push(`“${q}”`);
if (lang) titleBits.push(languageLabel(lang));
if (lang) titleBits.push(languageLabel(lang));
titleBits.push(sort === 'views' ? 'most viewed' : 'newest');
titleBits.push(sort === 'views' ? 'most viewed' : 'newest');
const title = `Pastes — ${titleBits.join(', ')}${page > 1 ? ` (page ${page})` : ''}`;
const title = `Pastes — ${titleBits.join(', ')}${page > 1 ? ` (page ${page})` : ''}`;
- const canonical = pagerHref({ q, sort, lang }, page);
+ const canonical = pagerHref({ q, sort, lang, type }, page);
const opt = (id, label) => `<option value="${esc(id)}"${id === (lang || '') ? ' selected' : ''}>${esc(label)}</option>`;
const opt = (id, label) => `<option value="${esc(id)}"${id === (lang || '') ? ' selected' : ''}>${esc(label)}</option>`;
const langOptions = [opt('', 'All languages')].concat((languages || []).map((l) => opt(l.language, `${languageLabel(l.language)} (${l.count})`)));
const langOptions = [opt('', 'All languages')].concat((languages || []).map((l) => opt(l.language, `${languageLabel(l.language)} (${l.count})`)));
if (lang && !(languages || []).some((l) => l.language === lang)) langOptions.push(opt(lang, languageLabel(lang)));
if (lang && !(languages || []).some((l) => l.language === lang)) langOptions.push(opt(lang, languageLabel(lang)));
const pager = pages > 1 ? `<nav class="pager" aria-label="Pages">
const pager = pages > 1 ? `<nav class="pager" aria-label="Pages">
- ${page > 1 ? `<a rel="prev" href="${esc(pagerHref({ q, sort, lang }, page - 1))}"><i class="fa-solid fa-chevron-left" aria-hidden="true"></i> Newer</a>` : '<span></span>'}
+ ${page > 1 ? `<a rel="prev" href="${esc(pagerHref({ q, sort, lang, type }, page - 1))}"><i class="fa-solid fa-chevron-left" aria-hidden="true"></i> Newer</a>` : '<span></span>'}
<span class="pager-info">Page ${page} of ${pages}</span>
<span class="pager-info">Page ${page} of ${pages}</span>
- ${page < pages ? `<a rel="next" href="${esc(pagerHref({ q, sort, lang }, page + 1))}">Older <i class="fa-solid fa-chevron-right" aria-hidden="true"></i></a>` : '<span></span>'}
+ ${page < pages ? `<a rel="next" href="${esc(pagerHref({ q, sort, lang, type }, page + 1))}">Older <i class="fa-solid fa-chevron-right" aria-hidden="true"></i></a>` : '<span></span>'}
</nav>` : '';
</nav>` : '';
const body = `
const body = `
@@ -172,6 +172,7 @@ function browsePage(result, languages) {
@@ -172,6 +172,7 @@ function browsePage(result, languages) {
</header>
</header>
<form class="filters" method="get" action="/pastes" role="search" data-browse>
<form class="filters" method="get" action="/pastes" role="search" data-browse>
<label class="search"><i class="fa-solid fa-magnifying-glass" aria-hidden="true"></i><input type="search" name="q" value="${esc(q)}" placeholder="Search titles and content" aria-label="Search pastes"></label>
<label class="search"><i class="fa-solid fa-magnifying-glass" aria-hidden="true"></i><input type="search" name="q" value="${esc(q)}" placeholder="Search titles and content" aria-label="Search pastes"></label>
+ <label>Show <select name="type"><option value="pastes"${type === 'pastes' ? ' selected' : ''}>Pastes</option><option value="images"${type === 'images' ? ' selected' : ''}>Images</option><option value="all"${type === 'all' ? ' selected' : ''}>All</option></select></label>
<label>Sort <select name="sort"><option value="new"${sort === 'new' ? ' selected' : ''}>Newest</option><option value="views"${sort === 'views' ? ' selected' : ''}>Most viewed</option></select></label>
<label>Sort <select name="sort"><option value="new"${sort === 'new' ? ' selected' : ''}>Newest</option><option value="views"${sort === 'views' ? ' selected' : ''}>Most viewed</option></select></label>
<label>Language <select name="lang">${langOptions.join('')}</select></label>
<label>Language <select name="lang">${langOptions.join('')}</select></label>
<button class="btn" type="submit">Apply</button>
<button class="btn" type="submit">Apply</button>
@@ -118,6 +118,30 @@ function threadLd({ space, thread, posts, opening, description }) {
@@ -118,6 +118,30 @@ function threadLd({ space, thread, posts, opening, description }) {
};
};
}
}
+// ── llms.txt: what this site is, for language-model crawlers (llmstxt.org) ────────────
+function llmsTxt() {
+ const u = config.baseUrl;
+ return [
+ '# OpenVibe.Community',
+ '',
+ '> The discussion side of the OpenVibe network: pastes (text and screenshots), spaces with threads, and the Pulse feed of what is happening across the network.',
+ '',
+ '## Browse',
+ `- [Pastes](${u}/pastes): public pastes, newest first (?type=pastes|images|all, ?sort=views, ?lang=<language>)`,
+ `- [Spaces](${u}/s): open discussion spaces and their threads`,
+ `- [Pulse](${u}/pulse): network activity`,
+ '',
+ '## Machine-readable',
+ `- [Sitemap](${u}/sitemap.xml)`,
+ `- [Latest pastes (RSS)](${u}/feed.xml)`,
+ `- [Latest threads (RSS)](${u}/s/feed.xml)`,
+ '',
+ '## Content labels',
+ '- Pastes made by the OpenVibe AI (moments caught from live streams) carry origin "ai" and are labelled as AI-generated; everything else was written by people.',
+ '',
+ ].join('\n');
+}
+
// ── robots.txt ───────────────────────────────────────────────
// ── robots.txt ───────────────────────────────────────────────
function robotsTxt() {
function robotsTxt() {
return [
return [
@@ -226,4 +250,4 @@ ${items.join('\n')}
@@ -226,4 +250,4 @@ ${items.join('\n')}
function resetCaches() { _sitemap = null; _sitemapAt = 0; }
function resetCaches() { _sitemap = null; _sitemapAt = 0; }
-module.exports = { isoDate, clean, websiteLd, breadcrumbLd, pasteLd, threadLd, discussionAuthorLd, robotsTxt, buildSitemap, sitemapHandler, feedHandler, threadFeed, useForum, resetCaches };
+module.exports = { llmsTxt, isoDate, clean, websiteLd, breadcrumbLd, pasteLd, threadLd, discussionAuthorLd, robotsTxt, buildSitemap, sitemapHandler, feedHandler, threadFeed, useForum, resetCaches };
@@ -22,7 +22,7 @@ const { boot, check, done } = require('./helpers/app');
@@ -22,7 +22,7 @@ const { boot, check, done } = require('./helpers/app');
has(r.text, '<h1>Share it with a link. Talk about it here.</h1>');
has(r.text, '<h1>Share it with a link. Talk about it here.</h1>');
has(r.text, '<h2>Latest pastes</h2>');
has(r.text, '<h2>Latest pastes</h2>');
has(r.text, '<h2>Most viewed</h2>');
has(r.text, '<h2>Most viewed</h2>');
- has(r.text, 'id="coming"');
+ has(r.text, 'id="spaces"');
has(r.text, 'href="/p/amber-fox-42"');
has(r.text, 'href="/p/amber-fox-42"');
// Most-viewed puts the 900-view nginx paste first.
// Most-viewed puts the 900-view nginx paste first.
const trendingAt = r.text.indexOf('id="trending"');
const trendingAt = r.text.indexOf('id="trending"');