openvibers/openvibe.host Commits
Host: the sitemap dates catalog pages by their newest price check (#82)
Changes
@@@ -242,9 +242,19 @@ async function createApp(opts = {}) {try {const providers = await catalog.providers();const plans = await catalog.sitemapPlans();-- const paths = ['/', '/hosting', ...legalPaths, '/catalog', '/catalog/promos', '/catalog/providers', ...providers.map((p) => `/catalog/providers/${p.slug}`), ...plans.map((p) => `/catalog/plans/${p.id}`)];++ // A catalog page's lastmod is the newest price check it shows: a plan's own, a provider's latest plan, and the++ // newest of all for the catalog and the front page. Static pages carry none.++ const day = (ms) => (Number.isFinite(Number(ms)) && ms != null ? new Date(Number(ms)).toISOString().slice(0, 10) : undefined);++ const latest = (rows) => rows.reduce((m, p) => Math.max(m, Number(p.checked_at) || 0), 0) || undefined;++ const newest = day(latest(plans));++ const urls = [++ { path: '/', lastmod: newest }, { path: '/hosting' }, ...legalPaths.map((path) => ({ path })),++ { path: '/catalog', lastmod: newest }, { path: '/catalog/promos' }, { path: '/catalog/providers', lastmod: newest },++ ...providers.map((v) => ({ path: `/catalog/providers/${v.slug}`, lastmod: day(latest(plans.filter((p) => p.provider_slug === v.slug))) })),++ ...plans.map((p) => ({ path: `/catalog/plans/${p.id}`, lastmod: day(p.checked_at) })),++ ];res.type('application/xml').set('Cache-Control', cache.htmlHeaders({ maxAge: 3600 }))-- .send(`<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n${paths.map((p) => ` <url><loc>${config.baseUrl}${p}</loc></url>`).join('\n')}\n</urlset>\n`);++ .send(sharedSeo.sitemapXml(urls.map((u) => ({ loc: `${config.baseUrl}${u.path}`, lastmod: u.lastmod }))));} catch (err) { next(err); }});// /llms.txt and /llms-full.txt (llmstxt.org): a map of Host's own public pages only. A tenant site, a preview@@@ -176,7 +176,8 @@ function createCatalog({ store, community = null, log = console }) {const r = await db.prepare("SELECT COUNT(*) AS plans, COUNT(DISTINCT p.provider_id) AS providers, MAX(p.checked_at) AS last FROM catalog_plans p JOIN catalog_providers v ON v.id = p.provider_id WHERE p.status = 'active' AND v.status = 'active'").get();return { plans: Number(r.plans) || 0, providers: Number(r.providers) || 0, lastChecked: r.last == null ? null : Number(r.last) };}-- async function sitemapPlans() { return await db.prepare("SELECT p.id FROM catalog_plans p JOIN catalog_providers v ON v.id = p.provider_id WHERE p.status = 'active' AND v.status = 'active' ORDER BY p.id").all(); }++ // Each public plan with the day its price was last checked, and its provider's slug, so the sitemap can date pages.++ async function sitemapPlans() { return await db.prepare("SELECT p.id, p.checked_at, v.slug AS provider_slug FROM catalog_plans p JOIN catalog_providers v ON v.id = p.provider_id WHERE p.status = 'active' AND v.status = 'active' ORDER BY p.id").all(); }return { onChange: (fn) => listeners.push(fn), save, get, applyItem, plans, plan, providers, promos, currencies: currenciesInUse, stats, sitemapPlans, discussionIdle: idle, all: async (type) => { const rows = await db.prepare(`SELECT * FROM catalog_${type} ORDER BY created_at DESC, id DESC LIMIT 200`).all(); return type === 'promos' ? rows.map(promoOut) : rows; } };}module.exports = { createCatalog };@@@ -56,6 +56,13 @@ const { boot, check, done } = require('./stageb/boot');assert.ok(!types((await t.api('GET', '/catalog?min_vcpu=2')).text).includes('ItemList'), 'a filtered view carries no JSON-LD');const promos = await t.api('GET', '/api/v1/catalog/promos'); assert.strictEqual(promos.json().promos.length, 1);});++ await check('the sitemap dates catalog pages by their newest price check', async () => {++ const xml = (await t.api('GET', '/sitemap.xml')).text;++ const planUrl = xml.match(/<url><loc>[^<]*\/catalog\/plans\/[^<]+<\/loc><lastmod>(\d{4}-\d{2}-\d{2})<\/lastmod><\/url>/);++ assert.ok(planUrl, `a plan page carries a lastmod: ${xml.slice(0, 400)}`);++ assert.match(xml, /<loc>[^<]*\/catalog<\/loc><lastmod>\d{4}-\d{2}-\d{2}<\/lastmod>/);++ assert.match(xml, /<loc>[^<]*\/terms<\/loc><\/url>/, 'a static page carries no lastmod');++ });} finally { await t.close(); }done();})().catch((e) => { console.error(e); process.exit(1); });
@@ -242,9 +242,19 @@ async function createApp(opts = {}) {
@@ -242,9 +242,19 @@ async function createApp(opts = {}) {
try {
try {
const providers = await catalog.providers();
const providers = await catalog.providers();
const plans = await catalog.sitemapPlans();
const plans = await catalog.sitemapPlans();
- const paths = ['/', '/hosting', ...legalPaths, '/catalog', '/catalog/promos', '/catalog/providers', ...providers.map((p) => `/catalog/providers/${p.slug}`), ...plans.map((p) => `/catalog/plans/${p.id}`)];
+ // A catalog page's lastmod is the newest price check it shows: a plan's own, a provider's latest plan, and the
+ // newest of all for the catalog and the front page. Static pages carry none.
+ const day = (ms) => (Number.isFinite(Number(ms)) && ms != null ? new Date(Number(ms)).toISOString().slice(0, 10) : undefined);
+ const latest = (rows) => rows.reduce((m, p) => Math.max(m, Number(p.checked_at) || 0), 0) || undefined;
+ const newest = day(latest(plans));
+ const urls = [
+ { path: '/', lastmod: newest }, { path: '/hosting' }, ...legalPaths.map((path) => ({ path })),
+ { path: '/catalog', lastmod: newest }, { path: '/catalog/promos' }, { path: '/catalog/providers', lastmod: newest },
+ ...providers.map((v) => ({ path: `/catalog/providers/${v.slug}`, lastmod: day(latest(plans.filter((p) => p.provider_slug === v.slug))) })),
+ ...plans.map((p) => ({ path: `/catalog/plans/${p.id}`, lastmod: day(p.checked_at) })),
+ ];
res.type('application/xml').set('Cache-Control', cache.htmlHeaders({ maxAge: 3600 }))
res.type('application/xml').set('Cache-Control', cache.htmlHeaders({ maxAge: 3600 }))
- .send(`<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n${paths.map((p) => ` <url><loc>${config.baseUrl}${p}</loc></url>`).join('\n')}\n</urlset>\n`);
+ .send(sharedSeo.sitemapXml(urls.map((u) => ({ loc: `${config.baseUrl}${u.path}`, lastmod: u.lastmod }))));
} catch (err) { next(err); }
} catch (err) { next(err); }
});
});
// /llms.txt and /llms-full.txt (llmstxt.org): a map of Host's own public pages only. A tenant site, a preview
// /llms.txt and /llms-full.txt (llmstxt.org): a map of Host's own public pages only. A tenant site, a preview
@@ -176,7 +176,8 @@ function createCatalog({ store, community = null, log = console }) {
@@ -176,7 +176,8 @@ function createCatalog({ store, community = null, log = console }) {
const r = await db.prepare("SELECT COUNT(*) AS plans, COUNT(DISTINCT p.provider_id) AS providers, MAX(p.checked_at) AS last FROM catalog_plans p JOIN catalog_providers v ON v.id = p.provider_id WHERE p.status = 'active' AND v.status = 'active'").get();
const r = await db.prepare("SELECT COUNT(*) AS plans, COUNT(DISTINCT p.provider_id) AS providers, MAX(p.checked_at) AS last FROM catalog_plans p JOIN catalog_providers v ON v.id = p.provider_id WHERE p.status = 'active' AND v.status = 'active'").get();
return { plans: Number(r.plans) || 0, providers: Number(r.providers) || 0, lastChecked: r.last == null ? null : Number(r.last) };
return { plans: Number(r.plans) || 0, providers: Number(r.providers) || 0, lastChecked: r.last == null ? null : Number(r.last) };
}
}
- async function sitemapPlans() { return await db.prepare("SELECT p.id FROM catalog_plans p JOIN catalog_providers v ON v.id = p.provider_id WHERE p.status = 'active' AND v.status = 'active' ORDER BY p.id").all(); }
+ // Each public plan with the day its price was last checked, and its provider's slug, so the sitemap can date pages.
+ async function sitemapPlans() { return await db.prepare("SELECT p.id, p.checked_at, v.slug AS provider_slug FROM catalog_plans p JOIN catalog_providers v ON v.id = p.provider_id WHERE p.status = 'active' AND v.status = 'active' ORDER BY p.id").all(); }
return { onChange: (fn) => listeners.push(fn), save, get, applyItem, plans, plan, providers, promos, currencies: currenciesInUse, stats, sitemapPlans, discussionIdle: idle, all: async (type) => { const rows = await db.prepare(`SELECT * FROM catalog_${type} ORDER BY created_at DESC, id DESC LIMIT 200`).all(); return type === 'promos' ? rows.map(promoOut) : rows; } };
return { onChange: (fn) => listeners.push(fn), save, get, applyItem, plans, plan, providers, promos, currencies: currenciesInUse, stats, sitemapPlans, discussionIdle: idle, all: async (type) => { const rows = await db.prepare(`SELECT * FROM catalog_${type} ORDER BY created_at DESC, id DESC LIMIT 200`).all(); return type === 'promos' ? rows.map(promoOut) : rows; } };
}
}
module.exports = { createCatalog };
module.exports = { createCatalog };
@@ -56,6 +56,13 @@ const { boot, check, done } = require('./stageb/boot');
@@ -56,6 +56,13 @@ const { boot, check, done } = require('./stageb/boot');
assert.ok(!types((await t.api('GET', '/catalog?min_vcpu=2')).text).includes('ItemList'), 'a filtered view carries no JSON-LD');
assert.ok(!types((await t.api('GET', '/catalog?min_vcpu=2')).text).includes('ItemList'), 'a filtered view carries no JSON-LD');
const promos = await t.api('GET', '/api/v1/catalog/promos'); assert.strictEqual(promos.json().promos.length, 1);
const promos = await t.api('GET', '/api/v1/catalog/promos'); assert.strictEqual(promos.json().promos.length, 1);
});
});
+ await check('the sitemap dates catalog pages by their newest price check', async () => {
+ const xml = (await t.api('GET', '/sitemap.xml')).text;
+ const planUrl = xml.match(/<url><loc>[^<]*\/catalog\/plans\/[^<]+<\/loc><lastmod>(\d{4}-\d{2}-\d{2})<\/lastmod><\/url>/);
+ assert.ok(planUrl, `a plan page carries a lastmod: ${xml.slice(0, 400)}`);
+ assert.match(xml, /<loc>[^<]*\/catalog<\/loc><lastmod>\d{4}-\d{2}-\d{2}<\/lastmod>/);
+ assert.match(xml, /<loc>[^<]*\/terms<\/loc><\/url>/, 'a static page carries no lastmod');
+ });
} finally { await t.close(); }
} finally { await t.close(); }
done();
done();
})().catch((e) => { console.error(e); process.exit(1); });
})().catch((e) => { console.error(e); process.exit(1); });