@karaoke-cms/theme-default 0.9.0 → 0.9.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/package.json +8 -5
- package/src/components/AudienceGrid.astro +143 -0
- package/src/components/CtaSection.astro +91 -0
- package/src/components/FeatureGrid.astro +138 -0
- package/src/components/Hero.astro +141 -0
- package/src/components/HowItWorks.astro +117 -0
- package/src/components/InstallBox.astro +71 -0
- package/src/components/MetaphorSection.astro +69 -0
- package/src/components/ScaleSection.astro +146 -0
- package/src/index.ts +53 -6
- package/src/pages/404.astro +3 -3
- package/src/pages/docs/[slug].astro +3 -3
- package/src/pages/docs/index.astro +3 -3
- package/src/pages/index.astro +18 -60
- package/src/pages/tags/[tag].astro +3 -3
- package/src/pages/tags/index.astro +3 -3
- package/src/styles/blog.css +9 -0
- package/src/styles.css +153 -20
- package/src/pages/blog/[slug].astro +0 -66
- package/src/pages/blog/index.astro +0 -31
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
---
|
|
2
|
+
interface Props {
|
|
3
|
+
command?: string;
|
|
4
|
+
size?: 'sm' | 'lg';
|
|
5
|
+
}
|
|
6
|
+
const { command = 'npm create @karaoke-cms@latest', size = 'sm' } = Astro.props;
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
<div class:list={['install-box', { large: size === 'lg' }]}>
|
|
10
|
+
<code>{command}</code>
|
|
11
|
+
<button class="copy-btn" data-copy={command} aria-label="Copy to clipboard">
|
|
12
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
13
|
+
<rect x="9" y="9" width="13" height="13" rx="2"/>
|
|
14
|
+
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>
|
|
15
|
+
</svg>
|
|
16
|
+
</button>
|
|
17
|
+
</div>
|
|
18
|
+
|
|
19
|
+
<script>
|
|
20
|
+
const CHECK_SVG = `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="20 6 9 17 4 12"/></svg>`;
|
|
21
|
+
const COPY_SVG = `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>`;
|
|
22
|
+
|
|
23
|
+
document.querySelectorAll<HTMLButtonElement>('.copy-btn').forEach(btn => {
|
|
24
|
+
if (btn.dataset.bound) return;
|
|
25
|
+
btn.dataset.bound = '1';
|
|
26
|
+
btn.addEventListener('click', () => {
|
|
27
|
+
const text = btn.getAttribute('data-copy') ?? '';
|
|
28
|
+
navigator.clipboard.writeText(text).then(() => {
|
|
29
|
+
btn.innerHTML = CHECK_SVG;
|
|
30
|
+
setTimeout(() => { btn.innerHTML = COPY_SVG; }, 1800);
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
</script>
|
|
35
|
+
|
|
36
|
+
<style>
|
|
37
|
+
.install-box {
|
|
38
|
+
display: inline-flex;
|
|
39
|
+
align-items: center;
|
|
40
|
+
background: var(--color-section-dark-2, #1e293b);
|
|
41
|
+
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
42
|
+
border-radius: var(--radius-lg, 12px);
|
|
43
|
+
overflow: hidden;
|
|
44
|
+
}
|
|
45
|
+
.install-box code {
|
|
46
|
+
background: transparent;
|
|
47
|
+
border: none;
|
|
48
|
+
padding: 10px 16px;
|
|
49
|
+
color: #a5b4fc;
|
|
50
|
+
font-size: 0.9rem;
|
|
51
|
+
font-family: var(--font-mono);
|
|
52
|
+
white-space: nowrap;
|
|
53
|
+
}
|
|
54
|
+
.install-box.large code {
|
|
55
|
+
font-size: 1rem;
|
|
56
|
+
padding: 12px 20px;
|
|
57
|
+
}
|
|
58
|
+
.copy-btn {
|
|
59
|
+
background: none;
|
|
60
|
+
border: none;
|
|
61
|
+
border-left: 1px solid rgba(255, 255, 255, 0.1);
|
|
62
|
+
padding: 10px 14px;
|
|
63
|
+
color: #64748b;
|
|
64
|
+
cursor: pointer;
|
|
65
|
+
display: flex;
|
|
66
|
+
align-items: center;
|
|
67
|
+
flex-shrink: 0;
|
|
68
|
+
transition: color 0.15s;
|
|
69
|
+
}
|
|
70
|
+
.copy-btn:hover { color: #a5b4fc; }
|
|
71
|
+
</style>
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
---
|
|
2
|
+
interface Props {
|
|
3
|
+
icon?: string;
|
|
4
|
+
}
|
|
5
|
+
const { icon = '🎤' } = Astro.props;
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
<section class="metaphor">
|
|
9
|
+
<div class="container">
|
|
10
|
+
<div class="metaphor-inner">
|
|
11
|
+
<div class="metaphor-icon" aria-hidden="true">{icon}</div>
|
|
12
|
+
<div class="metaphor-text">
|
|
13
|
+
<h2><slot name="heading">The karaoke idea</slot></h2>
|
|
14
|
+
<p>
|
|
15
|
+
<slot>
|
|
16
|
+
In karaoke, the stage is set, the music is playing, the lyrics scroll on screen.
|
|
17
|
+
<strong>You just show up and sing.</strong>
|
|
18
|
+
karaoke-cms works the same way: the infrastructure is in place, the AI is running,
|
|
19
|
+
the site is deployed. You write. We handle the rest.
|
|
20
|
+
</slot>
|
|
21
|
+
</p>
|
|
22
|
+
</div>
|
|
23
|
+
</div>
|
|
24
|
+
</div>
|
|
25
|
+
</section>
|
|
26
|
+
|
|
27
|
+
<style>
|
|
28
|
+
.metaphor {
|
|
29
|
+
background: var(--color-bg);
|
|
30
|
+
border-bottom: 1px solid var(--color-border);
|
|
31
|
+
padding: 72px 0;
|
|
32
|
+
}
|
|
33
|
+
.container {
|
|
34
|
+
max-width: var(--width-landing, 1100px);
|
|
35
|
+
margin: 0 auto;
|
|
36
|
+
padding: 0 24px;
|
|
37
|
+
}
|
|
38
|
+
.metaphor-inner {
|
|
39
|
+
display: flex;
|
|
40
|
+
align-items: center;
|
|
41
|
+
gap: 48px;
|
|
42
|
+
}
|
|
43
|
+
.metaphor-icon {
|
|
44
|
+
font-size: 4rem;
|
|
45
|
+
flex-shrink: 0;
|
|
46
|
+
line-height: 1;
|
|
47
|
+
}
|
|
48
|
+
.metaphor-text h2 {
|
|
49
|
+
font-size: clamp(1.5rem, 2.5vw, 2rem);
|
|
50
|
+
font-weight: 700;
|
|
51
|
+
letter-spacing: -0.01em;
|
|
52
|
+
color: var(--color-text);
|
|
53
|
+
margin: 0 0 12px;
|
|
54
|
+
}
|
|
55
|
+
.metaphor-text p {
|
|
56
|
+
font-size: 1.0625rem;
|
|
57
|
+
line-height: 1.7;
|
|
58
|
+
color: var(--color-muted);
|
|
59
|
+
max-width: 600px;
|
|
60
|
+
margin: 0;
|
|
61
|
+
}
|
|
62
|
+
.metaphor-text strong {
|
|
63
|
+
color: var(--color-text);
|
|
64
|
+
}
|
|
65
|
+
@media (max-width: 640px) {
|
|
66
|
+
.metaphor-inner { flex-direction: column; gap: 24px; }
|
|
67
|
+
.metaphor-icon { font-size: 3rem; }
|
|
68
|
+
}
|
|
69
|
+
</style>
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
---
|
|
2
|
+
interface ScaleRow {
|
|
3
|
+
label: string;
|
|
4
|
+
pct: number; // 0–100
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
label?: string;
|
|
9
|
+
heading?: string;
|
|
10
|
+
body?: string;
|
|
11
|
+
items?: string[];
|
|
12
|
+
scale?: ScaleRow[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const {
|
|
16
|
+
label = 'Scale',
|
|
17
|
+
heading = 'Starts simple. Grows with you.',
|
|
18
|
+
body = 'Launch a personal blog in an afternoon. Grow into a multi-author editorial platform on the same foundation — same npm packages, same git workflow, same privacy model.',
|
|
19
|
+
items = [
|
|
20
|
+
'Collections scoped per team or department',
|
|
21
|
+
'Module system for search, comments, custom features',
|
|
22
|
+
'Composable layout regions — swap any component',
|
|
23
|
+
'Publish your own npm theme for your brand',
|
|
24
|
+
],
|
|
25
|
+
scale = [
|
|
26
|
+
{ label: 'Personal blog', pct: 28 },
|
|
27
|
+
{ label: 'Team wiki', pct: 52 },
|
|
28
|
+
{ label: 'Docs site', pct: 70 },
|
|
29
|
+
{ label: 'Enterprise platform', pct: 100 },
|
|
30
|
+
],
|
|
31
|
+
} = Astro.props;
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
<section class="scale-section">
|
|
35
|
+
<div class="container">
|
|
36
|
+
<div class="inner">
|
|
37
|
+
<div class="copy">
|
|
38
|
+
<span class="section-label on-dark">{label}</span>
|
|
39
|
+
<h2>{heading}</h2>
|
|
40
|
+
<p>{body}</p>
|
|
41
|
+
<ul>
|
|
42
|
+
{items.map(item => (
|
|
43
|
+
<li>
|
|
44
|
+
<span class="dot" aria-hidden="true"></span>
|
|
45
|
+
<span>{item}</span>
|
|
46
|
+
</li>
|
|
47
|
+
))}
|
|
48
|
+
</ul>
|
|
49
|
+
</div>
|
|
50
|
+
<div class="scale-bars" aria-hidden="true">
|
|
51
|
+
{scale.map(row => (
|
|
52
|
+
<div class="scale-row">
|
|
53
|
+
<span class="scale-label">{row.label}</span>
|
|
54
|
+
<div class="scale-track">
|
|
55
|
+
<div class="scale-fill" style={`width: ${row.pct}%`}></div>
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
))}
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
</section>
|
|
63
|
+
|
|
64
|
+
<style>
|
|
65
|
+
.scale-section {
|
|
66
|
+
background: var(--color-section-dark, #0f172a);
|
|
67
|
+
padding: 88px 0;
|
|
68
|
+
}
|
|
69
|
+
.container {
|
|
70
|
+
max-width: var(--width-landing, 1100px);
|
|
71
|
+
margin: 0 auto;
|
|
72
|
+
padding: 0 24px;
|
|
73
|
+
}
|
|
74
|
+
.inner {
|
|
75
|
+
display: grid;
|
|
76
|
+
grid-template-columns: 1fr 1fr;
|
|
77
|
+
gap: 64px;
|
|
78
|
+
align-items: center;
|
|
79
|
+
}
|
|
80
|
+
.copy h2 {
|
|
81
|
+
font-size: clamp(1.5rem, 3vw, 2.25rem);
|
|
82
|
+
font-weight: 700;
|
|
83
|
+
letter-spacing: -0.01em;
|
|
84
|
+
color: var(--color-on-dark, #e2e8f0);
|
|
85
|
+
margin: 0 0 16px;
|
|
86
|
+
}
|
|
87
|
+
.copy p {
|
|
88
|
+
color: var(--color-on-dark-2, #94a3b8);
|
|
89
|
+
font-size: 1.0625rem;
|
|
90
|
+
line-height: 1.7;
|
|
91
|
+
margin: 0 0 28px;
|
|
92
|
+
}
|
|
93
|
+
ul {
|
|
94
|
+
list-style: none;
|
|
95
|
+
padding: 0;
|
|
96
|
+
margin: 0;
|
|
97
|
+
display: flex;
|
|
98
|
+
flex-direction: column;
|
|
99
|
+
gap: 12px;
|
|
100
|
+
}
|
|
101
|
+
ul li {
|
|
102
|
+
display: flex;
|
|
103
|
+
align-items: flex-start;
|
|
104
|
+
gap: 10px;
|
|
105
|
+
color: var(--color-on-dark-2, #94a3b8);
|
|
106
|
+
font-size: 0.9375rem;
|
|
107
|
+
}
|
|
108
|
+
.dot {
|
|
109
|
+
width: 6px;
|
|
110
|
+
height: 6px;
|
|
111
|
+
border-radius: 50%;
|
|
112
|
+
background: var(--color-accent, #6366f1);
|
|
113
|
+
flex-shrink: 0;
|
|
114
|
+
margin-top: 8px;
|
|
115
|
+
}
|
|
116
|
+
.scale-bars {
|
|
117
|
+
display: flex;
|
|
118
|
+
flex-direction: column;
|
|
119
|
+
gap: 20px;
|
|
120
|
+
}
|
|
121
|
+
.scale-row {
|
|
122
|
+
display: flex;
|
|
123
|
+
flex-direction: column;
|
|
124
|
+
gap: 6px;
|
|
125
|
+
}
|
|
126
|
+
.scale-label {
|
|
127
|
+
font-size: 0.8125rem;
|
|
128
|
+
color: var(--color-on-dark-2, #94a3b8);
|
|
129
|
+
font-weight: 500;
|
|
130
|
+
}
|
|
131
|
+
.scale-track {
|
|
132
|
+
height: 8px;
|
|
133
|
+
background: rgba(255, 255, 255, 0.08);
|
|
134
|
+
border-radius: 999px;
|
|
135
|
+
overflow: hidden;
|
|
136
|
+
}
|
|
137
|
+
.scale-fill {
|
|
138
|
+
height: 100%;
|
|
139
|
+
background: linear-gradient(90deg, var(--color-accent, #6366f1), var(--color-accent-2, #8b5cf6));
|
|
140
|
+
border-radius: 999px;
|
|
141
|
+
}
|
|
142
|
+
@media (max-width: 768px) {
|
|
143
|
+
.inner { grid-template-columns: 1fr; gap: 40px; }
|
|
144
|
+
.scale-bars { display: none; }
|
|
145
|
+
}
|
|
146
|
+
</style>
|
package/src/index.ts
CHANGED
|
@@ -1,17 +1,31 @@
|
|
|
1
1
|
import type { AstroIntegration } from 'astro';
|
|
2
|
-
import type { KaraokeConfig } from '@karaoke-cms/astro';
|
|
3
2
|
import { fileURLToPath } from 'url';
|
|
3
|
+
import { existsSync } from 'fs';
|
|
4
4
|
|
|
5
5
|
const __dirname = fileURLToPath(new URL('.', import.meta.url));
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
// Constructs the Astro integration for theme-default.
|
|
8
|
+
// Exported separately so it can be called by both the new factory (themeDefault)
|
|
9
|
+
// and the legacy default export without triggering a @karaoke-cms/astro import.
|
|
10
|
+
function buildIntegration(): AstroIntegration {
|
|
8
11
|
return {
|
|
9
12
|
name: '@karaoke-cms/theme-default',
|
|
10
13
|
hooks: {
|
|
11
|
-
'astro:config:setup': ({ injectRoute, updateConfig }) => {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
14
|
+
'astro:config:setup': ({ injectRoute, updateConfig, config: astroConfig }) => {
|
|
15
|
+
// Blog routes are owned by @karaoke-cms/module-blog.
|
|
16
|
+
// theme-default injects them here so that the old string-based theme
|
|
17
|
+
// config (theme: '@karaoke-cms/theme-default') continues to work.
|
|
18
|
+
// When the new modules[] array config is fully adopted, theme-default
|
|
19
|
+
// will stop injecting these and let the core integration do it.
|
|
20
|
+
injectRoute({ pattern: '/blog', entrypoint: '@karaoke-cms/module-blog/pages/list' });
|
|
21
|
+
injectRoute({ pattern: '/blog/[slug]', entrypoint: '@karaoke-cms/module-blog/pages/post' });
|
|
22
|
+
|
|
23
|
+
// Docs, tags, and homepage remain owned by theme-default.
|
|
24
|
+
// Skip injecting '/' if the user already has src/pages/index.astro.
|
|
25
|
+
const userIndex = fileURLToPath(new URL('src/pages/index.astro', astroConfig.root));
|
|
26
|
+
if (!existsSync(userIndex)) {
|
|
27
|
+
injectRoute({ pattern: '/', entrypoint: `${__dirname}pages/index.astro` });
|
|
28
|
+
}
|
|
15
29
|
injectRoute({ pattern: '/docs', entrypoint: `${__dirname}pages/docs/index.astro` });
|
|
16
30
|
injectRoute({ pattern: '/docs/[slug]', entrypoint: `${__dirname}pages/docs/[slug].astro` });
|
|
17
31
|
injectRoute({ pattern: '/tags', entrypoint: `${__dirname}pages/tags/index.astro` });
|
|
@@ -29,3 +43,36 @@ export default function themeDefault(_config: KaraokeConfig): AstroIntegration {
|
|
|
29
43
|
},
|
|
30
44
|
};
|
|
31
45
|
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* New API: returns a ThemeInstance for use with defineConfig().
|
|
49
|
+
*
|
|
50
|
+
* Note: ThemeInstance is constructed manually here (without importing defineTheme
|
|
51
|
+
* from @karaoke-cms/astro) so that this file remains loadable via Node's native
|
|
52
|
+
* ESM when used through the legacy string-based theme config path.
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* // karaoke.config.ts
|
|
56
|
+
* import { themeDefault } from '@karaoke-cms/theme-default';
|
|
57
|
+
* export default defineConfig({
|
|
58
|
+
* theme: themeDefault({ implements: [blog({ mount: '/blog' })] }),
|
|
59
|
+
* });
|
|
60
|
+
*/
|
|
61
|
+
export function themeDefault(config: { implements?: Array<{ id: string }> } = {}) {
|
|
62
|
+
return {
|
|
63
|
+
_type: 'theme-instance' as const,
|
|
64
|
+
id: 'theme-default',
|
|
65
|
+
implementedModuleIds: (config.implements ?? []).map(m => m.id),
|
|
66
|
+
toAstroIntegration: () => buildIntegration(),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Legacy default export — supports the old string-based theme config:
|
|
72
|
+
* theme: '@karaoke-cms/theme-default'
|
|
73
|
+
* The core integration calls themeModule.default(config) when loading
|
|
74
|
+
* themes by package name string.
|
|
75
|
+
*/
|
|
76
|
+
export default function themeDefaultLegacy(_config: unknown): AstroIntegration {
|
|
77
|
+
return buildIntegration();
|
|
78
|
+
}
|
package/src/pages/404.astro
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
---
|
|
2
|
-
import
|
|
2
|
+
import DefaultPage from '@karaoke-cms/astro/layouts/DefaultPage.astro';
|
|
3
3
|
import { siteTitle } from 'virtual:karaoke-cms/config';
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
<
|
|
6
|
+
<DefaultPage title={`Page not found — ${siteTitle}`}>
|
|
7
7
|
<div class="post-header">
|
|
8
8
|
<h1>Page not found</h1>
|
|
9
9
|
<p class="post-meta">The page you're looking for doesn't exist or hasn't been published.</p>
|
|
@@ -11,4 +11,4 @@ import { siteTitle } from 'virtual:karaoke-cms/config';
|
|
|
11
11
|
<div class="prose">
|
|
12
12
|
<p><a href="/">Go home →</a></p>
|
|
13
13
|
</div>
|
|
14
|
-
</
|
|
14
|
+
</DefaultPage>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
import { getCollection, render } from 'astro:content';
|
|
3
|
-
import
|
|
3
|
+
import DefaultPage from '@karaoke-cms/astro/layouts/DefaultPage.astro';
|
|
4
4
|
import ModuleLoader from '@karaoke-cms/astro/components/ModuleLoader.astro';
|
|
5
5
|
import { siteTitle } from 'virtual:karaoke-cms/config';
|
|
6
6
|
|
|
@@ -27,7 +27,7 @@ const related = relatedIds.length > 0
|
|
|
27
27
|
: [];
|
|
28
28
|
---
|
|
29
29
|
|
|
30
|
-
<
|
|
30
|
+
<DefaultPage title={`${entry.data.title} — ${siteTitle}`} description={entry.data.description} type="article">
|
|
31
31
|
<article>
|
|
32
32
|
<div class="post-header">
|
|
33
33
|
<h1>{entry.data.title}</h1>
|
|
@@ -63,4 +63,4 @@ const related = relatedIds.length > 0
|
|
|
63
63
|
</div>
|
|
64
64
|
</article>
|
|
65
65
|
<ModuleLoader comments={entry.data.comments} />
|
|
66
|
-
</
|
|
66
|
+
</DefaultPage>
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
---
|
|
2
2
|
import { getCollection } from 'astro:content';
|
|
3
|
-
import
|
|
3
|
+
import DefaultPage from '@karaoke-cms/astro/layouts/DefaultPage.astro';
|
|
4
4
|
import { siteTitle } from 'virtual:karaoke-cms/config';
|
|
5
5
|
|
|
6
6
|
const docs = (await getCollection('docs', ({ data }) => data.publish === true))
|
|
7
7
|
.sort((a, b) => a.data.title.localeCompare(b.data.title));
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
-
<
|
|
10
|
+
<DefaultPage title={`Docs — ${siteTitle}`}>
|
|
11
11
|
<div class="listing-header">
|
|
12
12
|
<h1>Docs</h1>
|
|
13
13
|
</div>
|
|
@@ -28,4 +28,4 @@ const docs = (await getCollection('docs', ({ data }) => data.publish === true))
|
|
|
28
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
|
+
</DefaultPage>
|
package/src/pages/index.astro
CHANGED
|
@@ -1,65 +1,23 @@
|
|
|
1
1
|
---
|
|
2
|
-
import
|
|
3
|
-
import Base from '@karaoke-cms/astro/layouts/Base.astro';
|
|
2
|
+
import DefaultPage from '@karaoke-cms/astro/layouts/DefaultPage.astro';
|
|
4
3
|
import { siteTitle, siteDescription } from 'virtual:karaoke-cms/config';
|
|
4
|
+
import Hero from '../components/Hero.astro';
|
|
5
|
+
import MetaphorSection from '../components/MetaphorSection.astro';
|
|
6
|
+
import AudienceGrid from '../components/AudienceGrid.astro';
|
|
7
|
+
import FeatureGrid from '../components/FeatureGrid.astro';
|
|
8
|
+
import HowItWorks from '../components/HowItWorks.astro';
|
|
9
|
+
import ScaleSection from '../components/ScaleSection.astro';
|
|
10
|
+
import CtaSection from '../components/CtaSection.astro';
|
|
5
11
|
|
|
6
|
-
const
|
|
7
|
-
.sort((a, b) => (b.data.date?.valueOf() ?? 0) - (a.data.date?.valueOf() ?? 0))
|
|
8
|
-
.slice(0, 5);
|
|
9
|
-
|
|
10
|
-
const docs = (await getCollection('docs', ({ data }) => data.publish === true))
|
|
11
|
-
.sort((a, b) => a.data.title.localeCompare(b.data.title))
|
|
12
|
-
.slice(0, 5);
|
|
12
|
+
const ghUrl = 'https://github.com/Tautai-Net/karaoke-cms';
|
|
13
13
|
---
|
|
14
14
|
|
|
15
|
-
<
|
|
16
|
-
<
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
<a href={`/blog/${post.id}`}>{post.data.title}</a>
|
|
25
|
-
{post.data.date && (
|
|
26
|
-
<span class="post-date">{post.data.date.toISOString().slice(0, 10)}</span>
|
|
27
|
-
)}
|
|
28
|
-
</li>
|
|
29
|
-
))}
|
|
30
|
-
</ul>
|
|
31
|
-
<a href="/blog" class="view-all">View all →</a>
|
|
32
|
-
</>
|
|
33
|
-
) : (
|
|
34
|
-
<div class="empty-state">
|
|
35
|
-
<p>No posts yet.</p>
|
|
36
|
-
<p>Add a Markdown file to <code>blog/</code> with <code>publish: true</code> in the frontmatter.</p>
|
|
37
|
-
</div>
|
|
38
|
-
)}
|
|
39
|
-
</section>
|
|
40
|
-
|
|
41
|
-
<section class="home-section">
|
|
42
|
-
<h2>Docs</h2>
|
|
43
|
-
{docs.length > 0 ? (
|
|
44
|
-
<>
|
|
45
|
-
<ul class="post-list">
|
|
46
|
-
{docs.map(doc => (
|
|
47
|
-
<li>
|
|
48
|
-
<a href={`/docs/${doc.id}`}>{doc.data.title}</a>
|
|
49
|
-
{doc.data.date && (
|
|
50
|
-
<span class="post-date">{doc.data.date.toISOString().slice(0, 10)}</span>
|
|
51
|
-
)}
|
|
52
|
-
</li>
|
|
53
|
-
))}
|
|
54
|
-
</ul>
|
|
55
|
-
<a href="/docs" class="view-all">View all →</a>
|
|
56
|
-
</>
|
|
57
|
-
) : (
|
|
58
|
-
<div class="empty-state">
|
|
59
|
-
<p>No docs yet.</p>
|
|
60
|
-
<p>Add a Markdown file to <code>docs/</code> with <code>publish: true</code> in the frontmatter.</p>
|
|
61
|
-
</div>
|
|
62
|
-
)}
|
|
63
|
-
</section>
|
|
64
|
-
</div>
|
|
65
|
-
</Base>
|
|
15
|
+
<DefaultPage title={siteTitle} description={siteDescription} variant="landing">
|
|
16
|
+
<Hero ghUrl={ghUrl} />
|
|
17
|
+
<MetaphorSection />
|
|
18
|
+
<AudienceGrid />
|
|
19
|
+
<FeatureGrid />
|
|
20
|
+
<HowItWorks />
|
|
21
|
+
<ScaleSection />
|
|
22
|
+
<CtaSection ghUrl={ghUrl} />
|
|
23
|
+
</DefaultPage>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
import { getCollection } from 'astro:content';
|
|
3
|
-
import
|
|
3
|
+
import DefaultPage from '@karaoke-cms/astro/layouts/DefaultPage.astro';
|
|
4
4
|
import { siteTitle } from 'virtual:karaoke-cms/config';
|
|
5
5
|
|
|
6
6
|
export async function getStaticPaths() {
|
|
@@ -34,7 +34,7 @@ function href(entry: { collection: string; id: string }) {
|
|
|
34
34
|
}
|
|
35
35
|
---
|
|
36
36
|
|
|
37
|
-
<
|
|
37
|
+
<DefaultPage title={`#${tag} — ${siteTitle}`}>
|
|
38
38
|
<div class="listing-header">
|
|
39
39
|
<h1>#{tag}</h1>
|
|
40
40
|
<p><a href="/tags">← All tags</a></p>
|
|
@@ -50,4 +50,4 @@ function href(entry: { collection: string; id: string }) {
|
|
|
50
50
|
</li>
|
|
51
51
|
))}
|
|
52
52
|
</ul>
|
|
53
|
-
</
|
|
53
|
+
</DefaultPage>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
import { getCollection } from 'astro:content';
|
|
3
|
-
import
|
|
3
|
+
import DefaultPage from '@karaoke-cms/astro/layouts/DefaultPage.astro';
|
|
4
4
|
import { siteTitle } from 'virtual:karaoke-cms/config';
|
|
5
5
|
|
|
6
6
|
const [blog, docs] = await Promise.all([
|
|
@@ -19,7 +19,7 @@ for (const entry of [...blog, ...docs]) {
|
|
|
19
19
|
const tags = [...counts.entries()].sort((a, b) => a[0].localeCompare(b[0]));
|
|
20
20
|
---
|
|
21
21
|
|
|
22
|
-
<
|
|
22
|
+
<DefaultPage title={`Tags — ${siteTitle}`}>
|
|
23
23
|
<div class="listing-header">
|
|
24
24
|
<h1>Tags</h1>
|
|
25
25
|
</div>
|
|
@@ -38,4 +38,4 @@ const tags = [...counts.entries()].sort((a, b) => a[0].localeCompare(b[0]));
|
|
|
38
38
|
<p>Tags are added to posts via the <code>tags</code> frontmatter field or by AI enrichment.</p>
|
|
39
39
|
</div>
|
|
40
40
|
)}
|
|
41
|
-
</
|
|
41
|
+
</DefaultPage>
|
|
@@ -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
|
+
*/
|