@gjsify/site 0.64.10 → 0.64.12-next.279
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 +85 -19
- package/lib/cli.js +4 -14
- package/lib/client/site.css +283 -3
- package/lib/index.d.ts +5 -0
- package/lib/index.js +5 -0
- package/lib/render/blog.d.ts +62 -0
- package/lib/render/blog.js +240 -0
- package/lib/render/developer.d.ts +30 -0
- package/lib/render/developer.js +285 -0
- package/lib/render/feed.d.ts +25 -0
- package/lib/render/feed.js +97 -0
- package/lib/render/i18n.d.ts +2 -2
- package/lib/render/i18n.js +2 -0
- package/lib/render/icons.d.ts +1 -1
- package/lib/render/icons.js +3 -0
- package/lib/render/image-backend.d.ts +2 -1
- package/lib/render/image-backend.js +2 -2
- package/lib/render/images.d.ts +2 -0
- package/lib/render/images.js +3 -2
- package/lib/render/layout.d.ts +94 -0
- package/lib/render/layout.js +216 -0
- package/lib/render/markdown.d.ts +15 -1
- package/lib/render/markdown.js +245 -20
- package/lib/render/pages.d.ts +21 -8
- package/lib/render/pages.js +99 -263
- package/lib/render/seo.d.ts +34 -1
- package/lib/render/seo.js +50 -4
- package/lib/site-data/blog.d.ts +30 -0
- package/lib/site-data/blog.js +226 -0
- package/lib/site-data/config.d.ts +7 -2
- package/lib/site-data/config.js +118 -23
- package/lib/site-data/developer.d.ts +16 -0
- package/lib/site-data/developer.js +12 -0
- package/lib/site-data/frontmatter.d.ts +7 -0
- package/lib/site-data/frontmatter.js +242 -0
- package/lib/site-data/index.d.ts +4 -0
- package/lib/site-data/index.js +4 -0
- package/lib/site-data/related.d.ts +4 -2
- package/lib/site-data/related.js +8 -3
- package/lib/site-data/resume.d.ts +64 -0
- package/lib/site-data/resume.js +277 -0
- package/lib/site-data/types.d.ts +57 -15
- package/lib/site.d.ts +2 -26
- package/lib/site.js +211 -78
- package/package.json +13 -12
- package/po/de.po +88 -25
- package/po/site.pot +86 -23
- package/lib/render/overview.d.ts +0 -19
- package/lib/render/overview.js +0 -53
package/lib/site-data/types.d.ts
CHANGED
|
@@ -141,27 +141,75 @@ export interface RelatedApp {
|
|
|
141
141
|
url: string;
|
|
142
142
|
languages: string[];
|
|
143
143
|
}
|
|
144
|
-
/**
|
|
145
|
-
export interface
|
|
146
|
-
|
|
144
|
+
/** `gjsify.site.blog`: a directory of Markdown posts, rendered at `/<lang>/blog/`. */
|
|
145
|
+
export interface SiteBlogConfig {
|
|
146
|
+
/** Repo-relative directory holding the `*.md` posts and the images they reference. */
|
|
147
|
+
posts: string;
|
|
148
|
+
}
|
|
149
|
+
/** A colour for each scheme; a single configured colour serves both. */
|
|
150
|
+
export interface SiteColor {
|
|
151
|
+
light: string;
|
|
152
|
+
dark: string;
|
|
153
|
+
}
|
|
154
|
+
/** `gjsify.site.theme`: how a site departs from the default look. Every key is optional. */
|
|
155
|
+
export interface SiteThemeConfig {
|
|
156
|
+
/** Links, buttons and focus rings. Default: the stylesheet's Adwaita blue. */
|
|
157
|
+
accent?: SiteColor;
|
|
158
|
+
/** Repo-relative woff2 file used for headings; the package ships and fetches no font itself. */
|
|
159
|
+
displayFont?: string;
|
|
160
|
+
/** `pixelated` keeps pixel art crisp: CSS `image-rendering` and nearest-neighbour renditions. */
|
|
161
|
+
imageRendering: 'auto' | 'pixelated';
|
|
162
|
+
}
|
|
163
|
+
/** The keys that mean the same on an app site and on a developer site. */
|
|
164
|
+
export interface SiteCommonConfig {
|
|
147
165
|
base: string;
|
|
148
166
|
url: string;
|
|
149
167
|
languages: string[];
|
|
150
168
|
/** Language `/` falls back to when the visitor's languages match none of `languages`. */
|
|
151
169
|
defaultLanguage: string;
|
|
152
|
-
/** Language of the
|
|
170
|
+
/** Language of the untranslated source text; the other languages are looked up in the catalogues. */
|
|
153
171
|
sourceLanguage: string;
|
|
154
|
-
repository: string;
|
|
155
|
-
flathub?: string;
|
|
156
|
-
webApp?: string;
|
|
157
|
-
/** Directory holding the app's `<lang>.po` catalogues, used when the metainfo lacks a translation. */
|
|
158
|
-
translations?: string;
|
|
159
172
|
/**
|
|
160
173
|
* Directory holding `<lang>.po` catalogues for the strings this configuration itself introduces —
|
|
161
174
|
* the download labels. They overlay the page catalogues shipped with the renderer, which cannot
|
|
162
175
|
* know a consumer's own copy.
|
|
163
176
|
*/
|
|
164
177
|
pageTranslations?: string;
|
|
178
|
+
support: SiteSupportEntry[];
|
|
179
|
+
legal: SiteLegalLink[];
|
|
180
|
+
/** A privacy/data-practices page at `/<lang>/privacy/`. `undefined`: no such page is built. */
|
|
181
|
+
privacy?: SitePrivacyConfig;
|
|
182
|
+
/** A Legal Notice / Impressum page at `/<lang>/legal-notice/`. `undefined`: no such page is built. */
|
|
183
|
+
legalNotice?: SiteLegalNoticeConfig;
|
|
184
|
+
/** `undefined`: no blog pages. */
|
|
185
|
+
blog?: SiteBlogConfig;
|
|
186
|
+
theme: SiteThemeConfig;
|
|
187
|
+
}
|
|
188
|
+
/** `gjsify.site.developer`: a page about a person and the apps they publish. */
|
|
189
|
+
export interface SiteDeveloperConfig {
|
|
190
|
+
/** A JSON Resume file per configured language, repo-relative. */
|
|
191
|
+
resume: Record<string, string>;
|
|
192
|
+
/** Same format as `relatedApps`, in the order the hub lists them. */
|
|
193
|
+
apps: SiteRelatedAppConfig[];
|
|
194
|
+
}
|
|
195
|
+
/** A developer site: `gjsify.site` with `developer` instead of `metainfo`. */
|
|
196
|
+
export interface DeveloperSiteConfig extends SiteCommonConfig {
|
|
197
|
+
kind: 'developer';
|
|
198
|
+
developer: SiteDeveloperConfig;
|
|
199
|
+
/** Source of the site itself, linked from the footer. */
|
|
200
|
+
repository?: string;
|
|
201
|
+
}
|
|
202
|
+
/** Either kind of `gjsify.site`, told apart by `kind`. */
|
|
203
|
+
export type AnySiteConfig = SiteConfig | DeveloperSiteConfig;
|
|
204
|
+
/** The `gjsify.site` block of an app's root package.json, with defaults applied. */
|
|
205
|
+
export interface SiteConfig extends SiteCommonConfig {
|
|
206
|
+
kind: 'app';
|
|
207
|
+
metainfo: string;
|
|
208
|
+
repository: string;
|
|
209
|
+
flathub?: string;
|
|
210
|
+
webApp?: string;
|
|
211
|
+
/** Directory holding the app's `<lang>.po` catalogues, used when the metainfo lacks a translation. */
|
|
212
|
+
translations?: string;
|
|
165
213
|
/** Repo-relative app icon; defaults to the Flatpak or `ship` icon of the root package.json. */
|
|
166
214
|
icon?: string;
|
|
167
215
|
/**
|
|
@@ -173,12 +221,6 @@ export interface SiteConfig {
|
|
|
173
221
|
downloads: SiteDownloadConfig[];
|
|
174
222
|
/** False ignores the latest release's assets, so `downloads` is the whole list. Default true. */
|
|
175
223
|
releaseAssets: boolean;
|
|
176
|
-
support: SiteSupportEntry[];
|
|
177
|
-
legal: SiteLegalLink[];
|
|
178
|
-
/** A privacy/data-practices page at `/<lang>/privacy/`. `undefined`: no such page is built. */
|
|
179
|
-
privacy?: SitePrivacyConfig;
|
|
180
|
-
/** A Legal Notice / Impressum page at `/<lang>/legal-notice/`. `undefined`: no such page is built. */
|
|
181
|
-
legalNotice?: SiteLegalNoticeConfig;
|
|
182
224
|
/** Sites of other apps to list under "More apps by <developer>", in order. Empty: no section. */
|
|
183
225
|
relatedApps: SiteRelatedAppConfig[];
|
|
184
226
|
}
|
package/lib/site.d.ts
CHANGED
|
@@ -9,6 +9,8 @@ export interface SiteTarget {
|
|
|
9
9
|
export interface SiteBuildOptions extends SiteTarget {
|
|
10
10
|
/** Skip every network call; the page is rendered from what is in the repository. */
|
|
11
11
|
offline?: boolean;
|
|
12
|
+
/** Render blog posts marked `draft: true`, for a local preview. */
|
|
13
|
+
drafts?: boolean;
|
|
12
14
|
/**
|
|
13
15
|
* Where the one-hour HTTP response cache and the image renditions live; `false` disables the
|
|
14
16
|
* response cache. A published package must not write inside its own install, so the default is
|
|
@@ -47,29 +49,3 @@ export interface SiteI18nOptions {
|
|
|
47
49
|
* own `po/` when called without a root, otherwise the consumer's `pageTranslations`.
|
|
48
50
|
*/
|
|
49
51
|
export declare function syncSiteI18n(options?: SiteI18nOptions): Promise<I18nResult>;
|
|
50
|
-
/**
|
|
51
|
-
* The `--overview` input: one JSON file naming the host and the apps whose support pages it lists.
|
|
52
|
-
* Each app is a repository root (relative to the file) with its own `gjsify.site` block, which is
|
|
53
|
-
* where its published address, languages and metainfo already live.
|
|
54
|
-
*/
|
|
55
|
-
export interface OverviewConfig {
|
|
56
|
-
/** Origin the overview is published on, e.g. `https://jumplink.eu/`. */
|
|
57
|
-
url: string;
|
|
58
|
-
/** Path the overview lives under; default `/support/`. */
|
|
59
|
-
base?: string;
|
|
60
|
-
languages: string[];
|
|
61
|
-
defaultLanguage?: string;
|
|
62
|
-
apps: string[];
|
|
63
|
-
}
|
|
64
|
-
export interface OverviewBuildOptions {
|
|
65
|
-
/** The overview JSON file. */
|
|
66
|
-
config: string;
|
|
67
|
-
out: string;
|
|
68
|
-
log?: (message: string) => void;
|
|
69
|
-
warn?: (message: string) => void;
|
|
70
|
-
packageDir?: string;
|
|
71
|
-
}
|
|
72
|
-
export declare function buildOverview(options: OverviewBuildOptions): Promise<{
|
|
73
|
-
languages: string[];
|
|
74
|
-
apps: number;
|
|
75
|
-
}>;
|
package/lib/site.js
CHANGED
|
@@ -9,11 +9,13 @@ import { catalogMsgids, configMsgids, rendererMsgids, syncCatalogs } from './ren
|
|
|
9
9
|
import { readableText } from './render/color.js';
|
|
10
10
|
import { svgToPng } from './render/image-backend.js';
|
|
11
11
|
import { processImage } from './render/images.js';
|
|
12
|
+
import { blogFeedUrl, blogIndexUrl, latestPostsSection, postDescription, postKey, postOgImage, postUrl, renderBlogFeed, renderBlogIndex, renderBlogPost, renderPostBody, tagsOf, tagUrl, } from './render/blog.js';
|
|
13
|
+
import { developerPageUrl, developerShell, renderCv, renderDeveloperHome, renderDeveloperLegalNoticePage, renderDeveloperPrivacyPage, renderDeveloperSupportPage, } from './render/developer.js';
|
|
12
14
|
import { ogCardSvg } from './render/og-card.js';
|
|
13
|
-
import { absolute, legalNoticeUrl, localized, pageMeta, pageUrl, privacyUrl, renderLegalNoticePage, renderPage, renderPrivacyPage, renderRoot, renderSupportPage, supportUrl, } from './render/pages.js';
|
|
14
|
-
import {
|
|
15
|
-
import { robotsTxt, seoWarnings, sitemapXml } from './render/seo.js';
|
|
16
|
-
import { UI_SOURCE_LANGUAGE, loadCatalogs, loadSiteData, mergeCatalogs,
|
|
15
|
+
import { absolute, appShell, legalNoticeUrl, localized, pageMeta, pageUrl, privacyUrl, renderLegalNoticePage, renderPage, renderPrivacyPage, renderRoot, renderSupportPage, supportUrl, } from './render/pages.js';
|
|
16
|
+
import { absoluteUrl, rootPage } from './render/layout.js';
|
|
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';
|
|
17
19
|
/**
|
|
18
20
|
* This package's own directory: `lib/` holds the client bundle, `po/` the page catalogues.
|
|
19
21
|
*
|
|
@@ -33,12 +35,29 @@ export function defaultCacheDir(root) {
|
|
|
33
35
|
}
|
|
34
36
|
async function readConfig(root) {
|
|
35
37
|
const manifest = JSON.parse(await readFile(join(root, 'package.json'), 'utf8'));
|
|
36
|
-
return
|
|
38
|
+
return parseAnySiteConfig(manifest);
|
|
37
39
|
}
|
|
38
40
|
/** Short, stable and enough to bust a cache: the first bytes of the content's SHA-256. */
|
|
39
41
|
function contentHash(content) {
|
|
40
42
|
return createHash('sha256').update(content).digest('hex').slice(0, 8);
|
|
41
43
|
}
|
|
44
|
+
async function writePage(out, path, html) {
|
|
45
|
+
await mkdir(dirname(join(out, path)), { recursive: true });
|
|
46
|
+
await writeFile(join(out, path), html);
|
|
47
|
+
}
|
|
48
|
+
/** Copies a repository file to `assets/` under a content-addressed name; answers its URL. */
|
|
49
|
+
async function publishHashed(source, out, base) {
|
|
50
|
+
const content = await readFile(source);
|
|
51
|
+
const extension = extname(source);
|
|
52
|
+
const name = `${basename(source, extension)}-${contentHash(content)}${extension}`;
|
|
53
|
+
await mkdir(join(out, 'assets'), { recursive: true });
|
|
54
|
+
await writeFile(join(out, 'assets', name), content);
|
|
55
|
+
return `${base}assets/${name}`;
|
|
56
|
+
}
|
|
57
|
+
async function publishDisplayFont(config, { root, out }) {
|
|
58
|
+
const font = config.theme.displayFont;
|
|
59
|
+
return font ? publishHashed(resolve(root, font), out, config.base) : undefined;
|
|
60
|
+
}
|
|
42
61
|
/**
|
|
43
62
|
* Stages the client bundle and the stylesheet this package ships, under content-addressed names.
|
|
44
63
|
* Both are built once, when the package is built — a site build neither needs a bundler nor an
|
|
@@ -61,6 +80,7 @@ async function processScreenshots(data, { out }, cacheDir) {
|
|
|
61
80
|
outDir: join(out, 'img'),
|
|
62
81
|
cacheDir: join(cacheDir, 'images'),
|
|
63
82
|
urlBase: `${data.config.base}img/`,
|
|
83
|
+
pixelated: data.config.theme.imageRendering === 'pixelated',
|
|
64
84
|
};
|
|
65
85
|
const entries = await Promise.all(data.screenshots.map(async (screenshot) => [screenshot.slug, await processImage(screenshot.file, screenshot.slug, options)]));
|
|
66
86
|
return new Map(entries);
|
|
@@ -106,6 +126,68 @@ async function loadPageCatalogs(config, root, self, warn) {
|
|
|
106
126
|
}
|
|
107
127
|
return catalogs;
|
|
108
128
|
}
|
|
129
|
+
/** Reads the posts and copies the files next to them to `<out>/blog/`, where the post URLs point. */
|
|
130
|
+
async function loadBlog(config, target, drafts) {
|
|
131
|
+
if (!config.blog)
|
|
132
|
+
return undefined;
|
|
133
|
+
const dir = resolve(target.root, config.blog.posts);
|
|
134
|
+
const posts = await loadBlogPosts({ dir, languages: config.languages, drafts });
|
|
135
|
+
for (const file of await blogAssetFiles(dir)) {
|
|
136
|
+
await mkdir(dirname(join(target.out, 'blog', file)), { recursive: true });
|
|
137
|
+
await copyFile(join(dir, file), join(target.out, 'blog', file));
|
|
138
|
+
}
|
|
139
|
+
const html = new Map(posts.map((post) => [postKey(post), renderPostBody(config, post, dir, posts)]));
|
|
140
|
+
return { posts, html };
|
|
141
|
+
}
|
|
142
|
+
/** Writes the blog of every language and answers its sitemap entries. */
|
|
143
|
+
async function writeBlog(config, out, pages) {
|
|
144
|
+
const { blog } = pages;
|
|
145
|
+
const absoluteAll = (urls) => Object.fromEntries(Object.entries(urls).map(([lang, url]) => [lang, absoluteUrl(config, url)]));
|
|
146
|
+
const sitemap = [
|
|
147
|
+
absoluteAll(Object.fromEntries(config.languages.map((lang) => [lang, blogIndexUrl(config, lang)]))),
|
|
148
|
+
];
|
|
149
|
+
const relative = (url) => url.slice(config.base.length);
|
|
150
|
+
for (const lang of config.languages) {
|
|
151
|
+
const shell = pages.shell(lang);
|
|
152
|
+
const feed = { posts: blog.posts, author: pages.author, html: blog.html, iconUrl: pages.iconUrl };
|
|
153
|
+
await writePage(out, `${relative(blogIndexUrl(config, lang))}index.html`, renderBlogIndex(shell, { posts: blog.posts, ogImage: pages.ogImage(lang) }));
|
|
154
|
+
await writePage(out, relative(blogFeedUrl(config, lang)), renderBlogFeed(shell, feed));
|
|
155
|
+
for (const tag of tagsOf(blog.posts, lang)) {
|
|
156
|
+
await writePage(out, `${relative(tagUrl(config, lang, tag))}index.html`, renderBlogIndex(shell, { posts: blog.posts, tag, ogImage: pages.ogImage(lang) }));
|
|
157
|
+
await writePage(out, relative(blogFeedUrl(config, lang, tag)), renderBlogFeed(shell, { ...feed, tag }));
|
|
158
|
+
sitemap.push({ [lang]: absoluteUrl(config, tagUrl(config, lang, tag)) });
|
|
159
|
+
}
|
|
160
|
+
for (const post of blog.posts.filter((candidate) => candidate.lang === lang)) {
|
|
161
|
+
const html = blog.html.get(postKey(post)) ?? '';
|
|
162
|
+
await writePage(out, `${relative(postUrl(config, post))}index.html`, renderBlogPost(shell, post, {
|
|
163
|
+
posts: blog.posts,
|
|
164
|
+
html,
|
|
165
|
+
author: { name: pages.author.name, url: pages.author.uri },
|
|
166
|
+
}));
|
|
167
|
+
const card = ogCardSvg({ ...pages.card, name: post.title, summary: postDescription(post) });
|
|
168
|
+
await writeFile(join(out, relative(postOgImage(config, post))), svgToPng(card));
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
for (const slug of new Set(blog.posts.map((post) => post.slug))) {
|
|
172
|
+
const translations = blog.posts.filter((post) => post.slug === slug);
|
|
173
|
+
sitemap.push(absoluteAll(Object.fromEntries(translations.map((post) => [post.lang, postUrl(config, post)]))));
|
|
174
|
+
}
|
|
175
|
+
return sitemap;
|
|
176
|
+
}
|
|
177
|
+
function writeSitemap(config, out, pages, apps) {
|
|
178
|
+
return [
|
|
179
|
+
writeFile(join(out, 'sitemap.xml'), sitemapXml(pages, config.defaultLanguage)),
|
|
180
|
+
// Only a site served from the host root can own `/robots.txt`; below a path the host does.
|
|
181
|
+
...(config.base === '/'
|
|
182
|
+
? [
|
|
183
|
+
writeFile(join(out, 'robots.txt'), robotsTxt([
|
|
184
|
+
absoluteUrl(config, `${config.base}sitemap.xml`),
|
|
185
|
+
...neighbourSitemaps(config.url, apps),
|
|
186
|
+
])),
|
|
187
|
+
]
|
|
188
|
+
: []),
|
|
189
|
+
];
|
|
190
|
+
}
|
|
109
191
|
/** Renders one static page per configured language, plus the language-picking root page. */
|
|
110
192
|
export async function buildSite(options) {
|
|
111
193
|
const target = { root: resolve(options.root), out: resolve(options.out) };
|
|
@@ -116,6 +198,13 @@ export async function buildSite(options) {
|
|
|
116
198
|
await mkdir(target.out, { recursive: true });
|
|
117
199
|
const assets = await stageClientAssets(config.base, target.out, self);
|
|
118
200
|
const catalogs = await loadPageCatalogs(config, target.root, self, options.warn);
|
|
201
|
+
const displayFontUrl = await publishDisplayFont(config, target);
|
|
202
|
+
const blog = await loadBlog(config, target, options.drafts ?? false);
|
|
203
|
+
const build = { options, target, cacheDir, assets, catalogs, displayFontUrl, blog };
|
|
204
|
+
return config.kind === 'app' ? buildAppSite(build) : buildDeveloperSite(config, build);
|
|
205
|
+
}
|
|
206
|
+
async function buildAppSite(build) {
|
|
207
|
+
const { options, target, cacheDir, assets, catalogs, displayFontUrl, blog } = build;
|
|
119
208
|
const data = await loadSiteData({
|
|
120
209
|
root: target.root,
|
|
121
210
|
screenshotCacheDir: join(cacheDir ?? defaultCacheDir(target.root), 'screenshots'),
|
|
@@ -128,6 +217,7 @@ export async function buildSite(options) {
|
|
|
128
217
|
const iconUrl = await publishIcon(data, target);
|
|
129
218
|
const iconHref = await iconDataUri(data, target);
|
|
130
219
|
const { light = '#84bbfd' } = data.app.branding;
|
|
220
|
+
const card = { background: light, foreground: readableText(light), iconHref };
|
|
131
221
|
const hasSupport = data.config.support.length > 0;
|
|
132
222
|
const privacyStatements = data.config.privacy
|
|
133
223
|
? await loadPrivacyStatements(data.config.privacy, target.root)
|
|
@@ -135,33 +225,33 @@ export async function buildSite(options) {
|
|
|
135
225
|
const legalNoticeStatements = data.config.legalNotice
|
|
136
226
|
? await loadLegalNoticeStatements(data.config.legalNotice, target.root)
|
|
137
227
|
: undefined;
|
|
228
|
+
const contextFor = (lang) => ({
|
|
229
|
+
data,
|
|
230
|
+
lang,
|
|
231
|
+
t: translator(catalogs[lang]),
|
|
232
|
+
assets,
|
|
233
|
+
images,
|
|
234
|
+
iconUrl,
|
|
235
|
+
displayFontUrl,
|
|
236
|
+
ogImage: `${data.config.base}og-${lang}.png`,
|
|
237
|
+
});
|
|
138
238
|
for (const lang of data.config.languages) {
|
|
139
|
-
const
|
|
140
|
-
|
|
239
|
+
const ctx = contextFor(lang);
|
|
240
|
+
if (blog)
|
|
241
|
+
ctx.latestPosts = latestPostsSection(appShell(ctx), blog.posts);
|
|
141
242
|
const meta = pageMeta(ctx);
|
|
142
243
|
for (const warning of seoWarnings({ lang, ...meta }))
|
|
143
244
|
options.warn?.(warning);
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
iconHref,
|
|
150
|
-
});
|
|
151
|
-
await writeFile(join(target.out, `og-${lang}.png`), svgToPng(card));
|
|
152
|
-
await mkdir(join(target.out, lang), { recursive: true });
|
|
153
|
-
await writeFile(join(target.out, lang, 'index.html'), renderPage(ctx));
|
|
154
|
-
if (hasSupport) {
|
|
155
|
-
await mkdir(join(target.out, lang, 'support'), { recursive: true });
|
|
156
|
-
await writeFile(join(target.out, lang, 'support', 'index.html'), renderSupportPage(ctx));
|
|
157
|
-
}
|
|
245
|
+
const summary = localized(data.app.summary, lang);
|
|
246
|
+
await writeFile(join(target.out, `og-${lang}.png`), svgToPng(ogCardSvg({ ...card, name: meta.name, summary })));
|
|
247
|
+
await writePage(target.out, `${lang}/index.html`, renderPage(ctx));
|
|
248
|
+
if (hasSupport)
|
|
249
|
+
await writePage(target.out, `${lang}/support/index.html`, renderSupportPage(ctx));
|
|
158
250
|
if (privacyStatements) {
|
|
159
|
-
await
|
|
160
|
-
await writeFile(join(target.out, lang, 'privacy', 'index.html'), renderPrivacyPage(ctx, privacyStatements[lang]));
|
|
251
|
+
await writePage(target.out, `${lang}/privacy/index.html`, renderPrivacyPage(ctx, privacyStatements[lang]));
|
|
161
252
|
}
|
|
162
253
|
if (legalNoticeStatements) {
|
|
163
|
-
await
|
|
164
|
-
await writeFile(join(target.out, lang, 'legal-notice', 'index.html'), renderLegalNoticePage(ctx, legalNoticeStatements[lang]));
|
|
254
|
+
await writePage(target.out, `${lang}/legal-notice/index.html`, renderLegalNoticePage(ctx, legalNoticeStatements[lang]));
|
|
165
255
|
}
|
|
166
256
|
}
|
|
167
257
|
const manifest = buildAppManifest(data, iconUrl ? absolute(data, iconUrl) : undefined);
|
|
@@ -174,13 +264,105 @@ export async function buildSite(options) {
|
|
|
174
264
|
...(privacyStatements ? [address(privacyUrl)] : []),
|
|
175
265
|
...(legalNoticeStatements ? [address(legalNoticeUrl)] : []),
|
|
176
266
|
];
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
267
|
+
if (blog) {
|
|
268
|
+
const developer = data.app.developer.name;
|
|
269
|
+
pages.push(...(await writeBlog(data.config, target.out, {
|
|
270
|
+
blog,
|
|
271
|
+
shell: (lang) => appShell({ ...contextFor(lang), current: 'blog' }),
|
|
272
|
+
author: { name: developer || localized(data.app.name, data.config.defaultLanguage) },
|
|
273
|
+
card,
|
|
274
|
+
iconUrl,
|
|
275
|
+
ogImage: (lang) => `${data.config.base}og-${lang}.png`,
|
|
276
|
+
})));
|
|
181
277
|
}
|
|
278
|
+
await Promise.all(writeSitemap(data.config, target.out, pages, data.config.relatedApps));
|
|
182
279
|
return { languages: [...data.config.languages], screenshots: images.size };
|
|
183
280
|
}
|
|
281
|
+
const HTTP_URL = /^https?:\/\//;
|
|
282
|
+
/** `basics.image`: a URL is used as it is, a repository file is published under `assets/`. */
|
|
283
|
+
async function publishAvatar(data, target) {
|
|
284
|
+
const image = data.resumes[data.config.defaultLanguage]?.basics.image;
|
|
285
|
+
if (!image)
|
|
286
|
+
return undefined;
|
|
287
|
+
return HTTP_URL.test(image) ? image : publishHashed(resolve(target.root, image), target.out, data.config.base);
|
|
288
|
+
}
|
|
289
|
+
async function avatarDataUri(data, { root }) {
|
|
290
|
+
const image = data.resumes[data.config.defaultLanguage]?.basics.image;
|
|
291
|
+
if (!image || HTTP_URL.test(image))
|
|
292
|
+
return undefined;
|
|
293
|
+
const type = ICON_TYPES[extname(image).toLowerCase()];
|
|
294
|
+
if (!type)
|
|
295
|
+
return undefined;
|
|
296
|
+
return `data:${type};base64,${(await readFile(resolve(root, image))).toString('base64')}`;
|
|
297
|
+
}
|
|
298
|
+
async function buildDeveloperSite(config, build) {
|
|
299
|
+
const { options, target, cacheDir, assets, catalogs, displayFontUrl, blog } = build;
|
|
300
|
+
const data = await loadDeveloperData(config, {
|
|
301
|
+
root: target.root,
|
|
302
|
+
offline: options.offline ?? false,
|
|
303
|
+
network: cacheDir ? { cacheDir: join(cacheDir, 'http') } : {},
|
|
304
|
+
warn: options.warn,
|
|
305
|
+
});
|
|
306
|
+
const avatarUrl = await publishAvatar(data, target);
|
|
307
|
+
const background = config.theme.accent?.light ?? '#84bbfd';
|
|
308
|
+
const card = { background, foreground: readableText(background), iconHref: await avatarDataUri(data, target) };
|
|
309
|
+
const privacyStatements = config.privacy ? await loadPrivacyStatements(config.privacy, target.root) : undefined;
|
|
310
|
+
const legalNoticeStatements = config.legalNotice
|
|
311
|
+
? await loadLegalNoticeStatements(config.legalNotice, target.root)
|
|
312
|
+
: undefined;
|
|
313
|
+
const contextFor = (lang) => ({
|
|
314
|
+
data,
|
|
315
|
+
lang,
|
|
316
|
+
t: translator(catalogs[lang]),
|
|
317
|
+
assets,
|
|
318
|
+
avatarUrl,
|
|
319
|
+
displayFontUrl,
|
|
320
|
+
ogImage: `${config.base}og-${lang}.png`,
|
|
321
|
+
});
|
|
322
|
+
for (const lang of config.languages) {
|
|
323
|
+
const ctx = contextFor(lang);
|
|
324
|
+
const { basics } = data.resumes[lang];
|
|
325
|
+
await writeFile(join(target.out, `og-${lang}.png`), svgToPng(ogCardSvg({ ...card, name: basics.name, summary: basics.label ?? '' })));
|
|
326
|
+
const latest = blog ? latestPostsSection(developerShell(ctx), blog.posts) : '';
|
|
327
|
+
await writePage(target.out, `${lang}/index.html`, renderDeveloperHome(ctx, latest));
|
|
328
|
+
await writePage(target.out, `${lang}/cv/index.html`, renderCv(ctx));
|
|
329
|
+
if (config.support.length > 0) {
|
|
330
|
+
await writePage(target.out, `${lang}/support/index.html`, renderDeveloperSupportPage(ctx));
|
|
331
|
+
}
|
|
332
|
+
if (privacyStatements) {
|
|
333
|
+
await writePage(target.out, `${lang}/privacy/index.html`, renderDeveloperPrivacyPage(ctx, privacyStatements[lang]));
|
|
334
|
+
}
|
|
335
|
+
if (legalNoticeStatements) {
|
|
336
|
+
await writePage(target.out, `${lang}/legal-notice/index.html`, renderDeveloperLegalNoticePage(ctx, legalNoticeStatements[lang]));
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
const name = data.resumes[config.defaultLanguage].basics.name;
|
|
340
|
+
await writeFile(join(target.out, 'index.html'), rootPage(config, name, assets, avatarUrl));
|
|
341
|
+
const address = (view) => Object.fromEntries(config.languages.map((lang) => [lang, absoluteUrl(config, developerPageUrl({ data }, lang, view))]));
|
|
342
|
+
const pages = [
|
|
343
|
+
address('home'),
|
|
344
|
+
address('cv'),
|
|
345
|
+
...(config.support.length > 0 ? [address('support')] : []),
|
|
346
|
+
...(privacyStatements ? [address('privacy')] : []),
|
|
347
|
+
...(legalNoticeStatements ? [address('legal-notice')] : []),
|
|
348
|
+
];
|
|
349
|
+
if (blog) {
|
|
350
|
+
const author = data.resumes[config.defaultLanguage].basics;
|
|
351
|
+
pages.push(...(await writeBlog(config, target.out, {
|
|
352
|
+
blog,
|
|
353
|
+
shell: (lang) => developerShell(contextFor(lang), 'blog'),
|
|
354
|
+
author: {
|
|
355
|
+
name: author.name,
|
|
356
|
+
uri: absoluteUrl(config, developerPageUrl({ data }, config.defaultLanguage, 'home')),
|
|
357
|
+
},
|
|
358
|
+
card,
|
|
359
|
+
iconUrl: avatarUrl,
|
|
360
|
+
ogImage: (lang) => `${config.base}og-${lang}.png`,
|
|
361
|
+
})));
|
|
362
|
+
}
|
|
363
|
+
await Promise.all(writeSitemap(config, target.out, pages, config.developer.apps));
|
|
364
|
+
return { languages: [...config.languages], screenshots: 0 };
|
|
365
|
+
}
|
|
184
366
|
/** The languages this package keeps a catalogue for: whichever `po/<lang>.po` files exist. */
|
|
185
367
|
async function packageLanguages(dir) {
|
|
186
368
|
const files = await readdir(dir);
|
|
@@ -222,52 +404,3 @@ export async function syncSiteI18n(options = {}) {
|
|
|
222
404
|
await mkdir(dir, { recursive: true });
|
|
223
405
|
return syncCatalogs({ dir, languages: config.languages, msgids, check, base: root });
|
|
224
406
|
}
|
|
225
|
-
export async function buildOverview(options) {
|
|
226
|
-
const file = resolve(options.config);
|
|
227
|
-
const overview = JSON.parse(await readFile(file, 'utf8'));
|
|
228
|
-
if (!overview.url || !Array.isArray(overview.apps) || !Array.isArray(overview.languages))
|
|
229
|
-
throw new Error(`${file} needs "url", "languages" and "apps"`);
|
|
230
|
-
const out = resolve(options.out);
|
|
231
|
-
const base = overview.base ?? '/support/';
|
|
232
|
-
const defaultLanguage = overview.defaultLanguage ?? overview.languages[0];
|
|
233
|
-
const self = selfDir(options.packageDir);
|
|
234
|
-
await rm(out, { recursive: true, force: true });
|
|
235
|
-
await mkdir(out, { recursive: true });
|
|
236
|
-
const assets = await stageClientAssets(base, out, self);
|
|
237
|
-
const catalogs = await loadCatalogs(poDir(self), overview.languages);
|
|
238
|
-
const apps = await Promise.all(overview.apps.map(async (entry) => {
|
|
239
|
-
const root = resolve(dirname(file), entry);
|
|
240
|
-
const config = await readConfig(root);
|
|
241
|
-
const metainfo = parseMetainfo(await readFile(resolve(root, config.metainfo), 'utf8'));
|
|
242
|
-
return { config, metainfo };
|
|
243
|
-
}));
|
|
244
|
-
const origin = new URL(overview.url).origin;
|
|
245
|
-
const pathFor = (lang) => `${base}${lang}/`;
|
|
246
|
-
const urls = Object.fromEntries(overview.languages.map((lang) => [lang, `${origin}${pathFor(lang)}`]));
|
|
247
|
-
const paths = Object.fromEntries(overview.languages.map((lang) => [lang, pathFor(lang)]));
|
|
248
|
-
for (const lang of overview.languages) {
|
|
249
|
-
const html = renderOverview({
|
|
250
|
-
lang,
|
|
251
|
-
languages: overview.languages,
|
|
252
|
-
defaultLanguage,
|
|
253
|
-
urls,
|
|
254
|
-
paths,
|
|
255
|
-
assets,
|
|
256
|
-
t: translator(catalogs[lang]),
|
|
257
|
-
apps: apps.map(({ config, metainfo }) => ({
|
|
258
|
-
name: pickByLanguage(metainfo.name.byLanguage, lang) ?? metainfo.name.source,
|
|
259
|
-
url: `${new URL(config.url).origin}${config.base}${config.languages.includes(lang) ? lang : config.defaultLanguage}/support/`,
|
|
260
|
-
})),
|
|
261
|
-
});
|
|
262
|
-
await mkdir(join(out, lang), { recursive: true });
|
|
263
|
-
await writeFile(join(out, lang, 'index.html'), html);
|
|
264
|
-
}
|
|
265
|
-
const root = `<!doctype html>
|
|
266
|
-
<html lang="${defaultLanguage}"><head><meta charset="utf-8"><title>Support</title>
|
|
267
|
-
<link rel="canonical" href="${urls[defaultLanguage]}"><meta http-equiv="refresh" content="0; url=${pathFor(defaultLanguage)}"></head>
|
|
268
|
-
<body><a href="${pathFor(defaultLanguage)}">Support</a></body></html>
|
|
269
|
-
`;
|
|
270
|
-
await writeFile(join(out, 'index.html'), root);
|
|
271
|
-
await writeFile(join(out, 'sitemap.xml'), sitemapXml([urls], defaultLanguage));
|
|
272
|
-
return { languages: overview.languages, apps: apps.length };
|
|
273
|
-
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gjsify/site",
|
|
3
|
-
"version": "0.64.
|
|
3
|
+
"version": "0.64.12-next.279",
|
|
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": "
|
|
58
|
-
"@gjsify/crypto": "
|
|
59
|
-
"@gjsify/fs": "
|
|
60
|
-
"@gjsify/module": "
|
|
61
|
-
"@gjsify/os": "
|
|
62
|
-
"@gjsify/path": "
|
|
63
|
-
"@gjsify/process": "
|
|
64
|
-
"@gjsify/url": "
|
|
57
|
+
"@gjsify/console": "0.64.12-next.279",
|
|
58
|
+
"@gjsify/crypto": "0.64.12-next.279",
|
|
59
|
+
"@gjsify/fs": "0.64.12-next.279",
|
|
60
|
+
"@gjsify/module": "0.64.12-next.279",
|
|
61
|
+
"@gjsify/os": "0.64.12-next.279",
|
|
62
|
+
"@gjsify/path": "0.64.12-next.279",
|
|
63
|
+
"@gjsify/process": "0.64.12-next.279",
|
|
64
|
+
"@gjsify/url": "0.64.12-next.279",
|
|
65
65
|
"fast-xml-parser": "^5.11.2",
|
|
66
66
|
"qrcode-generator": "^2.0.4"
|
|
67
67
|
},
|
|
68
68
|
"devDependencies": {
|
|
69
|
-
"@gjsify/cli": "
|
|
69
|
+
"@gjsify/cli": "0.64.12-next.279",
|
|
70
70
|
"@gjsify/node-gi": "file:../../node-gi/node-gi",
|
|
71
|
-
"@gjsify/unit": "
|
|
71
|
+
"@gjsify/unit": "0.64.12-next.279",
|
|
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": "e9026ad9f11b386102628e9e6fb93cb8b6cbd5ee"
|
|
78
79
|
}
|