@karaoke-cms/astro 0.6.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/client.d.ts +19 -0
- package/package.json +46 -0
- package/src/collections.ts +42 -0
- package/src/components/ModuleLoader.astro +24 -0
- package/src/components/RegionRenderer.astro +24 -0
- package/src/components/regions/MainMenu.astro +22 -0
- package/src/components/regions/RecentPosts.astro +23 -0
- package/src/components/regions/SiteFooter.astro +7 -0
- package/src/components/regions/SiteHeader.astro +4 -0
- package/src/consts.ts +8 -0
- package/src/index.ts +112 -0
- package/src/layouts/Base.astro +73 -0
- package/src/modules/comments/Comments.astro +44 -0
- package/src/modules/comments/index.ts +1 -0
- package/src/modules/search/Search.astro +32 -0
- package/src/modules/search/index.ts +1 -0
- package/src/pages/404.astro +14 -0
- package/src/pages/blog/[slug].astro +66 -0
- package/src/pages/blog/index.astro +31 -0
- package/src/pages/docs/[slug].astro +66 -0
- package/src/pages/docs/index.astro +31 -0
- package/src/pages/index.astro +65 -0
- package/src/pages/rss.xml.ts +36 -0
- package/src/pages/tags/[tag].astro +53 -0
- package/src/pages/tags/index.astro +41 -0
- package/src/themes/default/styles.css +446 -0
- package/src/themes/minimal/styles.css +388 -0
- package/src/types.ts +51 -0
- package/src/utils/resolve-layout.ts +13 -0
- package/src/utils/resolve-modules.ts +40 -0
- package/src/validate-config.d.ts +14 -0
- package/src/validate-config.js +59 -0
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
/* karaoke-cms minimal theme — serif, typography-first, warm */
|
|
2
|
+
|
|
3
|
+
:root {
|
|
4
|
+
--font-body: Georgia, 'Times New Roman', serif;
|
|
5
|
+
--font-mono: ui-monospace, 'Cascadia Code', 'Fira Code', monospace;
|
|
6
|
+
|
|
7
|
+
--font-size-base: 1.125rem;
|
|
8
|
+
--font-size-sm: 0.9375rem;
|
|
9
|
+
--font-size-lg: 1.375rem;
|
|
10
|
+
--font-size-xl: 2rem;
|
|
11
|
+
|
|
12
|
+
--line-height-body: 1.75;
|
|
13
|
+
--line-height-heading: 1.2;
|
|
14
|
+
|
|
15
|
+
--color-bg: #faf8f5;
|
|
16
|
+
--color-text: #1c1917;
|
|
17
|
+
--color-muted: #78716c;
|
|
18
|
+
--color-border: #e7e5e4;
|
|
19
|
+
--color-link: #92400e;
|
|
20
|
+
--color-link-hover: #78350f;
|
|
21
|
+
--color-link-visited: #6b21a8;
|
|
22
|
+
|
|
23
|
+
--width-content: 620px;
|
|
24
|
+
--width-site: 720px;
|
|
25
|
+
|
|
26
|
+
--spacing-xs: 0.25rem;
|
|
27
|
+
--spacing-sm: 0.5rem;
|
|
28
|
+
--spacing-md: 1rem;
|
|
29
|
+
--spacing-lg: 2rem;
|
|
30
|
+
--spacing-xl: 4rem;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
@media (prefers-color-scheme: dark) {
|
|
34
|
+
:root {
|
|
35
|
+
--color-bg: #171310;
|
|
36
|
+
--color-text: #e7e5e4;
|
|
37
|
+
--color-muted: #a8a29e;
|
|
38
|
+
--color-border: #292524;
|
|
39
|
+
--color-link: #fb923c;
|
|
40
|
+
--color-link-hover: #fdba74;
|
|
41
|
+
--color-link-visited: #c084fc;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
*, *::before, *::after {
|
|
46
|
+
box-sizing: border-box;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
html {
|
|
50
|
+
font-size: var(--font-size-base);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
body {
|
|
54
|
+
margin: 0;
|
|
55
|
+
background: var(--color-bg);
|
|
56
|
+
color: var(--color-text);
|
|
57
|
+
font-family: var(--font-body);
|
|
58
|
+
line-height: var(--line-height-body);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/* --- Layout --- */
|
|
62
|
+
|
|
63
|
+
.site-wrap {
|
|
64
|
+
max-width: var(--width-site);
|
|
65
|
+
margin: 0 auto;
|
|
66
|
+
padding: 0 var(--spacing-md);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/* --- Navigation --- */
|
|
70
|
+
|
|
71
|
+
header {
|
|
72
|
+
border-bottom: 1px solid var(--color-border);
|
|
73
|
+
margin-bottom: var(--spacing-xl);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.header-inner {
|
|
77
|
+
max-width: var(--width-site);
|
|
78
|
+
margin: 0 auto;
|
|
79
|
+
padding: 0 var(--spacing-md);
|
|
80
|
+
display: flex;
|
|
81
|
+
align-items: center;
|
|
82
|
+
gap: var(--spacing-lg);
|
|
83
|
+
height: 3rem;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.site-name {
|
|
87
|
+
font-weight: 700;
|
|
88
|
+
font-style: italic;
|
|
89
|
+
text-decoration: none;
|
|
90
|
+
color: var(--color-text);
|
|
91
|
+
flex-shrink: 0;
|
|
92
|
+
letter-spacing: -0.01em;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.site-name:hover {
|
|
96
|
+
color: var(--color-link);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
header nav ul {
|
|
100
|
+
list-style: none;
|
|
101
|
+
margin: 0;
|
|
102
|
+
padding: 0;
|
|
103
|
+
display: flex;
|
|
104
|
+
gap: var(--spacing-md);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
header nav ul a {
|
|
108
|
+
text-decoration: none;
|
|
109
|
+
color: var(--color-muted);
|
|
110
|
+
font-size: var(--font-size-sm);
|
|
111
|
+
font-family: var(--font-mono);
|
|
112
|
+
padding: var(--spacing-xs) 0;
|
|
113
|
+
min-height: 44px;
|
|
114
|
+
display: flex;
|
|
115
|
+
align-items: center;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
header nav ul a:hover,
|
|
119
|
+
header nav ul a[aria-current="page"] {
|
|
120
|
+
color: var(--color-text);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/* --- Page body (left + main + right regions) --- */
|
|
124
|
+
|
|
125
|
+
.page-body {
|
|
126
|
+
display: flex;
|
|
127
|
+
align-items: flex-start;
|
|
128
|
+
max-width: var(--width-site);
|
|
129
|
+
margin: 0 auto;
|
|
130
|
+
padding: 0 var(--spacing-md);
|
|
131
|
+
min-height: calc(100vh - 12rem);
|
|
132
|
+
gap: var(--spacing-lg);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.page-body.has-left,
|
|
136
|
+
.page-body.has-right {
|
|
137
|
+
max-width: calc(var(--width-site) + var(--width-sidebar, 220px) + var(--spacing-lg));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.page-body.has-left.has-right {
|
|
141
|
+
max-width: calc(var(--width-site) + 2 * (var(--width-sidebar, 220px) + var(--spacing-lg)));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
main {
|
|
145
|
+
flex: 1;
|
|
146
|
+
min-width: 0;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
.region-left,
|
|
150
|
+
.region-right {
|
|
151
|
+
width: var(--width-sidebar, 220px);
|
|
152
|
+
flex-shrink: 0;
|
|
153
|
+
padding-top: var(--spacing-xs);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/* --- Sidebar --- */
|
|
157
|
+
|
|
158
|
+
.sidebar-section {
|
|
159
|
+
margin-bottom: var(--spacing-lg);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
.sidebar-heading {
|
|
163
|
+
font-size: var(--font-size-sm);
|
|
164
|
+
font-family: var(--font-mono);
|
|
165
|
+
text-transform: uppercase;
|
|
166
|
+
letter-spacing: 0.08em;
|
|
167
|
+
color: var(--color-muted);
|
|
168
|
+
margin: 0 0 var(--spacing-sm);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
.sidebar-list {
|
|
172
|
+
list-style: none;
|
|
173
|
+
margin: 0;
|
|
174
|
+
padding: 0;
|
|
175
|
+
display: flex;
|
|
176
|
+
flex-direction: column;
|
|
177
|
+
gap: var(--spacing-xs);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
.sidebar-list li {
|
|
181
|
+
display: flex;
|
|
182
|
+
flex-direction: column;
|
|
183
|
+
gap: 2px;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
.sidebar-list a {
|
|
187
|
+
font-size: var(--font-size-sm);
|
|
188
|
+
text-decoration: none;
|
|
189
|
+
color: var(--color-text);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
.sidebar-list a:hover {
|
|
193
|
+
color: var(--color-link);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/* --- Footer --- */
|
|
197
|
+
|
|
198
|
+
footer {
|
|
199
|
+
border-top: 1px solid var(--color-border);
|
|
200
|
+
margin-top: var(--spacing-xl);
|
|
201
|
+
padding: var(--spacing-lg) var(--spacing-md);
|
|
202
|
+
max-width: var(--width-site);
|
|
203
|
+
margin-left: auto;
|
|
204
|
+
margin-right: auto;
|
|
205
|
+
color: var(--color-muted);
|
|
206
|
+
font-size: var(--font-size-sm);
|
|
207
|
+
font-family: var(--font-mono);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
footer a {
|
|
211
|
+
color: var(--color-muted);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
footer a:hover {
|
|
215
|
+
color: var(--color-link);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/* --- Typography --- */
|
|
219
|
+
|
|
220
|
+
h1, h2, h3, h4 {
|
|
221
|
+
line-height: var(--line-height-heading);
|
|
222
|
+
margin-top: 0;
|
|
223
|
+
letter-spacing: -0.02em;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
a {
|
|
227
|
+
color: var(--color-link);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
a:hover {
|
|
231
|
+
color: var(--color-link-hover);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
a:visited {
|
|
235
|
+
color: var(--color-link-visited);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
code {
|
|
239
|
+
font-family: var(--font-mono);
|
|
240
|
+
font-size: 0.875em;
|
|
241
|
+
background: var(--color-border);
|
|
242
|
+
padding: 0.1em 0.3em;
|
|
243
|
+
border-radius: 2px;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
pre {
|
|
247
|
+
background: var(--color-border);
|
|
248
|
+
padding: var(--spacing-md);
|
|
249
|
+
overflow-x: auto;
|
|
250
|
+
border-radius: 3px;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
pre code {
|
|
254
|
+
background: none;
|
|
255
|
+
padding: 0;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/* --- Post/doc prose --- */
|
|
259
|
+
|
|
260
|
+
.prose {
|
|
261
|
+
max-width: var(--width-content);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
.prose img {
|
|
265
|
+
max-width: 100%;
|
|
266
|
+
height: auto;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/* --- Listing pages --- */
|
|
270
|
+
|
|
271
|
+
.listing-header {
|
|
272
|
+
margin-bottom: var(--spacing-lg);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
.listing-header h1 {
|
|
276
|
+
font-size: var(--font-size-xl);
|
|
277
|
+
margin-bottom: 0;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
.post-list {
|
|
281
|
+
list-style: none;
|
|
282
|
+
margin: 0;
|
|
283
|
+
padding: 0;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
.post-list li {
|
|
287
|
+
padding: var(--spacing-sm) 0;
|
|
288
|
+
border-bottom: 1px solid var(--color-border);
|
|
289
|
+
display: flex;
|
|
290
|
+
gap: var(--spacing-md);
|
|
291
|
+
align-items: baseline;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
.post-list li:first-child {
|
|
295
|
+
border-top: 1px solid var(--color-border);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
.post-date {
|
|
299
|
+
color: var(--color-muted);
|
|
300
|
+
font-size: var(--font-size-sm);
|
|
301
|
+
font-family: var(--font-mono);
|
|
302
|
+
white-space: nowrap;
|
|
303
|
+
flex-shrink: 0;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
.empty-state {
|
|
307
|
+
color: var(--color-muted);
|
|
308
|
+
padding: var(--spacing-lg) 0;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
.empty-state p {
|
|
312
|
+
margin: 0 0 var(--spacing-sm);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/* --- Homepage two-column --- */
|
|
316
|
+
|
|
317
|
+
.home-grid {
|
|
318
|
+
display: grid;
|
|
319
|
+
grid-template-columns: 1fr 1fr;
|
|
320
|
+
gap: var(--spacing-xl);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
@media (max-width: 640px) {
|
|
324
|
+
.home-grid {
|
|
325
|
+
grid-template-columns: 1fr;
|
|
326
|
+
gap: var(--spacing-lg);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
.page-body {
|
|
330
|
+
flex-direction: column;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
.region-left,
|
|
334
|
+
.region-right {
|
|
335
|
+
width: 100%;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
.home-section h2 {
|
|
340
|
+
font-size: var(--font-size-sm);
|
|
341
|
+
font-family: var(--font-mono);
|
|
342
|
+
text-transform: uppercase;
|
|
343
|
+
letter-spacing: 0.08em;
|
|
344
|
+
color: var(--color-muted);
|
|
345
|
+
margin-bottom: var(--spacing-md);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
.home-section .post-list li {
|
|
349
|
+
flex-direction: column;
|
|
350
|
+
gap: var(--spacing-xs);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
.view-all {
|
|
354
|
+
display: inline-block;
|
|
355
|
+
margin-top: var(--spacing-md);
|
|
356
|
+
font-size: var(--font-size-sm);
|
|
357
|
+
font-family: var(--font-mono);
|
|
358
|
+
color: var(--color-muted);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
.view-all:hover {
|
|
362
|
+
color: var(--color-link);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/* --- Single post/doc --- */
|
|
366
|
+
|
|
367
|
+
.post-header {
|
|
368
|
+
max-width: var(--width-content);
|
|
369
|
+
margin-bottom: var(--spacing-lg);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
.post-header h1 {
|
|
373
|
+
font-size: var(--font-size-xl);
|
|
374
|
+
margin-bottom: var(--spacing-sm);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
.post-meta {
|
|
378
|
+
color: var(--color-muted);
|
|
379
|
+
font-size: var(--font-size-sm);
|
|
380
|
+
font-family: var(--font-mono);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
.post-footer {
|
|
384
|
+
margin-top: var(--spacing-xl);
|
|
385
|
+
padding-top: var(--spacing-md);
|
|
386
|
+
border-top: 1px solid var(--color-border);
|
|
387
|
+
max-width: var(--width-content);
|
|
388
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export type RegionComponent = 'header' | 'main-menu' | 'search' | 'recent-posts' | 'footer';
|
|
2
|
+
|
|
3
|
+
export interface KaraokeConfig {
|
|
4
|
+
/** Site title — displayed in the browser tab and nav bar. Defaults to 'Karaoke'. */
|
|
5
|
+
title?: string;
|
|
6
|
+
/** Site description — used in RSS feed and OG meta tags. */
|
|
7
|
+
description?: string;
|
|
8
|
+
/** Theme name — must match a folder in themes/. Defaults to 'default'. */
|
|
9
|
+
theme?: string;
|
|
10
|
+
modules?: {
|
|
11
|
+
search?: { enabled?: boolean };
|
|
12
|
+
comments?: {
|
|
13
|
+
enabled?: boolean;
|
|
14
|
+
/** GitHub repo in "owner/repo" format */
|
|
15
|
+
repo?: string;
|
|
16
|
+
repoId?: string;
|
|
17
|
+
category?: string;
|
|
18
|
+
categoryId?: string;
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
layout?: {
|
|
22
|
+
regions?: {
|
|
23
|
+
top?: { components?: RegionComponent[] };
|
|
24
|
+
left?: { components?: RegionComponent[] };
|
|
25
|
+
right?: { components?: RegionComponent[] };
|
|
26
|
+
bottom?: { components?: RegionComponent[] };
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Resolved (defaults filled in) modules config — available at build time via virtual module. */
|
|
32
|
+
export interface ResolvedModules {
|
|
33
|
+
search: { enabled: boolean };
|
|
34
|
+
comments: {
|
|
35
|
+
enabled: boolean;
|
|
36
|
+
repo: string;
|
|
37
|
+
repoId: string;
|
|
38
|
+
category: string;
|
|
39
|
+
categoryId: string;
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Resolved (defaults filled in) layout config — available at build time via virtual module. */
|
|
44
|
+
export interface ResolvedLayout {
|
|
45
|
+
regions: {
|
|
46
|
+
top: { components: RegionComponent[] };
|
|
47
|
+
left: { components: RegionComponent[] };
|
|
48
|
+
right: { components: RegionComponent[] };
|
|
49
|
+
bottom: { components: RegionComponent[] };
|
|
50
|
+
};
|
|
51
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { KaraokeConfig, ResolvedLayout } from '../types.js';
|
|
2
|
+
|
|
3
|
+
export function resolveLayout(config: KaraokeConfig | null | undefined): ResolvedLayout {
|
|
4
|
+
const regions = config?.layout?.regions;
|
|
5
|
+
return {
|
|
6
|
+
regions: {
|
|
7
|
+
top: { components: regions?.top?.components ?? ['header', 'main-menu', 'search'] },
|
|
8
|
+
left: { components: regions?.left?.components ?? [] },
|
|
9
|
+
right: { components: regions?.right?.components ?? [] },
|
|
10
|
+
bottom: { components: regions?.bottom?.components ?? ['footer'] },
|
|
11
|
+
},
|
|
12
|
+
};
|
|
13
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// Minimal config shape — mirrors KaraokeConfig.modules without importing from root
|
|
2
|
+
// so this utility stays self-contained and testable.
|
|
3
|
+
export interface ModuleConfig {
|
|
4
|
+
modules?: {
|
|
5
|
+
search?: { enabled?: boolean };
|
|
6
|
+
comments?: {
|
|
7
|
+
enabled?: boolean;
|
|
8
|
+
repo?: string;
|
|
9
|
+
repoId?: string;
|
|
10
|
+
category?: string;
|
|
11
|
+
categoryId?: string;
|
|
12
|
+
};
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface ResolvedModules {
|
|
17
|
+
search: { enabled: boolean };
|
|
18
|
+
comments: {
|
|
19
|
+
enabled: boolean;
|
|
20
|
+
repo: string;
|
|
21
|
+
repoId: string;
|
|
22
|
+
category: string;
|
|
23
|
+
categoryId: string;
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function resolveModules(config: ModuleConfig | null | undefined): ResolvedModules {
|
|
28
|
+
return {
|
|
29
|
+
search: {
|
|
30
|
+
enabled: config?.modules?.search?.enabled ?? false,
|
|
31
|
+
},
|
|
32
|
+
comments: {
|
|
33
|
+
enabled: config?.modules?.comments?.enabled ?? false,
|
|
34
|
+
repo: config?.modules?.comments?.repo ?? '',
|
|
35
|
+
repoId: config?.modules?.comments?.repoId ?? '',
|
|
36
|
+
category: config?.modules?.comments?.category ?? '',
|
|
37
|
+
categoryId: config?.modules?.comments?.categoryId ?? '',
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { KaraokeConfig } from './types.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Resolve the active theme name from karaoke.config.
|
|
5
|
+
* Falls back to 'default' if config is absent.
|
|
6
|
+
* Throws a clear error if the named theme folder doesn't exist.
|
|
7
|
+
*/
|
|
8
|
+
export function getTheme(config: KaraokeConfig | null | undefined, themesDir: string): string;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Validate module config at build time.
|
|
12
|
+
* Throws a clear error if comments is enabled but required fields are missing.
|
|
13
|
+
*/
|
|
14
|
+
export function validateModules(config: KaraokeConfig | null | undefined): void;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* validate-config.js — karaoke.config helpers
|
|
3
|
+
*
|
|
4
|
+
* Extracted here so the logic is unit-testable separately from astro.config.mjs.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { readdirSync, statSync } from 'fs'
|
|
8
|
+
import { join } from 'path'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Resolve the active theme name from karaoke.config.
|
|
12
|
+
* Falls back to 'default' if config is absent.
|
|
13
|
+
* Throws a clear error if the named theme folder doesn't exist.
|
|
14
|
+
*
|
|
15
|
+
* @param {import('../karaoke.config').KaraokeConfig | null | undefined} config
|
|
16
|
+
* @param {string} themesDir - absolute path to the src/themes directory
|
|
17
|
+
* @returns {string} the resolved theme name
|
|
18
|
+
*/
|
|
19
|
+
/**
|
|
20
|
+
* Validate module config at build time.
|
|
21
|
+
* Throws a clear error if comments is enabled but required fields are missing.
|
|
22
|
+
*
|
|
23
|
+
* @param {import('../karaoke.config').KaraokeConfig | null | undefined} config
|
|
24
|
+
*/
|
|
25
|
+
export function validateModules(config) {
|
|
26
|
+
const comments = config?.modules?.comments
|
|
27
|
+
if (!comments?.enabled) return
|
|
28
|
+
|
|
29
|
+
const required = ['repo', 'repoId', 'category', 'categoryId']
|
|
30
|
+
const missing = required.filter(k => !comments[k])
|
|
31
|
+
if (missing.length > 0) {
|
|
32
|
+
throw new Error(
|
|
33
|
+
`karaoke-cms: modules.comments.enabled is true but the following required fields are missing: ` +
|
|
34
|
+
`${missing.join(', ')}. Get these values from https://giscus.app`
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function getTheme(config, themesDir) {
|
|
40
|
+
const theme = config?.theme ?? 'default'
|
|
41
|
+
let available
|
|
42
|
+
try {
|
|
43
|
+
available = readdirSync(themesDir)
|
|
44
|
+
} catch {
|
|
45
|
+
throw new Error(
|
|
46
|
+
`karaoke-cms: src/themes/ directory not found at ${themesDir} — is this a karaoke-cms repo?`
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
available = available.filter(d => {
|
|
50
|
+
try { return statSync(join(themesDir, d)).isDirectory() } catch { return false }
|
|
51
|
+
})
|
|
52
|
+
if (!available.includes(theme)) {
|
|
53
|
+
throw new Error(
|
|
54
|
+
`karaoke-cms: theme "${theme}" not found in src/themes/. ` +
|
|
55
|
+
`Available: ${available.join(', ')}`
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
return theme
|
|
59
|
+
}
|