@karaoke-cms/theme-default 0.9.0 → 0.9.2
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/package.json +6 -3
- package/src/index.ts +46 -4
- package/src/styles/blog.css +9 -0
- package/src/styles.css +61 -9
- package/src/pages/blog/[slug].astro +0 -66
- package/src/pages/blog/index.astro +0 -31
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@karaoke-cms/theme-default",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.9.
|
|
4
|
+
"version": "0.9.2",
|
|
5
5
|
"description": "Default theme for karaoke-cms — two-column knowledge base with blog and docs",
|
|
6
6
|
"main": "./src/index.ts",
|
|
7
7
|
"exports": {
|
|
@@ -16,13 +16,16 @@
|
|
|
16
16
|
"theme",
|
|
17
17
|
"karaoke-cms"
|
|
18
18
|
],
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"@karaoke-cms/module-blog": "0.9.2"
|
|
21
|
+
},
|
|
19
22
|
"peerDependencies": {
|
|
20
23
|
"astro": ">=6.0.0",
|
|
21
|
-
"@karaoke-cms/astro": "^0.9.
|
|
24
|
+
"@karaoke-cms/astro": "^0.9.2"
|
|
22
25
|
},
|
|
23
26
|
"devDependencies": {
|
|
24
27
|
"astro": "^6.0.8",
|
|
25
|
-
"@karaoke-cms/astro": "0.9.
|
|
28
|
+
"@karaoke-cms/astro": "0.9.2"
|
|
26
29
|
},
|
|
27
30
|
"scripts": {
|
|
28
31
|
"test": "echo \"Stub — no tests\""
|
package/src/index.ts
CHANGED
|
@@ -1,17 +1,26 @@
|
|
|
1
1
|
import type { AstroIntegration } from 'astro';
|
|
2
|
-
import type { KaraokeConfig } from '@karaoke-cms/astro';
|
|
3
2
|
import { fileURLToPath } from 'url';
|
|
4
3
|
|
|
5
4
|
const __dirname = fileURLToPath(new URL('.', import.meta.url));
|
|
6
5
|
|
|
7
|
-
|
|
6
|
+
// Constructs the Astro integration for theme-default.
|
|
7
|
+
// Exported separately so it can be called by both the new factory (themeDefault)
|
|
8
|
+
// and the legacy default export without triggering a @karaoke-cms/astro import.
|
|
9
|
+
function buildIntegration(): AstroIntegration {
|
|
8
10
|
return {
|
|
9
11
|
name: '@karaoke-cms/theme-default',
|
|
10
12
|
hooks: {
|
|
11
13
|
'astro:config:setup': ({ injectRoute, updateConfig }) => {
|
|
14
|
+
// Blog routes are owned by @karaoke-cms/module-blog.
|
|
15
|
+
// theme-default injects them here so that the old string-based theme
|
|
16
|
+
// config (theme: '@karaoke-cms/theme-default') continues to work.
|
|
17
|
+
// When the new modules[] array config is fully adopted, theme-default
|
|
18
|
+
// will stop injecting these and let the core integration do it.
|
|
19
|
+
injectRoute({ pattern: '/blog', entrypoint: '@karaoke-cms/module-blog/pages/list' });
|
|
20
|
+
injectRoute({ pattern: '/blog/[slug]', entrypoint: '@karaoke-cms/module-blog/pages/post' });
|
|
21
|
+
|
|
22
|
+
// Docs, tags, and homepage remain owned by theme-default
|
|
12
23
|
injectRoute({ pattern: '/', entrypoint: `${__dirname}pages/index.astro` });
|
|
13
|
-
injectRoute({ pattern: '/blog', entrypoint: `${__dirname}pages/blog/index.astro` });
|
|
14
|
-
injectRoute({ pattern: '/blog/[slug]', entrypoint: `${__dirname}pages/blog/[slug].astro` });
|
|
15
24
|
injectRoute({ pattern: '/docs', entrypoint: `${__dirname}pages/docs/index.astro` });
|
|
16
25
|
injectRoute({ pattern: '/docs/[slug]', entrypoint: `${__dirname}pages/docs/[slug].astro` });
|
|
17
26
|
injectRoute({ pattern: '/tags', entrypoint: `${__dirname}pages/tags/index.astro` });
|
|
@@ -29,3 +38,36 @@ export default function themeDefault(_config: KaraokeConfig): AstroIntegration {
|
|
|
29
38
|
},
|
|
30
39
|
};
|
|
31
40
|
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* New API: returns a ThemeInstance for use with defineConfig().
|
|
44
|
+
*
|
|
45
|
+
* Note: ThemeInstance is constructed manually here (without importing defineTheme
|
|
46
|
+
* from @karaoke-cms/astro) so that this file remains loadable via Node's native
|
|
47
|
+
* ESM when used through the legacy string-based theme config path.
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* // karaoke.config.ts
|
|
51
|
+
* import { themeDefault } from '@karaoke-cms/theme-default';
|
|
52
|
+
* export default defineConfig({
|
|
53
|
+
* theme: themeDefault({ implements: [blog({ mount: '/blog' })] }),
|
|
54
|
+
* });
|
|
55
|
+
*/
|
|
56
|
+
export function themeDefault(config: { implements?: Array<{ id: string }> } = {}) {
|
|
57
|
+
return {
|
|
58
|
+
_type: 'theme-instance' as const,
|
|
59
|
+
id: 'theme-default',
|
|
60
|
+
implementedModuleIds: (config.implements ?? []).map(m => m.id),
|
|
61
|
+
toAstroIntegration: () => buildIntegration(),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Legacy default export — supports the old string-based theme config:
|
|
67
|
+
* theme: '@karaoke-cms/theme-default'
|
|
68
|
+
* The core integration calls themeModule.default(config) when loading
|
|
69
|
+
* themes by package name string.
|
|
70
|
+
*/
|
|
71
|
+
export default function themeDefaultLegacy(_config: unknown): AstroIntegration {
|
|
72
|
+
return buildIntegration();
|
|
73
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/* theme-default implementation of the blog module CSS contract.
|
|
2
|
+
*
|
|
3
|
+
* These rules target the class names defined in @karaoke-cms/module-blog cssContract.
|
|
4
|
+
* Currently the blog-specific rules live in ../styles.css (shared with docs).
|
|
5
|
+
* A future pass will extract them fully here and remove the duplication.
|
|
6
|
+
*
|
|
7
|
+
* TODO: move .post-list, .post-header, .post-meta, .post-footer, .post-tags,
|
|
8
|
+
* .tag, .tag-list, .related-posts here and out of styles.css.
|
|
9
|
+
*/
|
package/src/styles.css
CHANGED
|
@@ -201,17 +201,47 @@ footer {
|
|
|
201
201
|
.footer-inner {
|
|
202
202
|
max-width: var(--width-site);
|
|
203
203
|
margin: 0 auto;
|
|
204
|
-
padding:
|
|
204
|
+
padding: 0 var(--spacing-md);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
.footer-grid {
|
|
208
|
+
display: grid;
|
|
209
|
+
grid-template-columns: repeat(4, 1fr);
|
|
210
|
+
gap: var(--spacing-lg);
|
|
211
|
+
padding: var(--spacing-xl) 0 var(--spacing-lg);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
.footer-col {
|
|
215
|
+
min-width: 0;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
.footer-brand {
|
|
219
|
+
font-weight: 700;
|
|
220
|
+
font-size: var(--font-size-sm);
|
|
221
|
+
color: var(--color-text);
|
|
222
|
+
margin: 0 0 var(--spacing-sm);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
.footer-tagline {
|
|
226
|
+
font-size: var(--font-size-sm);
|
|
227
|
+
color: var(--color-muted);
|
|
228
|
+
margin: 0;
|
|
229
|
+
line-height: var(--line-height-body);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
.footer-below {
|
|
233
|
+
border-top: 1px solid var(--color-border);
|
|
234
|
+
padding: var(--spacing-md) 0;
|
|
205
235
|
display: flex;
|
|
206
236
|
align-items: center;
|
|
207
237
|
justify-content: space-between;
|
|
208
238
|
gap: var(--spacing-md);
|
|
209
|
-
color: var(--color-muted);
|
|
210
239
|
font-size: var(--font-size-sm);
|
|
240
|
+
color: var(--color-muted);
|
|
211
241
|
}
|
|
212
242
|
|
|
213
243
|
.footer-attr {
|
|
214
|
-
|
|
244
|
+
flex-shrink: 0;
|
|
215
245
|
}
|
|
216
246
|
|
|
217
247
|
footer a {
|
|
@@ -223,6 +253,26 @@ footer a:hover {
|
|
|
223
253
|
color: var(--color-link);
|
|
224
254
|
}
|
|
225
255
|
|
|
256
|
+
/* Footer vertical menu — top-level entry as column heading */
|
|
257
|
+
.footer-col nav.karaoke-menu > ul > li > a {
|
|
258
|
+
font-size: var(--font-size-sm);
|
|
259
|
+
font-weight: 600;
|
|
260
|
+
color: var(--color-text);
|
|
261
|
+
display: block;
|
|
262
|
+
padding-bottom: var(--spacing-xs);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
.footer-col nav.karaoke-menu > ul > li > ul a {
|
|
266
|
+
font-size: var(--font-size-sm);
|
|
267
|
+
color: var(--color-muted);
|
|
268
|
+
display: block;
|
|
269
|
+
padding: 2px 0;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
.footer-col nav.karaoke-menu > ul > li > ul a:hover {
|
|
273
|
+
color: var(--color-link);
|
|
274
|
+
}
|
|
275
|
+
|
|
226
276
|
/* --- Typography --- */
|
|
227
277
|
|
|
228
278
|
h1, h2, h3, h4 {
|
|
@@ -378,14 +428,16 @@ pre code {
|
|
|
378
428
|
width: 100%;
|
|
379
429
|
}
|
|
380
430
|
|
|
381
|
-
.footer-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
431
|
+
.footer-grid {
|
|
432
|
+
grid-template-columns: 1fr 1fr;
|
|
433
|
+
gap: var(--spacing-md);
|
|
434
|
+
padding: var(--spacing-lg) 0 var(--spacing-md);
|
|
385
435
|
}
|
|
386
436
|
|
|
387
|
-
.footer-
|
|
388
|
-
|
|
437
|
+
.footer-below {
|
|
438
|
+
flex-direction: column;
|
|
439
|
+
align-items: flex-start;
|
|
440
|
+
gap: var(--spacing-xs);
|
|
389
441
|
}
|
|
390
442
|
}
|
|
391
443
|
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
import { getCollection, render } from 'astro:content';
|
|
3
|
-
import Base from '@karaoke-cms/astro/layouts/Base.astro';
|
|
4
|
-
import ModuleLoader from '@karaoke-cms/astro/components/ModuleLoader.astro';
|
|
5
|
-
import { siteTitle } from 'virtual:karaoke-cms/config';
|
|
6
|
-
|
|
7
|
-
export async function getStaticPaths() {
|
|
8
|
-
const posts = await getCollection('blog', ({ data }) => data.publish === true);
|
|
9
|
-
return posts.map(entry => ({
|
|
10
|
-
params: { slug: entry.id },
|
|
11
|
-
props: { entry },
|
|
12
|
-
}));
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
const { entry } = Astro.props;
|
|
16
|
-
const { Content } = await render(entry);
|
|
17
|
-
|
|
18
|
-
// Resolve related entries by ID across both collections
|
|
19
|
-
const relatedIds = entry.data.related ?? [];
|
|
20
|
-
const related = relatedIds.length > 0
|
|
21
|
-
? (await Promise.all([
|
|
22
|
-
getCollection('blog', ({ data }) => data.publish === true),
|
|
23
|
-
getCollection('docs', ({ data }) => data.publish === true),
|
|
24
|
-
]))
|
|
25
|
-
.flat()
|
|
26
|
-
.filter(e => relatedIds.includes(e.id))
|
|
27
|
-
: [];
|
|
28
|
-
---
|
|
29
|
-
|
|
30
|
-
<Base title={`${entry.data.title} — ${siteTitle}`} description={entry.data.description} type="article">
|
|
31
|
-
<article>
|
|
32
|
-
<div class="post-header">
|
|
33
|
-
<h1>{entry.data.title}</h1>
|
|
34
|
-
<div class="post-meta">
|
|
35
|
-
{entry.data.date && <span>{entry.data.date.toISOString().slice(0, 10)}</span>}
|
|
36
|
-
{entry.data.author && entry.data.date && <span> · </span>}
|
|
37
|
-
{entry.data.author && (
|
|
38
|
-
<span>{Array.isArray(entry.data.author) ? entry.data.author.join(' · ') : entry.data.author}</span>
|
|
39
|
-
)}
|
|
40
|
-
{entry.data.reading_time && <span> · {entry.data.reading_time} min read</span>}
|
|
41
|
-
</div>
|
|
42
|
-
</div>
|
|
43
|
-
<div class="prose">
|
|
44
|
-
<Content />
|
|
45
|
-
</div>
|
|
46
|
-
<div class="post-footer">
|
|
47
|
-
{entry.data.tags && entry.data.tags.length > 0 && (
|
|
48
|
-
<div class="post-tags">
|
|
49
|
-
{entry.data.tags.map(tag => <a href={`/tags/${tag}`} class="tag">#{tag}</a>)}
|
|
50
|
-
</div>
|
|
51
|
-
)}
|
|
52
|
-
{related.length > 0 && (
|
|
53
|
-
<div class="related-posts">
|
|
54
|
-
<p class="related-label">Related</p>
|
|
55
|
-
<ul>
|
|
56
|
-
{related.map(r => (
|
|
57
|
-
<li><a href={`/${r.collection}/${r.id}`}>{r.data.title}</a></li>
|
|
58
|
-
))}
|
|
59
|
-
</ul>
|
|
60
|
-
</div>
|
|
61
|
-
)}
|
|
62
|
-
<a href="/blog">← Blog</a>
|
|
63
|
-
</div>
|
|
64
|
-
</article>
|
|
65
|
-
<ModuleLoader comments={entry.data.comments} />
|
|
66
|
-
</Base>
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
import { getCollection } from 'astro:content';
|
|
3
|
-
import Base from '@karaoke-cms/astro/layouts/Base.astro';
|
|
4
|
-
import { siteTitle } from 'virtual:karaoke-cms/config';
|
|
5
|
-
|
|
6
|
-
const posts = (await getCollection('blog', ({ data }) => data.publish === true))
|
|
7
|
-
.sort((a, b) => (b.data.date?.valueOf() ?? 0) - (a.data.date?.valueOf() ?? 0));
|
|
8
|
-
---
|
|
9
|
-
|
|
10
|
-
<Base title={`Blog — ${siteTitle}`}>
|
|
11
|
-
<div class="listing-header">
|
|
12
|
-
<h1>Blog</h1>
|
|
13
|
-
</div>
|
|
14
|
-
{posts.length > 0 ? (
|
|
15
|
-
<ul class="post-list">
|
|
16
|
-
{posts.map(post => (
|
|
17
|
-
<li>
|
|
18
|
-
{post.data.date && (
|
|
19
|
-
<span class="post-date">{post.data.date.toISOString().slice(0, 10)}</span>
|
|
20
|
-
)}
|
|
21
|
-
<a href={`/blog/${post.id}`}>{post.data.title}</a>
|
|
22
|
-
</li>
|
|
23
|
-
))}
|
|
24
|
-
</ul>
|
|
25
|
-
) : (
|
|
26
|
-
<div class="empty-state">
|
|
27
|
-
<p>No posts published yet.</p>
|
|
28
|
-
<p>Create a Markdown file in your vault's <code>blog/</code> folder and set <code>publish: true</code> in the frontmatter to make it appear here.</p>
|
|
29
|
-
</div>
|
|
30
|
-
)}
|
|
31
|
-
</Base>
|