meowpress 0.0.0-stage → 0.1.1-alpha.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Lidessen
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,55 @@
1
- # Temporary Holding Version
1
+ # Meowpress
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ `meowpress` 提供静态站点的内容读取、页面渲染、主题和构建流程。
4
+ 消费者持有站点配置与内容,通过本包构建;本包不持有应用的文章或输出目录。
5
+
6
+ 当前准备 `0.1.1-alpha.0` 本地候选,尚未发布该版本。CSS 与 JSX 均为必需 peer,允许 `^0.1.0 || ^0.1.1-alpha.0`,保留稳定版兼容范围,并接受本轮 JSX/CSS alpha。宿主选定共享的 CSS/JSX 模块,本包不私装另一份 CSS;不依赖 Meowtail 或 MeowSlides。实际包外构建与主题身份验收绑定精确 tgz,不通过 override 绕过 peer 合同。发布仍需独立明确授权。
7
+
8
+ 本包是独立内容发布库,源码位于 `packages/meowpress`,已有公开构建入口、独立测试与类型检查,
9
+ 不依赖 Assistant、WorkCell、Deno 或特定 Blog 的内容与运行状态。样式生成来自 `meowcss`,
10
+ HTML 渲染来自 `meowjsx/web`,无需安装 MeowUI。
11
+
12
+ ## 入口
13
+
14
+ - `meowpress/build`:`meowSite()` 定义配置,`buildSite()` 执行构建;这是唯一对外 export。
15
+ 内容读取、页面渲染、技能索引与主题是包内模块。
16
+ - `meowpress --config ./site.config.ts`:Bun CLI,加载消费者提供的配置并构建。
17
+
18
+ ```ts
19
+ import { meowSite } from "meowpress/build";
20
+
21
+ export default meowSite({
22
+ content: [{ directory: "/path/to/content" }],
23
+ skillsDirectory: "/path/to/workspace",
24
+ outputDirectory: "/path/to/dist",
25
+ });
26
+ ```
27
+
28
+ 配置中的目录由应用明确提供;默认主题从本包解析,不依赖启动目录。
29
+ 可通过 `theme: defineTheme(...)` 替换整套主题;所有静态页面输出该主题的根属性和默认选项。
30
+ 构建将 `theme.cssText` 写入临时 CSS,再由 Bun 处理字体与其他资源,最终通过 stylesheet 链接交付,
31
+ 无需客户端 JavaScript 即可应用主题。默认字体 Geist 的文件随站点打包,不依赖外部字体服务。
32
+ 使用默认字体时,输出同时包含 `licenses/geist-OFL.txt`,保留字体的 SIL OFL 许可。
33
+ 默认插画使用包内矢量与 CSS,不要求业务图片。可用 `site.images` 的 `hero`、`feed`、`about`
34
+ 显式设置装饰图路径,例如 `images/cover.svg`;路径相对内容根的 `assets/`,库负责各页面深度,
35
+ 资源沿现有资产构建流程交付。未设置 About 图片时使用站点名首字母。
36
+ 内容源包含 `posts/`、`thoughts/` 和可选资源;以下划线开头的草稿不进入发布输出。
37
+ 站点有自己的主题,不隐式安装 MeowUI 的应用主题。
38
+
39
+ ## 验证与构建
40
+
41
+ 安装 `meowpress` 及其 `meowcss`、`meowjsx` 依赖后,在消费者目录运行:
42
+
43
+ ```sh
44
+ bunx --no-install meowpress --config ./site.config.ts
45
+ ```
46
+
47
+ CLI 需要 Bun,输出位置由 `outputDirectory` 决定。字体文件从本包的正式 Geist 依赖解析,
48
+ 不要求消费者复制源码、安装工作区根依赖或访问外部字体服务。
49
+
50
+ ## 独立验证
51
+
52
+ 仓库依赖就绪后运行 `bun run --cwd packages/meowpress test` 和
53
+ `bun run --cwd packages/meowpress typecheck`。测试位于 `tests/unit/`;真实构建测试通过公开
54
+ `meowpress/build` 入口创建临时内容、主题和图片,
55
+ 覆盖 Markdown、MDX、Thought 输出及草稿页面/资产排除,不读取 Blog 内容或调用 Blog 构建。
@@ -0,0 +1,28 @@
1
+ import { type ThemeDefinition } from "meowcss";
2
+ import { type SiteIdentity } from "./identity.js";
3
+ import { type ContentSourceInput } from "./content.js";
4
+ export interface SiteConfig {
5
+ /** One or more roots containing `posts/`, `thoughts/`, and optional assets. */
6
+ content: ContentSourceInput | readonly ContentSourceInput[];
7
+ /** Workspace root used to discover public `skills/` and project `.agents/skills/`. */
8
+ skillsDirectory: string;
9
+ /** Static output directory for the generated site. */
10
+ outputDirectory: string;
11
+ /** Defaults to this package's own Blog theme. */
12
+ theme?: ThemeDefinition;
13
+ /** Optional project root when project Skills live outside `skillsDirectory`. */
14
+ projectRootDirectory?: string;
15
+ /** Site name, description, repository links and About copy; unset fields use neutral defaults. */
16
+ site?: Partial<SiteIdentity>;
17
+ }
18
+ export interface ResolvedSiteConfig {
19
+ content: readonly ContentSourceInput[];
20
+ skillsDirectory: string;
21
+ outputDirectory: string;
22
+ theme: ThemeDefinition;
23
+ projectRootDirectory?: string;
24
+ site: SiteIdentity;
25
+ }
26
+ /** Define a site config in the app package without taking on the build pipeline. */
27
+ export declare function meowSite(config: SiteConfig): ResolvedSiteConfig;
28
+ export declare function buildSite(input: SiteConfig | ResolvedSiteConfig): Promise<void>;
@@ -0,0 +1,114 @@
1
+ import { blogTheme } from "./theme.js";
2
+ import { DEFAULT_SITE } from "./identity.js";
3
+ import { mkdir, rm, writeFile, cp } from "node:fs/promises";
4
+ import { join, resolve } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import { collectContentAssets, isDraftAsset, loadSection, normalizeContentSources, } from "./content.js";
7
+ import { aboutPage, agentMarkdown, homePage, listPage, llmText, postPage, skillPage, skillsIndexPage, thoughtPage, } from "./site.js";
8
+ import { loadSkills } from "./skills.js";
9
+ /** Define a site config in the app package without taking on the build pipeline. */
10
+ export function meowSite(config) {
11
+ return {
12
+ content: normalizeContentSources(config.content),
13
+ skillsDirectory: resolve(config.skillsDirectory),
14
+ outputDirectory: resolve(config.outputDirectory),
15
+ theme: config.theme ?? blogTheme,
16
+ projectRootDirectory: config.projectRootDirectory
17
+ ? resolve(config.projectRootDirectory)
18
+ : undefined,
19
+ site: { ...DEFAULT_SITE, ...config.site },
20
+ };
21
+ }
22
+ async function writePage(generatedDirectory, pathname, html) {
23
+ const target = join(generatedDirectory, pathname);
24
+ await mkdir(resolve(target, ".."), { recursive: true });
25
+ await writeFile(target, html, "utf8");
26
+ return target;
27
+ }
28
+ async function stageAssets(generatedDirectory, sources) {
29
+ const assets = await collectContentAssets(sources, { directory: "assets" });
30
+ await Promise.all(assets
31
+ .filter((asset) => !isDraftAsset(asset.sourcePath, asset.assetRoot))
32
+ .map(async (asset) => {
33
+ const target = join(generatedDirectory, asset.relativePath);
34
+ await mkdir(resolve(target, ".."), { recursive: true });
35
+ await cp(asset.sourcePath, target, { force: true });
36
+ }));
37
+ }
38
+ async function copyPostAssets(outputDirectory, sources) {
39
+ const assets = await collectContentAssets(sources, { directory: "posts/assets" });
40
+ await Promise.all(assets
41
+ .filter((asset) => !isDraftAsset(asset.sourcePath, asset.assetRoot))
42
+ .map(async (asset) => {
43
+ const target = join(outputDirectory, "posts/assets", asset.relativePath);
44
+ await mkdir(resolve(target, ".."), { recursive: true });
45
+ await cp(asset.sourcePath, target, { force: true });
46
+ }));
47
+ }
48
+ async function buildStaticHtmlFromEntrypoints(entrypoints, generatedDirectory, outputDirectory) {
49
+ const result = await Bun.build({
50
+ entrypoints,
51
+ root: generatedDirectory,
52
+ outdir: outputDirectory,
53
+ target: "browser",
54
+ minify: true,
55
+ });
56
+ if (!result.success) {
57
+ console.error(result.logs);
58
+ throw new Error("Unable to build site static HTML");
59
+ }
60
+ }
61
+ async function writeContentPages(generatedDirectory, posts, thoughts, theme, site) {
62
+ const entrypoints = [];
63
+ for (const post of posts) {
64
+ entrypoints.push(await writePage(generatedDirectory, `posts/${post.slug}/index.html`, postPage(post, posts, thoughts, theme, site)));
65
+ }
66
+ for (const thought of thoughts) {
67
+ entrypoints.push(await writePage(generatedDirectory, `thoughts/${thought.slug}/index.html`, thoughtPage(thought, theme, site)));
68
+ }
69
+ return entrypoints;
70
+ }
71
+ export async function buildSite(input) {
72
+ const config = meowSite(input);
73
+ const generatedDirectory = resolve(config.outputDirectory, "../.generated");
74
+ const [posts, thoughts, skills] = await Promise.all([
75
+ loadSection(config.content, "post"),
76
+ loadSection(config.content, "thought"),
77
+ loadSkills(config.skillsDirectory, {
78
+ projectRootDirectory: config.projectRootDirectory,
79
+ }),
80
+ ]);
81
+ const timeline = [...posts, ...thoughts].sort((left, right) => (right.date?.getTime() ?? 0) - (left.date?.getTime() ?? 0));
82
+ await rm(config.outputDirectory, { recursive: true, force: true });
83
+ await rm(generatedDirectory, { recursive: true, force: true });
84
+ await mkdir(config.outputDirectory, { recursive: true });
85
+ await mkdir(generatedDirectory, { recursive: true });
86
+ const entrypoints = await Promise.all([
87
+ writePage(generatedDirectory, "index.html", homePage(timeline, config.theme, config.site)),
88
+ writePage(generatedDirectory, "about/index.html", aboutPage(config.theme, config.site)),
89
+ writePage(generatedDirectory, "posts/index.html", listPage("post", posts, config.theme, config.site)),
90
+ writePage(generatedDirectory, "thoughts/index.html", listPage("thought", thoughts, config.theme, config.site)),
91
+ writePage(generatedDirectory, "skills/index.html", skillsIndexPage(skills, config.theme, config.site)),
92
+ ]);
93
+ entrypoints.push(...(await writeContentPages(generatedDirectory, posts, thoughts, config.theme, config.site)));
94
+ for (const skill of skills) {
95
+ const skillPath = skill.scope === "public" ? `skills/${skill.slug}` : `skills/project/${skill.slug}`;
96
+ entrypoints.push(await writePage(generatedDirectory, `${skillPath}/index.html`, skillPage(skill, config.theme, config.site)));
97
+ }
98
+ await writeFile(join(generatedDirectory, "blog-theme.css"), config.theme.cssText.replaceAll('"@fontsource-variable/geist"', JSON.stringify(fileURLToPath(import.meta.resolve("@fontsource-variable/geist")))), "utf8");
99
+ await stageAssets(generatedDirectory, config.content);
100
+ await buildStaticHtmlFromEntrypoints(entrypoints, generatedDirectory, config.outputDirectory);
101
+ if (config.theme.cssText.includes('"@fontsource-variable/geist"')) {
102
+ const licensesDirectory = join(config.outputDirectory, "licenses");
103
+ await mkdir(licensesDirectory, { recursive: true });
104
+ await cp(fileURLToPath(import.meta.resolve("@fontsource-variable/geist/LICENSE")), join(licensesDirectory, "geist-OFL.txt"));
105
+ }
106
+ await copyPostAssets(config.outputDirectory, config.content);
107
+ await Promise.all([
108
+ writeFile(join(config.outputDirectory, "llm.txt"), llmText(posts, thoughts, config.site), "utf8"),
109
+ writeFile(join(config.outputDirectory, "llms.txt"), llmText(posts, thoughts, config.site), "utf8"),
110
+ writeFile(join(config.outputDirectory, "agents.md"), agentMarkdown(posts, thoughts, config.site), "utf8"),
111
+ ]);
112
+ await rm(generatedDirectory, { recursive: true, force: true });
113
+ console.log(`Built ${posts.length} posts and ${thoughts.length} thoughts into site dist/.`);
114
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env bun
2
+ export declare function runSiteCli(args?: string[]): Promise<void>;
@@ -0,0 +1,22 @@
1
+ #!/usr/bin/env bun
2
+ import { resolve } from "node:path";
3
+ import { pathToFileURL } from "node:url";
4
+ import { buildSite } from "./build.js";
5
+ function configPathFromArgs(args) {
6
+ const index = args.indexOf("--config");
7
+ const configPath = index >= 0 ? args[index + 1] : undefined;
8
+ if (!configPath || configPath.startsWith("--")) {
9
+ throw new Error("Usage: meowpress --config <site-config.ts>");
10
+ }
11
+ return resolve(process.cwd(), configPath);
12
+ }
13
+ export async function runSiteCli(args = Bun.argv.slice(2)) {
14
+ const configPath = configPathFromArgs(args);
15
+ const module = (await import(pathToFileURL(configPath).href));
16
+ const config = module.default ?? module.site;
17
+ if (!config)
18
+ throw new Error(`No default site config exported from ${configPath}`);
19
+ await buildSite(config);
20
+ }
21
+ if (import.meta.main)
22
+ await runSiteCli();
@@ -0,0 +1,50 @@
1
+ export type ContentKind = "post" | "thought";
2
+ export type Frontmatter = Record<string, unknown>;
3
+ /** A content root containing `posts/`, `thoughts/`, and optional asset folders. */
4
+ export interface ContentSource {
5
+ directory: string;
6
+ /** Namespace slugs and asset paths from this source to avoid collisions. */
7
+ slugPrefix?: string;
8
+ }
9
+ export type ContentSourceInput = string | ContentSource;
10
+ export type DuplicateContentPolicy = "first-wins" | "error";
11
+ export interface LoadSectionOptions {
12
+ /** The first source wins by default, so the source order is an explicit overlay order. */
13
+ duplicate?: DuplicateContentPolicy;
14
+ }
15
+ export interface ContentItem {
16
+ kind: ContentKind;
17
+ slug: string;
18
+ sourcePath: string;
19
+ title: string;
20
+ excerpt: string;
21
+ body: string;
22
+ html: string;
23
+ metadata: Frontmatter;
24
+ date?: Date;
25
+ dateKey: string;
26
+ timeLabel: string;
27
+ tags: string[];
28
+ readingMinutes: number;
29
+ characterCount: number;
30
+ }
31
+ export interface ParsedDocument {
32
+ metadata: Frontmatter;
33
+ body: string;
34
+ }
35
+ export interface ContentAsset {
36
+ sourcePath: string;
37
+ assetRoot: string;
38
+ relativePath: string;
39
+ }
40
+ export interface CollectContentAssetsOptions {
41
+ /** Relative directory below every source, for example `assets` or `posts/assets`. */
42
+ directory?: string;
43
+ duplicate?: DuplicateContentPolicy;
44
+ }
45
+ export declare function normalizeContentSources(input: ContentSourceInput | readonly ContentSourceInput[]): ContentSource[];
46
+ export declare function loadSection(sources: ContentSourceInput | readonly ContentSourceInput[], kind: ContentKind, options?: LoadSectionOptions): Promise<ContentItem[]>;
47
+ export declare function withoutLeadingHeading(html: string): string;
48
+ export declare function collectAssetFiles(directory: string): Promise<string[]>;
49
+ export declare function collectContentAssets(sources: ContentSourceInput | readonly ContentSourceInput[], options?: CollectContentAssetsOptions): Promise<ContentAsset[]>;
50
+ export declare function isDraftAsset(assetPath: string, assetRoot: string): boolean;
@@ -0,0 +1,312 @@
1
+ import { compile, run } from "@mdx-js/mdx";
2
+ import remarkGfm from "remark-gfm";
3
+ import { readdir, readFile, stat } from "node:fs/promises";
4
+ import { relative, resolve, sep } from "node:path";
5
+ import { parse as parseYaml } from "yaml";
6
+ import { Fragment, h } from "meowjsx/runtime";
7
+ import { renderToHtml } from "meowjsx/web";
8
+ const CONTENT_EXTENSIONS = new Set([".md", ".mdx"]);
9
+ function isRecord(value) {
10
+ return typeof value === "object" && value !== null && !Array.isArray(value);
11
+ }
12
+ function asText(value) {
13
+ if (value === null || value === undefined)
14
+ return undefined;
15
+ if (value instanceof Date)
16
+ return value.toISOString();
17
+ if (typeof value === "string")
18
+ return value;
19
+ if (typeof value === "number" || typeof value === "boolean") {
20
+ return String(value);
21
+ }
22
+ return undefined;
23
+ }
24
+ function parseFrontmatter(source) {
25
+ const match = source.match(/^---\s*\r?\n([\s\S]*?)\r?\n---\s*(?:\r?\n|$)/);
26
+ if (!match)
27
+ return { metadata: {}, body: source };
28
+ const parsed = parseYaml(match[1]);
29
+ const metadata = isRecord(parsed) ? parsed : {};
30
+ return { metadata, body: source.slice(match[0].length) };
31
+ }
32
+ function metadataValue(metadata, keys) {
33
+ return keys.map((key) => metadata[key]).find((value) => value !== undefined);
34
+ }
35
+ function parseDate(value) {
36
+ const text = asText(value);
37
+ if (!text)
38
+ return undefined;
39
+ const date = new Date(text);
40
+ return Number.isNaN(date.getTime()) ? undefined : date;
41
+ }
42
+ function makeDateKey(value, date) {
43
+ const text = asText(value);
44
+ const explicitDate = text?.match(/^\d{4}-\d{2}-\d{2}/)?.[0];
45
+ if (explicitDate)
46
+ return explicitDate;
47
+ if (!date)
48
+ return "undated";
49
+ return new Intl.DateTimeFormat("en-CA", {
50
+ timeZone: "Asia/Shanghai",
51
+ year: "numeric",
52
+ month: "2-digit",
53
+ day: "2-digit",
54
+ }).format(date);
55
+ }
56
+ function makeTimeLabel(value, date) {
57
+ const text = asText(value);
58
+ if (text) {
59
+ return text.match(/(?:T|\s)(\d{2}:\d{2})/)?.[1] ?? "";
60
+ }
61
+ if (!date)
62
+ return "";
63
+ return new Intl.DateTimeFormat("zh-CN", {
64
+ timeZone: "Asia/Shanghai",
65
+ hour: "2-digit",
66
+ minute: "2-digit",
67
+ hour12: false,
68
+ }).format(date);
69
+ }
70
+ function titleFromBody(body, slug) {
71
+ const heading = body.match(/^#\s+(.+)$/m)?.[1]?.trim();
72
+ if (heading)
73
+ return heading.replaceAll(/[*_`]/g, "");
74
+ return slug
75
+ .split("/")
76
+ .at(-1)
77
+ .replaceAll(/[-_]+/g, " ")
78
+ .replace(/\b\w/g, (letter) => letter.toUpperCase());
79
+ }
80
+ function stripMarkdown(source) {
81
+ return source
82
+ .replaceAll(/<!--[\s\S]*?-->/g, "")
83
+ .replaceAll(/^#{1,6}\s+/gm, "")
84
+ .replaceAll(/!\[([^\]]*)\]\([^)]*\)/g, "$1")
85
+ .replaceAll(/\[([^\]]+)\]\([^)]*\)/g, "$1")
86
+ .replaceAll(/[`*_~]/g, "")
87
+ .replaceAll(/^>\s?/gm, "")
88
+ .replaceAll(/^[-*+]\s+/gm, "")
89
+ .replaceAll(/^\d+\.\s+/gm, "")
90
+ .replaceAll(/\s+/g, " ")
91
+ .trim();
92
+ }
93
+ function getTags(metadata) {
94
+ const value = metadata.tags;
95
+ if (Array.isArray(value))
96
+ return value.map(asText).filter(Boolean);
97
+ if (typeof value === "string") {
98
+ return value
99
+ .split(",")
100
+ .map((tag) => tag.trim())
101
+ .filter(Boolean);
102
+ }
103
+ return [];
104
+ }
105
+ function getReadingMinutes(text) {
106
+ return Math.max(1, Math.ceil(text.length / 400));
107
+ }
108
+ function Callout(props) {
109
+ const tone = asText(props.tone) ?? "note";
110
+ return h("aside", { className: `mdx-callout mdx-callout-${tone}` }, props.children);
111
+ }
112
+ const MDX_COMPONENTS = {
113
+ Callout,
114
+ };
115
+ async function renderMdx(source) {
116
+ const compiled = await compile({ value: source }, {
117
+ outputFormat: "function-body",
118
+ remarkPlugins: [remarkGfm],
119
+ });
120
+ const module = await run(String(compiled), {
121
+ Fragment,
122
+ jsx: h,
123
+ jsxs: h,
124
+ });
125
+ const component = module.default;
126
+ return renderToHtml(component({ components: MDX_COMPONENTS }));
127
+ }
128
+ async function renderSource(source, sourcePath) {
129
+ if (sourcePath.endsWith(".md")) {
130
+ return Bun.markdown.html(source, {
131
+ tagFilter: true,
132
+ });
133
+ }
134
+ return renderMdx(source);
135
+ }
136
+ async function collectFiles(directory) {
137
+ const entries = await readdir(directory, { withFileTypes: true });
138
+ const files = [];
139
+ for (const entry of entries) {
140
+ if (entry.name.startsWith("."))
141
+ continue;
142
+ const entryPath = resolve(directory, entry.name);
143
+ if (entry.isDirectory()) {
144
+ files.push(...(await collectFiles(entryPath)));
145
+ continue;
146
+ }
147
+ if (CONTENT_EXTENSIONS.has(entry.name.slice(entry.name.lastIndexOf(".")))) {
148
+ files.push(entryPath);
149
+ }
150
+ }
151
+ return files.sort((left, right) => left.localeCompare(right));
152
+ }
153
+ function isDraft(sourcePath, sectionDirectory, kind) {
154
+ if (kind !== "post")
155
+ return false;
156
+ const contentPath = relative(sectionDirectory, sourcePath);
157
+ return contentPath.split(sep).some((part) => part.startsWith("_"));
158
+ }
159
+ function makeSlug(sourcePath, sectionDirectory) {
160
+ const contentPath = relative(sectionDirectory, sourcePath);
161
+ return contentPath
162
+ .replace(/\.(md|mdx)$/i, "")
163
+ .split(sep)
164
+ .join("/");
165
+ }
166
+ function normalizeSlugPrefix(prefix) {
167
+ return (prefix ?? "")
168
+ .replaceAll("\\", "/")
169
+ .replace(/^\/+|\/+$/g, "")
170
+ .replaceAll(/\/+/g, "/");
171
+ }
172
+ function withSlugPrefix(prefix, slug) {
173
+ const normalizedPrefix = normalizeSlugPrefix(prefix);
174
+ return normalizedPrefix ? `${normalizedPrefix}/${slug}` : slug;
175
+ }
176
+ export function normalizeContentSources(input) {
177
+ const sources = Array.isArray(input) ? input : [input];
178
+ return sources.map((source) => {
179
+ const normalized = typeof source === "string" ? { directory: source } : source;
180
+ return {
181
+ directory: resolve(normalized.directory),
182
+ slugPrefix: normalizeSlugPrefix(normalized.slugPrefix) || undefined,
183
+ };
184
+ });
185
+ }
186
+ function duplicateContentError(kind, slug) {
187
+ return new Error(`Duplicate ${kind} slug "${slug}" while merging content sources.`);
188
+ }
189
+ async function loadSectionFromSource(source, kind) {
190
+ const sectionDirectory = resolve(source.directory, `${kind}s`);
191
+ let sourceFiles;
192
+ try {
193
+ sourceFiles = await collectFiles(sectionDirectory);
194
+ }
195
+ catch (error) {
196
+ if (error.code === "ENOENT")
197
+ return [];
198
+ throw error;
199
+ }
200
+ const items = [];
201
+ for (const sourcePath of sourceFiles) {
202
+ if (isDraft(sourcePath, sectionDirectory, kind))
203
+ continue;
204
+ const sourceText = await readFile(sourcePath, "utf8");
205
+ const document = parseFrontmatter(sourceText);
206
+ const slug = withSlugPrefix(source.slugPrefix, makeSlug(sourcePath, sectionDirectory));
207
+ const dateValue = metadataValue(document.metadata, [
208
+ "published_at",
209
+ "publishedAt",
210
+ "date",
211
+ "created_at",
212
+ "createdAt",
213
+ "updated_at",
214
+ "updatedAt",
215
+ ]);
216
+ const date = parseDate(dateValue) ?? (await stat(sourcePath)).mtime;
217
+ const bodyWithoutLeadingHeading = document.body.replace(/^\s*#\s+.+?(?:\r?\n|$)+/, "");
218
+ const plainText = stripMarkdown(bodyWithoutLeadingHeading);
219
+ const title = asText(document.metadata.title) ?? titleFromBody(document.body, slug);
220
+ const html = withoutLeadingHeading(await renderSource(document.body, sourcePath));
221
+ items.push({
222
+ kind,
223
+ slug,
224
+ sourcePath,
225
+ title,
226
+ excerpt: plainText.slice(0, 180),
227
+ body: document.body,
228
+ html,
229
+ metadata: document.metadata,
230
+ date,
231
+ dateKey: makeDateKey(dateValue, date),
232
+ timeLabel: makeTimeLabel(dateValue, date),
233
+ tags: getTags(document.metadata),
234
+ readingMinutes: getReadingMinutes(plainText),
235
+ characterCount: plainText.length,
236
+ });
237
+ }
238
+ return items;
239
+ }
240
+ export async function loadSection(sources, kind, options = {}) {
241
+ const merged = new Map();
242
+ const duplicatePolicy = options.duplicate ?? "first-wins";
243
+ for (const source of normalizeContentSources(sources)) {
244
+ const items = await loadSectionFromSource(source, kind);
245
+ for (const item of items) {
246
+ if (merged.has(item.slug)) {
247
+ if (duplicatePolicy === "error")
248
+ throw duplicateContentError(kind, item.slug);
249
+ continue;
250
+ }
251
+ merged.set(item.slug, item);
252
+ }
253
+ }
254
+ return [...merged.values()].sort((left, right) => {
255
+ return (right.date?.getTime() ?? 0) - (left.date?.getTime() ?? 0);
256
+ });
257
+ }
258
+ export function withoutLeadingHeading(html) {
259
+ return html.replace(/^\s*<h1>.*?<\/h1>\s*/s, "");
260
+ }
261
+ export async function collectAssetFiles(directory) {
262
+ try {
263
+ return await collectFilesForAssets(directory);
264
+ }
265
+ catch (error) {
266
+ if (error.code === "ENOENT")
267
+ return [];
268
+ throw error;
269
+ }
270
+ }
271
+ async function collectFilesForAssets(directory) {
272
+ const entries = await readdir(directory, { withFileTypes: true });
273
+ const files = [];
274
+ for (const entry of entries) {
275
+ if (entry.name.startsWith("."))
276
+ continue;
277
+ const entryPath = resolve(directory, entry.name);
278
+ if (entry.isDirectory()) {
279
+ files.push(...(await collectFilesForAssets(entryPath)));
280
+ }
281
+ else {
282
+ files.push(entryPath);
283
+ }
284
+ }
285
+ return files.sort((left, right) => left.localeCompare(right));
286
+ }
287
+ export async function collectContentAssets(sources, options = {}) {
288
+ const directory = options.directory ?? "assets";
289
+ const duplicatePolicy = options.duplicate ?? "first-wins";
290
+ const assets = new Map();
291
+ for (const source of normalizeContentSources(sources)) {
292
+ const assetRoot = resolve(source.directory, directory);
293
+ const sourceFiles = await collectAssetFiles(assetRoot);
294
+ for (const sourcePath of sourceFiles) {
295
+ const localPath = relative(assetRoot, sourcePath).split(sep).join("/");
296
+ const relativePath = withSlugPrefix(source.slugPrefix, localPath);
297
+ if (assets.has(relativePath)) {
298
+ if (duplicatePolicy === "error") {
299
+ throw new Error(`Duplicate asset path "${relativePath}" while merging content sources.`);
300
+ }
301
+ continue;
302
+ }
303
+ assets.set(relativePath, { sourcePath, assetRoot, relativePath });
304
+ }
305
+ }
306
+ return [...assets.values()];
307
+ }
308
+ export function isDraftAsset(assetPath, assetRoot) {
309
+ return relative(assetRoot, assetPath)
310
+ .split(sep)
311
+ .some((part) => part.startsWith("_"));
312
+ }
@@ -0,0 +1,38 @@
1
+ /** A profile link shown on the About page (e.g. a GitHub account). */
2
+ export interface SiteProfileLink {
3
+ /** Service label, e.g. `GitHub`. */
4
+ label: string;
5
+ /** Visible handle, e.g. `@name`. */
6
+ handle: string;
7
+ href: string;
8
+ }
9
+ export interface SiteAbout {
10
+ /** Page heading. */
11
+ heading: string;
12
+ paragraphs: readonly string[];
13
+ /** Heading above the profile links. */
14
+ linksHeading?: string;
15
+ links?: readonly SiteProfileLink[];
16
+ }
17
+ /** Site-owned identity and copy. The application supplies it; the library keeps neutral defaults. */
18
+ export interface SiteIdentity {
19
+ /** Display name used in header, footer and document titles. */
20
+ name: string;
21
+ /** Stable `data-site` value. */
22
+ id: string;
23
+ description: string;
24
+ /** `npx skills add <package>` source for public Skills; when absent the source path is shown. */
25
+ skillsPackage?: string;
26
+ /** Repository tree URL that Skill source paths resolve against; when absent no source link is shown. */
27
+ repositorySourceUrl?: string;
28
+ /** Content directory described in the agent-facing index, e.g. `apps/blog/content`. */
29
+ contentDirectory: string;
30
+ /** Optional artwork paths relative to the content assets root, e.g. `images/cover.png`. */
31
+ images?: {
32
+ hero?: string;
33
+ feed?: string;
34
+ about?: string;
35
+ };
36
+ about?: SiteAbout;
37
+ }
38
+ export declare const DEFAULT_SITE: SiteIdentity;
@@ -0,0 +1,6 @@
1
+ export const DEFAULT_SITE = {
2
+ name: "Meowpress site",
3
+ id: "meowpress",
4
+ description: "长文与碎片想法的混排时间线。",
5
+ contentDirectory: "content",
6
+ };
@@ -0,0 +1,6 @@
1
+ export { collectContentAssets, collectAssetFiles, isDraftAsset, loadSection, normalizeContentSources, type ContentItem, type ContentAsset, type ContentKind, type ContentSource, type ContentSourceInput, type DuplicateContentPolicy, type LoadSectionOptions, } from "./content.js";
2
+ export { buildSite, meowSite, type ResolvedSiteConfig, type SiteConfig } from "./build.js";
3
+ export { aboutPage, agentMarkdown, homePage, listPage, llmText, postPage, skillPage, skillsIndexPage, thoughtPage, } from "./site.js";
4
+ export { DEFAULT_SITE, type SiteAbout, type SiteIdentity, type SiteProfileLink } from "./identity.js";
5
+ export { blogTheme, blogTokens, blogOptions } from "./theme.js";
6
+ export { loadSkills, type LoadSkillsOptions, type Skill, type SkillFile, type SkillScope, } from "./skills.js";
@@ -0,0 +1,6 @@
1
+ export { collectContentAssets, collectAssetFiles, isDraftAsset, loadSection, normalizeContentSources, } from "./content.js";
2
+ export { buildSite, meowSite } from "./build.js";
3
+ export { aboutPage, agentMarkdown, homePage, listPage, llmText, postPage, skillPage, skillsIndexPage, thoughtPage, } from "./site.js";
4
+ export { DEFAULT_SITE } from "./identity.js";
5
+ export { blogTheme, blogTokens, blogOptions } from "./theme.js";
6
+ export { loadSkills, } from "./skills.js";