website-xp-phone 1.6.6-b → 2.2.11

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 (317) hide show
  1. package/.github/workflows/ci.yml +13 -0
  2. package/.gitmodules +4 -0
  3. package/.opencode/cache/index.akinevz.html +54 -0
  4. package/Makefile +7 -7
  5. package/README.md +23 -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 +67 -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/eslint.config.js +2 -11
  20. package/package.json +21 -19
  21. package/public/Click.ogg +0 -0
  22. package/public/addons/rage-volley.txt +1 -1
  23. package/public/addons/rueg-wa-druid.txt +1 -1
  24. package/public/addons/rueg-wa-mage.txt +1 -1
  25. package/public/addons/rueg-wa-paladin.txt +1 -0
  26. package/public/addons/rueg-wa-priest.txt +1 -1
  27. package/public/addons/rueg-wa-rueg.txt +1 -1
  28. package/public/addons/rueg-wa-shaman.txt +1 -1
  29. package/public/addons/sidetip-0.1.19.zip +0 -0
  30. package/public/blog/random/branch.md +29 -0
  31. package/public/blog/random/cpus.md +39 -0
  32. package/public/blog/random/gdiff.md +5 -0
  33. package/public/blog/random/google.md +25 -0
  34. package/public/checksums.txt +87 -44
  35. package/public/documents/drift-report.json +74 -0
  36. package/public/documents/formal/resume.pdf +0 -0
  37. package/public/documents/resume.pdf +0 -0
  38. package/public/images/development-1.png +0 -0
  39. package/public/images/herbst-1.png +0 -0
  40. package/public/images/herbst-2.png +0 -0
  41. package/public/images/herbst-3.png +0 -0
  42. package/public/loading.css +8 -1
  43. package/public/resume.html +190 -228
  44. package/public/stories/1.md +360 -0
  45. package/render.yaml +1 -1
  46. package/scripts/asset-reference.mjs +37 -0
  47. package/scripts/ensure-xpcss.mjs +27 -0
  48. package/scripts/mutable-assets.mjs +46 -33
  49. package/scripts/refresh-content.mjs +28 -21
  50. package/scripts/refresh-soundcloud.mjs +153 -416
  51. package/scripts/verify-policy.mjs +196 -324
  52. package/scripts/write-dist-checksums.mjs +251 -135
  53. package/src/content/addons.json +101 -0
  54. package/{public/blog/posts.json → src/content/blog.json} +24 -100
  55. package/src/content/characters.json +43 -0
  56. package/src/content/configs.json +33 -0
  57. package/{contacts.json → src/content/contacts.json} +4 -5
  58. package/src/content/index.ts +137 -0
  59. package/src/content/music-ext.json +133 -0
  60. package/src/content/pages.json +82 -0
  61. package/src/content/projects/development.json +24 -0
  62. package/src/content/projects/doftiles.json +21 -0
  63. package/src/content/projects/frontend.json +21 -0
  64. package/src/content/projects/graphbrowser.json +21 -0
  65. package/src/content/projects/herbstutil.json +102 -0
  66. package/src/content/projects/lightdni-jssas-toggle.json +21 -0
  67. package/src/content/projects/pagerts.json +67 -0
  68. package/src/content/projects/rarebert.json +23 -0
  69. package/src/content/projects/webuser.json +21 -0
  70. package/src/content/schema.ts +87 -0
  71. package/{sections.json → src/content/sections.json} +5 -7
  72. package/{public → src/content}/soundcloud.json +230 -218
  73. package/src/lib/{audioOverlap.ts → audio.ts} +14 -0
  74. package/src/lib/clipboard.ts +47 -0
  75. package/src/lib/clippy.ts +215 -0
  76. package/src/lib/clippyRules.ts +38 -0
  77. package/src/lib/cookies.ts +11 -0
  78. package/src/lib/embeds.ts +109 -0
  79. package/src/lib/markdown.tsx +67 -0
  80. package/src/lib/musicSchema.ts +1 -0
  81. package/src/lib/theme.ts +22 -0
  82. package/src/main.tsx +19 -5
  83. package/src/pages/AddonsPage.tsx +3 -22
  84. package/src/pages/BlogPage.tsx +7 -0
  85. package/src/pages/BlogPostPage.tsx +16 -0
  86. package/src/pages/ConfigsPage.tsx +4 -0
  87. package/src/pages/ContactPage.tsx +3 -19
  88. package/src/pages/HomePage.tsx +18 -17
  89. package/src/pages/MusicPage.tsx +144 -0
  90. package/src/pages/NotFoundPage.tsx +12 -16
  91. package/src/pages/ProjectPage.tsx +22 -0
  92. package/src/pages/ProjectsPage.tsx +22 -0
  93. package/src/pages/ResumePage.tsx +31 -302
  94. package/src/pages/SitemapPage.tsx +101 -0
  95. package/src/pages/WowPage.tsx +90 -179
  96. package/src/pages/index.ts +31 -7
  97. package/src/shell/App.tsx +49 -0
  98. package/src/shell/Clippy.tsx +237 -0
  99. package/src/shell/MenuBar.tsx +58 -0
  100. package/src/shell/menu.ts +18 -0
  101. package/src/shell/resolveRoute.ts +45 -0
  102. package/src/shell/useRouteMeta.ts +23 -0
  103. package/src/styles/extras.css +400 -0
  104. package/src/styles/index.css +5 -0
  105. package/src/styles/layout.css +217 -0
  106. package/src/styles/menu.css +57 -0
  107. package/src/styles/window.css +526 -0
  108. package/src/themes/pageSlide/direction.ts +16 -0
  109. package/src/themes/pageSlide/index.ts +318 -0
  110. package/src/themes/pageSlide/pageSlide.css +185 -0
  111. package/src/window/CopyAddon.tsx +25 -0
  112. package/src/window/Embed.tsx +67 -0
  113. package/src/window/MaximizedWindow.tsx +250 -0
  114. package/src/window/OkButton.tsx +49 -0
  115. package/src/window/PageWindows.tsx +55 -0
  116. package/src/window/ResourceEmbed.tsx +39 -0
  117. package/src/window/TitleBar.tsx +47 -0
  118. package/src/window/Window.tsx +351 -0
  119. package/src/window/hidden.ts +22 -0
  120. package/src/window/index.ts +10 -0
  121. package/src/window/model.ts +125 -0
  122. package/src/window/state.tsx +208 -0
  123. package/src/window/themes.ts +9 -0
  124. package/test/asset-reference.test.js +53 -0
  125. package/test/clippy.test.js +31 -0
  126. package/test/content.test.js +101 -0
  127. package/test/model.test.js +22 -0
  128. package/test/mutable-assets.test.js +27 -0
  129. package/test/pageSlide.test.js +16 -0
  130. package/test/verify-policy.test.js +92 -0
  131. package/tsconfig.json +5 -3
  132. package/tsconfig.node.json +9 -0
  133. package/tsconfig.node.tsbuildinfo +1 -0
  134. package/tsconfig.tsbuildinfo +1 -0
  135. package/vite.config.ts +20 -286
  136. package/xp.css/.editorconfig +18 -0
  137. package/xp.css/LICENSE +7 -0
  138. package/xp.css/README.md +95 -0
  139. package/xp.css/build.js +101 -0
  140. package/xp.css/docs/docs.css +168 -0
  141. package/xp.css/docs/icon.png +0 -0
  142. package/xp.css/docs/index.html.ejs +986 -0
  143. package/xp.css/docs/script.js +51 -0
  144. package/xp.css/docs/vs.css +67 -0
  145. package/xp.css/docs/window.png +0 -0
  146. package/xp.css/gui/_buttons.scss +30 -0
  147. package/xp.css/gui/_fonts.scss +26 -0
  148. package/xp.css/gui/_forms.scss +349 -0
  149. package/xp.css/gui/_global.scss +112 -0
  150. package/xp.css/gui/_groupbox.scss +42 -0
  151. package/xp.css/gui/_tabs.scss +60 -0
  152. package/xp.css/gui/_treeview.scss +91 -0
  153. package/xp.css/gui/_variables.scss +57 -0
  154. package/xp.css/gui/_window.scss +69 -0
  155. package/xp.css/gui/fonts/converted/PerfectDOSVGA437Win.woff +0 -0
  156. package/xp.css/gui/fonts/converted/PerfectDOSVGA437Win.woff2 +0 -0
  157. package/xp.css/gui/fonts/converted/ms_sans_serif.woff +0 -0
  158. package/xp.css/gui/fonts/converted/ms_sans_serif.woff2 +0 -0
  159. package/xp.css/gui/fonts/converted/ms_sans_serif_bold.woff +0 -0
  160. package/xp.css/gui/fonts/converted/ms_sans_serif_bold.woff2 +0 -0
  161. package/xp.css/gui/fonts/src/ms-sans-serif/MS Sans Serif.ttf +0 -0
  162. package/xp.css/gui/fonts/src/ms-sans-serif/license.txt +4 -0
  163. package/xp.css/gui/fonts/src/ms-sans-serif/readme.txt +26 -0
  164. package/xp.css/gui/fonts/src/ms-sans-serif-bold/MS Sans Serif Bold.ttf +0 -0
  165. package/xp.css/gui/fonts/src/ms-sans-serif-bold/license.txt +4 -0
  166. package/xp.css/gui/fonts/src/ms-sans-serif-bold/readme.txt +26 -0
  167. package/xp.css/gui/icon/button-down-active.svg +5 -0
  168. package/xp.css/gui/icon/button-down.svg +8 -0
  169. package/xp.css/gui/icon/button-left.svg +8 -0
  170. package/xp.css/gui/icon/button-right.svg +8 -0
  171. package/xp.css/gui/icon/button-up.svg +8 -0
  172. package/xp.css/gui/icon/checkmark-disabled.svg +3 -0
  173. package/xp.css/gui/icon/checkmark.svg +3 -0
  174. package/xp.css/gui/icon/close.svg +3 -0
  175. package/xp.css/gui/icon/groupbox-border.svg +4 -0
  176. package/xp.css/gui/icon/help.svg +8 -0
  177. package/xp.css/gui/icon/indicator-horizontal.svg +6 -0
  178. package/xp.css/gui/icon/indicator-rectangle-horizontal.svg +6 -0
  179. package/xp.css/gui/icon/maximize.svg +3 -0
  180. package/xp.css/gui/icon/minimize.svg +3 -0
  181. package/xp.css/gui/icon/radio-border-disabled.svg +7 -0
  182. package/xp.css/gui/icon/radio-border.svg +8 -0
  183. package/xp.css/gui/icon/radio-dot-disabled.svg +3 -0
  184. package/xp.css/gui/icon/radio-dot.svg +3 -0
  185. package/xp.css/gui/icon/scrollbar-background.svg +4 -0
  186. package/xp.css/gui/index.scss +16 -0
  187. package/xp.css/now.json +4 -0
  188. package/xp.css/package-lock.json +1322 -0
  189. package/xp.css/package.json +38 -0
  190. package/xp.css/server.js +16 -0
  191. package/xp.css/themes/98/_buttons.scss +8 -0
  192. package/xp.css/themes/98/_forms.scss +33 -0
  193. package/xp.css/themes/98/_global.scss +18 -0
  194. package/xp.css/themes/98/_tabs.scss +39 -0
  195. package/xp.css/themes/98/_treeview.scss +8 -0
  196. package/xp.css/themes/98/_variables.scss +44 -0
  197. package/xp.css/themes/98/_window.scss +31 -0
  198. package/xp.css/themes/98/icon/button-down-active.svg +5 -0
  199. package/xp.css/themes/98/icon/button-down.svg +8 -0
  200. package/xp.css/themes/98/icon/button-left.svg +8 -0
  201. package/xp.css/themes/98/icon/button-right.svg +8 -0
  202. package/xp.css/themes/98/icon/button-up.svg +8 -0
  203. package/xp.css/themes/98/icon/checkmark-disabled.svg +3 -0
  204. package/xp.css/themes/98/icon/checkmark.svg +3 -0
  205. package/xp.css/themes/98/icon/close.svg +3 -0
  206. package/xp.css/themes/98/icon/help.svg +8 -0
  207. package/xp.css/themes/98/icon/indicator-horizontal.svg +6 -0
  208. package/xp.css/themes/98/icon/indicator-rectangle-horizontal.svg +6 -0
  209. package/xp.css/themes/98/icon/maximize.svg +3 -0
  210. package/xp.css/themes/98/icon/minimize.svg +3 -0
  211. package/xp.css/themes/98/icon/radio-border-disabled.svg +7 -0
  212. package/xp.css/themes/98/icon/radio-border.svg +8 -0
  213. package/xp.css/themes/98/icon/radio-dot-disabled.svg +3 -0
  214. package/xp.css/themes/98/icon/radio-dot.svg +3 -0
  215. package/xp.css/themes/98/icon/scrollbar-background.svg +4 -0
  216. package/xp.css/themes/98/index.scss +15 -0
  217. package/xp.css/themes/XP/_buttons.scss +43 -0
  218. package/xp.css/themes/XP/_forms.scss +241 -0
  219. package/xp.css/themes/XP/_global.scss +56 -0
  220. package/xp.css/themes/XP/_groupbox.scss +45 -0
  221. package/xp.css/themes/XP/_progressbar.scss +161 -0
  222. package/xp.css/themes/XP/_tabs.scss +56 -0
  223. package/xp.css/themes/XP/_treeview.scss +9 -0
  224. package/xp.css/themes/XP/_variables.scss +26 -0
  225. package/xp.css/themes/XP/_window.scss +126 -0
  226. package/xp.css/themes/XP/icon/checkmark-disabled.svg +3 -0
  227. package/xp.css/themes/XP/icon/checkmark.svg +3 -0
  228. package/xp.css/themes/XP/icon/close-active.svg +176 -0
  229. package/xp.css/themes/XP/icon/close-hover.svg +262 -0
  230. package/xp.css/themes/XP/icon/close.svg +284 -0
  231. package/xp.css/themes/XP/icon/dropdown-active.svg +94 -0
  232. package/xp.css/themes/XP/icon/dropdown-hover.svg +80 -0
  233. package/xp.css/themes/XP/icon/dropdown.svg +79 -0
  234. package/xp.css/themes/XP/icon/help-active.svg +192 -0
  235. package/xp.css/themes/XP/icon/help-hover.svg +316 -0
  236. package/xp.css/themes/XP/icon/help.svg +320 -0
  237. package/xp.css/themes/XP/icon/indicator-horizontal.svg +55 -0
  238. package/xp.css/themes/XP/icon/indicator-rectangle-horizontal.svg +43 -0
  239. package/xp.css/themes/XP/icon/maximize-active.svg +138 -0
  240. package/xp.css/themes/XP/icon/maximize-hover.svg +276 -0
  241. package/xp.css/themes/XP/icon/maximize.svg +289 -0
  242. package/xp.css/themes/XP/icon/minimize-active.svg +150 -0
  243. package/xp.css/themes/XP/icon/minimize-hover.svg +301 -0
  244. package/xp.css/themes/XP/icon/minimize.svg +318 -0
  245. package/xp.css/themes/XP/icon/radio-dot-disabled.svg +15 -0
  246. package/xp.css/themes/XP/icon/radio-dot.svg +19 -0
  247. package/xp.css/themes/XP/icon/restore-active.svg +179 -0
  248. package/xp.css/themes/XP/icon/restore-hover.svg +331 -0
  249. package/xp.css/themes/XP/icon/restore.svg +351 -0
  250. package/xp.css/themes/XP/icon/scroll-arrow-down.svg +117 -0
  251. package/xp.css/themes/XP/icon/scroll-arrow-left.svg +116 -0
  252. package/xp.css/themes/XP/icon/scroll-arrow-right.svg +119 -0
  253. package/xp.css/themes/XP/icon/scroll-arrow-up.svg +121 -0
  254. package/xp.css/themes/XP/icon/scroll-background-horizontal.svg +15 -0
  255. package/xp.css/themes/XP/icon/scroll-background.svg +15 -0
  256. package/xp.css/themes/XP/icon/scroll-thumb-horizontal.svg +7 -0
  257. package/xp.css/themes/XP/icon/scroll-thumb.svg +7 -0
  258. package/xp.css/themes/XP/index.scss +18 -0
  259. package/.env.firebase.example +0 -8
  260. package/FIREBASE_SETUP.md +0 -69
  261. package/addons.json +0 -90
  262. package/characters.json +0 -58
  263. package/firestore.rules +0 -29
  264. package/opencode.json +0 -20
  265. package/pagerts.json +0 -96
  266. package/pagerts_output.json +0 -1
  267. package/pages.json +0 -62
  268. package/public/blog/music-links.json +0 -133
  269. package/public/blog/soundcloud.json +0 -57
  270. package/public/images/Screenshot 2026-03-10 143742.png +0 -0
  271. package/public/images/Screenshot 2026-07-06 120649.png +0 -0
  272. package/public/incorrect_responses.txt +0 -120
  273. package/scripts/generate-soundcloud-json.mjs +0 -266
  274. package/src/App.tsx +0 -1448
  275. package/src/components/Addon.tsx +0 -28
  276. package/src/components/BlogContent.tsx +0 -98
  277. package/src/components/CopyToClipboardButton.tsx +0 -24
  278. package/src/components/MenuBar.tsx +0 -212
  279. package/src/components/MenuBarWithContext.tsx +0 -6
  280. package/src/components/Modal.tsx +0 -16
  281. package/src/components/MusicContent.tsx +0 -702
  282. package/src/components/NavBarController.tsx +0 -55
  283. package/src/components/NavBarControllerWrapper.tsx +0 -13
  284. package/src/components/Page.tsx +0 -65
  285. package/src/components/SitemapContent.tsx +0 -134
  286. package/src/config/runtimeConfig.ts +0 -57
  287. package/src/env.d.ts +0 -13
  288. package/src/lib/assistantStateMachine.ts +0 -80
  289. package/src/lib/keyboardInputUtils.ts +0 -346
  290. package/src/lib/musicSchema.mjs +0 -56
  291. package/src/lib/musicSchema.mjs.d.ts +0 -31
  292. package/src/lib/naggingAssistantClient.ts +0 -245
  293. package/src/lib/resumeAnalytics.ts +0 -176
  294. package/src/lib/urlTypes.ts +0 -44
  295. package/src/pages/PagertsPage.tsx +0 -20
  296. package/src/styles/main.css +0 -865
  297. package/src/utils/assistantReducer.ts +0 -269
  298. package/src/utils/blogSecurity.ts +0 -109
  299. package/src/utils/clippyEffectReducer.ts +0 -123
  300. package/src/utils/clippyReducer.ts +0 -122
  301. package/src/utils/menuItems.ts +0 -38
  302. package/src/utils/routingReducer.ts +0 -107
  303. package/src/windowing/MinimizedSections.tsx +0 -93
  304. package/src/windowing/OkButton.tsx +0 -277
  305. package/src/windowing/Section.tsx +0 -1392
  306. package/src/windowing/ShowPermalinkButton.tsx +0 -46
  307. package/src/windowing/context.tsx +0 -20
  308. package/src/windowing/hooks.ts +0 -129
  309. package/src/windowing/index.ts +0 -16
  310. package/src/windowing/provider.tsx +0 -111
  311. package/src/windowing/server.ts +0 -3
  312. package/src/windowing/themeEngine.ts +0 -274
  313. package/src/windowing/types.ts +0 -70
  314. package/src/windowing/utils.ts +0 -191
  315. /package/{IMMUTABLE_DEPLOYS_BLUEPRINT.md → doc/IMMUTABLE_DEPLOYS_BLUEPRINT.md} +0 -0
  316. /package/{SECURITY.md → doc/SECURITY.md} +0 -0
  317. /package/{WEBSITE_STRUCTURE.md → doc/WEBSITE_STRUCTURE.md} +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,8 +35,19 @@ 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
 
