@homie-rocks/studio 0.26.1 → 0.28.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 +64 -1
- package/README.md +18 -1
- package/lib/build.mjs +65 -1
- package/lib/feedback.mjs +181 -0
- package/lib/mcp-tools.mjs +134 -1
- package/lib/mcp.mjs +6 -2
- package/lib/media.mjs +7 -0
- package/lib/redact.mjs +99 -0
- package/lib/scaffold.mjs +17 -2
- package/lib/schema-check.mjs +227 -0
- package/lib/schema-vocab.json +1 -0
- package/lib/site.mjs +15 -0
- package/lib/template-history.json +79 -0
- package/mcp/ui/card.css +8 -0
- package/mcp/ui/feedback.js +112 -0
- package/media/MEDIA.md +5 -0
- package/package.json +1 -1
- package/site/SITE.md +67 -2
- package/worker/discover.mjs +232 -0
- package/worker/index.mjs +41 -4
- package/worker/schema.mjs +321 -0
- package/worker/site.mjs +46 -12
- package/worker/version.mjs +1 -1
package/worker/site.mjs
CHANGED
|
@@ -13,9 +13,9 @@ import { isLocalOrigin, qrSvg } from './qr.mjs';
|
|
|
13
13
|
import { STUDIO_VERSION_TAG } from './version.mjs';
|
|
14
14
|
import { licenseLabel, licenseOf, remixRow } from './license.mjs';
|
|
15
15
|
import { POLICY_WORDS, policyWords } from './servers.mjs';
|
|
16
|
+
import { blogNode, breadcrumbs, gameNode, itemList, ldScript, musicIndexNode, postNode, songNode, studioNode, videoNode, websiteNode } from './schema.mjs';
|
|
16
17
|
|
|
17
18
|
export const esc = (value) => String(value ?? '').replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[c]);
|
|
18
|
-
const jsonScript = (v) => JSON.stringify(v).replace(/</g, '\\u003c');
|
|
19
19
|
|
|
20
20
|
/* ------------------------------------------------------------------ headers */
|
|
21
21
|
|
|
@@ -288,6 +288,9 @@ img,video{display:block;max-width:100%}
|
|
|
288
288
|
.credit li{margin:4px 0}
|
|
289
289
|
.credit strong{color:var(--fg)}
|
|
290
290
|
.credit a{color:var(--fg)}
|
|
291
|
+
.shots{display:grid;grid-template-columns:repeat(auto-fill,minmax(min(100%,260px),1fr));gap:14px;margin-top:24px}
|
|
292
|
+
.shots a{display:block;border-radius:var(--r);overflow:hidden;border:1px solid var(--line);background:var(--panel)}
|
|
293
|
+
.shots img{width:100%;aspect-ratio:16/9;object-fit:cover}
|
|
291
294
|
|
|
292
295
|
/* make a game like this */
|
|
293
296
|
.make{position:relative;overflow:hidden;border-radius:calc(var(--r) + 8px);padding:clamp(26px,4vw,48px);border:1px solid color-mix(in srgb,var(--hot) 55%,transparent);
|
|
@@ -599,7 +602,7 @@ const ogTags = (props) => Object.entries(props).filter(([, v]) => v).map(([k, v]
|
|
|
599
602
|
* A whole generated page. `hero` pages draw the top line over the hero; `style` is the game's own accent on its
|
|
600
603
|
* landing (game.json landing.theme).
|
|
601
604
|
*/
|
|
602
|
-
export function layout(cat, { title, description = '', origin = '', path = '/', image = null, type = 'website', active = null, page = 'page', hero = false, head = '', main, over = null, status = 200, extraHeaders = {} }) {
|
|
605
|
+
export function layout(cat, { title, description = '', origin = '', path = '/', image = null, type = 'website', active = null, page = 'page', hero = false, head = '', ld = null, main, over = null, status = 200, extraHeaders = {} }) {
|
|
603
606
|
const theme = cat.studio?.theme ?? {};
|
|
604
607
|
const abs = (u) => (u && u.startsWith('/') ? `${origin}${u}` : u);
|
|
605
608
|
const icon = theme.icon ? `<link rel="icon" href="${esc(theme.icon)}">` : `<link rel="icon" href="data:image/svg+xml,${encodeURIComponent(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><rect x="3" y="3" width="10" height="10" rx="3" fill="${(over?.accent ?? theme.accent ?? TOKEN_DEFAULTS.accent)}"/></svg>`)}">`;
|
|
@@ -614,7 +617,7 @@ ${description ? `<meta name="description" content="${esc(description)}">` : ''}
|
|
|
614
617
|
${origin ? `<link rel="canonical" href="${esc(`${origin}${path}`)}">` : ''}
|
|
615
618
|
<meta name="theme-color" content="${esc(over?.bg ?? theme.bg ?? TOKEN_DEFAULTS.bg)}">
|
|
616
619
|
${ogTags({ 'og:title': title, 'og:description': description, 'og:type': type, 'og:url': origin ? `${origin}${path}` : null, 'og:image': abs(image ?? theme.social ?? null), 'og:site_name': studioName(cat), 'twitter:card': image || theme.social ? 'summary_large_image' : 'summary' })}
|
|
617
|
-
${icon}${feeds}${head}
|
|
620
|
+
${icon}${feeds}${head}${origin && ld ? ldScript(ld) : ''}
|
|
618
621
|
<style>${tokensCss(theme, over)}${BASE_CSS}${cat.site?.css ?? ''}</style>
|
|
619
622
|
${partial(cat, 'head') ?? ''}
|
|
620
623
|
</head>
|
|
@@ -632,12 +635,14 @@ ${footer(cat)}
|
|
|
632
635
|
}
|
|
633
636
|
|
|
634
637
|
/**
|
|
635
|
-
* A whole page from site/pages, served as it is, in the site's frame rules (never framed, no-transform).
|
|
638
|
+
* A whole page from site/pages, served as it is, in the site's frame rules (never framed, no-transform). Five
|
|
636
639
|
* markers let it borrow the generated parts: <!-- homie:style --> (the tokens and the stylesheet),
|
|
637
|
-
* <!-- homie:header -->, <!-- homie:footer --> ("Made with Homie")
|
|
640
|
+
* <!-- homie:header -->, <!-- homie:footer --> ("Made with Homie"), <!-- homie:script --> and, in its <head>,
|
|
641
|
+
* <!-- homie:schema --> (the structured data the generated page at that address would carry: worker/schema.mjs).
|
|
638
642
|
*/
|
|
639
|
-
export function customPage(cat, html, { active = null } = {}) {
|
|
643
|
+
export function customPage(cat, html, { active = null, schema = '' } = {}) {
|
|
640
644
|
const out = String(html)
|
|
645
|
+
.replace(/<!--\s*homie:schema\s*-->/g, () => schema)
|
|
641
646
|
.replace(/<!--\s*homie:style\s*-->/g, () => `<style>${tokensCss(cat.studio?.theme ?? {})}${BASE_CSS}${cat.site?.css ?? ''}</style>`)
|
|
642
647
|
.replace(/<!--\s*homie:header\s*-->/g, () => header(cat, active))
|
|
643
648
|
.replace(/<!--\s*homie:footer\s*-->/g, () => footer(cat))
|
|
@@ -770,6 +775,15 @@ export function featuredOf(cat) {
|
|
|
770
775
|
return games.find((g) => g.id === want) ?? games.find((g) => g.landing?.hero?.wide || g.landing?.hero?.tall) ?? games.find((g) => coverOf(g)) ?? games[0] ?? null;
|
|
771
776
|
}
|
|
772
777
|
|
|
778
|
+
/** Home's structured data: the studio (an Organization) and its site. */
|
|
779
|
+
export const homeLd = (cat, origin) => [studioNode(cat, origin, { full: true }), websiteNode(cat, origin)];
|
|
780
|
+
|
|
781
|
+
/** A game landing's structured data: its breadcrumbs and its VideoGame (worker/schema.mjs gameNode). */
|
|
782
|
+
export const landingLd = (cat, g, origin, opts = {}) => [breadcrumbs(origin, [['Home', '/'], ['Games', '/games/'], [g.name, `/${g.id}/`]]), gameNode(cat, g, origin, opts)];
|
|
783
|
+
|
|
784
|
+
/** The public games as a list of their landings (Games, Rooms). */
|
|
785
|
+
const gamesList = (cat, origin, id, name) => itemList(origin, id, name, (cat.games ?? []).map((g) => ({ name: g.name, path: `/${g.id}/` })));
|
|
786
|
+
|
|
773
787
|
export function homePage(cat, { origin = '', rooms = [], live = {} } = {}) {
|
|
774
788
|
const name = studioName(cat);
|
|
775
789
|
const games = cat.games ?? [];
|
|
@@ -846,7 +860,7 @@ export function homePage(cat, { origin = '', rooms = [], live = {} } = {}) {
|
|
|
846
860
|
return layout(cat, {
|
|
847
861
|
title: tagline ? `${name} — ${tagline}` : name,
|
|
848
862
|
description: tagline || (f ? `${name}: ${games.map((g) => g.name).join(', ')}. Free in your browser, on a phone, a computer or a TV.` : soon ? `${name}: first game coming soon.` : name),
|
|
849
|
-
origin, path: '/', image: f ? (f.landing?.hero?.wideImage ?? coverOf(f)) : null, page: 'home', hero: true,
|
|
863
|
+
origin, path: '/', image: f ? (f.landing?.hero?.wideImage ?? coverOf(f)) : null, page: 'home', hero: true, ld: homeLd(cat, origin),
|
|
850
864
|
head: f?.landing?.hero?.wideImage ? `<link rel="preload" as="image" href="${esc(f.landing.hero.tallImage ?? f.landing.hero.wideImage)}">` : '',
|
|
851
865
|
main: `${hero}${extra ? `<section class="band">${extra}</section>` : ''}${roomsBand}${gamesBand}${postsBand}${soon}${mediaBand(videos, 'videos', 'Trailers and clips')}${mediaBand(songs, 'music', 'Songs and scores')}`,
|
|
852
866
|
});
|
|
@@ -863,6 +877,7 @@ export function gamesPage(cat, { origin = '', live = {} } = {}) {
|
|
|
863
877
|
const name = studioName(cat);
|
|
864
878
|
return layout(cat, {
|
|
865
879
|
title: `Games · ${name}`, description: `Every game from ${name}: press Play and you are in a live room.`, origin, path: '/games/', page: 'games', active: 'games',
|
|
880
|
+
ld: [breadcrumbs(origin, [['Home', '/'], ['Games', '/games/']]), gamesList(cat, origin, '/games/#list', `Games from ${name}`)],
|
|
866
881
|
main: `${pageHead(name, 'Games', `${games.length === 1 ? 'One game' : `${games.length} games`}. Press Play on a phone, a computer or a TV browser and you are in a live public room with whoever is playing; bots hold the empty seats.`)}
|
|
867
882
|
<div class="wrap"><div class="cards">${games.map((g) => gameCard(g, live[g.id] ?? 0)).join('')}</div></div>`,
|
|
868
883
|
});
|
|
@@ -874,6 +889,8 @@ export function roomsPage(cat, { origin = '', rooms = [] } = {}) {
|
|
|
874
889
|
const total = rooms.reduce((n, r) => n + r.players, 0);
|
|
875
890
|
return layout(cat, {
|
|
876
891
|
title: `Rooms · ${name}`, description: `Public rooms playing now across ${name}'s games. Join one.`, origin, path: '/rooms/', page: 'rooms', active: 'rooms',
|
|
892
|
+
// The games a room can be opened in, never who is in one now: a cached copy of the page must not go stale.
|
|
893
|
+
ld: [breadcrumbs(origin, [['Home', '/'], ['Rooms', '/rooms/']]), gamesList(cat, origin, '/rooms/#games', `Games with public rooms at ${name}`)],
|
|
877
894
|
main: `${pageHead('Live now', 'Rooms', rooms.length ? `${total} ${total === 1 ? 'person' : 'people'} playing in ${rooms.length} public ${rooms.length === 1 ? 'room' : 'rooms'} right now. Join one, or press Play on any game.` : `Public rooms across ${name}'s games, as they happen.`)}
|
|
878
895
|
<div class="wrap">
|
|
879
896
|
<ol class="rooms" data-rooms data-src="/api/rooms"${rooms.length ? '' : ' hidden'}>${roomRows(rooms)}</ol>
|
|
@@ -921,6 +938,7 @@ export function serversPage(cat, g, { origin = '', servers = [], hidden = false,
|
|
|
921
938
|
const cards = [...(pub && !hidden ? [pub] : []), ...shown];
|
|
922
939
|
return layout(cat, {
|
|
923
940
|
title: `Servers · ${g.name}`, description: `${g.name}'s servers: ${SERVERS_LEAD}`, origin, path: `/${g.id}/servers/`, page: 'servers', active: 'games',
|
|
941
|
+
ld: pick ? null : [breadcrumbs(origin, [['Home', '/'], ['Games', '/games/'], [g.name, `/${g.id}/`], ['Servers', `/${g.id}/servers/`]])],
|
|
924
942
|
main: `${pageHead(g.name, pick ? 'Pick a server' : 'Servers', SERVERS_LEAD)}
|
|
925
943
|
<div class="wrap">${cards.length ? `<div class="scards">${cards.map((sv) => serverCard(g, sv)).join('')}</div>` : `<p class="quiet">No server is open right now.</p>`}
|
|
926
944
|
<p class="sec">Every policy</p><ul class="howto">${Object.values(POLICY_WORDS).map((w) => `<li>${esc(w.line.replace('{n}', 'N'))}</li>`).join('')}</ul></div>`,
|
|
@@ -942,6 +960,7 @@ export function serverPage(cat, g, sv, { origin = '', rooms = [], door = { ok: t
|
|
|
942
960
|
const blocked = !door.ok ? `<p class="door-why">${esc(doorWords(g, sv, door).line)}</p>` : '';
|
|
943
961
|
return layout(cat, {
|
|
944
962
|
title: `${sv.name} · ${g.name}`, description: `${sv.name}, a ${words.badge.toLowerCase()} server of ${g.name}. ${words.line}`, origin, path: `/${g.id}/s/${sv.id}/`, page: 'server', active: 'games',
|
|
963
|
+
ld: [breadcrumbs(origin, [['Home', '/'], ['Games', '/games/'], [g.name, `/${g.id}/`], ['Servers', `/${g.id}/servers/`], [sv.name, `/${g.id}/s/${sv.id}/`]])],
|
|
945
964
|
main: `<header class="head"><p class="kicker">${esc(g.name)} · server</p><h1>${esc(sv.name)}</h1>
|
|
946
965
|
<p class="lead"><span class="pol ${esc(sv.policy)}">${esc(words.badge)}</span> ${esc(words.line)}</p>${sv.blurb ? `<p class="lead">${esc(sv.blurb)}</p>` : ''}
|
|
947
966
|
${blocked}<div class="keys">${door.ok ? `<a class="btn" href="${esc(play)}" data-play>${PLAY_ICON}<span>Play on ${esc(sv.name)}</span></a>` : ''}${membership}</div></header>
|
|
@@ -988,6 +1007,7 @@ export function postsPage(cat, { origin = '' } = {}) {
|
|
|
988
1007
|
const items = posts.map((p) => `<li class="pitem reveal"><a href="/posts/${esc(p.slug)}/"><div><time datetime="${esc(p.date)}">${esc(fmtDay(p.date))}</time><h3>${esc(p.title)}</h3><p>${esc(p.summary)}</p>${p.links?.game && gameName(p.links.game) ? `<div class="chips"><span>${esc(gameName(p.links.game))}</span></div>` : ''}</div>${p.image ? `<img src="${esc(p.image)}" alt="" loading="lazy" decoding="async">` : ''}</a></li>`).join('');
|
|
989
1008
|
return layout(cat, {
|
|
990
1009
|
title: `Posts · ${name}`, description: `News and drops from ${name}.`, origin, path: '/posts/', page: 'posts', active: 'posts',
|
|
1010
|
+
ld: [breadcrumbs(origin, [['Home', '/'], ['Posts', '/posts/']]), blogNode(cat, origin, posts)],
|
|
991
1011
|
main: `${pageHead(name, 'Posts', 'News, drops and notes from the studio.', `<div class="feeds"><a href="/posts/feed.xml">${icon('feed')}Atom feed</a><a href="/posts/feed.json">${icon('feed')}JSON feed</a></div>`)}
|
|
992
1012
|
<div class="wrap"><ol class="plist">${items}</ol></div>`,
|
|
993
1013
|
});
|
|
@@ -1003,6 +1023,7 @@ export function postPage(cat, p, { origin = '' } = {}) {
|
|
|
1003
1023
|
const linked = [g ? gameCard(g) : '', video ? mediaCard(video, 'videos', cat) : '', song ? mediaCard(song, 'music', cat) : ''].filter(Boolean).join('');
|
|
1004
1024
|
return layout(cat, {
|
|
1005
1025
|
title: `${p.title} · ${name}`, description: p.summary, origin, path: `/posts/${p.slug}/`, image: p.image ?? (g ? coverOf(g) : null), type: 'article', page: 'post', active: 'posts',
|
|
1026
|
+
ld: [breadcrumbs(origin, [['Home', '/'], ['Posts', '/posts/'], [p.title, `/posts/${p.slug}/`]]), postNode(cat, p, origin, { image: p.image ?? (g ? coverOf(g) : null) })],
|
|
1006
1027
|
head: `<meta property="article:published_time" content="${esc(p.date)}">${p.updated ? `<meta property="article:modified_time" content="${esc(p.updated)}">` : ''}`,
|
|
1007
1028
|
main: `<div class="wrap" style="padding-top:clamp(36px,7vw,90px)"><article class="article">
|
|
1008
1029
|
<p class="post-meta"><time datetime="${esc(p.date)}">${esc(fmtDay(p.date))}</time>${p.author ? ` · ${esc(p.author)}` : ''}</p>
|
|
@@ -1092,7 +1113,7 @@ export function jsonFeed(cat, posts, origin) {
|
|
|
1092
1113
|
* public room, phone / computer / TV (with the join code), live rooms, how to play, credits, and "Make a game like
|
|
1093
1114
|
* this". A studio's site/pages/<id>/index.html replaces it; site/partials/game.html and game-<id>.html add a band.
|
|
1094
1115
|
*/
|
|
1095
|
-
export function gameLanding(cat, g, { origin = '', rooms = [], playing = 0, week = null, remix = true, servers = null } = {}) {
|
|
1116
|
+
export function gameLanding(cat, g, { origin = '', rooms = [], playing = 0, week = null, remix = true, servers = null, listed = true, shop = null } = {}) {
|
|
1096
1117
|
const name = studioName(cat);
|
|
1097
1118
|
const L = g.landing ?? {};
|
|
1098
1119
|
// A remix says what it is a remix of, linked, under its name and in its credits (game.json `remixOf`).
|
|
@@ -1155,7 +1176,8 @@ export function gameLanding(cat, g, { origin = '', rooms = [], playing = 0, week
|
|
|
1155
1176
|
${week ? `<div class="stat"><span>Played this week: <b>${esc(week.plays)}</b> ${week.plays === 1 ? 'time' : 'times'}</span><span><b>${esc(week.rounds)}</b> ${week.rounds === 1 ? 'round' : 'rounds'} with people</span>${week.peak ? `<span>Most at once today: <b>${esc(week.peak)}</b></span>` : ''}</div>` : ''}
|
|
1156
1177
|
</div></section>`;
|
|
1157
1178
|
|
|
1158
|
-
const
|
|
1179
|
+
const genres = (Array.isArray(g.genre) ? g.genre : g.genre ? [g.genre] : []).filter((x) => typeof x === 'string' && x.trim());
|
|
1180
|
+
const facts = [...genres, 'free, in the browser', 'bots fill empty seats', 'join any time: take a bot’s place', 'rounds restart on their own', ...(tvOn ? ['phones as controllers on a TV'] : [])];
|
|
1159
1181
|
const side = trailer ? mediaCard(trailer, 'videos', cat) : song ? mediaCard(song, 'music', cat) : (L.cover ? `<figure class="media-card reveal" style="margin:0"><span class="art"><img src="${esc(L.cover)}" alt="${esc(g.name)}, a moment of play" loading="lazy"></span><figcaption class="cap"><b>${esc(g.name)}</b><span>${esc(playersText(g))}${minutes(g.roundSeconds) ? ` · ${esc(minutes(g.roundSeconds))}` : ''}</span></figcaption></figure>` : '');
|
|
1160
1182
|
const howBand = `<section class="band hotband" aria-labelledby="how-title"><div class="band-in split">
|
|
1161
1183
|
<div class="reveal">
|
|
@@ -1168,6 +1190,11 @@ export function gameLanding(cat, g, { origin = '', rooms = [], playing = 0, week
|
|
|
1168
1190
|
${side ? `<div>${side}${trailer && song ? `<div style="margin-top:14px">${mediaCard(song, 'music', cat)}</div>` : ''}</div>` : ''}
|
|
1169
1191
|
</div></section>`;
|
|
1170
1192
|
|
|
1193
|
+
// Screenshots (landing.screenshots, else games/<id>/screenshots/): what the game looks like in play.
|
|
1194
|
+
const shots = (L.screenshots ?? []).slice(0, 8);
|
|
1195
|
+
const shotsBand = shots.length ? `<section class="band tight" aria-labelledby="shots-title"><div class="band-in"><p class="kicker reveal">Screenshots</p><h2 class="small-h reveal" id="shots-title">${esc(g.name)}, in play</h2>
|
|
1196
|
+
<div class="shots reveal">${shots.map((u, i) => `<a href="${esc(u)}"><img src="${esc(u)}" alt="${esc(`${g.name}: screenshot ${i + 1}`)}" loading="lazy" decoding="async"></a>`).join('')}</div></div></section>` : '';
|
|
1197
|
+
|
|
1171
1198
|
const C = L.credits ?? {};
|
|
1172
1199
|
const o = C.original;
|
|
1173
1200
|
const creditCards = [
|
|
@@ -1199,13 +1226,14 @@ export function gameLanding(cat, g, { origin = '', rooms = [], playing = 0, week
|
|
|
1199
1226
|
// A partial that is its own <section> stands as it is; anything else sits in one of the page's bands.
|
|
1200
1227
|
const band = (html) => (!html ? '' : /^\s*<section\b/i.test(html) ? html : `<section class="band tight"><div class="band-in">${html}</div></section>`);
|
|
1201
1228
|
const vars = { 'game.name': g.name, 'game.id': g.id, 'game.play': `/${g.id}/play` };
|
|
1202
|
-
const ld = { '@context': 'https://schema.org', '@type': 'VideoGame', name: g.name, description: L.pitch ?? g.blurb ?? '', url: `${origin}/${g.id}/`, playMode: 'MultiPlayer', gamePlatform: ['Web browser', 'Phone', 'TV'], applicationCategory: 'Game', ...(h.wideImage ? { image: `${origin}${h.wideImage}` } : {}), author: { '@type': 'Organization', name, url: `${origin}/` }, offers: { '@type': 'Offer', price: 0, priceCurrency: 'USD' } };
|
|
1203
1229
|
return layout(cat, {
|
|
1204
1230
|
title: `${g.name} — play free in your browser`,
|
|
1205
1231
|
description: `${g.name}: ${L.pitch ?? g.blurb ?? ''} Free in your browser, nothing to download; a TV or laptop browser can be the big screen, with phones as controllers.`.replace(/\s+/g, ' ').trim(),
|
|
1206
1232
|
origin, path: `/${g.id}/`, image: h.wideImage ?? L.cover ?? null, page: 'landing', hero: true, active: 'games', over: landingTokens(cat, L),
|
|
1207
|
-
head: `${h.tallImage || h.wideImage ? `<link rel="preload" as="image" href="${esc(h.tallImage ?? h.wideImage)}"${h.tallImage && h.wideImage ? ' media="(max-aspect-ratio: 3/4)"' : ''}>${h.tallImage && h.wideImage ? `<link rel="preload" as="image" href="${esc(h.wideImage)}" media="(min-aspect-ratio: 3/4)">` : ''}` : ''}
|
|
1208
|
-
|
|
1233
|
+
head: `${h.tallImage || h.wideImage ? `<link rel="preload" as="image" href="${esc(h.tallImage ?? h.wideImage)}"${h.tallImage && h.wideImage ? ' media="(max-aspect-ratio: 3/4)"' : ''}>${h.tallImage && h.wideImage ? `<link rel="preload" as="image" href="${esc(h.wideImage)}" media="(min-aspect-ratio: 3/4)">` : ''}` : ''}`,
|
|
1234
|
+
// A game that is not public yet (private, or an invite-only beta) is never indexed, and offers nothing.
|
|
1235
|
+
ld: landingLd(cat, g, origin, { listed, shop }), ...(listed ? {} : { extraHeaders: { 'x-robots-tag': 'noindex' } }),
|
|
1236
|
+
main: `${hero}${ways}${liveBand}${servers ? serversBand(g, servers) : ''}${howBand}${shotsBand}${band(partial(cat, 'game', vars))}${band(partial(cat, `game-${g.id}`, vars))}${creditsBand}${makeBand}`,
|
|
1209
1237
|
});
|
|
1210
1238
|
}
|
|
1211
1239
|
|
|
@@ -1216,6 +1244,7 @@ export function creditsPage(cat, g, texts, { origin = '' } = {}) {
|
|
|
1216
1244
|
const of = lineage ? `<p class="sec">Remix of</p><p>${lineage.page ? `<a href="${esc(lineage.page)}" rel="noopener">${esc(lineage.name)}</a>` : esc(lineage.name)}${lineage.studio ? ` by ${esc(lineage.studio)}` : ''}</p>` : '';
|
|
1217
1245
|
return layout(cat, {
|
|
1218
1246
|
title: `Credits · ${g.name} · ${name}`, description: `Credits and licences for ${g.name}.`, origin, path: `/${g.id}/credits`, page: 'credits', active: 'games',
|
|
1247
|
+
ld: [breadcrumbs(origin, [['Home', '/'], ['Games', '/games/'], [g.name, `/${g.id}/`], ['Credits', `/${g.id}/credits`]])],
|
|
1219
1248
|
main: `${pageHead(g.name, 'Credits and licences', null, `<div class="keys"><a class="ghost" href="/${esc(g.id)}/">${icon('arrow')}<span>Back to ${esc(g.name)}</span></a></div>`)}
|
|
1220
1249
|
<div class="wrap">${of}${texts.map(({ file, text }) => `<p class="sec">${esc(file)}</p><pre class="licence">${esc(text)}</pre>`).join('')}</div>`,
|
|
1221
1250
|
});
|
|
@@ -1246,6 +1275,9 @@ export function mediaIndexPage(cat, kind, { origin = '' } = {}) {
|
|
|
1246
1275
|
const title = kind === 'music' ? 'Music' : 'Videos';
|
|
1247
1276
|
return layout(cat, {
|
|
1248
1277
|
title: `${title} · ${name}`, description: `${title} from ${name}.`, origin, path: `/${kind}/`, page: kind, active: kind,
|
|
1278
|
+
ld: [breadcrumbs(origin, [['Home', '/'], [title, `/${kind}/`]]), kind === 'music'
|
|
1279
|
+
? musicIndexNode(cat, origin, list, { art: (e) => mediaArt(e, 'music', cat) })
|
|
1280
|
+
: itemList(origin, '/videos/#list', `Videos from ${name}`, list.map((e) => ({ name: e.title, path: `/videos/${e.slug}/` })))],
|
|
1249
1281
|
main: `${pageHead(name, title, kind === 'music' ? 'Songs, scores and loops from the studio.' : 'Trailers, music videos and clips from the studio.')}
|
|
1250
1282
|
<div class="wrap"><div class="cards">${list.map((e) => mediaCard(e, kind, cat)).join('')}</div></div>`,
|
|
1251
1283
|
});
|
|
@@ -1261,6 +1293,7 @@ export function songPage(cat, e, origin = '') {
|
|
|
1261
1293
|
const abs = (u) => (u && u.startsWith('/') ? `${origin}${u}` : u);
|
|
1262
1294
|
return layout(cat, {
|
|
1263
1295
|
title: `${e.title} · ${name}`, description: e.blurb ?? '', origin, path: `/music/${e.slug}/`, image: cover?.url ?? null, type: 'music.song', page: 'song', active: 'music',
|
|
1296
|
+
ld: [breadcrumbs(origin, [['Home', '/'], ['Music', '/music/'], [e.title, `/music/${e.slug}/`]]), songNode(cat, e, origin, { art: cover?.url ?? null })],
|
|
1264
1297
|
head: ogTags({ 'og:audio': abs(audio.url) }),
|
|
1265
1298
|
main: `<div class="wrap" style="padding-top:clamp(36px,7vw,90px)">
|
|
1266
1299
|
<section class="songhead${cover ? ' has-cover' : ''}">
|
|
@@ -1293,6 +1326,7 @@ export function videoPage(cat, e, origin = '') {
|
|
|
1293
1326
|
const abs = (u) => (u && u.startsWith('/') ? `${origin}${u}` : u);
|
|
1294
1327
|
return layout(cat, {
|
|
1295
1328
|
title: `${e.title} · ${name}`, description: e.blurb ?? '', origin, path: `/videos/${e.slug}/`, image: poster?.url ?? null, type: 'video.other', page: 'video', active: 'videos',
|
|
1329
|
+
ld: [breadcrumbs(origin, [['Home', '/'], ['Videos', '/videos/'], [e.title, `/videos/${e.slug}/`]]), videoNode(cat, e, origin, { art: poster?.url ?? null })],
|
|
1296
1330
|
head: ogTags({ 'og:video': abs(video.url) }),
|
|
1297
1331
|
main: `<div class="wrap" style="padding-top:clamp(28px,5vw,64px)">
|
|
1298
1332
|
<div class="player"${beaconAttr('video', e.slug)}${vertical ? ` data-vertical="${esc(vertical.url)}"` : ''}><video data-main controls playsinline preload="metadata" src="${esc(video.url)}"${poster ? ` poster="${esc(poster.url)}"` : ''}>${captions ? `<track kind="captions" src="${esc(captions.url)}" srclang="en" label="English">` : ''}</video></div>
|
package/worker/version.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** The @homie-rocks/studio version this Worker code is (package.json's; a test keeps them equal). Cache-busts /_homie/site.js. */
|
|
2
|
-
export const STUDIO_VERSION_TAG = '0.
|
|
2
|
+
export const STUDIO_VERSION_TAG = '0.28.0';
|