@joystyk/catalog 0.0.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +24 -0
- package/README.md +29 -2
- package/dist/advertise.d.ts +82 -0
- package/dist/advertise.js +203 -0
- package/dist/advertise.js.map +1 -0
- package/dist/categories.d.ts +12 -0
- package/dist/categories.js +45 -0
- package/dist/categories.js.map +1 -0
- package/dist/client.d.ts +1 -0
- package/dist/client.js +9 -0
- package/dist/client.js.map +1 -0
- package/dist/components/advertise.d.ts +4 -0
- package/dist/components/advertise.js +1304 -0
- package/dist/components/advertise.js.map +1 -0
- package/dist/components/app.d.ts +25 -0
- package/dist/components/app.js +106 -0
- package/dist/components/app.js.map +1 -0
- package/dist/components/leaving.d.ts +2 -0
- package/dist/components/leaving.js +104 -0
- package/dist/components/leaving.js.map +1 -0
- package/dist/components/views.d.ts +13 -0
- package/dist/components/views.js +1410 -0
- package/dist/components/views.js.map +1 -0
- package/dist/head.d.ts +6 -0
- package/dist/head.js +33 -0
- package/dist/head.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +12 -0
- package/dist/labels.d.ts +10 -0
- package/dist/labels.js +27 -0
- package/dist/labels.js.map +1 -0
- package/dist/placeholders.d.ts +3 -0
- package/dist/placeholders.js +7 -0
- package/dist/placeholders.js.map +1 -0
- package/dist/registry.d.ts +46 -0
- package/dist/registry.js +106 -0
- package/dist/registry.js.map +1 -0
- package/dist/routes.d.ts +56 -0
- package/dist/routes.js +89 -0
- package/dist/routes.js.map +1 -0
- package/dist/seo.d.ts +31 -0
- package/dist/seo.js +174 -0
- package/dist/seo.js.map +1 -0
- package/dist/server.d.ts +50 -0
- package/dist/server.js +162 -0
- package/dist/server.js.map +1 -0
- package/dist/store.d.ts +202 -0
- package/dist/store.js +136 -0
- package/dist/store.js.map +1 -0
- package/dist/styles.css +1121 -0
- package/dist/wire.d.ts +108 -0
- package/dist/wire.js +74 -0
- package/dist/wire.js.map +1 -0
- package/package.json +63 -11
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"views.js","names":["_gyral$compiled"],"sources":["../../src/components/views.ts"],"sourcesContent":["// The page views <jy-app> renders: plain functions of the catalog state, in light DOM so\n// crawlers and the document stylesheet see them. List rows are module-level and pure: they\n// read only their arguments (Gyral's `each` rule).\nimport { each, html, nothing } from '@gyral/core';\nimport '@joystyk/launcher';\nimport type { LauncherGame } from '@joystyk/launcher';\nimport '@joystyk/ui/ad-slot';\nimport type { SlotId, SlotSurface } from '@joystyk/ui/slots';\nimport { MODE_LABELS, STATUS_LABELS, playersLabel, playersRange, rankLabel } from '../labels.ts';\nimport { ADVERTISE_PATH, categoryPath, gamePath, type Page } from '../routes.ts';\nimport { pageTitleOf } from '../seo.ts';\nimport type { CatalogState } from '../store.ts';\nimport type { CategoryInfo, GameDetail, GameSummary } from '../wire.ts';\nimport { advertiseView, policyView, sentView } from './advertise.ts';\nimport { leavingView } from './leaving.ts';\n\n/** What a page is about, for the slot's contextual ad request: never who is looking. */\ninterface SlotContext {\n readonly page: SlotSurface;\n readonly game?: string;\n readonly category?: string;\n}\n\n/** A billboard slot; `<ad-slot>` reserves its box from the slot registry and fills it lazily. */\nconst adSlot = (id: SlotId, c: SlotContext) =>\n html`<ad-slot slot-id=${id} page=${c.page} game-id=${c.game} category=${c.category}></ad-slot>`;\n\n/** What the launcher needs from a game page's data. */\nexport const launcherGameOf = (g: GameDetail): LauncherGame => ({\n id: g.id,\n name: g.name,\n modes: g.modes,\n players: g.players,\n aiLevels: g.aiLevels,\n status: g.status,\n aiContent: g.aiContent,\n ...(g.options === undefined ? {} : { options: g.options }),\n});\n\n// --- Shared pieces ---------------------------------------------------------------------\n\nconst Badge = (text: string) => html`<li>${text}</li>`;\n\nconst badges = (g: GameSummary) => html`<ul class=\"badges\" aria-label=\"At a glance\">\n <li class=\"badge-status\" data-status=${g.status}>${STATUS_LABELS[g.status]}</li>\n <li>${playersLabel(g.players)}</li>\n ${each(\n g.modes,\n (m) => m,\n (m) => Badge(MODE_LABELS[m]),\n )}\n</ul>`;\n\nconst cardBody = (g: GameSummary) => html`<p class=\"card-summary\">${g.summary}</p>\n ${badges(g)}\n <jy-offline-availability game-id=${g.id} hidden></jy-offline-availability>`;\n\n/** A game card under an h2 section heading. */\nconst CardH3 = (g: GameSummary) => html`<li>\n <article class=\"game-card\" data-category=${g.category}>\n <h3><a href=${gamePath(g.id)}>${g.name}</a></h3>\n ${cardBody(g)}\n </article>\n</li>`;\n\n/** A game card directly under the page's h1. */\nconst CardH2 = (g: GameSummary) => html`<li>\n <article class=\"game-card\" data-category=${g.category}>\n <h2><a href=${gamePath(g.id)}>${g.name}</a></h2>\n ${cardBody(g)}\n </article>\n</li>`;\n\nconst grid = (games: readonly GameSummary[], card: typeof CardH3) =>\n html`<ul class=\"game-grid\" role=\"list\">${each(games, (g) => g.id, card)}</ul>`;\n\nconst loading = (failed: boolean, what: string) =>\n html`<p class=\"loading\" aria-busy=${failed ? 'false' : 'true'}>\n ${failed ? `The ${what} could not load. Reload the page to try again.` : `Loading ${what}…`}\n </p>`;\n\ninterface Crumb {\n readonly name: string;\n readonly href: string;\n}\n\nconst CrumbItem = (c: Crumb, current: boolean) =>\n html`<li><a href=${c.href} aria-current=${current ? 'page' : null}>${c.name}</a></li>`;\n\nconst breadcrumbs = (crumbs: readonly Crumb[]) => {\n const last = crumbs.at(-1)?.href;\n return html`<nav class=\"crumbs\" aria-label=\"Breadcrumb\">\n <ol>${each(\n crumbs,\n (c) => c.href,\n CrumbItem,\n (c) => c.href === last,\n )}</ol>\n </nav>`;\n};\n\nconst categoryOf = (c: CatalogState, id: string): CategoryInfo | undefined => c.categories.find((k) => k.id === id);\n\n// --- Home ------------------------------------------------------------------------------\n\nconst CategorySection = (k: CategoryInfo, games: readonly GameSummary[]) => html`<section\n class=\"category-section\"\n data-category=${k.id}\n aria-labelledby=\"category-${k.id}\"\n>\n <header class=\"section-head\">\n <h2 id=\"category-${k.id}\"><a href=${categoryPath(k.id)}>${k.name}</a></h2>\n <p>${k.description}</p>\n </header>\n ${grid(games, CardH3)}\n</section>`;\n\ninterface LineupRow {\n readonly rank: number;\n readonly game: GameSummary;\n readonly category: string;\n}\n\nconst LineupItem = (r: LineupRow) => html`<tr>\n <td class=\"rank\">${rankLabel(r.rank)}</td>\n <th scope=\"row\"><a href=${gamePath(r.game.id)}>${r.game.name}</a><jy-offline-availability game-id=${r.game.id} hidden></jy-offline-availability></th>\n <td>${r.category}</td>\n <td class=\"num\">${playersRange(r.game.players)}</td>\n <td>${STATUS_LABELS[r.game.status]}</td>\n</tr>`;\n\nconst lineup = (rows: readonly LineupRow[]) => html`<section class=\"lineup\" aria-labelledby=\"lineup-heading\">\n <h2 id=\"lineup-heading\">The lineup</h2>\n <div class=\"score-screen\" role=\"region\" aria-labelledby=\"lineup-caption\" tabindex=\"0\">\n <table class=\"score-table\">\n <caption id=\"lineup-caption\">Every game on Joystyk, high-score style</caption>\n <thead>\n <tr>\n <th scope=\"col\">No.</th>\n <th scope=\"col\">Game</th>\n <th scope=\"col\">Category</th>\n <th scope=\"col\" class=\"num\">Players</th>\n <th scope=\"col\">Status</th>\n </tr>\n </thead>\n <tbody>\n ${each(rows, (r) => r.game.id, LineupItem)}\n </tbody>\n </table>\n </div>\n</section>`;\n\nconst rail = (c: SlotContext) => html`<aside class=\"rail\" aria-label=\"Sponsor\">${adSlot('category-rail', c)}</aside>`;\n\nconst HOME: SlotContext = { page: 'home' };\n\nfunction homeView(c: CatalogState) {\n const games = c.summaries;\n const featured = games?.filter((g) => g.featured) ?? [];\n const [first, ...rest] = c.categories;\n const section = (k: CategoryInfo) => CategorySection(k, games?.filter((g) => g.category === k.id) ?? []);\n return html`<div class=\"home\">\n <section class=\"hero\" aria-labelledby=\"hero-heading\">\n <div class=\"screen\">\n <p class=\"kicker\"><span class=\"blink\">Press start</span></p>\n <h1 id=\"hero-heading\" tabindex=\"-1\">Free games for your browser. No quarters needed.</h1>\n <p class=\"lede\">\n Arcade, card and dice games that start in one click: no sign-up, no download, safe for every age. Play alone,\n against the computer, or with friends.\n </p>\n <p class=\"hero-actions\"><a class=\"cta\" href=\"#categories\" aria-label=\"Insert coin: browse the games\">Insert coin</a></p>\n </div>\n </section>\n <div class=\"with-rail\">\n <div class=\"column\">\n ${\n games === null\n ? loading(c.summariesLoad === 'failed', 'games')\n : html`<section class=\"featured\" aria-labelledby=\"featured-heading\">\n <h2 id=\"featured-heading\">Featured</h2>\n ${grid(featured, CardH3)}\n </section>\n ${adSlot('index-hero', HOME)}\n <div id=\"categories\" class=\"categories\">\n ${first === undefined ? nothing : section(first)} ${rest.length > 0 ? adSlot('index-inline', HOME) : nothing}\n ${rest.map(section)}\n </div>\n ${lineup(games.map((game, n) => ({ rank: n + 1, game, category: categoryOf(c, game.category)?.name ?? game.category })))}`\n }\n </div>\n ${rail(HOME)}\n </div>\n </div>`;\n}\n\n// --- Category --------------------------------------------------------------------------\n\nconst OtherCategory = (k: CategoryInfo) =>\n html`<li><a href=${categoryPath(k.id)}>${k.name}</a> <span class=\"count\">(${k.games})</span></li>`;\n\nfunction categoryView(k: CategoryInfo, c: CatalogState) {\n const games = c.summaries?.filter((g) => g.category === k.id);\n return html`<div class=\"category-page\" data-category=${k.id}>\n ${breadcrumbs([\n { name: 'Games', href: '/' },\n { name: k.name, href: categoryPath(k.id) },\n ])}\n <header class=\"page-head\">\n <p class=\"kicker\">Category</p>\n <h1 tabindex=\"-1\">${k.name} games</h1>\n <p class=\"lede\">${k.description}</p>\n </header>\n <div class=\"with-rail\">\n <div class=\"column\">\n ${games === undefined ? loading(c.summariesLoad === 'failed', 'games') : grid(games, CardH2)}\n <nav class=\"more\" aria-labelledby=\"other-categories\">\n <h2 id=\"other-categories\">Other categories</h2>\n <ul>${each(\n c.categories.filter((o) => o.id !== k.id),\n (o) => o.id,\n OtherCategory,\n )}</ul>\n </nav>\n </div>\n ${rail({ page: 'category', category: k.id })}\n </div>\n </div>`;\n}\n\n// --- Game ------------------------------------------------------------------------------\n\n/**\n * The PWA's \"Make available offline\" control (@joystyk/pwa defines the element in the\n * browser). Server-rendered as an empty box; it fills in once a service worker is active.\n */\nconst offlineToggle = (g: GameDetail) =>\n html`<jy-offline-toggle game-id=${g.id} game-name=${g.name} assets=${g.assets}></jy-offline-toggle>`;\n\nconst Rule = (rule: string) => html`<li>${rule}</li>`;\n\nconst OtherGame = (g: GameSummary) =>\n html`<li><a href=${gamePath(g.id)}>${g.name}</a>: ${g.summary} <jy-offline-availability game-id=${g.id} hidden></jy-offline-availability></li>`;\n\nfunction gameView(g: GameDetail, c: CatalogState) {\n const k = categoryOf(c, g.category);\n const others = c.summaries?.filter((o) => o.id !== g.id) ?? [];\n return html`<article class=\"game-page\" data-category=${g.category} aria-labelledby=\"game-title\">\n ${breadcrumbs([\n { name: 'Games', href: '/' },\n ...(k === undefined ? [] : [{ name: k.name, href: categoryPath(k.id) }]),\n { name: g.name, href: gamePath(g.id) },\n ])}\n <header class=\"page-head\">\n <p class=\"kicker\">${k === undefined ? 'Game' : `${k.name} game`}</p>\n <h1 id=\"game-title\" tabindex=\"-1\">${pageTitleOf(g)}</h1>\n <p class=\"lede\">${g.summary}</p>\n <dl class=\"facts\">\n <div><dt>Status</dt><dd>${STATUS_LABELS[g.status]}</dd></div>\n <div><dt>Players</dt><dd>${playersLabel(g.players)}</dd></div>\n <div><dt>Modes</dt><dd>${g.modes.map((m) => MODE_LABELS[m]).join(', ')}</dd></div>\n ${g.aiLevels.length === 0 ? nothing : html`<div><dt>Computer players</dt><dd>Yes</dd></div>`}\n </dl>\n </header>\n ${g.modes.includes('daily') ? adSlot('daily-presenting', { page: 'daily', game: g.id, category: g.category }) : nothing}\n <section class=\"cabinet\" aria-labelledby=\"play-heading\">\n <h2 id=\"play-heading\">Play ${g.name}</h2>\n <jy-launcher .game=${launcherGameOf(g)}></jy-launcher>\n ${g.status === 'available' ? offlineToggle(g) : nothing}\n </section>\n ${adSlot('game-page', { page: 'game', game: g.id, category: g.category })}\n <section aria-labelledby=\"about-heading\">\n <h2 id=\"about-heading\">About ${g.name}</h2>\n <p>${g.description}</p>\n ${g.attribution === undefined ? nothing : html`<p class=\"credit\"><a href=${g.attribution.href} target=\"_blank\" rel=\"noopener noreferrer external\">${g.attribution.text}</a></p>`}\n </section>\n <section aria-labelledby=\"rules-heading\">\n <h2 id=\"rules-heading\">How to play ${g.name}</h2>\n ${g.placeholder ? html`<p class=\"note\">A short summary for now. The full rules arrive with the game.</p>` : nothing}\n <ol class=\"rules\">${each(g.rules, (r) => r, Rule)}</ol>\n </section>\n ${\n others.length === 0\n ? nothing\n : html`<nav class=\"more\" aria-labelledby=\"more-heading\">\n <h2 id=\"more-heading\">More games</h2>\n <ul>${each(others, (o) => o.id, OtherGame)}</ul>\n </nav>`\n }\n </article>`;\n}\n\n// --- Not found -------------------------------------------------------------------------\n\nconst notFoundView = () => html`<section class=\"not-found\" aria-labelledby=\"nf-heading\">\n <p class=\"kicker\">Game over</p>\n <h1 id=\"nf-heading\" tabindex=\"-1\">Page not found</h1>\n <p class=\"lede\">There is no page at this address. It may have moved, or the link may have a typo.</p>\n <p class=\"continue\">Continue? <a href=\"/\">See all games</a></p>\n</section>`;\n\n// --- The page --------------------------------------------------------------------------\n\n/** True when the page's content (and so its h1) is on screen, rather than a loading line. */\nexport function isReady(page: Page, c: CatalogState): boolean {\n switch (page._tag) {\n case 'Home':\n return c.summaries !== null;\n case 'Category':\n return c.summaries !== null || !c.categories.some((k) => k.id === page.id);\n case 'Game':\n return page.id in c.games || c.missing.includes(page.id);\n case 'Advertise':\n case 'Licenses':\n case 'AdvertisePolicy':\n case 'AdvertiseSent':\n case 'LeavingSite':\n case 'NotFound':\n return true;\n }\n}\n\nexport function mainView(page: Page, c: CatalogState) {\n switch (page._tag) {\n case 'Home':\n return homeView(c);\n case 'NotFound':\n return notFoundView();\n case 'Licenses':\n return html`<article class=\"policy\" aria-labelledby=\"licenses-title\"><h1 id=\"licenses-title\" tabindex=\"-1\">Source licenses and credits</h1><section><h2>Map data</h2><p>Made with Natural Earth. Map Guess uses Natural Earth vector data v5.1.2, with source checksums and a mechanical combination of default, ISO and national border views. The data is in the public domain.</p><p><a href=\"https://www.naturalearthdata.com/about/terms-of-use/\" target=\"_blank\" rel=\"noopener noreferrer\">Natural Earth terms of use</a> · <a href=\"https://www.naturalearthdata.com/about/disputed-boundaries-policy/\" target=\"_blank\" rel=\"noopener noreferrer\">Disputed boundaries policy</a></p></section><section><h2>Word lists</h2><p>Several games use derived SCOWL 2020.12.07 word lists. Copyright 2000–2018 Kevin Atkinson, with portions copyright by the source contributors. SCOWL combines word-list sources under permissive licenses; the full copyright notices are retained with each derived list in our repository.</p><p><a href=\"https://github.com/en-wl/wordlist/blob/5ef55f9/scowl/Copyright\" target=\"_blank\" rel=\"noopener noreferrer\">SCOWL license notices</a></p></section><section><h2>Fonts</h2><p>Letter Drop and Shift 23 use Zilla Slab, copyright 2017 The Mozilla Foundation, under the SIL Open Font License 1.1. The bundled font and full license are retained with each game.</p><p><a href=\"https://github.com/mozilla/zilla-slab\" target=\"_blank\" rel=\"noopener noreferrer\">Zilla Slab source and license</a></p></section><section><h2>All-ages word filters</h2><p>The curated filters draw on the English LDNOOBW list, copyright Shutterstock, Inc. and the LDNOOBW contributors, under Creative Commons Attribution 4.0 International. Our filters select entries, add inflected forms and stems, and include local additions.</p><p><a href=\"https://github.com/LDNOOBW/List-of-Dirty-Naughty-Obscene-and-Otherwise-Bad-Words\" target=\"_blank\" rel=\"noopener noreferrer\">Source list</a> · <a href=\"https://creativecommons.org/licenses/by/4.0/\" target=\"_blank\" rel=\"noopener noreferrer\">CC BY 4.0</a></p></section></article>`;\n case 'Advertise':\n return advertiseView(c);\n case 'AdvertisePolicy':\n return policyView();\n case 'AdvertiseSent':\n return sentView();\n case 'LeavingSite':\n return c.leaving?.id === page.id ? leavingView(c.leaving) : notFoundView();\n case 'Category': {\n const k = categoryOf(c, page.id);\n return k === undefined ? notFoundView() : categoryView(k, c);\n }\n case 'Game': {\n const g = c.games[page.id];\n if (g !== undefined) return gameView(g, c);\n if (c.missing.includes(page.id)) return notFoundView();\n return loading(c.gameLoads[page.id] === 'failed', 'game');\n }\n }\n}\n\nconst NavItem = (k: CategoryInfo, current: boolean) =>\n html`<li><a href=${categoryPath(k.id)} aria-current=${current ? 'page' : null}>${k.name}</a></li>`;\n\nexport const masthead = (page: Page, c: CatalogState) => html`<a class=\"skip-link\" href=\"#main\">Skip to content</a>\n <header class=\"masthead\">\n <a class=\"brand\" href=\"/\">\n <svg class=\"logo\" viewBox=\"0 0 32 32\" aria-hidden=\"true\" focusable=\"false\">\n <rect x=\"4\" y=\"22\" width=\"24\" height=\"6\" rx=\"1.5\" class=\"logo-base\" />\n <rect x=\"14.5\" y=\"8\" width=\"3\" height=\"15\" class=\"logo-stick\" />\n <circle cx=\"16\" cy=\"8\" r=\"5\" class=\"logo-ball\" />\n <circle cx=\"24\" cy=\"24.5\" r=\"1.6\" class=\"logo-button\" />\n </svg>\n <span class=\"wordmark\">Joystyk</span>\n </a>\n <nav class=\"main-nav\" aria-label=\"Main\">\n <ul>\n <li><a href=\"/\" aria-current=${page._tag === 'Home' ? 'page' : null}>All games</a></li>\n ${each(\n c.categories,\n (k) => k.id,\n NavItem,\n (k) => page._tag === 'Category' && page.id === k.id,\n )}\n </ul>\n </nav>\n </header>`;\n\nexport const footer = () => html`<footer class=\"site-footer\">\n <p><span class=\"wordmark small\">Joystyk</span> Free games for every age. No sign-up, no download.</p>\n <nav class=\"footer-nav\" aria-label=\"Footer\">\n <ul>\n <li><a href=${ADVERTISE_PATH}>Advertise with us</a></li><li><a href=\"/licenses\">Source licenses</a></li>\n </ul>\n </nav>\n <p><small>Game listings marked “Coming soon” are previews while the platform is built.</small></p>\n</footer>`;\n"],"mappings":"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"}
|
package/dist/head.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { Head as GyralHead } from '@gyral/core';
|
|
2
|
+
import { type Head } from './seo.js';
|
|
3
|
+
/** Open Graph properties for a head; og:url only for indexable pages. */
|
|
4
|
+
export declare function openGraph(head: Head): readonly (readonly [property: string, content: string])[];
|
|
5
|
+
/** Both SSR and client navigation use this value, including removal of absent metadata. */
|
|
6
|
+
export declare function toGyralHead(head: Head): GyralHead;
|
package/dist/head.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { SITE_NAME } from "./seo.js";
|
|
2
|
+
//#region src/head.ts
|
|
3
|
+
/** Open Graph properties for a head; og:url only for indexable pages. */
|
|
4
|
+
function openGraph(head) {
|
|
5
|
+
return [
|
|
6
|
+
["og:site_name", SITE_NAME],
|
|
7
|
+
["og:type", head.ogType],
|
|
8
|
+
["og:title", head.title],
|
|
9
|
+
["og:description", head.description],
|
|
10
|
+
...head.canonical === null ? [] : [["og:url", head.canonical]]
|
|
11
|
+
];
|
|
12
|
+
}
|
|
13
|
+
/** Both SSR and client navigation use this value, including removal of absent metadata. */
|
|
14
|
+
function toGyralHead(head) {
|
|
15
|
+
return {
|
|
16
|
+
title: head.title,
|
|
17
|
+
description: head.description,
|
|
18
|
+
...head.canonical === null ? {} : { canonical: head.canonical },
|
|
19
|
+
robots: head.robots === "index" ? "index, follow" : "noindex",
|
|
20
|
+
meta: [...openGraph(head).map(([property, content]) => ({
|
|
21
|
+
property,
|
|
22
|
+
content
|
|
23
|
+
})), {
|
|
24
|
+
name: "twitter:card",
|
|
25
|
+
content: "summary"
|
|
26
|
+
}],
|
|
27
|
+
jsonLd: head.jsonLd === null ? [] : [head.jsonLd]
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
//#endregion
|
|
31
|
+
export { openGraph, toGyralHead };
|
|
32
|
+
|
|
33
|
+
//# sourceMappingURL=head.js.map
|
package/dist/head.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"head.js","names":[],"sources":["../src/head.ts"],"sourcesContent":["// Adapt the catalog's SEO facts to Gyral's shared server/client head model.\nimport type { Head as GyralHead } from '@gyral/core';\nimport { SITE_NAME, type Head } from './seo.ts';\n\n/** Open Graph properties for a head; og:url only for indexable pages. */\nexport function openGraph(head: Head): readonly (readonly [property: string, content: string])[] {\n return [\n ['og:site_name', SITE_NAME],\n ['og:type', head.ogType],\n ['og:title', head.title],\n ['og:description', head.description],\n ...(head.canonical === null ? [] : [['og:url', head.canonical] as const]),\n ];\n}\n\n/** Both SSR and client navigation use this value, including removal of absent metadata. */\nexport function toGyralHead(head: Head): GyralHead {\n return {\n title: head.title,\n description: head.description,\n ...(head.canonical === null ? {} : { canonical: head.canonical }),\n robots: head.robots === 'index' ? 'index, follow' : 'noindex',\n meta: [\n ...openGraph(head).map(([property, content]) => ({ property, content })),\n { name: 'twitter:card', content: 'summary' },\n ],\n jsonLd: head.jsonLd === null ? [] : [head.jsonLd],\n };\n}\n"],"mappings":";;;AAKA,SAAgB,UAAU,MAAuE;CAC/F,OAAO;EACL,CAAC,gBAAgB,SAAS;EAC1B,CAAC,WAAW,KAAK,MAAM;EACvB,CAAC,YAAY,KAAK,KAAK;EACvB,CAAC,kBAAkB,KAAK,WAAW;EACnC,GAAI,KAAK,cAAc,OAAO,CAAC,IAAI,CAAC,CAAC,UAAU,KAAK,SAAS,CAAU;CACzE;AACF;;AAGA,SAAgB,YAAY,MAAuB;CACjD,OAAO;EACL,OAAO,KAAK;EACZ,aAAa,KAAK;EAClB,GAAI,KAAK,cAAc,OAAO,CAAC,IAAI,EAAE,WAAW,KAAK,UAAU;EAC/D,QAAQ,KAAK,WAAW,UAAU,kBAAkB;EACpD,MAAM,CACJ,GAAG,UAAU,IAAI,CAAC,CAAC,KAAK,CAAC,UAAU,cAAc;GAAE;GAAU;EAAQ,EAAE,GACvE;GAAE,MAAM;GAAgB,SAAS;EAAU,CAC7C;EACA,QAAQ,KAAK,WAAW,OAAO,CAAC,IAAI,CAAC,KAAK,MAAM;CAClD;AACF"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export { ADVERTISE_EMAIL, CONTACT_HANDLED_RETENTION_DAYS, CONTACT_HONEYPOT, CONTACT_INTENT, CONTACT_LIMITS, CONTACT_RETENTION_MONTHS, CREATIVE_SPECS, REPLY_BUSINESS_DAYS, SLOT_NAMES, type ContactFormState, } from './advertise.js';
|
|
2
|
+
export { CATEGORIES, findCategory, type CategoryDefinition } from './categories.js';
|
|
3
|
+
export { toGyralHead, openGraph } from './head.js';
|
|
4
|
+
export { MODE_LABELS, STATUS_LABELS, playersLabel } from './labels.js';
|
|
5
|
+
export { PLACEHOLDER_ENTRIES } from './placeholders.js';
|
|
6
|
+
export { RegistryError, createRegistry, defineEntry, type CatalogEntry, type Listing, type Registry, } from './registry.js';
|
|
7
|
+
export { ADVERTISE_PATH, ADVERTISE_POLICY_PATH, ADVERTISE_SENT_PATH, categoryPath, gamePath, pageFor, pathOf, samePage, site, type Page, } from './routes.js';
|
|
8
|
+
export { SITE_NAME, headFor, pageTitleOf, playModes, type Head, type JsonLd } from './seo.js';
|
|
9
|
+
export { SLOTS, SLOT_IDS, isSlotId, slot, type SlotDefinition, type SlotId, type SlotSize, type SlotSurface, type TargetingKey, } from '@joystyk/ui/slots';
|
|
10
|
+
export { CatalogStateSchema, emptyCatalog, type CatalogState } from './store.js';
|
|
11
|
+
export { CategoryInfoSchema, GAME_STATUSES, GameDetailSchema, GameSummaryListSchema, GameSummarySchema, MODES, type CategoryInfo, type GameDetail, type GameStatus, type GameSummary, } from './wire.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { ADVERTISE_EMAIL, CONTACT_HANDLED_RETENTION_DAYS, CONTACT_HONEYPOT, CONTACT_INTENT, CONTACT_LIMITS, CONTACT_RETENTION_MONTHS, CREATIVE_SPECS, REPLY_BUSINESS_DAYS, SLOT_NAMES } from "./advertise.js";
|
|
2
|
+
import { CATEGORIES, findCategory } from "./categories.js";
|
|
3
|
+
import { MODE_LABELS, STATUS_LABELS, playersLabel } from "./labels.js";
|
|
4
|
+
import { ADVERTISE_PATH, ADVERTISE_POLICY_PATH, ADVERTISE_SENT_PATH, categoryPath, gamePath, pageFor, pathOf, samePage, site } from "./routes.js";
|
|
5
|
+
import { SITE_NAME, headFor, pageTitleOf, playModes } from "./seo.js";
|
|
6
|
+
import { openGraph, toGyralHead } from "./head.js";
|
|
7
|
+
import { CategoryInfoSchema, GAME_STATUSES, GameDetailSchema, GameSummaryListSchema, GameSummarySchema, MODES } from "./wire.js";
|
|
8
|
+
import { CatalogStateSchema, emptyCatalog } from "./store.js";
|
|
9
|
+
import { PLACEHOLDER_ENTRIES } from "./placeholders.js";
|
|
10
|
+
import { RegistryError, createRegistry, defineEntry } from "./registry.js";
|
|
11
|
+
import { SLOTS, SLOT_IDS, isSlotId, slot } from "@joystyk/ui/slots";
|
|
12
|
+
export { ADVERTISE_EMAIL, ADVERTISE_PATH, ADVERTISE_POLICY_PATH, ADVERTISE_SENT_PATH, CATEGORIES, CONTACT_HANDLED_RETENTION_DAYS, CONTACT_HONEYPOT, CONTACT_INTENT, CONTACT_LIMITS, CONTACT_RETENTION_MONTHS, CREATIVE_SPECS, CatalogStateSchema, CategoryInfoSchema, GAME_STATUSES, GameDetailSchema, GameSummaryListSchema, GameSummarySchema, MODES, MODE_LABELS, PLACEHOLDER_ENTRIES, REPLY_BUSINESS_DAYS, RegistryError, SITE_NAME, SLOTS, SLOT_IDS, SLOT_NAMES, STATUS_LABELS, categoryPath, createRegistry, defineEntry, emptyCatalog, findCategory, gamePath, headFor, isSlotId, openGraph, pageFor, pageTitleOf, pathOf, playModes, playersLabel, samePage, site, slot, toGyralHead };
|
package/dist/labels.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Mode } from '@joystyk/contracts';
|
|
2
|
+
import type { GameStatus, GameSummary } from './wire.js';
|
|
3
|
+
export declare const MODE_LABELS: Readonly<Record<Mode, string>>;
|
|
4
|
+
export declare const STATUS_LABELS: Readonly<Record<GameStatus, string>>;
|
|
5
|
+
/** "1 player", "2 players", "2–8 players". */
|
|
6
|
+
export declare function playersLabel(players: GameSummary['players']): string;
|
|
7
|
+
/** "2–8", "1": the compact form for table cells. */
|
|
8
|
+
export declare const playersRange: ({ min, max }: GameSummary["players"]) => string;
|
|
9
|
+
/** A two-digit rank for the lineup table: 1 → "01". */
|
|
10
|
+
export declare const rankLabel: (rank: number) => string;
|
package/dist/labels.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
//#region src/labels.ts
|
|
2
|
+
var MODE_LABELS = {
|
|
3
|
+
solo: "Solo",
|
|
4
|
+
ai: "Against the computer",
|
|
5
|
+
"pass-and-play": "Pass and play",
|
|
6
|
+
p2p: "Online, peer to peer",
|
|
7
|
+
server: "Online with friends",
|
|
8
|
+
daily: "Daily puzzle"
|
|
9
|
+
};
|
|
10
|
+
var STATUS_LABELS = {
|
|
11
|
+
"coming-soon": "Coming soon",
|
|
12
|
+
available: "Play now"
|
|
13
|
+
};
|
|
14
|
+
/** "1 player", "2 players", "2–8 players". */
|
|
15
|
+
function playersLabel(players) {
|
|
16
|
+
const { min, max } = players;
|
|
17
|
+
if (min === max) return `${String(min)} ${min === 1 ? "player" : "players"}`;
|
|
18
|
+
return `${String(min)}–${String(max)} players`;
|
|
19
|
+
}
|
|
20
|
+
/** "2–8", "1": the compact form for table cells. */
|
|
21
|
+
var playersRange = ({ min, max }) => min === max ? String(min) : `${String(min)}–${String(max)}`;
|
|
22
|
+
/** A two-digit rank for the lineup table: 1 → "01". */
|
|
23
|
+
var rankLabel = (rank) => String(rank).padStart(2, "0");
|
|
24
|
+
//#endregion
|
|
25
|
+
export { MODE_LABELS, STATUS_LABELS, playersLabel, playersRange, rankLabel };
|
|
26
|
+
|
|
27
|
+
//# sourceMappingURL=labels.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"labels.js","names":[],"sources":["../src/labels.ts"],"sourcesContent":["// Words the pages use for manifest values. Pure and shared by the views, the head and tests.\nimport type { Mode } from '@joystyk/contracts';\nimport type { GameStatus, GameSummary } from './wire.ts';\n\nexport const MODE_LABELS: Readonly<Record<Mode, string>> = {\n solo: 'Solo',\n ai: 'Against the computer',\n 'pass-and-play': 'Pass and play',\n // The launcher's words (packages/launcher/src/model.ts): server rooms are how friends play online in v1 (D-48).\n p2p: 'Online, peer to peer',\n server: 'Online with friends',\n daily: 'Daily puzzle',\n};\n\nexport const STATUS_LABELS: Readonly<Record<GameStatus, string>> = {\n 'coming-soon': 'Coming soon',\n available: 'Play now',\n};\n\n/** \"1 player\", \"2 players\", \"2–8 players\". */\nexport function playersLabel(players: GameSummary['players']): string {\n const { min, max } = players;\n if (min === max) return `${String(min)} ${min === 1 ? 'player' : 'players'}`;\n return `${String(min)}–${String(max)} players`;\n}\n\n/** \"2–8\", \"1\": the compact form for table cells. */\nexport const playersRange = ({ min, max }: GameSummary['players']): string =>\n min === max ? String(min) : `${String(min)}–${String(max)}`;\n\n/** A two-digit rank for the lineup table: 1 → \"01\". */\nexport const rankLabel = (rank: number): string => String(rank).padStart(2, '0');\n"],"mappings":";AAIA,IAAa,cAA8C;CACzD,MAAM;CACN,IAAI;CACJ,iBAAiB;CAEjB,KAAK;CACL,QAAQ;CACR,OAAO;AACT;AAEA,IAAa,gBAAsD;CACjE,eAAe;CACf,WAAW;AACb;;AAGA,SAAgB,aAAa,SAAyC;CACpE,MAAM,EAAE,KAAK,QAAQ;CACrB,IAAI,QAAQ,KAAK,OAAO,GAAG,OAAO,GAAG,EAAE,GAAG,QAAQ,IAAI,WAAW;CACjE,OAAO,GAAG,OAAO,GAAG,EAAE,GAAG,OAAO,GAAG,EAAE;AACvC;;AAGA,IAAa,gBAAgB,EAAE,KAAK,UAClC,QAAQ,MAAM,OAAO,GAAG,IAAI,GAAG,OAAO,GAAG,EAAE,GAAG,OAAO,GAAG;;AAG1D,IAAa,aAAa,SAAyB,OAAO,IAAI,CAAC,CAAC,SAAS,GAAG,GAAG"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"placeholders.js","names":[],"sources":["../src/placeholders.ts"],"sourcesContent":["// PLACEHOLDER ENTRIES: games whose page should exist before their code does.\n//\n// Each entry uses the game's real id, name and category from its rules spec\n// (docs/product-specs/games/), so URLs stay stable, but the versions are 0.0.0, the asset\n// lists don't exist, and the copy is a short summary rather than the shipped rules. When a\n// game lands in games/<id>/, its entry.ts exports the real manifest and the site registers\n// that instead; delete the matching entry here in the same change. Shift 23, the last one,\n// landed in games/shift-23.\nimport type { CatalogEntry } from './registry.ts';\n\n/** Every placeholder, in the order the site registers them. */\nexport const PLACEHOLDER_ENTRIES: readonly CatalogEntry[] = [];\n"],"mappings":";;AAWA,IAAa,sBAA+C,CAAC"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { type GameManifest } from '@joystyk/contracts';
|
|
2
|
+
import { type CategoryDefinition } from './categories.js';
|
|
3
|
+
import { type CategoryInfo, type GameDetail, type GameStatus, type GameSummary } from './wire.js';
|
|
4
|
+
/** What a game's pages say about it, beyond the manifest. */
|
|
5
|
+
export interface Listing {
|
|
6
|
+
readonly status: GameStatus;
|
|
7
|
+
/**
|
|
8
|
+
* True for an entry written before its game exists. Placeholders keep the real id, name and
|
|
9
|
+
* category, so URLs stay stable when the game's own `entry.ts` replaces the entry.
|
|
10
|
+
*/
|
|
11
|
+
readonly placeholder: boolean;
|
|
12
|
+
/** Shown in the index's Featured section. */
|
|
13
|
+
readonly featured: boolean;
|
|
14
|
+
/** One or two paragraphs for the game page. */
|
|
15
|
+
readonly description: string;
|
|
16
|
+
/** A short rules summary, one step per item. The full rules ship with the game. */
|
|
17
|
+
readonly rules: readonly string[];
|
|
18
|
+
}
|
|
19
|
+
export interface CatalogEntry {
|
|
20
|
+
readonly manifest: GameManifest;
|
|
21
|
+
readonly listing: Listing;
|
|
22
|
+
}
|
|
23
|
+
/** Returns the entry unchanged; a typed spot for a game to declare its catalog entry. */
|
|
24
|
+
export declare const defineEntry: <const E extends CatalogEntry>(entry: E) => E;
|
|
25
|
+
export interface Registry {
|
|
26
|
+
/** Every entry, sorted by name. */
|
|
27
|
+
readonly entries: readonly CatalogEntry[];
|
|
28
|
+
readonly find: (id: string) => CatalogEntry | undefined;
|
|
29
|
+
readonly summaries: () => readonly GameSummary[];
|
|
30
|
+
readonly summary: (id: string) => GameSummary | undefined;
|
|
31
|
+
readonly detail: (id: string) => GameDetail | undefined;
|
|
32
|
+
readonly featured: () => readonly GameSummary[];
|
|
33
|
+
/** Categories with at least one game, in the order CATEGORIES lists them. */
|
|
34
|
+
readonly categories: () => readonly CategoryInfo[];
|
|
35
|
+
readonly inCategory: (categoryId: string) => readonly GameSummary[];
|
|
36
|
+
/** Game ids per tag, tags sorted alphabetically. */
|
|
37
|
+
readonly tags: () => ReadonlyMap<string, readonly string[]>;
|
|
38
|
+
}
|
|
39
|
+
export declare class RegistryError extends Error {
|
|
40
|
+
readonly name = "RegistryError";
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Builds the registry, or throws a RegistryError listing every invalid entry and duplicate
|
|
44
|
+
* id. Call it once at startup so a bad manifest stops the deploy, not a page view.
|
|
45
|
+
*/
|
|
46
|
+
export declare function createRegistry(entries: readonly CatalogEntry[], categories?: readonly CategoryDefinition[]): Registry;
|
package/dist/registry.js
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { CATEGORIES } from "./categories.js";
|
|
2
|
+
import { GameDetailSchema } from "./wire.js";
|
|
3
|
+
import { isGameId, isGameOptions } from "@joystyk/contracts";
|
|
4
|
+
//#region src/registry.ts
|
|
5
|
+
/** Returns the entry unchanged; a typed spot for a game to declare its catalog entry. */
|
|
6
|
+
var defineEntry = (entry) => entry;
|
|
7
|
+
var RegistryError = class extends Error {
|
|
8
|
+
name = "RegistryError";
|
|
9
|
+
};
|
|
10
|
+
/** Every problem with one entry, as sentences that name the fix. */
|
|
11
|
+
function problems(entry, known, categories) {
|
|
12
|
+
const { manifest: m, listing } = entry;
|
|
13
|
+
const found = [];
|
|
14
|
+
if (!isGameId(m.id)) found.push(`id "${m.id}" must be kebab-case, e.g. "shift-23".`);
|
|
15
|
+
if (m.name.trim() === "") found.push("name is empty.");
|
|
16
|
+
if (m.pageTitle !== void 0 && !m.pageTitle.includes(m.name)) found.push(`pageTitle "${m.pageTitle}" must contain the name "${m.name}", so the page heading matches the cards that link to it; leave it out to use the name.`);
|
|
17
|
+
if (m.summary.trim() === "") found.push("summary is empty; it is the card text and meta description.");
|
|
18
|
+
if (known(m.category) === void 0) found.push(`category "${m.category}" is not in CATEGORIES (packages/catalog/src/categories.ts); add it there or use one of ${categories.map((c) => c.id).join(", ")}.`);
|
|
19
|
+
if (m.modes.length === 0) found.push("modes is empty; list at least one way to play.");
|
|
20
|
+
if (!Number.isInteger(m.players.min) || m.players.min < 1 || m.players.max < m.players.min) found.push(`players { min: ${String(m.players.min)}, max: ${String(m.players.max)} } must be whole numbers with 1 <= min <= max.`);
|
|
21
|
+
const preset = m.players.default;
|
|
22
|
+
if (preset !== void 0 && (!Number.isInteger(preset) || preset < m.players.min || preset > m.players.max)) found.push(`players.default ${String(preset)} must be a whole number from min (${String(m.players.min)}) to max (${String(m.players.max)}), or left out.`);
|
|
23
|
+
if (m.options !== void 0 && !isGameOptions(m.options)) found.push("options must have unique keys, valid choices and matching defaults.");
|
|
24
|
+
if (m.attribution !== void 0 && !GameDetailSchema.shape.attribution.safeParse(m.attribution).success) found.push("attribution needs nonempty text (up to 500 characters) and an HTTPS link without credentials.");
|
|
25
|
+
if (listing.description.trim() === "") found.push("listing.description is empty.");
|
|
26
|
+
if (listing.rules.length === 0) found.push("listing.rules is empty; give at least a one-line rules summary.");
|
|
27
|
+
return found.map((p) => `${m.id}: ${p}`);
|
|
28
|
+
}
|
|
29
|
+
var toSummary = ({ manifest: m, listing: l }) => ({
|
|
30
|
+
id: m.id,
|
|
31
|
+
name: m.name,
|
|
32
|
+
summary: m.summary,
|
|
33
|
+
category: m.category,
|
|
34
|
+
tags: [...m.tags],
|
|
35
|
+
modes: [...m.modes],
|
|
36
|
+
players: m.players.default === void 0 ? {
|
|
37
|
+
min: m.players.min,
|
|
38
|
+
max: m.players.max
|
|
39
|
+
} : {
|
|
40
|
+
min: m.players.min,
|
|
41
|
+
max: m.players.max,
|
|
42
|
+
default: m.players.default
|
|
43
|
+
},
|
|
44
|
+
status: l.status,
|
|
45
|
+
placeholder: l.placeholder,
|
|
46
|
+
featured: l.featured
|
|
47
|
+
});
|
|
48
|
+
var toDetail = (entry) => ({
|
|
49
|
+
...toSummary(entry),
|
|
50
|
+
description: entry.listing.description,
|
|
51
|
+
...entry.manifest.pageTitle === void 0 ? {} : { pageTitle: entry.manifest.pageTitle },
|
|
52
|
+
rules: [...entry.listing.rules],
|
|
53
|
+
aiLevels: entry.manifest.ai === null ? [] : [...entry.manifest.ai.levels],
|
|
54
|
+
essentialMotion: entry.manifest.essentialMotion,
|
|
55
|
+
aiContent: entry.manifest.aiContent === true,
|
|
56
|
+
assets: entry.manifest.assets,
|
|
57
|
+
...entry.manifest.options === void 0 ? {} : { options: [...entry.manifest.options] },
|
|
58
|
+
...entry.manifest.attribution === void 0 ? {} : { attribution: entry.manifest.attribution }
|
|
59
|
+
});
|
|
60
|
+
/**
|
|
61
|
+
* Builds the registry, or throws a RegistryError listing every invalid entry and duplicate
|
|
62
|
+
* id. Call it once at startup so a bad manifest stops the deploy, not a page view.
|
|
63
|
+
*/
|
|
64
|
+
function createRegistry(entries, categories = CATEGORIES) {
|
|
65
|
+
const known = (id) => categories.find((c) => c.id === id);
|
|
66
|
+
const found = entries.flatMap((e) => problems(e, known, categories));
|
|
67
|
+
const seen = /* @__PURE__ */ new Set();
|
|
68
|
+
for (const { manifest } of entries) {
|
|
69
|
+
if (seen.has(manifest.id)) found.push(`${manifest.id}: two entries share this id; ids are unique and stable forever.`);
|
|
70
|
+
seen.add(manifest.id);
|
|
71
|
+
}
|
|
72
|
+
if (found.length > 0) throw new RegistryError(`Invalid catalog entries:\n- ${found.join("\n- ")}`);
|
|
73
|
+
const sorted = [...entries].sort((a, b) => a.manifest.name.localeCompare(b.manifest.name, "en"));
|
|
74
|
+
const byId = new Map(sorted.map((e) => [e.manifest.id, e]));
|
|
75
|
+
const summaries = sorted.map(toSummary);
|
|
76
|
+
const summaryById = new Map(summaries.map((s) => [s.id, s]));
|
|
77
|
+
const details = new Map(sorted.map((e) => [e.manifest.id, toDetail(e)]));
|
|
78
|
+
const infos = categories.flatMap((c) => {
|
|
79
|
+
const games = summaries.filter((s) => s.category === c.id).length;
|
|
80
|
+
return games === 0 ? [] : [{
|
|
81
|
+
id: c.id,
|
|
82
|
+
name: c.name,
|
|
83
|
+
noun: c.noun,
|
|
84
|
+
description: c.description,
|
|
85
|
+
games
|
|
86
|
+
}];
|
|
87
|
+
});
|
|
88
|
+
const tags = /* @__PURE__ */ new Map();
|
|
89
|
+
for (const s of summaries) for (const tag of s.tags) tags.set(tag, [...tags.get(tag) ?? [], s.id]);
|
|
90
|
+
const sortedTags = new Map([...tags].sort(([a], [b]) => a.localeCompare(b, "en")));
|
|
91
|
+
return {
|
|
92
|
+
entries: sorted,
|
|
93
|
+
find: (id) => byId.get(id),
|
|
94
|
+
summaries: () => summaries,
|
|
95
|
+
summary: (id) => summaryById.get(id),
|
|
96
|
+
detail: (id) => details.get(id),
|
|
97
|
+
featured: () => summaries.filter((s) => s.featured),
|
|
98
|
+
categories: () => infos,
|
|
99
|
+
inCategory: (categoryId) => summaries.filter((s) => s.category === categoryId),
|
|
100
|
+
tags: () => sortedTags
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
//#endregion
|
|
104
|
+
export { RegistryError, createRegistry, defineEntry };
|
|
105
|
+
|
|
106
|
+
//# sourceMappingURL=registry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"registry.js","names":[],"sources":["../src/registry.ts"],"sourcesContent":["// The manifest registry: every game the site lists, as static data. A game registers its\n// GameManifest (plain JSON, so the index renders without loading game code) together with\n// the listing copy its pages show. The registry checks the whole set once, at startup, and\n// answers the grouping questions the pages ask. It is server-side data: pages receive what\n// they show through the store seed and /api/games.\nimport { isGameId, isGameOptions, type GameManifest } from '@joystyk/contracts';\nimport { CATEGORIES, type CategoryDefinition } from './categories.ts';\nimport { GameDetailSchema, type CategoryInfo, type GameDetail, type GameStatus, type GameSummary } from './wire.ts';\n\n/** What a game's pages say about it, beyond the manifest. */\nexport interface Listing {\n readonly status: GameStatus;\n /**\n * True for an entry written before its game exists. Placeholders keep the real id, name and\n * category, so URLs stay stable when the game's own `entry.ts` replaces the entry.\n */\n readonly placeholder: boolean;\n /** Shown in the index's Featured section. */\n readonly featured: boolean;\n /** One or two paragraphs for the game page. */\n readonly description: string;\n /** A short rules summary, one step per item. The full rules ship with the game. */\n readonly rules: readonly string[];\n}\n\nexport interface CatalogEntry {\n readonly manifest: GameManifest;\n readonly listing: Listing;\n}\n\n/** Returns the entry unchanged; a typed spot for a game to declare its catalog entry. */\nexport const defineEntry = <const E extends CatalogEntry>(entry: E): E => entry;\n\nexport interface Registry {\n /** Every entry, sorted by name. */\n readonly entries: readonly CatalogEntry[];\n readonly find: (id: string) => CatalogEntry | undefined;\n readonly summaries: () => readonly GameSummary[];\n readonly summary: (id: string) => GameSummary | undefined;\n readonly detail: (id: string) => GameDetail | undefined;\n readonly featured: () => readonly GameSummary[];\n /** Categories with at least one game, in the order CATEGORIES lists them. */\n readonly categories: () => readonly CategoryInfo[];\n readonly inCategory: (categoryId: string) => readonly GameSummary[];\n /** Game ids per tag, tags sorted alphabetically. */\n readonly tags: () => ReadonlyMap<string, readonly string[]>;\n}\n\nexport class RegistryError extends Error {\n override readonly name = 'RegistryError';\n}\n\n/** Every problem with one entry, as sentences that name the fix. */\nfunction problems(\n entry: CatalogEntry,\n known: (id: string) => CategoryDefinition | undefined,\n categories: readonly CategoryDefinition[],\n): string[] {\n const { manifest: m, listing } = entry;\n const found: string[] = [];\n if (!isGameId(m.id)) found.push(`id \"${m.id}\" must be kebab-case, e.g. \"shift-23\".`);\n if (m.name.trim() === '') found.push('name is empty.');\n if (m.pageTitle !== undefined && !m.pageTitle.includes(m.name)) {\n found.push(\n `pageTitle \"${m.pageTitle}\" must contain the name \"${m.name}\", so the page heading matches the cards that link to it; leave it out to use the name.`,\n );\n }\n if (m.summary.trim() === '') found.push('summary is empty; it is the card text and meta description.');\n if (known(m.category) === undefined) {\n found.push(\n `category \"${m.category}\" is not in CATEGORIES (packages/catalog/src/categories.ts); add it there or use one of ${categories.map((c) => c.id).join(', ')}.`,\n );\n }\n if (m.modes.length === 0) found.push('modes is empty; list at least one way to play.');\n if (!Number.isInteger(m.players.min) || m.players.min < 1 || m.players.max < m.players.min) {\n found.push(\n `players { min: ${String(m.players.min)}, max: ${String(m.players.max)} } must be whole numbers with 1 <= min <= max.`,\n );\n }\n const preset = m.players.default;\n if (preset !== undefined && (!Number.isInteger(preset) || preset < m.players.min || preset > m.players.max)) {\n found.push(\n `players.default ${String(preset)} must be a whole number from min (${String(m.players.min)}) to max (${String(m.players.max)}), or left out.`,\n );\n }\n if (m.options !== undefined && !isGameOptions(m.options))\n found.push('options must have unique keys, valid choices and matching defaults.');\n if (m.attribution !== undefined && !GameDetailSchema.shape.attribution.safeParse(m.attribution).success)\n found.push('attribution needs nonempty text (up to 500 characters) and an HTTPS link without credentials.');\n if (listing.description.trim() === '') found.push('listing.description is empty.');\n if (listing.rules.length === 0) found.push('listing.rules is empty; give at least a one-line rules summary.');\n return found.map((p) => `${m.id}: ${p}`);\n}\n\nconst toSummary = ({ manifest: m, listing: l }: CatalogEntry): GameSummary => ({\n id: m.id,\n name: m.name,\n summary: m.summary,\n category: m.category,\n tags: [...m.tags],\n modes: [...m.modes],\n players:\n m.players.default === undefined\n ? { min: m.players.min, max: m.players.max }\n : { min: m.players.min, max: m.players.max, default: m.players.default },\n status: l.status,\n placeholder: l.placeholder,\n featured: l.featured,\n});\n\nconst toDetail = (entry: CatalogEntry): GameDetail => ({\n ...toSummary(entry),\n description: entry.listing.description,\n ...(entry.manifest.pageTitle === undefined ? {} : { pageTitle: entry.manifest.pageTitle }),\n rules: [...entry.listing.rules],\n aiLevels: entry.manifest.ai === null ? [] : [...entry.manifest.ai.levels],\n essentialMotion: entry.manifest.essentialMotion,\n aiContent: entry.manifest.aiContent === true,\n assets: entry.manifest.assets,\n ...(entry.manifest.options === undefined ? {} : { options: [...entry.manifest.options] }),\n ...(entry.manifest.attribution === undefined ? {} : { attribution: entry.manifest.attribution }),\n});\n\n/**\n * Builds the registry, or throws a RegistryError listing every invalid entry and duplicate\n * id. Call it once at startup so a bad manifest stops the deploy, not a page view.\n */\nexport function createRegistry(\n entries: readonly CatalogEntry[],\n categories: readonly CategoryDefinition[] = CATEGORIES,\n): Registry {\n const known = (id: string) => categories.find((c) => c.id === id);\n const found = entries.flatMap((e) => problems(e, known, categories));\n const seen = new Set<string>();\n for (const { manifest } of entries) {\n if (seen.has(manifest.id))\n found.push(`${manifest.id}: two entries share this id; ids are unique and stable forever.`);\n seen.add(manifest.id);\n }\n if (found.length > 0) throw new RegistryError(`Invalid catalog entries:\\n- ${found.join('\\n- ')}`);\n\n const sorted = [...entries].sort((a, b) => a.manifest.name.localeCompare(b.manifest.name, 'en'));\n const byId = new Map(sorted.map((e) => [e.manifest.id, e]));\n const summaries = sorted.map(toSummary);\n const summaryById = new Map(summaries.map((s) => [s.id, s]));\n const details = new Map(sorted.map((e) => [e.manifest.id, toDetail(e)]));\n const infos: readonly CategoryInfo[] = categories.flatMap((c) => {\n const games = summaries.filter((s) => s.category === c.id).length;\n return games === 0 ? [] : [{ id: c.id, name: c.name, noun: c.noun, description: c.description, games }];\n });\n const tags = new Map<string, string[]>();\n for (const s of summaries) {\n for (const tag of s.tags) tags.set(tag, [...(tags.get(tag) ?? []), s.id]);\n }\n const sortedTags: ReadonlyMap<string, readonly string[]> = new Map(\n [...tags].sort(([a], [b]) => a.localeCompare(b, 'en')),\n );\n\n return {\n entries: sorted,\n find: (id) => byId.get(id),\n summaries: () => summaries,\n summary: (id) => summaryById.get(id),\n detail: (id) => details.get(id),\n featured: () => summaries.filter((s) => s.featured),\n categories: () => infos,\n inCategory: (categoryId) => summaries.filter((s) => s.category === categoryId),\n tags: () => sortedTags,\n };\n}\n"],"mappings":"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"}
|
package/dist/routes.d.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
export declare const site: import("@gyral/router").Routes<{
|
|
2
|
+
readonly home: "/";
|
|
3
|
+
readonly category: "/categories/:id";
|
|
4
|
+
readonly game: "/games/:id";
|
|
5
|
+
readonly licenses: "/licenses";
|
|
6
|
+
readonly advertise: "/advertise";
|
|
7
|
+
readonly advertisePolicy: "/advertise/policy";
|
|
8
|
+
readonly advertiseSent: "/advertise/thanks";
|
|
9
|
+
readonly adClick: "/ads/click/:id";
|
|
10
|
+
}>;
|
|
11
|
+
export type Page = {
|
|
12
|
+
readonly _tag: 'Home';
|
|
13
|
+
} | {
|
|
14
|
+
readonly _tag: 'Category';
|
|
15
|
+
readonly id: string;
|
|
16
|
+
} | {
|
|
17
|
+
readonly _tag: 'Game';
|
|
18
|
+
readonly id: string;
|
|
19
|
+
}
|
|
20
|
+
/** "Advertise with us": the slots, the audience, rates, the rules and the contact form. */
|
|
21
|
+
| {
|
|
22
|
+
readonly _tag: 'Licenses';
|
|
23
|
+
} | {
|
|
24
|
+
readonly _tag: 'Advertise';
|
|
25
|
+
}
|
|
26
|
+
/** The creative policy, for advertisers. */
|
|
27
|
+
| {
|
|
28
|
+
readonly _tag: 'AdvertisePolicy';
|
|
29
|
+
}
|
|
30
|
+
/** Where a sent contact form lands (Post/Redirect/Get). */
|
|
31
|
+
| {
|
|
32
|
+
readonly _tag: 'AdvertiseSent';
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* "You're leaving Joystyk": where an ad click lands (monetization.md, "Counting"). The site's
|
|
36
|
+
* ad routes count the click and render it with the creative's approved link; `id` is the
|
|
37
|
+
* creative's.
|
|
38
|
+
*/
|
|
39
|
+
| {
|
|
40
|
+
readonly _tag: 'LeavingSite';
|
|
41
|
+
readonly id: string;
|
|
42
|
+
} | {
|
|
43
|
+
readonly _tag: 'NotFound';
|
|
44
|
+
readonly path: string;
|
|
45
|
+
};
|
|
46
|
+
export declare function pageFor(path: string): Page;
|
|
47
|
+
export declare const samePage: (a: Page, b: Page) => boolean;
|
|
48
|
+
/** The one URL path for a page. The router ignores trailing slashes; the server redirects. */
|
|
49
|
+
export declare function pathOf(page: Page): string;
|
|
50
|
+
export declare const gamePath: (id: string) => string;
|
|
51
|
+
export declare const categoryPath: (id: string) => string;
|
|
52
|
+
/** The "Advertise with us" page, where the contact form posts. */
|
|
53
|
+
export declare const ADVERTISE_PATH: string;
|
|
54
|
+
export declare const ADVERTISE_POLICY_PATH: string;
|
|
55
|
+
/** Where the contact form's handler sends a visitor after a post. */
|
|
56
|
+
export declare const ADVERTISE_SENT_PATH: string;
|
package/dist/routes.js
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { routes } from "@gyral/router";
|
|
2
|
+
import { isGameId } from "@joystyk/contracts";
|
|
3
|
+
//#region src/routes.ts
|
|
4
|
+
var site = routes({
|
|
5
|
+
home: "/",
|
|
6
|
+
category: "/categories/:id",
|
|
7
|
+
game: "/games/:id",
|
|
8
|
+
licenses: "/licenses",
|
|
9
|
+
advertise: "/advertise",
|
|
10
|
+
advertisePolicy: "/advertise/policy",
|
|
11
|
+
advertiseSent: "/advertise/thanks",
|
|
12
|
+
adClick: "/ads/click/:id"
|
|
13
|
+
});
|
|
14
|
+
/** A creative id in a click URL: a positive integer, as the ad routes accept it. */
|
|
15
|
+
var CREATIVE_ID = /^[1-9]\d{0,15}$/;
|
|
16
|
+
var BASE = "http://joystyk.invalid";
|
|
17
|
+
function pageFor(path) {
|
|
18
|
+
const match = site.match(new URL(path, BASE));
|
|
19
|
+
if (match === void 0) return {
|
|
20
|
+
_tag: "NotFound",
|
|
21
|
+
path
|
|
22
|
+
};
|
|
23
|
+
switch (match.name) {
|
|
24
|
+
case "home": return { _tag: "Home" };
|
|
25
|
+
case "category": return isGameId(match.params.id) ? {
|
|
26
|
+
_tag: "Category",
|
|
27
|
+
id: match.params.id
|
|
28
|
+
} : {
|
|
29
|
+
_tag: "NotFound",
|
|
30
|
+
path
|
|
31
|
+
};
|
|
32
|
+
case "game": return isGameId(match.params.id) ? {
|
|
33
|
+
_tag: "Game",
|
|
34
|
+
id: match.params.id
|
|
35
|
+
} : {
|
|
36
|
+
_tag: "NotFound",
|
|
37
|
+
path
|
|
38
|
+
};
|
|
39
|
+
case "licenses": return { _tag: "Licenses" };
|
|
40
|
+
case "advertise": return { _tag: "Advertise" };
|
|
41
|
+
case "advertisePolicy": return { _tag: "AdvertisePolicy" };
|
|
42
|
+
case "advertiseSent": return { _tag: "AdvertiseSent" };
|
|
43
|
+
case "adClick": return CREATIVE_ID.test(match.params.id) ? {
|
|
44
|
+
_tag: "LeavingSite",
|
|
45
|
+
id: match.params.id
|
|
46
|
+
} : {
|
|
47
|
+
_tag: "NotFound",
|
|
48
|
+
path
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
var samePage = (a, b) => {
|
|
53
|
+
switch (a._tag) {
|
|
54
|
+
case "Home": return b._tag === "Home";
|
|
55
|
+
case "Category": return b._tag === "Category" && a.id === b.id;
|
|
56
|
+
case "Game": return b._tag === "Game" && a.id === b.id;
|
|
57
|
+
case "LeavingSite": return b._tag === "LeavingSite" && a.id === b.id;
|
|
58
|
+
case "Advertise":
|
|
59
|
+
case "AdvertisePolicy":
|
|
60
|
+
case "AdvertiseSent":
|
|
61
|
+
case "Licenses": return b._tag === a._tag;
|
|
62
|
+
case "NotFound": return b._tag === "NotFound" && a.path === b.path;
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
/** The one URL path for a page. The router ignores trailing slashes; the server redirects. */
|
|
66
|
+
function pathOf(page) {
|
|
67
|
+
switch (page._tag) {
|
|
68
|
+
case "Home": return site.href("home", {});
|
|
69
|
+
case "Category": return site.href("category", { id: page.id });
|
|
70
|
+
case "Game": return site.href("game", { id: page.id });
|
|
71
|
+
case "Licenses": return site.href("licenses", {});
|
|
72
|
+
case "Advertise": return site.href("advertise", {});
|
|
73
|
+
case "AdvertisePolicy": return site.href("advertisePolicy", {});
|
|
74
|
+
case "AdvertiseSent": return site.href("advertiseSent", {});
|
|
75
|
+
case "LeavingSite": return site.href("adClick", { id: page.id });
|
|
76
|
+
case "NotFound": return page.path;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
var gamePath = (id) => site.href("game", { id });
|
|
80
|
+
var categoryPath = (id) => site.href("category", { id });
|
|
81
|
+
/** The "Advertise with us" page, where the contact form posts. */
|
|
82
|
+
var ADVERTISE_PATH = site.href("advertise", {});
|
|
83
|
+
var ADVERTISE_POLICY_PATH = site.href("advertisePolicy", {});
|
|
84
|
+
/** Where the contact form's handler sends a visitor after a post. */
|
|
85
|
+
var ADVERTISE_SENT_PATH = site.href("advertiseSent", {});
|
|
86
|
+
//#endregion
|
|
87
|
+
export { ADVERTISE_PATH, ADVERTISE_POLICY_PATH, ADVERTISE_SENT_PATH, categoryPath, gamePath, pageFor, pathOf, samePage, site };
|
|
88
|
+
|
|
89
|
+
//# sourceMappingURL=routes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"routes.js","names":[],"sources":["../src/routes.ts"],"sourcesContent":["// One route table for server and client. `pageFor` turns a URL path into the page the shell\n// shows; the server calls it to pick a status and to redirect, and the shell calls it on\n// every navigation. `pathOf` gives each page its one canonical path.\nimport { isGameId } from '@joystyk/contracts';\nimport { routes } from '@gyral/router';\n\nexport const site = routes({\n home: '/',\n category: '/categories/:id',\n game: '/games/:id',\n licenses: '/licenses',\n advertise: '/advertise',\n advertisePolicy: '/advertise/policy',\n advertiseSent: '/advertise/thanks',\n adClick: '/ads/click/:id',\n});\n\nexport type Page =\n | { readonly _tag: 'Home' }\n | { readonly _tag: 'Category'; readonly id: string }\n | { readonly _tag: 'Game'; readonly id: string }\n /** \"Advertise with us\": the slots, the audience, rates, the rules and the contact form. */\n | { readonly _tag: 'Licenses' }\n | { readonly _tag: 'Advertise' }\n /** The creative policy, for advertisers. */\n | { readonly _tag: 'AdvertisePolicy' }\n /** Where a sent contact form lands (Post/Redirect/Get). */\n | { readonly _tag: 'AdvertiseSent' }\n /**\n * \"You're leaving Joystyk\": where an ad click lands (monetization.md, \"Counting\"). The site's\n * ad routes count the click and render it with the creative's approved link; `id` is the\n * creative's.\n */\n | { readonly _tag: 'LeavingSite'; readonly id: string }\n | { readonly _tag: 'NotFound'; readonly path: string };\n\n/** A creative id in a click URL: a positive integer, as the ad routes accept it. */\nconst CREATIVE_ID = /^[1-9]\\d{0,15}$/;\n\nconst BASE = 'http://joystyk.invalid';\n\nexport function pageFor(path: string): Page {\n const match = site.match(new URL(path, BASE));\n if (match === undefined) return { _tag: 'NotFound', path };\n switch (match.name) {\n case 'home':\n return { _tag: 'Home' };\n case 'category':\n // Category ids share the game id's kebab-case shape.\n return isGameId(match.params.id) ? { _tag: 'Category', id: match.params.id } : { _tag: 'NotFound', path };\n case 'game':\n return isGameId(match.params.id) ? { _tag: 'Game', id: match.params.id } : { _tag: 'NotFound', path };\n case 'licenses':\n return { _tag: 'Licenses' };\n case 'advertise':\n return { _tag: 'Advertise' };\n case 'advertisePolicy':\n return { _tag: 'AdvertisePolicy' };\n case 'advertiseSent':\n return { _tag: 'AdvertiseSent' };\n case 'adClick':\n return CREATIVE_ID.test(match.params.id)\n ? { _tag: 'LeavingSite', id: match.params.id }\n : { _tag: 'NotFound', path };\n }\n}\n\nexport const samePage = (a: Page, b: Page): boolean => {\n switch (a._tag) {\n case 'Home':\n return b._tag === 'Home';\n case 'Category':\n return b._tag === 'Category' && a.id === b.id;\n case 'Game':\n return b._tag === 'Game' && a.id === b.id;\n case 'LeavingSite':\n return b._tag === 'LeavingSite' && a.id === b.id;\n case 'Advertise':\n case 'AdvertisePolicy':\n case 'AdvertiseSent':\n case 'Licenses':\n return b._tag === a._tag;\n case 'NotFound':\n return b._tag === 'NotFound' && a.path === b.path;\n }\n};\n\n/** The one URL path for a page. The router ignores trailing slashes; the server redirects. */\nexport function pathOf(page: Page): string {\n switch (page._tag) {\n case 'Home':\n return site.href('home', {});\n case 'Category':\n return site.href('category', { id: page.id });\n case 'Game':\n return site.href('game', { id: page.id });\n case 'Licenses':\n return site.href('licenses', {});\n case 'Advertise':\n return site.href('advertise', {});\n case 'AdvertisePolicy':\n return site.href('advertisePolicy', {});\n case 'AdvertiseSent':\n return site.href('advertiseSent', {});\n case 'LeavingSite':\n return site.href('adClick', { id: page.id });\n case 'NotFound':\n return page.path;\n }\n}\n\nexport const gamePath = (id: string): string => site.href('game', { id });\nexport const categoryPath = (id: string): string => site.href('category', { id });\n\n/** The \"Advertise with us\" page, where the contact form posts. */\nexport const ADVERTISE_PATH = site.href('advertise', {});\nexport const ADVERTISE_POLICY_PATH = site.href('advertisePolicy', {});\n/** Where the contact form's handler sends a visitor after a post. */\nexport const ADVERTISE_SENT_PATH = site.href('advertiseSent', {});\n"],"mappings":";;;AAMA,IAAa,OAAO,OAAO;CACzB,MAAM;CACN,UAAU;CACV,MAAM;CACN,UAAU;CACV,WAAW;CACX,iBAAiB;CACjB,eAAe;CACf,SAAS;AACX,CAAC;;AAsBD,IAAM,cAAc;AAEpB,IAAM,OAAO;AAEb,SAAgB,QAAQ,MAAoB;CAC1C,MAAM,QAAQ,KAAK,MAAM,IAAI,IAAI,MAAM,IAAI,CAAC;CAC5C,IAAI,UAAU,KAAA,GAAW,OAAO;EAAE,MAAM;EAAY;CAAK;CACzD,QAAQ,MAAM,MAAd;EACE,KAAK,QACH,OAAO,EAAE,MAAM,OAAO;EACxB,KAAK,YAEH,OAAO,SAAS,MAAM,OAAO,EAAE,IAAI;GAAE,MAAM;GAAY,IAAI,MAAM,OAAO;EAAG,IAAI;GAAE,MAAM;GAAY;EAAK;EAC1G,KAAK,QACH,OAAO,SAAS,MAAM,OAAO,EAAE,IAAI;GAAE,MAAM;GAAQ,IAAI,MAAM,OAAO;EAAG,IAAI;GAAE,MAAM;GAAY;EAAK;EACtG,KAAK,YACH,OAAO,EAAE,MAAM,WAAW;EAC5B,KAAK,aACH,OAAO,EAAE,MAAM,YAAY;EAC7B,KAAK,mBACH,OAAO,EAAE,MAAM,kBAAkB;EACnC,KAAK,iBACH,OAAO,EAAE,MAAM,gBAAgB;EACjC,KAAK,WACH,OAAO,YAAY,KAAK,MAAM,OAAO,EAAE,IACnC;GAAE,MAAM;GAAe,IAAI,MAAM,OAAO;EAAG,IAC3C;GAAE,MAAM;GAAY;EAAK;CACjC;AACF;AAEA,IAAa,YAAY,GAAS,MAAqB;CACrD,QAAQ,EAAE,MAAV;EACE,KAAK,QACH,OAAO,EAAE,SAAS;EACpB,KAAK,YACH,OAAO,EAAE,SAAS,cAAc,EAAE,OAAO,EAAE;EAC7C,KAAK,QACH,OAAO,EAAE,SAAS,UAAU,EAAE,OAAO,EAAE;EACzC,KAAK,eACH,OAAO,EAAE,SAAS,iBAAiB,EAAE,OAAO,EAAE;EAChD,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK,YACH,OAAO,EAAE,SAAS,EAAE;EACtB,KAAK,YACH,OAAO,EAAE,SAAS,cAAc,EAAE,SAAS,EAAE;CACjD;AACF;;AAGA,SAAgB,OAAO,MAAoB;CACzC,QAAQ,KAAK,MAAb;EACE,KAAK,QACH,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC;EAC7B,KAAK,YACH,OAAO,KAAK,KAAK,YAAY,EAAE,IAAI,KAAK,GAAG,CAAC;EAC9C,KAAK,QACH,OAAO,KAAK,KAAK,QAAQ,EAAE,IAAI,KAAK,GAAG,CAAC;EAC1C,KAAK,YACH,OAAO,KAAK,KAAK,YAAY,CAAC,CAAC;EACjC,KAAK,aACH,OAAO,KAAK,KAAK,aAAa,CAAC,CAAC;EAClC,KAAK,mBACH,OAAO,KAAK,KAAK,mBAAmB,CAAC,CAAC;EACxC,KAAK,iBACH,OAAO,KAAK,KAAK,iBAAiB,CAAC,CAAC;EACtC,KAAK,eACH,OAAO,KAAK,KAAK,WAAW,EAAE,IAAI,KAAK,GAAG,CAAC;EAC7C,KAAK,YACH,OAAO,KAAK;CAChB;AACF;AAEA,IAAa,YAAY,OAAuB,KAAK,KAAK,QAAQ,EAAE,GAAG,CAAC;AACxE,IAAa,gBAAgB,OAAuB,KAAK,KAAK,YAAY,EAAE,GAAG,CAAC;;AAGhF,IAAa,iBAAiB,KAAK,KAAK,aAAa,CAAC,CAAC;AACvD,IAAa,wBAAwB,KAAK,KAAK,mBAAmB,CAAC,CAAC;;AAEpE,IAAa,sBAAsB,KAAK,KAAK,iBAAiB,CAAC,CAAC"}
|
package/dist/seo.d.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { JsonValue } from '@gyral/core';
|
|
2
|
+
import type { Mode } from '@joystyk/contracts';
|
|
3
|
+
import { type Page } from './routes.js';
|
|
4
|
+
import type { CatalogState } from './store.js';
|
|
5
|
+
import type { CategoryInfo, GameDetail } from './wire.js';
|
|
6
|
+
export declare const SITE_NAME = "Joystyk";
|
|
7
|
+
export type JsonLd = Readonly<Record<string, JsonValue>>;
|
|
8
|
+
export interface Head {
|
|
9
|
+
readonly title: string;
|
|
10
|
+
readonly description: string;
|
|
11
|
+
/** Absolute canonical URL, or null for pages that must not be indexed. */
|
|
12
|
+
readonly canonical: string | null;
|
|
13
|
+
readonly robots: 'index' | 'noindex';
|
|
14
|
+
/** Open Graph type: the index is a website; everything else an article-like page. */
|
|
15
|
+
readonly ogType: 'website' | 'article';
|
|
16
|
+
readonly jsonLd: JsonLd | null;
|
|
17
|
+
}
|
|
18
|
+
/** schema.org playMode values for a game's modes. */
|
|
19
|
+
export declare function playModes(modes: readonly Mode[]): readonly string[];
|
|
20
|
+
/**
|
|
21
|
+
* What a game page calls the game in its title, heading and structured data: the manifest's
|
|
22
|
+
* `pageTitle` ("Klondike Solitaire") when it has one, else the display name. Links, cards
|
|
23
|
+
* and breadcrumbs keep the display name.
|
|
24
|
+
*/
|
|
25
|
+
export declare const pageTitleOf: (game: Pick<GameDetail, "name" | "pageTitle">) => string;
|
|
26
|
+
export declare function gameJsonLd(game: GameDetail, category: CategoryInfo | undefined, origin: string): JsonLd;
|
|
27
|
+
export declare function categoryJsonLd(category: CategoryInfo, origin: string): JsonLd;
|
|
28
|
+
export declare const NOT_FOUND_HEAD: Head;
|
|
29
|
+
/** A game page's description: the summary, then the facts a searcher wants. */
|
|
30
|
+
export declare const gameDescription: (g: GameDetail) => string;
|
|
31
|
+
export declare function headFor(page: Page, c: CatalogState): Head;
|