@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
package/client.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type declarations for karaoke-cms virtual modules.
|
|
3
|
+
*
|
|
4
|
+
* Add to your project's src/env.d.ts:
|
|
5
|
+
* /// <reference types="@karaoke-cms/astro/client" />
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
declare module 'virtual:karaoke-cms/config' {
|
|
9
|
+
import type { ResolvedModules, ResolvedLayout } from '@karaoke-cms/astro';
|
|
10
|
+
|
|
11
|
+
/** Site title from karaoke.config.ts */
|
|
12
|
+
export const siteTitle: string;
|
|
13
|
+
/** Site description from karaoke.config.ts */
|
|
14
|
+
export const siteDescription: string;
|
|
15
|
+
/** Resolved modules config (defaults filled in) */
|
|
16
|
+
export const resolvedModules: ResolvedModules;
|
|
17
|
+
/** Resolved layout config (defaults filled in) */
|
|
18
|
+
export const resolvedLayout: ResolvedLayout;
|
|
19
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@karaoke-cms/astro",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"version": "0.6.1",
|
|
5
|
+
"description": "Astro integration for karaoke-cms — ships all routes, themes, and modules",
|
|
6
|
+
"main": "./src/index.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/index.ts",
|
|
9
|
+
"./client": "./client.d.ts",
|
|
10
|
+
"./collections": "./src/collections.ts",
|
|
11
|
+
"./pages/index.astro": "./src/pages/index.astro",
|
|
12
|
+
"./pages/blog/index.astro": "./src/pages/blog/index.astro",
|
|
13
|
+
"./pages/blog/[slug].astro": "./src/pages/blog/[slug].astro",
|
|
14
|
+
"./pages/docs/index.astro": "./src/pages/docs/index.astro",
|
|
15
|
+
"./pages/docs/[slug].astro": "./src/pages/docs/[slug].astro",
|
|
16
|
+
"./pages/tags/index.astro": "./src/pages/tags/index.astro",
|
|
17
|
+
"./pages/tags/[tag].astro": "./src/pages/tags/[tag].astro",
|
|
18
|
+
"./pages/404.astro": "./src/pages/404.astro",
|
|
19
|
+
"./pages/rss.xml.ts": "./src/pages/rss.xml.ts"
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"src/",
|
|
23
|
+
"client.d.ts"
|
|
24
|
+
],
|
|
25
|
+
"keywords": [
|
|
26
|
+
"astro",
|
|
27
|
+
"cms",
|
|
28
|
+
"obsidian",
|
|
29
|
+
"astro-integration"
|
|
30
|
+
],
|
|
31
|
+
"peerDependencies": {
|
|
32
|
+
"astro": ">=6.0.0"
|
|
33
|
+
},
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"@astrojs/rss": "^4.0.17",
|
|
36
|
+
"@astrojs/sitemap": "^3.7.1",
|
|
37
|
+
"remark-wiki-link": "^2.0.1"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"vitest": "^4.1.1",
|
|
41
|
+
"astro": "^6.0.8"
|
|
42
|
+
},
|
|
43
|
+
"scripts": {
|
|
44
|
+
"test": "vitest run test/validate-config.test.js test/resolve-modules.test.js test/resolve-layout.test.js"
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { defineCollection, z } from 'astro:content';
|
|
2
|
+
import { glob } from 'astro/loaders';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
import { join } from 'path';
|
|
5
|
+
|
|
6
|
+
const baseSchema = z.object({
|
|
7
|
+
title: z.string(),
|
|
8
|
+
publish: z.boolean().optional().default(false), // missing = false (private by default)
|
|
9
|
+
date: z.coerce.date().optional(),
|
|
10
|
+
author: z.union([z.string(), z.array(z.string())]).optional(),
|
|
11
|
+
// v0.2 AI-enriched fields — optional so notes validate before and after enrichment
|
|
12
|
+
tags: z.array(z.string()).optional(),
|
|
13
|
+
description: z.string().optional(),
|
|
14
|
+
reading_time: z.number().optional(),
|
|
15
|
+
related: z.array(z.string()).optional(),
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Create Astro content collections for karaoke-cms.
|
|
20
|
+
*
|
|
21
|
+
* @param root - URL pointing to the project root (where content/ lives).
|
|
22
|
+
* Typically: `new URL('../..', import.meta.url)` from src/content.config.ts
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* // src/content.config.ts
|
|
26
|
+
* import { makeCollections } from '@karaoke-cms/astro/collections';
|
|
27
|
+
* export const collections = makeCollections(new URL('../..', import.meta.url));
|
|
28
|
+
*/
|
|
29
|
+
export function makeCollections(root: URL) {
|
|
30
|
+
const rootDir = fileURLToPath(root);
|
|
31
|
+
return {
|
|
32
|
+
// base paths are absolute, resolving content/ relative to the project root
|
|
33
|
+
blog: defineCollection({
|
|
34
|
+
loader: glob({ pattern: '**/*.md', base: join(rootDir, 'content/blog') }),
|
|
35
|
+
schema: baseSchema.extend({ comments: z.boolean().optional().default(true) }),
|
|
36
|
+
}),
|
|
37
|
+
docs: defineCollection({
|
|
38
|
+
loader: glob({ pattern: '**/*.md', base: join(rootDir, 'content/docs') }),
|
|
39
|
+
schema: baseSchema.extend({ comments: z.boolean().optional().default(false) }),
|
|
40
|
+
}),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
---
|
|
2
|
+
// ModuleLoader.astro — renders per-page modules (comments) for blog/docs slug pages.
|
|
3
|
+
// Search lives in Base.astro nav (slot: 'nav'); comments are here (slot: 'post-footer').
|
|
4
|
+
import Comments from '../modules/comments/Comments.astro';
|
|
5
|
+
import { resolvedModules } from 'virtual:karaoke-cms/config';
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
/** Per-page frontmatter override. When provided, takes precedence over the config-level enabled flag. */
|
|
9
|
+
comments?: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const { comments } = Astro.props;
|
|
13
|
+
const modules = resolvedModules;
|
|
14
|
+
const showComments = comments !== undefined ? comments : modules.comments.enabled;
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
{showComments && modules.comments.repo && (
|
|
18
|
+
<Comments
|
|
19
|
+
repo={modules.comments.repo}
|
|
20
|
+
repoId={modules.comments.repoId}
|
|
21
|
+
category={modules.comments.category}
|
|
22
|
+
categoryId={modules.comments.categoryId}
|
|
23
|
+
/>
|
|
24
|
+
)}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
---
|
|
2
|
+
// RegionRenderer.astro — renders a pre-resolved list of components for a layout region.
|
|
3
|
+
// Component order is fixed by the template below; the components array controls
|
|
4
|
+
// which ones are present (included vs excluded).
|
|
5
|
+
import SiteHeader from './regions/SiteHeader.astro';
|
|
6
|
+
import MainMenu from './regions/MainMenu.astro';
|
|
7
|
+
import SiteFooter from './regions/SiteFooter.astro';
|
|
8
|
+
import RecentPosts from './regions/RecentPosts.astro';
|
|
9
|
+
import Search from '../modules/search/Search.astro';
|
|
10
|
+
import type { RegionComponent } from '../types.js';
|
|
11
|
+
|
|
12
|
+
interface Props {
|
|
13
|
+
components: RegionComponent[];
|
|
14
|
+
searchEnabled: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const { components, searchEnabled } = Astro.props;
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
{components.includes('header') && <SiteHeader />}
|
|
21
|
+
{components.includes('main-menu') && <MainMenu />}
|
|
22
|
+
{components.includes('search') && searchEnabled && <Search />}
|
|
23
|
+
{components.includes('recent-posts') && <RecentPosts />}
|
|
24
|
+
{components.includes('footer') && <SiteFooter />}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
---
|
|
2
|
+
const pathname = Astro.url.pathname;
|
|
3
|
+
---
|
|
4
|
+
<nav>
|
|
5
|
+
<ul>
|
|
6
|
+
<li>
|
|
7
|
+
<a href="/blog" aria-current={pathname.startsWith('/blog') ? 'page' : undefined}>
|
|
8
|
+
Blog
|
|
9
|
+
</a>
|
|
10
|
+
</li>
|
|
11
|
+
<li>
|
|
12
|
+
<a href="/docs" aria-current={pathname.startsWith('/docs') ? 'page' : undefined}>
|
|
13
|
+
Docs
|
|
14
|
+
</a>
|
|
15
|
+
</li>
|
|
16
|
+
<li>
|
|
17
|
+
<a href="/tags" aria-current={pathname.startsWith('/tags') ? 'page' : undefined}>
|
|
18
|
+
Tags
|
|
19
|
+
</a>
|
|
20
|
+
</li>
|
|
21
|
+
</ul>
|
|
22
|
+
</nav>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { getCollection } from 'astro:content';
|
|
3
|
+
|
|
4
|
+
const posts = (await getCollection('blog', ({ data }) => data.publish === true))
|
|
5
|
+
.sort((a, b) => (b.data.date?.valueOf() ?? 0) - (a.data.date?.valueOf() ?? 0))
|
|
6
|
+
.slice(0, 5);
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
{posts.length > 0 && (
|
|
10
|
+
<div class="sidebar-section">
|
|
11
|
+
<h3 class="sidebar-heading">Recent</h3>
|
|
12
|
+
<ul class="sidebar-list">
|
|
13
|
+
{posts.map(post => (
|
|
14
|
+
<li>
|
|
15
|
+
<a href={`/blog/${post.id}`}>{post.data.title}</a>
|
|
16
|
+
{post.data.date && (
|
|
17
|
+
<span class="post-date">{post.data.date.toISOString().slice(0, 10)}</span>
|
|
18
|
+
)}
|
|
19
|
+
</li>
|
|
20
|
+
))}
|
|
21
|
+
</ul>
|
|
22
|
+
</div>
|
|
23
|
+
)}
|
package/src/consts.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Site constants — values come from the user's karaoke.config.ts via virtual module.
|
|
3
|
+
* This file is a thin re-export layer so pages can keep their existing import paths.
|
|
4
|
+
*/
|
|
5
|
+
import { siteTitle, siteDescription } from 'virtual:karaoke-cms/config';
|
|
6
|
+
|
|
7
|
+
export const SITE_TITLE = siteTitle;
|
|
8
|
+
export const SITE_DESCRIPTION = siteDescription;
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import type { AstroIntegration } from 'astro';
|
|
2
|
+
import { fileURLToPath } from 'url';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import { wikiLinkPlugin } from 'remark-wiki-link';
|
|
5
|
+
import sitemap from '@astrojs/sitemap';
|
|
6
|
+
import { getTheme, validateModules } from './validate-config.js';
|
|
7
|
+
import { resolveModules } from './utils/resolve-modules.js';
|
|
8
|
+
import { resolveLayout } from './utils/resolve-layout.js';
|
|
9
|
+
import type { KaraokeConfig, ResolvedModules, ResolvedLayout } from './types.js';
|
|
10
|
+
|
|
11
|
+
const __dirname = fileURLToPath(new URL('.', import.meta.url));
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* karaoke() — the main Astro integration for karaoke-cms.
|
|
15
|
+
*
|
|
16
|
+
* Injects all routes (/, /blog, /blog/[slug], /docs, /docs/[slug],
|
|
17
|
+
* /tags, /tags/[tag], /rss.xml, /404), sets up the @theme CSS alias,
|
|
18
|
+
* enables wikilinks, and provides a virtual module with your resolved config.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* // astro.config.mjs
|
|
22
|
+
* import { defineConfig } from 'astro/config';
|
|
23
|
+
* import karaoke from '@karaoke-cms/astro';
|
|
24
|
+
* import karaokeConfig from './karaoke.config.ts';
|
|
25
|
+
*
|
|
26
|
+
* export default defineConfig({
|
|
27
|
+
* site: 'https://your-site.pages.dev',
|
|
28
|
+
* integrations: [karaoke(karaokeConfig)],
|
|
29
|
+
* });
|
|
30
|
+
*
|
|
31
|
+
* Module slots (contract for Base.astro — extensible in v0.7+):
|
|
32
|
+
* 'nav' — rendered in the nav bar (e.g., Search)
|
|
33
|
+
* 'post-footer' — rendered after post content (e.g., Comments)
|
|
34
|
+
*/
|
|
35
|
+
export default function karaoke(config: KaraokeConfig = {}): AstroIntegration {
|
|
36
|
+
const themesDir = join(__dirname, 'themes');
|
|
37
|
+
const theme = getTheme(config, themesDir);
|
|
38
|
+
validateModules(config);
|
|
39
|
+
const resolved = resolveModules(config);
|
|
40
|
+
const layout = resolveLayout(config);
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
name: '@karaoke-cms/astro',
|
|
44
|
+
hooks: {
|
|
45
|
+
'astro:config:setup'({ injectRoute, updateConfig }) {
|
|
46
|
+
// ── Inject all framework routes ──────────────────────────────────
|
|
47
|
+
injectRoute({ pattern: '/', entrypoint: '@karaoke-cms/astro/pages/index.astro' });
|
|
48
|
+
injectRoute({ pattern: '/blog', entrypoint: '@karaoke-cms/astro/pages/blog/index.astro' });
|
|
49
|
+
injectRoute({ pattern: '/blog/[slug]', entrypoint: '@karaoke-cms/astro/pages/blog/[slug].astro' });
|
|
50
|
+
injectRoute({ pattern: '/docs', entrypoint: '@karaoke-cms/astro/pages/docs/index.astro' });
|
|
51
|
+
injectRoute({ pattern: '/docs/[slug]', entrypoint: '@karaoke-cms/astro/pages/docs/[slug].astro' });
|
|
52
|
+
injectRoute({ pattern: '/tags', entrypoint: '@karaoke-cms/astro/pages/tags/index.astro' });
|
|
53
|
+
injectRoute({ pattern: '/tags/[tag]', entrypoint: '@karaoke-cms/astro/pages/tags/[tag].astro' });
|
|
54
|
+
injectRoute({ pattern: '/404', entrypoint: '@karaoke-cms/astro/pages/404.astro' });
|
|
55
|
+
injectRoute({ pattern: '/rss.xml', entrypoint: '@karaoke-cms/astro/pages/rss.xml.ts' });
|
|
56
|
+
|
|
57
|
+
updateConfig({
|
|
58
|
+
integrations: [sitemap()],
|
|
59
|
+
vite: {
|
|
60
|
+
resolve: {
|
|
61
|
+
alias: {
|
|
62
|
+
// @theme → packages/astro/src/themes/<active-theme>/
|
|
63
|
+
'@theme': join(themesDir, theme),
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
plugins: [virtualConfigPlugin(config, resolved, layout)],
|
|
67
|
+
},
|
|
68
|
+
markdown: {
|
|
69
|
+
remarkPlugins: [
|
|
70
|
+
[wikiLinkPlugin, {
|
|
71
|
+
// [[blog/hello-world]] → /blog/hello-world/
|
|
72
|
+
// [[docs/getting-started]] → /docs/getting-started/
|
|
73
|
+
// [[blog/hello-world|Hello World]] → link text "Hello World"
|
|
74
|
+
pageResolver: (name: string) => [name.toLowerCase().replace(/ /g, '-')],
|
|
75
|
+
hrefTemplate: (permalink: string) => `/${permalink}/`,
|
|
76
|
+
aliasDivider: '|',
|
|
77
|
+
}],
|
|
78
|
+
],
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Vite plugin providing `virtual:karaoke-cms/config`.
|
|
88
|
+
* Base.astro and ModuleLoader.astro import from it at build time.
|
|
89
|
+
*/
|
|
90
|
+
function virtualConfigPlugin(config: KaraokeConfig, resolved: ResolvedModules, layout: ResolvedLayout) {
|
|
91
|
+
const VIRTUAL_ID = 'virtual:karaoke-cms/config';
|
|
92
|
+
const RESOLVED_ID = '\0' + VIRTUAL_ID;
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
name: 'vite-plugin-karaoke-cms-config',
|
|
96
|
+
resolveId(id: string) {
|
|
97
|
+
if (id === VIRTUAL_ID) return RESOLVED_ID;
|
|
98
|
+
},
|
|
99
|
+
load(id: string) {
|
|
100
|
+
if (id === RESOLVED_ID) {
|
|
101
|
+
return `
|
|
102
|
+
export const siteTitle = ${JSON.stringify(config.title ?? 'Karaoke')};
|
|
103
|
+
export const siteDescription = ${JSON.stringify(config.description ?? '')};
|
|
104
|
+
export const resolvedModules = ${JSON.stringify(resolved)};
|
|
105
|
+
export const resolvedLayout = ${JSON.stringify(layout)};
|
|
106
|
+
`;
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export type { KaraokeConfig, ResolvedModules, ResolvedLayout, RegionComponent } from './types.js';
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
---
|
|
2
|
+
import '@theme/styles.css';
|
|
3
|
+
import RegionRenderer from '../components/RegionRenderer.astro';
|
|
4
|
+
import { resolvedModules, resolvedLayout } from 'virtual:karaoke-cms/config';
|
|
5
|
+
import { SITE_TITLE } from '../consts';
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
title: string;
|
|
9
|
+
description?: string;
|
|
10
|
+
type?: 'website' | 'article';
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const { title, description, type = 'website' } = Astro.props;
|
|
14
|
+
const canonicalUrl = Astro.url.href;
|
|
15
|
+
const layout = resolvedLayout;
|
|
16
|
+
const modules = resolvedModules;
|
|
17
|
+
const searchEnabled = modules.search.enabled;
|
|
18
|
+
|
|
19
|
+
const hasLeft = Astro.slots.has('left') || layout.regions.left.components.length > 0;
|
|
20
|
+
const hasRight = Astro.slots.has('right') || layout.regions.right.components.length > 0;
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
<!doctype html>
|
|
24
|
+
<html lang="en">
|
|
25
|
+
<head>
|
|
26
|
+
<meta charset="utf-8" />
|
|
27
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
28
|
+
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
29
|
+
<link rel="icon" href="/favicon.ico" />
|
|
30
|
+
<meta name="generator" content={Astro.generator} />
|
|
31
|
+
<title>{title === SITE_TITLE ? title : `${title} — ${SITE_TITLE}`}</title>
|
|
32
|
+
{description && <meta name="description" content={description} />}
|
|
33
|
+
<meta property="og:title" content={title} />
|
|
34
|
+
<meta property="og:type" content={type} />
|
|
35
|
+
<meta property="og:url" content={canonicalUrl} />
|
|
36
|
+
{description && <meta property="og:description" content={description} />}
|
|
37
|
+
<meta name="twitter:card" content="summary" />
|
|
38
|
+
<link rel="alternate" type="application/rss+xml" title={SITE_TITLE} href="/rss.xml" />
|
|
39
|
+
</head>
|
|
40
|
+
<body>
|
|
41
|
+
<header>
|
|
42
|
+
<div class="header-inner">
|
|
43
|
+
<slot name="top">
|
|
44
|
+
<RegionRenderer components={layout.regions.top.components} {searchEnabled} />
|
|
45
|
+
</slot>
|
|
46
|
+
</div>
|
|
47
|
+
</header>
|
|
48
|
+
<div class:list={['page-body', { 'has-left': hasLeft, 'has-right': hasRight }]}>
|
|
49
|
+
{hasLeft && (
|
|
50
|
+
<aside class="region-left">
|
|
51
|
+
<slot name="left">
|
|
52
|
+
<RegionRenderer components={layout.regions.left.components} {searchEnabled} />
|
|
53
|
+
</slot>
|
|
54
|
+
</aside>
|
|
55
|
+
)}
|
|
56
|
+
<main>
|
|
57
|
+
<slot />
|
|
58
|
+
</main>
|
|
59
|
+
{hasRight && (
|
|
60
|
+
<aside class="region-right">
|
|
61
|
+
<slot name="right">
|
|
62
|
+
<RegionRenderer components={layout.regions.right.components} {searchEnabled} />
|
|
63
|
+
</slot>
|
|
64
|
+
</aside>
|
|
65
|
+
)}
|
|
66
|
+
</div>
|
|
67
|
+
<footer>
|
|
68
|
+
<slot name="bottom">
|
|
69
|
+
<RegionRenderer components={layout.regions.bottom.components} {searchEnabled} />
|
|
70
|
+
</slot>
|
|
71
|
+
</footer>
|
|
72
|
+
</body>
|
|
73
|
+
</html>
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
---
|
|
2
|
+
// Comments.astro — Giscus GitHub Discussions comments widget
|
|
3
|
+
//
|
|
4
|
+
// Props mirror the Giscus config fields in KaraokeConfig.
|
|
5
|
+
// Rendered only when comments.enabled is true and repo is set.
|
|
6
|
+
interface Props {
|
|
7
|
+
repo: string;
|
|
8
|
+
repoId: string;
|
|
9
|
+
category: string;
|
|
10
|
+
categoryId: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const { repo, repoId, category, categoryId } = Astro.props;
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
<div class="comments">
|
|
17
|
+
<script
|
|
18
|
+
define:vars={{ repo, repoId, category, categoryId }}
|
|
19
|
+
>
|
|
20
|
+
// define:vars injects repo/repoId/category/categoryId as const locals.
|
|
21
|
+
// We build the Giscus <script> tag dynamically so Astro doesn't try to
|
|
22
|
+
// process the external Giscus URL as an internal module.
|
|
23
|
+
// Store parent reference immediately — document.currentScript is nulled after async gaps.
|
|
24
|
+
const _parent = document.currentScript?.parentElement;
|
|
25
|
+
if (repo && repoId) {
|
|
26
|
+
const s = document.createElement('script');
|
|
27
|
+
s.src = 'https://giscus.app/client.js';
|
|
28
|
+
s.setAttribute('data-repo', repo);
|
|
29
|
+
s.setAttribute('data-repo-id', repoId);
|
|
30
|
+
s.setAttribute('data-category', category);
|
|
31
|
+
s.setAttribute('data-category-id', categoryId);
|
|
32
|
+
s.setAttribute('data-mapping', 'pathname');
|
|
33
|
+
s.setAttribute('data-strict', '0');
|
|
34
|
+
s.setAttribute('data-reactions-enabled', '1');
|
|
35
|
+
s.setAttribute('data-emit-metadata', '0');
|
|
36
|
+
s.setAttribute('data-input-position', 'bottom');
|
|
37
|
+
s.setAttribute('data-theme', 'preferred_color_scheme');
|
|
38
|
+
s.setAttribute('data-lang', 'en');
|
|
39
|
+
s.setAttribute('crossorigin', 'anonymous');
|
|
40
|
+
s.async = true;
|
|
41
|
+
_parent?.appendChild(s);
|
|
42
|
+
}
|
|
43
|
+
</script>
|
|
44
|
+
</div>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const id = 'comments';
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
---
|
|
2
|
+
// Search.astro — Pagefind full-text search UI
|
|
3
|
+
//
|
|
4
|
+
// Pagefind index is only built post-`astro build` (not in dev mode).
|
|
5
|
+
// We probe for the index file client-side and initialize only when present,
|
|
6
|
+
// so `npm run dev` silently shows nothing rather than a 404 cascade.
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
<div id="search"></div>
|
|
10
|
+
|
|
11
|
+
<script>
|
|
12
|
+
async function initSearch() {
|
|
13
|
+
// Pagefind won't exist in dev mode — probe before initialising
|
|
14
|
+
const probe = await fetch('/pagefind/pagefind-ui.js', { method: 'HEAD' }).catch(() => null);
|
|
15
|
+
if (!probe?.ok) return;
|
|
16
|
+
|
|
17
|
+
const link = document.createElement('link');
|
|
18
|
+
link.rel = 'stylesheet';
|
|
19
|
+
link.href = '/pagefind/pagefind-ui.css';
|
|
20
|
+
document.head.appendChild(link);
|
|
21
|
+
|
|
22
|
+
const script = document.createElement('script');
|
|
23
|
+
script.src = '/pagefind/pagefind-ui.js';
|
|
24
|
+
script.onload = () => {
|
|
25
|
+
// @ts-ignore — PagefindUI loaded by the external script above
|
|
26
|
+
new window.PagefindUI({ element: '#search', showSubResults: true });
|
|
27
|
+
};
|
|
28
|
+
document.head.appendChild(script);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
initSearch();
|
|
32
|
+
</script>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const id = 'search';
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
---
|
|
2
|
+
import Base from '../layouts/Base.astro';
|
|
3
|
+
import { SITE_TITLE } from '../consts';
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
<Base title={`Page not found — ${SITE_TITLE}`}>
|
|
7
|
+
<div class="post-header">
|
|
8
|
+
<h1>Page not found</h1>
|
|
9
|
+
<p class="post-meta">The page you're looking for doesn't exist or hasn't been published.</p>
|
|
10
|
+
</div>
|
|
11
|
+
<div class="prose">
|
|
12
|
+
<p><a href="/">Go home →</a></p>
|
|
13
|
+
</div>
|
|
14
|
+
</Base>
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { getCollection, render } from 'astro:content';
|
|
3
|
+
import Base from '../../layouts/Base.astro';
|
|
4
|
+
import ModuleLoader from '../../components/ModuleLoader.astro';
|
|
5
|
+
import { SITE_TITLE } from '../../consts';
|
|
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} — ${SITE_TITLE}`} 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>
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { getCollection } from 'astro:content';
|
|
3
|
+
import Base from '../../layouts/Base.astro';
|
|
4
|
+
import { SITE_TITLE } from '../../consts';
|
|
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 — ${SITE_TITLE}`}>
|
|
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 <code>content/blog/</code> and set <code>publish: true</code> in the frontmatter to make it appear here.</p>
|
|
29
|
+
</div>
|
|
30
|
+
)}
|
|
31
|
+
</Base>
|