import type { Context } from 'hono'; import { createRouter } from '../../create-router'; import { discoverThemes, readThemeCss } from '../../themes/discover'; /** * Installed themes — the ones in `platform/themes/`, not the ones the platform ships. * * **This prefix is UNAUTHENTICATED** (`UNPROTECTED_API_PREFIXES` in hono.ts) and must stay that way: the * sign-in screen paints in the chosen theme, before any account exists to ask. So nothing per-account can * live here — theme FAVOURITES are on `/theme-favorites`, behind the `account` permission, for exactly that * reason. Adding them here returned 500 on an anonymous request, which was the polite version of the * problem. * * The shipped themes are compiled into the bundle and need no server. These are read off disk at request * time, so dropping a directory in makes a theme appear on the next page load. No install step, no * rebuild, no restart — a theme is CSS custom properties, and none of that needs compiling. * * Read-only on purpose. Installing a theme is `git clone` into a directory, exactly as it is for plugins; * an upload endpoint would be a way to write arbitrary CSS into the page from a request, which is a worse * trade than a shell command. */ export const themesRouter = createRouter(); // GET /themes — what is installed, and what is nearly installed. // // `problems` is returned rather than swallowed: a theme with a typo in its JSON otherwise just fails to // appear, and the author has nowhere to look. The app store can render these. themesRouter.get('/', async (ctx: Context) => { const { themes, problems } = discoverThemes(); return ctx.json({ themes, problems }); }); // GET /themes/:id/theme.css — the stylesheet, injected by the client as a . // // Served as CSS rather than embedded in the JSON above so the browser caches it, and so a theme with a // syntax error breaks only itself rather than the payload that lists every theme. themesRouter.get('/:id/theme.css', async (ctx: Context) => { const css = readThemeCss(ctx.req.param('id')); if (css === null) return ctx.text('/* no such theme */', 404, { 'content-type': 'text/css; charset=utf-8' }); return ctx.body(css, 200, { 'content-type': 'text/css; charset=utf-8', // Short, because editing a theme and reloading is the whole authoring loop. Long enough that a page // load does not refetch fourteen stylesheets. 'cache-control': 'public, max-age=60', }); });