From e160b39f9e5895e61df1ff47d9d2a5b09fe94b3b Mon Sep 17 00:00:00 2001 From: Anthony Ettinger Date: Mon, 19 Oct 2026 12:38:13 +0000 Subject: [PATCH] Render the Profullstack webring footer server-side The landing and docs pages now end with @profullstack/footer (site links, the one copyright line, the << Profullstack >> ring), rendered on the server from the @latest template so rssamplifier's verifier sees the ring links. It replaces the old footer rather than adding a second copyright row. The page CSP forbids inline styles, so the template CSS is served from /assets/pfs-footer.css (css: false in the markup). Adds /.well-known/openwebring.json. The CrawlProof sponsored line moves from directly above the footer to the top of the content (no ads in or against footers). Co-Authored-By: Claude Opus 5.5 --- package.json | 1 + pnpm-lock.yaml | 15 +++++++++++++++ src/web.ts | 26 +++++++++++++++++++++++++- test/web.test.ts | 23 +++++++++++++++++++++++ web/docs.html | 12 ++++++------ web/index.html | 17 ++++++----------- 6 files changed, 76 insertions(+), 18 deletions(-) diff --git a/package.json b/package.json index b7f7226..3b4df8d 100644 --- a/package.json +++ b/package.json @@ -51,6 +51,7 @@ "@hono/node-server": "2.1.5", "@libsql/client": "0.18.0", "@modelcontextprotocol/sdk": "1.32.1", + "@profullstack/footer": "^0.2.4", "@simplewebauthn/server": "^13.3.3", "hono": "4.13.13", "undici": "^7.30.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ed4c953..4d044a8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,6 +17,9 @@ importers: '@modelcontextprotocol/sdk': specifier: 1.32.1 version: 1.32.1(zod@4.6.7) + '@profullstack/footer': + specifier: ^0.2.4 + version: 0.2.4 '@simplewebauthn/server': specifier: ^13.3.3 version: 13.3.3 @@ -347,6 +350,16 @@ packages: resolution: {integrity: sha512-C2Xj8FZ0uHWeCXXqX5B4/gVFQmtSkiuOolzAgutjTfseNOHT3pUjljDZsTSxXFGgio54bCzVFqmEOUrIVk8RDA==} engines: {node: '>=20.0.0'} + '@profullstack/footer@0.2.4': + resolution: {integrity: sha512-2ErIzHJCDA2baP8aK6f7rCHohW4EX+wnp7D1mrVMQTUxRcyZwKzmho96hhEFJndXRBGgxT3H/78BHOeBfLxTzg==} + engines: {node: '>=18'} + hasBin: true + peerDependencies: + react: '>=18' + peerDependenciesMeta: + react: + optional: true + '@rolldown/binding-android-arm-eabi@1.2.13': resolution: {integrity: sha512-Vu/NICrgdRvUOeaJZIyB2FP63s6fO/4ud5l8dXmxDx9yDyn+mN0y+gsvsVviSli+T361Aqo0h46EuMoobrwawA==} engines: {node: ^20.19.0 || >=22.12.0} @@ -1553,6 +1566,8 @@ snapshots: tslib: 2.8.1 tsyringe: 4.10.0 + '@profullstack/footer@0.2.4': {} + '@rolldown/binding-android-arm-eabi@1.2.13': optional: true diff --git a/src/web.ts b/src/web.ts index 4115000..71be9ef 100644 --- a/src/web.ts +++ b/src/web.ts @@ -5,6 +5,7 @@ import { fileURLToPath } from 'node:url'; import type { Context, Hono } from 'hono'; import { deleteCookie, getCookie, setCookie } from 'hono/cookie'; import { z } from 'zod'; +import { descriptorResponse, footerHtml, latestTemplate } from '@profullstack/footer'; import * as auth from './auth.js'; import { escapeHtml } from './adapters/tinylaunch.js'; import { loginMail, signupMail, type Mailer } from './mail.js'; @@ -80,6 +81,18 @@ function loadStatic(): Map { return files; } +export const SITE_URL = 'https://submitfleet.com/'; + +/** + * The shared Profullstack footer (links, copyright, << Profullstack >> webring), + * rendered on the server from the @latest template so the ring verifier, which + * reads HTML and runs no script, sees the ring links. The page CSP forbids + * inline styles, so its CSS is served from /assets/pfs-footer.css instead. + */ +export function siteFooter(links: { label: string; href: string }[]): Promise { + return footerHtml({ site: SITE_URL, links, css: false }); +} + function safeNext(raw: string | undefined): string { // Only same-site paths: never "//evil", never a scheme. return raw && /^\/(?!\/)[\w\-./?=&%#]*$/.test(raw) ? raw : '/app'; @@ -138,6 +151,7 @@ export function mountWeb(app: Hono<{ Variables: { actor: Actor } }>, fleet: Subm app.get('/', async (c) => { const stats = await fleet.catalogStats(); const body = page('index.html') + .replace('{{FOOTER}}', await siteFooter([{ label: 'Docs', href: '/docs' }, { label: 'GitHub', href: 'https://github.com/profullstack/submitfleet' }, { label: 'Sign in', href: '/login' }])) .replaceAll('{{TOTAL}}', String(stats.total)) .replaceAll('{{FREE}}', String(stats.free)) .replaceAll('{{DIRECTORIES}}', String((stats.by_channel.directory ?? 0) + (stats.by_channel.citation ?? 0))) @@ -147,7 +161,10 @@ export function mountWeb(app: Hono<{ Variables: { actor: Actor } }>, fleet: Subm }); for (const p of ['/signup', '/login']) app.get(p, (c) => html(c, page('auth.html'))); app.get('/auth/link', (c) => html(c, page('link.html'))); - app.get('/docs', (c) => html(c, page('docs.html'))); + app.get('/docs', async (c) => + html(c, page('docs.html').replace('{{FOOTER}}', await siteFooter([{ label: 'Home', href: '/' }, { label: 'GitHub', href: 'https://github.com/profullstack/submitfleet' }]))), + ); + app.get('/.well-known/openwebring.json', () => descriptorResponse({ site: SITE_URL, name: 'SubmitFleet' })); const appShell = async (c: Context) => { const user = await auth.sessionUser(fleet.db, getCookie(c, auth.SESSION_COOKIE)); if (!user) return c.redirect(`/login?next=${encodeURIComponent(new URL(c.req.url).pathname)}`); @@ -155,6 +172,13 @@ export function mountWeb(app: Hono<{ Variables: { actor: Actor } }>, fleet: Subm }; app.get('/app', appShell); app.get('/app/*', appShell); + // The footer template's CSS (@latest, same cache as the markup); before /assets/*. + app.get('/assets/pfs-footer.css', async (c) => + c.body((await latestTemplate()).css.replace(/@import[^;]*;?/gi, '').replace(/<\/?style/gi, ''), 200, { + 'content-type': 'text/css; charset=utf-8', + 'cache-control': 'public, max-age=300', + }), + ); app.get('/assets/*', (c) => { const f = files.get(c.req.path.replace(/^\//, '')); if (!f) return c.notFound(); diff --git a/test/web.test.ts b/test/web.test.ts index 0651672..476d702 100644 --- a/test/web.test.ts +++ b/test/web.test.ts @@ -5,6 +5,9 @@ import { buildApp } from '../src/http.js'; import type { Mail } from '../src/mail.js'; import { harness, type Harness } from './helpers.js'; +// Render the footer from the bundled template: no CDN fetch in tests. +process.env.PROFULLSTACK_FOOTER_OFFLINE ??= '1'; + let h: Harness; let mails: Mail[]; let app: ReturnType; @@ -72,6 +75,26 @@ describe('pages', () => { }); }); +describe('webring footer', () => { + it('server-renders the Profullstack ring footer on the landing and docs pages, with its CSS from a route', async () => { + for (const p of ['/', '/docs']) { + const html = await (await req('GET', p)).text(); + expect(html, p).toContain('https://rssamplifier.com/ring/profullstack/next?from=https%3A%2F%2Fsubmitfleet.com%2F'); + expect(html, p).toContain('/assets/pfs-footer.css'); + expect(html, p).not.toContain(' { it('signs up through the emailed link only, then signs in with the password', async () => { jar.clear(); diff --git a/web/docs.html b/web/docs.html index 9a233ab..860cf1d 100644 --- a/web/docs.html +++ b/web/docs.html @@ -7,6 +7,7 @@ + @@ -16,6 +17,10 @@ + +

Docs

@@ -64,11 +69,6 @@

Self-hosting

- - - - +{{FOOTER}} diff --git a/web/index.html b/web/index.html index fd8581c..859c41e 100644 --- a/web/index.html +++ b/web/index.html @@ -10,6 +10,7 @@ + @@ -24,6 +25,10 @@ + +
@@ -98,16 +103,6 @@

What is in the catalog

- - - - +{{FOOTER}}