openvibers/openvibe.tips Commits

Route every Cache-Control value through openvibe-shared cache-policy (#2)

OP OpenVibers committed 2 Oct 2026 · 52fb3cb102 · 3 files, +40 −4

checks run by GitHub CI (test, test-pg) · CI ✓ 3/3 · review SHIP by space-bunny-alpha

https://github.com/OpenVibers/OpenVibe.Tips/pull/2

Changes

ddiff --git a/server/app.js b/server/app.js
iindex dcba874..4cf4de1 100644
---- a/server/app.js
++++ b/server/app.js
@@@ -21,6 +21,7 @@ const express = require('express');
const cookieParser = require('cookie-parser');
const { http } = require('openvibe-contracts');
const { isLoopbackDirect } = require('openvibe-shared/metrics');
++const cache = require('openvibe-shared/cache-policy');
const { loadConfig } = require('./config');
const { openDb } = require('./db');
const { createKeyProvider, createUserAuth } = require('./network');
@@@ -116,7 +117,7 @@ function createApp(opts = {}) {
setHeaders(res, filePath) {
const rel = path.relative(PUBLIC_DIR, filePath).split(path.sep).join('/');
const v = res.req && res.req.query && res.req.query.v;
-- res.setHeader('Cache-Control', v && v === assetVersion(rel) ? 'public, max-age=31536000, immutable' : 'no-cache');
++ res.setHeader('Cache-Control', cache.assetHeaders(rel, { hashed: v && v === assetVersion(rel) }));
},
}));
app.use(createWebRoutes({ domain, config, layout, userAuth }));
ddiff --git a/server/web/routes.js b/server/web/routes.js
iindex 36923a4..e8a7433 100644
---- a/server/web/routes.js
++++ b/server/web/routes.js
@@@ -23,6 +23,7 @@
*/
const crypto = require('crypto');
const ovServe = require('openvibe-shared/serve');
++const cache = require('openvibe-shared/cache-policy');
const frame = require('openvibe-shared/frame');
const seo = require('openvibe-shared/seo');
const express = require('express');
@@@ -79,7 +80,7 @@ function createWebRoutes({ domain, config, layout, userAuth }) {
r.get('/updates', withViewer, (req, res) => html(res, pageFor(req, { canonicalPath: '/updates', title: 'What shipped on OpenVibe.Tips', body: frame.updatesBody({ service: 'tips', siteName: 'OpenVibe.Tips' }) + `<script src="${ovServe.url('shipped.js')}" defer></script>` })));
// Crawl and machine-readability artifacts, built from openvibe-shared/seo — the same
// toolkit the other OpenVibe sites use. Public data only, never the viewer.
-- r.get('/robots.txt', (req, res) => res.type('text/plain').set('Cache-Control', 'public, max-age=3600').send(seo.robotsTxt({
++ r.get('/robots.txt', (req, res) => res.type('text/plain').set('Cache-Control', cache.htmlHeaders({ maxAge: 3600 })).send(seo.robotsTxt({
sitemaps: [`${config.baseUrl}/sitemap.xml`],
disallow: ['/dashboard', '/receipts', '/overlay/', '/moderate/', '/auth/', '/api/', '/internal/'],
allowAI: false,
@@@ -87,10 +88,10 @@ function createWebRoutes({ domain, config, layout, userAuth }) {
r.get('/sitemap.xml', async (req, res) => {
const rows = await db.many(sql`SELECT handle, updated_at FROM creator_tip_profiles WHERE page_enabled ORDER BY handle`);
const urls = [{ loc: `${config.baseUrl}/`, lastmod: BOOT_AT }, ...rows.map((p) => ({ loc: `${config.baseUrl}/${p.handle}`, lastmod: p.updated_at }))];
-- res.type('application/xml').set('Cache-Control', 'public, max-age=900').send(seo.sitemapXml(urls));
++ res.type('application/xml').set('Cache-Control', cache.htmlHeaders({ maxAge: 900 })).send(seo.sitemapXml(urls));
});
// /llms.txt (llmstxt.org): a plain-markdown map of the site for language-model crawlers.
-- r.get('/llms.txt', (req, res) => res.type('text/plain').set('Cache-Control', 'public, max-age=3600').send(seo.llmsTxt({
++ r.get('/llms.txt', (req, res) => res.type('text/plain').set('Cache-Control', cache.htmlHeaders({ maxAge: 3600 })).send(seo.llmsTxt({
name: 'OpenVibe.Tips',
summary: 'OpenVibe.Tips: creator support — tips, goals, paid messages, TTS and media requests, and overlays.',
details: 'A creator page lists the goals and a tip form that works without JavaScript; tips settle through Billing, and Tips owns the interaction, the goals and the overlay alerts. The home page lists creators whose pages are switched on. Reading any public page needs no account; tipping needs a signed-in supporter, and the dashboard, receipts, moderation, overlays, sign-in and the API are per-person and never listed here.',
ddiff --git a/test/asset-cache.test.js b/test/asset-cache.test.js
nnew file mode 100644
iindex 0000000..9507abd
---- /dev/null
++++ b/test/asset-cache.test.js
@@@ -0,0 +1,34 @@
++'use strict';
++// Cache-Control comes from openvibe-shared/cache-policy, not from a literal per route: a versioned
++// asset is immutable for a year, any other asset is a five-minute window with a day of
++// stale-while-revalidate, and the machine-readable pages get the shared html policy.
++const assert = require('assert');
++const { boot, check, done } = require('./helpers/app');
++const cache = require('openvibe-shared/cache-policy');
++const { assetVersion } = require('../server/web/layout');
++
++(async () => {
++ const t = await boot();
++ const get = async (p) => { const r = await fetch(t.base + p, { redirect: 'manual' }); return { status: r.status, headers: r.headers, text: await r.text() }; };
++
++ await check('static assets: immutable only under their own ?v=<version>, short-lived otherwise', async () => {
++ const rel = 'css/overlay.css';
++ const good = await get(`/${rel}?v=${assetVersion(rel)}`);
++ assert.strictEqual(good.status, 200);
++ assert.strictEqual(good.headers.get('cache-control'), cache.IMMUTABLE);
++ const wrong = await get(`/${rel}?v=deadbeefdeadbeef`);
++ assert.strictEqual(wrong.status, 200);
++ assert.strictEqual(wrong.headers.get('cache-control'), 'public, max-age=300, stale-while-revalidate=86400');
++ const bare = await get(`/${rel}`);
++ assert.strictEqual(bare.status, 200);
++ assert.strictEqual(bare.headers.get('cache-control'), 'public, max-age=300, stale-while-revalidate=86400');
++ });
++
++ await check('robots.txt: the shared html policy at the same window', async () => {
++ const r = await get('/robots.txt');
++ assert.strictEqual(r.status, 200);
++ assert.strictEqual(r.headers.get('cache-control'), cache.htmlHeaders({ maxAge: 3600 }));
++ });
++
++ await done();
++})();