39
44
  - name: Build site
40
45
  run: npm run build
46
+
47
+ - name: Verify drift policy
48
+ run: |
49
+ if [ "${{ github.event_name }}" = "pull_request" ]; then
50
+ npm run verify:policy -- --nonblocking
51
+ else
52
+ npm run verify:policy
53
+ fi
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,20 @@
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 verify-policy 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"
14
+ @echo " make verify-policy - Fail if the drift report shows genuine drift"
15
15
  @echo " make clean - Remove dist output"
16
16
 
17
- # Full end-to-end pipeline (refresh -> policy verify -> vite build -> checksums).
18
- content-regen: refresh policy build checksums
17
+ # Full end-to-end pipeline (refresh -> vite build -> checksums).
18
+ content-regen: refresh build checksums
19
19
  @echo "Pipeline complete."
20
20
 
21
21
  regen: content-regen
@@ -26,14 +26,14 @@ refresh:
26
26
  git add -A
27
27
  git commit -a
28
28
 
29
- policy:
30
- npm run verify:policy
31
-
32
29
  build:
33
30
  npm exec -- vite build
34
31
 
35
32
  checksums:
36
33
  npm run checksums
37
34
 
35
+ verify-policy:
36
+ npm run verify:policy
37
+
38
38
  clean:
