diff --git a/apps/installer/package.json b/apps/installer/package.json index 7e5251d4..aca50a24 100644 --- a/apps/installer/package.json +++ b/apps/installer/package.json @@ -15,6 +15,7 @@ }, "dependencies": { "@hono/node-server": "^1.13.0", + "@profullstack/footer": "^0.2.6", "hono": "^4.6.0" }, "devDependencies": { diff --git a/apps/installer/src/server.ts b/apps/installer/src/server.ts index 39b595bd..22f0b09b 100644 --- a/apps/installer/src/server.ts +++ b/apps/installer/src/server.ts @@ -2,6 +2,7 @@ import { serve } from '@hono/node-server'; import { Hono } from 'hono'; import { cors } from 'hono/cors'; import { logger } from 'hono/logger'; +import { descriptor, escapeHtml, footerHtml } from '@profullstack/footer'; import { existsSync, readFileSync } from 'node:fs'; import { join, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; @@ -107,9 +108,67 @@ app.get('/health', async (c) => { }); }); -// Root - service info +// The site's homepage, as the Profullstack webring and the built.indie / pwamart +// badges know it. +const SITE = `${BASE_URL.replace(/\/+$/, '')}/`; + +// Browsers (and crawlers such as the ring verifier) get an HTML page with the +// install commands and the server-rendered Profullstack footer; scripts and +// `curl` (Accept: */* or application/json) keep getting the JSON service info. +function wantsHtml(accept: string | undefined): boolean { + return /text\/html/i.test(accept ?? ''); +} + +async function landingPage(latestVersion: string): Promise { + const unix = `curl -fsSL ${BASE_URL}/install.sh | bash`; + const windows = `irm ${BASE_URL}/install.ps1 | iex`; + const footer = await footerHtml({ site: SITE, name: 'PairUX Installer' }); + return ` + + + + +Install PairUX + + + + + +
+

Install PairUX

+

Latest release: ${escapeHtml(latestVersion)}. pairux.com

+

macOS and Linux

+
${escapeHtml(unix)}
+

Windows (PowerShell)

+
${escapeHtml(windows)}
+

Downloads

+

All releases on GitHub · /api/releases · /api/version

+
+${footer} + +`; +} + +app.get('/.well-known/openwebring.json', (c) => + c.json(descriptor({ site: SITE, name: 'PairUX Installer' })) +); + +// Root - HTML landing page for browsers, service info JSON for everything else app.get('/', async (c) => { const latestVersion = await getLatestVersion(); + c.header('Vary', 'Accept'); + if (wantsHtml(c.req.header('accept'))) { + return c.html(await landingPage(latestVersion)); + } return c.json({ name: 'PairUX Installer Service', version: getPackageVersion(), diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 84cd5233..ad9ae2d5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -221,6 +221,9 @@ importers: '@hono/node-server': specifier: ^1.19.10 version: 1.19.14(hono@4.12.25) + '@profullstack/footer': + specifier: ^0.2.6 + version: 0.2.6(react@19.2.3) hono: specifier: ^4.12.4 version: 4.12.25 @@ -370,7 +373,7 @@ importers: version: 1.0.3 '@profullstack/footer': specifier: latest - version: 0.2.4(react@19.2.3) + version: 0.2.6(react@19.2.3) '@profullstack/live-translate': specifier: ^0.2.1 version: 0.2.1 @@ -2949,8 +2952,8 @@ packages: '@profullstack/emailer@1.0.3': resolution: {integrity: sha512-VyAtCcfnSI6LOwJvWogCr3ZNmEsaZw+iuWrqnosLsdcKsbRyCrCmXigMNZJZEjlgYlR+lK//PwNPG+xtCFvWAw==} - '@profullstack/footer@0.2.4': - resolution: {integrity: sha512-2ErIzHJCDA2baP8aK6f7rCHohW4EX+wnp7D1mrVMQTUxRcyZwKzmho96hhEFJndXRBGgxT3H/78BHOeBfLxTzg==} + '@profullstack/footer@0.2.6': + resolution: {integrity: sha512-BKwbDszAc7e0mYIBiPjgr/5WS8WyDt8IW718s/zW/8pCWOIB1BGwSNs6fmImRSo5kqIjQKUVbXp22jZQ5GEipA==} engines: {node: '>=18'} hasBin: true peerDependencies: @@ -12117,7 +12120,7 @@ snapshots: '@profullstack/emailer@1.0.3': {} - '@profullstack/footer@0.2.4(react@19.2.3)': + '@profullstack/footer@0.2.6(react@19.2.3)': optionalDependencies: react: 19.2.3