@jay-framework/production-server 0.25.1 → 0.25.3

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/dist/index.d.ts CHANGED
@@ -1,9 +1,60 @@
1
- import { R as RouteManifest, a as RouteEntry } from './serve-index-CUXzYZBC.js';
2
- export { A as ActionEntry, b as ArtifactStore, B as BuildMetadata, c as BuildOptions, C as CacheEntry, F as FilesystemArtifactStore, I as InstanceEntry, M as MatchResult, P as PageModule, d as PluginEntry, e as PreImportedPlugin, f as RouteSegment, S as ServerElementModule, g as fetchActionRequest, h as fetchPageRequest, i as fetchStaticFile, j as initializeServices, k as initializeServicesFromModules, l as isActionRequest, m as loadPagePartsFromConfig, n as matchRequest, r as registerActionsFromManifest, o as registerActionsFromModules } from './serve-index-CUXzYZBC.js';
1
+ import { R as RouteManifest, a as RouteEntry, A as ArtifactStore } from './serve-index-C28bP6tW.js';
2
+ export { b as ActionEntry, B as BuildMetadata, c as BuildOptions, C as CacheEntry, E as ErrorPageEntry, F as FilesystemArtifactStore, I as InstanceEntry, M as MatchResult, P as PageModule, d as PluginEntry, e as PreImportedPlugin, f as RouteSegment, S as ServerElementModule, g as fetchActionRequest, h as fetchPageRequest, i as fetchStaticFile, j as initializeServices, k as initializeServicesFromModules, l as isActionRequest, m as loadPagePartsFromConfig, n as matchRequest, r as registerActionsFromManifest, o as registerActionsFromModules } from './serve-index-C28bP6tW.js';
3
+ import { HeadTag } from '@jay-framework/fullstack-component';
4
+ import { ErrorInfo } from '@jay-framework/stack-server-runtime';
3
5
  import '@jay-framework/ssr-runtime';
4
- import '@jay-framework/stack-server-runtime';
5
6
 
6
7
  declare function generateSitemap(manifest: RouteManifest, baseUrl: string, outputPath: string): Promise<number>;
8
+ declare function buildUrlFromManifest(pattern: string, params: Record<string, string>): string;
9
+
10
+ /**
11
+ * Site-level identity config that drives Organization + WebSite JSON-LD (DL#213).
12
+ * Mirrors the `site` block of `.jay` config.
13
+ */
14
+ interface SiteJsonLdConfig {
15
+ name?: string;
16
+ baseUrl?: string;
17
+ logo?: string;
18
+ description?: string;
19
+ sameAs?: string[];
20
+ }
21
+ /**
22
+ * Build the site-level `Organization` + `WebSite` JSON-LD head tags from `.jay` `site` config.
23
+ *
24
+ * Returns `[]` when `site.name` is absent — there is no identity to describe, and a validation
25
+ * warning (DL#213 Part 5) nudges the author. Emitted once at build (next to the sitemap) and
26
+ * carried on the RouteManifest so every served page gets identical site identity.
27
+ */
28
+ declare function generateSiteJsonLd(site: SiteJsonLdConfig | undefined): HeadTag[];
29
+
30
+ /**
31
+ * Site identity used for the llms.txt header (DL#214). Mirrors the `.jay` `site` config subset
32
+ * that `generateSiteJsonLd` consumes.
33
+ */
34
+ interface LlmsTxtSiteConfig {
35
+ name?: string;
36
+ description?: string;
37
+ baseUrl?: string;
38
+ }
39
+ /**
40
+ * Generate an `llms.txt` (llmstxt.org) index from the build manifest + site config (DL#214).
41
+ * A curated, markdown map of the site for LLMs: site H1 + summary, then per-page links with
42
+ * descriptions, grouped into `##` sections by top-level path segment.
43
+ *
44
+ * Returns the number of page links written. Requires `site.name` (the H1); callers skip the
45
+ * emission when it is absent (parallel to the sitemap requiring `baseUrl`).
46
+ */
47
+ declare function generateLlmsTxt(manifest: RouteManifest, site: LlmsTxtSiteConfig, backendDir: string, outputPath: string): Promise<number>;
48
+
49
+ /**
50
+ * Generate a default `robots.txt` (DL#214) — **only when the author has not provided one**
51
+ * (mirrors the sitemap "don't clobber a static file" convention). Returns true if written,
52
+ * false if an author file already exists at `outputPath`.
53
+ *
54
+ * The `Sitemap:` line is emitted only when `baseUrl` is set; without it, the sitemap URL is
55
+ * unknown (and the existing `site.baseUrl` validation warning already fires).
56
+ */
57
+ declare function generateRobotsTxtIfAbsent(outputPath: string, baseUrl: string | undefined): Promise<boolean>;
7
58
 
