website-xp-phone 1.6.6-a → 2.2.1

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 (298) hide show
  1. package/.github/workflows/ci.yml +5 -0
  2. package/.gitmodules +4 -0
  3. package/.opencode/cache/index.akinevz.html +54 -0
  4. package/Makefile +3 -7
  5. package/README.md +14 -0
  6. package/build/blogDocumentsPlugin.ts +33 -0
  7. package/build/chunks.ts +25 -0
  8. package/build/env.ts +19 -0
  9. package/build/meta.ts +76 -0
  10. package/build/routeSkeletonsPlugin.ts +51 -0
  11. package/build/routes.ts +50 -0
  12. package/build/sitemap.ts +36 -0
  13. package/doc/ISSUES.md +397 -0
  14. package/doc/MAIN-PARITY-CHECKLIST.md +444 -0
  15. package/doc/MIGRATION_TODO.md +84 -0
  16. package/doc/PAGES_TYPES.md +95 -0
  17. package/doc/STYLES_EXPLORATION_REPORT.md +223 -0
  18. package/doc/UUID-SYSTEM-SPECIFICATION.md +157 -0
  19. package/doc/WEBSITE_STRUCTURE.md +228 -0
  20. package/eslint.config.js +2 -11
  21. package/package.json +21 -19
  22. package/public/Click.ogg +0 -0
  23. package/public/addons/sidetip-0.1.19.zip +0 -0
  24. package/public/blog/random/cpus.md +39 -0
  25. package/public/blog/random/gdiff.md +5 -0
  26. package/public/checksums.txt +58 -37
  27. package/public/documents/drift-report.json +176 -0
  28. package/public/documents/formal/resume.pdf +0 -0
  29. package/public/documents/resume.pdf +0 -0
  30. package/public/images/development-1.png +0 -0
  31. package/public/images/herbst-1.png +0 -0
  32. package/public/images/herbst-2.png +0 -0
  33. package/public/images/herbst-3.png +0 -0
  34. package/public/loading.css +1 -1
  35. package/public/resume.html +190 -228
  36. package/public/stories/1.md +360 -0
  37. package/render.yaml +12 -1
  38. package/scripts/ensure-xpcss.mjs +27 -0
  39. package/scripts/mutable-assets.mjs +40 -3
  40. package/scripts/refresh-content.mjs +28 -21
  41. package/scripts/refresh-soundcloud.mjs +116 -15
  42. package/src/content/addons.json +105 -0
  43. package/{public/blog/posts.json → src/content/blog.json} +64 -82
  44. package/src/content/characters.json +43 -0
  45. package/src/content/configs.json +29 -0
  46. package/{contacts.json → src/content/contacts.json} +8 -6
  47. package/src/content/index.ts +44 -0
  48. package/src/content/music-ext.json +133 -0
  49. package/src/content/pages.json +73 -0
  50. package/src/content/projects/development.json +16 -0
  51. package/src/content/projects/doftiles.json +13 -0
  52. package/src/content/projects/frontend.json +13 -0
  53. package/src/content/projects/graphbrowser.json +13 -0
  54. package/src/content/projects/herbstutil.json +13 -0
  55. package/src/content/projects/lightdni-jssas-toggle.json +13 -0
  56. package/src/content/projects/pagerts.json +56 -0
  57. package/src/content/projects/rarebert.json +13 -0
  58. package/src/content/projects/webuser.json +13 -0
  59. package/src/content/schema.ts +78 -0
  60. package/{sections.json → src/content/sections.json} +4 -5
  61. package/{public → src/content}/soundcloud.json +340 -388
  62. package/src/lib/clipboard.ts +47 -0
  63. package/src/lib/clippy.ts +221 -0
  64. package/src/lib/clippyRules.ts +38 -0
  65. package/src/lib/cookies.ts +11 -0
  66. package/src/lib/embeds.ts +109 -0
  67. package/src/lib/markdown.tsx +67 -0
  68. package/src/lib/musicSchema.ts +1 -1
  69. package/src/lib/theme.ts +22 -0
  70. package/src/main.tsx +11 -5
  71. package/src/pages/AddonsPage.tsx +3 -22
  72. package/src/pages/BlogPage.tsx +7 -0
  73. package/src/pages/BlogPostPage.tsx +16 -0
  74. package/src/pages/ConfigsPage.tsx +4 -0
  75. package/src/pages/ContactPage.tsx +3 -19
  76. package/src/pages/HomePage.tsx +18 -17
  77. package/src/pages/MusicPage.tsx +145 -0
  78. package/src/pages/NotFoundPage.tsx +12 -16
  79. package/src/pages/ProjectPage.tsx +22 -0
  80. package/src/pages/ProjectsPage.tsx +22 -0
  81. package/src/pages/ResumePage.tsx +31 -302
  82. package/src/pages/SitemapPage.tsx +101 -0
  83. package/src/pages/WowPage.tsx +90 -179
  84. package/src/pages/index.ts +31 -6
  85. package/src/shell/App.tsx +46 -0
  86. package/src/shell/Clippy.tsx +237 -0
  87. package/src/shell/MenuBar.tsx +58 -0
  88. package/src/shell/menu.ts +18 -0
  89. package/src/shell/resolveRoute.ts +45 -0
  90. package/src/shell/useRouteMeta.ts +23 -0
  91. package/src/styles/extras.css +345 -0
  92. package/src/styles/index.css +5 -0
  93. package/src/styles/layout.css +208 -0
  94. package/src/styles/menu.css +57 -0
  95. package/src/styles/{main.css → window.css} +73 -406
  96. package/src/themes/pageSlide/direction.ts +16 -0
  97. package/src/themes/pageSlide/index.ts +216 -0
  98. package/src/themes/pageSlide/pageSlide.css +92 -0
  99. package/src/window/CopyAddon.tsx +25 -0
  100. package/src/window/Embed.tsx +65 -0
  101. package/src/window/OkButton.tsx +44 -0
  102. package/src/window/PageWindows.tsx +61 -0
  103. package/src/window/ResourceEmbed.tsx +39 -0
  104. package/src/window/TitleBar.tsx +47 -0
  105. package/src/window/Window.tsx +275 -0
  106. package/src/window/hidden.ts +22 -0
  107. package/src/window/index.ts +10 -0
  108. package/src/window/model.ts +103 -0
  109. package/src/window/state.tsx +184 -0
  110. package/src/window/themes.ts +9 -0
  111. package/test/clippy.test.js +31 -0
  112. package/test/content.test.js +47 -0
  113. package/test/model.test.js +22 -0
  114. package/test/pageSlide.test.js +16 -0
  115. package/tsconfig.json +5 -3
  116. package/tsconfig.node.json +9 -0
  117. package/tsconfig.node.tsbuildinfo +1 -0
  118. package/tsconfig.tsbuildinfo +1 -1
  119. package/vite.config.ts +20 -286
  120. package/xp.css/.editorconfig +18 -0
  121. package/xp.css/LICENSE +7 -0
  122. package/xp.css/README.md +85 -0
  123. package/xp.css/build.js +101 -0
  124. package/xp.css/docs/docs.css +168 -0
  125. package/xp.css/docs/icon.png +0 -0
  126. package/xp.css/docs/index.html.ejs +986 -0
  127. package/xp.css/docs/script.js +51 -0
  128. package/xp.css/docs/vs.css +67 -0
  129. package/xp.css/docs/window.png +0 -0
  130. package/xp.css/gui/_buttons.scss +30 -0
  131. package/xp.css/gui/_fonts.scss +26 -0
  132. package/xp.css/gui/_forms.scss +349 -0
  133. package/xp.css/gui/_global.scss +112 -0
  134. package/xp.css/gui/_groupbox.scss +42 -0
  135. package/xp.css/gui/_tabs.scss +60 -0
  136. package/xp.css/gui/_treeview.scss +91 -0
  137. package/xp.css/gui/_variables.scss +57 -0
  138. package/xp.css/gui/_window.scss +69 -0
  139. package/xp.css/gui/fonts/converted/PerfectDOSVGA437Win.woff +0 -0
  140. package/xp.css/gui/fonts/converted/PerfectDOSVGA437Win.woff2 +0 -0
  141. package/xp.css/gui/fonts/converted/ms_sans_serif.woff +0 -0
  142. package/xp.css/gui/fonts/converted/ms_sans_serif.woff2 +0 -0
  143. package/xp.css/gui/fonts/converted/ms_sans_serif_bold.woff +0 -0
  144. package/xp.css/gui/fonts/converted/ms_sans_serif_bold.woff2 +0 -0
  145. package/xp.css/gui/fonts/src/ms-sans-serif/MS Sans Serif.ttf +0 -0
  146. package/xp.css/gui/fonts/src/ms-sans-serif/license.txt +4 -0
  147. package/xp.css/gui/fonts/src/ms-sans-serif/readme.txt +26 -0
  148. package/xp.css/gui/fonts/src/ms-sans-serif-bold/MS Sans Serif Bold.ttf +0 -0
  149. package/xp.css/gui/fonts/src/ms-sans-serif-bold/license.txt +4 -0
  150. package/xp.css/gui/fonts/src/ms-sans-serif-bold/readme.txt +26 -0
  151. package/xp.css/gui/icon/button-down-active.svg +5 -0
  152. package/xp.css/gui/icon/button-down.svg +8 -0
  153. package/xp.css/gui/icon/button-left.svg +8 -0
  154. package/xp.css/gui/icon/button-right.svg +8 -0
  155. package/xp.css/gui/icon/button-up.svg +8 -0
  156. package/xp.css/gui/icon/checkmark-disabled.svg +3 -0
  157. package/xp.css/gui/icon/checkmark.svg +3 -0
  158. package/xp.css/gui/icon/close.svg +3 -0
  159. package/xp.css/gui/icon/groupbox-border.svg +4 -0
  160. package/xp.css/gui/icon/help.svg +8 -0
  161. package/xp.css/gui/icon/indicator-horizontal.svg +6 -0
  162. package/xp.css/gui/icon/indicator-rectangle-horizontal.svg +6 -0
  163. package/xp.css/gui/icon/maximize.svg +3 -0
  164. package/xp.css/gui/icon/minimize.svg +3 -0
  165. package/xp.css/gui/icon/radio-border-disabled.svg +7 -0
  166. package/xp.css/gui/icon/radio-border.svg +8 -0
  167. package/xp.css/gui/icon/radio-dot-disabled.svg +3 -0
  168. package/xp.css/gui/icon/radio-dot.svg +3 -0
  169. package/xp.css/gui/icon/scrollbar-background.svg +4 -0
  170. package/xp.css/gui/index.scss +16 -0
  171. package/xp.css/now.json +4 -0
  172. package/xp.css/package-lock.json +1322 -0
  173. package/xp.css/package.json +38 -0
  174. package/xp.css/server.js +16 -0
  175. package/xp.css/themes/98/_buttons.scss +8 -0
  176. package/xp.css/themes/98/_forms.scss +33 -0
  177. package/xp.css/themes/98/_global.scss +18 -0
  178. package/xp.css/themes/98/_tabs.scss +39 -0
  179. package/xp.css/themes/98/_treeview.scss +8 -0
  180. package/xp.css/themes/98/_variables.scss +44 -0
  181. package/xp.css/themes/98/_window.scss +31 -0
  182. package/xp.css/themes/98/icon/button-down-active.svg +5 -0
  183. package/xp.css/themes/98/icon/button-down.svg +8 -0
  184. package/xp.css/themes/98/icon/button-left.svg +8 -0
  185. package/xp.css/themes/98/icon/button-right.svg +8 -0
  186. package/xp.css/themes/98/icon/button-up.svg +8 -0
  187. package/xp.css/themes/98/icon/checkmark-disabled.svg +3 -0
  188. package/xp.css/themes/98/icon/checkmark.svg +3 -0
  189. package/xp.css/themes/98/icon/close.svg +3 -0
  190. package/xp.css/themes/98/icon/help.svg +8 -0
  191. package/xp.css/themes/98/icon/indicator-horizontal.svg +6 -0
  192. package/xp.css/themes/98/icon/indicator-rectangle-horizontal.svg +6 -0
  193. package/xp.css/themes/98/icon/maximize.svg +3 -0
  194. package/xp.css/themes/98/icon/minimize.svg +3 -0
  195. package/xp.css/themes/98/icon/radio-border-disabled.svg +7 -0
  196. package/xp.css/themes/98/icon/radio-border.svg +8 -0
  197. package/xp.css/themes/98/icon/radio-dot-disabled.svg +3 -0
  198. package/xp.css/themes/98/icon/radio-dot.svg +3 -0
  199. package/xp.css/themes/98/icon/scrollbar-background.svg +4 -0
  200. package/xp.css/themes/98/index.scss +15 -0
  201. package/xp.css/themes/XP/_buttons.scss +43 -0
  202. package/xp.css/themes/XP/_forms.scss +241 -0
  203. package/xp.css/themes/XP/_global.scss +56 -0
  204. package/xp.css/themes/XP/_groupbox.scss +45 -0
  205. package/xp.css/themes/XP/_progressbar.scss +161 -0
  206. package/xp.css/themes/XP/_tabs.scss +56 -0
  207. package/xp.css/themes/XP/_treeview.scss +9 -0
  208. package/xp.css/themes/XP/_variables.scss +26 -0
  209. package/xp.css/themes/XP/_window.scss +126 -0
  210. package/xp.css/themes/XP/icon/checkmark-disabled.svg +3 -0
  211. package/xp.css/themes/XP/icon/checkmark.svg +3 -0
  212. package/xp.css/themes/XP/icon/close-active.svg +176 -0
  213. package/xp.css/themes/XP/icon/close-hover.svg +262 -0
  214. package/xp.css/themes/XP/icon/close.svg +284 -0
  215. package/xp.css/themes/XP/icon/dropdown-active.svg +94 -0
  216. package/xp.css/themes/XP/icon/dropdown-hover.svg +80 -0
  217. package/xp.css/themes/XP/icon/dropdown.svg +79 -0
  218. package/xp.css/themes/XP/icon/help-active.svg +192 -0
  219. package/xp.css/themes/XP/icon/help-hover.svg +316 -0
  220. package/xp.css/themes/XP/icon/help.svg +320 -0
  221. package/xp.css/themes/XP/icon/indicator-horizontal.svg +55 -0
  222. package/xp.css/themes/XP/icon/indicator-rectangle-horizontal.svg +43 -0
  223. package/xp.css/themes/XP/icon/maximize-active.svg +138 -0
  224. package/xp.css/themes/XP/icon/maximize-hover.svg +276 -0
  225. package/xp.css/themes/XP/icon/maximize.svg +289 -0
  226. package/xp.css/themes/XP/icon/minimize-active.svg +150 -0
  227. package/xp.css/themes/XP/icon/minimize-hover.svg +301 -0
  228. package/xp.css/themes/XP/icon/minimize.svg +318 -0
  229. package/xp.css/themes/XP/icon/radio-dot-disabled.svg +15 -0
  230. package/xp.css/themes/XP/icon/radio-dot.svg +19 -0
  231. package/xp.css/themes/XP/icon/restore-active.svg +179 -0
  232. package/xp.css/themes/XP/icon/restore-hover.svg +331 -0
  233. package/xp.css/themes/XP/icon/restore.svg +351 -0
  234. package/xp.css/themes/XP/icon/scroll-arrow-down.svg +117 -0
  235. package/xp.css/themes/XP/icon/scroll-arrow-left.svg +116 -0
  236. package/xp.css/themes/XP/icon/scroll-arrow-right.svg +119 -0
  237. package/xp.css/themes/XP/icon/scroll-arrow-up.svg +121 -0
  238. package/xp.css/themes/XP/icon/scroll-background-horizontal.svg +15 -0
  239. package/xp.css/themes/XP/icon/scroll-background.svg +15 -0
  240. package/xp.css/themes/XP/icon/scroll-thumb-horizontal.svg +7 -0
  241. package/xp.css/themes/XP/icon/scroll-thumb.svg +7 -0
  242. package/xp.css/themes/XP/index.scss +18 -0
  243. package/.env.firebase.example +0 -8
  244. package/FIREBASE_SETUP.md +0 -69
  245. package/addons.json +0 -90
  246. package/characters.json +0 -58
  247. package/firestore.rules +0 -29
  248. package/pagerts_output.json +0 -1
  249. package/pages.json +0 -56
  250. package/public/blog/music-links.json +0 -133
  251. package/public/blog/soundcloud.json +0 -57
  252. package/public/images/Screenshot 2026-03-10 143742.png +0 -0
  253. package/public/images/Screenshot 2026-07-06 120649.png +0 -0
  254. package/public/incorrect_responses.txt +0 -120
  255. package/scripts/generate-soundcloud-json.mjs +0 -266
  256. package/scripts/verify-policy.mjs +0 -334
  257. package/src/App.tsx +0 -1420
  258. package/src/components/Addon.tsx +0 -28
  259. package/src/components/BlogContent.tsx +0 -98
  260. package/src/components/CopyToClipboardButton.tsx +0 -24
  261. package/src/components/MenuBar.tsx +0 -212
  262. package/src/components/MenuBarWithContext.tsx +0 -6
  263. package/src/components/Modal.tsx +0 -16
  264. package/src/components/MusicContent.tsx +0 -702
  265. package/src/components/NavBarController.tsx +0 -55
  266. package/src/components/NavBarControllerWrapper.tsx +0 -13
  267. package/src/components/Page.tsx +0 -65
  268. package/src/components/SitemapContent.tsx +0 -134
  269. package/src/config/runtimeConfig.ts +0 -57
  270. package/src/env.d.ts +0 -13
  271. package/src/lib/assistantStateMachine.ts +0 -80
  272. package/src/lib/keyboardInputUtils.ts +0 -346
  273. package/src/lib/musicSchema.mjs +0 -58
  274. package/src/lib/musicSchema.mjs.d.ts +0 -31
  275. package/src/lib/naggingAssistantClient.ts +0 -245
  276. package/src/lib/resumeAnalytics.ts +0 -176
  277. package/src/lib/urlTypes.ts +0 -44
  278. package/src/utils/assistantReducer.ts +0 -269
  279. package/src/utils/blogSecurity.ts +0 -109
  280. package/src/utils/clippyEffectReducer.ts +0 -123
  281. package/src/utils/clippyReducer.ts +0 -122
  282. package/src/utils/menuItems.ts +0 -38
  283. package/src/utils/routingReducer.ts +0 -105
  284. package/src/windowing/MinimizedSections.tsx +0 -93
  285. package/src/windowing/OkButton.tsx +0 -277
  286. package/src/windowing/Section.tsx +0 -1361
  287. package/src/windowing/ShowPermalinkButton.tsx +0 -46
  288. package/src/windowing/context.tsx +0 -20
  289. package/src/windowing/hooks.ts +0 -129
  290. package/src/windowing/index.ts +0 -16
  291. package/src/windowing/provider.tsx +0 -111
  292. package/src/windowing/server.ts +0 -3
  293. package/src/windowing/themeEngine.ts +0 -274
  294. package/src/windowing/types.ts +0 -70
  295. package/src/windowing/utils.ts +0 -191
  296. /package/{IMMUTABLE_DEPLOYS_BLUEPRINT.md → doc/IMMUTABLE_DEPLOYS_BLUEPRINT.md} +0 -0
  297. /package/{SECURITY.md → doc/SECURITY.md} +0 -0
  298. /package/src/lib/{audioOverlap.ts → audio.ts} +0 -0
