prometheus/service/pages-browse.js

495 lines
20 KiB
JavaScript
Raw Normal View History

/**
* Browse pages — category index and per-listing detail.
*
* Everything here is rendered server-side from the SQLite listings table. No
* client JS, so Googlebot sees the real page, Theseus sees the real page, and
* the s3 snapshot (future milestone #12) is just the same HTML written to disk.
*
* These pages are also where the "0 listings" messaging on the home page and
* /browse flips to real counts — `countsByCategory()` is exported here and
* imported by pages.js so the two render surfaces agree on what is live.
*/
import {
listingsByCategory,
getListing,
countByCategory as countByCategoryDb,
} from './db.js';
import { escapeHtml } from './layout.js';
import { notFound } from './pages.js';
import { softwareApplicationLd } from './seo.js';
prometheus: taxonomy — Apps / Games / FinTech, with sub-chips inside Apps Three top-level groups replace the flat eight categories on the home page and /browse: Apps 7 sub-categories inside: browsers / communication / privacy / devtools / media / productivity / other Games single bucket for now; sub-chips later when there's volume FinTech single bucket for now (BCH wallets, bridges, POS, DeFi) Routes: /t/<slug> top-level page. For Apps shows the 7 sub-chips + an aggregated listings view across them. For Games and FinTech shows the listings directly (no sub-chips). /c/<slug> sub-category page under Apps (browsers, communication, …). The sole-sub categories /c/games and /c/fintech 301 to /t/games and /t/fintech respectively, so each view has exactly one canonical URL. Slug renames + removals in the CATEGORIES allow-list on listings.js: utilities → productivity (the display label was already pointing at that in people's heads — "productivity" fits storage + sync + file tools better) wallets → fintech (broader scope: bridges + POS + DeFi, not just wallets) games stays as a top-level sole-bucket browsers NEW sub-category under Apps for BCNR resolvers, mobile browsers, extensions Seed listings re-categorised + two slug fixes: theseus.x/theseus-navigator-win category: other → browsers ariadne.x/ariadne-resolver-win → RENAMED to ariadne-thread-win, name "Ariadne's Thread (Windows resolver)" shortened to just "Ariadne's Thread"; category: privacy → browsers ariadne.x/ariadne-android → RENAMED to ariadne-navigator, name "Ariadne's Thread (Android)" → "Ariadne Navigator" (it is a different program, not a platform build of Thread); category: privacy → browsers pithos.x/pithos-desktop category: utilities → productivity scripts/migrate-taxonomy.mjs is a one-shot that deletes every row with signature='SM_OPERATOR_SEED'; a subsequent `seed.mjs` run then inserts the fresh set with the new slugs. Publisher-POSTed listings (none in production today) are left alone. Idempotent — a second run finds nothing to remove. New icons: top-apps.svg 2×2 rounded tile grid (OS launcher metaphor) cat-browsers.svg browser window + 3 chrome dots + content block Three icons reused: cat-games.svg doubles as the Games top icon, cat-wallets.svg as the FinTech top icon (same visual, different label); cat-utilities.svg stays as the Productivity sub icon.
2026-10-22 22:39:08 +03:00
/* ------------------------------- taxonomy ------------------------------- */
/**
prometheus: taxonomy — Apps / Games / FinTech, with sub-chips inside Apps Three top-level groups replace the flat eight categories on the home page and /browse: Apps 7 sub-categories inside: browsers / communication / privacy / devtools / media / productivity / other Games single bucket for now; sub-chips later when there's volume FinTech single bucket for now (BCH wallets, bridges, POS, DeFi) Routes: /t/<slug> top-level page. For Apps shows the 7 sub-chips + an aggregated listings view across them. For Games and FinTech shows the listings directly (no sub-chips). /c/<slug> sub-category page under Apps (browsers, communication, …). The sole-sub categories /c/games and /c/fintech 301 to /t/games and /t/fintech respectively, so each view has exactly one canonical URL. Slug renames + removals in the CATEGORIES allow-list on listings.js: utilities → productivity (the display label was already pointing at that in people's heads — "productivity" fits storage + sync + file tools better) wallets → fintech (broader scope: bridges + POS + DeFi, not just wallets) games stays as a top-level sole-bucket browsers NEW sub-category under Apps for BCNR resolvers, mobile browsers, extensions Seed listings re-categorised + two slug fixes: theseus.x/theseus-navigator-win category: other → browsers ariadne.x/ariadne-resolver-win → RENAMED to ariadne-thread-win, name "Ariadne's Thread (Windows resolver)" shortened to just "Ariadne's Thread"; category: privacy → browsers ariadne.x/ariadne-android → RENAMED to ariadne-navigator, name "Ariadne's Thread (Android)" → "Ariadne Navigator" (it is a different program, not a platform build of Thread); category: privacy → browsers pithos.x/pithos-desktop category: utilities → productivity scripts/migrate-taxonomy.mjs is a one-shot that deletes every row with signature='SM_OPERATOR_SEED'; a subsequent `seed.mjs` run then inserts the fresh set with the new slugs. Publisher-POSTed listings (none in production today) are left alone. Idempotent — a second run finds nothing to remove. New icons: top-apps.svg 2×2 rounded tile grid (OS launcher metaphor) cat-browsers.svg browser window + 3 chrome dots + content block Three icons reused: cat-games.svg doubles as the Games top icon, cat-wallets.svg as the FinTech top icon (same visual, different label); cat-utilities.svg stays as the Productivity sub icon.
2026-10-22 22:39:08 +03:00
* Two levels: three top categories (Apps / Games / FinTech) on the home + browse
* tiles, and for Apps, seven sub-categories a reader clicks through to narrow.
* Games and FinTech do not have sub-categories at launch — their top-level
* page IS the listings page. (Sub-chips can be added inside them later when
* there's volume: Wallets / Bridges / POS inside FinTech, genres inside Games.)
*
prometheus: taxonomy — Apps / Games / FinTech, with sub-chips inside Apps Three top-level groups replace the flat eight categories on the home page and /browse: Apps 7 sub-categories inside: browsers / communication / privacy / devtools / media / productivity / other Games single bucket for now; sub-chips later when there's volume FinTech single bucket for now (BCH wallets, bridges, POS, DeFi) Routes: /t/<slug> top-level page. For Apps shows the 7 sub-chips + an aggregated listings view across them. For Games and FinTech shows the listings directly (no sub-chips). /c/<slug> sub-category page under Apps (browsers, communication, …). The sole-sub categories /c/games and /c/fintech 301 to /t/games and /t/fintech respectively, so each view has exactly one canonical URL. Slug renames + removals in the CATEGORIES allow-list on listings.js: utilities → productivity (the display label was already pointing at that in people's heads — "productivity" fits storage + sync + file tools better) wallets → fintech (broader scope: bridges + POS + DeFi, not just wallets) games stays as a top-level sole-bucket browsers NEW sub-category under Apps for BCNR resolvers, mobile browsers, extensions Seed listings re-categorised + two slug fixes: theseus.x/theseus-navigator-win category: other → browsers ariadne.x/ariadne-resolver-win → RENAMED to ariadne-thread-win, name "Ariadne's Thread (Windows resolver)" shortened to just "Ariadne's Thread"; category: privacy → browsers ariadne.x/ariadne-android → RENAMED to ariadne-navigator, name "Ariadne's Thread (Android)" → "Ariadne Navigator" (it is a different program, not a platform build of Thread); category: privacy → browsers pithos.x/pithos-desktop category: utilities → productivity scripts/migrate-taxonomy.mjs is a one-shot that deletes every row with signature='SM_OPERATOR_SEED'; a subsequent `seed.mjs` run then inserts the fresh set with the new slugs. Publisher-POSTed listings (none in production today) are left alone. Idempotent — a second run finds nothing to remove. New icons: top-apps.svg 2×2 rounded tile grid (OS launcher metaphor) cat-browsers.svg browser window + 3 chrome dots + content block Three icons reused: cat-games.svg doubles as the Games top icon, cat-wallets.svg as the FinTech top icon (same visual, different label); cat-utilities.svg stays as the Productivity sub icon.
2026-10-22 22:39:08 +03:00
* `icon` paths point at /static/icons/ SVGs rendered via <img> at the sizes
* the templates need. All icons share one family with the sibling .x marks:
* 12-radius dark square + acid-green mark.
*
* `CATEGORIES` carries the sub-category metadata AND the single-bucket
* categories under Games and FinTech. Every row has a `top` field that places
* it under one of the three top-level groups.
*/
prometheus: taxonomy — Apps / Games / FinTech, with sub-chips inside Apps Three top-level groups replace the flat eight categories on the home page and /browse: Apps 7 sub-categories inside: browsers / communication / privacy / devtools / media / productivity / other Games single bucket for now; sub-chips later when there's volume FinTech single bucket for now (BCH wallets, bridges, POS, DeFi) Routes: /t/<slug> top-level page. For Apps shows the 7 sub-chips + an aggregated listings view across them. For Games and FinTech shows the listings directly (no sub-chips). /c/<slug> sub-category page under Apps (browsers, communication, …). The sole-sub categories /c/games and /c/fintech 301 to /t/games and /t/fintech respectively, so each view has exactly one canonical URL. Slug renames + removals in the CATEGORIES allow-list on listings.js: utilities → productivity (the display label was already pointing at that in people's heads — "productivity" fits storage + sync + file tools better) wallets → fintech (broader scope: bridges + POS + DeFi, not just wallets) games stays as a top-level sole-bucket browsers NEW sub-category under Apps for BCNR resolvers, mobile browsers, extensions Seed listings re-categorised + two slug fixes: theseus.x/theseus-navigator-win category: other → browsers ariadne.x/ariadne-resolver-win → RENAMED to ariadne-thread-win, name "Ariadne's Thread (Windows resolver)" shortened to just "Ariadne's Thread"; category: privacy → browsers ariadne.x/ariadne-android → RENAMED to ariadne-navigator, name "Ariadne's Thread (Android)" → "Ariadne Navigator" (it is a different program, not a platform build of Thread); category: privacy → browsers pithos.x/pithos-desktop category: utilities → productivity scripts/migrate-taxonomy.mjs is a one-shot that deletes every row with signature='SM_OPERATOR_SEED'; a subsequent `seed.mjs` run then inserts the fresh set with the new slugs. Publisher-POSTed listings (none in production today) are left alone. Idempotent — a second run finds nothing to remove. New icons: top-apps.svg 2×2 rounded tile grid (OS launcher metaphor) cat-browsers.svg browser window + 3 chrome dots + content block Three icons reused: cat-games.svg doubles as the Games top icon, cat-wallets.svg as the FinTech top icon (same visual, different label); cat-utilities.svg stays as the Productivity sub icon.
2026-10-22 22:39:08 +03:00
export const TOP_CATEGORIES = [
{ slug: 'apps', icon: '/static/icons/top-apps.svg', title: 'Apps',
blurb: `Everything that isn't a game or a fintech tool. Browsers, messengers, media, developer tools, productivity.` },
{ slug: 'games', icon: '/static/icons/cat-games.svg', title: 'Games',
blurb: `Standalone titles, on-chain leaderboards, in-app purchases that reach the publisher instead of a 30% toll.` },
{ slug: 'fintech', icon: '/static/icons/cat-wallets.svg', title: 'FinTech',
blurb: `BCH wallets, cross-chain bridges, point-of-sale terminals, DeFi primitives. The chain publishes the listing and the chain settles the payment.` },
];
const TOP_BY_SLUG = Object.fromEntries(TOP_CATEGORIES.map((t) => [t.slug, t]));
export const CATEGORIES = [
prometheus: taxonomy — Apps / Games / FinTech, with sub-chips inside Apps Three top-level groups replace the flat eight categories on the home page and /browse: Apps 7 sub-categories inside: browsers / communication / privacy / devtools / media / productivity / other Games single bucket for now; sub-chips later when there's volume FinTech single bucket for now (BCH wallets, bridges, POS, DeFi) Routes: /t/<slug> top-level page. For Apps shows the 7 sub-chips + an aggregated listings view across them. For Games and FinTech shows the listings directly (no sub-chips). /c/<slug> sub-category page under Apps (browsers, communication, …). The sole-sub categories /c/games and /c/fintech 301 to /t/games and /t/fintech respectively, so each view has exactly one canonical URL. Slug renames + removals in the CATEGORIES allow-list on listings.js: utilities → productivity (the display label was already pointing at that in people's heads — "productivity" fits storage + sync + file tools better) wallets → fintech (broader scope: bridges + POS + DeFi, not just wallets) games stays as a top-level sole-bucket browsers NEW sub-category under Apps for BCNR resolvers, mobile browsers, extensions Seed listings re-categorised + two slug fixes: theseus.x/theseus-navigator-win category: other → browsers ariadne.x/ariadne-resolver-win → RENAMED to ariadne-thread-win, name "Ariadne's Thread (Windows resolver)" shortened to just "Ariadne's Thread"; category: privacy → browsers ariadne.x/ariadne-android → RENAMED to ariadne-navigator, name "Ariadne's Thread (Android)" → "Ariadne Navigator" (it is a different program, not a platform build of Thread); category: privacy → browsers pithos.x/pithos-desktop category: utilities → productivity scripts/migrate-taxonomy.mjs is a one-shot that deletes every row with signature='SM_OPERATOR_SEED'; a subsequent `seed.mjs` run then inserts the fresh set with the new slugs. Publisher-POSTed listings (none in production today) are left alone. Idempotent — a second run finds nothing to remove. New icons: top-apps.svg 2×2 rounded tile grid (OS launcher metaphor) cat-browsers.svg browser window + 3 chrome dots + content block Three icons reused: cat-games.svg doubles as the Games top icon, cat-wallets.svg as the FinTech top icon (same visual, different label); cat-utilities.svg stays as the Productivity sub icon.
2026-10-22 22:39:08 +03:00
// ===== Apps sub-categories =====
{ slug: 'browsers', top: 'apps', icon: '/static/icons/cat-browsers.svg', title: 'Browsers & web tools',
blurb: `Web3 browsers and decentralized domain name resolvers. BCNR tooling that teaches Chrome, Firefox and other browsers to reach .x and the other decentralized TLDs.` },
prometheus: taxonomy — Apps / Games / FinTech, with sub-chips inside Apps Three top-level groups replace the flat eight categories on the home page and /browse: Apps 7 sub-categories inside: browsers / communication / privacy / devtools / media / productivity / other Games single bucket for now; sub-chips later when there's volume FinTech single bucket for now (BCH wallets, bridges, POS, DeFi) Routes: /t/<slug> top-level page. For Apps shows the 7 sub-chips + an aggregated listings view across them. For Games and FinTech shows the listings directly (no sub-chips). /c/<slug> sub-category page under Apps (browsers, communication, …). The sole-sub categories /c/games and /c/fintech 301 to /t/games and /t/fintech respectively, so each view has exactly one canonical URL. Slug renames + removals in the CATEGORIES allow-list on listings.js: utilities → productivity (the display label was already pointing at that in people's heads — "productivity" fits storage + sync + file tools better) wallets → fintech (broader scope: bridges + POS + DeFi, not just wallets) games stays as a top-level sole-bucket browsers NEW sub-category under Apps for BCNR resolvers, mobile browsers, extensions Seed listings re-categorised + two slug fixes: theseus.x/theseus-navigator-win category: other → browsers ariadne.x/ariadne-resolver-win → RENAMED to ariadne-thread-win, name "Ariadne's Thread (Windows resolver)" shortened to just "Ariadne's Thread"; category: privacy → browsers ariadne.x/ariadne-android → RENAMED to ariadne-navigator, name "Ariadne's Thread (Android)" → "Ariadne Navigator" (it is a different program, not a platform build of Thread); category: privacy → browsers pithos.x/pithos-desktop category: utilities → productivity scripts/migrate-taxonomy.mjs is a one-shot that deletes every row with signature='SM_OPERATOR_SEED'; a subsequent `seed.mjs` run then inserts the fresh set with the new slugs. Publisher-POSTed listings (none in production today) are left alone. Idempotent — a second run finds nothing to remove. New icons: top-apps.svg 2×2 rounded tile grid (OS launcher metaphor) cat-browsers.svg browser window + 3 chrome dots + content block Three icons reused: cat-games.svg doubles as the Games top icon, cat-wallets.svg as the FinTech top icon (same visual, different label); cat-utilities.svg stays as the Productivity sub icon.
2026-10-22 22:39:08 +03:00
{ slug: 'communication', top: 'apps', icon: '/static/icons/cat-communication.svg', title: 'Communication',
blurb: `Messengers, voice, boards, group chats. Clients that route through peers rather than someone else's server.` },
prometheus: taxonomy — Apps / Games / FinTech, with sub-chips inside Apps Three top-level groups replace the flat eight categories on the home page and /browse: Apps 7 sub-categories inside: browsers / communication / privacy / devtools / media / productivity / other Games single bucket for now; sub-chips later when there's volume FinTech single bucket for now (BCH wallets, bridges, POS, DeFi) Routes: /t/<slug> top-level page. For Apps shows the 7 sub-chips + an aggregated listings view across them. For Games and FinTech shows the listings directly (no sub-chips). /c/<slug> sub-category page under Apps (browsers, communication, …). The sole-sub categories /c/games and /c/fintech 301 to /t/games and /t/fintech respectively, so each view has exactly one canonical URL. Slug renames + removals in the CATEGORIES allow-list on listings.js: utilities → productivity (the display label was already pointing at that in people's heads — "productivity" fits storage + sync + file tools better) wallets → fintech (broader scope: bridges + POS + DeFi, not just wallets) games stays as a top-level sole-bucket browsers NEW sub-category under Apps for BCNR resolvers, mobile browsers, extensions Seed listings re-categorised + two slug fixes: theseus.x/theseus-navigator-win category: other → browsers ariadne.x/ariadne-resolver-win → RENAMED to ariadne-thread-win, name "Ariadne's Thread (Windows resolver)" shortened to just "Ariadne's Thread"; category: privacy → browsers ariadne.x/ariadne-android → RENAMED to ariadne-navigator, name "Ariadne's Thread (Android)" → "Ariadne Navigator" (it is a different program, not a platform build of Thread); category: privacy → browsers pithos.x/pithos-desktop category: utilities → productivity scripts/migrate-taxonomy.mjs is a one-shot that deletes every row with signature='SM_OPERATOR_SEED'; a subsequent `seed.mjs` run then inserts the fresh set with the new slugs. Publisher-POSTed listings (none in production today) are left alone. Idempotent — a second run finds nothing to remove. New icons: top-apps.svg 2×2 rounded tile grid (OS launcher metaphor) cat-browsers.svg browser window + 3 chrome dots + content block Three icons reused: cat-games.svg doubles as the Games top icon, cat-wallets.svg as the FinTech top icon (same visual, different label); cat-utilities.svg stays as the Productivity sub icon.
2026-10-22 22:39:08 +03:00
{ slug: 'privacy', top: 'apps', icon: '/static/icons/cat-privacy.svg', title: 'Privacy & security',
blurb: `Tor and mix-net tooling, key and vault managers, disk crypto. The things a conventional store delists first when a government writes.` },
prometheus: taxonomy — Apps / Games / FinTech, with sub-chips inside Apps Three top-level groups replace the flat eight categories on the home page and /browse: Apps 7 sub-categories inside: browsers / communication / privacy / devtools / media / productivity / other Games single bucket for now; sub-chips later when there's volume FinTech single bucket for now (BCH wallets, bridges, POS, DeFi) Routes: /t/<slug> top-level page. For Apps shows the 7 sub-chips + an aggregated listings view across them. For Games and FinTech shows the listings directly (no sub-chips). /c/<slug> sub-category page under Apps (browsers, communication, …). The sole-sub categories /c/games and /c/fintech 301 to /t/games and /t/fintech respectively, so each view has exactly one canonical URL. Slug renames + removals in the CATEGORIES allow-list on listings.js: utilities → productivity (the display label was already pointing at that in people's heads — "productivity" fits storage + sync + file tools better) wallets → fintech (broader scope: bridges + POS + DeFi, not just wallets) games stays as a top-level sole-bucket browsers NEW sub-category under Apps for BCNR resolvers, mobile browsers, extensions Seed listings re-categorised + two slug fixes: theseus.x/theseus-navigator-win category: other → browsers ariadne.x/ariadne-resolver-win → RENAMED to ariadne-thread-win, name "Ariadne's Thread (Windows resolver)" shortened to just "Ariadne's Thread"; category: privacy → browsers ariadne.x/ariadne-android → RENAMED to ariadne-navigator, name "Ariadne's Thread (Android)" → "Ariadne Navigator" (it is a different program, not a platform build of Thread); category: privacy → browsers pithos.x/pithos-desktop category: utilities → productivity scripts/migrate-taxonomy.mjs is a one-shot that deletes every row with signature='SM_OPERATOR_SEED'; a subsequent `seed.mjs` run then inserts the fresh set with the new slugs. Publisher-POSTed listings (none in production today) are left alone. Idempotent — a second run finds nothing to remove. New icons: top-apps.svg 2×2 rounded tile grid (OS launcher metaphor) cat-browsers.svg browser window + 3 chrome dots + content block Three icons reused: cat-games.svg doubles as the Games top icon, cat-wallets.svg as the FinTech top icon (same visual, different label); cat-utilities.svg stays as the Productivity sub icon.
2026-10-22 22:39:08 +03:00
{ slug: 'devtools', top: 'apps', icon: '/static/icons/cat-devtools.svg', title: 'Developer tools',
blurb: `SDKs, libraries, indexers, faucets, protocol utilities. The pieces other apps on this list are built out of.` },
prometheus: taxonomy — Apps / Games / FinTech, with sub-chips inside Apps Three top-level groups replace the flat eight categories on the home page and /browse: Apps 7 sub-categories inside: browsers / communication / privacy / devtools / media / productivity / other Games single bucket for now; sub-chips later when there's volume FinTech single bucket for now (BCH wallets, bridges, POS, DeFi) Routes: /t/<slug> top-level page. For Apps shows the 7 sub-chips + an aggregated listings view across them. For Games and FinTech shows the listings directly (no sub-chips). /c/<slug> sub-category page under Apps (browsers, communication, …). The sole-sub categories /c/games and /c/fintech 301 to /t/games and /t/fintech respectively, so each view has exactly one canonical URL. Slug renames + removals in the CATEGORIES allow-list on listings.js: utilities → productivity (the display label was already pointing at that in people's heads — "productivity" fits storage + sync + file tools better) wallets → fintech (broader scope: bridges + POS + DeFi, not just wallets) games stays as a top-level sole-bucket browsers NEW sub-category under Apps for BCNR resolvers, mobile browsers, extensions Seed listings re-categorised + two slug fixes: theseus.x/theseus-navigator-win category: other → browsers ariadne.x/ariadne-resolver-win → RENAMED to ariadne-thread-win, name "Ariadne's Thread (Windows resolver)" shortened to just "Ariadne's Thread"; category: privacy → browsers ariadne.x/ariadne-android → RENAMED to ariadne-navigator, name "Ariadne's Thread (Android)" → "Ariadne Navigator" (it is a different program, not a platform build of Thread); category: privacy → browsers pithos.x/pithos-desktop category: utilities → productivity scripts/migrate-taxonomy.mjs is a one-shot that deletes every row with signature='SM_OPERATOR_SEED'; a subsequent `seed.mjs` run then inserts the fresh set with the new slugs. Publisher-POSTed listings (none in production today) are left alone. Idempotent — a second run finds nothing to remove. New icons: top-apps.svg 2×2 rounded tile grid (OS launcher metaphor) cat-browsers.svg browser window + 3 chrome dots + content block Three icons reused: cat-games.svg doubles as the Games top icon, cat-wallets.svg as the FinTech top icon (same visual, different label); cat-utilities.svg stays as the Productivity sub icon.
2026-10-22 22:39:08 +03:00
{ slug: 'media', top: 'apps', icon: '/static/icons/cat-media.svg', title: 'Media & publishing',
blurb: `Readers, players, one-file blogs, podcast hosts. Clients that find their audience without going through an aggregator.` },
prometheus: taxonomy — Apps / Games / FinTech, with sub-chips inside Apps Three top-level groups replace the flat eight categories on the home page and /browse: Apps 7 sub-categories inside: browsers / communication / privacy / devtools / media / productivity / other Games single bucket for now; sub-chips later when there's volume FinTech single bucket for now (BCH wallets, bridges, POS, DeFi) Routes: /t/<slug> top-level page. For Apps shows the 7 sub-chips + an aggregated listings view across them. For Games and FinTech shows the listings directly (no sub-chips). /c/<slug> sub-category page under Apps (browsers, communication, …). The sole-sub categories /c/games and /c/fintech 301 to /t/games and /t/fintech respectively, so each view has exactly one canonical URL. Slug renames + removals in the CATEGORIES allow-list on listings.js: utilities → productivity (the display label was already pointing at that in people's heads — "productivity" fits storage + sync + file tools better) wallets → fintech (broader scope: bridges + POS + DeFi, not just wallets) games stays as a top-level sole-bucket browsers NEW sub-category under Apps for BCNR resolvers, mobile browsers, extensions Seed listings re-categorised + two slug fixes: theseus.x/theseus-navigator-win category: other → browsers ariadne.x/ariadne-resolver-win → RENAMED to ariadne-thread-win, name "Ariadne's Thread (Windows resolver)" shortened to just "Ariadne's Thread"; category: privacy → browsers ariadne.x/ariadne-android → RENAMED to ariadne-navigator, name "Ariadne's Thread (Android)" → "Ariadne Navigator" (it is a different program, not a platform build of Thread); category: privacy → browsers pithos.x/pithos-desktop category: utilities → productivity scripts/migrate-taxonomy.mjs is a one-shot that deletes every row with signature='SM_OPERATOR_SEED'; a subsequent `seed.mjs` run then inserts the fresh set with the new slugs. Publisher-POSTed listings (none in production today) are left alone. Idempotent — a second run finds nothing to remove. New icons: top-apps.svg 2×2 rounded tile grid (OS launcher metaphor) cat-browsers.svg browser window + 3 chrome dots + content block Three icons reused: cat-games.svg doubles as the Games top icon, cat-wallets.svg as the FinTech top icon (same visual, different label); cat-utilities.svg stays as the Productivity sub icon.
2026-10-22 22:39:08 +03:00
{ slug: 'productivity', top: 'apps', icon: '/static/icons/cat-utilities.svg', title: 'Productivity',
blurb: `File tools, storage, format converters, backup, calculators. The small sharp things that solve one problem and do not ask for an account first.` },
{ slug: 'other', top: 'apps', icon: '/static/icons/cat-other.svg', title: 'Other',
blurb: `Apps that don't fit the categories above. A new category can be proposed by publishing against it and asking an operator to promote it.` },
// ===== Games (single bucket for now) =====
{ slug: 'games', top: 'games', icon: '/static/icons/cat-games.svg', title: 'Games',
blurb: TOP_BY_SLUG.games?.blurb || '' },
// ===== FinTech (single bucket for now) =====
{ slug: 'fintech', top: 'fintech', icon: '/static/icons/cat-wallets.svg', title: 'FinTech',
blurb: TOP_BY_SLUG.fintech?.blurb || '' },
];
const CATEGORY_BY_SLUG = Object.fromEntries(CATEGORIES.map((c) => [c.slug, c]));
prometheus: taxonomy — Apps / Games / FinTech, with sub-chips inside Apps Three top-level groups replace the flat eight categories on the home page and /browse: Apps 7 sub-categories inside: browsers / communication / privacy / devtools / media / productivity / other Games single bucket for now; sub-chips later when there's volume FinTech single bucket for now (BCH wallets, bridges, POS, DeFi) Routes: /t/<slug> top-level page. For Apps shows the 7 sub-chips + an aggregated listings view across them. For Games and FinTech shows the listings directly (no sub-chips). /c/<slug> sub-category page under Apps (browsers, communication, …). The sole-sub categories /c/games and /c/fintech 301 to /t/games and /t/fintech respectively, so each view has exactly one canonical URL. Slug renames + removals in the CATEGORIES allow-list on listings.js: utilities → productivity (the display label was already pointing at that in people's heads — "productivity" fits storage + sync + file tools better) wallets → fintech (broader scope: bridges + POS + DeFi, not just wallets) games stays as a top-level sole-bucket browsers NEW sub-category under Apps for BCNR resolvers, mobile browsers, extensions Seed listings re-categorised + two slug fixes: theseus.x/theseus-navigator-win category: other → browsers ariadne.x/ariadne-resolver-win → RENAMED to ariadne-thread-win, name "Ariadne's Thread (Windows resolver)" shortened to just "Ariadne's Thread"; category: privacy → browsers ariadne.x/ariadne-android → RENAMED to ariadne-navigator, name "Ariadne's Thread (Android)" → "Ariadne Navigator" (it is a different program, not a platform build of Thread); category: privacy → browsers pithos.x/pithos-desktop category: utilities → productivity scripts/migrate-taxonomy.mjs is a one-shot that deletes every row with signature='SM_OPERATOR_SEED'; a subsequent `seed.mjs` run then inserts the fresh set with the new slugs. Publisher-POSTed listings (none in production today) are left alone. Idempotent — a second run finds nothing to remove. New icons: top-apps.svg 2×2 rounded tile grid (OS launcher metaphor) cat-browsers.svg browser window + 3 chrome dots + content block Three icons reused: cat-games.svg doubles as the Games top icon, cat-wallets.svg as the FinTech top icon (same visual, different label); cat-utilities.svg stays as the Productivity sub icon.
2026-10-22 22:39:08 +03:00
const CATEGORIES_BY_TOP = Object.fromEntries(
TOP_CATEGORIES.map((t) => [t.slug, CATEGORIES.filter((c) => c.top === t.slug)])
);
/**
prometheus: taxonomy — Apps / Games / FinTech, with sub-chips inside Apps Three top-level groups replace the flat eight categories on the home page and /browse: Apps 7 sub-categories inside: browsers / communication / privacy / devtools / media / productivity / other Games single bucket for now; sub-chips later when there's volume FinTech single bucket for now (BCH wallets, bridges, POS, DeFi) Routes: /t/<slug> top-level page. For Apps shows the 7 sub-chips + an aggregated listings view across them. For Games and FinTech shows the listings directly (no sub-chips). /c/<slug> sub-category page under Apps (browsers, communication, …). The sole-sub categories /c/games and /c/fintech 301 to /t/games and /t/fintech respectively, so each view has exactly one canonical URL. Slug renames + removals in the CATEGORIES allow-list on listings.js: utilities → productivity (the display label was already pointing at that in people's heads — "productivity" fits storage + sync + file tools better) wallets → fintech (broader scope: bridges + POS + DeFi, not just wallets) games stays as a top-level sole-bucket browsers NEW sub-category under Apps for BCNR resolvers, mobile browsers, extensions Seed listings re-categorised + two slug fixes: theseus.x/theseus-navigator-win category: other → browsers ariadne.x/ariadne-resolver-win → RENAMED to ariadne-thread-win, name "Ariadne's Thread (Windows resolver)" shortened to just "Ariadne's Thread"; category: privacy → browsers ariadne.x/ariadne-android → RENAMED to ariadne-navigator, name "Ariadne's Thread (Android)" → "Ariadne Navigator" (it is a different program, not a platform build of Thread); category: privacy → browsers pithos.x/pithos-desktop category: utilities → productivity scripts/migrate-taxonomy.mjs is a one-shot that deletes every row with signature='SM_OPERATOR_SEED'; a subsequent `seed.mjs` run then inserts the fresh set with the new slugs. Publisher-POSTed listings (none in production today) are left alone. Idempotent — a second run finds nothing to remove. New icons: top-apps.svg 2×2 rounded tile grid (OS launcher metaphor) cat-browsers.svg browser window + 3 chrome dots + content block Three icons reused: cat-games.svg doubles as the Games top icon, cat-wallets.svg as the FinTech top icon (same visual, different label); cat-utilities.svg stays as the Productivity sub icon.
2026-10-22 22:39:08 +03:00
* {slug: count} for every sub-category — categories with 0 listings are present
* with the value 0, so templates can read them unconditionally.
*/
export function countsByCategory() {
const raw = countByCategoryDb();
const out = {};
for (const c of CATEGORIES) out[c.slug] = raw[c.slug] || 0;
return out;
}
/**
prometheus: taxonomy — Apps / Games / FinTech, with sub-chips inside Apps Three top-level groups replace the flat eight categories on the home page and /browse: Apps 7 sub-categories inside: browsers / communication / privacy / devtools / media / productivity / other Games single bucket for now; sub-chips later when there's volume FinTech single bucket for now (BCH wallets, bridges, POS, DeFi) Routes: /t/<slug> top-level page. For Apps shows the 7 sub-chips + an aggregated listings view across them. For Games and FinTech shows the listings directly (no sub-chips). /c/<slug> sub-category page under Apps (browsers, communication, …). The sole-sub categories /c/games and /c/fintech 301 to /t/games and /t/fintech respectively, so each view has exactly one canonical URL. Slug renames + removals in the CATEGORIES allow-list on listings.js: utilities → productivity (the display label was already pointing at that in people's heads — "productivity" fits storage + sync + file tools better) wallets → fintech (broader scope: bridges + POS + DeFi, not just wallets) games stays as a top-level sole-bucket browsers NEW sub-category under Apps for BCNR resolvers, mobile browsers, extensions Seed listings re-categorised + two slug fixes: theseus.x/theseus-navigator-win category: other → browsers ariadne.x/ariadne-resolver-win → RENAMED to ariadne-thread-win, name "Ariadne's Thread (Windows resolver)" shortened to just "Ariadne's Thread"; category: privacy → browsers ariadne.x/ariadne-android → RENAMED to ariadne-navigator, name "Ariadne's Thread (Android)" → "Ariadne Navigator" (it is a different program, not a platform build of Thread); category: privacy → browsers pithos.x/pithos-desktop category: utilities → productivity scripts/migrate-taxonomy.mjs is a one-shot that deletes every row with signature='SM_OPERATOR_SEED'; a subsequent `seed.mjs` run then inserts the fresh set with the new slugs. Publisher-POSTed listings (none in production today) are left alone. Idempotent — a second run finds nothing to remove. New icons: top-apps.svg 2×2 rounded tile grid (OS launcher metaphor) cat-browsers.svg browser window + 3 chrome dots + content block Three icons reused: cat-games.svg doubles as the Games top icon, cat-wallets.svg as the FinTech top icon (same visual, different label); cat-utilities.svg stays as the Productivity sub icon.
2026-10-22 22:39:08 +03:00
* {slug: count} for every top-level group — sums its subs' counts.
*/
export function countsByTop() {
const subs = countsByCategory();
const out = {};
for (const t of TOP_CATEGORIES) {
out[t.slug] = CATEGORIES_BY_TOP[t.slug].reduce((n, c) => n + subs[c.slug], 0);
}
return out;
}
/**
* The three big tiles — the primary navigation into the directory.
* Used on the home page and on /browse.
*/
export function renderTopGrid({ verbose = true } = {}) {
const counts = countsByTop();
return `<div class="categories">
${TOP_CATEGORIES.map((t) => {
const n = counts[t.slug];
const emptyCls = n === 0 ? ' empty' : '';
const countLabel = `${n} ${n === 1 ? 'listing' : 'listings'}`;
return `<a class="cat${emptyCls}" href="/t/${t.slug}">
<img class="ico" src="${t.icon}" alt="" width="40" height="40">
<h3>${escapeHtml(t.title)}</h3>
${verbose ? `<p>${escapeHtml(t.blurb)}</p>` : ''}
<span class="count">${countLabel}</span>
</a>`;
}).join('\n ')}
</div>`;
}
/**
* Secondary grid of the sub-categories under one top. Only used on /t/apps
* (Games and FinTech are single-bucket so their top page is the listings).
*/
prometheus: taxonomy — Apps / Games / FinTech, with sub-chips inside Apps Three top-level groups replace the flat eight categories on the home page and /browse: Apps 7 sub-categories inside: browsers / communication / privacy / devtools / media / productivity / other Games single bucket for now; sub-chips later when there's volume FinTech single bucket for now (BCH wallets, bridges, POS, DeFi) Routes: /t/<slug> top-level page. For Apps shows the 7 sub-chips + an aggregated listings view across them. For Games and FinTech shows the listings directly (no sub-chips). /c/<slug> sub-category page under Apps (browsers, communication, …). The sole-sub categories /c/games and /c/fintech 301 to /t/games and /t/fintech respectively, so each view has exactly one canonical URL. Slug renames + removals in the CATEGORIES allow-list on listings.js: utilities → productivity (the display label was already pointing at that in people's heads — "productivity" fits storage + sync + file tools better) wallets → fintech (broader scope: bridges + POS + DeFi, not just wallets) games stays as a top-level sole-bucket browsers NEW sub-category under Apps for BCNR resolvers, mobile browsers, extensions Seed listings re-categorised + two slug fixes: theseus.x/theseus-navigator-win category: other → browsers ariadne.x/ariadne-resolver-win → RENAMED to ariadne-thread-win, name "Ariadne's Thread (Windows resolver)" shortened to just "Ariadne's Thread"; category: privacy → browsers ariadne.x/ariadne-android → RENAMED to ariadne-navigator, name "Ariadne's Thread (Android)" → "Ariadne Navigator" (it is a different program, not a platform build of Thread); category: privacy → browsers pithos.x/pithos-desktop category: utilities → productivity scripts/migrate-taxonomy.mjs is a one-shot that deletes every row with signature='SM_OPERATOR_SEED'; a subsequent `seed.mjs` run then inserts the fresh set with the new slugs. Publisher-POSTed listings (none in production today) are left alone. Idempotent — a second run finds nothing to remove. New icons: top-apps.svg 2×2 rounded tile grid (OS launcher metaphor) cat-browsers.svg browser window + 3 chrome dots + content block Three icons reused: cat-games.svg doubles as the Games top icon, cat-wallets.svg as the FinTech top icon (same visual, different label); cat-utilities.svg stays as the Productivity sub icon.
2026-10-22 22:39:08 +03:00
export function renderSubGrid(topSlug) {
const subs = CATEGORIES_BY_TOP[topSlug] || [];
if (subs.length < 2) return ''; // nothing worth showing for a single-sub top
const counts = countsByCategory();
return `<div class="categories">
prometheus: taxonomy — Apps / Games / FinTech, with sub-chips inside Apps Three top-level groups replace the flat eight categories on the home page and /browse: Apps 7 sub-categories inside: browsers / communication / privacy / devtools / media / productivity / other Games single bucket for now; sub-chips later when there's volume FinTech single bucket for now (BCH wallets, bridges, POS, DeFi) Routes: /t/<slug> top-level page. For Apps shows the 7 sub-chips + an aggregated listings view across them. For Games and FinTech shows the listings directly (no sub-chips). /c/<slug> sub-category page under Apps (browsers, communication, …). The sole-sub categories /c/games and /c/fintech 301 to /t/games and /t/fintech respectively, so each view has exactly one canonical URL. Slug renames + removals in the CATEGORIES allow-list on listings.js: utilities → productivity (the display label was already pointing at that in people's heads — "productivity" fits storage + sync + file tools better) wallets → fintech (broader scope: bridges + POS + DeFi, not just wallets) games stays as a top-level sole-bucket browsers NEW sub-category under Apps for BCNR resolvers, mobile browsers, extensions Seed listings re-categorised + two slug fixes: theseus.x/theseus-navigator-win category: other → browsers ariadne.x/ariadne-resolver-win → RENAMED to ariadne-thread-win, name "Ariadne's Thread (Windows resolver)" shortened to just "Ariadne's Thread"; category: privacy → browsers ariadne.x/ariadne-android → RENAMED to ariadne-navigator, name "Ariadne's Thread (Android)" → "Ariadne Navigator" (it is a different program, not a platform build of Thread); category: privacy → browsers pithos.x/pithos-desktop category: utilities → productivity scripts/migrate-taxonomy.mjs is a one-shot that deletes every row with signature='SM_OPERATOR_SEED'; a subsequent `seed.mjs` run then inserts the fresh set with the new slugs. Publisher-POSTed listings (none in production today) are left alone. Idempotent — a second run finds nothing to remove. New icons: top-apps.svg 2×2 rounded tile grid (OS launcher metaphor) cat-browsers.svg browser window + 3 chrome dots + content block Three icons reused: cat-games.svg doubles as the Games top icon, cat-wallets.svg as the FinTech top icon (same visual, different label); cat-utilities.svg stays as the Productivity sub icon.
2026-10-22 22:39:08 +03:00
${subs.map((c) => {
const n = counts[c.slug];
const emptyCls = n === 0 ? ' empty' : '';
const countLabel = `${n} ${n === 1 ? 'listing' : 'listings'}`;
return `<a class="cat${emptyCls}" href="/c/${c.slug}">
prometheus: SVG icons for the 8 categories Replaces the emoji glyphs (💬🛡️💸🎮🧰🎧⚙️✨) in the category grid with dedicated SVG icons under /static/icons/cat-<slug>.svg, following the same visual family as the sibling .x marks (theseus, sirius, ariadne, pithos, aegis): 12-radius dark square + acid-green mark, legible at 16 px tab-icon size and 40 px grid size. Designs chosen to not blur with the Silent Mode product icons also rendered on the site: cat-communication speech bubble + three dots (not an envelope — too close to the Hephaestus letter imagery elsewhere) cat-privacy shield + checkmark (DISTINCT from aegis.svg's shield+chevron, so category + listing don't collide) cat-wallets stacked coins + B serif (hints at the BCH coin) cat-games stylised controller (d-pad + two action buttons) cat-devtools code brackets < / > (reads as "code" at 16 px where a wrench/hammer blurs with the Utilities gear) cat-media play triangle in a circle (equally "player" and "publish/broadcast") cat-utilities gear with hollow centre (less visual weight than the Media circle; the two side-by-side don't crowd each other) cat-other sparkle burst + two smaller sparkles (sibling of sirius.svg's 4-point mark, extras so the two don't look identical) pages-browse.js: CATEGORIES[].icon is now the path to the SVG, not an emoji string. The three places the icon rendered — grid card, category hero, and the inline chip on listing detail — all now use <img> at the size they need (40, 56, 14 respectively). The CSS entry `.cat .ico` loses the font-size + grayscale (text styling) and gains width/height for the image plus a tiny hover rotation that matches the existing lift transform. Also: when a listing has no `icon` of its own, the detail page's icon slot now falls back to the category's SVG (centered + padded) instead of a plain 📦 emoji, so the detail hero always has a vector icon.
2026-10-22 21:30:13 +03:00
<img class="ico" src="${c.icon}" alt="" width="40" height="40">
<h3>${escapeHtml(c.title)}</h3>
prometheus: taxonomy — Apps / Games / FinTech, with sub-chips inside Apps Three top-level groups replace the flat eight categories on the home page and /browse: Apps 7 sub-categories inside: browsers / communication / privacy / devtools / media / productivity / other Games single bucket for now; sub-chips later when there's volume FinTech single bucket for now (BCH wallets, bridges, POS, DeFi) Routes: /t/<slug> top-level page. For Apps shows the 7 sub-chips + an aggregated listings view across them. For Games and FinTech shows the listings directly (no sub-chips). /c/<slug> sub-category page under Apps (browsers, communication, …). The sole-sub categories /c/games and /c/fintech 301 to /t/games and /t/fintech respectively, so each view has exactly one canonical URL. Slug renames + removals in the CATEGORIES allow-list on listings.js: utilities → productivity (the display label was already pointing at that in people's heads — "productivity" fits storage + sync + file tools better) wallets → fintech (broader scope: bridges + POS + DeFi, not just wallets) games stays as a top-level sole-bucket browsers NEW sub-category under Apps for BCNR resolvers, mobile browsers, extensions Seed listings re-categorised + two slug fixes: theseus.x/theseus-navigator-win category: other → browsers ariadne.x/ariadne-resolver-win → RENAMED to ariadne-thread-win, name "Ariadne's Thread (Windows resolver)" shortened to just "Ariadne's Thread"; category: privacy → browsers ariadne.x/ariadne-android → RENAMED to ariadne-navigator, name "Ariadne's Thread (Android)" → "Ariadne Navigator" (it is a different program, not a platform build of Thread); category: privacy → browsers pithos.x/pithos-desktop category: utilities → productivity scripts/migrate-taxonomy.mjs is a one-shot that deletes every row with signature='SM_OPERATOR_SEED'; a subsequent `seed.mjs` run then inserts the fresh set with the new slugs. Publisher-POSTed listings (none in production today) are left alone. Idempotent — a second run finds nothing to remove. New icons: top-apps.svg 2×2 rounded tile grid (OS launcher metaphor) cat-browsers.svg browser window + 3 chrome dots + content block Three icons reused: cat-games.svg doubles as the Games top icon, cat-wallets.svg as the FinTech top icon (same visual, different label); cat-utilities.svg stays as the Productivity sub icon.
2026-10-22 22:39:08 +03:00
<p>${escapeHtml(c.blurb)}</p>
<span class="count">${countLabel}</span>
</a>`;
}).join('\n ')}
</div>`;
}
prometheus: taxonomy — Apps / Games / FinTech, with sub-chips inside Apps Three top-level groups replace the flat eight categories on the home page and /browse: Apps 7 sub-categories inside: browsers / communication / privacy / devtools / media / productivity / other Games single bucket for now; sub-chips later when there's volume FinTech single bucket for now (BCH wallets, bridges, POS, DeFi) Routes: /t/<slug> top-level page. For Apps shows the 7 sub-chips + an aggregated listings view across them. For Games and FinTech shows the listings directly (no sub-chips). /c/<slug> sub-category page under Apps (browsers, communication, …). The sole-sub categories /c/games and /c/fintech 301 to /t/games and /t/fintech respectively, so each view has exactly one canonical URL. Slug renames + removals in the CATEGORIES allow-list on listings.js: utilities → productivity (the display label was already pointing at that in people's heads — "productivity" fits storage + sync + file tools better) wallets → fintech (broader scope: bridges + POS + DeFi, not just wallets) games stays as a top-level sole-bucket browsers NEW sub-category under Apps for BCNR resolvers, mobile browsers, extensions Seed listings re-categorised + two slug fixes: theseus.x/theseus-navigator-win category: other → browsers ariadne.x/ariadne-resolver-win → RENAMED to ariadne-thread-win, name "Ariadne's Thread (Windows resolver)" shortened to just "Ariadne's Thread"; category: privacy → browsers ariadne.x/ariadne-android → RENAMED to ariadne-navigator, name "Ariadne's Thread (Android)" → "Ariadne Navigator" (it is a different program, not a platform build of Thread); category: privacy → browsers pithos.x/pithos-desktop category: utilities → productivity scripts/migrate-taxonomy.mjs is a one-shot that deletes every row with signature='SM_OPERATOR_SEED'; a subsequent `seed.mjs` run then inserts the fresh set with the new slugs. Publisher-POSTed listings (none in production today) are left alone. Idempotent — a second run finds nothing to remove. New icons: top-apps.svg 2×2 rounded tile grid (OS launcher metaphor) cat-browsers.svg browser window + 3 chrome dots + content block Three icons reused: cat-games.svg doubles as the Games top icon, cat-wallets.svg as the FinTech top icon (same visual, different label); cat-utilities.svg stays as the Productivity sub icon.
2026-10-22 22:39:08 +03:00
/**
* Kept as an alias so pages.js's older call site still works; home page now
* shows the top grid instead of the sub grid. Removing this would be a
* minor churn in pages.js that landed in the previous commit.
*/
export const renderCategoriesGrid = renderTopGrid;
/* ------------------------------- top-category page ------------------------------- */
export function topCategoryPage(req, slug) {
const top = TOP_BY_SLUG[slug];
if (!top) return notFound(req);
const subs = CATEGORIES_BY_TOP[slug] || [];
const subSlugs = subs.map((c) => c.slug);
// Aggregate across all subs. For single-bucket tops (Games, FinTech)
// this is just one slug; for Apps it's seven.
const rows = [];
for (const s of subSlugs) rows.push(...listingsByCategory(s));
rows.sort((a, b) => (b.created_at || '').localeCompare(a.created_at || ''));
const title = top.title;
const here = `/t/${slug}`;
const hasSubs = subs.length > 1;
return {
title,
description: `${top.title} on Prometheus — ${top.blurb}`,
here,
body: `
<header class="hero compact">
<img class="mark" src="${top.icon}" alt="" width="64" height="64" style="display:block;margin:0 auto 1rem">
<h1>${escapeHtml(title)}</h1>
<p class="tag">${escapeHtml(top.blurb)}</p>
</header>
<div class="wrap">
${hasSubs ? `
<section class="plain">
<h2>Narrow by sub-category</h2>
${renderSubGrid(slug)}
</section>
<section>
<h2>All ${escapeHtml(title.toLowerCase())}</h2>
<p class="lede">
${rows.length === 0
? `No listings in ${escapeHtml(title)} yet. <a href="/submit">Be the first</a>.`
: `${rows.length} ${rows.length === 1 ? 'listing' : 'listings'} across ${subs.filter((c) => countsByCategory()[c.slug] > 0).length} sub-categories, newest first.`}
</p>
${rows.length === 0 ? '' : renderListingList(rows)}
</section>
` : `
<section class="plain">
<p class="lede">
${rows.length === 0
? `No listings in ${escapeHtml(title)} yet. <a href="/submit">Be the first</a>.`
: `${rows.length} ${rows.length === 1 ? 'listing' : 'listings'}, newest first.`}
</p>
${rows.length === 0 ? emptyTop(top) : renderListingList(rows)}
</section>
`}
<p style="margin-top:1.8rem;color:var(--dim);font-size:13px">
<a href="/browse">← All categories</a>
</p>
</div>`,
};
}
function emptyTop(top) {
return `<div class="note">
<b>Nothing here yet.</b> If you ship ${escapeHtml(top.title.toLowerCase())},
the <a href="/submit">submit flow</a> is live — upload, scan, sign, your listing
appears in this view.
</div>`;
}
/* ------------------------------- category page ------------------------------- */
export function categoryPage(req, slug) {
const cat = CATEGORY_BY_SLUG[slug];
if (!cat) return notFound(req);
prometheus: taxonomy — Apps / Games / FinTech, with sub-chips inside Apps Three top-level groups replace the flat eight categories on the home page and /browse: Apps 7 sub-categories inside: browsers / communication / privacy / devtools / media / productivity / other Games single bucket for now; sub-chips later when there's volume FinTech single bucket for now (BCH wallets, bridges, POS, DeFi) Routes: /t/<slug> top-level page. For Apps shows the 7 sub-chips + an aggregated listings view across them. For Games and FinTech shows the listings directly (no sub-chips). /c/<slug> sub-category page under Apps (browsers, communication, …). The sole-sub categories /c/games and /c/fintech 301 to /t/games and /t/fintech respectively, so each view has exactly one canonical URL. Slug renames + removals in the CATEGORIES allow-list on listings.js: utilities → productivity (the display label was already pointing at that in people's heads — "productivity" fits storage + sync + file tools better) wallets → fintech (broader scope: bridges + POS + DeFi, not just wallets) games stays as a top-level sole-bucket browsers NEW sub-category under Apps for BCNR resolvers, mobile browsers, extensions Seed listings re-categorised + two slug fixes: theseus.x/theseus-navigator-win category: other → browsers ariadne.x/ariadne-resolver-win → RENAMED to ariadne-thread-win, name "Ariadne's Thread (Windows resolver)" shortened to just "Ariadne's Thread"; category: privacy → browsers ariadne.x/ariadne-android → RENAMED to ariadne-navigator, name "Ariadne's Thread (Android)" → "Ariadne Navigator" (it is a different program, not a platform build of Thread); category: privacy → browsers pithos.x/pithos-desktop category: utilities → productivity scripts/migrate-taxonomy.mjs is a one-shot that deletes every row with signature='SM_OPERATOR_SEED'; a subsequent `seed.mjs` run then inserts the fresh set with the new slugs. Publisher-POSTed listings (none in production today) are left alone. Idempotent — a second run finds nothing to remove. New icons: top-apps.svg 2×2 rounded tile grid (OS launcher metaphor) cat-browsers.svg browser window + 3 chrome dots + content block Three icons reused: cat-games.svg doubles as the Games top icon, cat-wallets.svg as the FinTech top icon (same visual, different label); cat-utilities.svg stays as the Productivity sub icon.
2026-10-22 22:39:08 +03:00
// Sole-sub categories (games, fintech) redirect to their /t/<top> page,
// because the top page IS the listings view and we don't want two URLs
// for the same content.
const top = TOP_BY_SLUG[cat.top];
const soleSub = top && CATEGORIES_BY_TOP[cat.top].length === 1;
if (soleSub) {
return {
title: cat.title,
description: cat.blurb,
status: 301,
redirect: `/t/${cat.top}`,
here: '',
body: '',
};
}
const rows = listingsByCategory(slug);
const title = cat.title;
const here = `/c/${slug}`;
prometheus: taxonomy — Apps / Games / FinTech, with sub-chips inside Apps Three top-level groups replace the flat eight categories on the home page and /browse: Apps 7 sub-categories inside: browsers / communication / privacy / devtools / media / productivity / other Games single bucket for now; sub-chips later when there's volume FinTech single bucket for now (BCH wallets, bridges, POS, DeFi) Routes: /t/<slug> top-level page. For Apps shows the 7 sub-chips + an aggregated listings view across them. For Games and FinTech shows the listings directly (no sub-chips). /c/<slug> sub-category page under Apps (browsers, communication, …). The sole-sub categories /c/games and /c/fintech 301 to /t/games and /t/fintech respectively, so each view has exactly one canonical URL. Slug renames + removals in the CATEGORIES allow-list on listings.js: utilities → productivity (the display label was already pointing at that in people's heads — "productivity" fits storage + sync + file tools better) wallets → fintech (broader scope: bridges + POS + DeFi, not just wallets) games stays as a top-level sole-bucket browsers NEW sub-category under Apps for BCNR resolvers, mobile browsers, extensions Seed listings re-categorised + two slug fixes: theseus.x/theseus-navigator-win category: other → browsers ariadne.x/ariadne-resolver-win → RENAMED to ariadne-thread-win, name "Ariadne's Thread (Windows resolver)" shortened to just "Ariadne's Thread"; category: privacy → browsers ariadne.x/ariadne-android → RENAMED to ariadne-navigator, name "Ariadne's Thread (Android)" → "Ariadne Navigator" (it is a different program, not a platform build of Thread); category: privacy → browsers pithos.x/pithos-desktop category: utilities → productivity scripts/migrate-taxonomy.mjs is a one-shot that deletes every row with signature='SM_OPERATOR_SEED'; a subsequent `seed.mjs` run then inserts the fresh set with the new slugs. Publisher-POSTed listings (none in production today) are left alone. Idempotent — a second run finds nothing to remove. New icons: top-apps.svg 2×2 rounded tile grid (OS launcher metaphor) cat-browsers.svg browser window + 3 chrome dots + content block Three icons reused: cat-games.svg doubles as the Games top icon, cat-wallets.svg as the FinTech top icon (same visual, different label); cat-utilities.svg stays as the Productivity sub icon.
2026-10-22 22:39:08 +03:00
const backLabel = top ? `← Back to ${escapeHtml(top.title)}` : '← All categories';
const backHref = top ? `/t/${top.slug}` : '/browse';
return {
title,
prometheus: taxonomy — Apps / Games / FinTech, with sub-chips inside Apps Three top-level groups replace the flat eight categories on the home page and /browse: Apps 7 sub-categories inside: browsers / communication / privacy / devtools / media / productivity / other Games single bucket for now; sub-chips later when there's volume FinTech single bucket for now (BCH wallets, bridges, POS, DeFi) Routes: /t/<slug> top-level page. For Apps shows the 7 sub-chips + an aggregated listings view across them. For Games and FinTech shows the listings directly (no sub-chips). /c/<slug> sub-category page under Apps (browsers, communication, …). The sole-sub categories /c/games and /c/fintech 301 to /t/games and /t/fintech respectively, so each view has exactly one canonical URL. Slug renames + removals in the CATEGORIES allow-list on listings.js: utilities → productivity (the display label was already pointing at that in people's heads — "productivity" fits storage + sync + file tools better) wallets → fintech (broader scope: bridges + POS + DeFi, not just wallets) games stays as a top-level sole-bucket browsers NEW sub-category under Apps for BCNR resolvers, mobile browsers, extensions Seed listings re-categorised + two slug fixes: theseus.x/theseus-navigator-win category: other → browsers ariadne.x/ariadne-resolver-win → RENAMED to ariadne-thread-win, name "Ariadne's Thread (Windows resolver)" shortened to just "Ariadne's Thread"; category: privacy → browsers ariadne.x/ariadne-android → RENAMED to ariadne-navigator, name "Ariadne's Thread (Android)" → "Ariadne Navigator" (it is a different program, not a platform build of Thread); category: privacy → browsers pithos.x/pithos-desktop category: utilities → productivity scripts/migrate-taxonomy.mjs is a one-shot that deletes every row with signature='SM_OPERATOR_SEED'; a subsequent `seed.mjs` run then inserts the fresh set with the new slugs. Publisher-POSTed listings (none in production today) are left alone. Idempotent — a second run finds nothing to remove. New icons: top-apps.svg 2×2 rounded tile grid (OS launcher metaphor) cat-browsers.svg browser window + 3 chrome dots + content block Three icons reused: cat-games.svg doubles as the Games top icon, cat-wallets.svg as the FinTech top icon (same visual, different label); cat-utilities.svg stays as the Productivity sub icon.
2026-10-22 22:39:08 +03:00
description: `${cat.title} on Prometheus — ${cat.blurb}`,
here,
body: `
<header class="hero compact">
prometheus: SVG icons for the 8 categories Replaces the emoji glyphs (💬🛡️💸🎮🧰🎧⚙️✨) in the category grid with dedicated SVG icons under /static/icons/cat-<slug>.svg, following the same visual family as the sibling .x marks (theseus, sirius, ariadne, pithos, aegis): 12-radius dark square + acid-green mark, legible at 16 px tab-icon size and 40 px grid size. Designs chosen to not blur with the Silent Mode product icons also rendered on the site: cat-communication speech bubble + three dots (not an envelope — too close to the Hephaestus letter imagery elsewhere) cat-privacy shield + checkmark (DISTINCT from aegis.svg's shield+chevron, so category + listing don't collide) cat-wallets stacked coins + B serif (hints at the BCH coin) cat-games stylised controller (d-pad + two action buttons) cat-devtools code brackets < / > (reads as "code" at 16 px where a wrench/hammer blurs with the Utilities gear) cat-media play triangle in a circle (equally "player" and "publish/broadcast") cat-utilities gear with hollow centre (less visual weight than the Media circle; the two side-by-side don't crowd each other) cat-other sparkle burst + two smaller sparkles (sibling of sirius.svg's 4-point mark, extras so the two don't look identical) pages-browse.js: CATEGORIES[].icon is now the path to the SVG, not an emoji string. The three places the icon rendered — grid card, category hero, and the inline chip on listing detail — all now use <img> at the size they need (40, 56, 14 respectively). The CSS entry `.cat .ico` loses the font-size + grayscale (text styling) and gains width/height for the image plus a tiny hover rotation that matches the existing lift transform. Also: when a listing has no `icon` of its own, the detail page's icon slot now falls back to the category's SVG (centered + padded) instead of a plain 📦 emoji, so the detail hero always has a vector icon.
2026-10-22 21:30:13 +03:00
<img class="mark" src="${cat.icon}" alt="" width="56" height="56" style="display:block;margin:0 auto 1rem">
<h1>${escapeHtml(title)}</h1>
<p class="tag">${escapeHtml(cat.blurb)}</p>
</header>
<div class="wrap">
<section class="plain">
<p class="lede">
${rows.length === 0
? `No listings in this category yet. <a href="/submit">Be the first</a>.`
: `${rows.length} ${rows.length === 1 ? 'listing' : 'listings'}, newest first.`}
</p>
${rows.length === 0 ? emptyCategory(cat) : renderListingList(rows)}
<p style="margin-top:1.8rem;color:var(--dim);font-size:13px">
prometheus: taxonomy — Apps / Games / FinTech, with sub-chips inside Apps Three top-level groups replace the flat eight categories on the home page and /browse: Apps 7 sub-categories inside: browsers / communication / privacy / devtools / media / productivity / other Games single bucket for now; sub-chips later when there's volume FinTech single bucket for now (BCH wallets, bridges, POS, DeFi) Routes: /t/<slug> top-level page. For Apps shows the 7 sub-chips + an aggregated listings view across them. For Games and FinTech shows the listings directly (no sub-chips). /c/<slug> sub-category page under Apps (browsers, communication, …). The sole-sub categories /c/games and /c/fintech 301 to /t/games and /t/fintech respectively, so each view has exactly one canonical URL. Slug renames + removals in the CATEGORIES allow-list on listings.js: utilities → productivity (the display label was already pointing at that in people's heads — "productivity" fits storage + sync + file tools better) wallets → fintech (broader scope: bridges + POS + DeFi, not just wallets) games stays as a top-level sole-bucket browsers NEW sub-category under Apps for BCNR resolvers, mobile browsers, extensions Seed listings re-categorised + two slug fixes: theseus.x/theseus-navigator-win category: other → browsers ariadne.x/ariadne-resolver-win → RENAMED to ariadne-thread-win, name "Ariadne's Thread (Windows resolver)" shortened to just "Ariadne's Thread"; category: privacy → browsers ariadne.x/ariadne-android → RENAMED to ariadne-navigator, name "Ariadne's Thread (Android)" → "Ariadne Navigator" (it is a different program, not a platform build of Thread); category: privacy → browsers pithos.x/pithos-desktop category: utilities → productivity scripts/migrate-taxonomy.mjs is a one-shot that deletes every row with signature='SM_OPERATOR_SEED'; a subsequent `seed.mjs` run then inserts the fresh set with the new slugs. Publisher-POSTed listings (none in production today) are left alone. Idempotent — a second run finds nothing to remove. New icons: top-apps.svg 2×2 rounded tile grid (OS launcher metaphor) cat-browsers.svg browser window + 3 chrome dots + content block Three icons reused: cat-games.svg doubles as the Games top icon, cat-wallets.svg as the FinTech top icon (same visual, different label); cat-utilities.svg stays as the Productivity sub icon.
2026-10-22 22:39:08 +03:00
<a href="${backHref}">${backLabel}</a>
</p>
</section>
</div>`,
};
}
/* ------------------------------- app detail page ------------------------------- */
export function appPage(req, publisher, slug) {
const id = `${String(publisher).toLowerCase()}/${String(slug).toLowerCase()}`;
const row = getListing(id);
if (!row || row.withdrawn_at) return notFound(req);
const cat = CATEGORY_BY_SLUG[row.category];
prometheus: taxonomy — Apps / Games / FinTech, with sub-chips inside Apps Three top-level groups replace the flat eight categories on the home page and /browse: Apps 7 sub-categories inside: browsers / communication / privacy / devtools / media / productivity / other Games single bucket for now; sub-chips later when there's volume FinTech single bucket for now (BCH wallets, bridges, POS, DeFi) Routes: /t/<slug> top-level page. For Apps shows the 7 sub-chips + an aggregated listings view across them. For Games and FinTech shows the listings directly (no sub-chips). /c/<slug> sub-category page under Apps (browsers, communication, …). The sole-sub categories /c/games and /c/fintech 301 to /t/games and /t/fintech respectively, so each view has exactly one canonical URL. Slug renames + removals in the CATEGORIES allow-list on listings.js: utilities → productivity (the display label was already pointing at that in people's heads — "productivity" fits storage + sync + file tools better) wallets → fintech (broader scope: bridges + POS + DeFi, not just wallets) games stays as a top-level sole-bucket browsers NEW sub-category under Apps for BCNR resolvers, mobile browsers, extensions Seed listings re-categorised + two slug fixes: theseus.x/theseus-navigator-win category: other → browsers ariadne.x/ariadne-resolver-win → RENAMED to ariadne-thread-win, name "Ariadne's Thread (Windows resolver)" shortened to just "Ariadne's Thread"; category: privacy → browsers ariadne.x/ariadne-android → RENAMED to ariadne-navigator, name "Ariadne's Thread (Android)" → "Ariadne Navigator" (it is a different program, not a platform build of Thread); category: privacy → browsers pithos.x/pithos-desktop category: utilities → productivity scripts/migrate-taxonomy.mjs is a one-shot that deletes every row with signature='SM_OPERATOR_SEED'; a subsequent `seed.mjs` run then inserts the fresh set with the new slugs. Publisher-POSTed listings (none in production today) are left alone. Idempotent — a second run finds nothing to remove. New icons: top-apps.svg 2×2 rounded tile grid (OS launcher metaphor) cat-browsers.svg browser window + 3 chrome dots + content block Three icons reused: cat-games.svg doubles as the Games top icon, cat-wallets.svg as the FinTech top icon (same visual, different label); cat-utilities.svg stays as the Productivity sub icon.
2026-10-22 22:39:08 +03:00
// Sole-sub categories (games, fintech) show as the top-level chip since
// the sub URL just redirects to the top anyway.
const chipTarget = cat && TOP_BY_SLUG[cat.top] && CATEGORIES_BY_TOP[cat.top].length === 1
? { href: `/t/${cat.top}`, title: TOP_BY_SLUG[cat.top].title, icon: TOP_BY_SLUG[cat.top].icon }
: cat
? { href: `/c/${cat.slug}`, title: cat.title, icon: cat.icon }
: null;
const catChip = chipTarget
? `<a href="${chipTarget.href}" class="pill p-upcoming" style="text-decoration:none;display:inline-flex;align-items:center;gap:4px"><img src="${chipTarget.icon}" alt="" width="14" height="14" style="vertical-align:-2px"> ${escapeHtml(chipTarget.title)}</a>`
: '';
const scanReport = safeParse(row.scan_report);
const scanPayload = scanReport?.payload ? safeParse(fromBase64Url(scanReport.payload)) : null;
const verdict = scanPayload?.scan?.verdict || 'unknown';
const scanBadge = renderScanBadge(verdict, scanPayload);
const ld = softwareApplicationLd({ listing: row, scanVerdict: verdict });
const headExtra = `<script type="application/ld+json">${ld}</script>`;
const iconHtml = row.icon
? `<img src="${escapeAttr(row.icon)}" alt="" width="80" height="80"
style="width:80px;height:80px;border-radius:16px;border:1px solid var(--line);background:var(--panel);object-fit:cover">`
prometheus: SVG icons for the 8 categories Replaces the emoji glyphs (💬🛡️💸🎮🧰🎧⚙️✨) in the category grid with dedicated SVG icons under /static/icons/cat-<slug>.svg, following the same visual family as the sibling .x marks (theseus, sirius, ariadne, pithos, aegis): 12-radius dark square + acid-green mark, legible at 16 px tab-icon size and 40 px grid size. Designs chosen to not blur with the Silent Mode product icons also rendered on the site: cat-communication speech bubble + three dots (not an envelope — too close to the Hephaestus letter imagery elsewhere) cat-privacy shield + checkmark (DISTINCT from aegis.svg's shield+chevron, so category + listing don't collide) cat-wallets stacked coins + B serif (hints at the BCH coin) cat-games stylised controller (d-pad + two action buttons) cat-devtools code brackets < / > (reads as "code" at 16 px where a wrench/hammer blurs with the Utilities gear) cat-media play triangle in a circle (equally "player" and "publish/broadcast") cat-utilities gear with hollow centre (less visual weight than the Media circle; the two side-by-side don't crowd each other) cat-other sparkle burst + two smaller sparkles (sibling of sirius.svg's 4-point mark, extras so the two don't look identical) pages-browse.js: CATEGORIES[].icon is now the path to the SVG, not an emoji string. The three places the icon rendered — grid card, category hero, and the inline chip on listing detail — all now use <img> at the size they need (40, 56, 14 respectively). The CSS entry `.cat .ico` loses the font-size + grayscale (text styling) and gains width/height for the image plus a tiny hover rotation that matches the existing lift transform. Also: when a listing has no `icon` of its own, the detail page's icon slot now falls back to the category's SVG (centered + padded) instead of a plain 📦 emoji, so the detail hero always has a vector icon.
2026-10-22 21:30:13 +03:00
: cat?.icon
? `<img src="${cat.icon}" alt="" width="80" height="80"
style="width:80px;height:80px;border-radius:16px;border:1px solid var(--line);background:var(--panel);padding:12px">`
: `<div style="width:80px;height:80px;border-radius:16px;border:1px solid var(--line);background:var(--panel);display:grid;place-items:center;font-size:36px">📦</div>`;
return {
title: `${row.name} by ${shortenPublisher(row.publisher)}`,
description: row.description.slice(0, 200),
here: `/app/${row.publisher}/${row.slug}`,
ogType: 'article',
ogImage: row.icon || 'https://prometheus.x/static/favicon.svg',
headExtra,
body: `
<header class="hero compact" style="padding-bottom:1rem">
<div style="display:flex;gap:18px;align-items:center;justify-content:center;flex-wrap:wrap;margin-bottom:.8rem">
${iconHtml}
<div style="text-align:left">
<h1 style="margin:0;font-size:1.8rem">${escapeHtml(row.name)}</h1>
<div style="color:var(--mut);font-size:14px;margin-top:.3rem">
by <b style="color:var(--ink)" title="${escapeAttr(row.publisher)}">${escapeHtml(shortenPublisher(row.publisher))}</b> ·
v${escapeHtml(row.version)} ·
${catChip}
</div>
</div>
</div>
</header>
<div class="wrap">
<section class="plain">
<div class="card" style="padding:22px 26px">
<div style="display:flex;gap:1rem;align-items:center;justify-content:space-between;flex-wrap:wrap">
<div>
<h3 style="margin:0 0 .3rem;font-size:1.05rem">Download</h3>
<p style="margin:0;color:var(--mut);font-size:13px">
${formatBytes(row.download_size)} ·
<code class="inline" title="SHA-256">${shortSha(row.download_sha256)}</code>
${scanBadge}
</p>
</div>
<a class="btn acid" href="${escapeAttr(row.download_url)}" rel="noopener">Download</a>
</div>
<details style="margin-top:1rem">
<summary style="color:var(--dim);font-size:13px;cursor:pointer">Full hash + source URL</summary>
<div style="margin-top:.6rem;font-size:13px;color:var(--mut)">
<div><b style="color:var(--ink)">URL</b> <a href="${escapeAttr(row.download_url)}" rel="noopener">${escapeHtml(row.download_url)}</a></div>
<div style="margin-top:.4rem"><b style="color:var(--ink)">SHA-256</b> <code class="inline">${row.download_sha256}</code></div>
</div>
</details>
</div>
</section>
<section>
<h2>About</h2>
<p style="white-space:pre-wrap">${escapeHtml(row.description)}</p>
${row.homepage ? `<p style="margin-top:1rem"><a href="${escapeAttr(row.homepage)}" rel="noopener">Project homepage →</a></p>` : ''}
</section>
<section>
<h2>Who published this</h2>
<p>Signed by <b title="${escapeAttr(row.publisher)}">${escapeHtml(shortenPublisher(row.publisher))}</b> —
${/^(bitcoincash|bchtest):q[a-z0-9]{41}$/.test(String(row.publisher))
? `a raw BCH address publisher. The signing key is this address itself; no BNS name is attached.`
: `a BNS name owned by <code class="inline">${escapeHtml(row.signer_address)}</code>.`}
Any installer the chain says belongs to that key is trusted by your client;
any client that follows a different trust list will not see it as trusted.</p>
</section>
<section>
<h2>Endorsements</h2>
<div class="note"><b>No endorsements yet.</b> An endorsement is a signed
on-chain statement from any BNS name saying "I vouch for this app." Readers
whose trust list includes the endorser see the endorsement; those who do
not, do not. The endorsement protocol lands with APS1.</div>
</section>
<p style="margin-top:1.8rem;color:var(--dim);font-size:13px">
prometheus: taxonomy — Apps / Games / FinTech, with sub-chips inside Apps Three top-level groups replace the flat eight categories on the home page and /browse: Apps 7 sub-categories inside: browsers / communication / privacy / devtools / media / productivity / other Games single bucket for now; sub-chips later when there's volume FinTech single bucket for now (BCH wallets, bridges, POS, DeFi) Routes: /t/<slug> top-level page. For Apps shows the 7 sub-chips + an aggregated listings view across them. For Games and FinTech shows the listings directly (no sub-chips). /c/<slug> sub-category page under Apps (browsers, communication, …). The sole-sub categories /c/games and /c/fintech 301 to /t/games and /t/fintech respectively, so each view has exactly one canonical URL. Slug renames + removals in the CATEGORIES allow-list on listings.js: utilities → productivity (the display label was already pointing at that in people's heads — "productivity" fits storage + sync + file tools better) wallets → fintech (broader scope: bridges + POS + DeFi, not just wallets) games stays as a top-level sole-bucket browsers NEW sub-category under Apps for BCNR resolvers, mobile browsers, extensions Seed listings re-categorised + two slug fixes: theseus.x/theseus-navigator-win category: other → browsers ariadne.x/ariadne-resolver-win → RENAMED to ariadne-thread-win, name "Ariadne's Thread (Windows resolver)" shortened to just "Ariadne's Thread"; category: privacy → browsers ariadne.x/ariadne-android → RENAMED to ariadne-navigator, name "Ariadne's Thread (Android)" → "Ariadne Navigator" (it is a different program, not a platform build of Thread); category: privacy → browsers pithos.x/pithos-desktop category: utilities → productivity scripts/migrate-taxonomy.mjs is a one-shot that deletes every row with signature='SM_OPERATOR_SEED'; a subsequent `seed.mjs` run then inserts the fresh set with the new slugs. Publisher-POSTed listings (none in production today) are left alone. Idempotent — a second run finds nothing to remove. New icons: top-apps.svg 2×2 rounded tile grid (OS launcher metaphor) cat-browsers.svg browser window + 3 chrome dots + content block Three icons reused: cat-games.svg doubles as the Games top icon, cat-wallets.svg as the FinTech top icon (same visual, different label); cat-utilities.svg stays as the Productivity sub icon.
2026-10-22 22:39:08 +03:00
${chipTarget ? `<a href="${chipTarget.href}">← Back to ${escapeHtml(chipTarget.title)}</a>` : `<a href="/browse">← Browse</a>`}
</p>
</div>`,
};
}
/* ------------------------------- helpers ------------------------------- */
function renderListingList(rows) {
return `<div class="grid" style="margin-top:1.2rem">
${rows.map(renderListingCard).join('\n')}
</div>`;
}
function renderListingCard(row) {
const href = `/app/${encodeURIComponent(row.publisher)}/${encodeURIComponent(row.slug)}`;
return `<a class="card" href="${href}"
style="text-decoration:none;color:inherit;display:flex;flex-direction:column;gap:.5rem">
<h3 style="margin:0">${escapeHtml(row.name)}</h3>
<p style="margin:0;color:var(--mut);font-size:13.5px;line-height:1.5">
${escapeHtml(truncate(row.description, 160))}
</p>
<div style="color:var(--dim);font-size:12px;margin-top:auto;padding-top:.5rem">
by ${escapeHtml(shortenPublisher(row.publisher))} · v${escapeHtml(row.version)} · ${formatBytes(row.download_size)}
</div>
</a>`;
}
function emptyCategory(cat) {
return `<div class="note">
<b>Nothing here yet.</b> If you ship software that fits "${escapeHtml(cat.title)}",
the <a href="/submit">submit flow</a> is live — upload, scan, sign, your listing
appears in this view.
</div>`;
}
function renderScanBadge(verdict, payload) {
const engine = payload?.scan?.engine || '';
const sigs = payload?.scan?.signatures || [];
switch (verdict) {
case 'clean':
return `<span class="pill p-live" style="margin-left:.4rem">scan ✓ clean${engine ? ` (${escapeHtml(engine)})` : ''}</span>`;
case 'infected':
return `<span class="pill" style="background:rgba(246,118,138,.14);color:var(--taken);margin-left:.4rem" title="${escapeAttr(sigs.join(', '))}">scan ⚠ infected</span>`;
default:
return `<span class="pill p-upcoming" style="margin-left:.4rem">scan ? ${escapeHtml(verdict)}</span>`;
}
}
function formatBytes(n) {
const num = Number(n) || 0;
if (num < 1024) return `${num} B`;
if (num < 1024 * 1024) return `${(num / 1024).toFixed(1)} KiB`;
if (num < 1024 * 1024 * 1024) return `${(num / 1024 / 1024).toFixed(1)} MiB`;
return `${(num / 1024 / 1024 / 1024).toFixed(2)} GiB`;
}
function shortSha(sha) {
if (!sha) return '';
const s = String(sha);
if (s.length <= 16) return s;
return `${s.slice(0, 8)}…${s.slice(-6)}`;
}
/**
* Shorten a publisher for display. BNS names (foo.x) are returned as-is;
* raw cashaddresses are collapsed to `bchtest:qpumq…sh478` so they fit a
* card without breaking the layout. The URL/path side still uses the full
* publisher — this is a display-only transform.
*/
function shortenPublisher(pub) {
const s = String(pub || '');
const m = s.match(/^(bitcoincash|bchtest):(q[a-z0-9]{41})$/);
if (!m) return s;
const addr = m[2];
return `${m[1]}:${addr.slice(0, 5)}…${addr.slice(-5)}`;
}
function truncate(s, max) {
const str = String(s || '');
if (str.length <= max) return str;
return str.slice(0, max - 1) + '…';
}
function escapeAttr(s) {
// Same as text escape; attributes are also always wrapped in double-quotes.
return escapeHtml(s);
}
function safeParse(s) {
if (!s) return null;
if (typeof s === 'object') return s;
try { return JSON.parse(s); } catch { return null; }
}
function fromBase64Url(s) {
try { return Buffer.from(String(s), 'base64url').toString('utf8'); }
catch { return ''; }
}