@junejs/server 1.0.0-dev.28 → 1.0.0-dev.29

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.
@@ -0,0 +1,6 @@
1
+ import { u as RouteResolver } from "./pipeline-cCR6MGNK.js";
2
+ import { LlmsLink } from "@junejs/core/discovery";
3
+ //#region src/llms-links.d.ts
4
+ declare function collectLlmsLinks(origin: string, routes: string[], resolve: RouteResolver): Promise<LlmsLink[]>;
5
+ //#endregion
6
+ export { collectLlmsLinks };
@@ -0,0 +1,35 @@
1
+ //#region src/llms-links.ts
2
+ const DEFAULT_SECTION = "Pages";
3
+ const mdPath = (path) => path === "/" ? "/index.md" : `${path.replace(/\/+$/, "")}.md`;
4
+ async function collectLlmsLinks(origin, routes, resolve) {
5
+ const links = [];
6
+ const seen = /* @__PURE__ */ new Set();
7
+ for (const route of routes) {
8
+ const resolved = await resolve(route);
9
+ if (!resolved || !("def" in resolved)) continue;
10
+ const { def } = resolved;
11
+ if (def.llms === false) continue;
12
+ const dynamic = route.includes("[");
13
+ const declared = typeof def.llms === "function" ? await def.llms() : def.llms;
14
+ const entries = declared ? Array.isArray(declared) ? declared : [declared] : dynamic ? [] : [{}];
15
+ const meta = typeof def.metadata === "object" ? def.metadata : void 0;
16
+ for (const entry of entries) {
17
+ const path = entry.path ?? (dynamic ? void 0 : route);
18
+ if (!path) continue;
19
+ const url = origin + (def.md === false ? path : mdPath(path));
20
+ if (seen.has(url)) continue;
21
+ seen.add(url);
22
+ const own = path === route;
23
+ links.push({
24
+ title: entry.title ?? (own ? meta?.title : void 0) ?? path,
25
+ url,
26
+ description: entry.description ?? (own ? meta?.description : void 0),
27
+ section: entry.section ?? DEFAULT_SECTION,
28
+ optional: entry.optional === true
29
+ });
30
+ }
31
+ }
32
+ return links;
33
+ }
34
+ //#endregion
35
+ export { collectLlmsLinks };
package/dist/pipeline.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { iconLetter } from "./icon-letter.js";
2
+ import { collectLlmsLinks } from "./llms-links.js";
2
3
  import { webManifest } from "./web-manifest.js";
3
4
  import { SEGMENT_HEADER, TITLE_HEADER, encodeTitle, negotiate } from "./negotiate.js";
4
5
  import { ensureScope, runInScope, setRequestLocale } from "@junejs/db";
@@ -172,7 +173,11 @@ function createPipeline(cfg) {
172
173
  }
