six-degrees 0.4.7 → 0.4.8

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 (172) hide show
  1. package/.next/standalone/.next/BUILD_ID +1 -1
  2. package/.next/standalone/.next/build-manifest.json +3 -3
  3. package/.next/standalone/.next/prerender-manifest.json +3 -3
  4. package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
  5. package/.next/standalone/.next/server/app/_global-error.html +1 -1
  6. package/.next/standalone/.next/server/app/_global-error.rsc +2 -2
  7. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  8. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +2 -2
  9. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  10. package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  11. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  12. package/.next/standalone/.next/server/app/_not-found.html +1 -1
  13. package/.next/standalone/.next/server/app/_not-found.rsc +13 -13
  14. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +13 -13
  15. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +11 -11
  16. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  17. package/.next/standalone/.next/server/app/api/admin-delete/route.js.nft.json +1 -1
  18. package/.next/standalone/.next/server/app/api/admin-update/route.js.nft.json +1 -1
  19. package/.next/standalone/.next/server/app/api/bridges/route.js.nft.json +1 -1
  20. package/.next/standalone/.next/server/app/api/bulk-import/route.js.nft.json +1 -1
  21. package/.next/standalone/.next/server/app/api/company-scores/legacy/route.js.nft.json +1 -1
  22. package/.next/standalone/.next/server/app/api/company-scores/route.js.nft.json +1 -1
  23. package/.next/standalone/.next/server/app/api/connections/route.js.nft.json +1 -1
  24. package/.next/standalone/.next/server/app/api/data/export/route.js.nft.json +1 -1
  25. package/.next/standalone/.next/server/app/api/data/import/route.js.nft.json +1 -1
  26. package/.next/standalone/.next/server/app/api/data/restart/route.js.nft.json +1 -1
  27. package/.next/standalone/.next/server/app/api/data/reveal/route.js.nft.json +1 -1
  28. package/.next/standalone/.next/server/app/api/data/route.js.nft.json +1 -1
  29. package/.next/standalone/.next/server/app/api/delete-cluster/route.js.nft.json +1 -1
  30. package/.next/standalone/.next/server/app/api/ingest/route.js.nft.json +1 -1
  31. package/.next/standalone/.next/server/app/api/network/route.js.nft.json +1 -1
  32. package/.next/standalone/.next/server/app/api/notifications/route.js.nft.json +1 -1
  33. package/.next/standalone/.next/server/app/api/outreach/route.js.nft.json +1 -1
  34. package/.next/standalone/.next/server/app/api/queue/route.js.nft.json +1 -1
  35. package/.next/standalone/.next/server/app/api/scraper/route.js.nft.json +1 -1
  36. package/.next/standalone/.next/server/app/api/settings/route.js.nft.json +1 -1
  37. package/.next/standalone/.next/server/app/api/settings/sector-preview/route.js.nft.json +1 -1
  38. package/.next/standalone/.next/server/app/api/settings/sector-suggestions/route.js.nft.json +1 -1
  39. package/.next/standalone/.next/server/app/api/setup-profile/route.js.nft.json +1 -1
  40. package/.next/standalone/.next/server/app/api/social/route.js.nft.json +1 -1
  41. package/.next/standalone/.next/server/app/api/unlock/route.js.nft.json +1 -1
  42. package/.next/standalone/.next/server/app/api/update/route.js.nft.json +1 -1
  43. package/.next/standalone/.next/server/app/api/update-images/route.js.nft.json +1 -1
  44. package/.next/standalone/.next/server/app/api/users/route.js.nft.json +1 -1
  45. package/.next/standalone/.next/server/app/avatars/[file]/route.js.nft.json +1 -1
  46. package/.next/standalone/.next/server/app/favicon.ico/route.js.nft.json +1 -1
  47. package/.next/standalone/.next/server/app/favicon.ico.body +0 -0
  48. package/.next/standalone/.next/server/app/import/page.js.nft.json +1 -1
  49. package/.next/standalone/.next/server/app/import/page_client-reference-manifest.js +1 -1
  50. package/.next/standalone/.next/server/app/import.html +1 -1
  51. package/.next/standalone/.next/server/app/import.rsc +15 -15
  52. package/.next/standalone/.next/server/app/import.segments/_full.segment.rsc +15 -15
  53. package/.next/standalone/.next/server/app/import.segments/_tree.segment.rsc +1 -1
  54. package/.next/standalone/.next/server/app/import.segments/import/__PAGE__.segment.rsc +13 -13
  55. package/.next/standalone/.next/server/app/index.html +1 -1
  56. package/.next/standalone/.next/server/app/index.rsc +15 -15
  57. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +13 -13
  58. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +15 -15
  59. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  60. package/.next/standalone/.next/server/app/launch/page.js.nft.json +1 -1
  61. package/.next/standalone/.next/server/app/launch/page_client-reference-manifest.js +1 -1
  62. package/.next/standalone/.next/server/app/launch.html +2 -2
  63. package/.next/standalone/.next/server/app/launch.rsc +15 -15
  64. package/.next/standalone/.next/server/app/launch.segments/_full.segment.rsc +15 -15
  65. package/.next/standalone/.next/server/app/launch.segments/_tree.segment.rsc +1 -1
  66. package/.next/standalone/.next/server/app/launch.segments/launch/__PAGE__.segment.rsc +13 -13
  67. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  68. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  69. package/.next/standalone/.next/server/app/paths/page.js.nft.json +1 -1
  70. package/.next/standalone/.next/server/app/paths/page_client-reference-manifest.js +1 -1
  71. package/.next/standalone/.next/server/app/paths.html +1 -1
  72. package/.next/standalone/.next/server/app/paths.rsc +15 -15
  73. package/.next/standalone/.next/server/app/paths.segments/_full.segment.rsc +15 -15
  74. package/.next/standalone/.next/server/app/paths.segments/_tree.segment.rsc +1 -1
  75. package/.next/standalone/.next/server/app/paths.segments/paths/__PAGE__.segment.rsc +13 -13
  76. package/.next/standalone/.next/server/app/profile/page.js.nft.json +1 -1
  77. package/.next/standalone/.next/server/app/profile/page_client-reference-manifest.js +1 -1
  78. package/.next/standalone/.next/server/app/profile.html +1 -1
  79. package/.next/standalone/.next/server/app/profile.rsc +15 -15
  80. package/.next/standalone/.next/server/app/profile.segments/_full.segment.rsc +15 -15
  81. package/.next/standalone/.next/server/app/profile.segments/_tree.segment.rsc +1 -1
  82. package/.next/standalone/.next/server/app/profile.segments/profile/__PAGE__.segment.rsc +13 -13
  83. package/.next/standalone/.next/server/app/queue/page.js.nft.json +1 -1
  84. package/.next/standalone/.next/server/app/queue/page_client-reference-manifest.js +1 -1
  85. package/.next/standalone/.next/server/app/queue.html +1 -1
  86. package/.next/standalone/.next/server/app/queue.rsc +15 -15
  87. package/.next/standalone/.next/server/app/queue.segments/_full.segment.rsc +15 -15
  88. package/.next/standalone/.next/server/app/queue.segments/_tree.segment.rsc +1 -1
  89. package/.next/standalone/.next/server/app/queue.segments/queue/__PAGE__.segment.rsc +13 -13
  90. package/.next/standalone/.next/server/app/scores/page.js.nft.json +1 -1
  91. package/.next/standalone/.next/server/app/scores/page_client-reference-manifest.js +1 -1
  92. package/.next/standalone/.next/server/app/scores.html +1 -1
  93. package/.next/standalone/.next/server/app/scores.rsc +15 -15
  94. package/.next/standalone/.next/server/app/scores.segments/_full.segment.rsc +15 -15
  95. package/.next/standalone/.next/server/app/scores.segments/_tree.segment.rsc +1 -1
  96. package/.next/standalone/.next/server/app/scores.segments/scores/__PAGE__.segment.rsc +13 -13
  97. package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
  98. package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  99. package/.next/standalone/.next/server/app/settings.html +1 -1
  100. package/.next/standalone/.next/server/app/settings.rsc +15 -15
  101. package/.next/standalone/.next/server/app/settings.segments/_full.segment.rsc +15 -15
  102. package/.next/standalone/.next/server/app/settings.segments/_tree.segment.rsc +1 -1
  103. package/.next/standalone/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +13 -13
  104. package/.next/standalone/.next/server/app/setup/page.js.nft.json +1 -1
  105. package/.next/standalone/.next/server/app/setup/page_client-reference-manifest.js +1 -1
  106. package/.next/standalone/.next/server/app/setup.html +1 -1
  107. package/.next/standalone/.next/server/app/setup.rsc +15 -15
  108. package/.next/standalone/.next/server/app/setup.segments/_full.segment.rsc +15 -15
  109. package/.next/standalone/.next/server/app/setup.segments/_tree.segment.rsc +1 -1
  110. package/.next/standalone/.next/server/app/setup.segments/setup/__PAGE__.segment.rsc +13 -13
  111. package/.next/standalone/.next/server/app/social/page.js.nft.json +1 -1
  112. package/.next/standalone/.next/server/app/social/page_client-reference-manifest.js +1 -1
  113. package/.next/standalone/.next/server/app/social.html +1 -1
  114. package/.next/standalone/.next/server/app/social.rsc +15 -15
  115. package/.next/standalone/.next/server/app/social.segments/_full.segment.rsc +15 -15
  116. package/.next/standalone/.next/server/app/social.segments/_tree.segment.rsc +1 -1
  117. package/.next/standalone/.next/server/app/social.segments/social/__PAGE__.segment.rsc +13 -13
  118. package/.next/standalone/.next/server/chunks/_1itd1o_._.js +1 -1
  119. package/.next/standalone/.next/server/chunks/_1mxn_lb._.js +1 -1
  120. package/.next/standalone/.next/server/chunks/lib_db-client_15le7cw.js +1 -1
  121. package/.next/standalone/.next/server/chunks/lib_rpc_0c9d9ti.js +5 -5
  122. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__00d8snt._.js +1 -1
  123. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__00g61b8._.js +1 -1
  124. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__08lf3gy._.js +1 -1
  125. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0hg5gib._.js +1 -1
  126. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ho-u0w._.js +1 -1
  127. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ze473p._.js +1 -1
  128. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1194jps._.js +1 -1
  129. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1cnxxc9._.js +1 -1
  130. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1jeztd-._.js +1 -1
  131. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1ll97ry._.js +1 -1
  132. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1m0255u._.js +1 -1
  133. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1w9z0zv._.js +1 -1
  134. package/.next/standalone/.next/server/chunks/ssr/app_paths_page_1ol90m7.js +1 -1
  135. package/.next/standalone/.next/server/chunks/ssr/lib_0i8v-s3._.js +1 -1
  136. package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
  137. package/.next/standalone/.next/server/middleware-manifest.json +5 -5
  138. package/.next/standalone/.next/server/pages/404.html +1 -1
  139. package/.next/standalone/.next/server/pages/500.html +1 -1
  140. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  141. package/.next/standalone/.next/server/server-reference-manifest.json +1 -1
  142. package/.next/{static/chunks/2_zu7rmb8flzl.js → standalone/.next/static/chunks/1z-zp93cyoxvz.js} +1 -1
  143. package/.next/standalone/.next/static/chunks/24tvdun1cb3g1.js +1 -0
  144. package/.next/standalone/.next/static/media/favicon.23vm2o6v3er2z.ico +0 -0
  145. package/.next/standalone/CHANGELOG.md +33 -0
  146. package/.next/standalone/CONTRIBUTING.md +5 -0
  147. package/.next/standalone/README.md +168 -56
  148. package/.next/standalone/app/favicon.ico +0 -0
  149. package/.next/standalone/app/paths/page.js +25 -18
  150. package/.next/standalone/lib/scoring.js +42 -1
  151. package/.next/standalone/package-lock.json +2 -2
  152. package/.next/standalone/package.json +1 -1
  153. package/.next/standalone/scripts/build-site.mjs +537 -0
  154. package/.next/standalone/scripts/site-markdown.mjs +188 -0
  155. package/.next/standalone/scripts/site-news.mjs +311 -0
  156. package/.next/{standalone/.next/static/chunks/2_zu7rmb8flzl.js → static/chunks/1z-zp93cyoxvz.js} +1 -1
  157. package/.next/static/chunks/24tvdun1cb3g1.js +1 -0
  158. package/.next/static/media/favicon.23vm2o6v3er2z.ico +0 -0
  159. package/CHANGELOG.md +33 -0
  160. package/README.md +168 -56
  161. package/lib/scoring.js +42 -1
  162. package/package.json +1 -1
  163. package/.next/standalone/.next/static/chunks/3p_pwu5gx2i9h.js +0 -1
  164. package/.next/standalone/.next/static/media/favicon.0zdzw5kurn8m-.ico +0 -0
  165. package/.next/static/chunks/3p_pwu5gx2i9h.js +0 -1
  166. package/.next/static/media/favicon.0zdzw5kurn8m-.ico +0 -0
  167. /package/.next/standalone/.next/static/{YNRcnda8lvMnnrOKL1L5l → T0eWMsKjgSiPnmx1E0q3G}/_buildManifest.js +0 -0
  168. /package/.next/standalone/.next/static/{YNRcnda8lvMnnrOKL1L5l → T0eWMsKjgSiPnmx1E0q3G}/_clientMiddlewareManifest.js +0 -0
  169. /package/.next/standalone/.next/static/{YNRcnda8lvMnnrOKL1L5l → T0eWMsKjgSiPnmx1E0q3G}/_ssgManifest.js +0 -0
  170. /package/.next/static/{YNRcnda8lvMnnrOKL1L5l → T0eWMsKjgSiPnmx1E0q3G}/_buildManifest.js +0 -0
  171. /package/.next/static/{YNRcnda8lvMnnrOKL1L5l → T0eWMsKjgSiPnmx1E0q3G}/_clientMiddlewareManifest.js +0 -0
  172. /package/.next/static/{YNRcnda8lvMnnrOKL1L5l → T0eWMsKjgSiPnmx1E0q3G}/_ssgManifest.js +0 -0
