@gjsify/site 0.64.11 → 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.
Files changed (49) hide show
  1. package/README.md +85 -19
  2. package/lib/cli.js +4 -14
  3. package/lib/client/site.css +283 -3
  4. package/lib/index.d.ts +5 -0
  5. package/lib/index.js +5 -0
  6. package/lib/render/blog.d.ts +62 -0
  7. package/lib/render/blog.js +240 -0
  8. package/lib/render/developer.d.ts +30 -0
  9. package/lib/render/developer.js +285 -0
  10. package/lib/render/feed.d.ts +25 -0
  11. package/lib/render/feed.js +97 -0
  12. package/lib/render/i18n.d.ts +2 -2
  13. package/lib/render/i18n.js +2 -0
  14. package/lib/render/icons.d.ts +1 -1
  15. package/lib/render/icons.js +3 -0
  16. package/lib/render/image-backend.d.ts +2 -1
  17. package/lib/render/image-backend.js +2 -2
  18. package/lib/render/images.d.ts +2 -0
  19. package/lib/render/images.js +3 -2
  20. package/lib/render/layout.d.ts +94 -0
  21. package/lib/render/layout.js +216 -0
  22. package/lib/render/markdown.d.ts +15 -1
  23. package/lib/render/markdown.js +245 -20
  24. package/lib/render/pages.d.ts +21 -8
  25. package/lib/render/pages.js +99 -263
  26. package/lib/render/seo.d.ts +34 -1
  27. package/lib/render/seo.js +50 -4
  28. package/lib/site-data/blog.d.ts +30 -0
  29. package/lib/site-data/blog.js +226 -0
  30. package/lib/site-data/config.d.ts +7 -2
  31. package/lib/site-data/config.js +118 -23
  32. package/lib/site-data/developer.d.ts +16 -0
  33. package/lib/site-data/developer.js +12 -0
  34. package/lib/site-data/frontmatter.d.ts +7 -0
  35. package/lib/site-data/frontmatter.js +242 -0
  36. package/lib/site-data/index.d.ts +4 -0
  37. package/lib/site-data/index.js +4 -0
  38. package/lib/site-data/related.d.ts +4 -2
  39. package/lib/site-data/related.js +8 -3
  40. package/lib/site-data/resume.d.ts +64 -0
  41. package/lib/site-data/resume.js +277 -0
  42. package/lib/site-data/types.d.ts +57 -15
  43. package/lib/site.d.ts +2 -26
  44. package/lib/site.js +211 -78
  45. package/package.json +13 -12
  46. package/po/de.po +88 -25
  47. package/po/site.pot +86 -23
  48. package/lib/render/overview.d.ts +0 -19
  49. 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. Everything on the page comes from files the
4
- project already has — the AppStream metainfo, the root `package.json`, the app's gettext
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 page title or meta description that is too short
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 `![shot](shot.png)` 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 { buildOverview, buildSite, syncSiteI18n } from './site.js';
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} pages and ${result.screenshots} screenshots`);
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) {
@@ -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 {};