@gjsify/site 0.64.11 → 0.64.12-next.280
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/README.md
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
# @gjsify/site
|
|
2
2
|
|
|
3
|
-
Static landing page generator for a GNOME-style app
|
|
4
|
-
|
|
3
|
+
Static landing page generator for a GNOME-style app — and, with the same look, for the developer
|
|
4
|
+
behind it: a hub, a CV and a blog (see [Developer sites](#developer-sites)). Everything on the page
|
|
5
|
+
comes from files the project already has — the AppStream metainfo, the root `package.json`, the app's gettext
|
|
5
6
|
catalogues, a screenshot directory — plus, at build time, the GitHub release and Flathub APIs.
|
|
6
7
|
Nothing about any particular app is compiled in: all app-specific values come from a
|
|
7
8
|
`gjsify.site` block.
|
|
@@ -39,10 +40,10 @@ gjsify site build --runtime node # build and run on a specific runtime
|
|
|
39
40
|
|
|
40
41
|
| Option | Meaning |
|
|
41
42
|
| --- | --- |
|
|
42
|
-
| `--overview <file>` | `build`: render the support overview listed in this JSON file instead of one app (see below). |
|
|
43
43
|
| `--root <path>` | Repository to work on — a checkout whose root `package.json` has `gjsify.site`. Default: the working directory. |
|
|
44
44
|
| `--out <dir>` | Output directory, emptied first. Default: `<root>/dist`. |
|
|
45
45
|
| `--offline` | Skip every network call. The page then links to the releases page and omits the counts. |
|
|
46
|
+
| `--drafts` | `build`: include blog posts marked `draft: true`. |
|
|
46
47
|
| `--no-cache` | Ignore the one-hour HTTP response cache. |
|
|
47
48
|
| `--cache-dir <dir>` | Where the response cache and the image renditions live. Default: `<root>/node_modules/.cache/gjsify-site`. |
|
|
48
49
|
| `--check` | `i18n`: report what is out of date instead of writing it. |
|
|
@@ -60,7 +61,8 @@ Every build also writes a 1200×630 link-preview card per language (`og-<lang>.p
|
|
|
60
61
|
and rasterised by GdkPixbuf), `SoftwareApplication` JSON-LD, `hreflang` alternates with an
|
|
61
62
|
`x-default` that points at the default-language page, a valid `og:locale` (`de_DE`), and a
|
|
62
63
|
`sitemap.xml`. `robots.txt` is written only when `gjsify.site.base` is `/`; below a path the host
|
|
63
|
-
root owns it and must list this sitemap. A
|
|
64
|
+
root owns it and must list this sitemap. A site at `/` does that for every related app (or
|
|
65
|
+
developer app) given as a site address on the same host. A page title or meta description that is too short
|
|
64
66
|
(or only the app name) is reported as a build warning.
|
|
65
67
|
|
|
66
68
|
A metainfo URL that points into the site's own `/<lang>/…` pages (it starts with `gjsify.site.url`)
|
|
@@ -118,21 +120,6 @@ this package does not assume or enforce that: each consumer supplies its own fil
|
|
|
118
120
|
`privacy` does, because what counts as identical is a fact about the publisher, not something a
|
|
119
121
|
shared package can derive.
|
|
120
122
|
|
|
121
|
-
`gjsify site build --overview <file> --out <dir>` renders one page that lists several apps, each
|
|
122
|
-
linking to its own support page, for a host such as `jumplink.eu/support/`:
|
|
123
|
-
|
|
124
|
-
```json
|
|
125
|
-
{
|
|
126
|
-
"url": "https://jumplink.eu/",
|
|
127
|
-
"base": "/support/",
|
|
128
|
-
"languages": ["en", "de"],
|
|
129
|
-
"apps": ["../learn6502", "../beifahrer"]
|
|
130
|
-
}
|
|
131
|
-
```
|
|
132
|
-
|
|
133
|
-
Each `apps` entry is a repository root (relative to the file) with its own `gjsify.site` block;
|
|
134
|
-
its name and published address are read from there.
|
|
135
|
-
|
|
136
123
|
`GITHUB_TOKEN` is optional and only raises the API rate limit. When a lookup fails the build
|
|
137
124
|
warns rather than failing: a release API outage is not a reason to have no page.
|
|
138
125
|
|
|
@@ -166,6 +153,9 @@ Lives in the root `package.json`, next to `ship` and `flatpak`.
|
|
|
166
153
|
| `support`, `legal` | `support`: `{kind, url}` links, or `{kind: "sepa", name, iban, bic?, reference?}`, a bank transfer shown as IBAN plus an EPC QR code. `legal`: `{label, url}` links. Both empty by default: nothing is invented. |
|
|
167
154
|
| `privacy` | `{statements: {<lang>: <path>}, hostingPolicyUrl?}`: a privacy page at `/<lang>/privacy/`, see below. Absent: no such page. |
|
|
168
155
|
| `legalNotice` | `{statements: {<lang>: <path>}}`: a Legal Notice/Impressum page at `/<lang>/legal-notice/`, see below. Absent: no such page. |
|
|
156
|
+
| `blog` | `{posts: <dir>}`: Markdown posts rendered at `/<lang>/blog/`, see [Blog](#blog). Absent: no blog. |
|
|
157
|
+
| `theme` | `{accent?, displayFont?, imageRendering?}`: how the site departs from the default look, see [Theme](#theme). |
|
|
158
|
+
| `developer` | Instead of `metainfo`: makes this a developer site, see [Developer sites](#developer-sites). |
|
|
169
159
|
|
|
170
160
|
A `sepa` entry carries no amount, and its IBAN is checked when the config loads — length per
|
|
171
161
|
country, alphabet, and the mod-97 checksum — so a typo fails the build instead of printing a QR
|
|
@@ -256,6 +246,82 @@ relative luminance and writes near-black or white into `--band-fg`, whichever th
|
|
|
256
246
|
Everything below the masthead keeps the site's own neutral palette, so a pale brand colour cannot
|
|
257
247
|
make the description unreadable. An app without `<branding>` gets the default blue.
|
|
258
248
|
|
|
249
|
+
## Blog
|
|
250
|
+
|
|
251
|
+
`"blog": { "posts": "blog" }` turns every `*.md` below that directory into a post. The language
|
|
252
|
+
comes from the file name (`hello.en.md`, `hello.de.md`) or the frontmatter; files sharing a slug
|
|
253
|
+
are translations of each other and link to each other through `hreflang`.
|
|
254
|
+
|
|
255
|
+
```markdown
|
|
256
|
+
---
|
|
257
|
+
title: Hello
|
|
258
|
+
date: 2026-10-20
|
|
259
|
+
updated: 2026-10-22
|
|
260
|
+
summary: One sentence for the index, the feed and the link preview.
|
|
261
|
+
tags: [GNOME, gjsify]
|
|
262
|
+
---
|
|
263
|
+
|
|
264
|
+
First paragraph — the description when there is no `summary`.
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
| Key | Meaning |
|
|
268
|
+
| --- | --- |
|
|
269
|
+
| `title`, `date` | Required. `date`/`updated` are `YYYY-MM-DD` or an ISO 8601 date-time. |
|
|
270
|
+
| `slug`, `lang` | Override what the file name says. |
|
|
271
|
+
| `updated`, `summary`, `tags` | Optional. |
|
|
272
|
+
| `draft` | `true` leaves the post out unless the build runs with `--drafts`. |
|
|
273
|
+
|
|
274
|
+
| Page | URL |
|
|
275
|
+
| --- | --- |
|
|
276
|
+
| Index + Atom feed, per language | `/<lang>/blog/`, `/<lang>/blog/feed.xml` |
|
|
277
|
+
| Post + its link-preview card | `/<lang>/blog/<slug>/`, `/<lang>/blog/<slug>/og.png` |
|
|
278
|
+
| Tag, only in the languages that use it | `/<lang>/blog/tags/<tag>/`, `…/feed.xml` |
|
|
279
|
+
|
|
280
|
+
Everything else in the directory — images, downloads — is published under `/blog/` at the same
|
|
281
|
+
relative path, so `` works in the editor and on the page. A link to another post's
|
|
282
|
+
`.md` file becomes a link to that post's page; one to a post the build does not render (a draft,
|
|
283
|
+
a typo) fails the build, since no Markdown is ever published. Posts are `BlogPosting` JSON-LD with
|
|
284
|
+
`article:*` meta, and the landing page shows the newest three of its language.
|
|
285
|
+
|
|
286
|
+
## Theme
|
|
287
|
+
|
|
288
|
+
| Key | Meaning |
|
|
289
|
+
| --- | --- |
|
|
290
|
+
| `accent` | `"#rrggbb"` or `{light, dark}`: links, buttons, focus rings. The text on it is measured like the brand colour. |
|
|
291
|
+
| `displayFont` | Repo-relative `.woff2` used for headings. The package ships and fetches no font itself. |
|
|
292
|
+
| `imageRendering` | `"pixelated"` keeps pixel art crisp: CSS `image-rendering` plus nearest-neighbour renditions. Default `"auto"`. |
|
|
293
|
+
|
|
294
|
+
The theme is written into each page as a short inline style, so a site without one gets exactly
|
|
295
|
+
the default stylesheet.
|
|
296
|
+
|
|
297
|
+
## Developer sites
|
|
298
|
+
|
|
299
|
+
`developer` instead of `metainfo` makes the site about a person rather than one app — the same
|
|
300
|
+
layout, support, privacy and legal pages, plus a hub and a CV:
|
|
301
|
+
|
|
302
|
+
```json
|
|
303
|
+
"gjsify": { "site": {
|
|
304
|
+
"url": "https://example.org",
|
|
305
|
+
"languages": ["en", "de"],
|
|
306
|
+
"developer": {
|
|
307
|
+
"resume": { "en": "content/resume.en.json", "de": "content/resume.de.json" },
|
|
308
|
+
"apps": ["https://example.org/notes/"]
|
|
309
|
+
},
|
|
310
|
+
"blog": { "posts": "blog" }
|
|
311
|
+
} }
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
- `resume` names a [JSON Resume](https://jsonresume.org/schema) file for **every** configured
|
|
315
|
+
language — no page of a CV falls back to another language. `basics.image` of the default
|
|
316
|
+
language's résumé is the avatar; `basics.summary` is Markdown.
|
|
317
|
+
- `apps` has the format of `relatedApps`; `relatedApps` itself is rejected here.
|
|
318
|
+
- `base` defaults to `/` and `repository` is optional (it links the site's source).
|
|
319
|
+
- With no metainfo `<branding>` to read, `theme.accent` also paints the masthead and the bar.
|
|
320
|
+
|
|
321
|
+
The hub `/<lang>/` shows the person, their apps, the newest posts and a link to the CV
|
|
322
|
+
`/<lang>/cv/`, which prints as a plain document. Both carry `ProfilePage` JSON-LD with the
|
|
323
|
+
résumé's profiles as `sameAs`.
|
|
324
|
+
|
|
259
325
|
## Translations
|
|
260
326
|
|
|
261
327
|
Two catalogue sets meet on the page, and they have different owners.
|
package/lib/cli.js
CHANGED
|
@@ -5,18 +5,17 @@
|
|
|
5
5
|
// the CLI itself is on, and runs it — so there is no `gjsify-site` bin bound to
|
|
6
6
|
// one runtime. See `packages/infra/cli/src/commands/site.ts`.
|
|
7
7
|
import { join, resolve } from 'node:path';
|
|
8
|
-
import {
|
|
8
|
+
import { buildSite, syncSiteI18n } from './site.js';
|
|
9
9
|
const USAGE = `Usage: gjsify site <command> [options]
|
|
10
10
|
|
|
11
11
|
build [options] Render the static pages of a repository
|
|
12
12
|
i18n [options] Update the page catalogues from the strings the build can emit
|
|
13
13
|
|
|
14
14
|
Options:
|
|
15
|
-
--overview <file> build: render the support overview listed in this JSON file
|
|
16
|
-
instead of one app (url, languages, apps)
|
|
17
15
|
--root <path> Repository to work on (default: the working directory)
|
|
18
16
|
--out <dir> Output directory, emptied first (default: <root>/dist)
|
|
19
17
|
--offline Skip every network call
|
|
18
|
+
--drafts build: include blog posts marked \`draft: true\`
|
|
20
19
|
--no-cache Ignore the one-hour HTTP response cache
|
|
21
20
|
--cache-dir <dir> Where to keep the response cache and image renditions
|
|
22
21
|
(default: <root>/node_modules/.cache/gjsify-site)
|
|
@@ -44,27 +43,18 @@ function cacheOption(flags) {
|
|
|
44
43
|
return flagValue(flags, '--cache-dir');
|
|
45
44
|
}
|
|
46
45
|
async function build(flags) {
|
|
47
|
-
const overview = flagValue(flags, '--overview');
|
|
48
|
-
if (overview) {
|
|
49
|
-
const result = await buildOverview({
|
|
50
|
-
config: overview,
|
|
51
|
-
out: resolve(flagValue(flags, '--out') ?? join(process.cwd(), 'dist')),
|
|
52
|
-
packageDir: flagValue(flags, '--package-dir'),
|
|
53
|
-
});
|
|
54
|
-
console.log(`Built ${result.languages.length} overview pages for ${result.apps} apps`);
|
|
55
|
-
return 0;
|
|
56
|
-
}
|
|
57
46
|
const root = resolve(flagValue(flags, '--root') ?? process.cwd());
|
|
58
47
|
const result = await buildSite({
|
|
59
48
|
root,
|
|
60
49
|
out: resolve(flagValue(flags, '--out') ?? join(root, 'dist')),
|
|
61
50
|
offline: flags.includes('--offline'),
|
|
51
|
+
drafts: flags.includes('--drafts'),
|
|
62
52
|
cacheDir: cacheOption(flags),
|
|
63
53
|
packageDir: flagValue(flags, '--package-dir'),
|
|
64
54
|
githubToken: process.env.GITHUB_TOKEN || undefined,
|
|
65
55
|
warn: (message) => console.warn(`warning: ${message}`),
|
|
66
56
|
});
|
|
67
|
-
console.log(`Built ${result.languages.length}
|
|
57
|
+
console.log(`Built the pages in ${result.languages.length} languages and ${result.screenshots} screenshots`);
|
|
68
58
|
return 0;
|
|
69
59
|
}
|
|
70
60
|
async function i18n(flags) {
|
package/lib/client/site.css
CHANGED
|
@@ -21,8 +21,8 @@
|
|
|
21
21
|
--fg: #1d1d20;
|
|
22
22
|
--muted: #5e5c64;
|
|
23
23
|
--line: rgb(0 0 0 / 0.1);
|
|
24
|
-
--accent: #1c71d8;
|
|
25
|
-
--accent-fg: #fff;
|
|
24
|
+
--accent: var(--accent-light, #1c71d8);
|
|
25
|
+
--accent-fg: var(--accent-fg-light, #fff);
|
|
26
26
|
--shadow: 0 8px 24px rgb(0 0 0 / 0.14);
|
|
27
27
|
/* Lifts the screenshot off the brand band without drawing a card around it. */
|
|
28
28
|
--shot-shadow: 0 0.75rem 2rem rgb(0 0 0 / 0.25);
|
|
@@ -46,7 +46,8 @@
|
|
|
46
46
|
--fg: #f6f5f4;
|
|
47
47
|
--muted: #b6b4b1;
|
|
48
48
|
--line: rgb(255 255 255 / 0.11);
|
|
49
|
-
--accent: #3584e4;
|
|
49
|
+
--accent: var(--accent-dark, #3584e4);
|
|
50
|
+
--accent-fg: var(--accent-fg-dark, #fff);
|
|
50
51
|
--shadow: 0 8px 24px rgb(0 0 0 / 0.5);
|
|
51
52
|
--shot-shadow: 0 0.75rem 2rem rgb(0 0 0 / 0.45);
|
|
52
53
|
}
|
|
@@ -78,6 +79,7 @@ picture {
|
|
|
78
79
|
h1,
|
|
79
80
|
h2,
|
|
80
81
|
h3 {
|
|
82
|
+
font-family: var(--display-font, inherit);
|
|
81
83
|
line-height: 1.2;
|
|
82
84
|
}
|
|
83
85
|
|
|
@@ -1422,3 +1424,281 @@ app-support {
|
|
|
1422
1424
|
color: var(--muted);
|
|
1423
1425
|
font-size: 0.78rem;
|
|
1424
1426
|
}
|
|
1427
|
+
|
|
1428
|
+
/* Blog */
|
|
1429
|
+
|
|
1430
|
+
.bar-link[aria-current="page"] {
|
|
1431
|
+
opacity: 1;
|
|
1432
|
+
text-decoration: underline;
|
|
1433
|
+
text-underline-offset: 0.3em;
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
.post-list {
|
|
1437
|
+
display: grid;
|
|
1438
|
+
gap: 1rem;
|
|
1439
|
+
margin: 0;
|
|
1440
|
+
padding: 0;
|
|
1441
|
+
list-style: none;
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
.post-card {
|
|
1445
|
+
padding: 1.25rem 1.5rem;
|
|
1446
|
+
border: 1px solid var(--line);
|
|
1447
|
+
border-radius: var(--radius-lg);
|
|
1448
|
+
background: var(--surface);
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
.post-card h2,
|
|
1452
|
+
.post-card h3 {
|
|
1453
|
+
margin: 0;
|
|
1454
|
+
font-size: 1.25rem;
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
.post-card h2 a,
|
|
1458
|
+
.post-card h3 a {
|
|
1459
|
+
color: var(--fg);
|
|
1460
|
+
text-decoration: none;
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
.post-card h2 a:hover,
|
|
1464
|
+
.post-card h3 a:hover {
|
|
1465
|
+
color: var(--accent);
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
.post-card > p:last-child {
|
|
1469
|
+
margin-block-end: 0;
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
.post-meta {
|
|
1473
|
+
margin: 0.25rem 0 0;
|
|
1474
|
+
color: var(--muted);
|
|
1475
|
+
font-size: 0.9rem;
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
.post-tags {
|
|
1479
|
+
display: flex;
|
|
1480
|
+
flex-wrap: wrap;
|
|
1481
|
+
gap: 0.4rem;
|
|
1482
|
+
margin: 0.5rem 0 0;
|
|
1483
|
+
padding: 0;
|
|
1484
|
+
list-style: none;
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
.tag {
|
|
1488
|
+
display: inline-block;
|
|
1489
|
+
padding: 0.05rem 0.6rem;
|
|
1490
|
+
border: 1px solid var(--line);
|
|
1491
|
+
border-radius: 999px;
|
|
1492
|
+
background: var(--bg);
|
|
1493
|
+
color: var(--muted);
|
|
1494
|
+
font-size: 0.8rem;
|
|
1495
|
+
text-decoration: none;
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
.tag:hover {
|
|
1499
|
+
color: var(--accent);
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
.post-feed a,
|
|
1503
|
+
.post-back a {
|
|
1504
|
+
display: inline-flex;
|
|
1505
|
+
align-items: center;
|
|
1506
|
+
gap: 0.4rem;
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
.post-feed {
|
|
1510
|
+
margin-block: 1.5rem 0;
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
.post-empty {
|
|
1514
|
+
color: var(--muted);
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
.blog,
|
|
1518
|
+
.post,
|
|
1519
|
+
.cv {
|
|
1520
|
+
max-width: var(--text);
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
.post-head {
|
|
1524
|
+
margin-block-end: 2rem;
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
.post-back {
|
|
1528
|
+
margin: 0 0 1rem;
|
|
1529
|
+
font-size: 0.9rem;
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
.post-body img {
|
|
1533
|
+
max-width: 100%;
|
|
1534
|
+
height: auto;
|
|
1535
|
+
border-radius: var(--radius);
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
.post-body pre {
|
|
1539
|
+
overflow-x: auto;
|
|
1540
|
+
padding: 1rem;
|
|
1541
|
+
border-radius: var(--radius);
|
|
1542
|
+
background: var(--surface);
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
.post-body blockquote {
|
|
1546
|
+
margin-inline: 0;
|
|
1547
|
+
padding-inline-start: 1rem;
|
|
1548
|
+
border-inline-start: 3px solid var(--line);
|
|
1549
|
+
color: var(--muted);
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1552
|
+
.latest-posts {
|
|
1553
|
+
margin-block: 2.5rem;
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
.latest-posts h2 {
|
|
1557
|
+
margin: 0 0 0.75rem;
|
|
1558
|
+
font-size: 1.5rem;
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1561
|
+
/* Developer site */
|
|
1562
|
+
|
|
1563
|
+
.developer-avatar img {
|
|
1564
|
+
border-radius: var(--radius-lg);
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
.developer-cv {
|
|
1568
|
+
margin-block-start: 1.5rem;
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
.developer-apps {
|
|
1572
|
+
margin-block: 2.5rem;
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
/* CV */
|
|
1576
|
+
|
|
1577
|
+
.cv-head {
|
|
1578
|
+
display: flex;
|
|
1579
|
+
align-items: center;
|
|
1580
|
+
gap: 1.5rem;
|
|
1581
|
+
margin-block-end: 2rem;
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1584
|
+
.cv-avatar {
|
|
1585
|
+
flex: none;
|
|
1586
|
+
border-radius: var(--radius-lg);
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
.cv-label {
|
|
1590
|
+
margin: 0.25rem 0 0;
|
|
1591
|
+
color: var(--muted);
|
|
1592
|
+
font-size: 1.1rem;
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1595
|
+
.cv-contact {
|
|
1596
|
+
display: flex;
|
|
1597
|
+
flex-wrap: wrap;
|
|
1598
|
+
gap: 0.25rem 1.25rem;
|
|
1599
|
+
margin: 0.75rem 0 0;
|
|
1600
|
+
padding: 0;
|
|
1601
|
+
list-style: none;
|
|
1602
|
+
font-size: 0.9rem;
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
.cv-contact li {
|
|
1606
|
+
display: inline-flex;
|
|
1607
|
+
align-items: center;
|
|
1608
|
+
gap: 0.35rem;
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
.cv-section {
|
|
1612
|
+
margin-block: 2rem;
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1615
|
+
.cv-section h2 {
|
|
1616
|
+
margin: 0 0 0.75rem;
|
|
1617
|
+
padding-block-end: 0.25rem;
|
|
1618
|
+
border-bottom: 1px solid var(--line);
|
|
1619
|
+
font-size: 1.35rem;
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1622
|
+
.cv-list {
|
|
1623
|
+
margin: 0;
|
|
1624
|
+
padding: 0;
|
|
1625
|
+
list-style: none;
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
.cv-entry + .cv-entry {
|
|
1629
|
+
margin-block-start: 1.25rem;
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
.cv-entry h3 {
|
|
1633
|
+
margin: 0;
|
|
1634
|
+
font-size: 1.05rem;
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
.cv-entry p {
|
|
1638
|
+
margin: 0.25rem 0 0;
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
.cv-period {
|
|
1642
|
+
color: var(--muted);
|
|
1643
|
+
font-size: 0.85rem;
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
.cv-highlights {
|
|
1647
|
+
margin: 0.4rem 0 0;
|
|
1648
|
+
padding-inline-start: 1.25rem;
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1651
|
+
.cv-entry .tags {
|
|
1652
|
+
margin-block-start: 0.5rem;
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1655
|
+
@media (max-width: 600px) {
|
|
1656
|
+
.cv-head {
|
|
1657
|
+
flex-direction: column;
|
|
1658
|
+
align-items: flex-start;
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
/* On paper the CV is the page: no bar, no footer, no colour, links spelled out where it matters. */
|
|
1663
|
+
@media print {
|
|
1664
|
+
:root {
|
|
1665
|
+
color-scheme: light;
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1668
|
+
.topbar,
|
|
1669
|
+
.footer,
|
|
1670
|
+
.post-feed,
|
|
1671
|
+
.post-back {
|
|
1672
|
+
display: none;
|
|
1673
|
+
}
|
|
1674
|
+
|
|
1675
|
+
body {
|
|
1676
|
+
background: #fff;
|
|
1677
|
+
color: #000;
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
main {
|
|
1681
|
+
padding: 0;
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1684
|
+
.column {
|
|
1685
|
+
width: auto;
|
|
1686
|
+
max-width: none;
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
.cv a {
|
|
1690
|
+
color: inherit;
|
|
1691
|
+
text-decoration: none;
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1694
|
+
.cv-section,
|
|
1695
|
+
.cv-entry {
|
|
1696
|
+
break-inside: avoid;
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1699
|
+
.tags li,
|
|
1700
|
+
.tag {
|
|
1701
|
+
border-color: #999;
|
|
1702
|
+
background: none;
|
|
1703
|
+
}
|
|
1704
|
+
}
|
package/lib/index.d.ts
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
|
+
export * from './render/blog.js';
|
|
2
|
+
export * from './render/developer.js';
|
|
3
|
+
export * from './render/feed.js';
|
|
1
4
|
export * from './render/html.js';
|
|
2
5
|
export * from './cli.js';
|
|
3
6
|
export * from './render/i18n.js';
|
|
4
7
|
export * from './render/icons.js';
|
|
5
8
|
export * from './render/images.js';
|
|
9
|
+
export * from './render/layout.js';
|
|
10
|
+
export * from './render/markdown.js';
|
|
6
11
|
export * from './render/pages.js';
|
|
7
12
|
export * from './render/qr.js';
|
|
8
13
|
export * from './site-data/index.js';
|
package/lib/index.js
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
|
+
export * from './render/blog.js';
|
|
2
|
+
export * from './render/developer.js';
|
|
3
|
+
export * from './render/feed.js';
|
|
1
4
|
export * from './render/html.js';
|
|
2
5
|
export * from './cli.js';
|
|
3
6
|
export * from './render/i18n.js';
|
|
4
7
|
export * from './render/icons.js';
|
|
5
8
|
export * from './render/images.js';
|
|
9
|
+
export * from './render/layout.js';
|
|
10
|
+
export * from './render/markdown.js';
|
|
6
11
|
export * from './render/pages.js';
|
|
7
12
|
export * from './render/qr.js';
|
|
8
13
|
export * from './site-data/index.js';
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { type BlogPost, type SiteCommonConfig } from '../site-data/index.js';
|
|
2
|
+
import { type Shell } from './layout.js';
|
|
3
|
+
type Config = Pick<SiteCommonConfig, 'base'>;
|
|
4
|
+
/** How many posts an app's landing page shows. */
|
|
5
|
+
export declare const LATEST_POSTS = 3;
|
|
6
|
+
export declare function blogIndexUrl(config: Config, lang: string): string;
|
|
7
|
+
export declare function blogFeedUrl(config: Config, lang: string, tag?: string): string;
|
|
8
|
+
export declare function postUrl(config: Config, post: Pick<BlogPost, 'lang' | 'slug'>): string;
|
|
9
|
+
export declare function tagUrl(config: Config, lang: string, tag: string): string;
|
|
10
|
+
/** The post's own link-preview card, written next to its page. */
|
|
11
|
+
export declare function postOgImage(config: Config, post: Pick<BlogPost, 'lang' | 'slug'>): string;
|
|
12
|
+
/**
|
|
13
|
+
* Rewrites the URLs a post's Markdown names. A relative image or file resolves against the post's
|
|
14
|
+
* own directory below `<base>blog/` — where the build copies the asset files. A relative link to
|
|
15
|
+
* another post's `.md` file becomes that post's page, so a writer links files the way the editor
|
|
16
|
+
* shows them and the published page links pages.
|
|
17
|
+
*/
|
|
18
|
+
export declare function postUrlResolver(config: Config, post: BlogPost, postsDir: string, posts: readonly BlogPost[]): (url: string) => string;
|
|
19
|
+
export declare function formatPostDate(date: string, lang: string): string;
|
|
20
|
+
/** `summary`, or the start of the body when the post has none. */
|
|
21
|
+
export declare function postDescription(post: BlogPost): string;
|
|
22
|
+
export interface BlogIndexOptions {
|
|
23
|
+
/** Every post of the site, all languages: which other languages have a blog decides the switch. */
|
|
24
|
+
posts: readonly BlogPost[];
|
|
25
|
+
/** Renders the page of one tag instead of the whole index. */
|
|
26
|
+
tag?: string;
|
|
27
|
+
ogImage?: string;
|
|
28
|
+
}
|
|
29
|
+
/** `/<lang>/blog/` or `/<lang>/blog/tags/<tag>/`. */
|
|
30
|
+
export declare function renderBlogIndex(shell: Shell, options: BlogIndexOptions): string;
|
|
31
|
+
export interface BlogPostOptions {
|
|
32
|
+
posts: readonly BlogPost[];
|
|
33
|
+
/** The post's body, rendered — with `postUrlResolver` applied. */
|
|
34
|
+
html: string;
|
|
35
|
+
author: {
|
|
36
|
+
name: string;
|
|
37
|
+
url?: string;
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
/** `/<lang>/blog/<slug>/`. */
|
|
41
|
+
export declare function renderBlogPost(shell: Shell, post: BlogPost, options: BlogPostOptions): string;
|
|
42
|
+
/** The newest posts as a landing-page section; empty when the language has none. */
|
|
43
|
+
export declare function latestPostsSection(shell: Shell, posts: readonly BlogPost[], count?: number): string;
|
|
44
|
+
export interface BlogFeedOptions {
|
|
45
|
+
posts: readonly BlogPost[];
|
|
46
|
+
tag?: string;
|
|
47
|
+
author: {
|
|
48
|
+
name: string;
|
|
49
|
+
uri?: string;
|
|
50
|
+
};
|
|
51
|
+
/** Rendered body per post, keyed `<lang>/<slug>`. */
|
|
52
|
+
html: ReadonlyMap<string, string>;
|
|
53
|
+
iconUrl?: string;
|
|
54
|
+
}
|
|
55
|
+
export declare function postKey(post: Pick<BlogPost, 'lang' | 'slug'>): string;
|
|
56
|
+
/** The Atom feed of one language, or of one tag in it. Relative URLs in the content are made absolute. */
|
|
57
|
+
export declare function renderBlogFeed(shell: Shell, options: BlogFeedOptions): string;
|
|
58
|
+
/** Every tag in one language, in first-use order. */
|
|
59
|
+
export declare function tagsOf(posts: readonly BlogPost[], lang: string): string[];
|
|
60
|
+
/** Renders a post's Markdown with its URLs resolved. */
|
|
61
|
+
export declare function renderPostBody(config: Config, post: BlogPost, postsDir: string, posts: readonly BlogPost[]): string;
|
|
62
|
+
export {};
|