meowpress 0.1.1-alpha.0 → 0.2.0-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/README.md CHANGED
@@ -1,19 +1,19 @@
1
1
  # Meowpress
2
2
 
3
- `meowpress` 提供静态站点的内容读取、页面渲染、主题和构建流程。
4
- 消费者持有站点配置与内容,通过本包构建;本包不持有应用的文章或输出目录。
3
+ `meowpress` provides content reading, page rendering, themes and the build pipeline for static sites.
4
+ Consumers own the site configuration and content and build through this package; the package does not own the application's posts or output directory.
5
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 合同。发布仍需独立明确授权。
6
+ Published history: `0.1.1-alpha.0` was accepted from the registry, followed by `0.1.1-alpha.1` with the English site and CLI defaults; `latest` remained `0.1.0`. The current workspace package is `0.2.0-alpha.0`, and its required CSS and JSX peer ranges accept `^0.2.0-alpha.0`. The host selects the shared CSS/JSX modules, and this package does not privately install another CSS copy; it does not depend on Meowtail or MeowSlides. Out-of-package builds and theme identity acceptance are bound to exact package archives, and peer contracts are not bypassed through overrides. Publishing requires separate explicit authorization.
7
7
 
8
- 本包是独立内容发布库,源码位于 `packages/meowpress`,已有公开构建入口、独立测试与类型检查,
9
- 不依赖 Assistant、WorkCell、Deno 或特定 Blog 的内容与运行状态。样式生成来自 `meowcss`,
10
- HTML 渲染来自 `meowjsx/web`,无需安装 MeowUI。
8
+ This package is a standalone content publishing library with source in `packages/meowpress`; it already has a public build entry, independent tests and typechecking,
9
+ and does not depend on Assistant, WorkCell, Deno or the content and runtime state of a specific Blog. Style generation comes from `meowcss`,
10
+ and HTML rendering comes from `meowjsx/web`; MeowUI does not need to be installed.
11
11
 
12
- ## 入口
12
+ ## Entries
13
13
 
14
- - `meowpress/build`:`meowSite()` 定义配置,`buildSite()` 执行构建;这是唯一对外 export。
15
- 内容读取、页面渲染、技能索引与主题是包内模块。
16
- - `meowpress --config ./site.config.ts`:Bun CLI,加载消费者提供的配置并构建。
14
+ - `meowpress/build`: `meowSite()` defines configuration and `buildSite()` performs the build; this is the only public export.
15
+ Content reading, page rendering, skill indexing and themes are internal package modules.
16
+ - `meowpress --config ./site.config.ts`: Bun CLI that loads the consumer-provided configuration and builds.
17
17
 
18
18
  ```ts
19
19
  import { meowSite } from "meowpress/build";
@@ -25,31 +25,62 @@ export default meowSite({
25
25
  });
26
26
  ```
27
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 的应用主题。
28
+ ## Pages a consumer owns
38
29
 
39
- ## 验证与构建
30
+ A consumer that supplies `pages` owns the whole site map. The provider receives the content that was
31
+ already loaded and returns complete documents; the package writes them at their paths, adds them to
32
+ the agent-facing indexes, and does not publish its own blog routes next to them. One site map, one
33
+ owner.
40
34
 
41
- 安装 `meowpress` 及其 `meowcss`、`meowjsx` 依赖后,在消费者目录运行:
35
+ ```ts
36
+ import { meowSite, type SitePage } from "meowpress/build";
37
+
38
+ export default meowSite({
39
+ content: [{ directory: "/path/to/content" }],
40
+ outputDirectory: "/path/to/dist",
41
+ themeStylesheet: "site-theme.css",
42
+ pages: ({ posts, theme }): SitePage[] =>
43
+ posts.map((post) => ({
44
+ path: post.slug,
45
+ title: post.title,
46
+ html: renderDocument(post, theme),
47
+ })),
48
+ });
49
+ ```
50
+
51
+ - `pages` is a provider of complete HTML documents. A path that resolves above the site root is
52
+ refused rather than clamped.
53
+ - `mdxComponents` adds named components to the MDX content pages; the built-in `Callout` stays
54
+ available.
55
+ - `themeStylesheet` names the theme stylesheet the build writes, so a consumer page can link it. It
56
+ applies to the built-in pages as well, so a renamed stylesheet never leaves a page linking a file
57
+ that was not written.
58
+
59
+ Directories in the configuration are provided explicitly by the application; the default theme resolves from this package and does not depend on the startup directory.
60
+ The whole theme can be replaced with `theme: defineTheme(...)`; all static pages output that theme's root attributes and default options.
61
+ The build writes `theme.cssText` to a temporary CSS file, then lets Bun handle fonts and other assets, and finally delivers through a stylesheet link,
62
+ so the theme applies without client-side JavaScript. Files for the default Geist font are bundled with the site and do not depend on an external font service.
63
+ When the default font is used, the output also includes `licenses/geist-OFL.txt`, preserving the font's SIL OFL license.
64
+ Default illustrations use in-package vectors and CSS and require no business images. The `hero`, `feed` and `about` keys of `site.images`
65
+ can explicitly set decorative image paths, for example `images/cover.svg`; paths are relative to the content root's `assets/`, the library handles each page depth,
66
+ and assets are delivered along the existing asset build flow. When no About image is set, the first letter of the site name is used.
67
+ The content source contains `posts/`, `thoughts/` and optional assets; drafts whose names start with an underscore do not enter the published output.
68
+ The site has its own theme and does not implicitly install MeowUI's application theme.
69
+
70
+ ## Verification and build
71
+
72
+ After installing `meowpress` and its `meowcss` and `meowjsx` dependencies, run in the consumer directory:
42
73
 
43
74
  ```sh
44
75
  bunx --no-install meowpress --config ./site.config.ts
45
76
  ```
46
77
 
47
- CLI 需要 Bun,输出位置由 `outputDirectory` 决定。字体文件从本包的正式 Geist 依赖解析,
48
- 不要求消费者复制源码、安装工作区根依赖或访问外部字体服务。
78
+ The CLI requires Bun, and the output location is determined by `outputDirectory`. Font files resolve from this package's official Geist dependency,
79
+ and consumers are not required to copy source, install workspace-root dependencies or access an external font service.
49
80
 
50
- ## 独立验证
81
+ ## Independent verification
51
82
 
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 构建。
83
+ Once repository dependencies are ready, run `bun run --cwd packages/meowpress test` and
84
+ `bun run --cwd packages/meowpress typecheck`. Tests live in `tests/unit/`; real build tests create temporary content, themes and images through the public
85
+ `meowpress/build` entry,
86
+ covering Markdown, MDX, Thought output and draft page/asset exclusion, without reading Blog content or invoking the Blog build.
@@ -1,6 +1,10 @@
1
1
  import { type ThemeDefinition } from "meowcss";
2
2
  import { type SiteIdentity } from "./identity.js";