@@ -0,0 +1,537 @@
1
+ #!/usr/bin/env node
2
+ // Builds the website, https://sixdegreesapp.com/, from site/ into a folder.
3
+ //
4
+ // node scripts/build-site.mjs # the whole site, into _site/
5
+ // node scripts/build-site.mjs <folder> # into another folder
6
+ // node scripts/build-site.mjs --home # refresh the generated parts of site/index.html
7
+ // node scripts/build-site.mjs --check # exit 1 if site/index.html's generated parts are stale
8
+ // --no-images # skip the 1200px image copies (tests)
9
+ //
10
+ // What it makes (site/README.md has the whole picture):
11
+ // / site/index.html, its <!-- gen:NAME --> slots filled
12
+ // /download/ /docs/ /roadmap/ /about/ site/_pages/<name>.html inside the shared layout
13
+ // /blog/ and /blog/<slug>/ site/_posts/<slug>.md
14
+ // /releases/ every version in CHANGELOG.md
15
+ // /blog/feed.xml /releases/feed.xml Atom feeds
16
+ // /sitemap.xml site/sitemap.xml plus every generated page
17
+ // Everything else in site/ is copied as it is, except names starting with "_"
18
+ // and site/README.md. The README's screenshots (docs/img) come in too, and the
19
+ // app's icon as img/icon.svg unless site/img/icon.svg exists. pages.yml runs
20
+ // this and publishes the folder. No dependencies.
21
+
22
+ import { readFileSync, writeFileSync, readdirSync, mkdirSync, rmSync, cpSync, existsSync, copyFileSync, statSync } from 'node:fs';
23
+ import { execFileSync } from 'node:child_process';
24
+ import path from 'node:path';
25
+ import { fileURLToPath } from 'node:url';
26
+ import {
27
+ generate, fill, fullReleases, parseChangelog, changeCounts, highlights, longDate, countTests,
28
+ } from './site-news.mjs';
29
+ import { markdown, frontMatter, htmlFrontMatter, escapeHtml, plainText } from './site-markdown.mjs';
30
+
31
+ export const SITE = 'https://sixdegreesapp.com';
32
+ export const REPO = 'https://github.com/blakeb056/six-degrees';
33
+ const AUTHOR = { '@type': 'Person', '@id': 'https://blakeburford.com/#person', name: 'Blake Burford', url: 'https://blakeburford.com/' };
34
+ const KINDS = ['added', 'changed', 'fixed', 'removed', 'security', 'deprecated'];
35
+ const TEASERS = 3;
36
+
37
+ const read = (root, f) => readFileSync(path.join(root, f), 'utf8');
38
+ const shortDate = (iso) => longDate(iso).replace(/ (\w{3})\w* /, ' $1 ');
39
+ const anchor = (version) => `v${version}`;
40
+
41
+ // ---------------------------------------------------------------- loading
42
+
43
+ /** Everything the build reads, from a checkout at `root`. */
44
+ export function loadSite(root) {
45
+ const siteDir = path.join(root, 'site');
46
+ const partial = (name) => read(root, `site/_partials/${name}.html`).trim();
47
+ const pages = readdirSync(path.join(siteDir, '_pages')).filter((f) => f.endsWith('.html')).sort().map((f) => {
48
+ const { data, body } = htmlFrontMatter(read(root, `site/_pages/${f}`));
49
+ const name = f.replace(/\.html$/, '');
50
+ return { name, path: data.path || `/${name}/`, ...data, body };
51
+ });
52
+ const posts = readdirSync(path.join(siteDir, '_posts')).filter((f) => f.endsWith('.md')).map((f) => {
53
+ const { data, body } = frontMatter(read(root, `site/_posts/${f}`));
54
+ const slug = f.replace(/\.md$/, '');
55
+ for (const need of ['title', 'description', 'date']) {
56
+ if (!data[need]) throw new Error(`site/_posts/${f} has no "${need}:" in its front matter.`);
57
+ }
58
+ const words = plainText(body).split(/\s+/).length;
59
+ return {
60
+ slug, path: `/blog/${slug}/`, ...data, updated: data.updated || data.date, body,
61
+ html: markdown(body, { headingOffset: 1 }), minutes: Math.max(1, Math.round(words / 220)),
62
+ tags: [].concat(data.tags || []),
63
+ };
64
+ }).sort((a, b) => (a.date === b.date ? a.title.localeCompare(b.title) : b.date.localeCompare(a.date)));
65
+ const indexHtml = read(root, 'site/index.html');
66
+ return {
67
+ root,
68
+ changelog: scanWording(read(root, 'CHANGELOG.md')),
69
+ testCount: countTests(path.join(root, 'tests')),
70
+ sample: JSON.parse(read(root, 'public/demo-data.json')),
71
+ partials: { header: partial('header'), footer: partial('footer'), downloads: partial('downloads') },
72
+ pages,
73
+ posts,
74
+ indexHtml,
75
+ sitemapXml: read(root, 'site/sitemap.xml'),
76
+ // One version for the stylesheet and script on every page: whatever the home page asks for.
77
+ assetVersion: (indexHtml.match(/style\.css\?v=([\w.-]+)/) || [])[1] || '1',
78
+ };
79
+ }
80
+
81
+ // ---------------------------------------------------------------- layout
82
+
83
+ /** The shared header, with the current section marked for screen readers and the eye. */
84
+ export function header(html, current) {
85
+ return html.replace(/<a href="([^"]+)"(?![^>]*aria-current)/g, (m, href) => {
86
+ const here = current && href !== '/' && !href.includes('#') && (current === href || current.startsWith(href));
87
+ return here ? `<a href="${href}" aria-current="page"` : m;
88
+ });
89
+ }
90
+
91
+ const FEEDS = `<link rel="alternate" type="application/atom+xml" title="Six Degrees blog" href="/blog/feed.xml">
92
+ <link rel="alternate" type="application/atom+xml" title="Six Degrees releases" href="/releases/feed.xml">`;
93
+
94
+ const breadcrumbs = (trail) => ({
95
+ '@type': 'BreadcrumbList',
96
+ itemListElement: trail.map(([name, url], i) => ({ '@type': 'ListItem', position: i + 1, name, item: `${SITE}${url}` })),
97
+ });
98
+
99
+ /** A whole page: head, header, main, footer. `graph` is its JSON-LD. */
100
+ export function layout(site, { path: pagePath, title, description, graph, main, image = '/img/og-image.jpg', type = 'website' }) {
101
+ const url = `${SITE}${pagePath}`;
102
+ const img = image.startsWith('http') ? image : `${SITE}${image}`;
103
+ const ld = JSON.stringify({ '@context': 'https://schema.org', '@graph': graph }, null, 2).replace(/</g, '\\u003c');
104
+ const t = escapeHtml(title);
105
+ const d = escapeHtml(description);
106
+ return `<!doctype html>
107
+ <html lang="en">
108
+ <head>
109
+ <meta charset="utf-8">
110
+ <meta name="viewport" content="width=device-width, initial-scale=1">
111
+ <title>${t}</title>
112
+ <meta name="description" content="${d}">
113
+ <meta http-equiv="Content-Security-Policy" content="default-src 'self'; img-src 'self'; style-src 'self'; script-src 'self'; connect-src https://api.github.com; base-uri 'none'; form-action 'none'">
114
+ <meta name="referrer" content="no-referrer">
115
+ <meta name="author" content="Blake Burford">
116
+ <meta name="robots" content="index, follow, max-image-preview:large, max-snippet:-1">
117
+ <meta name="color-scheme" content="dark">
118
+ <link rel="canonical" href="${url}">
119
+ ${FEEDS}
120
+ <meta property="og:type" content="${type}">
121
+ <meta property="og:site_name" content="Six Degrees">
122
+ <meta property="og:url" content="${url}">
123
+ <meta property="og:title" content="${t}">
124
+ <meta property="og:description" content="${d}">
125
+ <meta property="og:image" content="${img}">
126
+ <meta name="twitter:card" content="summary_large_image">
127
+ <meta name="twitter:title" content="${t}">
128
+ <meta name="twitter:description" content="${d}">
129
+ <meta name="twitter:image" content="${img}">
130
+ <meta name="theme-color" content="#0b0a1c">
131
+ <link rel="icon" href="/img/icon.svg" type="image/svg+xml">
132
+ <link rel="apple-touch-icon" href="/apple-touch-icon.png">
133
+ <script type="application/ld+json">
134
+ ${ld}
135
+ </script>
136
+ <link rel="stylesheet" href="/style.css?v=${site.assetVersion}">
137
+ <script src="/app.js?v=${site.assetVersion}" defer></script>
138
+ </head>
139
+ <body>
140
+ ${header(site.partials.header, pagePath)}
141
+
142
+ <main id="main" class="page">
143
+ ${main}
144
+ </main>
145
+
146
+ ${site.partials.footer}
147
+ </body>
148
+ </html>
149
+ `;
150
+ }
151
+
152
+ function webPage(pagePath, name, description, type = 'WebPage', extra = {}) {
153
+ return {
154
+ '@type': type, '@id': `${SITE}${pagePath}#page`, url: `${SITE}${pagePath}`, name, description,
155
+ isPartOf: { '@id': `${SITE}/#website` }, about: { '@id': `${SITE}/#app` }, inLanguage: 'en', ...extra,
156
+ };
157
+ }
158
+
159
+ // ---------------------------------------------------------------- the parts pages share
160
+
161
+ const chips = (counts) => `<ul class="chips" aria-label="What changed">${
162
+ KINDS.filter((k) => counts[k]).map((k) => `<li class="k-${k}">${counts[k]} ${k}</li>`).join('')}</ul>`;
163
+
164
+ /** Cards for the newest releases, each linking to its entry on /releases/. */
165
+ export function releaseTeaser(full, count = TEASERS) {
166
+ return `\n${full.slice(0, count).map((r, i) => {
167
+ const { title, more } = highlights(r);
168
+ return ` <article class="post${i === 0 ? ' latest-post' : ''}">
169
+ <p class="post-meta"><time datetime="${r.date}">${longDate(r.date)}</time><span class="tag">v${escapeHtml(r.version)}</span>${i === 0 ? '<span class="tag latest">Latest</span>' : ''}</p>
170
+ <h3><a href="/releases/#${anchor(r.version)}">${escapeHtml(title)}</a></h3>${more.length ? `\n <p class="post-more">Also: ${more.map(escapeHtml).join(' · ')}</p>` : ''}
171
+ ${chips(changeCounts(r))}
172
+ </article>`;
173
+ }).join('\n')}\n `;
174
+ }
175
+
176
+ /** Cards for the newest blog posts. */
177
+ export function postTeaser(posts, count = TEASERS) {
178
+ return `\n${posts.slice(0, count).map((p) => ` <article class="post">
179
+ <p class="post-meta"><time datetime="${p.date}">${longDate(p.date)}</time><span>${p.minutes} min read</span></p>
180
+ <h3><a href="${p.path}">${escapeHtml(p.title)}</a></h3>
181
+ <p class="post-more">${escapeHtml(p.description)}</p>
182
+ </article>`).join('\n')}\n `;
183
+ }
184
+
185
+ /** Every value a <!-- gen:NAME --> slot can take, on any page. */
186
+ export function slotValues(site) {
187
+ const values = generate({ changelog: site.changelog, testCount: site.testCount, sample: site.sample });
188
+ const full = fullReleases(parseChangelog(site.changelog));
189
+ return {
190
+ ...values,
191
+ news: releaseTeaser(full),
192
+ 'blog-teaser': postTeaser(site.posts),
193
+ header: `\n${header(site.partials.header, '/')}\n`,
194
+ footer: `\n${site.partials.footer}\n`,
195
+ downloads: `\n${site.partials.downloads}\n`,
196
+ };
197
+ }
198
+
199
+ // ---------------------------------------------------------------- pages
200
+
201
+ function pageHead({ kicker, title, lede }) {
202
+ return ` <header class="page-hero">
203
+ ${kicker ? `<p class="kicker">${kicker}</p>` : ''}
204
+ <h1>${title}</h1>
205
+ ${lede ? `<p class="lede">${lede}</p>` : ''}
206
+ </header>`;
207
+ }
208
+
209
+ /** The FAQ on a page, as FAQPage data: each <dt> and the <dd> after it. */
210
+ export function faqFrom(html) {
211
+ const text = (s) => s.replace(/<[^>]+>/g, '').replace(/&amp;/g, '&').replace(/&quot;/g, '"').replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/\s+/g, ' ').trim();
212
+ return [...html.matchAll(/<dt>([\s\S]*?)<\/dt>\s*<dd>([\s\S]*?)<\/dd>/g)].map(([, q, a]) => ({
213
+ '@type': 'Question', name: text(q), acceptedAnswer: { '@type': 'Answer', text: text(a) },
214
+ }));
215
+ }
216
+
217
+ export function renderPage(site, page, values) {
218
+ const body = fill(page.body, values);
219
+ const graph = [
220
+ webPage(page.path, page.title, page.description, page.type || 'WebPage', page.updated ? { dateModified: page.updated } : {}),
221
+ breadcrumbs([['Six Degrees', '/'], [page.crumb || page.title, page.path]]),
222
+ ];
223
+ if (page.faq === 'true') graph.push({ '@type': 'FAQPage', '@id': `${SITE}${page.path}#faq`, mainEntity: faqFrom(body) });
224
+ return layout(site, { path: page.path, title: page.title, description: page.description, graph, main: body });
225
+ }
226
+
227
+ export function renderBlogIndex(site) {
228
+ const list = site.posts.map((p) => ` <article class="post-row">
229
+ <p class="post-meta"><time datetime="${p.date}">${longDate(p.date)}</time><span>${p.minutes} min read</span>${p.tags.map((t) => `<span class="tag">${escapeHtml(t)}</span>`).join('')}</p>
230
+ <h2><a href="${p.path}">${escapeHtml(p.title)}</a></h2>
231
+ <p>${escapeHtml(p.description)}</p>
232
+ <a class="more" href="${p.path}">Read it<span class="sr-only">: ${escapeHtml(p.title)}</span> →</a>
233
+ </article>`).join('\n');
234
+ const title = 'Blog: how Six Degrees works, and why';
235
+ const description = 'Notes on how Six Degrees ranks who can introduce you, why it runs on your own computer, the Galaxy physics lab, and scanning slowly and safely.';
236
+ const main = `${pageHead({ kicker: 'Blog', title: 'How it works, and why', lede: 'Notes on the ideas behind Six Degrees, written from the code. Every example uses the invented sample network that ships with the app.' })}
237
+ <div class="post-list">
238
+ ${list}
239
+ </div>
240
+ <p class="feed-line"><a href="/blog/feed.xml">Follow the blog (Atom feed)</a> · <a href="/releases/">Release notes</a></p>`;
241
+ const graph = [
242
+ { '@type': 'Blog', '@id': `${SITE}/blog/#blog`, url: `${SITE}/blog/`, name: 'Six Degrees blog', description, publisher: { '@id': AUTHOR['@id'] },
243
+ blogPost: site.posts.map((p) => ({ '@id': `${SITE}${p.path}#post` })) },
244
+ breadcrumbs([['Six Degrees', '/'], ['Blog', '/blog/']]),
245
+ ];
246
+ return layout(site, { path: '/blog/', title, description, graph, main });
247
+ }
248
+
249
+ export function renderPost(site, post) {
250
+ const i = site.posts.indexOf(post);
251
+ const newer = site.posts[i - 1];
252
+ const older = site.posts[i + 1];
253
+ const others = site.posts.filter((p) => p !== post).slice(0, 3);
254
+ const main = ` <article class="article">
255
+ <header class="page-hero article-head">
256
+ <p class="kicker"><a href="/blog/">Blog</a></p>
257
+ <h1>${escapeHtml(post.title)}</h1>
258
+ <p class="lede">${escapeHtml(post.description)}</p>
259
+ <p class="post-meta center"><time datetime="${post.date}">${longDate(post.date)}</time><span>${post.minutes} min read</span><span>By Blake Burford</span></p>
260
+ </header>
261
+ <div class="prose">
262
+ ${post.html}
263
+ </div>
264
+ <footer class="article-foot">
265
+ <p class="fine">Every person named in this post is invented: they're from the sample network that ships with Six Degrees.
266
+ Something wrong? <a href="${REPO}/issues">Open an issue</a>.</p>
267
+ <nav class="post-nav" aria-label="More posts">
268
+ ${older ? `<a href="${older.path}"><span>Older</span>${escapeHtml(older.title)}</a>` : '<span></span>'}
269
+ ${newer ? `<a class="newer" href="${newer.path}"><span>Newer</span>${escapeHtml(newer.title)}</a>` : '<span></span>'}
270
+ </nav>
271
+ </footer>
272
+ </article>
273
+ <section class="more-posts">
274
+ <h2>More from the blog</h2>
275
+ <div class="posts">${postTeaser(others)}</div>
276
+ <p class="more-links"><a class="button" href="/download/">Download Six Degrees</a> <a href="/blog/">All posts</a></p>
277
+ </section>`;
278
+ const graph = [
279
+ {
280
+ '@type': 'BlogPosting', '@id': `${SITE}${post.path}#post`, url: `${SITE}${post.path}`, mainEntityOfPage: `${SITE}${post.path}`,
281
+ headline: post.title, description: post.description, datePublished: post.date, dateModified: post.updated,
282
+ author: AUTHOR, publisher: { '@id': AUTHOR['@id'] }, image: `${SITE}${post.image || '/img/og-image.jpg'}`,
283
+ isPartOf: { '@id': `${SITE}/blog/#blog` }, about: { '@id': `${SITE}/#app` }, keywords: post.tags.join(', '),
284
+ wordCount: plainText(post.body).split(/\s+/).length, inLanguage: 'en',
285
+ },
286
+ breadcrumbs([['Six Degrees', '/'], ['Blog', '/blog/'], [post.title, post.path]]),
287
+ ];
288
+ return layout(site, { path: post.path, title: `${post.title} · Six Degrees`, description: post.description, graph, main, image: post.image, type: 'article' });
289
+ }
290
+
291
+ /**
292
+ * The generations before this codebase, shown on /releases/ under "Before 0.1".
293
+ * Dates and one line each: their repositories are private, so they're never
294
+ * linked. Not in the releases feed: they aren't releases of this app.
295
+ */
296
+ export const BEFORE = [
297
+ {
298
+ id: 'beta-static', stage: 'Beta', date: '2026-07-12', title: 'The static version',
299
+ text: 'Your LinkedIn network as a galaxy, with leverage tiers, bridges and introduction paths. Local-first and entirely static: the first version where your data never left your device.',
300
+ },
301
+ {
302
+ id: 'alpha-first-build', stage: 'Alpha', date: '2026-06-11', title: 'The first build',
303
+ text: '"6 Degrees of Separation": a LinkedIn network research tool with a force-directed D3 graph, hosted online. Where Six Degrees started.',
304
+ archive: 'https://six-degrees-linkedin.vercel.app/',
305
+ image: { src: '/img/first-build-june-2026.jpg', alt: 'The first build\'s welcome screen: 6 Degrees of Separation, Map your LinkedIn power network', width: 800, height: 500, caption: 'The first build, June 2026' },
306
+ },
307
+ ];
308
+
309
+ export function beforeHtml() {
310
+ const entries = BEFORE.map((b) => ` <article class="release archive" id="${b.id}">
311
+ <header>
312
+ <h2><a href="#${b.id}">${escapeHtml(b.title)}</a> <span class="tag stage">${b.stage}</span></h2>
313
+ <p class="post-meta"><time datetime="${b.date}">${longDate(b.date)}</time></p>${b.archive ? `
314
+ <p class="release-links"><a href="${b.archive}">Archive: the original prototype (June 2026), kept as it was</a></p>` : ''}
315
+ </header>
316
+ <div class="prose release-notes"><p>${escapeHtml(b.text)}</p></div>${b.image ? `
317
+ <figure class="archive-shot"><a href="${b.archive}"><img src="${b.image.src}" alt="${escapeHtml(b.image.alt)}" width="${b.image.width}" height="${b.image.height}" loading="lazy"></a><figcaption>${escapeHtml(b.image.caption)}</figcaption></figure>` : ''}
318
+ </article>`).join('\n');
319
+ return ` <section class="before" id="before-0-1" aria-labelledby="before-h">
320
+ <h2 id="before-h">Before 0.1</h2>
321
+ <p class="fine">The generations before this codebase. The open-source rebuild began on
322
+ <time datetime="2026-08-21">21 August 2026</time> and led to 0.1.0. <a href="/roadmap/#how-it-started">How it started</a>.</p>
323
+ ${entries}
324
+ </section>`;
325
+ }
326
+
327
+ /**
328
+ * The site says "scanning", never "scraping": older changelog entries used the
329
+ * other word. Outside `code` (file and route names stay as they are), each form
330
+ * is swapped for its "scan" form when the notes are published.
331
+ */
332
+ export function scanWording(md) {
333
+ const forms = [[/\bscraping\b/g, 'scanning'], [/\bScraping\b/g, 'Scanning'], [/\bscraper(s?)\b/g, 'scanner$1'], [/\bScraper(s?)\b/g, 'Scanner$1'],
334
+ [/\bscraped\b/g, 'scanned'], [/\bScraped\b/g, 'Scanned'], [/\bscrapes\b/g, 'scans'], [/\bScrapes\b/g, 'Scans'],
335
+ [/\bscrape\b/g, 'scan'], [/\bScrape\b/g, 'Scan']];
336
+ return md.split(/(`[^`]*`)/).map((part, i) => (i % 2 ? part : forms.reduce((t, [re, to]) => t.replace(re, to), part))).join('');
337
+ }
338
+
339
+ /** Every version in the changelog, newest first, with its notes in full. */
340
+ export function renderReleases(site, values) {
341
+ const all = parseChangelog(site.changelog);
342
+ const full = fullReleases(all);
343
+ const entries = all.map((r) => {
344
+ const counts = r.prerelease ? changeCounts({ ...r, betas: [] }) : changeCounts(full.find((f) => f.version === r.version));
345
+ const notes = markdown(scanWording(r.body.replace(/^\s+|\s+$/g, '')), { headingOffset: 0, ids: false });
346
+ return ` <article class="release${r.prerelease ? ' beta' : ''}" id="${anchor(r.version)}">
347
+ <header>
348
+ <h2><a href="#${anchor(r.version)}">${escapeHtml(r.version)}</a>${r.prerelease ? ' <span class="tag">beta</span>' : ''}${r === full[0] ? ' <span class="tag latest">Latest</span>' : ''}</h2>
349
+ <p class="post-meta"><time datetime="${r.date}">${longDate(r.date)}</time>${r.prerelease ? '<span>A pre-release, never installed automatically</span>' : ''}</p>
350
+ ${chips(counts)}
351
+ <p class="release-links"><a href="${REPO}/releases/tag/v${escapeHtml(r.version)}">On GitHub${r.prerelease ? '' : ', with the downloads'}</a></p>
352
+ </header>
353
+ <div class="prose release-notes">
354
+ ${notes}
355
+ </div>
356
+ </article>`;
357
+ }).join('\n');
358
+ const toc = `${full.map((r) => `<a href="#${anchor(r.version)}">${escapeHtml(r.version)}</a>`).join(' ')} <a href="#before-0-1">Before 0.1</a>`;
359
+ const title = 'Release notes: every version of Six Degrees';
360
+ const description = `Everything added, changed and fixed in each of Six Degrees' ${full.length} releases, from ${full.at(-1).version} to ${full[0].version}, newest first.`;
361
+ const main = `${pageHead({ kicker: 'Releases', title: 'Release notes', lede: `Every version, newest first, from the <a href="${REPO}/blob/main/CHANGELOG.md">changelog</a>. Downloads for each are on <a href="${REPO}/releases">GitHub Releases</a>.` })}
362
+ <div class="panel chart-group releases-chart">
363
+ <div class="panel-head">
364
+ <h2>Every release, by what it changed</h2>
365
+ <p class="fine">${values['change-count']} changes in ${values['release-count']} releases since ${values['first-release']}. Betas are counted in the release they became.</p>
366
+ </div>
367
+ <div class="chart">
368
+ <div class="bars-box tall">${values['release-chart']}</div>
369
+ <ul class="legend inline" role="list">${KINDS.slice(0, 5).map((k) => `<li class="k-${k}">${k[0].toUpperCase()}${k.slice(1)}</li>`).join('')}</ul>
370
+ </div>
371
+ </div>
372
+ <nav class="release-toc" aria-label="Versions"><span>Jump to</span> ${toc}</nav>
373
+ <div class="releases">
374
+ ${entries}
375
+ ${beforeHtml()}
376
+
377
+ </div>
378
+ <p class="feed-line"><a href="/releases/feed.xml">Follow releases (Atom feed)</a> · <a href="${REPO}/releases">All releases on GitHub</a></p>`;
379
+ const graph = [
380
+ webPage('/releases/', title, description, 'CollectionPage', { dateModified: full[0].date }),
381
+ breadcrumbs([['Six Degrees', '/'], ['Releases', '/releases/']]),
382
+ ];
383
+ return layout(site, { path: '/releases/', title, description, graph, main });
384
+ }
385
+
386
+ // ---------------------------------------------------------------- feeds and sitemap
387
+
388
+ const xml = (s) => escapeHtml(s).replace(/'/g, '&apos;');
389
+
390
+ function atom({ id, title, subtitle, self, alternate, updated, entries }) {
391
+ return `<?xml version="1.0" encoding="utf-8"?>
392
+ <feed xmlns="http://www.w3.org/2005/Atom">
393
+ <id>${id}</id>
394
+ <title>${xml(title)}</title>
395
+ <subtitle>${xml(subtitle)}</subtitle>
396
+ <link rel="self" href="${self}"/>
397
+ <link rel="alternate" type="text/html" href="${alternate}"/>
398
+ <updated>${updated}T00:00:00Z</updated>
399
+ <author><name>Blake Burford</name><uri>https://blakeburford.com/</uri></author>
400
+ <icon>${SITE}/img/icon.svg</icon>
401
+ ${entries.map((e) => ` <entry>
402
+ <id>${e.id}</id>
403
+ <title>${xml(e.title)}</title>
404
+ <link rel="alternate" type="text/html" href="${e.url}"/>
405
+ <published>${e.published}T00:00:00Z</published>
406
+ <updated>${e.updated}T00:00:00Z</updated>
407
+ <summary>${xml(e.summary)}</summary>
408
+ <content type="html">${xml(e.html)}</content>
409
+ </entry>`).join('\n')}
410
+ </feed>
411
+ `;
412
+ }
413
+
414
+ export function blogFeed(site) {
415
+ return atom({
416
+ id: `${SITE}/blog/`, title: 'Six Degrees blog', subtitle: 'How Six Degrees works, and why.',
417
+ self: `${SITE}/blog/feed.xml`, alternate: `${SITE}/blog/`, updated: site.posts.reduce((m, p) => (p.updated > m ? p.updated : m), '0000'),
418
+ entries: site.posts.map((p) => ({ id: `${SITE}${p.path}`, title: p.title, url: `${SITE}${p.path}`, published: p.date, updated: p.updated, summary: p.description, html: p.html })),
419
+ });
420
+ }
421
+
422
+ export function releaseFeed(site) {
423
+ const full = fullReleases(parseChangelog(site.changelog)).slice(0, 20);
424
+ return atom({
425
+ id: `${SITE}/releases/`, title: 'Six Degrees releases', subtitle: 'What each version added, changed and fixed.',
426
+ self: `${SITE}/releases/feed.xml`, alternate: `${SITE}/releases/`, updated: full[0].date,
427
+ entries: full.map((r) => ({
428
+ id: `${SITE}/releases/#${anchor(r.version)}`, title: `Six Degrees ${r.version}: ${highlights(r).title}`,
429
+ url: `${SITE}/releases/#${anchor(r.version)}`, published: r.date, updated: r.date,
430
+ summary: KINDS.filter((k) => changeCounts(r)[k]).map((k) => `${changeCounts(r)[k]} ${k}`).join(', '),
431
+ html: markdown(scanWording(r.body.trim()), { ids: false }),
432
+ })),
433
+ });
434
+ }
435
+
436
+ /** site/sitemap.xml (the home page, dated by hand at each release) plus every generated page. */
437
+ export function sitemap(site, extra) {
438
+ const urls = extra.map(({ path: p, lastmod }) => ` <url>
439
+ <loc>${SITE}${p}</loc>
440
+ <lastmod>${lastmod}</lastmod>
441
+ </url>`).join('\n');
442
+ return site.sitemapXml.replace(/\s*<\/urlset>\s*$/, `\n${urls}\n</urlset>\n`);
443
+ }
444
+
445
+ // ---------------------------------------------------------------- the build
446
+
447
+ // The screenshots the pages use at 1200px wide as well (srcset "…-1200").
448
+ function resize(src, dest) {
449
+ const tries = [['sips', ['-Z', '1200', src, '--out', dest]], ['convert', [src, '-resize', '1200x', dest]]];
450
+ for (const [cmd, args] of tries) {
451
+ try { execFileSync(cmd, args, { stdio: 'ignore' }); return; } catch { /* next */ }
452
+ }
453
+ copyFileSync(src, dest); // no resizer here: the full-size file stands in
454
+ }
455
+
456
+ export function build(root, out, { images = true } = {}) {
457
+ const site = loadSite(root);
458
+ const values = slotValues(site);
459
+ rmSync(out, { recursive: true, force: true });
460
+ mkdirSync(out, { recursive: true });
461
+ cpSync(path.join(root, 'site'), out, {
462
+ recursive: true,
463
+ filter: (src) => { const b = path.basename(src); return !(b.startsWith('_') || (b === 'README.md' && path.dirname(src) === path.join(root, 'site'))); },
464
+ });
465
+ const img = path.join(out, 'img');
466
+ mkdirSync(img, { recursive: true });
467
+ for (const f of ['app-window', 'degrees', 'paths']) copyFileSync(path.join(root, 'docs/img', `${f}.png`), path.join(img, `${f}.png`));
468
+ // The site's own mark (site/img/icon.svg) when it has one; else the app's icon.
469
+ if (!existsSync(path.join(root, 'site/img/icon.svg'))) copyFileSync(path.join(root, 'desktop/icon/icon.svg'), path.join(img, 'icon.svg'));
470
+ if (images) {
471
+ for (const f of ['app-window', 'degrees', 'paths']) resize(path.join(img, `${f}.png`), path.join(img, `${f}-1200.png`));
472
+ const appDir = path.join(img, 'app');
473
+ if (existsSync(appDir)) {
474
+ for (const f of readdirSync(appDir).filter((x) => /\.(jpg|png)$/.test(x) && !/-1200\./.test(x))) {
475
+ resize(path.join(appDir, f), path.join(appDir, f.replace(/\.(\w+)$/, '-1200.$1')));
476
+ }
477
+ }
478
+ }
479
+
480
+ const write = (p, html) => {
481
+ const dir = path.join(out, p);
482
+ mkdirSync(dir, { recursive: true });
483
+ writeFileSync(path.join(dir, p.endsWith('.xml') ? '' : 'index.html'), html);
484
+ };
485
+ const writeFile = (p, text) => { mkdirSync(path.dirname(path.join(out, p)), { recursive: true }); writeFileSync(path.join(out, p), text); };
486
+
487
+ writeFileSync(path.join(out, 'index.html'), fill(site.indexHtml, values));
488
+ const full = fullReleases(parseChangelog(site.changelog));
489
+ const lastPost = site.posts.reduce((m, p) => (p.updated > m ? p.updated : m), full[0].date);
490
+ const listed = [];
491
+ for (const page of site.pages) {
492
+ write(page.path, renderPage(site, page, values));
493
+ listed.push({ path: page.path, lastmod: page.updated || full[0].date });
494
+ }
495
+ write('/releases/', renderReleases(site, values));
496
+ listed.push({ path: '/releases/', lastmod: full[0].date });
497
+ write('/blog/', renderBlogIndex(site));
498
+ listed.push({ path: '/blog/', lastmod: lastPost });
499
+ for (const post of site.posts) {
500
+ write(post.path, renderPost(site, post));
501
+ listed.push({ path: post.path, lastmod: post.updated });
502
+ }
503
+ writeFile('blog/feed.xml', blogFeed(site));
504
+ writeFile('releases/feed.xml', releaseFeed(site));
505
+ writeFile('sitemap.xml', sitemap(site, listed));
506
+ return { site, values, pages: ['/', ...listed.map((l) => l.path)] };
507
+ }
508
+
509
+ function main() {
510
+ const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
511
+ const args = process.argv.slice(2);
512
+ if (args.includes('--home') || args.includes('--check')) {
513
+ const site = loadSite(root);
514
+ const after = fill(site.indexHtml, slotValues(site));
515
+ if (args.includes('--check')) {
516
+ if (after === site.indexHtml) { console.log(' ✓ site/index.html\'s generated parts are current.'); return; }
517
+ console.error('\n ✗ site/index.html\'s generated parts are out of date. Run: node scripts/build-site.mjs --home\n');
518
+ process.exit(1);
519
+ }
520
+ if (after !== site.indexHtml) writeFileSync(path.join(root, 'site/index.html'), after);
521
+ console.log(` ✓ site/index.html${after === site.indexHtml ? ' (no change)' : ' refreshed'}.`);
522
+ return;
523
+ }
524
+ const out = path.resolve(args.find((a) => !a.startsWith('--')) || path.join(root, '_site'));
525
+ const { values, pages } = build(root, out, { images: !args.includes('--no-images') });
526
+ const size = (dir) => readdirSync(dir).reduce((n, f) => { const p = path.join(dir, f); const s = statSync(p); return n + (s.isDirectory() ? size(p) : s.size); }, 0);
527
+ console.log(` ✓ Built ${pages.length} pages into ${path.relative(process.cwd(), out) || out} (${Math.round(size(out) / 1e6)} MB): v${values['latest-version']}, ${values['release-count']} releases, ${values['test-count']} tests.`);
528
+ }
529
+
530
+ if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
531
+ try {
532
+ main();
533
+ } catch (err) {
534
+ console.error(`\n ✗ ${err.message}\n`);
535
+ process.exit(1);
536
+ }
537
+ }