@@ -23,6 +23,8 @@ jobs:
23
23
  steps:
24
24
  - name: Checkout
25
25
  uses: actions/checkout@v6
26
+ with:
27
+ submodules: recursive
26
28
 
27
29
  - name: Setup Node.js
28
30
  uses: actions/setup-node@v6
@@ -33,6 +35,9 @@ jobs:
33
35
  - name: Install dependencies
34
36
  run: npm ci --no-audit
35
37
 
38
+ - name: Build xp.css (submodule, dist is gitignored)
39
+ run: npm --prefix xp.css install --no-audit --no-fund && npm --prefix xp.css run build
40
+
36
41
  - name: Verify lockfile STABLE
37
42
  run: git diff --exit-code package-lock.json
38
43
 
package/.gitmodules ADDED
@@ -0,0 +1,4 @@
1
+ [submodule "xp.css"]
2
+ path = xp.css
3
+ url = https://github.com/akinevz2/XP.css.git
4
+ branch = build-system-modernization
@@ -0,0 +1,54 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <meta charset="UTF-8" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=10.0, user-scalable=yes" />
7
+ <meta name="description" content="my cozy little personal website" data-route-meta="description" />
8
+ <link rel="canonical" href="https://akinevz.com/" data-route-meta="canonical" />
9
+ <meta property="og:type" content="website" />
10
+ <meta property="og:title" content="home of kine" data-route-meta="og:title" />
11
+ <meta property="og:description" content="my cozy little personal website" data-route-meta="og:description" />
12
+ <meta property="og:url" content="https://akinevz.com/" data-route-meta="og:url" />
13
+ <meta property="og:image" content="https://akinevz.com/avatar.png" data-route-meta="og:image" />
14
+ <meta name="twitter:card" content="summary" data-route-meta="twitter:card" />
15
+ <meta name="twitter:image" content="https://akinevz.com/avatar.png" data-route-meta="twitter:image" />
16
+ <meta name="twitter:title" content="home of kine" data-route-meta="twitter:title" />
17
+ <meta name="twitter:description" content="my cozy little personal website" data-route-meta="twitter:description" />
18
+ <title data-route-meta="title">home of kine</title>
19
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
20
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
21
+ <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Fira+Code:wght@400;700&display=swap" />
22
+ <style>
23
+ @keyframes clippyEnter {
24
+ 0% {
25
+ opacity: 0;
26
+ transform: translateY(100%);
27
+ }
28
+
29
+ 100% {
30
+ opacity: 1;
31
+ transform: translateY(0);
32
+ }
33
+ }
34
+ </style>
35
+ <noscript>
36
+ <link rel="stylesheet" href="/loading.css" />
37
+ </noscript>
38
+ <script type="module" crossorigin src="/assets/index-BjLmS1zP.js"></script>
39
+ <link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-hePW80VL.js">
40
+ <link rel="modulepreload" crossorigin href="/assets/markdown-9shrkdTH.js">
41
+ <link rel="modulepreload" crossorigin href="/assets/react-core-BeHEj0YX.js">
42
+ <link rel="modulepreload" crossorigin href="/assets/vendor-Co0yLJea.js">
43
+ <link rel="modulepreload" crossorigin href="/assets/firebase-D6L9R_Ai.js">
44
+ <link rel="stylesheet" crossorigin href="/assets/react-core-DVtug25g.css">
45
+ <link rel="stylesheet" crossorigin href="/assets/index-B6fcJ3lr.css">
46
+ <link rel="stylesheet" crossorigin href="/assets/xpcss-DnhuGufg.css">
47
+ <script type="application/ld+json" id="homepage-music-structured-data">{"@context":"https://schema.org","@type":"MusicGroup","name":"akinevz","alternateName":["KINE","KALE","I lied my name isn't actually KINE","I lied my name isn't actually KALE"],"url":"https://akinevz.com","genre":["Electronic","Experimental","Industrial","Drone","Glitch"],"description":"Independent sound designer, music creator, and electronic music artist.","sameAs":["https://soundcloud.com/akinevz2","https://youtube.com/@akinevz","https://x.com/akinevz","https://github.com/akinevz2"],"track":[{"@type":"MusicRecording","name":"130-D Barrel","url":"https://soundcloud.com/akinevz/130-d-barrel","byArtist":{"@type":"MusicGroup","name":"KINE"}},{"@type":"MusicRecording","name":"Beef Stroganoff","url":"https://soundcloud.com/akinevz/beef-stroganoff","byArtist":{"@type":"MusicGroup","name":"KINE"}},{"@type":"MusicRecording","name":"78-T Section","url":"https://soundcloud.com/akinevz/chamber78","byArtist":{"@type":"MusicGroup","name":"KINE"}},{"@type":"MusicRecording","name":"256v2EQ","url":"https://soundcloud.com/akinevz/256a1","byArtist":{"@type":"MusicGroup","name":"KINE"}},{"@type":"MusicRecording","name":"GloveCore (Corporate Dresscore)","url":"https://soundcloud.com/akinevz/booming-subdivisions","byArtist":{"@type":"MusicGroup","name":"KINE"}},{"@type":"MusicRecording","name":"Atrophy Cultism","url":"https://soundcloud.com/akinevz/atrophy-cultism","byArtist":{"@type":"MusicGroup","name":"KINE"}},{"@type":"MusicRecording","name":"Xen","url":"https://soundcloud.com/akinevz/xen","byArtist":{"@type":"MusicGroup","name":"KINE"}},{"@type":"MusicRecording","name":"Zone12","url":"https://soundcloud.com/akinevz/zone12","byArtist":{"@type":"MusicGroup","name":"KINE"}},{"@type":"MusicRecording","name":"Thing (Diagonal Figure)","url":"https://soundcloud.com/akinevz/thing","byArtist":{"@type":"MusicGroup","name":"KINE"}},{"@type":"MusicRecording","name":"Hoppa","url":"https://soundcloud.com/akinevz/hoppa","byArtist":{"@type":"MusicGroup","name":"KINE"}},{"@type":"MusicRecording","name":"Er","url":"https://soundcloud.com/akinevz2/wat","byArtist":{"@type":"MusicGroup","name":"KINE"}},{"@type":"MusicRecording","name":"Somewhat Ebrsting","url":"https://soundcloud.com/akinevz2/somewhat-ebrsting","byArtist":{"@type":"MusicGroup","name":"KINE"}},{"@type":"MusicRecording","name":"has","url":"https://soundcloud.com/akinevz2/has","byArtist":{"@type":"MusicGroup","name":"KINE"}},{"@type":"MusicRecording","name":"got","url":"https://soundcloud.com/akinevz2/got","byArtist":{"@type":"MusicGroup","name":"KINE"}},{"@type":"MusicRecording","name":"OKAY WHAT COLOUR??? UH HQ+","url":"https://soundcloud.com/akinevz2/glitch-boxers2","byArtist":{"@type":"MusicGroup","name":"KINE"}},{"@type":"MusicRecording","name":"YOU KNOW WHAT'S? PANTS HQ+","url":"https://soundcloud.com/akinevz2/glitch-boxers","byArtist":{"@type":"MusicGroup","name":"KINE"}},{"@type":"MusicRecording","name":"Paladin's Oath","url":"https://soundcloud.com/akinevz2/paladins-oath","byArtist":{"@type":"MusicGroup","name":"KINE"}},{"@type":"MusicRecording","name":"Palladium Spaces","url":"https://soundcloud.com/akinevz2/happy-new-year-in-a-cozy","byArtist":{"@type":"MusicGroup","name":"KINE"}},{"@type":"MusicRecording","name":"Zero Zero VIP","url":"https://soundcloud.com/akinevz2/zero-zero-yay-wip","byArtist":{"@type":"MusicGroup","name":"KINE"}},{"@type":"MusicRecording","name":"Noodle Shop Courier EQ","url":"https://soundcloud.com/akinevz2/noodle-shop-courier","byArtist":{"@type":"MusicGroup","name":"KINE"}},{"@type":"MusicRecording","name":"Memento Mori","url":"https://soundcloud.com/akinevz/bloody-bits","byArtist":{"@type":"MusicGroup","name":"KINE"}},{"@type":"MusicRecording","name":"[MAN GO, ENGINE (VRUM)*] HQ+","url":"https://soundcloud.com/akinevz/man-go-engine-vrum","byArtist":{"@type":"MusicGroup","name":"KINE"}},{"@type":"MusicRecording","name":"Counterspell","url":"https://soundcloud.com/akinevz/constant-diagonality","byArtist":{"@type":"MusicGroup","name":"KINE"}},{"@type":"MusicRecording","name":"Single Minute Vudoo","url":"https://soundcloud.com/akinevz/minute","byArtist":{"@type":"MusicGroup","name":"KINE"}},{"@type":"MusicRecording","name":"Whispered","url":"https://soundcloud.com/akinevz/out-of-breath","byArtist":{"@type":"MusicGroup","name":"KINE"}},{"@type":"MusicRecording","name":"TRY TRY TRY","url":"https://soundcloud.com/akinevz/try-try-try","byArtist":{"@type":"MusicGroup","name":"KINE"}},{"@type":"MusicRecording","name":"LATE LATE LATE","url":"https://soundcloud.com/akinevz/late-late-late","byArtist":{"@type":"MusicGroup","name":"KINE"}},{"@type":"MusicRecording","name":"REPEATING STATIC NOISE","url":"https://soundcloud.com/akinevz/repeating-static-noise","byArtist":{"@type":"MusicGroup","name":"KINE"}},{"@type":"MusicRecording","name":"JAMMED ELEVATOR FUNCTION","url":"https://soundcloud.com/akinevz/door-jamming-button","byArtist":{"@type":"MusicGroup","name":"KINE"}},{"@type":"MusicRecording","name":"CHECKPOINT MARKER","url":"https://soundcloud.com/akinevz/resummoning-fried-beans","byArtist":{"@type":"MusicGroup","name":"KINE"}},{"@type":"MusicRecording","name":"Spirit Broker","url":"https://soundcloud.com/akinevz/spiritual-overload","byArtist":{"@type":"MusicGroup","name":"KINE"}},{"@type":"MusicRecording","name":"Spiritual Overload","url":"https://soundcloud.com/akinevz/corporate-dresscore","byArtist":{"@type":"MusicGroup","name":"KINE"}},{"@type":"MusicRecording","name":"Prawn Vox","url":"https://soundcloud.com/akinevz/project-vox","byArtist":{"@type":"MusicGroup","name":"KINE"}},{"@type":"MusicRecording","name":"Blossoming Subdivisions","url":"https://soundcloud.com/akinevz/perception-recalibrator","byArtist":{"@type":"MusicGroup","name":"KINE"}},{"@type":"MusicRecording","name":"Crow HQ","url":"https://soundcloud.com/akinevz/crow","byArtist":{"@type":"MusicGroup","name":"KINE"}},{"@type":"MusicRecording","name":"Marrowful Fragments HQ","url":"https://soundcloud.com/akinevz/marrowful-fragments","byArtist":{"@type":"MusicGroup","name":"KINE"}},{"@type":"MusicRecording","name":"Dawning on the Underground","url":"https://soundcloud.com/akinevz/atrophic-cultism","byArtist":{"@type":"MusicGroup","name":"KINE"}},{"@type":"MusicRecording","name":"Amanita Temporalis HQ","url":"https://soundcloud.com/akinevz/important-cheese","byArtist":{"@type":"MusicGroup","name":"KINE"}},{"@type":"MusicRecording","name":"Ivory Guardians HQ","url":"https://soundcloud.com/akinevz/ivory-guardians","byArtist":{"@type":"MusicGroup","name":"KINE"}},{"@type":"MusicRecording","name":"Open Door HQ","url":"https://soundcloud.com/akinevz/frozen-scream","byArtist":{"@type":"MusicGroup","name":"KINE"}},{"@type":"MusicRecording","name":"Radio Contact HQ","url":"https://soundcloud.com/akinevz/radio-contact","byArtist":{"@type":"MusicGroup","name":"KINE"}},{"@type":"MusicRecording","name":"Timewarped HQ","url":"https://soundcloud.com/akinevz/timewarped","byArtist":{"@type":"MusicGroup","name":"KINE"}},{"@type":"MusicRecording","name":"Spectral Pain","url":"https://soundcloud.com/akinevz/pein","byArtist":{"@type":"MusicGroup","name":"KINE"}},{"@type":"MusicRecording","name":"Crypt Ghouls","url":"https://soundcloud.com/akinevz/crypt_ghouls","byArtist":{"@type":"MusicGroup","name":"KINE"}},{"@type":"MusicRecording","name":"Blank Note","url":"https://soundcloud.com/akinevz/blank-note","byArtist":{"@type":"MusicGroup","name":"KINE"}},{"@type":"MusicRecording","name":"Black Cat","url":"https://soundcloud.com/akinevz/a_break_in_space","byArtist":{"@type":"MusicGroup","name":"KINE"}},{"@type":"MusicRecording","name":"Good Night","url":"https://soundcloud.com/akinevz/good-night","byArtist":{"@type":"MusicGroup","name":"KINE"}},{"@type":"MusicRecording","name":"Careful Offering?","url":"https://soundcloud.com/akinevz/crow-flock","byArtist":{"@type":"MusicGroup","name":"KINE"}},{"@type":"MusicRecording","name":"Er","url":"https://soundcloud.com/akinevz/wat","byArtist":{"@type":"MusicGroup","name":"KINE"}},{"@type":"MusicRecording","name":"Warrior Stance","url":"https://soundcloud.com/akinevz/warrer-style","byArtist":{"@type":"MusicGroup","name":"KINE"}},{"@type":"MusicRecording","name":"Bit Jungle","url":"https://soundcloud.com/akinevz/bit-jungle","byArtist":{"@type":"MusicGroup","name":"KINE"}},{"@type":"MusicRecording","name":"Corrupted Moonwell HQ","url":"https://soundcloud.com/akinevz/corrupted-moonwell","byArtist":{"@type":"MusicGroup","name":"KINE"}},{"@type":"MusicRecording","name":"Hairdryer","url":"https://soundcloud.com/akinevz/hairdryer","byArtist":{"@type":"MusicGroup","name":"KINE"}},{"@type":"MusicRecording","name":"advantage","url":"https://soundcloud.com/akinevz/advantage","byArtist":{"@type":"MusicGroup","name":"KINE"}},{"@type":"MusicRecording","name":"grabs anemona.wav","url":"https://soundcloud.com/akinevz/anemona","byArtist":{"@type":"MusicGroup","name":"KINE"}},{"@type":"MusicRecording","name":"microwaved_tomato_and_mushroom.soup","url":"https://soundcloud.com/akinevz/microwaved_tomato_and_mushroom","byArtist":{"@type":"MusicGroup","name":"KINE"}},{"@type":"MusicRecording","name":"mechanical_augmentation_device.wav","url":"https://soundcloud.com/akinevz/mechanical_augmentation_device","byArtist":{"@type":"MusicGroup","name":"KINE"}},{"@type":"MusicRecording","name":"vermillion_wallpapers.jpeg","url":"https://soundcloud.com/akinevz/vermillion_wallpapers","byArtist":{"@type":"MusicGroup","name":"KINE"}},{"@type":"MusicRecording","name":"DECAF.mdx","url":"https://soundcloud.com/akinevz/displacing-caffeine","byArtist":{"@type":"MusicGroup","name":"KINE"}},{"@type":"MusicRecording","name":"On No! Not the EQ.w3m","url":"https://soundcloud.com/akinevz/oh-no-not-the-eq","byArtist":{"@type":"MusicGroup","name":"KINE"}},{"@type":"MusicRecording","name":"They Were Laid to Rest","url":"https://soundcloud.com/akinevz/they-were-laid-to-rest","byArtist":{"@type":"MusicGroup","name":"KINE"}},{"@type":"MusicRecording","name":"They Were Laid to Rest Cont.","url":"https://soundcloud.com/akinevz/u-dont-know-gabber","byArtist":{"@type":"MusicGroup","name":"KINE"}},{"@type":"MusicRecording","name":"Blank Note (FULL SEND)","url":"https://soundcloud.com/akinevz2/blank-note","byArtist":{"@type":"MusicGroup","name":"KINE"}},{"@type":"MusicRecording","name":"KITS KITS KITS EQ","url":"https://soundcloud.com/akinevz2/rip","byArtist":{"@type":"MusicGroup","name":"KINE"}},{"@type":"MusicRecording","name":"BEESNARED HQ","url":"https://soundcloud.com/akinevz2/beesnared","byArtist":{"@type":"MusicGroup","name":"KINE"}},{"@type":"MusicRecording","name":"Dwarf Tech","url":"https://soundcloud.com/akinevz2/dwarf-tech","byArtist":{"@type":"MusicGroup","name":"KINE"}},{"@type":"MusicRecording","name":"Intermittent Tensors (KINE edit)","url":"https://soundcloud.com/akinevz2/intermittent-tensors","byArtist":{"@type":"MusicGroup","name":"KINE"}},{"@type":"MusicRecording","name":"Lion's Mane","url":"https://soundcloud.com/akinevz2/round-2","byArtist":{"@type":"MusicGroup","name":"KINE"}},{"@type":"MusicRecording","name":"Rainbow Loop (N.R.D., edit)","url":"https://soundcloud.com/akinevz2/rainbow_loop","byArtist":{"@type":"MusicGroup","name":"KINE"}},{"@type":"MusicRecording","name":"Tennis Ball","url":"https://soundcloud.com/akinevz2/tennis-ball","byArtist":{"@type":"MusicGroup","name":"KINE"}},{"@type":"MusicRecording","name":"Alarm Clock","url":"https://soundcloud.com/akinevz2/10-42am","byArtist":{"@type":"MusicGroup","name":"KINE"}},{"@type":"MusicRecording","name":"Guiding Light","url":"https://soundcloud.com/akinevz2/small_guiding_light","byArtist":{"@type":"MusicGroup","name":"KINE"}},{"@type":"MusicRecording","name":"Desert Fruit","url":"https://soundcloud.com/akinevz2/small-apricot-jam","byArtist":{"@type":"MusicGroup","name":"KINE"}},{"@type":"MusicRecording","name":"stroopwafel","url":"https://soundcloud.com/akinevz2/stroopwafel","byArtist":{"@type":"MusicGroup","name":"KINE"}},{"@type":"MusicRecording","name":"straw ad","url":"https://soundcloud.com/akinevz2/infinitum","byArtist":{"@type":"MusicGroup","name":"KINE"}},{"@type":"MusicRecording","name":"Infinite Fish","url":"https://soundcloud.com/akinevz2/fishamans","byArtist":{"@type":"MusicGroup","name":"KINE"}},{"@type":"MusicRecording","name":"freg","url":"https://soundcloud.com/akinevz2/other-frog","byArtist":{"@type":"MusicGroup","name":"KINE"}},{"@type":"MusicRecording","name":"Spooky","url":"https://soundcloud.com/akinevz2/spooky","byArtist":{"@type":"MusicGroup","name":"KINE"}},{"@type":"MusicRecording","name":"Boingy","url":"https://soundcloud.com/akinevz2/boingy","byArtist":{"@type":"MusicGroup","name":"KINE"}},{"@type":"MusicRecording","name":"Panic of the Skeleton HQ","url":"https://soundcloud.com/akinevz2/panic-of-the-skeleton","byArtist":{"@type":"MusicGroup","name":"KINE"}},{"@type":"MusicRecording","name":"2.22am","url":"https://soundcloud.com/akinevz2/moonwell","byArtist":{"@type":"MusicGroup","name":"KINE"}},{"@type":"MusicRecording","name":"Crow","url":"https://soundcloud.com/akinevz2/crow","byArtist":{"@type":"MusicGroup","name":"KINE"}},{"@type":"MusicRecording","name":"Inkwell","url":"https://soundcloud.com/akinevz2/inkwell","byArtist":{"@type":"MusicGroup","name":"KINE"}},{"@type":"MusicRecording","name":"Fresh Water","url":"https://soundcloud.com/akinevz2/fresh-water","byArtist":{"@type":"MusicGroup","name":"KINE"}},{"@type":"MusicRecording","name":"Akula Shark","url":"https://soundcloud.com/akinevz2/shark","byArtist":{"@type":"MusicGroup","name":"KINE"}}]}</script>
48
+ </head>
49
+
50
+ <body>
51
+ <div id="root"></div>
52
+ </body>
53
+
54
+ </html>
package/Makefile CHANGED
@@ -2,20 +2,19 @@
2
2
 
