meowpress 0.3.0-alpha.1 → 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
@@ -3,6 +3,7 @@ import { type SiteFont } from "./fonts.js";
3
3
  import { type SiteIdentity } from "./identity.js";
4
4
  import { type ContentSourceInput, type MdxComponent } from "./content.js";
5
5
  import { type SitePageProvider } from "./pages.js";
6
+ export { sitePageMarkdownFile } from "./pages.js";
6
7
  export type { SitePage, SitePageContext, SitePageProvider } from "./pages.js";
7
8
  export type { ContentItem } from "./content.js";
8
9
  export type { SiteFont } from "./fonts.js";
@@ -22,6 +23,11 @@ export interface SiteConfig {
22
23
  site?: Partial<SiteIdentity>;
23
24
  /** Consumer-supplied pages; the site writes and indexes them next to its own. */
24
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;
25
31
  /** Extra components available to MDX content pages. */
26
32
  mdxComponents?: Record<string, MdxComponent>;
27
33
  /** Stylesheet file name for the theme; defaults to "blog-theme.css". */
@@ -40,6 +46,7 @@ export interface ResolvedSiteConfig {
40
46
  projectRootDirectory?: string;
41
47
  site: SiteIdentity;
42
48
  pages?: SitePageProvider;
49
+ agentGuide?: string;
43
50
  mdxComponents: Record<string, MdxComponent>;
44
51
  themeStylesheet: string;
45
52
  fonts: readonly SiteFont[];
package/dist/src/build.js CHANGED
@@ -5,9 +5,10 @@ import { DEFAULT_SITE } from "./identity.js";
5
5
  import { mkdir, readFile, rm, writeFile, cp } from "node:fs/promises";
6
6
  import { isAbsolute, join, relative, resolve } from "node:path";
7
7
  import { collectContentAssets, isDraftAsset, loadSection, normalizeContentSources, } from "./content.js";
8
- 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";
9
9
  import { loadSkills } from "./skills.js";
10
- import { sitePageFile } from "./pages.js";
10
+ import { sitePageFile, sitePageMarkdownFile } from "./pages.js";
11
+ export { sitePageMarkdownFile } from "./pages.js";
11
12
  /** Define a site config in the app package without taking on the build pipeline. */
12
13
  export function meowSite(config) {
13
14
  return {
@@ -20,6 +21,7 @@ export function meowSite(config) {
20
21
  : undefined,
21
22
  site: { ...DEFAULT_SITE, ...config.site },
22
23
  pages: config.pages,
24
+ agentGuide: config.agentGuide,
23
25
  mdxComponents: config.mdxComponents ?? {},
24
26
  themeStylesheet: config.themeStylesheet ?? DEFAULT_THEME_STYLESHEET,
25
27
  fonts: config.fonts ?? (config.theme === undefined || config.theme === blogTheme ? blogFonts : []),
@@ -71,8 +73,8 @@ async function buildStaticHtmlFromEntrypoints(entrypoints, generatedDirectory, o
71
73
  await writeFile(output.path, removeLegacyIsFallbacks(await readFile(output.path, "utf8")));
72
74
  }
73
75
  /** Consumer pages are written inside the generated directory and never above it. */
74
- function consumerPageFile(generatedDirectory, path) {
75
- const file = sitePageFile(path);
76
+ function consumerPageFile(generatedDirectory, path, fileFor = sitePageFile) {
77
+ const file = fileFor(path);
76
78
  const target = resolve(generatedDirectory, file);
77
79
  const inside = relative(generatedDirectory, target);
78
80
  if (!inside || inside.startsWith("..") || isAbsolute(inside)) {
@@ -87,6 +89,18 @@ async function writeConsumerPages(generatedDirectory, pages) {
87
89
  }
88
90
  return entrypoints;
89
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
+ }
90
104
  async function writeContentPages(generatedDirectory, posts, thoughts, theme, site, stylesheet) {
91
105
  const entrypoints = [];
92
106
  for (const post of posts) {
@@ -148,9 +162,15 @@ export async function buildSite(input) {
148
162
  await stageAssets(generatedDirectory, config.content);
149
163
  await buildStaticHtmlFromEntrypoints(entrypoints, generatedDirectory, config.outputDirectory);
150
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;
151
168
  await Promise.all([
152
- writeFile(join(config.outputDirectory, "llm.txt"), llmText(posts, thoughts, config.site, pages), "utf8"),
153
- 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")]),
154
174
  writeFile(join(config.outputDirectory, "agents.md"), agentMarkdown(posts, thoughts, config.site, pages), "utf8"),
155
175
  ]);
156
176
  await rm(generatedDirectory, { recursive: true, force: true });
@@ -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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "meowpress",
3
- "version": "0.3.0-alpha.1",
3
+ "version": "0.3.0-alpha.2",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -41,7 +41,7 @@
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
  }