39
39
  rm -rf dist
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:
@@ -54,6 +68,15 @@ Dependency audit:
54
68
  npm run check:audit
55
69
  ```
56
70
 
71
+ Drift gate (CI runs this after every build; fails on genuine drift of
72
+ non-mutable artifacts vs the live deployment, after reconciling Vite
73
+ rebuild noise — rehashed bundle references and stale-baseline entries —
74
+ against the live site):
75
+
76
+ ```
77
+ npm run verify:policy
78
+ ```
79
+
57
80
  ## Contact
58
81
 
59
82
  If you are here to learn more about me professionally, the website includes my resume and contact details.
@@ -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,67 @@
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
+ /** Route per project mirror of src/content/index.ts: canonical + alias. */
37
+ function projectRouteConfigs(dir: string, pages: RouteConfig[]): RouteConfig[] {
38
+ const slugs = fs.readdirSync(dir).filter((name) => name.endsWith(".json"));
39
+ const projects = slugs.map((name) =>
40
+ readJson<ProjectDoc>(path.join(dir, name)),
41
+ );
42
+ const canonical = (project: ProjectDoc) =>
43
+ project.path ?? `/projects/${project.slug}`;
44
+ const short = (project: ProjectDoc) => `/${project.slug}`;
45
+ return projects.flatMap((project): RouteConfig[] => {
46
+ const taken =
47
+ pages.some((page) => page.path === short(project)) ||
48
+ projects.some(
49
+ (other) => other !== project && canonical(other) === short(project),
50
+ );
51
+ const common = {
52
+ title: project.title,
53
+ description: project.description,
54
+ };
55
+ const base: RouteConfig = { path: canonical(project), ...common };
56
+ return taken
57
+ ? [base]
58
+ : [base, { path: short(project), ...common, hidden: true }];
59
+ });
60
+ }
61
+
62
+ /** Routes: src/content/pages.json plus one route per src/content/projects/*.json. */
63
+ export function loadRoutes(): RouteConfig[] {
64
+ const pages = readJson<RouteConfig[]>(path.join(CONTENT, "pages.json"));
65
+ const dir = path.join(CONTENT, "projects");
66
+ return [...pages, ...projectRouteConfigs(dir, pages)];
67
+ }
@@ -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
+ }