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.
- package/.github/workflows/ci.yml +5 -0
- package/.gitmodules +4 -0
- package/.opencode/cache/index.akinevz.html +54 -0
- package/Makefile +3 -7
- package/README.md +14 -0
- package/build/blogDocumentsPlugin.ts +33 -0
- package/build/chunks.ts +25 -0
- package/build/env.ts +19 -0
- package/build/meta.ts +76 -0
- package/build/routeSkeletonsPlugin.ts +51 -0
- package/build/routes.ts +50 -0
- package/build/sitemap.ts +36 -0
- package/doc/ISSUES.md +397 -0
- package/doc/MAIN-PARITY-CHECKLIST.md +444 -0
- package/doc/MIGRATION_TODO.md +84 -0
- package/doc/PAGES_TYPES.md +95 -0
- package/doc/STYLES_EXPLORATION_REPORT.md +223 -0
- package/doc/UUID-SYSTEM-SPECIFICATION.md +157 -0
- package/doc/WEBSITE_STRUCTURE.md +228 -0
- package/eslint.config.js +2 -11
- package/package.json +21 -19
- package/public/Click.ogg +0 -0
- package/public/addons/sidetip-0.1.19.zip +0 -0
- package/public/blog/random/cpus.md +39 -0
- package/public/blog/random/gdiff.md +5 -0
- package/public/checksums.txt +58 -37
- package/public/documents/drift-report.json +176 -0
- package/public/documents/formal/resume.pdf +0 -0
- package/public/documents/resume.pdf +0 -0
- package/public/images/development-1.png +0 -0
- package/public/images/herbst-1.png +0 -0
- package/public/images/herbst-2.png +0 -0
- package/public/images/herbst-3.png +0 -0
- package/public/loading.css +1 -1
- package/public/resume.html +190 -228
- package/public/stories/1.md +360 -0
- package/render.yaml +12 -1
- package/scripts/ensure-xpcss.mjs +27 -0
- package/scripts/mutable-assets.mjs +40 -3
- package/scripts/refresh-content.mjs +28 -21
- package/scripts/refresh-soundcloud.mjs +116 -15
- package/src/content/addons.json +105 -0
- package/{public/blog/posts.json → src/content/blog.json} +64 -82
- package/src/content/characters.json +43 -0
- package/src/content/configs.json +29 -0
- package/{contacts.json → src/content/contacts.json} +8 -6
- package/src/content/index.ts +44 -0
- package/src/content/music-ext.json +133 -0
- package/src/content/pages.json +73 -0
- package/src/content/projects/development.json +16 -0
- package/src/content/projects/doftiles.json +13 -0
- package/src/content/projects/frontend.json +13 -0
- package/src/content/projects/graphbrowser.json +13 -0
- package/src/content/projects/herbstutil.json +13 -0
- package/src/content/projects/lightdni-jssas-toggle.json +13 -0
- package/src/content/projects/pagerts.json +56 -0
- package/src/content/projects/rarebert.json +13 -0
- package/src/content/projects/webuser.json +13 -0
- package/src/content/schema.ts +78 -0
- package/{sections.json → src/content/sections.json} +4 -5
- package/{public → src/content}/soundcloud.json +340 -388
- package/src/lib/clipboard.ts +47 -0
- package/src/lib/clippy.ts +221 -0
- package/src/lib/clippyRules.ts +38 -0
- package/src/lib/cookies.ts +11 -0
- package/src/lib/embeds.ts +109 -0
- package/src/lib/markdown.tsx +67 -0
- package/src/lib/musicSchema.ts +1 -1
- package/src/lib/theme.ts +22 -0
- package/src/main.tsx +11 -5
- package/src/pages/AddonsPage.tsx +3 -22
- package/src/pages/BlogPage.tsx +7 -0
- package/src/pages/BlogPostPage.tsx +16 -0
- package/src/pages/ConfigsPage.tsx +4 -0
- package/src/pages/ContactPage.tsx +3 -19
- package/src/pages/HomePage.tsx +18 -17
- package/src/pages/MusicPage.tsx +145 -0
- package/src/pages/NotFoundPage.tsx +12 -16
- package/src/pages/ProjectPage.tsx +22 -0
- package/src/pages/ProjectsPage.tsx +22 -0
- package/src/pages/ResumePage.tsx +31 -302
- package/src/pages/SitemapPage.tsx +101 -0
- package/src/pages/WowPage.tsx +90 -179
- package/src/pages/index.ts +31 -6
- package/src/shell/App.tsx +46 -0
- package/src/shell/Clippy.tsx +237 -0
- package/src/shell/MenuBar.tsx +58 -0
- package/src/shell/menu.ts +18 -0
- package/src/shell/resolveRoute.ts +45 -0
- package/src/shell/useRouteMeta.ts +23 -0
- package/src/styles/extras.css +345 -0
- package/src/styles/index.css +5 -0
- package/src/styles/layout.css +208 -0
- package/src/styles/menu.css +57 -0
- package/src/styles/{main.css → window.css} +73 -406
- package/src/themes/pageSlide/direction.ts +16 -0
- package/src/themes/pageSlide/index.ts +216 -0
- package/src/themes/pageSlide/pageSlide.css +92 -0
- package/src/window/CopyAddon.tsx +25 -0
- package/src/window/Embed.tsx +65 -0
- package/src/window/OkButton.tsx +44 -0
- package/src/window/PageWindows.tsx +61 -0
- package/src/window/ResourceEmbed.tsx +39 -0
- package/src/window/TitleBar.tsx +47 -0
- package/src/window/Window.tsx +275 -0
- package/src/window/hidden.ts +22 -0
- package/src/window/index.ts +10 -0
- package/src/window/model.ts +103 -0
- package/src/window/state.tsx +184 -0
- package/src/window/themes.ts +9 -0
- package/test/clippy.test.js +31 -0
- package/test/content.test.js +47 -0
- package/test/model.test.js +22 -0
- package/test/pageSlide.test.js +16 -0
- package/tsconfig.json +5 -3
- package/tsconfig.node.json +9 -0
- package/tsconfig.node.tsbuildinfo +1 -0
- package/tsconfig.tsbuildinfo +1 -1
- package/vite.config.ts +20 -286
- package/xp.css/.editorconfig +18 -0
- package/xp.css/LICENSE +7 -0
- package/xp.css/README.md +85 -0
- package/xp.css/build.js +101 -0
- package/xp.css/docs/docs.css +168 -0
- package/xp.css/docs/icon.png +0 -0
- package/xp.css/docs/index.html.ejs +986 -0
- package/xp.css/docs/script.js +51 -0
- package/xp.css/docs/vs.css +67 -0
- package/xp.css/docs/window.png +0 -0
- package/xp.css/gui/_buttons.scss +30 -0
- package/xp.css/gui/_fonts.scss +26 -0
- package/xp.css/gui/_forms.scss +349 -0
- package/xp.css/gui/_global.scss +112 -0
- package/xp.css/gui/_groupbox.scss +42 -0
- package/xp.css/gui/_tabs.scss +60 -0
- package/xp.css/gui/_treeview.scss +91 -0
- package/xp.css/gui/_variables.scss +57 -0
- package/xp.css/gui/_window.scss +69 -0
- package/xp.css/gui/fonts/converted/PerfectDOSVGA437Win.woff +0 -0
- package/xp.css/gui/fonts/converted/PerfectDOSVGA437Win.woff2 +0 -0
- package/xp.css/gui/fonts/converted/ms_sans_serif.woff +0 -0
- package/xp.css/gui/fonts/converted/ms_sans_serif.woff2 +0 -0
- package/xp.css/gui/fonts/converted/ms_sans_serif_bold.woff +0 -0
- package/xp.css/gui/fonts/converted/ms_sans_serif_bold.woff2 +0 -0
- package/xp.css/gui/fonts/src/ms-sans-serif/MS Sans Serif.ttf +0 -0
- package/xp.css/gui/fonts/src/ms-sans-serif/license.txt +4 -0
- package/xp.css/gui/fonts/src/ms-sans-serif/readme.txt +26 -0
- package/xp.css/gui/fonts/src/ms-sans-serif-bold/MS Sans Serif Bold.ttf +0 -0
- package/xp.css/gui/fonts/src/ms-sans-serif-bold/license.txt +4 -0
- package/xp.css/gui/fonts/src/ms-sans-serif-bold/readme.txt +26 -0
- package/xp.css/gui/icon/button-down-active.svg +5 -0
- package/xp.css/gui/icon/button-down.svg +8 -0
- package/xp.css/gui/icon/button-left.svg +8 -0
- package/xp.css/gui/icon/button-right.svg +8 -0
- package/xp.css/gui/icon/button-up.svg +8 -0
- package/xp.css/gui/icon/checkmark-disabled.svg +3 -0
- package/xp.css/gui/icon/checkmark.svg +3 -0
- package/xp.css/gui/icon/close.svg +3 -0
- package/xp.css/gui/icon/groupbox-border.svg +4 -0
- package/xp.css/gui/icon/help.svg +8 -0
- package/xp.css/gui/icon/indicator-horizontal.svg +6 -0
- package/xp.css/gui/icon/indicator-rectangle-horizontal.svg +6 -0
- package/xp.css/gui/icon/maximize.svg +3 -0
- package/xp.css/gui/icon/minimize.svg +3 -0
- package/xp.css/gui/icon/radio-border-disabled.svg +7 -0
- package/xp.css/gui/icon/radio-border.svg +8 -0
- package/xp.css/gui/icon/radio-dot-disabled.svg +3 -0
- package/xp.css/gui/icon/radio-dot.svg +3 -0
- package/xp.css/gui/icon/scrollbar-background.svg +4 -0
- package/xp.css/gui/index.scss +16 -0
- package/xp.css/now.json +4 -0
- package/xp.css/package-lock.json +1322 -0
- package/xp.css/package.json +38 -0
- package/xp.css/server.js +16 -0
- package/xp.css/themes/98/_buttons.scss +8 -0
- package/xp.css/themes/98/_forms.scss +33 -0
- package/xp.css/themes/98/_global.scss +18 -0
- package/xp.css/themes/98/_tabs.scss +39 -0
- package/xp.css/themes/98/_treeview.scss +8 -0
- package/xp.css/themes/98/_variables.scss +44 -0
- package/xp.css/themes/98/_window.scss +31 -0
- package/xp.css/themes/98/icon/button-down-active.svg +5 -0
- package/xp.css/themes/98/icon/button-down.svg +8 -0
- package/xp.css/themes/98/icon/button-left.svg +8 -0
- package/xp.css/themes/98/icon/button-right.svg +8 -0
- package/xp.css/themes/98/icon/button-up.svg +8 -0
- package/xp.css/themes/98/icon/checkmark-disabled.svg +3 -0
- package/xp.css/themes/98/icon/checkmark.svg +3 -0
- package/xp.css/themes/98/icon/close.svg +3 -0
- package/xp.css/themes/98/icon/help.svg +8 -0
- package/xp.css/themes/98/icon/indicator-horizontal.svg +6 -0
- package/xp.css/themes/98/icon/indicator-rectangle-horizontal.svg +6 -0
- package/xp.css/themes/98/icon/maximize.svg +3 -0
- package/xp.css/themes/98/icon/minimize.svg +3 -0
- package/xp.css/themes/98/icon/radio-border-disabled.svg +7 -0
- package/xp.css/themes/98/icon/radio-border.svg +8 -0
- package/xp.css/themes/98/icon/radio-dot-disabled.svg +3 -0
- package/xp.css/themes/98/icon/radio-dot.svg +3 -0
- package/xp.css/themes/98/icon/scrollbar-background.svg +4 -0
- package/xp.css/themes/98/index.scss +15 -0
- package/xp.css/themes/XP/_buttons.scss +43 -0
- package/xp.css/themes/XP/_forms.scss +241 -0
- package/xp.css/themes/XP/_global.scss +56 -0
- package/xp.css/themes/XP/_groupbox.scss +45 -0
- package/xp.css/themes/XP/_progressbar.scss +161 -0
- package/xp.css/themes/XP/_tabs.scss +56 -0
- package/xp.css/themes/XP/_treeview.scss +9 -0
- package/xp.css/themes/XP/_variables.scss +26 -0
- package/xp.css/themes/XP/_window.scss +126 -0
- package/xp.css/themes/XP/icon/checkmark-disabled.svg +3 -0
- package/xp.css/themes/XP/icon/checkmark.svg +3 -0
- package/xp.css/themes/XP/icon/close-active.svg +176 -0
- package/xp.css/themes/XP/icon/close-hover.svg +262 -0
- package/xp.css/themes/XP/icon/close.svg +284 -0
- package/xp.css/themes/XP/icon/dropdown-active.svg +94 -0
- package/xp.css/themes/XP/icon/dropdown-hover.svg +80 -0
- package/xp.css/themes/XP/icon/dropdown.svg +79 -0
- package/xp.css/themes/XP/icon/help-active.svg +192 -0
- package/xp.css/themes/XP/icon/help-hover.svg +316 -0
- package/xp.css/themes/XP/icon/help.svg +320 -0
- package/xp.css/themes/XP/icon/indicator-horizontal.svg +55 -0
- package/xp.css/themes/XP/icon/indicator-rectangle-horizontal.svg +43 -0
- package/xp.css/themes/XP/icon/maximize-active.svg +138 -0
- package/xp.css/themes/XP/icon/maximize-hover.svg +276 -0
- package/xp.css/themes/XP/icon/maximize.svg +289 -0
- package/xp.css/themes/XP/icon/minimize-active.svg +150 -0
- package/xp.css/themes/XP/icon/minimize-hover.svg +301 -0
- package/xp.css/themes/XP/icon/minimize.svg +318 -0
- package/xp.css/themes/XP/icon/radio-dot-disabled.svg +15 -0
- package/xp.css/themes/XP/icon/radio-dot.svg +19 -0
- package/xp.css/themes/XP/icon/restore-active.svg +179 -0
- package/xp.css/themes/XP/icon/restore-hover.svg +331 -0
- package/xp.css/themes/XP/icon/restore.svg +351 -0
- package/xp.css/themes/XP/icon/scroll-arrow-down.svg +117 -0
- package/xp.css/themes/XP/icon/scroll-arrow-left.svg +116 -0
- package/xp.css/themes/XP/icon/scroll-arrow-right.svg +119 -0
- package/xp.css/themes/XP/icon/scroll-arrow-up.svg +121 -0
- package/xp.css/themes/XP/icon/scroll-background-horizontal.svg +15 -0
- package/xp.css/themes/XP/icon/scroll-background.svg +15 -0
- package/xp.css/themes/XP/icon/scroll-thumb-horizontal.svg +7 -0
- package/xp.css/themes/XP/icon/scroll-thumb.svg +7 -0
- package/xp.css/themes/XP/index.scss +18 -0
- package/.env.firebase.example +0 -8
- package/FIREBASE_SETUP.md +0 -69
- package/addons.json +0 -90
- package/characters.json +0 -58
- package/firestore.rules +0 -29
- package/pagerts_output.json +0 -1
- package/pages.json +0 -56
- package/public/blog/music-links.json +0 -133
- package/public/blog/soundcloud.json +0 -57
- package/public/images/Screenshot 2026-03-10 143742.png +0 -0
- package/public/images/Screenshot 2026-07-06 120649.png +0 -0
- package/public/incorrect_responses.txt +0 -120
- package/scripts/generate-soundcloud-json.mjs +0 -266
- package/scripts/verify-policy.mjs +0 -334
- package/src/App.tsx +0 -1420
- package/src/components/Addon.tsx +0 -28
- package/src/components/BlogContent.tsx +0 -98
- package/src/components/CopyToClipboardButton.tsx +0 -24
- package/src/components/MenuBar.tsx +0 -212
- package/src/components/MenuBarWithContext.tsx +0 -6
- package/src/components/Modal.tsx +0 -16
- package/src/components/MusicContent.tsx +0 -702
- package/src/components/NavBarController.tsx +0 -55
- package/src/components/NavBarControllerWrapper.tsx +0 -13
- package/src/components/Page.tsx +0 -65
- package/src/components/SitemapContent.tsx +0 -134
- package/src/config/runtimeConfig.ts +0 -57
- package/src/env.d.ts +0 -13
- package/src/lib/assistantStateMachine.ts +0 -80
- package/src/lib/keyboardInputUtils.ts +0 -346
- package/src/lib/musicSchema.mjs +0 -58
- package/src/lib/musicSchema.mjs.d.ts +0 -31
- package/src/lib/naggingAssistantClient.ts +0 -245
- package/src/lib/resumeAnalytics.ts +0 -176
- package/src/lib/urlTypes.ts +0 -44
- package/src/utils/assistantReducer.ts +0 -269
- package/src/utils/blogSecurity.ts +0 -109
- package/src/utils/clippyEffectReducer.ts +0 -123
- package/src/utils/clippyReducer.ts +0 -122
- package/src/utils/menuItems.ts +0 -38
- package/src/utils/routingReducer.ts +0 -105
- package/src/windowing/MinimizedSections.tsx +0 -93
- package/src/windowing/OkButton.tsx +0 -277
- package/src/windowing/Section.tsx +0 -1361
- package/src/windowing/ShowPermalinkButton.tsx +0 -46
- package/src/windowing/context.tsx +0 -20
- package/src/windowing/hooks.ts +0 -129
- package/src/windowing/index.ts +0 -16
- package/src/windowing/provider.tsx +0 -111
- package/src/windowing/server.ts +0 -3
- package/src/windowing/themeEngine.ts +0 -274
- package/src/windowing/types.ts +0 -70
- package/src/windowing/utils.ts +0 -191
- /package/{IMMUTABLE_DEPLOYS_BLUEPRINT.md → doc/IMMUTABLE_DEPLOYS_BLUEPRINT.md} +0 -0
- /package/{SECURITY.md → doc/SECURITY.md} +0 -0
- /package/src/lib/{audioOverlap.ts → audio.ts} +0 -0
package/.github/workflows/ci.yml
CHANGED
|
@@ -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,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
|
|
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 ->
|
|
18
|
-
content-regen: refresh
|
|
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
|
+
}
|
package/build/chunks.ts
ADDED
|
@@ -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
|
+
}
|
package/build/routes.ts
ADDED
|
@@ -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, "&")
|
|
20
|
+
.replace(/</g, "<")
|
|
21
|
+
.replace(/>/g, ">")
|
|
22
|
+
.replace(/"/g, """)
|
|
23
|
+
.replace(/'/g, "'");
|
|
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
|
+
}
|
package/build/sitemap.ts
ADDED
|
@@ -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
|
+
}
|