@anglefeint/astro-theme 0.1.0-alpha.0

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 (33) hide show
  1. package/README.md +50 -0
  2. package/package.json +38 -0
  3. package/public/scripts/about-effects.js +535 -0
  4. package/public/scripts/blogpost-effects.js +956 -0
  5. package/public/scripts/home-matrix.js +117 -0
  6. package/public/styles/about-page.css +756 -0
  7. package/public/styles/blog-post.css +390 -0
  8. package/public/styles/home-page.css +186 -0
  9. package/src/assets/theme/placeholders/theme-placeholder-1.jpg +0 -0
  10. package/src/assets/theme/placeholders/theme-placeholder-2.jpg +0 -0
  11. package/src/assets/theme/placeholders/theme-placeholder-3.jpg +0 -0
  12. package/src/assets/theme/placeholders/theme-placeholder-4.jpg +0 -0
  13. package/src/assets/theme/placeholders/theme-placeholder-5.jpg +0 -0
  14. package/src/assets/theme/placeholders/theme-placeholder-about.jpg +0 -0
  15. package/src/assets/theme/red-queen/theme-redqueen1.webp +0 -0
  16. package/src/assets/theme/red-queen/theme-redqueen2.gif +0 -0
  17. package/src/cli-new-page.mjs +118 -0
  18. package/src/cli-new-post.mjs +139 -0
  19. package/src/components/BaseHead.astro +177 -0
  20. package/src/components/Footer.astro +74 -0
  21. package/src/components/FormattedDate.astro +17 -0
  22. package/src/components/Header.astro +328 -0
  23. package/src/components/HeaderLink.astro +35 -0
  24. package/src/consts.ts +12 -0
  25. package/src/content-schema.ts +33 -0
  26. package/src/i18n/config.ts +46 -0
  27. package/src/i18n/messages.ts +220 -0
  28. package/src/i18n/posts.ts +11 -0
  29. package/src/index.ts +5 -0
  30. package/src/layouts/BasePageLayout.astro +67 -0
  31. package/src/layouts/BlogPost.astro +279 -0
  32. package/src/layouts/HomePage.astro +73 -0
  33. package/src/styles/global.css +1867 -0
