meowpress 0.3.0-alpha.0 → 0.3.0-alpha.2

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
@@ -50,6 +50,14 @@ export default meowSite({
50
50
 
51
51
  - `pages` is a provider of complete HTML documents. A path that resolves above the site root is
52
52
  refused rather than clamped.
53
+ - A page's optional `markdown` is the same page for agents. It is published next to the route
54
+ (`components/button.md` for `/components/button/`, `index.md` for the root), and `llms.txt`,
55
+ `llm.txt` and `agents.md` link it instead of the HTML. `llms.txt` follows the
56
+ [llms.txt](https://llmstxt.org/) shape: the site summary, then one section of links per page
57
+ `group` with each page's `description`. `llms-full.txt` holds every Markdown copy in one file.
58
+ - `agentGuide` is Markdown that opens `llms.txt` and `llms-full.txt` after the summary: what an
59
+ agent must know before following any link, such as how to install and set up the documented
60
+ packages.
53
61
  - `mdxComponents` adds named components to the MDX content pages; the built-in `Callout` stays
54
62
  available.
55
63
  - `themeStylesheet` names the theme stylesheet the build writes, so a consumer page can link it. It
@@ -58,9 +66,17 @@ export default meowSite({
58
66
 
59
67
  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
68
  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.
69
+ The build writes `theme.cssText` to a temporary CSS file, then lets Bun handle the other assets, and finally delivers through a stylesheet link,
70
+ so the theme applies without client-side JavaScript.
71
+
72
+ `fonts` lists the web fonts the theme names, as Fontsource variable packages
73
+ (`{ family, package, styles }`, the same shape as Meowtail's `journalFonts`). The build publishes
74
+ each package's weight-axis `woff2` files into `fonts/` with a content hash, puts their `@font-face`
75
+ rules first in the theme stylesheet with each subset's `unicode-range`, and copies each license to
76
+ `licenses/<name>-OFL.txt`. A browser fetches only the files a page's text and families use, and
77
+ nothing for a family the page does not use, so a site with a theme switch pays only for the active
78
+ theme. The default Blog theme defaults to `blogFonts` (Geist); any other theme defaults to none.
79
+ Install the listed packages in the site; nothing depends on an external font service.
64
80
  Default illustrations use in-package vectors and CSS and require no business images. The `hero`, `feed` and `about` keys of `site.images`
65
81
  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
82
  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.
@@ -75,8 +91,8 @@ After installing `meowpress` and its `meowcss` and `meowjsx` dependencies, run i
75
91
  bunx --no-install meowpress --config ./site.config.ts
76
92
  ```
77
93
 
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.
94
+ The CLI requires Bun, and the output location is determined by `outputDirectory`. Font packages resolve from the startup directory first, then from
95
+ this package, which carries the default Geist, so consumers are not required to copy source, install workspace-root dependencies or access an external font service.
80
96
 
81
97
  ## Independent verification
82
98
 
@@ -1,9 +1,12 @@
1
1
  import { type ThemeDefinition } from "meowcss";
2
+ import { type SiteFont } from "./fonts.js";
2
3
  import { type SiteIdentity } from "./identity.js";
3
4
  import { type ContentSourceInput, type MdxComponent } from "./content.js";
4
5
  import { type SitePageProvider } from "./pages.js";
6
+ export { sitePageMarkdownFile } from "./pages.js";
5
7
  export type { SitePage, SitePageContext, SitePageProvider } from "./pages.js";
6
8
  export type { ContentItem } from "./content.js";
9
+ export type { SiteFont } from "./fonts.js";
7
10
  export type { SiteIdentity } from "./identity.js";
8
11
  export interface SiteConfig {
9
12
  /** One or more roots containing `posts/`, `thoughts/`, and optional assets. */
@@ -20,10 +23,20 @@ export interface SiteConfig {
20
23
  site?: Partial<SiteIdentity>;
21
24
  /** Consumer-supplied pages; the site writes and indexes them next to its own. */
22
25
  pages?: SitePageProvider;
26
+ /**
27
+ * Markdown that opens `llms.txt` and `llms-full.txt` after the site summary: what an agent must
28
+ * know before following any link, such as how to install and set up what the site documents.
29
+ */
30
+ agentGuide?: string;
23
31
  /** Extra components available to MDX content pages. */
24
32
  mdxComponents?: Record<string, MdxComponent>;
25
33
  /** Stylesheet file name for the theme; defaults to "blog-theme.css". */
26
34
  themeStylesheet?: string;
35
+ /**
36
+ * Web fonts the theme names, published as separate files with their licenses. Defaults to Geist
37
+ * for the default Blog theme and to none for any other theme.
38
+ */
39
+ fonts?: readonly SiteFont[];
27
40
  }
28
41
  export interface ResolvedSiteConfig {
29
42
  content: readonly ContentSourceInput[];
@@ -33,8 +46,10 @@ export interface ResolvedSiteConfig {
33
46
  projectRootDirectory?: string;
34
47
  site: SiteIdentity;
35
48
  pages?: SitePageProvider;
49
+ agentGuide?: string;
36
50
  mdxComponents: Record<string, MdxComponent>;
37
51
  themeStylesheet: string;
52
+ fonts: readonly SiteFont[];
38
53
  }
39
54
  /** Define a site config in the app package without taking on the build pipeline. */
40
55
  export declare function meowSite(config: SiteConfig): ResolvedSiteConfig;
package/dist/src/build.js CHANGED
@@ -1,12 +1,14 @@
1
- import { blogTheme } from "./theme.js";
1
+ import { removeLegacyIsFallbacks } from "meowcss/bundle";
2
+ import { blogFonts, blogTheme } from "./theme.js";
3
+ import { FONT_EXTERNAL, publishFonts } from "./fonts.js";
2
4
  import { DEFAULT_SITE } from "./identity.js";
3
- import { mkdir, rm, writeFile, cp } from "node:fs/promises";
5
+ import { mkdir, readFile, rm, writeFile, cp } from "node:fs/promises";
4
6
  import { isAbsolute, join, relative, resolve } from "node:path";
5
- import { fileURLToPath } from "node:url";
6
7
  import { collectContentAssets, isDraftAsset, loadSection, normalizeContentSources, } from "./content.js";
7
- import { aboutPage, agentMarkdown, DEFAULT_THEME_STYLESHEET, homePage, listPage, llmText, postPage, skillPage, skillsIndexPage, thoughtPage, } from "./site.js";
8
+ import { aboutPage, agentMarkdown, DEFAULT_THEME_STYLESHEET, homePage, listPage, llmFullText, llmText, postPage, skillPage, skillsIndexPage, thoughtPage, } from "./site.js";
8
9
  import { loadSkills } from "./skills.js";
9
- import { sitePageFile } from "./pages.js";
10
+ import { sitePageFile, sitePageMarkdownFile } from "./pages.js";
11
+ export { sitePageMarkdownFile } from "./pages.js";
10
12
  /** Define a site config in the app package without taking on the build pipeline. */
11
13
  export function meowSite(config) {
12
14
  return {
@@ -19,8 +21,10 @@ export function meowSite(config) {
19
21
  : undefined,
20
22
  site: { ...DEFAULT_SITE, ...config.site },
21
23
  pages: config.pages,
24
+ agentGuide: config.agentGuide,
22
25
  mdxComponents: config.mdxComponents ?? {},
23
26
  themeStylesheet: config.themeStylesheet ?? DEFAULT_THEME_STYLESHEET,
27
+ fonts: config.fonts ?? (config.theme === undefined || config.theme === blogTheme ? blogFonts : []),
24
28
  };
25
29
  }
26
30
  async function writePage(generatedDirectory, pathname, html) {
@@ -56,15 +60,21 @@ async function buildStaticHtmlFromEntrypoints(entrypoints, generatedDirectory, o
56
60
  outdir: outputDirectory,
57
61
  target: "browser",
58
62
  minify: true,
63
+ // Published font files stay separate, so subsets load by unicode-range instead of inline.
64
+ external: [FONT_EXTERNAL],
59
65
  });
60
66
  if (!result.success) {
61
67
  console.error(result.logs);
62
68
  throw new Error("Unable to build site static HTML");
63
69
  }
70
+ // Bun writes legacy :-webkit-any()/:-moz-any() copies of every :is() rule; Safari misreads them.
71
+ for (const output of result.outputs)
72
+ if (output.path.endsWith(".css"))
73
+ await writeFile(output.path, removeLegacyIsFallbacks(await readFile(output.path, "utf8")));
64
74
  }
65
75
  /** Consumer pages are written inside the generated directory and never above it. */
66
- function consumerPageFile(generatedDirectory, path) {
67
- const file = sitePageFile(path);
76
+ function consumerPageFile(generatedDirectory, path, fileFor = sitePageFile) {
77
+ const file = fileFor(path);
68
78
  const target = resolve(generatedDirectory, file);
69
79
  const inside = relative(generatedDirectory, target);
70
80
  if (!inside || inside.startsWith("..") || isAbsolute(inside)) {
@@ -79,6 +89,18 @@ async function writeConsumerPages(generatedDirectory, pages) {
79
89
  }
80
90
  return entrypoints;
81
91
  }
92
+ /** Files the build writes itself; a page's Markdown copy may not take their place. */
93
+ const RESERVED_MARKDOWN = new Set(["agents.md"]);
94
+ /** Each page's Markdown copy, written straight into the output next to its route. */
95
+ async function writeConsumerMarkdown(outputDirectory, pages) {
96
+ for (const page of pages) {
97
+ if (page.markdown === undefined)
98
+ continue;
99
+ if (RESERVED_MARKDOWN.has(sitePageMarkdownFile(page.path)))
100
+ throw new Error(`Site page Markdown would replace the agent index: ${page.path}`);
101
+ await writePage(outputDirectory, consumerPageFile(outputDirectory, page.path, sitePageMarkdownFile), page.markdown);
102
+ }
103
+ }
82
104
  async function writeContentPages(generatedDirectory, posts, thoughts, theme, site, stylesheet) {
83
105
  const entrypoints = [];
84
106
  for (const post of posts) {
@@ -133,18 +155,22 @@ export async function buildSite(input) {
133
155
  entrypoints.push(await writePage(generatedDirectory, `${skillPath}/index.html`, skillPage(skill, config.theme, config.site, config.themeStylesheet)));
134
156
  }
135
157
  }
136
- await writeFile(join(generatedDirectory, config.themeStylesheet), config.theme.cssText.replaceAll('"@fontsource-variable/geist"', JSON.stringify(fileURLToPath(import.meta.resolve("@fontsource-variable/geist")))), "utf8");
158
+ // Font faces come first in the one theme stylesheet; the bundled stylesheet sits at the output
159
+ // root, next to the published font directory its rules reference.
160
+ const fontFaces = await publishFonts(config.fonts, config.outputDirectory);
161
+ await writeFile(join(generatedDirectory, config.themeStylesheet), fontFaces ? `${fontFaces}\n${config.theme.cssText}` : config.theme.cssText, "utf8");
137
162
  await stageAssets(generatedDirectory, config.content);
138
163
  await buildStaticHtmlFromEntrypoints(entrypoints, generatedDirectory, config.outputDirectory);
139
- if (config.theme.cssText.includes('"@fontsource-variable/geist"')) {
140
- const licensesDirectory = join(config.outputDirectory, "licenses");
141
- await mkdir(licensesDirectory, { recursive: true });
142
- await cp(fileURLToPath(import.meta.resolve("@fontsource-variable/geist/LICENSE")), join(licensesDirectory, "geist-OFL.txt"));
143
- }
144
164
  await copyPostAssets(config.outputDirectory, config.content);
165
+ if (pages)
166
+ await writeConsumerMarkdown(config.outputDirectory, pages);
167
+ const full = pages ? llmFullText(config.site, pages, config.agentGuide) : undefined;
145
168
  await Promise.all([
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"),
169
+ writeFile(join(config.outputDirectory, "llm.txt"), llmText(posts, thoughts, config.site, pages, config.agentGuide), "utf8"),
170
+ writeFile(join(config.outputDirectory, "llms.txt"), llmText(posts, thoughts, config.site, pages, config.agentGuide), "utf8"),
171
+ ...(full === undefined
172
+ ? []
173
+ : [writeFile(join(config.outputDirectory, "llms-full.txt"), full, "utf8")]),
148
174
  writeFile(join(config.outputDirectory, "agents.md"), agentMarkdown(posts, thoughts, config.site, pages), "utf8"),
149
175
  ]);
150
176
  await rm(generatedDirectory, { recursive: true, force: true });
@@ -0,0 +1,21 @@
1
+ /**
2
+ * A web font a site loads, as a Fontsource variable package: `family` is the name the theme's
3
+ * stacks use, `package` the npm package with its weight-axis files, and `styles` which of them to
4
+ * load (`italic` loads `wght-italic.css`). The same shape as Meowtail's `ThemeFont`, so a theme's
5
+ * manifest (such as `journalFonts`) can be passed as it is.
6
+ */
7
+ export interface SiteFont {
8
+ readonly family: string;
9
+ readonly package: string;
10
+ readonly styles: readonly ("normal" | "italic")[];
11
+ }
12
+ /** Font files are published here, next to the bundled stylesheet at the output root. */
13
+ export declare const FONT_DIRECTORY = "fonts";
14
+ /** The bundler leaves these references alone; the build copies the files itself. */
15
+ export declare const FONT_EXTERNAL = "./fonts/*";
16
+ /**
17
+ * Publish each font's files into `<output>/fonts/` with a content hash and its license into
18
+ * `<output>/licenses/`, and return the `@font-face` rules that reference them. Each subset keeps
19
+ * its `unicode-range`, so a browser fetches only the files a page's text and families use.
20
+ */
21
+ export declare function publishFonts(fonts: readonly SiteFont[], outputDirectory: string): Promise<string>;
@@ -0,0 +1,56 @@
1
+ import { mkdir, readFile, writeFile, cp } from "node:fs/promises";
2
+ import { basename, dirname, join } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ /** Font files are published here, next to the bundled stylesheet at the output root. */
5
+ export const FONT_DIRECTORY = "fonts";
6
+ /** The bundler leaves these references alone; the build copies the files itself. */
7
+ export const FONT_EXTERNAL = `./${FONT_DIRECTORY}/*`;
8
+ /**
9
+ * Resolve a package file from the site first (where a host installs its theme's fonts), then from
10
+ * this package (which carries the default Geist).
11
+ */
12
+ function resolveFontFile(specifier) {
13
+ for (const from of [process.cwd(), dirname(fileURLToPath(import.meta.url))]) {
14
+ try {
15
+ return Bun.resolveSync(specifier, from);
16
+ }
17
+ catch {
18
+ // Try the next root.
19
+ }
20
+ }
21
+ throw new Error(`Cannot resolve the font file ${specifier}; install its package in the site`);
22
+ }
23
+ const contentHash = (bytes) => new Bun.CryptoHasher("sha256").update(bytes).digest("hex").slice(0, 8);
24
+ /**
25
+ * Publish each font's files into `<output>/fonts/` with a content hash and its license into
26
+ * `<output>/licenses/`, and return the `@font-face` rules that reference them. Each subset keeps
27
+ * its `unicode-range`, so a browser fetches only the files a page's text and families use.
28
+ */
29
+ export async function publishFonts(fonts, outputDirectory) {
30
+ if (!fonts.length)
31
+ return "";
32
+ const fontDirectory = join(outputDirectory, FONT_DIRECTORY);
33
+ const licenseDirectory = join(outputDirectory, "licenses");
34
+ await mkdir(fontDirectory, { recursive: true });
35
+ await mkdir(licenseDirectory, { recursive: true });
36
+ const rules = [];
37
+ for (const font of fonts) {
38
+ for (const style of font.styles) {
39
+ const sheet = resolveFontFile(`${font.package}/${style === "italic" ? "wght-italic" : "wght"}.css`);
40
+ const source = await readFile(sheet, "utf8");
41
+ const files = new Map();
42
+ for (const [, path] of source.matchAll(/url\(\.\/(files\/[^)]+\.woff2)\)/g)) {
43
+ if (files.has(path))
44
+ continue;
45
+ const bytes = await readFile(join(dirname(sheet), path));
46
+ const name = `${basename(path, ".woff2")}-${contentHash(bytes)}.woff2`;
47
+ await writeFile(join(fontDirectory, name), bytes);
48
+ files.set(path, name);
49
+ }
50
+ rules.push(source.replace(/url\(\.\/(files\/[^)]+\.woff2)\)/g, (_match, path) => `url(./${FONT_DIRECTORY}/${files.get(path)})`));
51
+ }
52
+ const license = resolveFontFile(`${font.package}/LICENSE`);
53
+ await cp(license, join(licenseDirectory, `${font.package.split("/").pop()}-OFL.txt`));
54
+ }
55
+ return rules.join("\n");
56
+ }
@@ -2,5 +2,6 @@ export { collectContentAssets, collectAssetFiles, isDraftAsset, loadSection, nor
2
2
  export { buildSite, meowSite, type ResolvedSiteConfig, type SiteConfig } from "./build.js";
3
3
  export { aboutPage, agentMarkdown, homePage, listPage, llmText, postPage, skillPage, skillsIndexPage, thoughtPage, } from "./site.js";
4
4
  export { DEFAULT_SITE, type SiteAbout, type SiteIdentity, type SiteProfileLink } from "./identity.js";
5
- export { blogTheme, blogTokens, blogOptions } from "./theme.js";
5
+ export { blogFonts, blogTheme, blogTokens, blogOptions } from "./theme.js";
6
+ export type { SiteFont } from "./fonts.js";
6
7
  export { loadSkills, type LoadSkillsOptions, type Skill, type SkillFile, type SkillScope, } from "./skills.js";
package/dist/src/index.js CHANGED
@@ -2,5 +2,5 @@ export { collectContentAssets, collectAssetFiles, isDraftAsset, loadSection, nor
2
2
  export { buildSite, meowSite } from "./build.js";
3
3
  export { aboutPage, agentMarkdown, homePage, listPage, llmText, postPage, skillPage, skillsIndexPage, thoughtPage, } from "./site.js";
4
4
  export { DEFAULT_SITE } from "./identity.js";
5
- export { blogTheme, blogTokens, blogOptions } from "./theme.js";
5
+ export { blogFonts, blogTheme, blogTokens, blogOptions } from "./theme.js";
6
6
  export { loadSkills, } from "./skills.js";
@@ -8,6 +8,11 @@ export interface SitePage {
8
8
  title: string;
9
9
  /** A complete HTML document for the route. */
10
10
  html: string;
11
+ /**
12
+ * The same page as Markdown for agents. When present it is published next to the route (see
13
+ * `sitePageMarkdownFile`), and the agent-facing indexes link it instead of the HTML document.
14
+ */
15
+ markdown?: string;
11
16
  description?: string;
12
17
  /** Grouping used by the site index and the agent-facing indexes. */
13
18
  group?: string;
@@ -26,3 +31,10 @@ export type SitePageProvider = (context: SitePageContext) => readonly SitePage[]
26
31
  export declare function sitePageUrl(path: string): string;
27
32
  /** The output file a page is written to, relative to the site root. */
28
33
  export declare function sitePageFile(path: string): string;
34
+ /**
35
+ * The Markdown copy of a page, relative to the site root: "index.md" for the root and
36
+ * "components/button.md" for /components/button/, the convention llms.txt readers expect.
37
+ */
38
+ export declare function sitePageMarkdownFile(path: string): string;
39
+ /** Where an agent reads a page: its Markdown copy when it has one, its HTML route otherwise. */
40
+ export declare function sitePageAgentUrl(page: SitePage): string;
package/dist/src/pages.js CHANGED
@@ -8,3 +8,17 @@ export function sitePageFile(path) {
8
8
  const normalized = path.replace(/^\/+|\/+$/g, "");
9
9
  return normalized ? `${normalized}/index.html` : "index.html";
10
10
  }
11
+ /**
12
+ * The Markdown copy of a page, relative to the site root: "index.md" for the root and
13
+ * "components/button.md" for /components/button/, the convention llms.txt readers expect.
14
+ */
15
+ export function sitePageMarkdownFile(path) {
16
+ const normalized = path.replace(/^\/+|\/+$/g, "");
17
+ return normalized ? `${normalized}.md` : "index.md";
18
+ }
19
+ /** Where an agent reads a page: its Markdown copy when it has one, its HTML route otherwise. */
20
+ export function sitePageAgentUrl(page) {
21
+ return page.markdown === undefined
22
+ ? sitePageUrl(page.path)
23
+ : `/${sitePageMarkdownFile(page.path)}`;
24
+ }
@@ -13,4 +13,9 @@ export declare function aboutPage(theme?: ThemeDefinition, site?: SiteIdentity,
13
13
  export declare function skillsIndexPage(skills: Skill[], theme?: ThemeDefinition, site?: SiteIdentity, stylesheet?: string): string;
14
14
  export declare function skillPage(skill: Skill, theme?: ThemeDefinition, site?: SiteIdentity, stylesheet?: string): string;
15
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;
16
+ export declare function llmText(posts: ContentItem[], thoughts: ContentItem[], site?: SiteIdentity, pages?: readonly SitePage[], agentGuide?: string): string;
17
+ /**
18
+ * Every page's Markdown in one file, after the same summary and guide as llms.txt. Pages without
19
+ * a Markdown copy are left out; llms.txt still links their HTML.
20
+ */
21
+ export declare function llmFullText(site: SiteIdentity, pages: readonly SitePage[], agentGuide?: string): string | undefined;
package/dist/src/site.js CHANGED
@@ -4,7 +4,7 @@ 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
+ import { sitePageAgentUrl } from "./pages.js";
8
8
  import { layoutSkillGraph } from "./skill-graph.js";
9
9
  /** The name of the theme stylesheet the package writes by default. */
10
10
  export const DEFAULT_THEME_STYLESHEET = "blog-theme.css";
@@ -675,7 +675,7 @@ export function skillPage(skill, theme = blogTheme, site = DEFAULT_SITE, stylesh
675
675
  }
676
676
  export function agentMarkdown(posts, thoughts, site = DEFAULT_SITE, pages) {
677
677
  const siteMap = pages
678
- ? pages.map((page) => `- [${page.title}](${sitePageUrl(page.path)})`)
678
+ ? pages.map((page) => `- [${page.title}](${sitePageAgentUrl(page)})`)
679
679
  : [
680
680
  "- [Home](/)",
681
681
  "- [Posts](/posts/)",
@@ -720,38 +720,94 @@ export function agentMarkdown(posts, thoughts, site = DEFAULT_SITE, pages) {
720
720
  ];
721
721
  return lines.join("\n");
722
722
  }
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
- ];
723
+ export function llmText(posts, thoughts, site = DEFAULT_SITE, pages, agentGuide) {
724
+ // A consumer that owns the site map owns the routes of its content too, so its index follows
725
+ // the llms.txt shape: the summary, the guide, then one section of links per page group.
726
+ if (pages)
727
+ return consumerLlmText(site, pages, agentGuide);
733
728
  return [
734
729
  `# ${site.name}`,
735
730
  `> ${site.description}`,
736
731
  "",
737
732
  "## Pages",
738
- ...routes,
733
+ "- Home: /",
734
+ "- Posts: /posts/",
735
+ "- Thoughts: /thoughts/",
736
+ "- Skills: /skills/",
737
+ "- About: /about/",
739
738
  "- Agent Markdown: /agents.md",
740
739
  "",
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
- ]),
740
+ "## Posts",
741
+ ...posts.map((post) => `- ${post.title}: ${itemUrl(post)}`),
742
+ "",
743
+ "## Thoughts",
744
+ ...thoughts.map((thought) => `- ${thought.title}: ${itemUrl(thought)}`),
745
+ "",
753
746
  "## Rules",
754
747
  "- Only the public build output is considered presentable content.",
755
748
  "- Drafts whose names start with _ under posts are not published or rendered.",
756
749
  ].join("\n");
757
750
  }
751
+ /** The pages by group, in the order each group first appears. */
752
+ function pageGroups(pages) {
753
+ const groups = new Map();
754
+ for (const page of pages) {
755
+ const group = page.group ?? "Pages";
756
+ const members = groups.get(group);
757
+ if (members)
758
+ members.push(page);
759
+ else
760
+ groups.set(group, [page]);
761
+ }
762
+ return [...groups];
763
+ }
764
+ /** One llms.txt link: the page's Markdown when it has one, and its description after a colon. */
765
+ function llmLink(page) {
766
+ const description = page.description?.replace(/\s+/g, " ").trim();
767
+ return `- [${page.title}](${sitePageAgentUrl(page)})${description ? `: ${description}` : ""}`;
768
+ }
769
+ function consumerLlmText(site, pages, agentGuide) {
770
+ const full = pages.some((page) => page.markdown !== undefined);
771
+ return [
772
+ `# ${site.name}`,
773
+ "",
774
+ `> ${site.description}`,
775
+ "",
776
+ ...(agentGuide ? [agentGuide.trim(), ""] : []),
777
+ ...pageGroups(pages).flatMap(([group, members]) => [
778
+ `## ${group}`,
779
+ "",
780
+ ...members.map(llmLink),
781
+ "",
782
+ ]),
783
+ "## Optional",
784
+ "",
785
+ ...(full
786
+ ? [
787
+ "- [Full text](/llms-full.txt): every page linked here as Markdown, in one file; large, so fetch single pages first",
788
+ ]
789
+ : []),
790
+ "- [Agent Markdown](/agents.md): the site map",
791
+ "",
792
+ ].join("\n");
793
+ }
794
+ /**
795
+ * Every page's Markdown in one file, after the same summary and guide as llms.txt. Pages without
796
+ * a Markdown copy are left out; llms.txt still links their HTML.
797
+ */
798
+ export function llmFullText(site, pages, agentGuide) {
799
+ const documents = pages.flatMap((page) => page.markdown === undefined
800
+ ? []
801
+ : [`<!-- Source: ${sitePageAgentUrl(page)} -->\n\n${page.markdown.trim()}`]);
802
+ if (!documents.length)
803
+ return undefined;
804
+ return [
805
+ `# ${site.name}`,
806
+ "",
807
+ `> ${site.description}`,
808
+ "",
809
+ ...(agentGuide ? [agentGuide.trim(), ""] : []),
810
+ documents.join("\n\n---\n\n"),
811
+ "",
812
+ ].join("\n");
813
+ }
@@ -37,9 +37,11 @@ export declare const blogTokens: import("meowcss").TokenRefs<{
37
37
  readonly fontMono: "\"SF Mono\", \"Cascadia Code\", Menlo, Consolas, ui-monospace, monospace";
38
38
  readonly paperGrain: "url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160' viewBox='0 0 160 160'%3E%3Cfilter id='grain'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.78' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23grain)' opacity='.065'/%3E%3C/svg%3E\")";
39
39
  }> & import("meowcss").TokenSet;
40
- export declare const blogTheme: Readonly<{
41
- name: string;
42
- defaults: import("meowcss").Selection;
43
- cssText: `@import "@fontsource-variable/geist";
44
- ${string}`;
45
- }>;
40
+ /** Static HTML owns the theme root; CSS remains available without JavaScript. */
41
+ export declare const blogTheme: import("meowcss").ThemeDefinition;
42
+ /** The web font the Blog theme names; the build publishes it with its license. */
43
+ export declare const blogFonts: readonly [{
44
+ readonly family: "Geist Variable";
45
+ readonly package: "@fontsource-variable/geist";
46
+ readonly styles: readonly ["normal"];
47
+ }];
package/dist/src/theme.js CHANGED
@@ -2217,14 +2217,13 @@ const styles = css `
2217
2217
  }
2218
2218
  `;
2219
2219
  /** Static HTML owns the theme root; CSS remains available without JavaScript. */
2220
- const definition = defineTheme({
2220
+ export const blogTheme = defineTheme({
2221
2221
  options: blogOptions,
2222
2222
  defaults: { scheme: "auto" },
2223
2223
  tokens: [blogTokens],
2224
2224
  styles,
2225
2225
  });
2226
- // CSS imports must precede the generated token declarations. Bun resolves and emits font assets.
2227
- export const blogTheme = Object.freeze({
2228
- ...definition,
2229
- cssText: `@import "@fontsource-variable/geist";\n${definition.cssText}`,
2230
- });
2226
+ /** The web font the Blog theme names; the build publishes it with its license. */
2227
+ export const blogFonts = [
2228
+ { family: "Geist Variable", package: "@fontsource-variable/geist", styles: ["normal"] },
2229
+ ];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "meowpress",
3
- "version": "0.3.0-alpha.0",
3
+ "version": "0.3.0-alpha.2",
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.3.0-alpha.0",
40
+ "meowcss": "^0.3.0-alpha.1",
41
41
  "meowjsx": "^0.3.0-alpha.0"
42
42
  },
43
43
  "devDependencies": {
44
- "meowjsx": "0.3.0-alpha.0",
45
- "meowcss": "0.3.0-alpha.0"
44
+ "meowjsx": "0.3.0-alpha.1",
45
+ "meowcss": "0.3.0-alpha.2"
46
46
  }
47
47
  }