3
- import { type ContentSourceInput } from "./content.js";
3
+ import { type ContentSourceInput, type MdxComponent } from "./content.js";
4
+ import { type SitePageProvider } from "./pages.js";
5
+ export type { SitePage, SitePageContext, SitePageProvider } from "./pages.js";
6
+ export type { ContentItem } from "./content.js";
7
+ export type { SiteIdentity } from "./identity.js";
4
8
  export interface SiteConfig {
5
9
  /** One or more roots containing `posts/`, `thoughts/`, and optional assets. */
6
10
  content: ContentSourceInput | readonly ContentSourceInput[];
@@ -14,6 +18,12 @@ export interface SiteConfig {
14
18
  projectRootDirectory?: string;
15
19
  /** Site name, description, repository links and About copy; unset fields use neutral defaults. */
16
20
  site?: Partial<SiteIdentity>;
21
+ /** Consumer-supplied pages; the site writes and indexes them next to its own. */
22
+ pages?: SitePageProvider;
23
+ /** Extra components available to MDX content pages. */
24
+ mdxComponents?: Record<string, MdxComponent>;
25
+ /** Stylesheet file name for the theme; defaults to "blog-theme.css". */
26
+ themeStylesheet?: string;
17
27
  }
18
28
  export interface ResolvedSiteConfig {
19
29
  content: readonly ContentSourceInput[];
@@ -22,6 +32,9 @@ export interface ResolvedSiteConfig {
22
32
  theme: ThemeDefinition;
23
33
  projectRootDirectory?: string;
24
34
  site: SiteIdentity;
35
+ pages?: SitePageProvider;
36
+ mdxComponents: Record<string, MdxComponent>;
37
+ themeStylesheet: string;
25
38
  }
26
39
  /** Define a site config in the app package without taking on the build pipeline. */
27
40
  export declare function meowSite(config: SiteConfig): ResolvedSiteConfig;
package/dist/src/build.js CHANGED
@@ -1,11 +1,12 @@
1
1
  import { blogTheme } from "./theme.js";
2
2
  import { DEFAULT_SITE } from "./identity.js";
3
3
  import { mkdir, rm, writeFile, cp } from "node:fs/promises";
4
- import { join, resolve } from "node:path";
4
+ import { isAbsolute, join, relative, resolve } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
6
  import { collectContentAssets, isDraftAsset, loadSection, normalizeContentSources, } from "./content.js";
7
- import { aboutPage, agentMarkdown, homePage, listPage, llmText, postPage, skillPage, skillsIndexPage, thoughtPage, } from "./site.js";
7
+ import { aboutPage, agentMarkdown, DEFAULT_THEME_STYLESHEET, homePage, listPage, llmText, postPage, skillPage, skillsIndexPage, thoughtPage, } from "./site.js";
8
8
  import { loadSkills } from "./skills.js";
9
+ import { sitePageFile } from "./pages.js";
9
10
  /** Define a site config in the app package without taking on the build pipeline. */
10
11
  export function meowSite(config) {
11
12
  return {
@@ -17,6 +18,9 @@ export function meowSite(config) {
17
18
  ? resolve(config.projectRootDirectory)
18
19
  : undefined,
19
20
  site: { ...DEFAULT_SITE, ...config.site },
21
+ pages: config.pages,
22
+ mdxComponents: config.mdxComponents ?? {},
23
+ themeStylesheet: config.themeStylesheet ?? DEFAULT_THEME_STYLESHEET,
20
24
  };
21
25
  }
22
26
  async function writePage(generatedDirectory, pathname, html) {
@@ -58,13 +62,30 @@ async function buildStaticHtmlFromEntrypoints(entrypoints, generatedDirectory, o
58
62
  throw new Error("Unable to build site static HTML");
59
63
  }
60
64
  }
61
- async function writeContentPages(generatedDirectory, posts, thoughts, theme, site) {
65
+ /** Consumer pages are written inside the generated directory and never above it. */
66
+ function consumerPageFile(generatedDirectory, path) {
67
+ const file = sitePageFile(path);
68
+ const target = resolve(generatedDirectory, file);
69
+ const inside = relative(generatedDirectory, target);
70
+ if (!inside || inside.startsWith("..") || isAbsolute(inside)) {
71
+ throw new Error(`Site page path stays inside the site root: ${path}`);
72
+ }
73
+ return file;
74
+ }
75
+ async function writeConsumerPages(generatedDirectory, pages) {
76
+ const entrypoints = [];
77
+ for (const page of pages) {
78
+ entrypoints.push(await writePage(generatedDirectory, consumerPageFile(generatedDirectory, page.path), page.html));
79
+ }
80
+ return entrypoints;
81
+ }
82
+ async function writeContentPages(generatedDirectory, posts, thoughts, theme, site, stylesheet) {
62
83
  const entrypoints = [];
63
84
  for (const post of posts) {
64
- entrypoints.push(await writePage(generatedDirectory, `posts/${post.slug}/index.html`, postPage(post, posts, thoughts, theme, site)));
85
+ entrypoints.push(await writePage(generatedDirectory, `posts/${post.slug}/index.html`, postPage(post, posts, thoughts, theme, site, stylesheet)));
65
86
  }
66
87
  for (const thought of thoughts) {
67
- entrypoints.push(await writePage(generatedDirectory, `thoughts/${thought.slug}/index.html`, thoughtPage(thought, theme, site)));
88
+ entrypoints.push(await writePage(generatedDirectory, `thoughts/${thought.slug}/index.html`, thoughtPage(thought, theme, site, stylesheet)));
68
89
  }
69
90
  return entrypoints;
70
91
  }
@@ -72,30 +93,47 @@ export async function buildSite(input) {
72
93
  const config = meowSite(input);
73
94
  const generatedDirectory = resolve(config.outputDirectory, "../.generated");
74
95
  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
- }),
96
+ loadSection(config.content, "post", { components: config.mdxComponents }),
97
+ loadSection(config.content, "thought", { components: config.mdxComponents }),
98
+ config.pages
99
+ ? Promise.resolve([])
100
+ : loadSkills(config.skillsDirectory, {
101
+ projectRootDirectory: config.projectRootDirectory,
102
+ }),
80
103
  ]);
81
104
  const timeline = [...posts, ...thoughts].sort((left, right) => (right.date?.getTime() ?? 0) - (left.date?.getTime() ?? 0));
82
105
  await rm(config.outputDirectory, { recursive: true, force: true });
83
106
  await rm(generatedDirectory, { recursive: true, force: true });
84
107
  await mkdir(config.outputDirectory, { recursive: true });
85
108
  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)));
109
+ // A consumer that supplies its own pages owns the whole site map; the package's
110
+ // own blog routes are not published alongside them.
111
+ const pages = config.pages
112
+ ? await config.pages({
113
+ posts,
114
+ thoughts,
115
+ site: config.site,
116
+ theme: config.theme,
117
+ })
118
+ : undefined;
119
+ const entrypoints = [];
120
+ if (pages) {
121
+ entrypoints.push(...(await writeConsumerPages(generatedDirectory, pages)));
122
+ }
123
+ else {
124
+ entrypoints.push(...(await Promise.all([
125
+ writePage(generatedDirectory, "index.html", homePage(timeline, config.theme, config.site, config.themeStylesheet)),
126
+ writePage(generatedDirectory, "about/index.html", aboutPage(config.theme, config.site, config.themeStylesheet)),
127
+ writePage(generatedDirectory, "posts/index.html", listPage("post", posts, config.theme, config.site, config.themeStylesheet)),
128
+ writePage(generatedDirectory, "thoughts/index.html", listPage("thought", thoughts, config.theme, config.site, config.themeStylesheet)),
129
+ writePage(generatedDirectory, "skills/index.html", skillsIndexPage(skills, config.theme, config.site, config.themeStylesheet)),
130
+ ])), ...(await writeContentPages(generatedDirectory, posts, thoughts, config.theme, config.site, config.themeStylesheet)));
131
+ for (const skill of skills) {
132
+ const skillPath = skill.scope === "public" ? `skills/${skill.slug}` : `skills/project/${skill.slug}`;
133
+ entrypoints.push(await writePage(generatedDirectory, `${skillPath}/index.html`, skillPage(skill, config.theme, config.site, config.themeStylesheet)));
134
+ }
97
135
  }
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");
136
+ await writeFile(join(generatedDirectory, config.themeStylesheet), config.theme.cssText.replaceAll('"@fontsource-variable/geist"', JSON.stringify(fileURLToPath(import.meta.resolve("@fontsource-variable/geist")))), "utf8");
99
137
  await stageAssets(generatedDirectory, config.content);
100
138
  await buildStaticHtmlFromEntrypoints(entrypoints, generatedDirectory, config.outputDirectory);
101
139
  if (config.theme.cssText.includes('"@fontsource-variable/geist"')) {
@@ -105,10 +143,12 @@ export async function buildSite(input) {
105
143
  }
106
144
  await copyPostAssets(config.outputDirectory, config.content);
107
145
  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"),
146
+ writeFile(join(config.outputDirectory, "llm.txt"), llmText(posts, thoughts, config.site, pages), "utf8"),
147
+ writeFile(join(config.outputDirectory, "llms.txt"), llmText(posts, thoughts, config.site, pages), "utf8"),
148
+ writeFile(join(config.outputDirectory, "agents.md"), agentMarkdown(posts, thoughts, config.site, pages), "utf8"),
111
149
  ]);
112
150
  await rm(generatedDirectory, { recursive: true, force: true });
113
- console.log(`Built ${posts.length} posts and ${thoughts.length} thoughts into site dist/.`);
151
+ console.log(pages
152
+ ? `Built ${posts.length} content items and ${pages.length} site pages into site dist/.`
153
+ : `Built ${posts.length} posts and ${thoughts.length} thoughts into site dist/.`);
114
154
  }
@@ -1,3 +1,4 @@
1
+ import { type Child, type Props } from "meowjsx/runtime";
1
2
  export type ContentKind = "post" | "thought";
2
3
  export type Frontmatter = Record<string, unknown>;
3
4
  /** A content root containing `posts/`, `thoughts/`, and optional asset folders. */
@@ -11,7 +12,11 @@ export type DuplicateContentPolicy = "first-wins" | "error";
11
12
  export interface LoadSectionOptions {
12
13
  /** The first source wins by default, so the source order is an explicit overlay order. */
13
14
  duplicate?: DuplicateContentPolicy;
15
+ /** Extra components available to MDX content pages. */
16
+ components?: Record<string, MdxComponent>;
14
17
  }
