meowpress 0.1.1-alpha.1 → 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
@@ -3,7 +3,7 @@
3
3
  `meowpress` provides content reading, page rendering, themes and the build pipeline for static sites.
4
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` has been published and accepted from the registry. The current `0.1.1-alpha.1` candidate updates the English site and CLI defaults and is not yet published. CSS and JSX are both required peers, allowing `^0.1.0 || ^0.1.1-alpha.0`, keeping the stable compatibility range and accepting this round's JSX/CSS alphas. 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. Actual out-of-package builds and theme identity acceptance are bound to exact tgz files, and peer contracts are not bypassed through overrides. Publishing still requires separate explicit authorization.
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
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
9
  and does not depend on Assistant, WorkCell, Deno or the content and runtime state of a specific Blog. Style generation comes from `meowcss`,
@@ -25,6 +25,37 @@ export default meowSite({
25
25
  });
26
26
  ```
27
27
 
28
+ ## Pages a consumer owns
29
+
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.
34
+
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
+
28
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.
29
60
  The whole theme can be replaced with `theme: defineTheme(...)`; all static pages output that theme's root attributes and default options.
30
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,
@@ -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")
@@ -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,7 +4,18 @@ 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),
@@ -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,14 +264,14 @@ 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
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"),
@@ -293,7 +304,7 @@ function RelatedThoughts({ item, thoughts, }) {
293
304
  h("h2", null, "Thoughts from the same day"),
294
305
  related.map((thought) => (h(ThoughtCard, { item: thought, linked: true })))));
295
306
  }
