@gjsify/site 0.64.24 → 0.64.26-next.312

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/README.md CHANGED
@@ -281,6 +281,37 @@ Two things happen to the artwork:
281
281
  The `<picture>` carries AVIF, WebP and PNG, each with a `srcset` of 640px, 1280px and 2560px
282
282
  renditions up to the source's own width.
283
283
 
284
+ #### Language and colour scheme
285
+
286
+ The metainfo stays the only source; the standard AppStream 1.0 attributes pick the rendition:
287
+
288
+ ```xml
289
+ <screenshot type="default">
290
+ <image type="source">https://…/list.png</image>
291
+ <image type="source" xml:lang="de">https://…/list-de.png</image>
292
+ </screenshot>
293
+ <screenshot environment="gnome:dark">
294
+ <image type="source">https://…/list-dark.png</image>
295
+ <image type="source" xml:lang="de">https://…/list-dark-de.png</image>
296
+ </screenshot>
297
+ ```
298
+
299
+ - An `<image xml:lang="…">` is the translation of the untagged image. The `/<lang>/` page shows the
300
+ image of its language and falls back to the untagged one.
301
+ - A `<screenshot>` whose `environment` ends in `:dark` is the dark twin of a light one: the n-th
302
+ dark screenshot pairs with the n-th non-dark one, in document order. It is never a slide of its
303
+ own; one without a partner is dropped with a warning. A dark twin without an image for the
304
+ page's language uses its untagged image, and a light screenshot without a twin has no dark
305
+ version.
306
+ - The `<picture>` lists the dark AVIF, WebP and PNG `<source>`s with
307
+ `media="(prefers-color-scheme: dark)"` first, which is the query the site's theme follows too.
308
+ No JavaScript is involved, and the full-screen view copies the `<picture>`, so it shows the same
309
+ rendition.
310
+ - Each rendition is cropped, capped and sized on its own, so the translations and the dark twin
311
+ may differ in size.
312
+
313
+ The store-listing module is unaffected: it publishes one default image per light screenshot.
314
+
284
315
  ### Colour
285
316
 
286
317
  Also nothing to configure: the metainfo's `<branding>` colour for the page's scheme paints the
@@ -40,8 +40,13 @@ export declare function supportReference(data: SiteData): string;
40
40
  * file are three different promises, so each gets its own wording.
41
41
  */
42
42
  export declare function ctaLabel(target: DownloadTarget, t: Translate): string;
43
+ /**
44
+ * The dark renditions come first: a browser takes the first `<source>` that matches, and the
45
+ * `prefers-color-scheme` media query is what the site's own theme follows too.
46
+ */
47
+ export declare function picture(ctx: Pick<RenderContext, 'images' | 'lang'>, screenshot: Screenshot, alt: string, eager: boolean): string;
43
48
  /** A window shot brings its own shadow; ours would draw a second one around it. */
44
- export declare function frameAttribute(ctx: Pick<RenderContext, 'images'>, screenshot: Screenshot): 'none' | undefined;
49
+ export declare function frameAttribute(ctx: Pick<RenderContext, 'images' | 'lang'>, screenshot: Screenshot): 'none' | undefined;
45
50
  /**
46
51
  * The split button plus the dialog holding every target. Both are server-rendered: without
47
52
  * JavaScript the dialog stays an open panel below the button (see `site.css`), with it the
@@ -1,4 +1,4 @@
1
- import { epcPayload, formatIban, isBrowserPlatform, needsOpenAnywayHint, PLATFORM_ORDER, pickByLanguage, relatedText, relatedUrl, } from '../site-data/index.js';
1
+ import { epcPayload, formatIban, isBrowserPlatform, needsOpenAnywayHint, PLATFORM_ORDER, pickByLanguage, relatedText, relatedUrl, screenshotFile, } from '../site-data/index.js';
2
2
  import { blogFeedUrl, blogIndexUrl } from './blog.js';
3
3
  import { attrs, esc, fmt, plain } from './html.js';
4
4
  import { icon } from './icons.js';
@@ -6,6 +6,8 @@ import { absoluteUrl, documentPage, githubUrl, rootPage, simplePage, sitePageUrl
6
6
  import { renderMarkdown } from './markdown.js';
7
7
  import { qrSvg } from './qr.js';
8
8
  import { metaDescription, pageTitle, softwareApplicationJsonLd } from './seo.js';
9
+ /** What `styles/site.css` follows for the theme; the dark `<source>`s answer the same question. */
10
+ const DARK_MEDIA = '(prefers-color-scheme: dark)';
9
11
  const FORMAT_TYPES = { avif: 'image/avif', webp: 'image/webp', png: 'image/png' };
