@karaoke-cms/astro 0.6.2 → 0.6.3
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 +3 -1
- package/package.json +8 -4
- package/src/collections.ts +71 -14
- package/src/components/regions/RecentPosts.astro +11 -3
- package/src/index.ts +28 -6
- package/src/pages/blog/index.astro +1 -1
- package/src/pages/docs/index.astro +1 -1
- package/src/pages/karaoke-cms/[...slug].astro +44 -0
- package/src/pages/karaoke-cms/index.astro +43 -0
- package/src/types.ts +24 -0
- package/src/utils/load-env.ts +37 -0
- package/src/utils/resolve-collections.ts +83 -0
package/client.d.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
declare module 'virtual:karaoke-cms/config' {
|
|
9
|
-
import type { ResolvedModules, ResolvedLayout } from '@karaoke-cms/astro';
|
|
9
|
+
import type { ResolvedModules, ResolvedLayout, ResolvedCollections } from '@karaoke-cms/astro';
|
|
10
10
|
|
|
11
11
|
/** Site title from karaoke.config.ts */
|
|
12
12
|
export const siteTitle: string;
|
|
@@ -16,4 +16,6 @@ declare module 'virtual:karaoke-cms/config' {
|
|
|
16
16
|
export const resolvedModules: ResolvedModules;
|
|
17
17
|
/** Resolved layout config (defaults filled in) */
|
|
18
18
|
export const resolvedLayout: ResolvedLayout;
|
|
19
|
+
/** Resolved collections with enabled/disabled status for current build mode */
|
|
20
|
+
export const resolvedCollections: ResolvedCollections;
|
|
19
21
|
}
|
package/package.json
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@karaoke-cms/astro",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.6.
|
|
4
|
+
"version": "0.6.3",
|
|
5
5
|
"description": "Astro integration for karaoke-cms — ships all routes, themes, and modules",
|
|
6
6
|
"main": "./src/index.ts",
|
|
7
7
|
"exports": {
|
|
8
8
|
".": "./src/index.ts",
|
|
9
9
|
"./client": "./client.d.ts",
|
|
10
10
|
"./collections": "./src/collections.ts",
|
|
11
|
+
"./env": "./src/utils/load-env.ts",
|
|
11
12
|
"./pages/index.astro": "./src/pages/index.astro",
|
|
12
13
|
"./pages/blog/index.astro": "./src/pages/blog/index.astro",
|
|
13
14
|
"./pages/blog/[slug].astro": "./src/pages/blog/[slug].astro",
|
|
@@ -16,7 +17,9 @@
|
|
|
16
17
|
"./pages/tags/index.astro": "./src/pages/tags/index.astro",
|
|
17
18
|
"./pages/tags/[tag].astro": "./src/pages/tags/[tag].astro",
|
|
18
19
|
"./pages/404.astro": "./src/pages/404.astro",
|
|
19
|
-
"./pages/rss.xml.ts": "./src/pages/rss.xml.ts"
|
|
20
|
+
"./pages/rss.xml.ts": "./src/pages/rss.xml.ts",
|
|
21
|
+
"./pages/karaoke-cms/index.astro": "./src/pages/karaoke-cms/index.astro",
|
|
22
|
+
"./pages/karaoke-cms/[...slug].astro": "./src/pages/karaoke-cms/[...slug].astro"
|
|
20
23
|
},
|
|
21
24
|
"files": [
|
|
22
25
|
"src/",
|
|
@@ -34,13 +37,14 @@
|
|
|
34
37
|
"dependencies": {
|
|
35
38
|
"@astrojs/rss": "^4.0.17",
|
|
36
39
|
"@astrojs/sitemap": "^3.7.1",
|
|
37
|
-
"remark-wiki-link": "^2.0.1"
|
|
40
|
+
"remark-wiki-link": "^2.0.1",
|
|
41
|
+
"yaml": "^2.7.0"
|
|
38
42
|
},
|
|
39
43
|
"devDependencies": {
|
|
40
44
|
"vitest": "^4.1.1",
|
|
41
45
|
"astro": "^6.0.8"
|
|
42
46
|
},
|
|
43
47
|
"scripts": {
|
|
44
|
-
"test": "vitest run test/validate-config.test.js test/resolve-modules.test.js test/resolve-layout.test.js"
|
|
48
|
+
"test": "vitest run test/validate-config.test.js test/resolve-modules.test.js test/resolve-layout.test.js test/resolve-collections.test.js test/load-env.test.js"
|
|
45
49
|
}
|
|
46
50
|
}
|
package/src/collections.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { defineCollection, z } from 'astro:content';
|
|
2
2
|
import { glob } from 'astro/loaders';
|
|
3
3
|
import { fileURLToPath } from 'url';
|
|
4
|
-
import { join } from 'path';
|
|
4
|
+
import { join, resolve, isAbsolute } from 'path';
|
|
5
|
+
import { resolveCollections } from './utils/resolve-collections.js';
|
|
6
|
+
import type { CollectionConfig } from './types.js';
|
|
5
7
|
|
|
6
8
|
const baseSchema = z.object({
|
|
7
9
|
title: z.string(),
|
|
@@ -15,28 +17,83 @@ const baseSchema = z.object({
|
|
|
15
17
|
related: z.array(z.string()).optional(),
|
|
16
18
|
});
|
|
17
19
|
|
|
20
|
+
// Relaxed schema for handbook / custom collections — title is optional
|
|
21
|
+
const relaxedSchema = z.object({
|
|
22
|
+
title: z.string().optional().default('Untitled'),
|
|
23
|
+
publish: z.boolean().optional().default(false),
|
|
24
|
+
date: z.coerce.date().optional(),
|
|
25
|
+
author: z.union([z.string(), z.array(z.string())]).optional(),
|
|
26
|
+
tags: z.array(z.string()).optional(),
|
|
27
|
+
description: z.string().optional(),
|
|
28
|
+
reading_time: z.number().optional(),
|
|
29
|
+
related: z.array(z.string()).optional(),
|
|
30
|
+
});
|
|
31
|
+
|
|
18
32
|
/**
|
|
19
33
|
* Create Astro content collections for karaoke-cms.
|
|
20
34
|
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
35
|
+
* Reads collections.yaml from the vault to determine which collections are
|
|
36
|
+
* active for the current build mode. The `karaoke-cms` collection (handbook)
|
|
37
|
+
* is dev-only and never enters the production build graph.
|
|
38
|
+
*
|
|
39
|
+
* @param root - URL pointing to the Astro project root (where astro.config.mjs lives).
|
|
40
|
+
* Typically: `new URL('..', import.meta.url)` from src/content.config.ts
|
|
41
|
+
* @param vault - Path to the Obsidian vault root (where content/ lives).
|
|
42
|
+
* Absolute, or relative to `root`. Defaults to `root` when omitted (vault = project).
|
|
43
|
+
* Typically sourced from `loadEnv(new URL('..', import.meta.url)).KARAOKE_VAULT`.
|
|
44
|
+
* @param configCollections - Optional per-collection overrides from karaoke.config.ts.
|
|
23
45
|
*
|
|
24
46
|
* @example
|
|
25
47
|
* // src/content.config.ts
|
|
26
48
|
* import { makeCollections } from '@karaoke-cms/astro/collections';
|
|
27
|
-
*
|
|
49
|
+
* import { loadEnv } from '@karaoke-cms/astro/env';
|
|
50
|
+
* const env = loadEnv(new URL('..', import.meta.url));
|
|
51
|
+
* export const collections = makeCollections(new URL('..', import.meta.url), env.KARAOKE_VAULT);
|
|
28
52
|
*/
|
|
29
|
-
export function makeCollections(
|
|
53
|
+
export function makeCollections(
|
|
54
|
+
root: URL,
|
|
55
|
+
vault?: string,
|
|
56
|
+
configCollections?: Record<string, CollectionConfig>,
|
|
57
|
+
) {
|
|
30
58
|
const rootDir = fileURLToPath(root);
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
59
|
+
const vaultDir = vault
|
|
60
|
+
? (isAbsolute(vault) ? vault : resolve(rootDir, vault))
|
|
61
|
+
: rootDir;
|
|
62
|
+
const isProd = import.meta.env.PROD;
|
|
63
|
+
const resolved = resolveCollections(vaultDir, isProd, configCollections);
|
|
64
|
+
|
|
65
|
+
const collections: Record<string, ReturnType<typeof defineCollection>> = {};
|
|
66
|
+
|
|
67
|
+
if (resolved.blog?.enabled) {
|
|
68
|
+
collections.blog = defineCollection({
|
|
69
|
+
loader: glob({ pattern: '**/*.md', base: join(vaultDir, 'blog') }),
|
|
35
70
|
schema: baseSchema.extend({ comments: z.boolean().optional().default(true) }),
|
|
36
|
-
})
|
|
37
|
-
|
|
38
|
-
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (resolved.docs?.enabled) {
|
|
75
|
+
collections.docs = defineCollection({
|
|
76
|
+
loader: glob({ pattern: '**/*.md', base: join(vaultDir, 'docs') }),
|
|
39
77
|
schema: baseSchema.extend({ comments: z.boolean().optional().default(false) }),
|
|
40
|
-
})
|
|
41
|
-
}
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (resolved['karaoke-cms']?.enabled) {
|
|
82
|
+
collections['karaoke-cms'] = defineCollection({
|
|
83
|
+
loader: glob({ pattern: '**/*.md', base: join(vaultDir, 'karaoke-cms') }),
|
|
84
|
+
schema: relaxedSchema,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Custom collections defined in yaml or configCollections
|
|
89
|
+
for (const [name, col] of Object.entries(resolved)) {
|
|
90
|
+
if (!collections[name] && col.enabled) {
|
|
91
|
+
collections[name] = defineCollection({
|
|
92
|
+
loader: glob({ pattern: '**/*.md', base: join(vaultDir, name) }),
|
|
93
|
+
schema: relaxedSchema,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return collections;
|
|
42
99
|
}
|
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
---
|
|
2
2
|
import { getCollection } from 'astro:content';
|
|
3
3
|
|
|
4
|
-
const
|
|
4
|
+
const LIMIT = 5;
|
|
5
|
+
|
|
6
|
+
// Detect which collection we're in and exclude the current page
|
|
7
|
+
const pathname = Astro.url.pathname.replace(/\/$/, '');
|
|
8
|
+
const collection = pathname.startsWith('/docs/') ? 'docs' : 'blog';
|
|
9
|
+
const currentSlug = pathname.split('/').at(-1) ?? '';
|
|
10
|
+
|
|
11
|
+
const posts = (await getCollection(collection, ({ data }) => data.publish === true))
|
|
5
12
|
.sort((a, b) => (b.data.date?.valueOf() ?? 0) - (a.data.date?.valueOf() ?? 0))
|
|
6
|
-
.
|
|
13
|
+
.filter(post => post.id !== currentSlug)
|
|
14
|
+
.slice(0, LIMIT);
|
|
7
15
|
---
|
|
8
16
|
|
|
9
17
|
{posts.length > 0 && (
|
|
@@ -12,7 +20,7 @@ const posts = (await getCollection('blog', ({ data }) => data.publish === true))
|
|
|
12
20
|
<ul class="sidebar-list">
|
|
13
21
|
{posts.map(post => (
|
|
14
22
|
<li>
|
|
15
|
-
<a href={
|
|
23
|
+
<a href={`/${collection}/${post.id}`}>{post.data.title}</a>
|
|
16
24
|
{post.data.date && (
|
|
17
25
|
<span class="post-date">{post.data.date.toISOString().slice(0, 10)}</span>
|
|
18
26
|
)}
|
package/src/index.ts
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import type { AstroIntegration } from 'astro';
|
|
2
2
|
import { fileURLToPath } from 'url';
|
|
3
|
-
import { join } from 'path';
|
|
3
|
+
import { join, resolve, isAbsolute } from 'path';
|
|
4
4
|
import { wikiLinkPlugin } from 'remark-wiki-link';
|
|
5
5
|
import sitemap from '@astrojs/sitemap';
|
|
6
6
|
import { getTheme, validateModules } from './validate-config.js';
|
|
7
7
|
import { resolveModules } from './utils/resolve-modules.js';
|
|
8
8
|
import { resolveLayout } from './utils/resolve-layout.js';
|
|
9
|
-
import
|
|
9
|
+
import { resolveCollections } from './utils/resolve-collections.js';
|
|
10
|
+
import type { KaraokeConfig, ResolvedModules, ResolvedLayout, ResolvedCollections } from './types.js';
|
|
10
11
|
|
|
11
12
|
const __dirname = fileURLToPath(new URL('.', import.meta.url));
|
|
12
13
|
|
|
@@ -17,6 +18,8 @@ const __dirname = fileURLToPath(new URL('.', import.meta.url));
|
|
|
17
18
|
* /tags, /tags/[tag], /rss.xml, /404), sets up the @theme CSS alias,
|
|
18
19
|
* enables wikilinks, and provides a virtual module with your resolved config.
|
|
19
20
|
*
|
|
21
|
+
* The /karaoke-cms and /karaoke-cms/[...slug] routes are injected in dev only.
|
|
22
|
+
*
|
|
20
23
|
* @example
|
|
21
24
|
* // astro.config.mjs
|
|
22
25
|
* import { defineConfig } from 'astro/config';
|
|
@@ -42,7 +45,14 @@ export default function karaoke(config: KaraokeConfig = {}): AstroIntegration {
|
|
|
42
45
|
return {
|
|
43
46
|
name: '@karaoke-cms/astro',
|
|
44
47
|
hooks: {
|
|
45
|
-
'astro:config:setup'({ injectRoute, updateConfig }) {
|
|
48
|
+
'astro:config:setup'({ injectRoute, updateConfig, command, config: astroConfig }) {
|
|
49
|
+
const rootDir = fileURLToPath(astroConfig.root);
|
|
50
|
+
const vaultDir = config.vault
|
|
51
|
+
? (isAbsolute(config.vault) ? config.vault : resolve(rootDir, config.vault))
|
|
52
|
+
: rootDir;
|
|
53
|
+
const isProd = command === 'build';
|
|
54
|
+
const resolvedCollections = resolveCollections(vaultDir, isProd, config.collections);
|
|
55
|
+
|
|
46
56
|
// ── Inject all framework routes ──────────────────────────────────
|
|
47
57
|
injectRoute({ pattern: '/', entrypoint: '@karaoke-cms/astro/pages/index.astro' });
|
|
48
58
|
injectRoute({ pattern: '/blog', entrypoint: '@karaoke-cms/astro/pages/blog/index.astro' });
|
|
@@ -54,6 +64,12 @@ export default function karaoke(config: KaraokeConfig = {}): AstroIntegration {
|
|
|
54
64
|
injectRoute({ pattern: '/404', entrypoint: '@karaoke-cms/astro/pages/404.astro' });
|
|
55
65
|
injectRoute({ pattern: '/rss.xml', entrypoint: '@karaoke-cms/astro/pages/rss.xml.ts' });
|
|
56
66
|
|
|
67
|
+
// ── Handbook routes — dev only ───────────────────────────────────
|
|
68
|
+
if (resolvedCollections['karaoke-cms']?.enabled) {
|
|
69
|
+
injectRoute({ pattern: '/karaoke-cms', entrypoint: '@karaoke-cms/astro/pages/karaoke-cms/index.astro' });
|
|
70
|
+
injectRoute({ pattern: '/karaoke-cms/[...slug]', entrypoint: '@karaoke-cms/astro/pages/karaoke-cms/[...slug].astro' });
|
|
71
|
+
}
|
|
72
|
+
|
|
57
73
|
updateConfig({
|
|
58
74
|
integrations: [sitemap()],
|
|
59
75
|
vite: {
|
|
@@ -63,7 +79,7 @@ export default function karaoke(config: KaraokeConfig = {}): AstroIntegration {
|
|
|
63
79
|
'@theme': join(themesDir, theme),
|
|
64
80
|
},
|
|
65
81
|
},
|
|
66
|
-
plugins: [virtualConfigPlugin(config, resolved, layout)],
|
|
82
|
+
plugins: [virtualConfigPlugin(config, resolved, layout, resolvedCollections)],
|
|
67
83
|
},
|
|
68
84
|
markdown: {
|
|
69
85
|
remarkPlugins: [
|
|
@@ -87,7 +103,12 @@ export default function karaoke(config: KaraokeConfig = {}): AstroIntegration {
|
|
|
87
103
|
* Vite plugin providing `virtual:karaoke-cms/config`.
|
|
88
104
|
* Base.astro and ModuleLoader.astro import from it at build time.
|
|
89
105
|
*/
|
|
90
|
-
function virtualConfigPlugin(
|
|
106
|
+
function virtualConfigPlugin(
|
|
107
|
+
config: KaraokeConfig,
|
|
108
|
+
resolved: ResolvedModules,
|
|
109
|
+
layout: ResolvedLayout,
|
|
110
|
+
resolvedCollections: ResolvedCollections,
|
|
111
|
+
) {
|
|
91
112
|
const VIRTUAL_ID = 'virtual:karaoke-cms/config';
|
|
92
113
|
const RESOLVED_ID = '\0' + VIRTUAL_ID;
|
|
93
114
|
|
|
@@ -103,10 +124,11 @@ export const siteTitle = ${JSON.stringify(config.title ?? 'Karaoke')};
|
|
|
103
124
|
export const siteDescription = ${JSON.stringify(config.description ?? '')};
|
|
104
125
|
export const resolvedModules = ${JSON.stringify(resolved)};
|
|
105
126
|
export const resolvedLayout = ${JSON.stringify(layout)};
|
|
127
|
+
export const resolvedCollections = ${JSON.stringify(resolvedCollections)};
|
|
106
128
|
`;
|
|
107
129
|
}
|
|
108
130
|
},
|
|
109
131
|
};
|
|
110
132
|
}
|
|
111
133
|
|
|
112
|
-
export type { KaraokeConfig, ResolvedModules, ResolvedLayout, RegionComponent } from './types.js';
|
|
134
|
+
export type { KaraokeConfig, ResolvedModules, ResolvedLayout, ResolvedCollections, RegionComponent } from './types.js';
|
|
@@ -25,7 +25,7 @@ const posts = (await getCollection('blog', ({ data }) => data.publish === true))
|
|
|
25
25
|
) : (
|
|
26
26
|
<div class="empty-state">
|
|
27
27
|
<p>No posts published yet.</p>
|
|
28
|
-
<p>Create a Markdown file in <code>
|
|
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
29
|
</div>
|
|
30
30
|
)}
|
|
31
31
|
</Base>
|
|
@@ -25,7 +25,7 @@ const docs = (await getCollection('docs', ({ data }) => data.publish === true))
|
|
|
25
25
|
) : (
|
|
26
26
|
<div class="empty-state">
|
|
27
27
|
<p>No docs published yet.</p>
|
|
28
|
-
<p>Create a Markdown file in <code>
|
|
28
|
+
<p>Create a Markdown file in your vault's <code>docs/</code> folder and set <code>publish: true</code> in the frontmatter to make it appear here.</p>
|
|
29
29
|
</div>
|
|
30
30
|
)}
|
|
31
31
|
</Base>
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { getCollection, render } from 'astro:content';
|
|
3
|
+
import Base from '../../layouts/Base.astro';
|
|
4
|
+
import { SITE_TITLE } from '../../consts';
|
|
5
|
+
|
|
6
|
+
// Handbook is dev-only — this route is never injected in production.
|
|
7
|
+
export async function getStaticPaths() {
|
|
8
|
+
const entries = await getCollection('karaoke-cms');
|
|
9
|
+
return entries.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
|
+
|
|
19
|
+
<Base title={`${entry.data.title} — ${SITE_TITLE}`} description={entry.data.description}>
|
|
20
|
+
<article>
|
|
21
|
+
<div class="post-header">
|
|
22
|
+
<span class="handbook-badge">Handbook — dev only</span>
|
|
23
|
+
<h1>{entry.data.title}</h1>
|
|
24
|
+
</div>
|
|
25
|
+
<div class="prose">
|
|
26
|
+
<Content />
|
|
27
|
+
</div>
|
|
28
|
+
<div class="post-footer">
|
|
29
|
+
<a href="/karaoke-cms">← Handbook</a>
|
|
30
|
+
</div>
|
|
31
|
+
</article>
|
|
32
|
+
</Base>
|
|
33
|
+
|
|
34
|
+
<style>
|
|
35
|
+
.handbook-badge {
|
|
36
|
+
display: inline-block;
|
|
37
|
+
font-size: 0.75rem;
|
|
38
|
+
background: var(--color-accent, #f59e0b);
|
|
39
|
+
color: #000;
|
|
40
|
+
padding: 0.1rem 0.5rem;
|
|
41
|
+
border-radius: 999px;
|
|
42
|
+
margin-bottom: 0.5rem;
|
|
43
|
+
}
|
|
44
|
+
</style>
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { getCollection } from 'astro:content';
|
|
3
|
+
import Base from '../../layouts/Base.astro';
|
|
4
|
+
import { SITE_TITLE } from '../../consts';
|
|
5
|
+
|
|
6
|
+
// Handbook is dev-only — this route is never injected in production.
|
|
7
|
+
// Show all entries (no publish filter — handbook pages are always visible in dev).
|
|
8
|
+
const entries = (await getCollection('karaoke-cms'))
|
|
9
|
+
.sort((a, b) => (a.data.title ?? '').localeCompare(b.data.title ?? ''));
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
<Base title={`Handbook — ${SITE_TITLE}`}>
|
|
13
|
+
<div class="listing-header">
|
|
14
|
+
<h1>Handbook</h1>
|
|
15
|
+
<p class="handbook-badge">dev only</p>
|
|
16
|
+
</div>
|
|
17
|
+
{entries.length > 0 ? (
|
|
18
|
+
<ul class="post-list">
|
|
19
|
+
{entries.map(entry => (
|
|
20
|
+
<li>
|
|
21
|
+
<a href={`/karaoke-cms/${entry.id}`}>{entry.data.title}</a>
|
|
22
|
+
</li>
|
|
23
|
+
))}
|
|
24
|
+
</ul>
|
|
25
|
+
) : (
|
|
26
|
+
<div class="empty-state">
|
|
27
|
+
<p>No handbook pages found.</p>
|
|
28
|
+
<p>Add Markdown files to <code>content/karaoke-cms/</code> in your vault.</p>
|
|
29
|
+
</div>
|
|
30
|
+
)}
|
|
31
|
+
</Base>
|
|
32
|
+
|
|
33
|
+
<style>
|
|
34
|
+
.handbook-badge {
|
|
35
|
+
display: inline-block;
|
|
36
|
+
font-size: 0.75rem;
|
|
37
|
+
background: var(--color-accent, #f59e0b);
|
|
38
|
+
color: #000;
|
|
39
|
+
padding: 0.1rem 0.5rem;
|
|
40
|
+
border-radius: 999px;
|
|
41
|
+
margin-top: 0.25rem;
|
|
42
|
+
}
|
|
43
|
+
</style>
|
package/src/types.ts
CHANGED
|
@@ -1,6 +1,30 @@
|
|
|
1
1
|
export type RegionComponent = 'header' | 'main-menu' | 'search' | 'recent-posts' | 'footer';
|
|
2
2
|
|
|
3
|
+
export type CollectionMode = 'dev' | 'prod';
|
|
4
|
+
|
|
5
|
+
export interface CollectionConfig {
|
|
6
|
+
modes?: CollectionMode[];
|
|
7
|
+
label?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface ResolvedCollection {
|
|
11
|
+
modes: CollectionMode[];
|
|
12
|
+
label: string;
|
|
13
|
+
enabled: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type ResolvedCollections = Record<string, ResolvedCollection>;
|
|
17
|
+
|
|
3
18
|
export interface KaraokeConfig {
|
|
19
|
+
/**
|
|
20
|
+
* Path to the Obsidian vault root (where content/ lives).
|
|
21
|
+
* Absolute, or relative to the Astro project root.
|
|
22
|
+
* Defaults to the project root (vault and project are the same directory).
|
|
23
|
+
* Typically set via KARAOKE_VAULT in .env (gitignored) with .env.default as fallback.
|
|
24
|
+
*/
|
|
25
|
+
vault?: string;
|
|
26
|
+
/** Per-collection mode overrides. Merges with collections.yaml; this field takes precedence. */
|
|
27
|
+
collections?: Record<string, CollectionConfig>;
|
|
4
28
|
/** Site title — displayed in the browser tab and nav bar. Defaults to 'Karaoke'. */
|
|
5
29
|
title?: string;
|
|
6
30
|
/** Site description — used in RSS feed and OG meta tags. */
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { readFileSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
|
|
5
|
+
function parseEnvFile(filePath: string): Record<string, string> {
|
|
6
|
+
try {
|
|
7
|
+
const content = readFileSync(filePath, 'utf8');
|
|
8
|
+
const result: Record<string, string> = {};
|
|
9
|
+
for (const line of content.split('\n')) {
|
|
10
|
+
const trimmed = line.trim();
|
|
11
|
+
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
12
|
+
const eq = trimmed.indexOf('=');
|
|
13
|
+
if (eq === -1) continue;
|
|
14
|
+
const key = trimmed.slice(0, eq).trim();
|
|
15
|
+
const val = trimmed.slice(eq + 1).trim().replace(/^["']|["']$/g, '');
|
|
16
|
+
if (key) result[key] = val;
|
|
17
|
+
}
|
|
18
|
+
return result;
|
|
19
|
+
} catch {
|
|
20
|
+
return {};
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Load env vars from .env.default (committed defaults) and .env (local overrides).
|
|
26
|
+
* .env takes precedence over .env.default.
|
|
27
|
+
* Missing files are silently skipped.
|
|
28
|
+
*
|
|
29
|
+
* @param dir - URL or path of the directory containing the .env files.
|
|
30
|
+
*/
|
|
31
|
+
export function loadEnv(dir: URL | string): Record<string, string> {
|
|
32
|
+
const dirPath = dir instanceof URL ? fileURLToPath(dir) : dir;
|
|
33
|
+
return {
|
|
34
|
+
...parseEnvFile(join(dirPath, '.env.default')),
|
|
35
|
+
...parseEnvFile(join(dirPath, '.env')),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { readFileSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { parse } from 'yaml';
|
|
4
|
+
import type { CollectionConfig, CollectionMode, ResolvedCollections } from '../types.js';
|
|
5
|
+
|
|
6
|
+
// Hardcoded defaults — used when collections.yaml is absent or unreadable.
|
|
7
|
+
// karaoke-cms is dev-only so it never enters the production build graph.
|
|
8
|
+
const DEFAULTS: Record<string, { modes: CollectionMode[]; label: string }> = {
|
|
9
|
+
blog: { modes: ['dev', 'prod'], label: 'Blog' },
|
|
10
|
+
docs: { modes: ['dev', 'prod'], label: 'Docs' },
|
|
11
|
+
'karaoke-cms': { modes: ['dev'], label: 'Handbook' },
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
interface YamlShape {
|
|
15
|
+
collections?: Record<string, { modes?: string[]; label?: string }>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Resolve the active content collections for the current build mode.
|
|
20
|
+
*
|
|
21
|
+
* Data flow:
|
|
22
|
+
* DEFAULTS
|
|
23
|
+
* ← merged with collections.yaml (YAML overrides defaults per-key)
|
|
24
|
+
* ← merged with configCollections (karaoke.config.ts overrides per-key)
|
|
25
|
+
* → filter by isProd
|
|
26
|
+
* → ResolvedCollections
|
|
27
|
+
*
|
|
28
|
+
* @param rootDir Absolute path to the vault root (where blog/, docs/, karaoke-cms/ live).
|
|
29
|
+
* @param isProd True during `astro build`, false during `astro dev`.
|
|
30
|
+
* @param configCollections Optional overrides from karaoke.config.ts `collections` field.
|
|
31
|
+
*/
|
|
32
|
+
export function resolveCollections(
|
|
33
|
+
rootDir: string,
|
|
34
|
+
isProd: boolean,
|
|
35
|
+
configCollections?: Record<string, CollectionConfig>,
|
|
36
|
+
): ResolvedCollections {
|
|
37
|
+
// 1. Read and parse collections.yaml (graceful fallback to defaults if absent)
|
|
38
|
+
let yamlCollections: Record<string, { modes?: CollectionMode[]; label?: string }> = {};
|
|
39
|
+
try {
|
|
40
|
+
const yamlPath = join(rootDir, 'karaoke-cms/config/collections.yaml');
|
|
41
|
+
const raw = readFileSync(yamlPath, 'utf8');
|
|
42
|
+
const parsed = parse(raw) as YamlShape;
|
|
43
|
+
if (parsed?.collections && typeof parsed.collections === 'object') {
|
|
44
|
+
for (const [name, val] of Object.entries(parsed.collections)) {
|
|
45
|
+
yamlCollections[name] = {
|
|
46
|
+
modes: Array.isArray(val?.modes)
|
|
47
|
+
? (val.modes.filter(m => m === 'dev' || m === 'prod') as CollectionMode[])
|
|
48
|
+
: undefined,
|
|
49
|
+
label: typeof val?.label === 'string' ? val.label : undefined,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
} catch {
|
|
54
|
+
// File absent or unreadable — use hardcoded defaults
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// 2. Merge: DEFAULTS ← yaml ← configCollections
|
|
58
|
+
const allNames = new Set([
|
|
59
|
+
...Object.keys(DEFAULTS),
|
|
60
|
+
...Object.keys(yamlCollections),
|
|
61
|
+
...(configCollections ? Object.keys(configCollections) : []),
|
|
62
|
+
]);
|
|
63
|
+
|
|
64
|
+
const result: ResolvedCollections = {};
|
|
65
|
+
for (const name of allNames) {
|
|
66
|
+
const base = DEFAULTS[name];
|
|
67
|
+
const yaml = yamlCollections[name];
|
|
68
|
+
const cfg = configCollections?.[name];
|
|
69
|
+
|
|
70
|
+
const modes: CollectionMode[] =
|
|
71
|
+
cfg?.modes ?? yaml?.modes ?? base?.modes ?? ['dev', 'prod'];
|
|
72
|
+
const label: string =
|
|
73
|
+
cfg?.label ?? yaml?.label ?? base?.label ?? name;
|
|
74
|
+
|
|
75
|
+
result[name] = {
|
|
76
|
+
modes,
|
|
77
|
+
label,
|
|
78
|
+
enabled: isProd ? modes.includes('prod') : modes.includes('dev'),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return result;
|
|
83
|
+
}
|