296
- export function postPage(item, posts, thoughts, theme = blogTheme, site = DEFAULT_SITE) {
307
+ export function postPage(item, posts, thoughts, theme = blogTheme, site = DEFAULT_SITE, stylesheet = DEFAULT_THEME_STYLESHEET) {
297
308
  const category = typeof item.metadata.category === "string" ? item.metadata.category : "Post";
298
309
  return pageHtml({
299
310
  site,
@@ -301,7 +312,7 @@ export function postPage(item, posts, thoughts, theme = blogTheme, site = DEFAUL
301
312
  title: item.title,
302
313
  description: item.excerpt,
303
314
  activePath: "/posts/",
304
- styleHref: "../../blog-theme.css",
315
+ styleHref: styleHref(1 + slugDepth(item.slug), stylesheet),
305
316
  children: (h("main", null,
306
317
  h("article", null,
307
318
  h("header", { className: "post-head" },
@@ -319,14 +330,14 @@ export function postPage(item, posts, thoughts, theme = blogTheme, site = DEFAUL
319
330
  h(PostNavigation, { item: item, posts: posts })))),
320
331
  });
321
332
  }
322
- export function thoughtPage(item, theme = blogTheme, site = DEFAULT_SITE) {
333
+ export function thoughtPage(item, theme = blogTheme, site = DEFAULT_SITE, stylesheet = DEFAULT_THEME_STYLESHEET) {
323
334
  return pageHtml({
324
335
  site,
325
336
  theme,
326
337
  title: item.title,
327
338
  description: item.excerpt,
328
339
  activePath: "/thoughts/",
329
- styleHref: "../../blog-theme.css",
340
+ styleHref: styleHref(1 + slugDepth(item.slug), stylesheet),
330
341
  children: (h("main", null,
331
342
  h("article", null,
332
343
  h("header", { className: "post-head" },
@@ -338,14 +349,14 @@ export function thoughtPage(item, theme = blogTheme, site = DEFAULT_SITE) {
338
349
  h("div", { className: "prose", dangerouslySetInnerHTML: { __html: item.html } })))),
339
350
  });
340
351
  }
341
- export function aboutPage(theme = blogTheme, site = DEFAULT_SITE) {
352
+ export function aboutPage(theme = blogTheme, site = DEFAULT_SITE, stylesheet = DEFAULT_THEME_STYLESHEET) {
342
353
  const about = site.about;
343
354
  return pageHtml({
344
355
  site,
345
356
  theme,
346
357
  title: "About",
347
358
  activePath: "/about/",
348
- styleHref: "../blog-theme.css",
359
+ styleHref: styleHref(1, stylesheet),
349
360
  children: (h("main", { className: "about" },
350
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"))),
351
362
  about ? (h(Fragment, null,
@@ -594,7 +605,7 @@ const SKILL_BROWSER_SCRIPT = `(() => {
594
605
  });
595
606
  });
596
607
  })();`;
597
- export function skillsIndexPage(skills, theme = blogTheme, site = DEFAULT_SITE) {
608
+ export function skillsIndexPage(skills, theme = blogTheme, site = DEFAULT_SITE, stylesheet = DEFAULT_THEME_STYLESHEET) {
598
609
  const publicSkills = skills.filter((skill) => skill.scope === "public");
599
610
  const projectSkills = skills.filter((skill) => skill.scope === "project");
600
611
  return pageHtml({
@@ -603,7 +614,7 @@ export function skillsIndexPage(skills, theme = blogTheme, site = DEFAULT_SITE)
603
614
  title: "Skills",
604
615
  description: "Methods and working conventions that can be handed to an Agent.",
605
616
  activePath: "/skills/",
606
- styleHref: "../blog-theme.css",
617
+ styleHref: styleHref(1, stylesheet),
607
618
  children: (h("main", { className: "skills-index" },
608
619
  h("header", { className: "skills-head" },
609
620
  h("span", { className: "skill-path" }, "~/skills"),
@@ -624,7 +635,7 @@ export function skillsIndexPage(skills, theme = blogTheme, site = DEFAULT_SITE)
624
635
  h("p", { className: "skills-prototype-note" }, "The reference graph starts from SKILL.md and is generated at build time from Markdown links."))),
625
636
  });
626
637
  }
627
- export function skillPage(skill, theme = blogTheme, site = DEFAULT_SITE) {
638
+ export function skillPage(skill, theme = blogTheme, site = DEFAULT_SITE, stylesheet = DEFAULT_THEME_STYLESHEET) {
628
639
  const firstFile = skill.files[0];
629
640
  const command = skill.scope === "public" ? skillInstallCommand(site, skill) : skill.sourceBasePath;
630
641
  return pageHtml({
@@ -633,7 +644,9 @@ export function skillPage(skill, theme = blogTheme, site = DEFAULT_SITE) {
633
644
  title: skill.name,
634
645
  description: skill.description,
635
646
  activePath: "/skills/",
636
- 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),
637
650
  children: (h("main", { className: "skill-page" },
638
651
  h("a", { className: "skill-back", href: "/skills/" }, "\u2190 All Skills"),
639
652
  h("header", { className: "skill-browser-head" },
@@ -660,7 +673,16 @@ export function skillPage(skill, theme = blogTheme, site = DEFAULT_SITE) {
660
673
  h("script", { dangerouslySetInnerHTML: { __html: SKILL_BROWSER_SCRIPT } }))),
661
674
  });
662
675
  }
663
- 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
+ ];
664
686
  const lines = [
665
687
  `# ${site.name}`,
666
688
  "",
@@ -668,11 +690,7 @@ export function agentMarkdown(posts, thoughts, site = DEFAULT_SITE) {
668
690
  "",
669
691
  "## Site map",
670
692
  "",
671
- "- [Home](/)",
672
- "- [Posts](/posts/)",
673
- "- [Thoughts](/thoughts/)",
674
- "- [Skills](/skills/) (browse files and the auto-generated Markdown reference graph)",
675
- "- [About](/about/)",
693
+ ...siteMap,
676
694
  "",
677
695
  "## Content model",
678
696
  "",
@@ -681,41 +699,57 @@ export function agentMarkdown(posts, thoughts, site = DEFAULT_SITE) {
681
699
  "- Content files support `.md` and `.mdx`, and may start with YAML frontmatter.",
682
700
  "- Frontmatter currently has no required fields; `title`, `date`, `created_at`, `category` and `tags` are recognized by the site.",
683
701
  "",
684
- "## Public content",
685
- "",
686
- `Public posts: ${posts.length}.`,
687
- `Public thoughts: ${thoughts.length}.`,
688
- "",
689
- "### Posts",
690
- "",
691
- ...posts.map((post) => `- [${post.title}](${itemUrl(post)}) — ${post.dateKey}`),
692
- "",
693
- "### Thoughts",
694
- "",
695
- ...thoughts.map((thought) => `- [${thought.title}](${itemUrl(thought)}) — ${thought.dateKey}`),
696
- "",
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
+ ]),
697
720
  ];
698
721
  return lines.join("\n");
699
722
  }
700
- 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
+ ];
701
733
  return [
702
734
  `# ${site.name}`,
703
735
  `> ${site.description}`,
704
736
  "",
705
737
  "## Pages",
706
- "- Home: /",
707
- "- Posts: /posts/",
708
- "- Thoughts: /thoughts/",
709
- "- Skills: /skills/",
710
- "- About: /about/",
738
+ ...routes,
711
739
  "- Agent Markdown: /agents.md",
712
740
  "",
713
- "## Posts",
714
- ...posts.map((post) => `- ${post.title}: ${itemUrl(post)}`),
715
- "",
716
- "## Thoughts",
717
- ...thoughts.map((thought) => `- ${thought.title}: ${itemUrl(thought)}`),
718
- "",
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
+ ]),
719
753
  "## Rules",
720
754
  "- Only the public build output is considered presentable content.",
721
755
  "- Drafts whose names start with _ under posts are not published or rendered.",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "meowpress",
3
- "version": "0.1.1-alpha.1",
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
  }