@@ -0,0 +1,390 @@
1
+ body.mesh-page main {
2
+ width: calc(100% - 2em);
3
+ max-width: 100%;
4
+ margin: 0;
5
+ }
6
+ .hero-shell {
7
+ width: min(1020px, 100%);
8
+ margin: 0 auto;
9
+ position: relative;
10
+ box-sizing: border-box;
11
+ }
12
+ .hero-image {
13
+ width: 100%;
14
+ margin: 0;
15
+ position: relative;
16
+ box-sizing: border-box;
17
+ border-radius: 0;
18
+ overflow: hidden;
19
+ }
20
+ .hero-pane {
21
+ position: relative;
22
+ border-radius: 12px;
23
+ overflow: hidden;
24
+ border: 1px solid rgba(156, 192, 218, 0.36);
25
+ box-shadow:
26
+ 0 0 0 1px rgba(186, 222, 246, 0.14),
27
+ 0 24px 48px rgba(8, 18, 30, 0.68),
28
+ 0 0 34px rgba(8, 18, 30, 0.64);
29
+ }
30
+ .hero-stack {
31
+ position: relative;
32
+ aspect-ratio: 1020 / 510;
33
+ background: #0a1218;
34
+ }
35
+ .hero-canvas-wrap {
36
+ position: absolute;
37
+ inset: 0;
38
+ z-index: 1;
39
+ opacity: 0;
40
+ transition: opacity 260ms ease;
41
+ }
42
+ .hero-canvas-wrap.ready { opacity: 1; }
43
+ .hero-canvas {
44
+ position: absolute;
45
+ inset: 0;
46
+ width: 100%;
47
+ height: 100%;
48
+ display: block;
49
+ }
50
+ .hero-frame {
51
+ position: absolute;
52
+ top: 0.65rem;
53
+ left: 0.8rem;
54
+ display: inline-flex;
55
+ align-items: center;
56
+ gap: 0.52rem;
57
+ padding: 0.26rem 0.62rem;
58
+ border: 1px solid rgba(164, 206, 232, 0.44);
59
+ background: rgba(8, 18, 30, 0.8);
60
+ border-radius: 6px;
61
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
62
+ font-size: 0.65rem;
63
+ letter-spacing: 0.14em;
64
+ color: rgba(208, 236, 254, 0.9);
65
+ text-transform: uppercase;
66
+ pointer-events: none;
67
+ z-index: 2;
68
+ box-shadow:
69
+ 0 0 0 1px rgba(176, 216, 240, 0.14),
70
+ 0 0 14px rgba(20, 54, 84, 0.4);
71
+ }
72
+ .hero-frame-dot {
73
+ width: 6px;
74
+ height: 6px;
75
+ border-radius: 50%;
76
+ background: rgba(255, 64, 82, 0.98);
77
+ box-shadow: 0 0 10px rgba(255, 72, 96, 0.72);
78
+ animation: hero-frame-alert 1.1s ease-in-out infinite;
79
+ }
80
+ .hero-threat-bar {
81
+ width: 100%;
82
+ margin: 0;
83
+ display: flex;
84
+ justify-content: space-between;
85
+ gap: 0.6rem;
86
+ padding: 0.24rem 0.5rem;
87
+ box-sizing: border-box;
88
+ border-top: 1px solid rgba(164, 206, 232, 0.24);
89
+ background: linear-gradient(90deg, rgba(10, 22, 38, 0.7), rgba(8, 16, 28, 0.76));
90
+ border-radius: 0;
91
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
92
+ font-size: 0.58rem;
93
+ letter-spacing: 0.12em;
94
+ color: rgba(188, 226, 248, 0.82);
95
+ text-transform: uppercase;
96
+ pointer-events: none;
97
+ z-index: 1;
98
+ }
99
+ @keyframes hero-frame-alert {
100
+ 0%,
101
+ 100% {
102
+ opacity: 1;
103
+ transform: scale(1);
104
+ }
105
+ 50% {
106
+ opacity: 0.35;
107
+ transform: scale(0.82);
108
+ }
109
+ }
110
+ @media (max-width: 720px) {
111
+ .hero-frame {
112
+ font-size: 0.6rem;
113
+ letter-spacing: 0.1em;
114
+ }
115
+ .hero-threat-bar {
116
+ font-size: 0.52rem;
117
+ letter-spacing: 0.08em;
118
+ }
119
+ }
120
+ body.mesh-page .prose {
121
+ width: 720px;
122
+ max-width: calc(100% - 2em);
123
+ margin: auto;
124
+ padding: 1em;
125
+ color: rgb(var(--text));
126
+ }
127
+ body.mesh-page .hero-shell + .prose {
128
+ margin-top: 1.1rem;
129
+ }
130
+ .rq-tv {
131
+ position: fixed;
132
+ /* Layout-driven sizing: bind to left gutter, not raw screen resolution */
133
+ --content-w: 720px;
134
+ --gutter: max(0px, (100vw - var(--content-w)) / 2);
135
+ left: max(1rem, calc(var(--gutter) * 0.18));
136
+ top: 25vh;
137
+ width: clamp(112px, calc(var(--gutter) * 0.42), 260px);
138
+ aspect-ratio: 4 / 3;
139
+ z-index: 22;
140
+ border-radius: 10px;
141
+ padding: 7px;
142
+ box-sizing: border-box;
143
+ background:
144
+ linear-gradient(165deg, rgba(34, 58, 78, 0.95), rgba(10, 19, 30, 0.94)),
145
+ radial-gradient(circle at 18% 8%, rgba(164, 214, 244, 0.24), rgba(164, 214, 244, 0));
146
+ border: 1px solid rgba(130, 178, 208, 0.34);
147
+ box-shadow:
148
+ 0 14px 30px rgba(4, 10, 18, 0.58),
149
+ 0 0 0 1px rgba(170, 214, 240, 0.16),
150
+ inset 0 1px 0 rgba(220, 242, 255, 0.22);
151
+ overflow: hidden;
152
+ backdrop-filter: blur(1.5px);
153
+ transition:
154
+ width 360ms cubic-bezier(0.23, 1, 0.32, 1),
155
+ top 320ms ease,
156
+ padding 320ms ease,
157
+ border-radius 320ms ease,
158
+ box-shadow 320ms ease,
159
+ background 320ms ease;
160
+ }
161
+ .rq-tv::before {
162
+ content: "";
163
+ position: absolute;
164
+ inset: 0;
165
+ pointer-events: none;
166
+ background:
167
+ repeating-linear-gradient(
168
+ 180deg,
169
+ rgba(182, 232, 255, 0.14) 0 1px,
170
+ rgba(182, 232, 255, 0) 1px 4px
171
+ );
172
+ mix-blend-mode: screen;
173
+ opacity: 0.36;
174
+ transition: opacity 240ms ease;
175
+ }
176
+ .rq-tv-screen {
177
+ position: relative;
178
+ width: 100%;
179
+ height: 100%;
180
+ border-radius: 7px;
181
+ display: block;
182
+ background: transparent;
183
+ border: 1px solid rgba(132, 186, 218, 0.34);
184
+ box-shadow:
185
+ inset 0 0 0 1px rgba(162, 214, 242, 0.12),
186
+ inset 0 0 18px rgba(86, 150, 196, 0.2);
187
+ transition: opacity 190ms ease;
188
+ }
189
+ .rq-tv-stage {
190
+ position: relative;
191
+ width: 100%;
192
+ height: 100%;
193
+ transition: opacity 190ms ease;
194
+ }
195
+ .rq-tv-badge {
196
+ position: absolute;
197
+ left: 0.55rem;
198
+ bottom: 0.42rem;
199
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
200
+ font-size: 0.5rem;
201
+ letter-spacing: 0.12em;
202
+ text-transform: uppercase;
203
+ color: rgba(174, 220, 246, 0.78);
204
+ text-shadow: 0 0 8px rgba(110, 188, 236, 0.36);
205
+ pointer-events: none;
206
+ transition: opacity 180ms ease;
207
+ }
208
+ .rq-tv-toggle {
209
+ position: absolute;
210
+ inset: 0;
211
+ display: grid;
212
+ place-items: center;
213
+ border: 1px solid rgba(132, 186, 218, 0.46);
214
+ background: radial-gradient(circle at 35% 28%, rgba(174, 224, 248, 0.26), rgba(10, 19, 30, 0.94));
215
+ color: rgba(194, 236, 255, 0.9);
216
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
217
+ font-size: 0.75rem;
218
+ letter-spacing: 0.05em;
219
+ text-shadow: 0 0 9px rgba(122, 200, 240, 0.45);
220
+ cursor: pointer;
221
+ opacity: 0;
222
+ pointer-events: none;
223
+ transition: opacity 180ms ease;
224
+ }
225
+ .rq-tv-toggle::before { content: none; }
226
+ .rq-tv.rq-tv-loading .rq-tv-toggle {
227
+ opacity: 1;
228
+ pointer-events: none;
229
+ cursor: wait;
230
+ color: transparent;
231
+ }
232
+ .rq-tv.rq-tv-loading .rq-tv-toggle::before {
233
+ content: "";
234
+ width: 12px;
235
+ height: 12px;
236
+ border-radius: 50%;
237
+ border: 2px solid rgba(184, 230, 250, 0.86);
238
+ border-top-color: rgba(184, 230, 250, 0.2);
239
+ animation: rq-tv-spin 0.8s linear infinite;
240
+ }
241
+ .rq-tv-toggle:focus-visible {
242
+ outline: 2px solid rgba(160, 224, 250, 0.78);
243
+ outline-offset: -2px;
244
+ }
245
+ .rq-tv.rq-tv-collapsed {
246
+ top: 27vh;
247
+ width: 34px;
248
+ padding: 0;
249
+ border-radius: 6px;
250
+ background: linear-gradient(165deg, rgba(34, 58, 78, 0.9), rgba(10, 19, 30, 0.92));
251
+ box-shadow:
252
+ 0 10px 20px rgba(4, 10, 18, 0.42),
253
+ 0 0 0 1px rgba(168, 214, 240, 0.2);
254
+ }
255
+ .rq-tv.rq-tv-collapsed::before {
256
+ opacity: 0;
257
+ }
258
+ .rq-tv.rq-tv-collapsed .rq-tv-screen,
259
+ .rq-tv.rq-tv-collapsed .rq-tv-stage,
260
+ .rq-tv.rq-tv-collapsed .rq-tv-badge {
261
+ opacity: 0;
262
+ pointer-events: none;
263
+ }
264
+ .rq-tv.rq-tv-collapsed .rq-tv-toggle {
265
+ opacity: 1;
266
+ pointer-events: auto;
267
+ }
268
+ .rq-tv-dot {
269
+ display: inline-block;
270
+ width: 5px;
271
+ height: 5px;
272
+ border-radius: 50%;
273
+ margin-left: 0.35rem;
274
+ background: rgba(254, 84, 108, 0.98);
275
+ box-shadow: 0 0 8px rgba(254, 90, 120, 0.7);
276
+ animation: rq-tv-dot-blink 1.3s ease-in-out infinite;
277
+ vertical-align: middle;
278
+ }
279
+ @keyframes rq-tv-dot-blink {
280
+ 0%, 100% { opacity: 1; }
281
+ 50% { opacity: 0.28; }
282
+ }
283
+ @keyframes rq-tv-spin {
284
+ to { transform: rotate(360deg); }
285
+ }
286
+ @media (max-width: 720px) {
287
+ body.mesh-page .hero-shell + .prose {
288
+ margin-top: 0.85rem;
289
+ }
290
+ }
291
+ @media (max-width: 1300px) {
292
+ .rq-tv { display: none; }
293
+ }
294
+ body.mesh-page .title {
295
+ margin-bottom: 1em;
296
+ padding: 1em 0;
297
+ text-align: center;
298
+ line-height: 1;
299
+ }
300
+ body.mesh-page .title h1 {
301
+ margin: 0 0 0.5em 0;
302
+ }
303
+ .mesh-subtitle-text {
304
+ margin: -0.15rem 0 0.85rem;
305
+ font-size: 1rem;
306
+ line-height: 1.5;
307
+ color: rgba(210, 240, 255, 0.86);
308
+ text-wrap: balance;
309
+ }
310
+ .mesh-title-flow {
311
+ position: relative;
312
+ height: 3px;
313
+ margin: 0.75rem 0 0.1rem;
314
+ border-radius: 999px;
315
+ overflow: hidden;
316
+ background: linear-gradient(
317
+ 90deg,
318
+ rgba(96, 176, 255, 0.22),
319
+ rgba(132, 214, 255, 0.3),
320
+ rgba(96, 176, 255, 0.22)
321
+ );
322
+ box-shadow:
323
+ 0 0 0 1px rgba(148, 216, 244, 0.08),
324
+ 0 0 14px rgba(124, 206, 246, 0.24);
325
+ }
326
+ .mesh-title-flow::before {
327
+ content: "";
328
+ position: absolute;
329
+ inset: 0;
330
+ background: repeating-linear-gradient(
331
+ 90deg,
332
+ rgba(162, 224, 250, 0.18) 0 9px,
333
+ rgba(162, 224, 250, 0) 9px 16px
334
+ );
335
+ }
336
+ .mesh-title-flow::after {
337
+ content: "";
338
+ position: absolute;
339
+ top: 0;
340
+ bottom: 0;
341
+ left: -35%;
342
+ width: 35%;
343
+ background: linear-gradient(
344
+ 90deg,
345
+ rgba(132, 214, 255, 0),
346
+ rgba(164, 228, 255, 0.88),
347
+ rgba(132, 214, 255, 0)
348
+ );
349
+ animation: mesh-title-flow-run 3.8s linear infinite;
350
+ }
351
+ @keyframes mesh-title-flow-run {
352
+ to {
353
+ left: 100%;
354
+ }
355
+ }
356
+ .mesh-stage-toast {
357
+ position: fixed;
358
+ right: 1rem;
359
+ bottom: 1rem;
360
+ z-index: 56;
361
+ padding: 0.5rem 0.75rem;
362
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
363
+ font-size: 0.7rem;
364
+ letter-spacing: 0.08em;
365
+ text-transform: uppercase;
366
+ color: rgba(214, 246, 255, 0.98);
367
+ background: rgba(6, 16, 28, 0.94);
368
+ border: 1px solid rgba(132, 214, 255, 0.44);
369
+ border-radius: 6px;
370
+ box-shadow:
371
+ 0 0 0 1px rgba(132, 214, 255, 0.16),
372
+ 0 0 24px rgba(124, 206, 246, 0.32);
373
+ opacity: 0;
374
+ transform: translateY(8px);
375
+ pointer-events: none;
376
+ transition:
377
+ opacity 0.18s ease,
378
+ transform 0.18s ease;
379
+ }
380
+ .mesh-stage-toast.visible {
381
+ opacity: 1;
382
+ transform: translateY(0);
383
+ }
384
+ body.mesh-page .date {
385
+ margin-bottom: 0.5em;
386
+ color: rgb(var(--text-muted));
387
+ }
388
+ body.mesh-page .last-updated-on {
389
+ font-style: italic;
390
+ }
@@ -0,0 +1,186 @@
1
+ @font-face {
2
+ font-family: "Matrix Code NFI";
3
+ src:
4
+ url("/fonts/ff2a407fe06752facf8828a86955e988.woff2") format("woff2"),
5
+ url("/fonts/ff2a407fe06752facf8828a86955e988.ttf") format("truetype");
6
+ font-weight: normal;
7
+ font-style: normal;
8
+ font-display: swap;
9
+ }
10
+
11
+ body.page-home {
12
+ --matrix-green: rgba(96, 255, 128, 0.92);
13
+ --matrix-green-soft: rgba(96, 255, 128, 0.65);
14
+ --matrix-border: rgba(96, 255, 128, 0.24);
15
+ --chrome-bg: rgba(0, 6, 0, 0.74);
16
+ --chrome-border: rgba(96, 255, 128, 0.24);
17
+ --chrome-link: rgba(176, 255, 192, 0.95);
18
+ --chrome-link-hover: rgba(210, 255, 220, 0.98);
19
+ --chrome-active: rgba(96, 255, 128, 0.9);
20
+ --chrome-text-muted: rgba(148, 216, 160, 0.74);
21
+ }
22
+
23
+ #matrix-bg {
24
+ position: fixed;
25
+ top: 0;
26
+ left: 0;
27
+ width: 100%;
28
+ height: 100%;
29
+ z-index: -1;
30
+ pointer-events: none;
31
+ }
32
+
33
+ body.page-home::before {
34
+ content: "";
35
+ position: fixed;
36
+ inset: 0;
37
+ z-index: 1;
38
+ pointer-events: none;
39
+ background: repeating-linear-gradient(
40
+ 0deg,
41
+ rgba(0, 0, 0, 0) 0px,
42
+ rgba(0, 0, 0, 0) 2px,
43
+ rgba(0, 0, 0, 0.22) 3px,
44
+ rgba(0, 0, 0, 0.22) 4px
45
+ );
46
+ opacity: 0.28;
47
+ }
48
+
49
+ body.page-home::after {
50
+ content: "";
51
+ position: fixed;
52
+ inset: 0;
53
+ z-index: 2;
54
+ pointer-events: none;
55
+ background:
56
+ radial-gradient(
57
+ circle at var(--matrix-mx, 50%) var(--matrix-my, 50%),
58
+ rgba(96, 255, 128, 0.12),
59
+ transparent 45%
60
+ ),
61
+ radial-gradient(ellipse 85% 80% at 50% 50%, transparent 55%, rgba(0, 0, 0, 0.42) 100%);
62
+ }
63
+
64
+ body.page-home main {
65
+ position: relative;
66
+ z-index: 4;
67
+ background: rgba(0, 8, 0, 0.62);
68
+ border: 1px solid var(--matrix-border);
69
+ border-radius: 10px;
70
+ padding: 2rem 2rem 2.2rem;
71
+ margin-top: calc(3em + 56px);
72
+ margin-bottom: 1.4rem;
73
+ box-shadow:
74
+ 0 0 18px rgba(96, 255, 128, 0.15),
75
+ inset 0 0 30px rgba(18, 70, 30, 0.26);
76
+ backdrop-filter: blur(2px);
77
+ }
78
+
79
+ body.page-home h1 {
80
+ color: var(--matrix-green);
81
+ letter-spacing: 0.04em;
82
+ text-shadow:
83
+ 0 0 14px rgba(96, 255, 128, 0.35),
84
+ 0 0 30px rgba(96, 255, 128, 0.12);
85
+ font-size: clamp(2rem, 5vw, 3rem);
86
+ }
87
+
88
+ body.page-home h1::before {
89
+ content: "INITIALIZE";
90
+ display: block;
91
+ margin-bottom: 0.65rem;
92
+ font-size: 0.34em;
93
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
94
+ color: var(--matrix-green-soft);
95
+ letter-spacing: 0.2em;
96
+ }
97
+
98
+ body.page-home p,
99
+ body.page-home li {
100
+ color: rgba(220, 255, 228, 0.88);
101
+ text-shadow: 0 0 10px rgba(96, 255, 128, 0.08);
102
+ }
103
+
104
+ body.page-home ul {
105
+ list-style: none;
106
+ padding-left: 0;
107
+ margin-left: 0;
108
+ }
109
+
110
+ body.page-home li {
111
+ position: relative;
112
+ padding-left: 1.1rem;
113
+ }
114
+
115
+ body.page-home li::before {
116
+ content: ">";
117
+ position: absolute;
118
+ left: 0;
119
+ color: var(--matrix-green-soft);
120
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
121
+ }
122
+
123
+ body.page-home a {
124
+ color: var(--matrix-green);
125
+ text-decoration-color: rgba(96, 255, 128, 0.5);
126
+ text-underline-offset: 2px;
127
+ }
128
+
129
+ body.page-home a:hover {
130
+ color: rgba(172, 255, 188, 0.98);
131
+ }
132
+
133
+ body.page-home code {
134
+ background: rgba(16, 54, 24, 0.7);
135
+ color: rgba(182, 255, 195, 0.95);
136
+ border: 1px solid rgba(96, 255, 128, 0.2);
137
+ }
138
+
139
+ .home-hero-copy {
140
+ margin-bottom: 1.1rem;
141
+ }
142
+
143
+ .home-section-title {
144
+ font-size: 0.88rem;
145
+ letter-spacing: 0.16em;
146
+ text-transform: uppercase;
147
+ color: var(--matrix-green-soft);
148
+ margin: 1.2rem 0 0.65rem;
149
+ }
150
+
151
+ .home-latest {
152
+ border-top: 1px solid rgba(96, 255, 128, 0.2);
153
+ padding-top: 0.9rem;
154
+ }
155
+
156
+ .home-post-list {
157
+ list-style: none;
158
+ padding: 0;
159
+ margin: 0;
160
+ }
161
+
162
+ .home-post-list li {
163
+ padding: 0.55rem 0;
164
+ border-bottom: 1px solid rgba(96, 255, 128, 0.12);
165
+ }
166
+
167
+ .home-post-list li:last-child {
168
+ border-bottom: none;
169
+ }
170
+
171
+ .home-post-title {
172
+ font-weight: 700;
173
+ }
174
+
175
+ .home-post-meta {
176
+ font-size: 0.82rem;
177
+ color: rgba(178, 246, 192, 0.8);
178
+ }
179
+
180
+ @media (max-width: 720px) {
181
+ body.page-home main {
182
+ padding: 1.25rem 1rem 1.4rem;
183
+ margin-top: calc(1em + 56px);
184
+ margin-bottom: 1rem;
185
+ }
186
+ }
@@ -0,0 +1,118 @@
1
+ import { access, mkdir, writeFile } from 'node:fs/promises';
2
+ import { constants } from 'node:fs';
3
+ import path from 'node:path';
4
+
5
+ const THEMES = ['base', 'cyber', 'ai', 'hacker', 'matrix'];
6
+ const PAGES_ROOT = path.resolve(process.cwd(), 'src/pages/[lang]');
7
+
8
+ function usage() {
9
+ console.error('Usage: npm run new-page -- <slug> [--theme <base|cyber|ai|hacker|matrix>]');
10
+ }
11
+
12
+ function parseArgs(argv) {
13
+ const args = argv.slice(2);
14
+ const positional = [];
15
+ let theme = 'base';
16
+
17
+ for (let i = 0; i < args.length; i += 1) {
18
+ const token = args[i];
19
+ if (token === '--theme') {
20
+ theme = args[i + 1] ?? '';
21
+ i += 1;
22
+ continue;
23
+ }
24
+ positional.push(token);
25
+ }
26
+
27
+ return { slug: positional[0], theme };
28
+ }
29
+
30
+ function validateSlug(slug) {
31
+ if (!slug) return false;
32
+ if (slug.startsWith('/') || slug.endsWith('/')) return false;
33
+ const parts = slug.split('/');
34
+ return parts.every((part) => /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(part));
35
+ }
36
+
37
+ function toTitleFromSlug(slug) {
38
+ const leaf = slug.split('/').pop() ?? slug;
39
+ return leaf
40
+ .split('-')
41
+ .filter(Boolean)
42
+ .map((segment) => segment[0].toUpperCase() + segment.slice(1))
43
+ .join(' ');
44
+ }
45
+
46
+ async function exists(filePath) {
47
+ try {
48
+ await access(filePath, constants.F_OK);
49
+ return true;
50
+ } catch {
51
+ return false;
52
+ }
53
+ }
54
+
55
+ function templateFor({ slug, theme }) {
56
+ const title = toTitleFromSlug(slug) || 'New Page';
57
+ return `---
58
+ import type { GetStaticPaths } from 'astro';
59
+ import BasePageLayout from '@anglefeint/astro-theme/layouts/BasePageLayout.astro';
60
+ import { SUPPORTED_LOCALES } from '@anglefeint/astro-theme/i18n/config';
61
+
62
+ const PAGE_THEME = '${theme}';
63
+
64
+ export const getStaticPaths = (() => SUPPORTED_LOCALES.map((lang) => ({ params: { lang } }))) satisfies GetStaticPaths;
65
+
66
+ const locale = Astro.params.lang;
67
+ const pageTitle = '${title}';
68
+ const pageDescription = 'A custom page built from the ${theme} theme shell.';
69
+ ---
70
+
71
+ <BasePageLayout locale={locale} title={pageTitle} description={pageDescription} theme={PAGE_THEME}>
72
+ \t<h1>${title}</h1>
73
+ \t<p>Replace this content with your own page content.</p>
74
+ </BasePageLayout>
75
+ `;
76
+ }
77
+
78
+ async function main() {
79
+ const { slug, theme: rawTheme } = parseArgs(process.argv);
80
+ const theme = String(rawTheme || '').toLowerCase();
81
+
82
+ if (!slug) {
83
+ usage();
84
+ process.exit(1);
85
+ }
86
+ if (!validateSlug(slug)) {
87
+ console.error('Invalid page slug. Use lowercase letters, numbers, hyphens, and optional nested paths.');
88
+ process.exit(1);
89
+ }
90
+ if (!THEMES.includes(theme)) {
91
+ console.error(`Invalid theme "${rawTheme}". Use one of: ${THEMES.join(', ')}.`);
92
+ process.exit(1);
93
+ }
94
+
95
+ const targetPath = path.join(PAGES_ROOT, `${slug}.astro`);
96
+
97
+ if (await exists(targetPath)) {
98
+ console.error(`File already exists: ${targetPath}`);
99
+ process.exit(1);
100
+ }
101
+
102
+ const targetDir = path.dirname(targetPath);
103
+ await mkdir(targetDir, { recursive: true });
104
+ await writeFile(
105
+ targetPath,
106
+ templateFor({ slug, theme }),
107
+ 'utf8',
108
+ );
109
+
110
+ console.log(`Created page: ${targetPath}`);
111
+ console.log(`Theme: ${theme}`);
112
+ console.log('This route is generated for all locales via getStaticPaths().');
113
+ }
114
+
115
+ main().catch((error) => {
116
+ console.error(error);
117
+ process.exit(1);
118
+ });