3
3
  .DEFAULT_GOAL := help
4
4
 
5
- .PHONY: help content-regen regen refresh policy build checksums clean
5
+ .PHONY: help content-regen regen refresh build checksums clean
6
6
 
7
7
  help:
8
8
  @echo "Available targets:"
9
9
  @echo " make content-regen - Run full content regeneration pipeline"
10
10
  @echo " make regen - Alias for content-regen"
11
11
  @echo " make refresh - Refresh generated content inputs"
12
- @echo " make policy - Verify content/policy checks"
13
12
  @echo " make build - Build the production site"
14
13
  @echo " make checksums - Recompute dist checksums"
15
14
  @echo " make clean - Remove dist output"
16
15
 
17
- # Full end-to-end pipeline (refresh -> policy verify -> vite build -> checksums).
18
- content-regen: refresh policy build checksums
16
+ # Full end-to-end pipeline (refresh -> vite build -> checksums).
17
+ content-regen: refresh build checksums
19
18
  @echo "Pipeline complete."
20
19
 
21
20
  regen: content-regen
@@ -26,9 +25,6 @@ refresh:
26
25
  git add -A
27
26
  git commit -a
28
27
 
29
- policy:
30
- npm run verify:policy
31
-
32
28
  build:
33
29
  npm exec -- vite build
