openvibers/openvibe.vip Commits
Plan T11 lane D follow-up — attempt 3 report
Changes
nnew file mode 120000@@@ -0,0 +1 @@++/home/workstation/OpenVibers/OpenVibe.VIP/node_modules\\ No newline at end of file@@@ -8,7 +8,7 @@* GET /dashboard[?as=network] creator dashboard: plans, versions, perks, members, gated resources* GET /:username a creator's plan page (join through Billing)* GET /:username/plans/:slug that plan's public terms history-- * GET /robots.txt, /sitemap.xml++ * GET /robots.txt, /sitemap.xml, /llms.txt** Every POST needs the signed-in viewer and the form's anti-forgery token; it acts through the same* domain calls as the API and redirects back with a notice (?ok= / ?error=).@@@ -16,13 +16,16 @@const crypto = require('crypto');const ovServe = require('openvibe-shared/serve');const frame = require('openvibe-shared/frame');++const seo = require('openvibe-shared/seo');const express = require('express');const { VipError, bool } = require('../util');const { viewerMiddleware } = require('./session');const { createForms } = require('./forms');const pages = require('./pages');--const RESERVED = new Set(['me', 'dashboard', 'auth', 'api', 'internal', 'css', 'js', 'terms', 'privacy', 'dmca', 'robots.txt', 'sitemap.xml', 'release.json', 'metrics', 'favicon.ico', 'embed']);++const RESERVED = new Set(['me', 'dashboard', 'auth', 'api', 'internal', 'css', 'js', 'terms', 'privacy', 'dmca', 'robots.txt', 'sitemap.xml', 'llms.txt', 'release.json', 'metrics', 'favicon.ico', 'embed']);++// The sitemap's lastmod: when this server booted — a real date, computed once, never per request.++const BOOT_AT = new Date().toISOString();function createWebRoutes({ domain, config, layout, userAuth }) {const router = express.Router();@@@ -73,11 +76,32 @@ function createWebRoutes({ domain, config, layout, userAuth }) {});}-- router.get('/robots.txt', (req, res) => res.type('text/plain').send(`User-agent: *\nDisallow: /me\nDisallow: /dashboard\nDisallow: /auth/\nDisallow: /api/\nDisallow: /embed/\nSitemap: ${config.baseUrl}/sitemap.xml\n`));++ // Crawl and machine-readability artifacts, built from openvibe-shared/seo — the same++ // toolkit the other OpenVibe sites use. Public data only, never the viewer.++ router.get('/robots.txt', (req, res) => res.type('text/plain').send(seo.robotsTxt({++ sitemaps: [`${config.baseUrl}/sitemap.xml`],++ disallow: ['/me', '/dashboard', '/auth/', '/api/', '/embed/'],++ })));router.get('/sitemap.xml', async (req, res) => {-- const urls = [`${config.baseUrl}/`, ...(await creators.listPublic({ limit: 5000 })).map((c) => `${config.baseUrl}/${encodeURIComponent(c.username)}`)];-- res.type('application/xml').send(`<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n${urls.map((u) => `<url><loc>${u.replace(/&/g, '&')}</loc></url>`).join('\n')}\n</urlset>\n`);++ const urls = [{ loc: `${config.baseUrl}/`, lastmod: BOOT_AT }, ...(await creators.listPublic({ limit: 5000 })).map((c) => ({ loc: `${config.baseUrl}/${encodeURIComponent(c.username)}`, lastmod: BOOT_AT }))];++ res.type('application/xml').send(seo.sitemapXml(urls));});++ // /llms.txt (llmstxt.org): a plain-markdown map of the site for language-model crawlers.++ router.get('/llms.txt', (req, res) => res.type('text/plain').send(seo.llmsTxt({++ name: 'OpenVibe.VIP',++ summary: 'OpenVibe.VIP: memberships for creators — plans, perks and benefits recognised across every OpenVibe site.',++ details: 'A creator page lists the plans and perks a creator offers and how to join through Billing. Every plan keeps its published terms history, and a membership always points at the version it was bought under. Reading any public page needs no account; joining needs a signed-in person. Memberships, the dashboard, sign-in, embeds and the API are per-person and are never listed here.',++ sections: [++ { title: 'Start here', links: [++ { title: 'Creators with published plans', url: `${config.baseUrl}/`, note: 'every creator on OpenVibe.VIP, with their public plans' },++ { title: 'What shipped on OpenVibe.VIP', url: `${config.baseUrl}/updates` },++ ] },++ { title: 'Machine-readable', links: [++ { title: 'Sitemap', url: `${config.baseUrl}/sitemap.xml`, note: 'the public pages, with lastmod' },++ { title: 'robots.txt', url: `${config.baseUrl}/robots.txt` },++ ] },++ ],++ })));// What shipped on OpenVibe.VIP: the shared update log every OpenVibe site has.router.get('/updates', (req, res) => send(res, 200, { viewer: req.viewer, canonicalPath: '/updates', title: 'What shipped on OpenVibe.VIP', body: frame.updatesBody({ service: 'vip', siteName: 'OpenVibe.VIP' }) + `<script src="${ovServe.url('shipped.js')}" defer></script>` }));@@@ -183,6 +183,25 @@ const { test, run } = harness('api');assert.match(sm.text, /http:\/\/vip\.test\/lena/);});++ test('discovery artifacts: robots.txt, sitemap.xml and llms.txt from the shared seo kit', async () => {++ const robots = await t.call('GET', '/robots.txt', { token: null });++ assert.strictEqual(robots.status, 200);++ assert.match(robots.headers.get('content-type'), /^text\/plain/);++ for (const d of ['/me', '/dashboard', '/auth/', '/api/', '/embed/']) assert.match(robots.text, new RegExp(`Disallow: ${d.replace(/\//g, '\\/')}\\n`), `robots disallows ${d}`);++ assert.match(robots.text, /Sitemap: http:\/\/vip\.test\/sitemap\.xml\n/, 'robots names the sitemap');++ const sm = await t.call('GET', '/sitemap.xml', { token: null });++ assert.strictEqual(sm.status, 200);++ assert.match(sm.headers.get('content-type'), /^application\/xml/);++ assert.match(sm.text, /<loc>http:\/\/vip\.test\/<\/loc>/, 'the sitemap lists the home URL');++ assert.match(sm.text, /<loc>http:\/\/vip\.test\/lena<\/loc>/, 'the sitemap lists creator pages');++ assert.match(sm.text, /<lastmod>\d{4}-\d{2}-\d{2}<\/lastmod>/, 'every url carries a real lastmod');++ const llms = await t.call('GET', '/llms.txt', { token: null });++ assert.strictEqual(llms.status, 200);++ assert.match(llms.headers.get('content-type'), /^text\/plain/);++ assert.match(llms.text, /^# /, 'llms.txt starts with a markdown heading');++ assert.match(llms.text, /\[Creators with published plans\]\(http:\/\/vip\.test\/\)/, 'llms.txt lists the main public pages');++ });++test('every rendered page carries the boost marker, boost script (data-main) and the {path} sign-in template', async () => {const pages = [['/', null], ['/lena', null], ['/updates', null], ['/me', buyer], ['/dashboard', creator]];for (const [p, user] of pages) {
new file mode 120000
new file mode 120000
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+/home/workstation/OpenVibers/OpenVibe.VIP/node_modules
\ No newline at end of file
\ No newline at end of file
@@ -8,7 +8,7 @@
@@ -8,7 +8,7 @@
* GET /dashboard[?as=network] creator dashboard: plans, versions, perks, members, gated resources
* GET /dashboard[?as=network] creator dashboard: plans, versions, perks, members, gated resources
* GET /:username a creator's plan page (join through Billing)
* GET /:username a creator's plan page (join through Billing)
* GET /:username/plans/:slug that plan's public terms history
* GET /:username/plans/:slug that plan's public terms history
- * GET /robots.txt, /sitemap.xml
+ * GET /robots.txt, /sitemap.xml, /llms.txt
*
*
* Every POST needs the signed-in viewer and the form's anti-forgery token; it acts through the same
* Every POST needs the signed-in viewer and the form's anti-forgery token; it acts through the same
* domain calls as the API and redirects back with a notice (?ok= / ?error=).
* domain calls as the API and redirects back with a notice (?ok= / ?error=).
@@ -16,13 +16,16 @@
@@ -16,13 +16,16 @@
const crypto = require('crypto');
const crypto = require('crypto');
const ovServe = require('openvibe-shared/serve');
const ovServe = require('openvibe-shared/serve');
const frame = require('openvibe-shared/frame');
const frame = require('openvibe-shared/frame');
+const seo = require('openvibe-shared/seo');
const express = require('express');
const express = require('express');
const { VipError, bool } = require('../util');
const { VipError, bool } = require('../util');
const { viewerMiddleware } = require('./session');
const { viewerMiddleware } = require('./session');
const { createForms } = require('./forms');
const { createForms } = require('./forms');
const pages = require('./pages');
const pages = require('./pages');
-const RESERVED = new Set(['me', 'dashboard', 'auth', 'api', 'internal', 'css', 'js', 'terms', 'privacy', 'dmca', 'robots.txt', 'sitemap.xml', 'release.json', 'metrics', 'favicon.ico', 'embed']);
+const RESERVED = new Set(['me', 'dashboard', 'auth', 'api', 'internal', 'css', 'js', 'terms', 'privacy', 'dmca', 'robots.txt', 'sitemap.xml', 'llms.txt', 'release.json', 'metrics', 'favicon.ico', 'embed']);
+// The sitemap's lastmod: when this server booted — a real date, computed once, never per request.
+const BOOT_AT = new Date().toISOString();
function createWebRoutes({ domain, config, layout, userAuth }) {
function createWebRoutes({ domain, config, layout, userAuth }) {
const router = express.Router();
const router = express.Router();
@@ -73,11 +76,32 @@ function createWebRoutes({ domain, config, layout, userAuth }) {
@@ -73,11 +76,32 @@ function createWebRoutes({ domain, config, layout, userAuth }) {
});
});
}
}
- router.get('/robots.txt', (req, res) => res.type('text/plain').send(`User-agent: *\nDisallow: /me\nDisallow: /dashboard\nDisallow: /auth/\nDisallow: /api/\nDisallow: /embed/\nSitemap: ${config.baseUrl}/sitemap.xml\n`));
+ // Crawl and machine-readability artifacts, built from openvibe-shared/seo — the same
+ // toolkit the other OpenVibe sites use. Public data only, never the viewer.
+ router.get('/robots.txt', (req, res) => res.type('text/plain').send(seo.robotsTxt({
+ sitemaps: [`${config.baseUrl}/sitemap.xml`],
+ disallow: ['/me', '/dashboard', '/auth/', '/api/', '/embed/'],
+ })));
router.get('/sitemap.xml', async (req, res) => {
router.get('/sitemap.xml', async (req, res) => {
- const urls = [`${config.baseUrl}/`, ...(await creators.listPublic({ limit: 5000 })).map((c) => `${config.baseUrl}/${encodeURIComponent(c.username)}`)];
- res.type('application/xml').send(`<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n${urls.map((u) => `<url><loc>${u.replace(/&/g, '&')}</loc></url>`).join('\n')}\n</urlset>\n`);
+ const urls = [{ loc: `${config.baseUrl}/`, lastmod: BOOT_AT }, ...(await creators.listPublic({ limit: 5000 })).map((c) => ({ loc: `${config.baseUrl}/${encodeURIComponent(c.username)}`, lastmod: BOOT_AT }))];
+ res.type('application/xml').send(seo.sitemapXml(urls));
});
});
+ // /llms.txt (llmstxt.org): a plain-markdown map of the site for language-model crawlers.
+ router.get('/llms.txt', (req, res) => res.type('text/plain').send(seo.llmsTxt({
+ name: 'OpenVibe.VIP',
+ summary: 'OpenVibe.VIP: memberships for creators — plans, perks and benefits recognised across every OpenVibe site.',
+ details: 'A creator page lists the plans and perks a creator offers and how to join through Billing. Every plan keeps its published terms history, and a membership always points at the version it was bought under. Reading any public page needs no account; joining needs a signed-in person. Memberships, the dashboard, sign-in, embeds and the API are per-person and are never listed here.',
+ sections: [
+ { title: 'Start here', links: [
+ { title: 'Creators with published plans', url: `${config.baseUrl}/`, note: 'every creator on OpenVibe.VIP, with their public plans' },
+ { title: 'What shipped on OpenVibe.VIP', url: `${config.baseUrl}/updates` },
+ ] },
+ { title: 'Machine-readable', links: [
+ { title: 'Sitemap', url: `${config.baseUrl}/sitemap.xml`, note: 'the public pages, with lastmod' },
+ { title: 'robots.txt', url: `${config.baseUrl}/robots.txt` },
+ ] },
+ ],
+ })));
// What shipped on OpenVibe.VIP: the shared update log every OpenVibe site has.
// What shipped on OpenVibe.VIP: the shared update log every OpenVibe site has.
router.get('/updates', (req, res) => send(res, 200, { viewer: req.viewer, canonicalPath: '/updates', title: 'What shipped on OpenVibe.VIP', body: frame.updatesBody({ service: 'vip', siteName: 'OpenVibe.VIP' }) + `<script src="${ovServe.url('shipped.js')}" defer></script>` }));
router.get('/updates', (req, res) => send(res, 200, { viewer: req.viewer, canonicalPath: '/updates', title: 'What shipped on OpenVibe.VIP', body: frame.updatesBody({ service: 'vip', siteName: 'OpenVibe.VIP' }) + `<script src="${ovServe.url('shipped.js')}" defer></script>` }));
@@ -183,6 +183,25 @@ const { test, run } = harness('api');
@@ -183,6 +183,25 @@ const { test, run } = harness('api');
assert.match(sm.text, /http:\/\/vip\.test\/lena/);
assert.match(sm.text, /http:\/\/vip\.test\/lena/);
});
});
+ test('discovery artifacts: robots.txt, sitemap.xml and llms.txt from the shared seo kit', async () => {
+ const robots = await t.call('GET', '/robots.txt', { token: null });
+ assert.strictEqual(robots.status, 200);
+ assert.match(robots.headers.get('content-type'), /^text\/plain/);
+ for (const d of ['/me', '/dashboard', '/auth/', '/api/', '/embed/']) assert.match(robots.text, new RegExp(`Disallow: ${d.replace(/\//g, '\\/')}\\n`), `robots disallows ${d}`);
+ assert.match(robots.text, /Sitemap: http:\/\/vip\.test\/sitemap\.xml\n/, 'robots names the sitemap');
+ const sm = await t.call('GET', '/sitemap.xml', { token: null });
+ assert.strictEqual(sm.status, 200);
+ assert.match(sm.headers.get('content-type'), /^application\/xml/);
+ assert.match(sm.text, /<loc>http:\/\/vip\.test\/<\/loc>/, 'the sitemap lists the home URL');
+ assert.match(sm.text, /<loc>http:\/\/vip\.test\/lena<\/loc>/, 'the sitemap lists creator pages');
+ assert.match(sm.text, /<lastmod>\d{4}-\d{2}-\d{2}<\/lastmod>/, 'every url carries a real lastmod');
+ const llms = await t.call('GET', '/llms.txt', { token: null });
+ assert.strictEqual(llms.status, 200);
+ assert.match(llms.headers.get('content-type'), /^text\/plain/);
+ assert.match(llms.text, /^# /, 'llms.txt starts with a markdown heading');
+ assert.match(llms.text, /\[Creators with published plans\]\(http:\/\/vip\.test\/\)/, 'llms.txt lists the main public pages');
+ });
+
test('every rendered page carries the boost marker, boost script (data-main) and the {path} sign-in template', async () => {
test('every rendered page carries the boost marker, boost script (data-main) and the {path} sign-in template', async () => {
const pages = [['/', null], ['/lena', null], ['/updates', null], ['/me', buyer], ['/dashboard', creator]];
const pages = [['/', null], ['/lena', null], ['/updates', null], ['/me', buyer], ['/dashboard', creator]];
for (const [p, user] of pages) {
for (const [p, user] of pages) {