173
174
  async function discovery(url) {
174
175
  switch (url.pathname) {
175
- case "/llms.txt": return text(llmsTxt(url.origin, await cfg.routeList(), agent, docConfig.site), "text/markdown; charset=utf-8");
176
+ case "/llms.txt": {
177
+ const routes = await cfg.routeList();
178
+ const links = await collectLlmsLinks(url.origin, routes, cfg.resolve);
179
+ return text(llmsTxt(url.origin, routes, agent, docConfig.site, links), "text/markdown; charset=utf-8");
180
+ }
176
181
  case "/robots.txt": return text(robotsTxt(url.origin), "text/plain; charset=utf-8");
177
182
  case "/sitemap.xml": return text(sitemapXml(url.origin, await cfg.routeList(), cfg.i18n), "application/xml; charset=utf-8");
178
183
  case "/.well-known/api-catalog": return text(JSON.stringify(apiCatalog(url.origin, agent)), "application/linkset+json");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@junejs/server",
3
- "version": "1.0.0-dev.28",
3
+ "version": "1.0.0-dev.29",
4
4
  "description": "June host adapters + dev server. Host-coupled; depends on the pure @junejs/core contract layer.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -140,7 +140,7 @@
140
140
  "rolldown": "^1.1.0"
141
141
  },
142
142
  "peerDependencies": {
143
- "@junejs/core": ">=0.2.0-dev.48",
143
+ "@junejs/core": ">=0.2.0-dev.50",
144
144
  "react": "^19.2.0",
145
145
  "react-dom": "^19.2.0",
146
146
  "react-server-dom-webpack": "19.2.7",
@@ -0,0 +1,51 @@
1
+ // /llms.txt links, collected from the routes themselves. Each route may export
2
+ // `llms` (see LlmsEntry in @junejs/core/route): a static route defaults to one link
3
+ // under "Pages" built from its static metadata; a dynamic route — a [param]
4
+ // template an agent can't fetch — lists each real page it serves, or is skipped.
5
+ // Links point at the markdown projection (<path>.md) unless the route turned md off,
6
+ // so an agent reads the page rather than its HTML (llmstxt.org's recommendation).
7
+ import type { LlmsLink } from "@junejs/core/discovery";
8
+ import type { LlmsEntry } from "@junejs/core/route";
9
+
10
+ import type { RouteResolver } from "./pipeline";
11
+
12
+ const DEFAULT_SECTION = "Pages";
13
+
14
+ const mdPath = (path: string) => (path === "/" ? "/index.md" : `${path.replace(/\/+$/, "")}.md`);
15
+
16
+ export async function collectLlmsLinks(origin: string, routes: string[], resolve: RouteResolver): Promise<LlmsLink[]> {
17
+ const links: LlmsLink[] = [];
18
+ const seen = new Set<string>();
19
+ for (const route of routes) {
20
+ // A dynamic template resolves against its own pattern ("/docs/[slug]" matches
21
+ // /docs/:slug), which is all we need: the route definition, not its data.
22
+ const resolved = await resolve(route);
23
+ if (!resolved || !("def" in resolved)) continue; // resource routes have no page to list
24
+ const { def } = resolved;
25
+ if (def.llms === false) continue;
26
+
27
+ const dynamic = route.includes("[");
28
+ const declared = typeof def.llms === "function" ? await def.llms() : def.llms;
29
+ const entries: LlmsEntry[] = declared ? (Array.isArray(declared) ? declared : [declared]) : dynamic ? [] : [{}];
30
+ // Static metadata only: a metadata FUNCTION needs loader data we don't run here.
31
+ const meta = typeof def.metadata === "object" ? def.metadata : undefined;
32
+
33
+ for (const entry of entries) {
34
+ const path = entry.path ?? (dynamic ? undefined : route);
35
+ if (!path) continue; // a dynamic entry must name its page
36
+ const url = origin + (def.md === false ? path : mdPath(path));
37
+ // dedupe on the FINAL url: "/a" and "/a/" both render /a.md, so they're one page
38
+ if (seen.has(url)) continue;
39
+ seen.add(url);
40
+ const own = path === route; // the route's own page → its metadata describes it
41
+ links.push({
42
+ title: entry.title ?? (own ? meta?.title : undefined) ?? path,
43
+ url,
44
+ description: entry.description ?? (own ? meta?.description : undefined),
45
+ section: entry.section ?? DEFAULT_SECTION,
46
+ optional: entry.optional === true,
47
+ });
48
+ }
49
+ }
50
+ return links;
51
+ }
package/src/pipeline.ts CHANGED
@@ -50,6 +50,7 @@ import {
50
50
  import type { Resources } from "@junejs/core/resources";
51
51
 
52
52
  import { iconLetter } from "./icon-letter";
53
+ import { collectLlmsLinks } from "./llms-links";
53
54
  import { webManifest } from "./web-manifest";
54
55
  import { negotiate, TITLE_HEADER, SEGMENT_HEADER, encodeTitle } from "./negotiate";
55
56
 
@@ -480,11 +481,12 @@ export function createPipeline(cfg: PipelineConfig): Pipeline {
480
481
 
481
482
  async function discovery(url: URL): Promise<Response | null> {
482
483
  switch (url.pathname) {
483
- case "/llms.txt":
484
- return text(
485
- llmsTxt(url.origin, await cfg.routeList(), agent, docConfig.site),
486
- "text/markdown; charset=utf-8",
487
- );
484
+ case "/llms.txt": {
485
+ const routes = await cfg.routeList();
486
+ // sections, descriptions, and Optional come from each route's `llms` export
487
+ const links = await collectLlmsLinks(url.origin, routes, cfg.resolve);
488
+ return text(llmsTxt(url.origin, routes, agent, docConfig.site, links), "text/markdown; charset=utf-8");
489
+ }
488
490
  case "/robots.txt":
489
491
  return text(robotsTxt(url.origin), "text/plain; charset=utf-8");
490
492
  case "/sitemap.xml":