34
30
 
package/README.md CHANGED
@@ -28,6 +28,20 @@ This repo is where I experiment with:
28
28
  - Vite for development and builds
29
29
  - xp.css for the nostalgia
30
30
 
31
+ ### xp.css notes
32
+
33
+ `xp.css` is the `xp.css` git submodule (the [akinevz2/XP.css](https://github.com/akinevz2/XP.css) fork, branch
34
+ `build-system-modernization`), not the npm package. Its `dist/` bundles are gitignored and produced by
35
+ `node build.js` inside the submodule, so before `vite build` you need:
36
+
37
+ ```
38
+ git submodule update --init xp.css && npm --prefix xp.css install --no-audit --no-fund && npm --prefix xp.css run build
39
+ ```
40
+
41
+ `vite.config.ts` resolves the bare `xp.css/...` import specifiers to the submodule via `resolve.alias`. When the
42
+ fork merges `build-system-modernization` into `main`, repoint `.gitmodules` (`branch = build-system-modernization`
43
+ → `branch = main` or simply drop the `branch` line), then `git submodule update --remote` and commit the new submodule SHA.
44
+
31
45
  ## Local development
32
46
 
33
47
  Start the dev server:
@@ -0,0 +1,33 @@
1
+ import type { Connect, Plugin } from "vite";
2
+
3
+ const ASSETS =
4
+ /\.(md|html?|txt|xml|json|css|js|pdf|png|jpe?g|gif|webp|svg|ico|avif|mp3|mp4|ogg|wav|woff2?)$/i;
5
+
6
+ /** Extension-less /blog/** URLs are post pages; the files live under + .md. */
7
+ const BLOG_PAGE = new RegExp(
8
+ `^/blog/.+(?<!${ASSETS.source.slice(2, -1)})$`,
9
+ "i",
10
+ );
11
+
12
+ /**
13
+ * Browser page loads for /blog/** (without an extension) get the SPA shell,
14
+ * rendered as a BlogPostPage embedding /blog/**.md. Requests for existing
15
+ * files — including /blog/**.md itself — pass through untouched and receive
16
+ * the raw resource from public/.
17
+ */
18
+ const middleware: Connect.NextHandleFunction = (req, _res, next) => {
19
+ const { pathname } = new URL(req.url ?? "/", "http://localhost");
20
+ const isPage =
21
+ req.headers["sec-fetch-dest"] === "document" ||
22
+ (req.headers.accept ?? "").includes("text/html");
23
+ if (isPage && BLOG_PAGE.test(pathname)) req.url = "/index.html";
24
+ next();
25
+ };
26
+
27
+ export function blogDocumentsPlugin(): Plugin {
28
+ return {
29
+ name: "blog-documents",
30
+ configureServer: (server) => void server.middlewares.use(middleware),
31
+ configurePreviewServer: (server) => void server.middlewares.use(middleware),
32
+ };
33
+ }
@@ -0,0 +1,25 @@
1
+ /** Vendor chunk grouping, first match wins (order matters: react-* before react). */
2
+ const GROUPS: Array<[chunk: string, needles: string[]]> = [
3
+ ["markdown",
4
+ [
5
+ "react-markdown",
6
+ "rehype",
7
+ "remark",
8
+ "unified",
9
+ "mdast",
10
+ "hast",
11
+ "micromark",
12
+ ],
13
+ ],
14
+ ["toastify", ["react-toastify"]],
15
+ ["react-core", ["react", "scheduler"]],
16
+ ["xpcss", ["xp.css"]],
17
+ ];
18
+
19
+ export function manualChunks(id: string): string | undefined {
20
+ if (!id.includes("node_modules")) return undefined;
21
+ for (const [chunk, needles] of GROUPS) {
22
+ if (needles.some((n) => id.includes(n))) return chunk;
23
+ }
24
+ return "vendor";
25
+ }
package/build/env.ts ADDED
@@ -0,0 +1,19 @@
1
+ import path from "node:path";
2
+ import process from "node:process";
3
+
4
+ /** Everything the build tooling reads from the environment lives here. */
5
+ export const SITE_ORIGIN = process.env.SITE_ORIGIN ?? "https://akinevz.com";
6
+ export const DEV_HOST = process.env.VITE_DEV_HOST ?? "0.0.0.0"; // bind all interfaces: reachable from the remote dev machine (was 127.0.0.1)
7
+ export const DEV_PORT = Number(process.env.VITE_DEV_PORT ?? "8086");
8
+
9
+ export const ROOT = process.cwd();
10
+ export const DIST = path.resolve(ROOT, "dist");
11
+ export const PUBLIC = path.resolve(ROOT, "public");
12
+ export const CONTENT = path.resolve(ROOT, "src/content");
13
+
14
+ export const SECURITY_HEADERS = {
15
+ "X-Content-Type-Options": "nosniff",
16
+ "X-Frame-Options": "SAMEORIGIN",
17
+ "Content-Security-Policy": "frame-ancestors 'self'",
18
+ "Referrer-Policy": "no-referrer",
19
+ };
package/build/meta.ts ADDED
@@ -0,0 +1,76 @@
1
+ import { SITE_ORIGIN } from "./env.ts";
2
+ import { escapeHtml, normalizeRoute, type RouteConfig } from "./routes.ts";
3
+
4
+ const escapeRegExp = (s: string) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
5
+
6
+ function replaceMeta(html: string, key: string, tag: string): string {
7
+ if (key === "title") {
8
+ return html.replace(
9
+ /<title[^>]*data-route-meta=["']title["'][^>]*>[\s\S]*?<\/title>/i,
10
+ tag,
11
+ );
12
+ }
13
+ return html.replace(
14
+ new RegExp(
15
+ `<[^>]*data-route-meta=["']${escapeRegExp(key)}["'][^>]*\\/?\\s*>`,
16
+ "i",
17
+ ),
18
+ tag,
19
+ );
20
+ }
21
+
22
+ /** Rewrites the data-route-meta tags in index.html for one route. */
23
+ export function withRouteMeta(html: string, route: RouteConfig): string {
24
+ const title = escapeHtml(route.title);
25
+ const description = escapeHtml(route.description);
26
+ const url = escapeHtml(
27
+ new URL(normalizeRoute(route.path), SITE_ORIGIN).toString(),
28
+ );
29
+ const image = escapeHtml(new URL("/avatar.png", SITE_ORIGIN).toString());
30
+
31
+ const tags: Array<[string, string]> = [
32
+ ["title", `<title data-route-meta="title">${title}</title>`],
33
+ [
34
+ "description",
35
+ `<meta name="description" content="${description}" data-route-meta="description" />`,
36
+ ],
37
+ [
38
+ "canonical",
39
+ `<link rel="canonical" href="${url}" data-route-meta="canonical" />`,
40
+ ],
41
+ [
42
+ "og:title",
43
+ `<meta property="og:title" content="${title}" data-route-meta="og:title" />`,
44
+ ],
45
+ [
46
+ "og:description",
47
+ `<meta property="og:description" content="${description}" data-route-meta="og:description" />`,
48
+ ],
49
+ [
50
+ "og:url",
51
+ `<meta property="og:url" content="${url}" data-route-meta="og:url" />`,
52
+ ],
53
+ [
54
+ "og:image",
55
+ `<meta property="og:image" content="${image}" data-route-meta="og:image" />`,
56
+ ],
57
+ [
58
+ "twitter:card",
59
+ `<meta name="twitter:card" content="summary" data-route-meta="twitter:card" />`,
60
+ ],
61
+ [
62
+ "twitter:image",
63
+ `<meta name="twitter:image" content="${image}" data-route-meta="twitter:image" />`,
64
+ ],
65
+ [
66
+ "twitter:title",
67
+ `<meta name="twitter:title" content="${title}" data-route-meta="twitter:title" />`,
68
+ ],
69
+ [
70
+ "twitter:description",
71
+ `<meta name="twitter:description" content="${description}" data-route-meta="twitter:description" />`,
72
+ ],
73
+ ];
74
+
75
+ return tags.reduce((acc, [key, tag]) => replaceMeta(acc, key, tag), html);
76
+ }
@@ -0,0 +1,51 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import type { Plugin } from "vite";
4
+ import { DIST, PUBLIC } from "./env.ts";
5
+ import { withRouteMeta } from "./meta.ts";
6
+ import { loadRoutes, normalizeRoute } from "./routes.ts";
7
+ import { generateSitemap } from "./sitemap.ts";
8
+
9
+ /**
10
+ * After the bundle is written, emit one index.html per route (with that
11
+ * route's title/description meta), a `<slug>.html` sibling for static hosts,
12
+ * and sitemap.xml. The SPA still hydrates from the same bundle.
13
+ */
14
+ export function routeSkeletonsPlugin(): Plugin {
15
+ return {
16
+ name: "route-skeletons",
17
+ apply: "build",
18
+ closeBundle() {
19
+ const indexPath = path.join(DIST, "index.html");
20
+ if (!fs.existsSync(indexPath)) return;
21
+
22
+ const indexHtml = fs.readFileSync(indexPath, "utf-8");
23
+ const routes = loadRoutes();
24
+
25
+ for (const route of routes) {
26
+ const normalized = normalizeRoute(route.path);
27
+ const html = withRouteMeta(indexHtml, route);
28
+
29
+ if (normalized === "/") {
30
+ fs.writeFileSync(indexPath, html, "utf-8");
31
+ continue;
32
+ }
33
+
34
+ const slug = normalized.slice(1);
35
+ fs.mkdirSync(path.join(DIST, slug), { recursive: true });
36
+ fs.writeFileSync(path.join(DIST, slug, "index.html"), html, "utf-8");
37
+
38
+ // a hand-written public/<slug>.html wins over the generated sibling
39
+ if (!fs.existsSync(path.join(PUBLIC, `${slug}.html`))) {
40
+ fs.writeFileSync(path.join(DIST, `${slug}.html`), html, "utf-8");
41
+ }
42
+ }
43
+
44
+ fs.writeFileSync(
45
+ path.join(DIST, "sitemap.xml"),
46
+ generateSitemap(routes),
47
+ "utf-8",
48
+ );
49
+ },
50
+ };
51
+ }
@@ -0,0 +1,50 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { CONTENT } from "./env.ts";
4
+
5
+ export type RouteConfig = {
6
+ path: string;
7
+ title: string;
8
+ description: string;
9
+ hidden?: boolean;
10
+ };
11
+
12
+ export function normalizeRoute(routePath: string): string {
13
+ if (!routePath || routePath === "/") return "/";
14
+ return `/${routePath.replace(/^\/+|\/+$/g, "")}`;
15
+ }
16
+
17
+ export function escapeHtml(value: string): string {
18
+ return value
19
+ .replace(/&/g, "&amp;")
20
+ .replace(/</g, "&lt;")
21
+ .replace(/>/g, "&gt;")
22
+ .replace(/"/g, "&quot;")
23
+ .replace(/'/g, "&#39;");
24
+ }
25
+
26
+ type ProjectDoc = {
27
+ slug: string;
28
+ path?: string;
29
+ title: string;
30
+ description: string;
31
+ };
32
+
33
+ const readJson = <T>(file: string): T =>
34
+ JSON.parse(fs.readFileSync(file, "utf-8")) as T;
35
+
36
+ /** Routes: src/content/pages.json plus one route per src/content/projects/*.json. */
37
+ export function loadRoutes(): RouteConfig[] {
38
+ const pages = readJson<RouteConfig[]>(path.join(CONTENT, "pages.json"));
39
+ const dir = path.join(CONTENT, "projects");
40
+ const projects = fs
41
+ .readdirSync(dir)
42
+ .filter((name) => name.endsWith(".json"))
43
+ .map((name) => readJson<ProjectDoc>(path.join(dir, name)))
44
+ .map((project): RouteConfig => ({
45
+ path: project.path ?? `/projects/${project.slug}`,
46
+ title: project.title,
47
+ description: project.description,
48
+ }));
49
+ return [...pages, ...projects];
50
+ }
@@ -0,0 +1,36 @@
1
+ import { SITE_ORIGIN } from "./env.ts";
2
+ import { escapeHtml, normalizeRoute, type RouteConfig } from "./routes.ts";
3
+
4
+ const PRIORITY: Record<string, string> = {
5
+ "/": "1.0",
6
+ "/sitemap": "0.9",
7
+ "/music": "0.1",
8
+ "/blog": "0.1",
9
+ };
10
+ const HOURLY = new Set(["/", "/sitemap", "/music", "/blog"]);
11
+
12
+ export function generateSitemap(routes: RouteConfig[]): string {
13
+ const today = new Date().toISOString().slice(0, 10);
14
+ const urls = routes
15
+ .filter((r) => !r.hidden && normalizeRoute(r.path) !== "/404")
16
+ .map((r) => {
17
+ const route = normalizeRoute(r.path);
18
+ const loc = escapeHtml(new URL(route, SITE_ORIGIN).toString());
19
+ return [
20
+ " <url>",
21
+ ` <loc>${loc}</loc>`,
22
+ ` <lastmod>${today}</lastmod>`,
23
+ ` <changefreq>${HOURLY.has(route) ? "hourly" : "daily"}</changefreq>`,
24
+ ` <priority>${PRIORITY[route] ?? "0.7"}</priority>`,
25
+ " </url>",
26
+ ].join("\n");
27
+ })
28
+ .join("\n");
29
+
30
+ return [
31
+ '<?xml version="1.0" encoding="UTF-8"?>',
32
+ '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">',
33
+ urls,
34
+ "</urlset>",
35
+ ].join("\n");
36
+ }