18
+ /** A component an MDX content page may use by name. */
19
+ export type MdxComponent = (props: Props) => Child;
15
20
  export interface ContentItem {
16
21
  kind: ContentKind;
17
22
  slug: string;
@@ -112,7 +112,7 @@ function Callout(props) {
112
112
  const MDX_COMPONENTS = {
113
113
  Callout,
114
114
  };
115
- async function renderMdx(source) {
115
+ async function renderMdx(source, components) {
116
116
  const compiled = await compile({ value: source }, {
117
117
  outputFormat: "function-body",
118
118
  remarkPlugins: [remarkGfm],
@@ -123,15 +123,15 @@ async function renderMdx(source) {
123
123
  jsxs: h,
124
124
  });
125
125
  const component = module.default;
126
- return renderToHtml(component({ components: MDX_COMPONENTS }));
126
+ return renderToHtml(component({ components: { ...MDX_COMPONENTS, ...components } }));
127
127
  }
128
- async function renderSource(source, sourcePath) {
128
+ async function renderSource(source, sourcePath, components) {
129
129
  if (sourcePath.endsWith(".md")) {
130
130
  return Bun.markdown.html(source, {
131
131
  tagFilter: true,
132
132
  });
133
133
  }
134
- return renderMdx(source);
134
+ return renderMdx(source, components);
135
135
  }
136
136
  async function collectFiles(directory) {
137
137
  const entries = await readdir(directory, { withFileTypes: true });
@@ -186,7 +186,7 @@ export function normalizeContentSources(input) {
186
186
  function duplicateContentError(kind, slug) {
187
187
  return new Error(`Duplicate ${kind} slug "${slug}" while merging content sources.`);
188
188
  }
189
- async function loadSectionFromSource(source, kind) {
189
+ async function loadSectionFromSource(source, kind, options) {
190
190
  const sectionDirectory = resolve(source.directory, `${kind}s`);
191
191
  let sourceFiles;
192
192
  try {
@@ -217,7 +217,7 @@ async function loadSectionFromSource(source, kind) {
217
217
  const bodyWithoutLeadingHeading = document.body.replace(/^\s*#\s+.+?(?:\r?\n|$)+/, "");
218
218
  const plainText = stripMarkdown(bodyWithoutLeadingHeading);
219
219
  const title = asText(document.metadata.title) ?? titleFromBody(document.body, slug);
220
- const html = withoutLeadingHeading(await renderSource(document.body, sourcePath));
220
+ const html = withoutLeadingHeading(await renderSource(document.body, sourcePath, options.components ?? {}));
221
221
  items.push({
222
222
  kind,
223
223
  slug,
@@ -241,7 +241,7 @@ export async function loadSection(sources, kind, options = {}) {
241
241
  const merged = new Map();
242
242
  const duplicatePolicy = options.duplicate ?? "first-wins";
243
243
  for (const source of normalizeContentSources(sources)) {
244
- const items = await loadSectionFromSource(source, kind);
244
+ const items = await loadSectionFromSource(source, kind, options);
245
245
  for (const item of items) {
246
246
  if (merged.has(item.slug)) {
247
247
  if (duplicatePolicy === "error")
@@ -1,6 +1,6 @@
1
1
  export const DEFAULT_SITE = {
2
2
  name: "Meowpress site",
3
3
  id: "meowpress",
4
- description: "长文与碎片想法的混排时间线。",
4
+ description: "A mixed timeline of long-form posts and fragmentary thoughts.",
5
5
  contentDirectory: "content",
6
6
  };
@@ -0,0 +1,28 @@
1
+ import type { ThemeDefinition } from "meowcss";
2
+ import type { ContentItem } from "./content.js";
3
+ import type { SiteIdentity } from "./identity.js";
4
+ /** A page supplied by the consumer of the site. */
5
+ export interface SitePage {
6
+ /** Route path below the site root; "packages/meowtail" becomes /packages/meowtail/. */
7
+ path: string;
8
+ title: string;
9
+ /** A complete HTML document for the route. */
10
+ html: string;
11
+ description?: string;
12
+ /** Grouping used by the site index and the agent-facing indexes. */
13
+ group?: string;
14
+ order?: number;
15
+ }
16
+ /** What a page provider receives when the site is built. */
17
+ export interface SitePageContext {
18
+ posts: readonly ContentItem[];
19
+ thoughts: readonly ContentItem[];
20
+ site: SiteIdentity;
21
+ theme: ThemeDefinition;
22
+ }
23
+ /** Supplies the consumer's own pages for the site. */
24
+ export type SitePageProvider = (context: SitePageContext) => readonly SitePage[] | Promise<readonly SitePage[]>;
25
+ /** The route a page is published under, with a leading and a trailing slash. */
26
+ export declare function sitePageUrl(path: string): string;
27
+ /** The output file a page is written to, relative to the site root. */
28
+ export declare function sitePageFile(path: string): string;
@@ -0,0 +1,10 @@
1
+ /** The route a page is published under, with a leading and a trailing slash. */
2
+ export function sitePageUrl(path) {
3
+ const normalized = path.replace(/^\/+|\/+$/g, "");
4
+ return normalized ? `/${normalized}/` : "/";
5
+ }
6
+ /** The output file a page is written to, relative to the site root. */
7
+ export function sitePageFile(path) {
8
+ const normalized = path.replace(/^\/+|\/+$/g, "");
9
+ return normalized ? `${normalized}/index.html` : "index.html";
10
+ }
@@ -1,13 +1,16 @@
1
1
  import { type ThemeDefinition } from "meowcss";
2
2
  import { type ContentItem } from "./content.js";
3
3
  import { type SiteIdentity } from "./identity.js";
4
+ import { type SitePage } from "./pages.js";
4
5
  import type { Skill } from "./skills.js";
5
- export declare function homePage(items: ContentItem[], theme?: ThemeDefinition, site?: SiteIdentity): string;
6
- export declare function listPage(kind: "post" | "thought", items: ContentItem[], theme?: ThemeDefinition, site?: SiteIdentity): string;
7
- export declare function postPage(item: ContentItem, posts: ContentItem[], thoughts: ContentItem[], theme?: ThemeDefinition, site?: SiteIdentity): string;
8
- export declare function thoughtPage(item: ContentItem, theme?: ThemeDefinition, site?: SiteIdentity): string;
9
- export declare function aboutPage(theme?: ThemeDefinition, site?: SiteIdentity): string;
10
- export declare function skillsIndexPage(skills: Skill[], theme?: ThemeDefinition, site?: SiteIdentity): string;
11
- export declare function skillPage(skill: Skill, theme?: ThemeDefinition, site?: SiteIdentity): string;
12
- export declare function agentMarkdown(posts: ContentItem[], thoughts: ContentItem[], site?: SiteIdentity): string;
13
- export declare function llmText(posts: ContentItem[], thoughts: ContentItem[], site?: SiteIdentity): string;
6
+ /** The name of the theme stylesheet the package writes by default. */
7
+ export declare const DEFAULT_THEME_STYLESHEET = "blog-theme.css";
8
+ export declare function homePage(items: ContentItem[], theme?: ThemeDefinition, site?: SiteIdentity, stylesheet?: string): string;
9
+ export declare function listPage(kind: "post" | "thought", items: ContentItem[], theme?: ThemeDefinition, site?: SiteIdentity, stylesheet?: string): string;
10
+ export declare function postPage(item: ContentItem, posts: ContentItem[], thoughts: ContentItem[], theme?: ThemeDefinition, site?: SiteIdentity, stylesheet?: string): string;
11
+ export declare function thoughtPage(item: ContentItem, theme?: ThemeDefinition, site?: SiteIdentity, stylesheet?: string): string;
12
+ export declare function aboutPage(theme?: ThemeDefinition, site?: SiteIdentity, stylesheet?: string): string;
13
+ export declare function skillsIndexPage(skills: Skill[], theme?: ThemeDefinition, site?: SiteIdentity, stylesheet?: string): string;
14
+ export declare function skillPage(skill: Skill, theme?: ThemeDefinition, site?: SiteIdentity, stylesheet?: string): string;
15
+ export declare function agentMarkdown(posts: ContentItem[], thoughts: ContentItem[], site?: SiteIdentity, pages?: readonly SitePage[]): string;
16
+ export declare function llmText(posts: ContentItem[], thoughts: ContentItem[], site?: SiteIdentity, pages?: readonly SitePage[]): string;
package/dist/src/site.js CHANGED
@@ -4,13 +4,24 @@ import { Fragment, h } from "meowjsx/runtime";
4
4
  import { renderToHtml } from "meowjsx/web";
5
5
  import { withoutLeadingHeading } from "./content.js";
6
6
  import { DEFAULT_SITE } from "./identity.js";
7
+ import { sitePageUrl } from "./pages.js";
7
8
  import { layoutSkillGraph } from "./skill-graph.js";
9
+ /** The name of the theme stylesheet the package writes by default. */
10
+ export const DEFAULT_THEME_STYLESHEET = "blog-theme.css";
11
+ /** The href of the theme stylesheet for a page at a given depth. */
12
+ function styleHref(depth, stylesheet) {
13
+ return `${"../".repeat(depth)}${stylesheet}`;
14
+ }
15
+ /** The number of directories a slug puts a page below its section. */
16
+ function slugDepth(slug) {
17
+ return slug.split("/").filter(Boolean).length;
18
+ }
8
19
  function SiteHeader({ activePath, site }) {
9
20
  return (h("header", { className: "site" },
10
21
  h("a", { className: "site-name", href: "/" }, site.name),
11
- h("nav", { className: "site-nav", "aria-label": "\u4E3B\u5BFC\u822A" },
22
+ h("nav", { className: "site-nav", "aria-label": "Primary navigation" },
12
23
  h("a", { className: activePath === "/skills/" ? "active" : "", href: "/skills/" }, "Skills"),
13
- h("a", { className: activePath === "/about/" ? "active" : "", href: "/about/" }, "\u5173\u4E8E"))));
24
+ h("a", { className: activePath === "/about/" ? "active" : "", href: "/about/" }, "About"))));
14
25
  }
15
26
  function SiteFooter({ site }) {
16
27
  return (h("footer", { className: "site" },
@@ -26,7 +37,7 @@ function SiteFooter({ site }) {
26
37
  }
27
38
  function Document({ title, description, activePath, styleHref, children, theme, site, }) {
28
39
  const documentTitle = title === site.name ? site.name : `${title} · ${site.name}`;
29
- return (h("html", { lang: "zh-CN", ...themeAttributes(theme) },
40
+ return (h("html", { lang: "en", ...themeAttributes(theme) },
30
41
  h("head", null,
31
42
  h("meta", { charSet: "UTF-8" }),
32
43
  h("meta", { name: "viewport", content: "width=device-width, initial-scale=1" }),
@@ -46,15 +57,15 @@ function pageHtml(props) {
46
57
  }
47
58
  function dateLabel(dateKey) {
48
59
  if (dateKey === "undated")
49
- return { date: "未标日期", weekday: "" };
60
+ return { date: "Undated", weekday: "" };
50
61
  const date = new Date(`${dateKey}T00:00:00+08:00`);
51
62
  return {
52
- date: new Intl.DateTimeFormat("zh-CN", {
63
+ date: new Intl.DateTimeFormat("en-US", {
53
64
  timeZone: "Asia/Shanghai",
54
65
  month: "long",
55
66
  day: "numeric",
56
67
  }).format(date),
57
- weekday: new Intl.DateTimeFormat("zh-CN", {
68
+ weekday: new Intl.DateTimeFormat("en-US", {
58
69
  timeZone: "Asia/Shanghai",
59
70
  weekday: "short",
60
71
  year: "numeric",
@@ -68,9 +79,9 @@ function Hero({ image }) {
68
79
  return (h("section", { className: "hero", "aria-labelledby": "hero-title" },
69
80
  h("div", { className: "hero-copy" },
70
81
  h("h1", { id: "hero-title" },
71
- "\u4E00\u4E07\u5E74\u592A\u4E45\uFF0C",
82
+ "Ten thousand years is too long;",
72
83
  h("br", null),
73
- h("em", null, "\u53EA\u771F\u671D\u5915\u3002"))),
84
+ h("em", null, "only this day is true."))),
74
85
  h("div", { className: "hero-work", "aria-hidden": "true" },
75
86
  h("svg", { className: "hero-doodle", viewBox: "0 0 260 190" },
76
87
  h("path", { className: "doodle-loop", d: "M24 96C20 45 58 18 122 20c72 2 116 33 114 84-2 48-48 70-113 68-65-2-96-27-99-76Z" }),
@@ -104,7 +115,7 @@ function ThoughtCard({ item, linked = false }) {
104
115
  const body = (h(Fragment, null,
105
116
  h("div", { className: "time" },
106
117
  h("span", { className: "t" }, item.timeLabel || item.dateKey),
107
- " \u00B7 \u60F3\u6CD5"),
118
+ " \u00B7 Thought"),
108
119
  h("div", { className: "body", dangerouslySetInnerHTML: { __html: withoutLeadingHeading(item.html) } }),
109
120
  h(Tags, { item: item })));
110
121
  if (!linked)
@@ -113,18 +124,18 @@ function ThoughtCard({ item, linked = false }) {
113
124
  h("a", { className: "thought-link", href: itemUrl(item) }, body)));
114
125
  }
115
126
  function PostCard({ item }) {
116
- const category = typeof item.metadata.category === "string" ? item.metadata.category : "文章";
127
+ const category = typeof item.metadata.category === "string" ? item.metadata.category : "Post";
117
128
  return (h("a", { className: "post-card", href: itemUrl(item), "data-kind": "post" },
118
129
  h("div", { className: "card-top" },
119
130
  h("span", { className: "kind" }, category),
120
131
  h("span", { className: "meta" },
121
- "\u7EA6 ",
132
+ "About ",
122
133
  item.readingMinutes,
123
- " \u5206\u949F \u00B7 ",
134
+ " min \u00B7 ",
124
135
  item.characterCount,
125
- " \u5B57")),
136
+ " characters")),
126
137
  h("h3", null, item.title),
127
- h("p", { className: "excerpt" }, item.excerpt || "这篇文章还没有摘要。"),
138
+ h("p", { className: "excerpt" }, item.excerpt || "This post has no summary yet."),
128
139
  h(Tags, { item: item })));
129
140
  }
130
141
  function groupByDate(items) {
@@ -168,7 +179,7 @@ function FeedDecor({ image }) {
168
179
  function Feed({ items, image }) {
169
180
  if (items.length === 0) {
170
181
  return (h("main", { className: "feed feed-empty" },
171
- h("p", { className: "empty-state" }, "\u8FD8\u6CA1\u6709\u516C\u5F00\u5185\u5BB9\u3002"),
182
+ h("p", { className: "empty-state" }, "No public content yet."),
172
183
  h(FeedDecor, { image: image })));
173
184
  }
174
185
  return (h("main", { className: "feed" },
@@ -185,15 +196,15 @@ function Feed({ items, image }) {
185
196
  }
186
197
  function Filter({ posts, thoughts }) {
187
198
  const total = posts + thoughts;
188
- return (h("div", { className: "tag-filter", "data-filter-group": "feed", role: "group", "aria-label": "\u7B5B\u9009\u9996\u9875\u5185\u5BB9" },
199
+ return (h("div", { className: "tag-filter", "data-filter-group": "feed", role: "group", "aria-label": "Filter home feed" },
189
200
  h("button", { type: "button", "data-filter": "all", className: "active", "aria-pressed": "true" },
190
- "\u5168\u90E8",
201
+ "All",
191
202
  h("i", null, total)),
192
203
  h("button", { type: "button", "data-filter": "post", "aria-pressed": "false" },
193
- "\u6587\u7AE0",
204
+ "Posts",
194
205
  h("i", null, posts)),
195
206
  h("button", { type: "button", "data-filter": "thought", "aria-pressed": "false" },
196
- "\u60F3\u6CD5",
207
+ "Thoughts",
197
208
  h("i", null, thoughts))));
198
209
  }
199
210
  const FILTER_SCRIPT = `(() => {
@@ -240,12 +251,12 @@ const FILTER_SCRIPT = `(() => {
240
251
  }
241
252
  }));
242
253
  })();`;
243
- export function homePage(items, theme = blogTheme, site = DEFAULT_SITE) {
254
+ export function homePage(items, theme = blogTheme, site = DEFAULT_SITE, stylesheet = DEFAULT_THEME_STYLESHEET) {
244
255
  return pageHtml({
245
256
  site,
246
257
  theme,
247
258
  title: site.name,
248
- styleHref: "./blog-theme.css",
259
+ styleHref: styleHref(0, stylesheet),
249
260
  children: (h(Fragment, null,
250
261
  h(Hero, { image: site.images?.hero ? `./${site.images.hero}` : undefined }),
251
262
  h(Filter, { posts: items.filter((item) => item.kind === "post").length, thoughts: items.filter((item) => item.kind === "thought").length }),
@@ -253,53 +264,55 @@ export function homePage(items, theme = blogTheme, site = DEFAULT_SITE) {
253
264
  h("script", { dangerouslySetInnerHTML: { __html: FILTER_SCRIPT } }))),
254
265
  });
255
266
  }
256
- export function listPage(kind, items, theme = blogTheme, site = DEFAULT_SITE) {
267
+ export function listPage(kind, items, theme = blogTheme, site = DEFAULT_SITE, stylesheet = DEFAULT_THEME_STYLESHEET) {
257
268
  const isPost = kind === "post";
258
269
  return pageHtml({
259
270
  site,
260
271
  theme,
261
- title: isPost ? "文章" : "想法",
272
+ title: isPost ? "Posts" : "Thoughts",
262
273
  activePath: isPost ? "/posts/" : "/thoughts/",
263
- styleHref: "../blog-theme.css",
274
+ styleHref: styleHref(1, stylesheet),
264
275
  children: (h("main", { className: "listing" },
265
276
  h("header", { className: "listing-head" },
266
277
  h("span", { className: "kind" }, isPost ? "Posts" : "Thoughts"),
267
- h("h1", null, isPost ? "文章" : "想法"),
268
- h("p", null, isPost ? "从碎片里长出来的完整论述。" : "经过剪裁、可以直接展示的短想法。")),
269
- h("div", { className: isPost ? "post-list" : "thought-list" }, items.length === 0 ? (h("p", { className: "empty-state" }, "\u8FD8\u6CA1\u6709\u516C\u5F00\u5185\u5BB9\u3002")) : (items.map((item) => isPost ? h(PostCard, { item: item }) : h(ThoughtCard, { item: item, linked: true })))))),
278
+ h("h1", null, isPost ? "Posts" : "Thoughts"),
279
+ h("p", null, isPost
280
+ ? "Complete arguments grown from fragments."
281
+ : "Short, trimmed thoughts ready to show as they are.")),
282
+ h("div", { className: isPost ? "post-list" : "thought-list" }, items.length === 0 ? (h("p", { className: "empty-state" }, "No public content yet.")) : (items.map((item) => isPost ? h(PostCard, { item: item }) : h(ThoughtCard, { item: item, linked: true })))))),
270
283
  });
271
284
  }
272
285
  function PostNavigation({ item, posts }) {
273
286
  const index = posts.findIndex((post) => post.slug === item.slug);
274
287
  const older = index >= 0 ? posts[index + 1] : undefined;
275
288
  const newer = index > 0 ? posts[index - 1] : undefined;
276
- return (h("nav", { className: "post-nav", "aria-label": "\u6587\u7AE0\u5BFC\u822A" },
289
+ return (h("nav", { className: "post-nav", "aria-label": "Post navigation" },
277
290
  older ? (h("a", { href: itemUrl(older) },
278
- h("span", { className: "label" }, "\u2190 \u4E0A\u4E00\u7BC7\uFF08\u66F4\u65E9\uFF09"),
291
+ h("span", { className: "label" }, "\u2190 Previous post (older)"),
279
292
  older.title)) : (h("span", null)),
280
293
  newer ? (h("a", { className: "next", href: itemUrl(newer) },
281
- h("span", { className: "label" }, "\u4E0B\u4E00\u7BC7\uFF08\u66F4\u65B0\uFF09\u2192"),
294
+ h("span", { className: "label" }, "Next post (newer) \u2192"),
282
295
  newer.title)) : (h("span", { className: "next" },
283
- h("span", { className: "label" }, "\u4E0B\u4E00\u7BC7\uFF08\u66F4\u65B0\uFF09\u2192"),
284
- "\u6CA1\u6709\u4E86\uFF0C\u8FD9\u662F\u6700\u65B0\u7684\u4E00\u7BC7"))));
296
+ h("span", { className: "label" }, "Next post (newer) \u2192"),
297
+ "Nothing newer \u2014 this is the latest post."))));
285
298
  }
286
299
  function RelatedThoughts({ item, thoughts, }) {
287
300
  const related = thoughts.filter((thought) => thought.dateKey === item.dateKey).slice(0, 3);
288
301
  if (related.length === 0)
289
302
  return null;
290
303
  return (h("aside", { className: "related-thoughts" },
291
- h("h2", null, "\u540C\u4E00\u5929\u7684\u60F3\u6CD5"),
304
+ h("h2", null, "Thoughts from the same day"),
292
305
  related.map((thought) => (h(ThoughtCard, { item: thought, linked: true })))));
293
306
  }
294
- export function postPage(item, posts, thoughts, theme = blogTheme, site = DEFAULT_SITE) {
295
- const category = typeof item.metadata.category === "string" ? item.metadata.category : "文章";
307
+ export function postPage(item, posts, thoughts, theme = blogTheme, site = DEFAULT_SITE, stylesheet = DEFAULT_THEME_STYLESHEET) {
308
+ const category = typeof item.metadata.category === "string" ? item.metadata.category : "Post";
296
309
  return pageHtml({
297
310
  site,
298
311
  theme,
299
312
  title: item.title,
300
313
  description: item.excerpt,
301
314
  activePath: "/posts/",
302
- styleHref: "../../blog-theme.css",
315
+ styleHref: styleHref(1 + slugDepth(item.slug), stylesheet),
303
316
  children: (h("main", null,
304
317
  h("article", null,
305
318
  h("header", { className: "post-head" },
@@ -307,28 +320,28 @@ export function postPage(item, posts, thoughts, theme = blogTheme, site = DEFAUL
307
320
  h("h1", null, item.title),
308
321
  h("div", { className: "meta" },
309
322
  item.dateKey,
310
- " \u00B7 \u7EA6 ",
323
+ " \u00B7 about ",
311
324
  item.readingMinutes,
312
- " \u5206\u949F \u00B7 ",
325
+ " min \u00B7 ",
313
326
  item.characterCount,
314
- " \u5B57")),
327
+ " characters")),
315
328
  h("div", { className: "prose", dangerouslySetInnerHTML: { __html: item.html } }),
316
329
  h(RelatedThoughts, { item: item, thoughts: thoughts }),
317
330
  h(PostNavigation, { item: item, posts: posts })))),
318
331
  });
319
332
  }
320
- export function thoughtPage(item, theme = blogTheme, site = DEFAULT_SITE) {
333
+ export function thoughtPage(item, theme = blogTheme, site = DEFAULT_SITE, stylesheet = DEFAULT_THEME_STYLESHEET) {
321
334
  return pageHtml({
322
335
  site,
323
336
  theme,
324
337
  title: item.title,
325
338
  description: item.excerpt,
326
339
  activePath: "/thoughts/",
327
- styleHref: "../../blog-theme.css",
340
+ styleHref: styleHref(1 + slugDepth(item.slug), stylesheet),
328
341
  children: (h("main", null,
329
342
  h("article", null,
330
343
  h("header", { className: "post-head" },
331
- h("span", { className: "kind" }, "\u60F3\u6CD5"),
344
+ h("span", { className: "kind" }, "Thought"),
332
345
  h("h1", null, item.title),
333
346
  h("div", { className: "meta" },
334
347
  item.dateKey,
@@ -336,21 +349,21 @@ export function thoughtPage(item, theme = blogTheme, site = DEFAULT_SITE) {
336
349
  h("div", { className: "prose", dangerouslySetInnerHTML: { __html: item.html } })))),
337
350
  });
338
351
  }
339
- export function aboutPage(theme = blogTheme, site = DEFAULT_SITE) {
352
+ export function aboutPage(theme = blogTheme, site = DEFAULT_SITE, stylesheet = DEFAULT_THEME_STYLESHEET) {
340
353
  const about = site.about;
341
354
  return pageHtml({
342
355
  site,
343
356
  theme,
344
- title: "关于",
357
+ title: "About",
345
358
  activePath: "/about/",
346
- styleHref: "../blog-theme.css",
359
+ styleHref: styleHref(1, stylesheet),
347
360
  children: (h("main", { className: "about" },
348
361
  h("div", { className: "avatar", "aria-hidden": "true" }, site.images?.about ? (h("img", { src: `../${site.images.about}`, alt: "", decoding: "async" })) : (h("span", { className: "avatar-initial" }, Array.from(site.name.trim())[0] ?? "M"))),
349
362
  about ? (h(Fragment, null,
350
363
  h("h1", null, about.heading),
351
364
  about.paragraphs.map((paragraph) => (h("p", null, paragraph))),
352
365
  about.links && about.links.length > 0 ? (h(Fragment, null,
353
- h("h2", null, about.linksHeading ?? "找到我"),
366
+ h("h2", null, about.linksHeading ?? "Find me"),
354
367
  about.links.map((link) => (h("a", { className: "github-link", href: link.href, target: "_blank", rel: "me noopener noreferrer" },
355
368
  h(DoodleGithubIcon, null),
356
369
  h("span", { className: "github-copy" },
@@ -371,17 +384,17 @@ function SkillCard({ skill, site }) {
371
384
  return (h("a", { className: `skill-card${skill.scope === "project" ? " project-skill" : ""}`, href: skillUrl(skill) },
372
385
  h("span", { className: "skill-card-source" },
373
386
  h("span", { className: "skill-card-path" }, skill.scope === "public" ? skillInstallCommand(site, skill) : skill.sourceBasePath),
374
- skill.scope === "project" ? h("span", { className: "skill-scope-badge" }, "\u9879\u76EE Skill") : null),
387
+ skill.scope === "project" ? (h("span", { className: "skill-scope-badge" }, "Project Skill")) : null),
375
388
  h("span", { className: "skill-card-main" },
376
389
  h("strong", null, skill.name),
377
390
  h("span", null, skill.description)),
378
391
  h("span", { className: "skill-card-foot" },
379
392
  h("span", null,
380
393
  skill.files.length,
381
- " \u4E2A Markdown \u00B7 ",
394
+ " Markdown files \u00B7 ",
382
395
  skill.relations.length,
383
- " \u6761\u5F15\u7528"),
384
- h("span", { className: "skill-open" }, "\u6253\u5F00\u76EE\u5F55\u00A0 \u2192"))));
396
+ " references"),
397
+ h("span", { className: "skill-open" }, "Open directory\u00A0 \u2192"))));
385
398
  }
386
399
  function skillTree(files) {
387
400
  const root = { name: "", children: [] };
@@ -410,14 +423,14 @@ function SkillTreeNodes({ nodes, activePath, }) {
410
423
  h("button", { type: "button", className: node.path === activePath ? "active" : "", "data-skill-file": node.path, "aria-pressed": node.path === activePath ? "true" : "false" }, node.name))))));
411
424
  }
412
425
  function SkillFileTree({ skill, activePath }) {
413
- return (h("nav", { className: "skill-files", "aria-label": `${skill.name} 文件目录` },
426
+ return (h("nav", { className: "skill-files", "aria-label": `${skill.name} file tree` },
414
427
  h("span", { className: "skill-files-label" }, "Files"),
415
428
  h(SkillTreeNodes, { nodes: skillTree(skill.files), activePath: activePath })));
416
429
  }
417
430
  function SkillGraphNodeView({ node }) {
418
431
  const width = node.width;
419
432
  const height = node.height;
420
- return (h("a", { className: `skill-graph-node${node.external ? " external" : ""}${node.kind ? ` ${node.kind}` : ""}`, href: node.external ? "#relations" : `#${encodeURIComponent(node.path)}`, "data-map-path": node.path, "data-map-file": node.external ? undefined : node.path, "data-map-detail": node.detail, "aria-label": `${node.name},${node.role}` },
433
+ return (h("a", { className: `skill-graph-node${node.external ? " external" : ""}${node.kind ? ` ${node.kind}` : ""}`, href: node.external ? "#relations" : `#${encodeURIComponent(node.path)}`, "data-map-path": node.path, "data-map-file": node.external ? undefined : node.path, "data-map-detail": node.detail, "aria-label": `${node.name}; ${node.role}` },
421
434
  h("rect", { x: node.x - width / 2, y: node.y - height / 2, width: width, height: height, rx: "9" }),
422
435
  h("text", { className: "skill-graph-node-name", x: node.x, y: node.y - 4 }, node.name),
423
436
  h("text", { className: "skill-graph-node-role", x: node.x, y: node.y + 17 }, node.role)));
@@ -425,7 +438,7 @@ function SkillGraphNodeView({ node }) {
425
438
  function SkillRelationGraph({ skill, direction }) {
426
439
  const graph = layoutSkillGraph(skill, direction);
427
440
  const markerId = `skill-arrow-${skill.slug}-${direction.toLowerCase()}`;
428
- return (h("svg", { className: `skill-graph-svg skill-graph-${direction.toLowerCase()}`, viewBox: `0 0 ${graph.width} ${graph.height}`, "aria-label": `${skill.name} 文件引用关系` },
441
+ return (h("svg", { className: `skill-graph-svg skill-graph-${direction.toLowerCase()}`, viewBox: `0 0 ${graph.width} ${graph.height}`, "aria-label": `${skill.name} file reference graph` },
429
442
  h("defs", null,
430
443
  h("marker", { id: markerId, viewBox: "0 0 10 10", refX: "8", refY: "5", markerWidth: "5", markerHeight: "5", orient: "auto-start-reverse" },
431
444
  h("path", { className: "skill-graph-arrow", d: "M 0 0 L 10 5 L 0 10 z" }))),
@@ -448,28 +461,28 @@ function SkillRelations({ skill }) {
448
461
  const hasRelations = skill.relations.length > 0;
449
462
  const compactGraph = skill.files.length + skill.externalNodes.length <= 3;
450
463
  return (h("section", { className: "skill-relations", "data-skill-view-panel": "relations", hidden: true },
451
- h("div", { className: "skill-reading-summary", "aria-label": "\u9605\u8BFB\u7ED3\u6784\u6458\u8981" },
464
+ h("div", { className: "skill-reading-summary", "aria-label": "Reading structure summary" },
452
465
  h("span", null,
453
466
  h("strong", null,
454
467
  reachableFiles,
455
468
  "/",
456
469
  skill.files.length),
457
- "\u53EF\u8FBE\u6587\u6863"),
470
+ "reachable docs"),
458
471
  h("span", null,
459
472
  h("strong", null, skill.relations.length),
460
- "\u5F15\u7528\u5173\u7CFB"),
473
+ " references"),
461
474
  h("span", null,
462
475
  h("strong", null, maxDepth),
463
- "\u6700\u6DF1\u5C42\u7EA7"),
476
+ " max depth"),
464
477
  h("span", { className: missingNodes.length > 0 ? "has-issue" : "" },
465
478
  h("strong", null, missingNodes.length),
466
- "\u7F3A\u5931\u76EE\u6807")),
479
+ " missing targets")),
467
480
  hasRelations ? (h(Fragment, null,
468
481
  h("div", { className: `skill-graph-canvas${compactGraph ? " compact" : ""}` },
469
482
  h(SkillRelationGraph, { skill: skill, direction: "LR" }),
470
483
  h(SkillRelationGraph, { skill: skill, direction: "TB" })),
471
484
  h("section", { className: "skill-reading-paths", "aria-labelledby": `${skill.slug}-paths` },
472
- h("h3", { id: `${skill.slug}-paths` }, "\u5F15\u7528\u53D1\u751F\u5728\u54EA\u91CC"),
485
+ h("h3", { id: `${skill.slug}-paths` }, "Where references happen"),
473
486
  h("ol", null, skill.relations.map((relation) => {
474
487
  const sourceFile = skill.files.find((file) => file.path === relation.source);
475
488
  return (h("li", { className: sourceFile?.reachable ? "" : "unreachable" },
@@ -479,27 +492,29 @@ function SkillRelations({ skill }) {
479
492
  h("code", { title: relation.target }, relation.target)),
480
493
  h("span", { className: "skill-reading-context" }, relation.contexts.map((context) => (h("span", null,
481
494
  context.section,
482
- context.line ? ` · 第 ${context.line} 行` : "",
483
- context.linkText !== relation.label ? ` · “${context.linkText}”` : "",
484
- !sourceFile?.reachable ? " · 入口不可达" : ""))))));
495
+ context.line ? ` · line ${context.line}` : "",
496
+ context.linkText !== relation.label ? ` · "${context.linkText}"` : "",
497
+ !sourceFile?.reachable ? " · entry unreachable" : ""))))));
485
498
  }))))) : (h("div", { className: "skill-reading-empty" },
486
- h("strong", null, "\u6CA1\u6709\u8DE8\u6587\u4EF6\u9605\u8BFB\u8DEF\u5F84"),
499
+ h("strong", null, "No cross-file reading paths"),
487
500
  h("p", null,
488
- "Agent \u8FDB\u5165\u540E\u53EA\u4F1A\u8BFB\u53D6 ",
501
+ "Once the Agent enters, it only reads ",
489
502
  h("code", null, "SKILL.md"),
490
- "\u3002\u5982\u679C\u8FD8\u6709\u5FC5\u987B\u9605\u8BFB\u7684\u53C2\u8003\u6750\u6599\uFF0C\u9700\u8981\u4ECE\u76F8\u5173\u7AE0\u8282\u7528 Markdown \u94FE\u63A5\u660E\u786E\u5F15\u51FA\u3002"))),
491
- unreferencedFiles.length > 0 || missingNodes.length > 0 ? (h("aside", { className: "skill-reading-issues", "aria-label": "\u4FE1\u606F\u7ED3\u6784\u95EE\u9898" },
492
- h("strong", null, "\u9700\u8981\u7559\u610F"),
503
+ ". If other reference material must be read, link it explicitly from the relevant sections with Markdown links."))),
504
+ unreferencedFiles.length > 0 || missingNodes.length > 0 ? (h("aside", { className: "skill-reading-issues", "aria-label": "Information structure issues" },
505
+ h("strong", null, "Needs attention"),
493
506
  unreferencedFiles.length > 0 ? (h("p", null,
494
- "\u5165\u53E3\u65E0\u6CD5\u5230\u8FBE\uFF1A",
507
+ "Unreachable from the entry:",
495
508
  unreferencedFiles.map((file) => (h("code", null, file.path))))) : null,
496
509
  missingNodes.length > 0 ? (h("p", null,
497
- "\u94FE\u63A5\u76EE\u6807\u4E0D\u5B58\u5728\uFF1A",
510
+ "Missing link targets:",
498
511
  missingNodes.map((node) => (h("code", null, node.path))))) : null)) : null,
499
512
  hasRelations ? (h("div", { className: "skill-graph-status", "data-map-status": true, "aria-live": "polite" },
500
- "\u70B9\u51FB\u56FE\u4E2D\u7684\u8282\u70B9\u67E5\u770B\u5B83\u5728\u9605\u8BFB\u94FE\u8DEF\u4E2D\u7684\u4F5C\u7528",
501
- externalNodes.length > 0 ? `;当前有 ${externalNodes.length} 个 Skill 外部目标` : "",
502
- "\u3002")) : null));
513
+ "Select a node in the graph to see its role in the reading chain",
514
+ externalNodes.length > 0
515
+ ? `; ${externalNodes.length} targets outside the Skill currently`
516
+ : "",
517
+ ".")) : null));
503
518
  }
504
519
  const SKILL_BROWSER_SCRIPT = `(() => {
505
520
  document.querySelectorAll('[data-skill-browser]').forEach((browser) => {
@@ -585,42 +600,42 @@ const SKILL_BROWSER_SCRIPT = `(() => {
585
600
  const command = copyButton.dataset.copyCommand;
586
601
  if (!command) return;
587
602
  await navigator.clipboard.writeText(command);
588
- copyButton.textContent = '已复制';
589
- window.setTimeout(() => { copyButton.textContent = '复制'; }, 1400);
603
+ copyButton.textContent = 'Copied';
604
+ window.setTimeout(() => { copyButton.textContent = 'Copy'; }, 1400);
590
605
  });
591
606
  });
592
607
  })();`;
593
- export function skillsIndexPage(skills, theme = blogTheme, site = DEFAULT_SITE) {
608
+ export function skillsIndexPage(skills, theme = blogTheme, site = DEFAULT_SITE, stylesheet = DEFAULT_THEME_STYLESHEET) {
594
609
  const publicSkills = skills.filter((skill) => skill.scope === "public");
595
610
  const projectSkills = skills.filter((skill) => skill.scope === "project");
596
611
  return pageHtml({
597
612
  site,
598
613
  theme,
599
614
  title: "Skills",
600
- description: "可以交给 Agent 使用的方法与工作约定。",
615
+ description: "Methods and working conventions that can be handed to an Agent.",
601
616
  activePath: "/skills/",
602
- styleHref: "../blog-theme.css",
617
+ styleHref: styleHref(1, stylesheet),
603
618
  children: (h("main", { className: "skills-index" },
604
619
  h("header", { className: "skills-head" },
605
620
  h("span", { className: "skill-path" }, "~/skills"),
606
621
  h("h1", null, "Skills"),
607
- h("p", null, "\u516C\u5F00\u5206\u4EAB\u3001\u53EF\u4EE5\u76F4\u63A5\u4EA4\u7ED9 Agent \u4F7F\u7528\u7684\u65B9\u6CD5\u3002")),
622
+ h("p", null, "Methods shared publicly and usable directly by an Agent.")),
608
623
  h("section", { className: "skill-group", "aria-labelledby": "public-skills-heading" },
609
624
  h("header", { className: "skill-group-head" },
610
- h("h2", { id: "public-skills-heading" }, "\u516C\u5F00 Skills"),
611
- h("span", null, "\u516C\u5F00\u5185\u5BB9")),
612
- publicSkills.length > 0 ? (h("div", { className: "skill-list" }, publicSkills.map((skill) => (h(SkillCard, { skill: skill, site: site }))))) : (h("p", { className: "skill-group-empty" }, "\u516C\u5F00 Skills \u76EE\u524D\u8FD8\u6CA1\u6709\u5185\u5BB9\u3002"))),
625
+ h("h2", { id: "public-skills-heading" }, "Public Skills"),
626
+ h("span", null, "Public content")),
627
+ publicSkills.length > 0 ? (h("div", { className: "skill-list" }, publicSkills.map((skill) => (h(SkillCard, { skill: skill, site: site }))))) : (h("p", { className: "skill-group-empty" }, "Public Skills have no content yet."))),
613
628
  projectSkills.length > 0 ? (h("section", { className: "skill-group project-skills", "aria-labelledby": "project-skills-heading" },
614
629
  h("header", { className: "skill-group-head" },
615
630
  h("div", null,
616
- h("h2", { id: "project-skills-heading" }, "\u9879\u76EE Skills"),
617
- h("small", null, "\u672C\u9879\u76EE\u5185\u90E8\u4F7F\u7528\u7684\u5F00\u53D1\u4E0E\u5185\u5BB9\u5DE5\u4F5C\u6D41")),
631
+ h("h2", { id: "project-skills-heading" }, "Project Skills"),
632
+ h("small", null, "Development and content workflows used inside this project")),
618
633
  h("span", null, ".agents/skills/")),
619
634
  h("div", { className: "skill-list" }, projectSkills.map((skill) => (h(SkillCard, { skill: skill, site: site })))))) : null,
620
- h("p", { className: "skills-prototype-note" }, "\u5F15\u7528\u5173\u7CFB\u56FE\u4ECE SKILL.md \u51FA\u53D1\uFF0C\u6839\u636E Markdown \u94FE\u63A5\u5728\u6784\u5EFA\u65F6\u81EA\u52A8\u751F\u6210\u3002"))),
635
+ h("p", { className: "skills-prototype-note" }, "The reference graph starts from SKILL.md and is generated at build time from Markdown links."))),
621
636
  });
622
637
  }
623
- export function skillPage(skill, theme = blogTheme, site = DEFAULT_SITE) {
638
+ export function skillPage(skill, theme = blogTheme, site = DEFAULT_SITE, stylesheet = DEFAULT_THEME_STYLESHEET) {
624
639
  const firstFile = skill.files[0];
625
640
  const command = skill.scope === "public" ? skillInstallCommand(site, skill) : skill.sourceBasePath;
626
641
  return pageHtml({
@@ -629,20 +644,22 @@ export function skillPage(skill, theme = blogTheme, site = DEFAULT_SITE) {
629
644
  title: skill.name,
630
645
  description: skill.description,
631
646
  activePath: "/skills/",
632
- styleHref: skill.scope === "public" ? "../../blog-theme.css" : "../../../blog-theme.css",
647
+ styleHref: skill.scope === "public"
648
+ ? styleHref(1 + slugDepth(skill.slug), stylesheet)
649
+ : styleHref(2 + slugDepth(skill.slug), stylesheet),
633
650
  children: (h("main", { className: "skill-page" },
634
- h("a", { className: "skill-back", href: "/skills/" }, "\u2190 \u6240\u6709 Skills"),
651
+ h("a", { className: "skill-back", href: "/skills/" }, "\u2190 All Skills"),
635
652
  h("header", { className: "skill-browser-head" },
636
653
  h("span", { className: "skill-command" },
637
- skill.scope === "project" ? (h("span", { className: "skill-scope-badge" }, "\u9879\u76EE Skill \u00B7 \u5185\u90E8")) : null,
654
+ skill.scope === "project" ? (h("span", { className: "skill-scope-badge" }, "Project Skill \u00B7 Internal")) : null,
638
655
  h("code", null, command)),
639
656
  h("span", { className: "skill-browser-actions" },
640
- h("button", { type: "button", "data-copy-command": command }, skill.scope === "public" ? "复制" : "复制路径"),
657
+ h("button", { type: "button", "data-copy-command": command }, skill.scope === "public" ? "Copy" : "Copy path"),
641
658
  site.repositorySourceUrl ? (h("a", { href: `${site.repositorySourceUrl}/${skill.sourceBasePath}`, target: "_blank", rel: "noopener noreferrer" }, "GitHub \u2197")) : null)),
642
659
  h("div", { className: "skill-stage", "data-skill-browser": true },
643
- h("nav", { className: "skill-view-tabs", "aria-label": "Skill \u6D4F\u89C8\u65B9\u5F0F" },
644
- h("button", { type: "button", "data-skill-view": "files", "aria-pressed": "true" }, "\u6587\u4EF6\u6D4F\u89C8"),
645
- h("button", { type: "button", "data-skill-view": "relations", "aria-pressed": "false" }, "\u5F15\u7528\u5173\u7CFB\u56FE")),
660
+ h("nav", { className: "skill-view-tabs", "aria-label": "Skill view mode" },
661
+ h("button", { type: "button", "data-skill-view": "files", "aria-pressed": "true" }, "File browser"),
662
+ h("button", { type: "button", "data-skill-view": "relations", "aria-pressed": "false" }, "Reference graph")),
646
663
  h("div", { className: "skill-browser", "data-skill-view-panel": "files" },
647
664
  h(SkillFileTree, { skill: skill, activePath: firstFile.path }),
648
665
  h("section", { className: "skill-viewer", "aria-live": "polite" },
@@ -656,64 +673,85 @@ export function skillPage(skill, theme = blogTheme, site = DEFAULT_SITE) {
656
673
  h("script", { dangerouslySetInnerHTML: { __html: SKILL_BROWSER_SCRIPT } }))),
657
674
  });
658
675
  }
659
- export function agentMarkdown(posts, thoughts, site = DEFAULT_SITE) {
676
+ export function agentMarkdown(posts, thoughts, site = DEFAULT_SITE, pages) {
677
+ const siteMap = pages
678
+ ? pages.map((page) => `- [${page.title}](${sitePageUrl(page.path)})`)
679
+ : [
680
+ "- [Home](/)",
681
+ "- [Posts](/posts/)",
682
+ "- [Thoughts](/thoughts/)",
683
+ "- [Skills](/skills/) (browse files and the auto-generated Markdown reference graph)",
684
+ "- [About](/about/)",
685
+ ];
660
686
  const lines = [
661
687
  `# ${site.name}`,
662
688
  "",
663
- "> 给 Agent 读取的站点索引与内容约定。",
689
+ "> Site index and content conventions for Agents to read.",
664
690
  "",
665
691
  "## Site map",
666
692
  "",
667
- "- [首页](/)",
668
- "- [文章](/posts/)",
669
- "- [想法](/thoughts/)",
670
- "- [Skills](/skills/)(可浏览文件与自动生成的 Markdown 引用关系)",
671
- "- [关于](/about/)",
693
+ ...siteMap,
672
694
  "",
673
695
  "## Content model",
674
696
  "",
675
- `- \`${site.contentDirectory}/thoughts/\` 保存经过剪裁、可直接展示的短想法。`,
676
- `- \`${site.contentDirectory}/posts/\` 保存完整文章;文件名或路径组件以 \`_\` 开头的文件是 draft,不公开、不渲染。`,
677
- "- 内容文件支持 `.md` 和 `.mdx`,开头可使用 YAML Frontmatter。",
678
- "- 目前 Frontmatter 没有强制字段;`title`、`date`、`created_at`、`category`、`tags` 会被站点识别。",
679
- "",
680
- "## Public content",
681
- "",
682
- `公开文章:${posts.length} 篇。`,
683
- `公开想法:${thoughts.length} 条。`,
684
- "",
685
- "### Posts",
686
- "",
687
- ...posts.map((post) => `- [${post.title}](${itemUrl(post)}) — ${post.dateKey}`),
688
- "",
689
- "### Thoughts",
690
- "",
691
- ...thoughts.map((thought) => `- [${thought.title}](${itemUrl(thought)}) — ${thought.dateKey}`),
697
+ `- \`${site.contentDirectory}/thoughts/\` stores trimmed, directly presentable short thoughts.`,
698
+ `- \`${site.contentDirectory}/posts/\` stores full posts; files whose name or path segment starts with \`_\` are drafts and are not published or rendered.`,
699
+ "- Content files support `.md` and `.mdx`, and may start with YAML frontmatter.",
700
+ "- Frontmatter currently has no required fields; `title`, `date`, `created_at`, `category` and `tags` are recognized by the site.",
692
701
  "",
702
+ // A consumer that owns the site map owns the routes of its content too.
703
+ ...(pages
704
+ ? []
705
+ : [
706
+ "## Public content",
707
+ "",
708
+ `Public posts: ${posts.length}.`,
709
+ `Public thoughts: ${thoughts.length}.`,
710
+ "",
711
+ "### Posts",
712
+ "",
713
+ ...posts.map((post) => `- [${post.title}](${itemUrl(post)}) — ${post.dateKey}`),
714
+ "",
715
+ "### Thoughts",
716
+ "",
717
+ ...thoughts.map((thought) => `- [${thought.title}](${itemUrl(thought)}) — ${thought.dateKey}`),
718
+ "",
719
+ ]),
693
720
  ];
694
721
  return lines.join("\n");
695
722
  }
696
- export function llmText(posts, thoughts, site = DEFAULT_SITE) {
723
+ export function llmText(posts, thoughts, site = DEFAULT_SITE, pages) {
724
+ const routes = pages
725
+ ? pages.map((page) => `- ${page.title}: ${sitePageUrl(page.path)}`)
726
+ : [
727
+ "- Home: /",
728
+ "- Posts: /posts/",
729
+ "- Thoughts: /thoughts/",
730
+ "- Skills: /skills/",
731
+ "- About: /about/",
732
+ ];
697
733
  return [
698
734
  `# ${site.name}`,
699
735
  `> ${site.description}`,
700
736
  "",
701
737
  "## Pages",
702
- "- 首页: /",
703
- "- 文章: /posts/",
704
- "- 想法: /thoughts/",
705
- "- Skills: /skills/",
706
- "- 关于: /about/",
738
+ ...routes,
707
739
  "- Agent Markdown: /agents.md",
708
740
  "",
709
- "## Posts",
710
- ...posts.map((post) => `- ${post.title}: ${itemUrl(post)}`),
711
- "",
712
- "## Thoughts",
713
- ...thoughts.map((thought) => `- ${thought.title}: ${itemUrl(thought)}`),
714
- "",
741
+ // A consumer that owns the site map owns the routes of its content too;
742
+ // listing the content under routes the site does not publish would be wrong.
743
+ ...(pages
744
+ ? []
745
+ : [
746
+ "## Posts",
747
+ ...posts.map((post) => `- ${post.title}: ${itemUrl(post)}`),
748
+ "",
749
+ "## Thoughts",
750
+ ...thoughts.map((thought) => `- ${thought.title}: ${itemUrl(thought)}`),
751
+ "",
752
+ ]),
715
753
  "## Rules",
716
- "- 只把公开构建产物视为可展示内容。",
717
- "- posts 中以 _ 开头的 draft 不公开、不渲染。",
754
+ "- Only the public build output is considered presentable content.",
755
+ "- Drafts whose names start with _ under posts are not published or rendered.",
718
756
  ].join("\n");
719
757
  }
@@ -44,7 +44,7 @@ function markdownLinks(source) {
44
44
  }
45
45
  if (url) {
46
46
  links.push({
47
- label: nodeText(node).replaceAll(/\s+/g, " ").trim() || "打开",
47
+ label: nodeText(node).replaceAll(/\s+/g, " ").trim() || "Open",
48
48
  url,
49
49
  section,
50
50
  line: node.position?.start?.line,
@@ -53,7 +53,7 @@ function markdownLinks(source) {
53
53
  }
54
54
  node.children?.forEach((child) => collectLinks(child, section));
55
55
  };
56
- let section = "文档开头";
56
+ let section = "Beginning of document";
57
57
  for (const node of tree.children ?? []) {
58
58
  if (node.type === "heading") {
59
59
  section = nodeText(node).replaceAll(/\s+/g, " ").trim() || section;
@@ -79,7 +79,7 @@ function inferredDescription(body) {
79
79
  .split(/\r?\n\s*\r?\n/)
80
80
  .map(plainText)
81
81
  .find(Boolean);
82
- return (paragraph ?? "一个可以交给 Agent 使用的 Skill。").slice(0, 180);
82
+ return (paragraph ?? "A skill that can be handed to an Agent.").slice(0, 180);
83
83
  }
84
84
  async function collectFiles(directory) {
85
85
  const entries = await readdir(directory, { withFileTypes: true });
@@ -107,16 +107,16 @@ function isExternalUrl(url) {
107
107
  function externalDetail(url) {
108
108
  try {
109
109
  const parsed = new URL(url);
110
- return `Agent 会离开 Skill,继续读取 ${parsed.hostname}。`;
110
+ return `The Agent leaves the Skill and continues to ${parsed.hostname}.`;
111
111
  }
112
112
  catch {
113
- return "指向 Skill 目录之外的输入或资料。";
113
+ return "Points to input or material outside the Skill directory.";
114
114
  }
115
115
  }
116
116
  function relationLabel(label, target) {
117
117
  const targetName = target.endsWith("/") ? target : (target.split("/").at(-1) ?? target);
118
118
  if (label === target || label === targetName)
119
- return "引用";
119
+ return "Reference";
120
120
  return label.length > 16 ? `${label.slice(0, 15)}…` : label;
121
121
  }
122
122
  function compactGraphLabel(label) {
@@ -209,7 +209,7 @@ async function loadSkill(directory, sourceBasePath, scope) {
209
209
  target = link.url;
210
210
  externalNodes.set(target, {
211
211
  path: target,
212
- role: "外部链接",
212
+ role: "External link",
213
213
  detail: externalDetail(target),
214
214
  kind: "external",
215
215
  });
@@ -223,15 +223,15 @@ async function loadSkill(directory, sourceBasePath, scope) {
223
223
  externalNodes.set(target, {
224
224
  path: target,
225
225
  role: resolved.kind === "asset"
226
- ? "本地资源"
226
+ ? "Local asset"
227
227
  : resolved.kind === "outside"
228
- ? "Skill 外部"
229
- : "缺失引用",
228
+ ? "Outside the Skill"
229
+ : "Missing reference",
230
230
  detail: resolved.kind === "asset"
231
- ? "Markdown 明确链接到这个非文本资源。"
231
+ ? "The Markdown explicitly links to this non-text asset."
232
232
  : resolved.kind === "outside"
233
- ? "链接指向 Skill 目录之外,Agent 会离开当前 Skill 继续读取。"
234
- : "Markdown 中存在此链接,但 Skill 目录内没有对应文件。",
233
+ ? "The link points outside the Skill directory; the Agent leaves the current Skill and continues reading."
234
+ : "The link exists in the Markdown, but there is no corresponding file in the Skill directory.",
235
235
  kind: resolved.kind === "outside" ? "external" : resolved.kind,
236
236
  });
237
237
  }
@@ -253,7 +253,7 @@ async function loadSkill(directory, sourceBasePath, scope) {
253
253
  return {
254
254
  source,
255
255
  target,
256
- label: compactGraphLabel(sections[0] === "文档开头" ? labels.join(" / ") : sections.join(" / ")),
256
+ label: compactGraphLabel(sections[0] === "Beginning of document" ? labels.join(" / ") : sections.join(" / ")),
257
257
  contexts,
258
258
  };
259
259
  });
@@ -272,12 +272,12 @@ async function loadSkill(directory, sourceBasePath, scope) {
272
272
  const reachable = depth !== undefined;
273
273
  return {
274
274
  path,
275
- role: path === "SKILL.md" ? "入口" : reachable ? `${depth} 级引用` : "未被引用",
275
+ role: path === "SKILL.md" ? "Entry" : reachable ? `${depth}-level reference` : "Not referenced",
276
276
  detail: path === "SKILL.md"
277
- ? "Agent 从这里开始读取这个 Skill。"
277
+ ? "The Agent starts reading this Skill here."
278
278
  : reachable
279
- ? `从 SKILL.md 经过 ${depth} 次链接可以读到这里。`
280
- : "不在从 SKILL.md 出发的链接链路中,Agent 不会自然读到这里。",
279
+ ? `Reachable from SKILL.md through ${depth} links.`
280
+ : "Not on the link chain from SKILL.md, so the Agent will not naturally read this.",
281
281
  content: documents.get(path).body,
282
282
  reachable,
283
283
  depth,
package/dist/src/theme.js CHANGED
@@ -71,7 +71,7 @@ const styles = css `
71
71
  text-rendering: optimizeLegibility;
72
72
  }
73
73
 
74
- /* 顶栏:保留秩序,但不再像刊头。 */
74
+ /* Top bar: keep the order, but stop looking like a masthead. */
75
75
  header.site {
76
76
  display: flex;
77
77
  align-items: center;
@@ -255,7 +255,7 @@ const styles = css `
255
255
  pointer-events: none;
256
256
  }
257
257
 
258
- /* 首页内容流:日期只是呼吸点,不再承担机械时间轴。 */
258
+ /* Home feed: dates are breathing points, not a mechanical timeline. */
259
259
  .feed {
260
260
  position: relative;
261
261
  padding: 8px 0 48px;
@@ -385,7 +385,7 @@ const styles = css `
385
385
  letter-spacing: 0.06em;
386
386
  }
387
387
 
388
- /* 两类内容都像桌上的纸,但保留各自的重量。 */
388
+ /* Both content kinds look like paper on a desk, but keep their own weight. */
389
389
  .post-card,
390
390
  .thought {
391
391
  background-color: ${blogTokens.card};
@@ -548,7 +548,7 @@ const styles = css `
548
548
  outline: none;
549
549
  }
550
550
 
551
- /* 过滤器是轻量控制,不再像出版物图例。 */
551
+ /* Filters are lightweight controls, not publication legends. */
552
552
  .tag-filter {
553
553
  display: flex;
554
554
  flex-wrap: wrap;
@@ -598,7 +598,7 @@ const styles = css `
598
598
  transform: translateY(1px);
599
599
  }
600
600
 
601
- /* 文章与想法详情。 */
601
+ /* Post and thought detail. */
602
602
  .post-head {
603
603
  padding: 68px 0 37px;
604
604
  border-bottom: 1px solid ${blogTokens.line};
@@ -892,7 +892,7 @@ const styles = css `
892
892
  text-align: right;
893
893
  }
894
894
 
895
- /* 索引页与关于页。 */
895
+ /* Index and about pages. */
896
896
  .listing,
897
897
  .about,
898
898
  .skills-index,
@@ -1009,7 +1009,7 @@ const styles = css `
1009
1009
  letter-spacing: 0.06em;
1010
1010
  }
1011
1011
 
1012
- /* Skills:用目录和文档的语义区分内容,不把它做成产品陈列。 */
1012
+ /* Skills: separate content by catalog and document semantics instead of displaying it as a product. */
1013
1013
  .skills-head {
1014
1014
  margin-bottom: 33px;
1015
1015
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "meowpress",
3
- "version": "0.1.1-alpha.0",
3
+ "version": "0.2.0-alpha.0",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -37,11 +37,11 @@
37
37
  "yaml": "^2.9.0"
38
38
  },
39
39
  "peerDependencies": {
40
- "meowcss": "^0.1.0 || ^0.1.1-alpha.0",
41
- "meowjsx": "^0.1.0 || ^0.1.1-alpha.0"
40
+ "meowcss": "^0.2.0-alpha.0",
41
+ "meowjsx": "^0.2.0-alpha.0"
42
42
  },
43
43
  "devDependencies": {
44
- "meowjsx": "0.1.1-alpha.0",
45
- "meowcss": "0.1.1-alpha.0"
44
+ "meowjsx": "0.2.0-alpha.0",
45
+ "meowcss": "0.2.0-alpha.0"
46
46
  }
47
47
  }