8
59
  interface MainServerOptions {
9
60
  buildRoot: string;
@@ -50,6 +101,12 @@ interface RebuildOptions {
50
101
  minify?: boolean;
51
102
  /** Site base URL for sitemap regeneration (e.g. "https://example.com"). */
52
103
  siteBaseUrl?: string;
104
+ /** Full site identity for llms.txt regeneration (DL#214). `name` drives the llms.txt H1. */
105
+ site?: {
106
+ name?: string;
107
+ description?: string;
108
+ baseUrl?: string;
109
+ };
53
110
  }
54
111
  interface RebuildResult {
55
112
  affected: number;
@@ -76,4 +133,14 @@ declare function rebuildContract(options: {
76
133
  }): Promise<RebuildResult>;
77
134
  declare function cleanupOrphanedFiles(buildRoot: string, version: string): Promise<number>;
78
135
 
79
- export { type MainServerOptions, type RebuildOptions, type RebuildResult, type RebuildTarget, type RendererServerOptions, RouteEntry, RouteManifest, cleanupOrphanedFiles, generateSitemap, rebuild, rebuildContract, resolveContractToRoutes, startMainServer, startRendererServer };
136
+ /**
137
+ * Render a designed error page for a 4xx/5xx outcome (DL#218). Resolves the page by status
138
+ * (exact → family) from `manifest.errorPages`; when none matches — or when rendering throws —
139
+ * falls back to the built-in plain-text response. The response always carries `info.status`.
140
+ *
141
+ * Error pages are a pass-through: no fast render runs; the framework injects the `error` ViewState
142
+ * (`buildErrorViewState`). Interactivity (hydration) still works via the compiled client bundle.
143
+ */
144
+ declare function renderErrorPage(manifest: RouteManifest, artifacts: ArtifactStore, staticBaseUrl: string, info: ErrorInfo): Promise<Response>;
145
+
146
+ export { ArtifactStore, type LlmsTxtSiteConfig, type MainServerOptions, type RebuildOptions, type RebuildResult, type RebuildTarget, type RendererServerOptions, RouteEntry, RouteManifest, type SiteJsonLdConfig, buildUrlFromManifest, cleanupOrphanedFiles, generateLlmsTxt, generateRobotsTxtIfAbsent, generateSiteJsonLd, generateSitemap, rebuild, rebuildContract, renderErrorPage, resolveContractToRoutes, startMainServer, startRendererServer };
package/dist/index.js CHANGED
@@ -1,11 +1,11 @@
1
1
  import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { headMetaToHeadTags, parseCookies, scanPlugins, DevSlowlyChangingPhase, slowRenderInstances } from "@jay-framework/stack-server-runtime";
2
4
  import http from "node:http";
3
5
  import { Readable } from "node:stream";
4
- import path from "node:path";
5
6
  import { getLogger } from "@jay-framework/logger";
6
- import { F as FilesystemArtifactStore, i as initializeServices, r as registerActionsFromManifest, d as isActionRequest, f as fetchActionRequest, b as fetchStaticFile, m as matchRequest, a as fetchPageRequest, l as loadPagePartsFromConfig } from "./init-services-XQt5vxN_.js";
7
- import { c, e } from "./init-services-XQt5vxN_.js";
8
- import { parseCookies, scanPlugins, DevSlowlyChangingPhase, slowRenderInstances } from "@jay-framework/stack-server-runtime";
7
+ import { F as FilesystemArtifactStore, i as initializeServices, r as registerActionsFromManifest, d as isActionRequest, f as fetchActionRequest, b as fetchStaticFile, m as matchRequest, g as renderErrorPage, a as fetchPageRequest, l as loadPagePartsFromConfig } from "./init-services-CG_wDJmY.js";
8
+ import { c, e } from "./init-services-CG_wDJmY.js";
9
9
  import { isJayWebhook } from "@jay-framework/fullstack-component";
10
10
  import crypto from "node:crypto";
11
11
  async function generateSitemap(manifest, baseUrl, outputPath) {
@@ -49,6 +49,133 @@ function buildUrlFromManifest(pattern, params) {
49
49
  function escapeXml(str) {
50
50
  return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
51
51
  }
52
+ function resolveUrl(baseUrl, value) {
53
+ if (/^https?:\/\//i.test(value)) return value;
54
+ if (!baseUrl) return value;
55
+ const base = baseUrl.replace(/\/$/, "");
56
+ const rel = value.startsWith("/") ? value : `/${value}`;
57
+ return `${base}${rel}`;
58
+ }
59
+ function generateSiteJsonLd(site) {
60
+ if (!site?.name) return [];
61
+ const base = site.baseUrl?.replace(/\/$/, "");
62
+ const organization = {
63
+ "@context": "https://schema.org",
64
+ "@type": "Organization",
65
+ name: site.name
66
+ };
67
+ if (base) organization.url = base;
68
+ if (site.logo) organization.logo = resolveUrl(site.baseUrl, site.logo);
69
+ if (site.description) organization.description = site.description;
70
+ if (site.sameAs && site.sameAs.length > 0) organization.sameAs = site.sameAs;
71
+ const website = {
72
+ "@context": "https://schema.org",
73
+ "@type": "WebSite",
74
+ name: site.name
75
+ };
76
+ if (base) website.url = base;
77
+ return [
78
+ {
79
+ tag: "script",
80
+ attrs: { type: "application/ld+json" },
81
+ children: JSON.stringify(organization)
82
+ },
83
+ {
84
+ tag: "script",
85
+ attrs: { type: "application/ld+json" },
86
+ children: JSON.stringify(website)
87
+ }
88
+ ];
89
+ }
90
+ const UNRESOLVED_BINDING = /\{[A-Za-z_$][\w$]*(?:\.[\w$]+)*\}/;
91
+ function hasUnresolved(value) {
92
+ return !!value && UNRESOLVED_BINDING.test(value);
93
+ }
94
+ async function loadSlowViewState(backendDir, cachePath) {
95
+ try {
96
+ const raw = await fs.readFile(path.join(backendDir, cachePath), "utf8");
97
+ return JSON.parse(raw).slowViewState ?? {};
98
+ } catch {
99
+ return {};
100
+ }
101
+ }
102
+ function resolveTitleAndDescription(route, viewState) {
103
+ const tags = headMetaToHeadTags(route.headMeta, viewState);
104
+ const titleTag = tags.find((t) => t.tag.toLowerCase() === "title");
105
+ const descTag = tags.find(
106
+ (t) => t.tag.toLowerCase() === "meta" && t.attrs?.name === "description"
107
+ );
108
+ const title = hasUnresolved(titleTag?.children) ? void 0 : titleTag?.children;
109
+ const description = hasUnresolved(descTag?.attrs?.content) ? void 0 : descTag?.attrs?.content;
110
+ return { title, description };
111
+ }
112
+ async function collectPageLinks(manifest, base, backendDir) {
113
+ const links = [];
114
+ for (const route of manifest.routes) {
115
+ if (route.devOnly || route.noIndex) continue;
116
+ if (route.instances.length === 0) {
117
+ const hasDynamic = route.segments.some((s) => s.type !== "static");
118
+ if (hasDynamic) continue;
119
+ const urlPath = route.pattern === "/" ? "/" : route.pattern;
120
+ const { title, description } = resolveTitleAndDescription(route, {});
121
+ links.push({ title: title ?? urlPath, url: `${base}${urlPath}`, description });
122
+ continue;
123
+ }
124
+ for (const instance of route.instances) {
125
+ const urlPath = buildUrlFromManifest(route.pattern, instance.params);
126
+ const viewState = await loadSlowViewState(backendDir, instance.cachePath);
127
+ const { title, description } = resolveTitleAndDescription(route, viewState);
128
+ links.push({ title: title ?? urlPath, url: `${base}${urlPath}`, description });
129
+ }
130
+ }
131
+ return links;
132
+ }
133
+ function sectionOf(url, base) {
134
+ const pathOnly = url.startsWith(base) ? url.slice(base.length) : url;
135
+ const seg = pathOnly.replace(/^\//, "").split("/")[0];
136
+ return seg || "Home";
137
+ }
138
+ async function generateLlmsTxt(manifest, site, backendDir, outputPath) {
139
+ const base = (site.baseUrl ?? "").replace(/\/$/, "");
140
+ const links = await collectPageLinks(manifest, base, backendDir);
141
+ const lines = [`# ${site.name}`, ""];
142
+ if (site.description) {
143
+ lines.push(`> ${site.description}`, "");
144
+ }
145
+ const bySection = /* @__PURE__ */ new Map();
146
+ for (const link of links) {
147
+ const section = sectionOf(link.url, base);
148
+ const bucket = bySection.get(section) ?? [];
149
+ bucket.push(link);
150
+ bySection.set(section, bucket);
151
+ }
152
+ for (const [section, bucket] of bySection) {
153
+ lines.push(`## ${section}`, "");
154
+ for (const link of bucket) {
155
+ const suffix = link.description ? `: ${link.description}` : "";
156
+ lines.push(`- [${link.title}](${link.url})${suffix}`);
157
+ }
158
+ lines.push("");
159
+ }
160
+ const tmpPath = outputPath + ".tmp";
161
+ await fs.writeFile(tmpPath, lines.join("\n").replace(/\n+$/, "\n"));
162
+ await fs.rename(tmpPath, outputPath);
163
+ return links.length;
164
+ }
165
+ async function generateRobotsTxtIfAbsent(outputPath, baseUrl) {
166
+ try {
167
+ await fs.access(outputPath);
168
+ return false;
169
+ } catch {
170
+ }
171
+ const base = baseUrl?.replace(/\/$/, "");
172
+ const lines = ["User-agent: *", "Allow: /"];
173
+ if (base) lines.push(`Sitemap: ${base}/sitemap.xml`);
174
+ const tmpPath = outputPath + ".tmp";
175
+ await fs.writeFile(tmpPath, lines.join("\n") + "\n");
176
+ await fs.rename(tmpPath, outputPath);
177
+ return true;
178
+ }
52
179
  function toFetchRequest(req) {
53
180
  const url = new URL(req.url || "/", `http://${req.headers.host}`);
54
181
  const headers = new Headers();
@@ -137,8 +264,11 @@ async function startMainServer(options) {
137
264
  const currentManifest = await artifacts.readManifest();
138
265
  const match = matchRequest(currentManifest, url.pathname);
139
266
  if (!match) {
140
- res.writeHead(404);
141
- res.end("Not Found");
267
+ const response2 = await renderErrorPage(currentManifest, artifacts, staticBaseUrl, {
268
+ status: 404,
269
+ url: url.pathname
270
+ });
271
+ await pipeFetchResponse(response2, res);
142
272
  return;
143
273
  }
144
274
  const cookies = parseCookies(req.headers.cookie);
@@ -155,8 +285,19 @@ async function startMainServer(options) {
155
285
  logger.error(`[Server] Error handling ${url.pathname}: ${err.message}`);
156
286
  if (err.stack) logger.error(err.stack);
157
287
  if (!res.headersSent) {
158
- res.writeHead(500);
159
- res.end("Internal Server Error");
288
+ try {
289
+ const currentManifest = await artifacts.readManifest();
290
+ const response = await renderErrorPage(
291
+ currentManifest,
292
+ artifacts,
293
+ staticBaseUrl,
294
+ { status: 500, url: url.pathname, message: err?.message }
295
+ );
296
+ await pipeFetchResponse(response, res);
297
+ } catch {
298
+ res.writeHead(500);
299
+ res.end("Internal Server Error");
300
+ }
160
301
  }
161
302
  }
162
303
  });
@@ -465,6 +606,12 @@ async function rebuild(options) {
465
606
  const urlCount = await generateSitemap(manifest, options.siteBaseUrl, sitemapPath);
466
607
  logger.info(`[Rebuild] Sitemap regenerated: ${urlCount} URLs`);
467
608
  }
609
+ const site = options.site ?? (options.siteBaseUrl ? { baseUrl: options.siteBaseUrl } : {});
610
+ if (site.name) {
611
+ const llmsPath = path.join(frontendDir, "llms.txt");
612
+ const count = await generateLlmsTxt(manifest, site, backendDir, llmsPath);
613
+ logger.info(`[Rebuild] llms.txt regenerated: ${count} pages`);
614
+ }
468
615
  if (orphanedFiles.length > 0) {
469
616
  await appendCleanupManifest(buildDir, orphanedFiles);
470
617
  logger.info(`[Rebuild] ${orphanedFiles.length} orphaned file(s) queued for cleanup`);
@@ -690,10 +837,14 @@ function readBody(req) {
690
837
  }
691
838
  export {
692
839
  FilesystemArtifactStore,
840
+ buildUrlFromManifest,
693
841
  cleanupOrphanedFiles,
694
842
  fetchActionRequest,
695
843
  fetchPageRequest,
696
844
  fetchStaticFile,
845
+ generateLlmsTxt,
846
+ generateRobotsTxtIfAbsent,
847
+ generateSiteJsonLd,
697
848
  generateSitemap,
698
849
  initializeServices,
699
850
  c as initializeServicesFromModules,
@@ -704,6 +855,7 @@ export {
704
855
  rebuildContract,
705
856
  registerActionsFromManifest,
706
857
  e as registerActionsFromModules,
858
+ renderErrorPage,
707
859
  resolveContractToRoutes,
708
860
  startMainServer,
709
861
  startRendererServer
@@ -3,11 +3,11 @@ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { en
3
3
  var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
4
4
  import fs from "node:fs/promises";
5
5
  import path from "node:path";
6
- import { makePassthroughInstanceComponent, renderFastChangingData, headMetaToHeadTags, mergeHeadTags, serializeHeadTags, serializeScripts, getClientInitData, actionRegistry, setClientInitData } from "@jay-framework/stack-server-runtime";
6
+ import { resolveErrorPageToken, buildErrorViewState, headMetaToHeadTags, mergeHeadTags, serializeHeadTags, serializeScripts, getClientInitData, makePassthroughInstanceComponent, renderFastChangingData, actionRegistry, setClientInitData } from "@jay-framework/stack-server-runtime";
7
7
  import { deepMergeViewStates } from "@jay-framework/view-state-merge";
8
8
  import { asyncSwapScript } from "@jay-framework/ssr-runtime";
9
- import { isJayAction, isJayStreamAction } from "@jay-framework/fullstack-component";
10
9
  import { getLogger } from "@jay-framework/logger";
10
+ import { isJayAction, isJayStreamAction } from "@jay-framework/fullstack-component";
11
11
  class FilesystemArtifactStore {
12
12
  constructor(basePath) {
13
13
  __publicField(this, "manifestCache");
@@ -137,6 +137,112 @@ function buildImportMap(sharedManifest, publicBasePath, sharedDir = "shared") {
137
137
  }
138
138
  return imports;
139
139
  }
140
+ async function renderErrorPage(manifest, artifacts, staticBaseUrl, info) {
141
+ const plainText = () => new Response(info.message || info.title || "Error", {
142
+ status: info.status,
143
+ headers: { "Content-Type": "text/plain; charset=utf-8" }
144
+ });
145
+ const errorPages = manifest.errorPages;
146
+ if (!errorPages) return plainText();
147
+ const token = resolveErrorPageToken(Object.keys(errorPages), info.status);
148
+ if (!token) return plainText();
149
+ const entry = errorPages[token];
150
+ try {
151
+ return await streamErrorPage(entry, manifest, artifacts, staticBaseUrl, info);
152
+ } catch (err) {
153
+ getLogger().error(
154
+ `[Server] Error page "${token}" render failed (${info.status} ${info.url}): ${err?.message}`
155
+ );
156
+ return plainText();
157
+ }
158
+ }
159
+ async function streamErrorPage(entry, manifest, artifacts, staticBaseUrl, info) {
160
+ const slowViewState = entry.slowCache?.slowViewState ?? {};
161
+ const fullViewState = buildErrorViewState(info, slowViewState);
162
+ const headTagSources = [];
163
+ if (manifest.siteHeadTags && manifest.siteHeadTags.length > 0)
164
+ headTagSources.push(manifest.siteHeadTags);
165
+ const templateHeadTags = headMetaToHeadTags(entry.headMeta, fullViewState);
166
+ const headTags = headTagSources.length > 0 || templateHeadTags.length > 0 ? mergeHeadTags(headTagSources, templateHeadTags) : [];
167
+ const hasCustomTitle = headTags.some((t) => t.tag?.toLowerCase() === "title");
168
+ const titleTag = hasCustomTitle ? "" : ` <title>${info.status} Error</title>
169
+ `;
170
+ const preconnectTags = headTags.filter(
171
+ (t) => t.tag === "link" && t.attrs?.rel === "preconnect"
172
+ );
173
+ const otherHeadTags = headTags.filter(
174
+ (t) => !(t.tag === "link" && t.attrs?.rel === "preconnect")
175
+ );
176
+ const preconnectHtml = preconnectTags.length > 0 ? serializeHeadTags(preconnectTags) + "\n" : "";
177
+ const headTagsHtml = otherHeadTags.length > 0 ? serializeHeadTags(otherHeadTags) + "\n" : "";
178
+ const headScriptsHtml = serializeScripts(entry.scripts, "head");
179
+ const bodyScriptsHtml = serializeScripts(entry.scripts, "body");
180
+ const serverElement = await artifacts.loadServerElement(entry.serverElementPath);
181
+ const asyncPromises = [];
182
+ const importMap = buildImportMap(manifest.sharedManifest, staticBaseUrl);
183
+ if (entry.routeHydratePath) {
184
+ importMap["jay-route-hydrate"] = `${staticBaseUrl}${entry.routeHydratePath}`;
185
+ }
186
+ const clientBundleUrl = entry.routeClientBundlePath ? `${staticBaseUrl}${entry.routeClientBundlePath}` : "";
187
+ const sharedPreloads = entry.sharedDeps ? entry.sharedDeps.map((dep) => importMap[dep]).filter(Boolean) : Object.values(importMap);
188
+ const preloadUrls = clientBundleUrl ? [...sharedPreloads, clientBundleUrl] : [];
189
+ const modulePreloads = preloadUrls.map((url) => ` <link rel="modulepreload" href="${url}" />`).join("\n");
190
+ const cssUrl = entry.routeCssPath ? `${staticBaseUrl}${entry.routeCssPath}` : "";
191
+ const cssPreload = cssUrl ? ` <link rel="preload" href="${cssUrl}" as="style" />` : "";
192
+ const cssLink = cssUrl ? ` <link rel="stylesheet" href="${cssUrl}" />` : "";
193
+ const cssImportPreloads = (entry.cssImports ?? []).map((url) => ` <link rel="preload" href="${url}" as="style" />`).join("\n");
194
+ const encoder = new TextEncoder();
195
+ const stream = new ReadableStream({
196
+ async start(controller) {
197
+ const write = (s) => controller.enqueue(encoder.encode(s));
198
+ const headParts = [headTagsHtml, modulePreloads, cssLink, headScriptsHtml].filter(Boolean).join("\n");
199
+ write(`<!doctype html>
200
+ <html lang="en">
201
+ <head>
202
+ <meta charset="UTF-8" />
203
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
204
+ ${titleTag}${[preconnectHtml, cssPreload, cssImportPreloads].filter(Boolean).map((l) => l + "\n").join(
205
+ ""
206
+ )} <script type="importmap">${JSON.stringify({ imports: importMap })}<\/script>
207
+ ${headParts}
208
+ </head>
209
+ <body>
210
+ <div id="target">`);
211
+ serverElement.renderToStream(fullViewState, {
212
+ write: (chunk) => write(chunk),
213
+ onAsync: (promise, id, templates) => {
214
+ asyncPromises.push(
215
+ promise.then(
216
+ (val) => asyncSwapScript(id, templates.resolved(val)),
217
+ (err) => asyncSwapScript(id, templates.rejected(err))
218
+ )
219
+ );
220
+ }
221
+ });
222
+ write("</div>");
223
+ const asyncScripts = (await Promise.all(asyncPromises)).filter((s) => s).join("");
224
+ if (asyncScripts) write(asyncScripts);
225
+ if (clientBundleUrl) {
226
+ const clientInitData = getClientInitData();
227
+ const initArgs = `${JSON.stringify({})}, ${JSON.stringify(fullViewState)}, ${JSON.stringify({})}, ${JSON.stringify(clientInitData)}`;
228
+ write(`
229
+ <script type="module">
230
+ import { init } from '${clientBundleUrl}';
231
+ await init(${initArgs});
232
+ <\/script>`);
233
+ }
234
+ if (bodyScriptsHtml) write("\n " + bodyScriptsHtml);
235
+ write(`
236
+ </body>
237
+ </html>`);
238
+ controller.close();
239
+ }
240
+ });
241
+ return new Response(stream, {
242
+ status: info.status,
243
+ headers: { "Content-Type": "text/html; charset=utf-8" }
244
+ });
245
+ }
140
246
  async function loadPagePartsFromConfig(configPath, artifacts) {
141
247
  const config = await artifacts.readPagePartsConfig(configPath);
142
248
  async function importModule(entry) {
@@ -237,8 +343,13 @@ async function fetchPageRequest(match, manifest, requestUrl, artifacts, staticBa
237
343
  });
238
344
  }
239
345
  if (fastResult.kind === "ServerError" || fastResult.kind === "ClientError") {
240
- return new Response(fastResult.message || "Error", {
241
- status: fastResult.status
346
+ return renderErrorPage(manifest, artifacts, staticBaseUrl, {
347
+ status: fastResult.status,
348
+ url: requestUrl.pathname,
349
+ title: fastResult.title,
350
+ message: fastResult.message,
351
+ code: fastResult.code,
352
+ details: fastResult.details
242
353
  });
243
354
  }
244
355
  const fastViewState = fastResult.rendered || {};
@@ -255,6 +366,8 @@ async function fetchPageRequest(match, manifest, requestUrl, artifacts, staticBa
255
366
  url: { path: requestUrl.pathname }
256
367
  };
257
368
  const headTagSources = [];
369
+ if (manifest.siteHeadTags && manifest.siteHeadTags.length > 0)
370
+ headTagSources.push(manifest.siteHeadTags);
258
371
  const slowHeadTags = cf.__slowHeadTags;
259
372
  if (slowHeadTags) headTagSources.push(...slowHeadTags);
260
373
  if (fastResult.headTags) headTagSources.push(fastResult.headTags);
@@ -538,7 +651,12 @@ const MIME_TYPES = {
538
651
  ".ttf": "font/ttf",
539
652
  ".webp": "image/webp",
540
653
  ".xml": "application/xml",
541
- ".txt": "text/plain"
654
+ ".txt": "text/plain",
655
+ // Agent-readiness resources (DL#217): SKILL.md + packaged skill archives.
656
+ ".md": "text/markdown",
657
+ ".gz": "application/gzip",
658
+ // `.tar.gz` resolves via its last extension `.gz`
659
+ ".tar": "application/x-tar"
542
660
  };
543
661
  async function fetchStaticFile(pathname, frontendDir) {
544
662
  const normalizedBase = path.resolve(frontendDir);
@@ -551,13 +669,15 @@ async function fetchStaticFile(pathname, frontendDir) {
551
669
  const contentType = MIME_TYPES[ext] || "application/octet-stream";
552
670
  const isHashed = /[-][a-zA-Z0-9_-]{6,}\./.test(path.basename(candidate));
553
671
  const cacheControl = isHashed ? "public, max-age=31536000, immutable" : "public, max-age=3600";
554
- return new Response(new Uint8Array(content), {
555
- headers: {
556
- "Content-Type": contentType,
557
- "Content-Length": String(content.length),
558
- "Cache-Control": cacheControl
559
- }
560
- });
672
+ const headers = {
673
+ "Content-Type": contentType,
674
+ "Content-Length": String(content.length),
675
+ "Cache-Control": cacheControl
676
+ };
677
+ if (pathname.startsWith("/.well-known/")) {
678
+ headers["Access-Control-Allow-Origin"] = "*";
679
+ }
680
+ return new Response(new Uint8Array(content), { headers });
561
681
  } catch {
562
682
  }
563
683
  }
@@ -633,6 +753,7 @@ export {
633
753
  isActionRequest as d,
634
754
  registerActionsFromModules as e,
635
755
  fetchActionRequest as f,
756
+ renderErrorPage as g,
636
757
  initializeServices as i,
637
758
  loadPagePartsFromConfig as l,
638
759
  matchRequest as m,
@@ -1,5 +1,6 @@
1
1
  import * as _jay_framework_ssr_runtime from '@jay-framework/ssr-runtime';
2
2
  import { JayHtmlHeadMeta, JayHtmlScript, DevServerPagePart, HeadlessInstanceComponent, ForEachHeadlessInstance, ActionRegistry } from '@jay-framework/stack-server-runtime';
3
+ import { HeadTag } from '@jay-framework/fullstack-component';
3
4
 
4
5
  interface RouteManifest {
5
6
  version: string;
@@ -8,6 +9,50 @@ interface RouteManifest {
8
9
  routes: RouteEntry[];
9
10
  actions: ActionEntry[];
10
11
  plugins: PluginEntry[];
12
+ /**
13
+ * Site-level JSON-LD (Organization + WebSite) head tags, generated once at build from
14
+ * `.jay` `site` config and injected into every served page's `<head>` (DL#213). Empty/absent
15
+ * when `site.name` is not configured.
16
+ */
17
+ siteHeadTags?: HeadTag[];
18
+ /**
19
+ * Compiled error pages keyed by status token (`"404"`, `"502"`, `"5xx"`, …) (DL#218).
20
+ * Resolved by status (exact → family), never by URL — kept out of `routes[]` so they are
21
+ * non-navigable and excluded from sitemap/llms.txt/robots/internal-link validation.
22
+ */
23
+ errorPages?: Record<string, ErrorPageEntry>;
24
+ }
25
+ /**
26
+ * A compiled error page (DL#218). Mirrors the per-route compiled refs of a single-instance
27
+ * route, minus URL/param machinery: an error page is resolved by status, not by matching a URL.
28
+ */
29
+ interface ErrorPageEntry {
30
+ /** Status token this page serves — exact (`"404"`, `"502"`) or family (`"4xx"`, `"5xx"`). */
31
+ token: string;
32
+ /** Per-page server element path (relative to backend dir). */
33
+ serverElementPath: string;
34
+ /** Per-page CSS file path (relative to frontend dir). */
35
+ routeCssPath?: string;
36
+ /** Per-page hydrate script path (relative to frontend dir). */
37
+ routeHydratePath?: string;
38
+ /** Per-page client bundle path (relative to frontend dir). */
39
+ routeClientBundlePath?: string;
40
+ /** External @import URLs extracted from CSS at build time. */
41
+ cssImports?: string[];
42
+ /** Head metadata from the error page's jay-html <head>. */
43
+ headMeta?: JayHtmlHeadMeta;
44
+ /** Passthrough `<script jay-script="allow">` tags from the template. */
45
+ scripts?: JayHtmlScript[];
46
+ /** Shared chunk package names this page's hydrate/client bundle imports (DL#177). */
47
+ sharedDeps?: string[];
48
+ /** trackBy map threaded to the client hydration entry. */
49
+ trackByMap?: Record<string, string>;
50
+ /**
51
+ * Optional pre-rendered slow (static chrome) ViewState. Unused in v1 — an error page is a
52
+ * pass-through of the injected `error` ViewState (DL#218 Q6); its static chrome renders
53
+ * from the template without slow data. The renderer merges `slowCache?.slowViewState ?? {}`.
54
+ */
55
+ slowCache?: CacheEntry;
11
56
  }
12
57
  interface RouteSegment {
13
58
  type: 'static' | 'param' | 'optional' | 'catchAll' | 'optionalCatchAll';
@@ -82,6 +127,14 @@ interface BuildOptions {
82
127
  minify?: boolean;
83
128
  /** Site base URL for sitemap generation (e.g. "https://example.com"). */
84
129
  siteBaseUrl?: string;
130
+ /** Site identity for Organization + WebSite JSON-LD generation (DL#213). */
131
+ site?: {
132
+ name?: string;
133
+ baseUrl?: string;
134
+ logo?: string;
135
+ description?: string;
136
+ sameAs?: string[];
137
+ };
85
138
  }
86
139
  interface ServerElementModule {
87
140
  renderToStream: (vs: object, ctx: _jay_framework_ssr_runtime.ServerRenderContext) => void;
@@ -204,4 +257,4 @@ interface PreImportedPlugin {
204
257
  declare function initializeServicesFromModules(plugins: PreImportedPlugin[], label: string): Promise<void>;
205
258
  declare function initializeServices(buildDir: string, projectRoot: string, label: string): Promise<void>;
206
259
 
207
- export { type ActionEntry as A, type BuildMetadata as B, type CacheEntry as C, FilesystemArtifactStore as F, type InstanceEntry as I, type MatchResult as M, type PageModule as P, type RouteManifest as R, type ServerElementModule as S, type RouteEntry as a, type ArtifactStore as b, type BuildOptions as c, type PluginEntry as d, type PreImportedPlugin as e, type RouteSegment as f, fetchActionRequest as g, fetchPageRequest as h, fetchStaticFile as i, initializeServices as j, initializeServicesFromModules as k, isActionRequest as l, loadPagePartsFromConfig as m, matchRequest as n, registerActionsFromModules as o, registerActionsFromManifest as r };
260
+ export { type ArtifactStore as A, type BuildMetadata as B, type CacheEntry as C, type ErrorPageEntry as E, FilesystemArtifactStore as F, type InstanceEntry as I, type MatchResult as M, type PageModule as P, type RouteManifest as R, type ServerElementModule as S, type RouteEntry as a, type ActionEntry as b, type BuildOptions as c, type PluginEntry as d, type PreImportedPlugin as e, type RouteSegment as f, fetchActionRequest as g, fetchPageRequest as h, fetchStaticFile as i, initializeServices as j, initializeServicesFromModules as k, isActionRequest as l, loadPagePartsFromConfig as m, matchRequest as n, registerActionsFromModules as o, registerActionsFromManifest as r };
@@ -1,3 +1,4 @@
1
- export { b as ArtifactStore, C as CacheEntry, F as FilesystemArtifactStore, I as InstanceEntry, M as MatchResult, e as PreImportedPlugin, a as RouteEntry, R as RouteManifest, S as ServerElementModule, g as fetchActionRequest, h as fetchPageRequest, i as fetchStaticFile, j as initializeServices, k as initializeServicesFromModules, l as isActionRequest, n as matchRequest, r as registerActionsFromManifest, o as registerActionsFromModules } from './serve-index-CUXzYZBC.js';
1
+ export { A as ArtifactStore, C as CacheEntry, F as FilesystemArtifactStore, I as InstanceEntry, M as MatchResult, e as PreImportedPlugin, a as RouteEntry, R as RouteManifest, S as ServerElementModule, g as fetchActionRequest, h as fetchPageRequest, i as fetchStaticFile, j as initializeServices, k as initializeServicesFromModules, l as isActionRequest, n as matchRequest, r as registerActionsFromManifest, o as registerActionsFromModules } from './serve-index-C28bP6tW.js';
2
2
  import '@jay-framework/ssr-runtime';
3
3
  import '@jay-framework/stack-server-runtime';
4
+ import '@jay-framework/fullstack-component';
@@ -1,4 +1,4 @@
1
- import { F, f, a, b, i, c, d, m, r, e } from "./init-services-XQt5vxN_.js";
1
+ import { F, f, a, b, i, c, d, m, r, e } from "./init-services-CG_wDJmY.js";
2
2
  export {
3
3
  F as FilesystemArtifactStore,
4
4
  f as fetchActionRequest,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jay-framework/production-server",
3
- "version": "0.25.1",
3
+ "version": "0.25.3",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -30,14 +30,14 @@
30
30
  "test:watch": "vitest"
31
31
  },
32
32
  "dependencies": {
33
- "@jay-framework/fullstack-component": "^0.25.1",
34
- "@jay-framework/logger": "^0.25.1",
35
- "@jay-framework/ssr-runtime": "^0.25.1",
36
- "@jay-framework/stack-server-runtime": "^0.25.1",
37
- "@jay-framework/view-state-merge": "^0.25.1"
33
+ "@jay-framework/fullstack-component": "^0.25.3",
34
+ "@jay-framework/logger": "^0.25.3",
35
+ "@jay-framework/ssr-runtime": "^0.25.3",
36
+ "@jay-framework/stack-server-runtime": "^0.25.3",
37
+ "@jay-framework/view-state-merge": "^0.25.3"
38
38
  },
39
39
  "devDependencies": {
40
- "@jay-framework/dev-environment": "^0.25.1",
40
+ "@jay-framework/dev-environment": "^0.25.3",
41
41
  "@types/node": "^22.15.21",
42
42
  "rimraf": "^5.0.5",
43
43
  "tsup": "^8.0.1",