openvibers/openvibe.events Commits

Events: /updates — what shipped on openvibe.events (the shared update log) (#80)

OP OpenVibers committed 29 days ago · a6d9b6925b · 4 files, +49 −8

openvibe.events gets `/updates`: what shipped on the site, from the network's shared update log (`openvibe-shared/frame` `updatesBody` + `shipped.js`), like the other OpenVibe sites. A site-wide crawl found openvibe.help linking it (404).

- One `<h1>`, a canonical URL, the home's CSP and a public 5-minute cache. The vhost's GET catch-all already reaches the app.
- The footer links it. The sitemap lists it with the release's date, and llms.txt and llms-full.txt carry an entry for it.

`test/home.test.js` checks the page and the sitemap's two entries.

Changes

ddiff --git a/server/app.js b/server/app.js
iindex 320c5a6..88b5d3c 100644
---- a/server/app.js
++++ b/server/app.js
@@@ -15,7 +15,7 @@ const { resourcesRouter } = require('./api/resources');
const { resourceControlRouter } = require('./api/resource-control');
const { mountLimits } = require('./limits');
const { createLimits } = require('./actor-limits');
--const { renderHome, renderNotFound, HOME_CSP } = require('./home');
++const { renderHome, renderUpdates, renderNotFound, HOME_CSP } = require('./home');
const appIcon = require('openvibe-shared/app-icon');
const { createDiscoveryRoutes } = require('./discovery');
const ovServe = require('openvibe-shared/serve');
@@@ -110,6 +110,12 @@ function createApp({ config, store, auth, keys, worker, realtime, metrics, dnsLo
// API index for curl and API clients (`*/*`, no Accept, or any non-HTML Accept). Vary: Accept so a
// shared cache never serves one to the other. The home carries its own CSP (HOME_CSP); the only
// global security header is X-Content-Type-Options above, kept on every route.
++ // GET /updates: what shipped on this site (the shared update log), for browsers; the vhost passes GET through.
++ app.get('/updates', (req, res) => {
++ res.type('html').set('Content-Security-Policy', HOME_CSP).set('Cache-Control', 'public, max-age=300')
++ .send(renderUpdates({ siteUrl: config.siteUrl }));
++ });
++
app.get('/', (req, res) => {
res.vary('Accept');
if (req.accepts(['text/plain', 'text/html']) === 'text/html') {
ddiff --git a/server/discovery.js b/server/discovery.js
iindex 763322a..4c1d226 100644
---- a/server/discovery.js
++++ b/server/discovery.js
@@@ -49,11 +49,12 @@ function catalog({ siteUrl, apiUrl }) {
return [
{ title: 'Start here', links: [
{ title: 'OpenVibe.Events home', url: home, text: DESCRIPTION },
++ { title: 'What shipped', url: seo.absolute('/updates', siteUrl), note: 'every change deployed, newest first', text: 'The update log of OpenVibe.Events: every change deployed to the service, newest first, each a commit from the OpenVibers repositories linked to the change itself (the network\'s shared log; the whole network\'s is at https://openvibe.network/updates).' },
{ title: 'Guide', url: GUIDE, note: 'the README: publishing, subscriptions, developer apps and realtime', text: details({ siteUrl, apiUrl }) },
{ title: 'Source code', url: SOURCE, text: 'The OpenVibe.Events source on GitHub: the durable event store, the publish, read and subscription API, the delivery worker, the retention tiers and the realtime gateway.' },
] },
{ title: 'Machine-readable', links: [
-- { title: 'Sitemap', url: seo.absolute('/sitemap.xml', siteUrl), text: `The product home only: ${home}` },
++ { title: 'Sitemap', url: seo.absolute('/sitemap.xml', siteUrl), text: `The product home and its update log: ${home} and ${seo.absolute('/updates', siteUrl)}` },
{ title: 'robots.txt', url: seo.absolute('/robots.txt', siteUrl), text: 'The crawler rules: the product home is welcome, the API and the realtime gateway are kept out of the index.' },
{ title: 'API and realtime', url: apiUrl, note: 'the JSON API and the realtime gateway', text: `${apiUrl}/api/v1 is the token-guarded JSON API; ${apiUrl}/realtime/stream is the browser Server-Sent Events gateway.` },
] },
@@@ -68,7 +69,7 @@ function createDiscoveryRoutes({ config, releaseLastmod }) {
.send(seo.robotsTxt({ sitemaps: [abs('/sitemap.xml')], disallow: ['/api/', '/realtime/'] })));
router.get('/sitemap.xml', (_req, res) => res.type('application/xml').set('Cache-Control', cache.htmlHeaders())
-- .send(seo.sitemapXml([{ loc: abs('/'), lastmod: releaseLastmod }])));
++ .send(seo.sitemapXml([{ loc: abs('/'), lastmod: releaseLastmod }, { loc: abs('/updates'), lastmod: releaseLastmod }])));
const pages = catalog({ siteUrl: config.siteUrl, apiUrl: config.baseUrl });
ddiff --git a/server/home.js b/server/home.js
iindex 794ad26..dc77495 100644
---- a/server/home.js
++++ b/server/home.js
@@@ -10,6 +10,7 @@
* HOME_CSP its Content-Security-Policy (the Frame's scripts and calls to the Network)
*/
const shell = require('openvibe-shared/shell');
++const frame = require('openvibe-shared/frame');
const showcase = require('openvibe-shared/showcase');
const ovServe = require('openvibe-shared/serve');
const appIcon = require('openvibe-shared/app-icon');
@@@ -97,6 +98,29 @@ function sections({ apiUrl }) {
// webhook steps two by two instead of three and one alone, each title level with its number.
const HOME_STYLE = '<style>body{margin:0}.page .sc-code{grid-template-columns:minmax(0,1fr)}.page .sc-steps{grid-template-columns:repeat(auto-fit,minmax(min(100%,420px),1fr))}.page .sc-steps li{align-items:start;align-content:start}.page .sc-steps b{line-height:30px}</style>';
++const FOOTER = { service: 'events', variant: 'full', mount: '#ov-footer', brandName: SITE_NAME, updates: '/updates' };
++
++/** /updates: what shipped on this site, the network's shared update log (openvibe-shared/frame updatesBody). */
++function renderUpdates({ siteUrl }) {
++ const links = [{ label: 'Guide', href: `${SOURCE}#readme` }];
++ return shell.page({
++ name: SITE_NAME, service: 'events', lang: 'en',
++ title: `What shipped on ${SITE_NAME}`,
++ siteName: SITE_NAME,
++ description: `Every change deployed to ${SITE_NAME}, newest first.`,
++ canonical: `${siteUrl}/updates`,
++ robots: 'index, follow',
++ navbar: { service: 'events', apiBase: NETWORK_URL, links, history: { type: 'page', title: SITE_NAME } },
++ footer: FOOTER, home: '/', navLinks: links,
++ head: appIcon.headTags({ site: 'events' }),
++ body: `<div id="navbar-mount"></div>
++<main id="main" class="page">
++${frame.updatesBody({ service: 'events', siteName: SITE_NAME })}
++</main>
++<script src="${ovServe.url('shipped.js')}" defer></script>`,
++ });
++}
++
function renderHome({ siteUrl, apiUrl }) {
const nav = {
service: 'events',
@@@ -104,7 +128,7 @@ function renderHome({ siteUrl, apiUrl }) {
links: [{ label: 'Guide', href: `${SOURCE}#readme` }],
history: { type: 'page', title: SITE_NAME },
};
-- const footer = { service: 'events', variant: 'full', mount: '#ov-footer', brandName: SITE_NAME };
++ const footer = FOOTER;
return shell.page({
name: SITE_NAME, service: 'events', lang: 'en',
title: `${SITE_NAME}: durable events, signed webhooks and replay`,
@@@ -134,4 +158,4 @@ function renderNotFound() {
'<h1>Not found</h1>', `<p>Nothing is here. <a href="/">${SITE_NAME}</a> · <a href="${NETWORK_URL}">OpenVibe</a></p>`, '</body>', '</html>'].join('\n');
}
--module.exports = { renderHome, renderNotFound, HOME_CSP, DESCRIPTION, sections };
++module.exports = { renderHome, renderUpdates, renderNotFound, HOME_CSP, DESCRIPTION, sections };
ddiff --git a/test/home.test.js b/test/home.test.js
iindex fe91e93..f82f7d6 100644
---- a/test/home.test.js
++++ b/test/home.test.js
@@@ -138,12 +138,22 @@ t('robots.txt welcomes the home and keeps the API out, and names the sitemap', a
assert.ok(r.body.includes(`Sitemap: ${SITE}/sitemap.xml`));
});
--t('sitemap.xml lists the product home only', async () => {
++t('sitemap.xml lists the product home and its update log', async () => {
const r = await raw(h.base, '/sitemap.xml');
assert.strictEqual(r.status, 200);
assert.match(r.headers['content-type'], /xml/);
-- assert.deepStrictEqual([...r.body.matchAll(/<loc>([^<]+)<\/loc>/g)].map((m) => m[1]), [`${SITE}/`]);
-- assert.strictEqual(assertSitemapLastmods(r.body).length, 1);
++ assert.deepStrictEqual([...r.body.matchAll(/<loc>([^<]+)<\/loc>/g)].map((m) => m[1]), [`${SITE}/`, `${SITE}/updates`]);
++ assert.strictEqual(assertSitemapLastmods(r.body).length, 2);
++});
++
++t('/updates: what shipped on this site, the shared update log, with one heading', async () => {
++ const r = await raw(h.base, '/updates', { accept: 'text/html' });
++ assert.strictEqual(r.status, 200);
++ assert.ok(r.body.includes('<h1>What shipped on OpenVibe.Events</h1>') && r.body.includes('data-ov-shipped="log" data-service="events"'));
++ assert.strictEqual((r.body.match(/<h1[\s>]/g) || []).length, 1);
++ assert.match(r.body, /<script src="\/shared\/shipped\.js\?v=[0-9a-f]+" defer><\/script>/);
++ assert.ok(r.body.includes(`<link rel="canonical" href="${SITE}/updates">`));
++ assert.ok(String(r.headers['content-security-policy']).includes('https://openvibe.network'), 'the log reads the network changelog');
});
t('llms.txt is a plain-text map that starts with the site name', async () => {