10
12
  const LINKS = [
11
13
  ['homepage', (t) => t('Website'), 'web'],
@@ -165,20 +167,44 @@ function srcset(files) {
165
167
  function sizes(image) {
166
168
  return `(max-width: 600px) 92vw, min(86vw, ${image.displayWidth}px)`;
167
169
  }
170
+ /** The processed image a screenshot has for the language being rendered, in one colour scheme. */
171
+ function shown(ctx, screenshot, scheme = 'light') {
172
+ return ctx.images.get(screenshotFile(screenshot, ctx.lang, scheme).slug);
173
+ }
168
174
  /** What a phone shows for this screenshot: portrait artwork needs a taller track than a landscape window. */
169
175
  function portraitOnPhone(ctx, screenshot) {
170
- const image = ctx.images.get(screenshot.slug);
176
+ const image = shown(ctx, screenshot);
171
177
  return image !== undefined && image.height > image.width * 1.1;
172
178
  }
173
- function picture(ctx, screenshot, alt, eager) {
174
- const image = ctx.images.get(screenshot.slug);
175
- const shared = { sizes: sizes(image), width: image.width, height: image.height };
176
- const sources = ['avif', 'webp'].map((format) => `<source${attrs({ type: FORMAT_TYPES[format], srcset: srcset(image.formats[format]), ...shared })}>`);
179
+ /**
180
+ * The dark renditions come first: a browser takes the first `<source>` that matches, and the
181
+ * `prefers-color-scheme` media query is what the site's own theme follows too.
182
+ */
183
+ export function picture(ctx, screenshot, alt, eager) {
184
+ const image = shown(ctx, screenshot);
185
+ const dark = shown(ctx, screenshot, 'dark');
186
+ const source = (rendition, format, media) => `<source${attrs({
187
+ type: FORMAT_TYPES[format],
188
+ srcset: srcset(rendition.formats[format]),
189
+ media,
190
+ sizes: sizes(rendition),
191
+ width: rendition.width,
192
+ height: rendition.height,
193
+ })}>`;
194
+ const sources = [];
195
+ if (dark !== image) {
196
+ for (const format of ['avif', 'webp', 'png'])
197
+ sources.push(source(dark, format, DARK_MEDIA));
198
+ }
199
+ for (const format of ['avif', 'webp'])
200
+ sources.push(source(image, format));
177
201
  const png = image.formats.png;
178
202
  const img = `<img${attrs({
179
203
  src: png[png.length - 1].url,
180
204
  srcset: srcset(png),
181
- ...shared,
205
+ sizes: sizes(image),
206
+ width: image.width,
207
+ height: image.height,
182
208
  alt,
183
209
  loading: eager ? undefined : 'lazy',
184
210
  decoding: 'async',
@@ -187,7 +213,7 @@ function picture(ctx, screenshot, alt, eager) {
187
213
  }
188
214
  /** A window shot brings its own shadow; ours would draw a second one around it. */
189
215
  export function frameAttribute(ctx, screenshot) {
190
- return ctx.images.get(screenshot.slug)?.frameless === true ? 'none' : undefined;
216
+ return shown(ctx, screenshot)?.frameless === true ? 'none' : undefined;
191
217
  }
192
218
  /** The screenshots, at the bottom of the brand-coloured masthead. */
193
219
  function screenshotBand(ctx) {
@@ -59,7 +59,7 @@ export async function loadSiteData(options) {
59
59
  const metainfoPath = resolve(options.root, config.metainfo);
60
60
  if (!existsSync(metainfoPath))
61
61
  throw new Error(`gjsify.site.metainfo points to "${config.metainfo}", which does not exist`);
62
- const metainfo = parseMetainfo(await readFile(metainfoPath, 'utf8'));
62
+ const metainfo = parseMetainfo(await readFile(metainfoPath, 'utf8'), warn);
63
63
  const appCatalogs = config.translations
64
64
  ? await loadCatalogs(resolve(options.root, config.translations), config.languages)
65
65
  : {};
@@ -6,4 +6,4 @@ import { type XmlElement } from './xml.js';
6
6
  */
7
7
  export declare function groupLocalized(elements: readonly XmlElement[]): Localized[];
8
8
  /** Reads the parts of an AppStream `<component>` the landing page needs. */
9
- export declare function parseMetainfo(xml: string): Metainfo;
9
+ export declare function parseMetainfo(xml: string, warn?: (message: string) => void): Metainfo;
@@ -99,8 +99,45 @@ function parseUrls(component) {
99
99
  }
100
100
  return urls;
101
101
  }
102
+ /** The `<image>`s of a `<screenshot>`: the untagged one, and the `xml:lang` ones as translations of it. */
103
+ function parseImages(screenshot) {
104
+ const pick = (images) => (images.find((image) => image.attrs.type === 'source') ?? images[0])?.text;
105
+ const images = childrenNamed(screenshot, 'image');
106
+ const byLanguage = {};
107
+ for (const language of new Set(images.map((image) => image.attrs[XML_LANG]))) {
108
+ if (language === undefined)
109
+ continue;
110
+ const url = pick(images.filter((image) => image.attrs[XML_LANG] === language));
111
+ if (url)
112
+ byLanguage[language] = url;
113
+ }
114
+ return { url: pick(images.filter((image) => image.attrs[XML_LANG] === undefined)) ?? '', byLanguage };
115
+ }
116
+ /** AppStream's `environment="gnome:dark"`: the screenshot shows the app in its dark style. */
117
+ function isDark(screenshot) {
118
+ return (screenshot.attrs.environment ?? '').endsWith(':dark');
119
+ }
120
+ /**
121
+ * The n-th dark screenshot is the dark twin of the n-th light one, in document order, and never a
122
+ * slide of its own. A dark screenshot with no light partner is dropped with a warning.
123
+ */
124
+ function parseScreenshots(screenshots, warn) {
125
+ const light = screenshots.filter((screenshot) => !isDark(screenshot));
126
+ const result = light.map((screenshot) => ({
127
+ ...parseImages(screenshot),
128
+ caption: singleLocalized(screenshot, 'caption') ?? null,
129
+ }));
130
+ for (const [index, dark] of screenshots.filter(isDark).entries()) {
131
+ const partner = result[index];
132
+ if (partner)
133
+ partner.dark = parseImages(dark);
134
+ else
135
+ warn(`Dark screenshot ${index + 1} of the metainfo has no light screenshot to pair with; dropping it`);
136
+ }
137
+ return result;
138
+ }
102
139
  /** Reads the parts of an AppStream `<component>` the landing page needs. */
103
- export function parseMetainfo(xml) {
140
+ export function parseMetainfo(xml, warn = () => { }) {
104
141
  const component = parseXml(xml);
105
142
  if (component.name !== 'component')
106
143
  throw new Error(`Metainfo root must be <component>, got <${component.name}>`);
@@ -113,11 +150,7 @@ export function parseMetainfo(xml) {
113
150
  const summary = singleLocalized(component, 'summary') ?? { source: '', byLanguage: {} };
114
151
  const developer = child(component, 'developer');
115
152
  const developerName = developer ? child(developer, 'name')?.text : undefined;
116
- const screenshots = childrenNamed(child(component, 'screenshots') ?? component, 'screenshot').map((screenshot) => {
117
- const images = childrenNamed(screenshot, 'image');
118
- const image = images.find((candidate) => candidate.attrs.type === 'source') ?? images[0];
119
- return { url: image?.text ?? '', caption: singleLocalized(screenshot, 'caption') ?? null };
120
- });
153
+ const screenshots = parseScreenshots(childrenNamed(child(component, 'screenshots') ?? component, 'screenshot'), warn);
121
154
  const contentRating = child(component, 'content_rating');
122
155
  const attributes = {};
123
156
  for (const attribute of contentRating ? childrenNamed(contentRating, 'content_attribute') : []) {
@@ -1,5 +1,5 @@
1
1
  import { type NetworkOptions } from './network.js';
2
- import type { Metainfo, Screenshot } from './types.js';
2
+ import type { Metainfo, Screenshot, ScreenshotFile } from './types.js';
3
3
  /**
4
4
  * The repo-relative path a `raw.githubusercontent.com` URL points at, or `undefined` when the URL
5
5
  * belongs to some other repository or host.
@@ -25,9 +25,18 @@ export interface ScreenshotOptions {
25
25
  }
26
26
  /**
27
27
  * The metainfo `<screenshots>` list with every image located on disk — the same artwork Flathub
28
- * shows, in the same order, so the page doubles as a preview of the store listing.
28
+ * shows, in the same order, so the page doubles as a preview of the store listing. Each screenshot
29
+ * also carries its `xml:lang` translations and its dark twin.
29
30
  *
30
- * A screenshot whose file cannot be found or fetched is skipped with a warning: a page missing one
31
- * image is better than no page at all, and the warning names what to fix.
31
+ * An image that cannot be found or fetched is skipped with a warning: a page missing one image is
32
+ * better than no page at all, and the warning names what to fix. When it is the default image the
33
+ * whole screenshot goes; a missing translation or dark rendition only falls back to the default.
32
34
  */
33
35
  export declare function resolveScreenshots(metainfo: Metainfo, options: ScreenshotOptions): Promise<Screenshot[]>;
36
+ /**
37
+ * The image a language sees in a colour scheme: its own translation, else the untagged image; and
38
+ * for dark, the dark twin's, else the light screenshot's.
39
+ */
40
+ export declare function screenshotFile(screenshot: Screenshot, language: string, scheme: 'light' | 'dark'): ScreenshotFile;
41
+ /** Every file of a screenshot, each once: what the page has to turn into renditions. */
42
+ export declare function screenshotFiles(screenshot: Screenshot): ScreenshotFile[];
@@ -3,6 +3,7 @@ import { mkdir, writeFile } from 'node:fs/promises';
3
3
  import { tmpdir } from 'node:os';
4
4
  import { extname, join, resolve } from 'node:path';
5
5
  import { fetchBinary } from './network.js';
6
+ import { pickByLanguage } from './po.js';
6
7
  const RAW_GITHUB = 'https://raw.githubusercontent.com/';
7
8
  /**
8
9
  * The repo-relative path a `raw.githubusercontent.com` URL points at, or `undefined` when the URL
@@ -42,49 +43,99 @@ export function imageSlug(path) {
42
43
  }
43
44
  /**
44
45
  * The metainfo `<screenshots>` list with every image located on disk — the same artwork Flathub
45
- * shows, in the same order, so the page doubles as a preview of the store listing.
46
+ * shows, in the same order, so the page doubles as a preview of the store listing. Each screenshot
47
+ * also carries its `xml:lang` translations and its dark twin.
46
48
  *
47
- * A screenshot whose file cannot be found or fetched is skipped with a warning: a page missing one
48
- * image is better than no page at all, and the warning names what to fix.
49
+ * An image that cannot be found or fetched is skipped with a warning: a page missing one image is
50
+ * better than no page at all, and the warning names what to fix. When it is the default image the
51
+ * whole screenshot goes; a missing translation or dark rendition only falls back to the default.
49
52
  */
50
53
  export async function resolveScreenshots(metainfo, options) {
51
54
  const warn = options.warn ?? (() => { });
52
55
  const store = options.cacheDir ?? join(tmpdir(), 'gjsify-site-screenshots');
53
- const screenshots = [];
54
- for (const [index, entry] of metainfo.screenshots.entries()) {
55
- const label = `Screenshot ${index + 1}`;
56
- if (!entry.url) {
57
- warn(`${label} of the metainfo has no <image>; skipping it`);
58
- continue;
59
- }
60
- const repoPath = repositoryPath(entry.url, options.repository);
61
- const slug = imageSlug(repoPath ?? urlPath(entry.url));
62
- const screenshot = { caption: entry.caption, url: entry.url, slug };
56
+ const locate = async (url, label) => {
57
+ const repoPath = repositoryPath(url, options.repository);
58
+ const slug = imageSlug(repoPath ?? urlPath(url));
63
59
  if (repoPath !== undefined) {
64
60
  const file = resolve(options.root, repoPath);
65
- if (!existsSync(file)) {
66
- warn(`${label} points at "${repoPath}" of this repository, which does not exist; skipping it`);
67
- continue;
68
- }
69
- screenshots.push({ ...screenshot, file });
70
- continue;
61
+ if (existsSync(file))
62
+ return { url, slug, file };
63
+ warn(`${label} points at "${repoPath}" of this repository, which does not exist; skipping it`);
64
+ return undefined;
71
65
  }
72
- const file = join(store, `${slug}${extname(urlPath(entry.url)) || '.png'}`);
66
+ const file = join(store, `${slug}${extname(urlPath(url)) || '.png'}`);
73
67
  if (!existsSync(file)) {
74
68
  if (options.offline) {
75
- warn(`${label} is hosted at ${entry.url}; skipping it because this build is offline`);
76
- continue;
69
+ warn(`${label} is hosted at ${url}; skipping it because this build is offline`);
70
+ return undefined;
77
71
  }
78
72
  try {
79
73
  await mkdir(store, { recursive: true });
80
- await writeFile(file, await fetchBinary(entry.url, options.network ?? {}));
74
+ await writeFile(file, await fetchBinary(url, options.network ?? {}));
81
75
  }
82
76
  catch (error) {
83
- warn(`Could not download ${label} from ${entry.url} (${String(error)}); skipping it`);
84
- continue;
77
+ warn(`Could not download ${label} from ${url} (${String(error)}); skipping it`);
78
+ return undefined;
85
79
  }
86
80
  }
87
- screenshots.push({ ...screenshot, file });
81
+ return { url, slug, file };
82
+ };
83
+ const locateTranslations = async (byLanguage, label) => {
84
+ const located = {};
85
+ for (const [language, url] of Object.entries(byLanguage)) {
86
+ const file = await locate(url, `${label} (${language})`);
87
+ if (file)
88
+ located[language] = file;
89
+ }
90
+ return located;
91
+ };
92
+ const screenshots = [];
93
+ for (const [index, entry] of metainfo.screenshots.entries()) {
94
+ const label = `Screenshot ${index + 1}`;
95
+ if (!entry.url) {
96
+ warn(`${label} of the metainfo has no <image>; skipping it`);
97
+ continue;
98
+ }
99
+ const file = await locate(entry.url, label);
100
+ if (!file)
101
+ continue;
102
+ const screenshot = {
103
+ ...file,
104
+ caption: entry.caption,
105
+ byLanguage: await locateTranslations(entry.byLanguage, label),
106
+ };
107
+ if (entry.dark) {
108
+ const darkLabel = `The dark rendition of ${label}`;
109
+ const dark = entry.dark.url ? await locate(entry.dark.url, darkLabel) : undefined;
110
+ if (!entry.dark.url)
111
+ warn(`${darkLabel} has no untagged <image>; ignoring it`);
112
+ if (dark) {
113
+ screenshot.dark = {
114
+ default: dark,
115
+ byLanguage: await locateTranslations(entry.dark.byLanguage, darkLabel),
116
+ };
117
+ }
118
+ }
119
+ screenshots.push(screenshot);
88
120
  }
89
121
  return screenshots;
90
122
  }
123
+ /**
124
+ * The image a language sees in a colour scheme: its own translation, else the untagged image; and
125
+ * for dark, the dark twin's, else the light screenshot's.
126
+ */
127
+ export function screenshotFile(screenshot, language, scheme) {
128
+ if (scheme === 'dark' && screenshot.dark) {
129
+ return pickByLanguage(screenshot.dark.byLanguage, language) ?? screenshot.dark.default;
130
+ }
131
+ return pickByLanguage(screenshot.byLanguage, language) ?? screenshot;
132
+ }
133
+ /** Every file of a screenshot, each once: what the page has to turn into renditions. */
134
+ export function screenshotFiles(screenshot) {
135
+ const files = [
136
+ screenshot,
137
+ ...Object.values(screenshot.byLanguage),
138
+ ...(screenshot.dark ? [screenshot.dark.default, ...Object.values(screenshot.dark.byLanguage)] : []),
139
+ ];
140
+ return [...new Map(files.map((file) => [file.slug, file])).values()];
141
+ }
@@ -230,9 +230,19 @@ export interface SiteConfig extends SiteCommonConfig {
230
230
  * One entry of the metainfo `<screenshots>` list, with its artwork located on disk. The same images
231
231
  * Flathub shows, so the page is a preview of the store listing.
232
232
  */
233
- export interface Screenshot {
233
+ export interface Screenshot extends ScreenshotFile {
234
234
  /** The metainfo caption, translated where the metainfo or a catalogue has a translation. */
235
235
  caption: Localized | null;
236
+ /** The `xml:lang` renditions of the light artwork; the language without one gets the file above. */
237
+ byLanguage: Record<string, ScreenshotFile>;
238
+ /** The `environment="…:dark"` twin, when the metainfo has one. It falls back to the light artwork. */
239
+ dark?: {
240
+ default: ScreenshotFile;
241
+ byLanguage: Record<string, ScreenshotFile>;
242
+ };
243
+ }
244
+ /** One located image of a screenshot. */
245
+ export interface ScreenshotFile {
236
246
  /** The `<image>` URL, kept for reference. */
237
247
  url: string;
238
248
  /** Absolute path of the artwork: the repository's own file, or the downloaded copy. */
@@ -240,6 +250,15 @@ export interface Screenshot {
240
250
  /** Stem of the published renditions, derived from the URL path; also the key of the image map. */
241
251
  slug: string;
242
252
  }
253
+ /** The `<image>` URLs of one screenshot: the untagged one and its `xml:lang` translations. */
254
+ export interface ImageUrls {
255
+ url: string;
256
+ byLanguage: Record<string, string>;
257
+ }
258
+ export interface MetainfoScreenshot extends ImageUrls {
259
+ caption: Localized | null;
260
+ dark?: ImageUrls;
261
+ }
243
262
  export interface Release {
244
263
  version: string;
245
264
  date: string;
@@ -324,11 +343,11 @@ export interface Metainfo {
324
343
  name: string;
325
344
  };
326
345
  description: DescriptionBlock[];
327
- /** Metainfo screenshots in document order; `url` is the remote image, kept for reference. */
328
- screenshots: Array<{
329
- url: string;
330
- caption: Localized | null;
331
- }>;
346
+ /**
347
+ * Metainfo screenshots in document order, dark twins folded into their light partner; `url` is
348
+ * the untagged remote image, kept for reference.
349
+ */
350
+ screenshots: MetainfoScreenshot[];
332
351
  releases: Release[];
333
352
  branding: Branding;
334
353
  urls: AppUrls;
package/lib/site.js CHANGED
@@ -15,7 +15,7 @@ import { ogCardSvg } from './render/og-card.js';
15
15
  import { absolute, appShell, legalNoticeUrl, localized, pageMeta, pageUrl, privacyUrl, renderLegalNoticePage, renderPage, renderPrivacyPage, renderRoot, renderSupportPage, supportUrl, } from './render/pages.js';
16
16
  import { absoluteUrl, rootPage } from './render/layout.js';
17
17
  import { neighbourSitemaps, robotsTxt, seoWarnings, sitemapXml } from './render/seo.js';
18
- import { UI_SOURCE_LANGUAGE, loadCatalogs, loadSiteData, mergeCatalogs, blogAssetFiles, loadBlogPosts, loadDeveloperData, parseAnySiteConfig, translator, APP_MANIFEST_FILE, buildAppManifest, loadLegalNoticeStatements, loadPrivacyStatements, } from './site-data/index.js';
18
+ import { UI_SOURCE_LANGUAGE, loadCatalogs, loadSiteData, mergeCatalogs, blogAssetFiles, loadBlogPosts, loadDeveloperData, parseAnySiteConfig, translator, APP_MANIFEST_FILE, buildAppManifest, loadLegalNoticeStatements, loadPrivacyStatements, screenshotFiles, } from './site-data/index.js';
19
19
  /**
20
20
  * This package's own directory: `lib/` holds the client bundle, `po/` the page catalogues.
21
21
  *
@@ -82,7 +82,8 @@ async function processScreenshots(data, { out }, cacheDir) {
82
82
  urlBase: `${data.config.base}img/`,
83
83
  pixelated: data.config.theme.imageRendering === 'pixelated',
84
84
  };
85
- const entries = await Promise.all(data.screenshots.map(async (screenshot) => [screenshot.slug, await processImage(screenshot.file, screenshot.slug, options)]));
85
+ const files = new Map(data.screenshots.flatMap(screenshotFiles).map((file) => [file.slug, file]));
86
+ const entries = await Promise.all([...files.values()].map(async (file) => [file.slug, await processImage(file.file, file.slug, options)]));
86
87
  return new Map(entries);
87
88
  }
88
89
  const ICON_TYPES = { '.svg': 'image/svg+xml', '.png': 'image/png' };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gjsify/site",
3
- "version": "0.64.24",
3
+ "version": "0.64.26-next.312",
4
4
  "description": "Static landing page generator for a GNOME-style app, rendered from its AppStream metainfo — downloads, screenshots, release notes, support links, no JavaScript required.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -54,25 +54,26 @@
54
54
  "license": "MIT",
55
55
  "dependencies": {
56
56
  "@girs/gdkpixbuf-2.0": "5.4.0",
57
- "@gjsify/console": "^0.64.24",
58
- "@gjsify/crypto": "^0.64.24",
59
- "@gjsify/fs": "^0.64.24",
60
- "@gjsify/module": "^0.64.24",
61
- "@gjsify/os": "^0.64.24",
62
- "@gjsify/path": "^0.64.24",
63
- "@gjsify/process": "^0.64.24",
64
- "@gjsify/url": "^0.64.24",
57
+ "@gjsify/console": "0.64.26-next.312",
58
+ "@gjsify/crypto": "0.64.26-next.312",
59
+ "@gjsify/fs": "0.64.26-next.312",
60
+ "@gjsify/module": "0.64.26-next.312",
61
+ "@gjsify/os": "0.64.26-next.312",
62
+ "@gjsify/path": "0.64.26-next.312",
63
+ "@gjsify/process": "0.64.26-next.312",
64
+ "@gjsify/url": "0.64.26-next.312",
65
65
  "fast-xml-parser": "^5.11.2",
66
66
  "qrcode-generator": "^2.0.4"
67
67
  },
68
68
  "devDependencies": {
69
- "@gjsify/cli": "^0.64.24",
69
+ "@gjsify/cli": "0.64.26-next.312",
70
70
  "@gjsify/node-gi": "file:../../node-gi/node-gi",
71
- "@gjsify/unit": "^0.64.24",
71
+ "@gjsify/unit": "0.64.26-next.312",
72
72
  "@types/node": "^25.9.2",
73
73
  "typescript": "^6.0.3"
74
74
  },
75
75
  "gjsify": {
76
76
  "tier": 2
77
- }
77
+ },
78
+ "gitHead": "75fa6155a8f1fa0f7e5d70c43418ae41ecac8c0e"
78
79
  }