@jay-framework/production-server 0.25.0 → 0.25.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/dist/index.d.ts CHANGED
@@ -1,9 +1,60 @@
1
- import { R as RouteManifest, a as RouteEntry } from './serve-index-C2Tfaz1E.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-C2Tfaz1E.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-CVQ-U9iX.js";
7
- import { c, e } from "./init-services-CVQ-U9iX.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
  });
@@ -298,10 +439,11 @@ async function rebuildInstance(route, params, backendDir, rebuildSuffix) {
298
439
  } catch (err) {
299
440
  return { status: "skipped", reason: `Failed to load page-parts.json: ${err.message}` };
300
441
  }
442
+ const pageUrl = buildUrlFromManifest(route.pattern, params);
301
443
  const slowPhase = new DevSlowlyChangingPhase();
302
444
  const slowResult = await slowPhase.runSlowlyForPage(
303
445
  params,
304
- { params },
446
+ { language: "en", url: pageUrl },
305
447
  pageParts.parts,
306
448
  pageParts.discoveredInstances,
307
449
  pageParts.headlessInstanceComponents
@@ -324,7 +466,7 @@ async function rebuildInstance(route, params, backendDir, rebuildSuffix) {
324
466
  {
325
467
  pageViewState: slowViewState,
326
468
  pageParams: params,
327
- pageProps: { language: "en", url: "" }
469
+ pageProps: { language: "en", url: pageUrl }
328
470
  }
329
471
  );
330
472
  if (instanceSlowResult) {
@@ -464,6 +606,12 @@ async function rebuild(options) {
464
606
  const urlCount = await generateSitemap(manifest, options.siteBaseUrl, sitemapPath);
465
607
  logger.info(`[Rebuild] Sitemap regenerated: ${urlCount} URLs`);
466
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
+ }
467
615
  if (orphanedFiles.length > 0) {
468
616
  await appendCleanupManifest(buildDir, orphanedFiles);
469
617
  logger.info(`[Rebuild] ${orphanedFiles.length} orphaned file(s) queued for cleanup`);
@@ -689,10 +837,14 @@ function readBody(req) {
689
837
  }
690
838
  export {
691
839
  FilesystemArtifactStore,
840
+ buildUrlFromManifest,
692
841
  cleanupOrphanedFiles,
693
842
  fetchActionRequest,
694
843
  fetchPageRequest,
695
844
  fetchStaticFile,
845
+ generateLlmsTxt,
846
+ generateRobotsTxtIfAbsent,
847
+ generateSiteJsonLd,
696
848
  generateSitemap,
697
849
  initializeServices,
698
850
  c as initializeServicesFromModules,
@@ -703,6 +855,7 @@ export {
703
855
  rebuildContract,
704
856
  registerActionsFromManifest,
705
857
  e as registerActionsFromModules,
858
+ renderErrorPage,
706
859
  resolveContractToRoutes,
707
860
  startMainServer,
708
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, 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,12 +366,13 @@ 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);
261
374
  const templateHeadTags = headMetaToHeadTags(route.headMeta, fullViewState);
262
- if (templateHeadTags.length > 0) headTagSources.push(templateHeadTags);
263
- const headTags = headTagSources.length > 0 ? mergeHeadTags(headTagSources) : [];
375
+ const headTags = headTagSources.length > 0 || templateHeadTags.length > 0 ? mergeHeadTags(headTagSources, templateHeadTags) : [];
264
376
  const hasCustomTitle = headTags.some((t) => t.tag?.toLowerCase() === "title");
265
377
  const titleTag = hasCustomTitle ? "" : " <title>Vite + TS</title>\n";
266
378
  const preconnectTags = headTags.filter(
@@ -271,6 +383,8 @@ async function fetchPageRequest(match, manifest, requestUrl, artifacts, staticBa
271
383
  );
272
384
  const preconnectHtml = preconnectTags.length > 0 ? serializeHeadTags(preconnectTags) + "\n" : "";
273
385
  const headTagsHtml = otherHeadTags.length > 0 ? serializeHeadTags(otherHeadTags) + "\n" : "";
386
+ const headScriptsHtml = serializeScripts(route.scripts, "head");
387
+ const bodyScriptsHtml = serializeScripts(route.scripts, "body");
274
388
  const serverElementPath = route.serverElementPath || instance.serverElementPath;
275
389
  const tLoadStart = Date.now();
276
390
  const serverElement = await artifacts.loadServerElement(serverElementPath);
@@ -292,7 +406,7 @@ async function fetchPageRequest(match, manifest, requestUrl, artifacts, staticBa
292
406
  const stream = new ReadableStream({
293
407
  async start(controller) {
294
408
  const write = (s) => controller.enqueue(encoder.encode(s));
295
- const headParts = [headTagsHtml, modulePreloads, cssLink].filter(Boolean).join("\n");
409
+ const headParts = [headTagsHtml, modulePreloads, cssLink, headScriptsHtml].filter(Boolean).join("\n");
296
410
  write(`<!doctype html>
297
411
  <html lang="en">
298
412
  <head>
@@ -340,7 +454,7 @@ ${headParts}
340
454
  const _t=performance.now();
341
455
  await init(${initArgs});
342
456
  console.log('[jay] hydrate: '+(performance.now()-_t).toFixed(1)+'ms');
343
- <\/script>
457
+ <\/script>${bodyScriptsHtml ? "\n " + bodyScriptsHtml : ""}
344
458
  </body>
345
459
  </html>`);
346
460
  controller.close();
@@ -537,7 +651,12 @@ const MIME_TYPES = {
537
651
  ".ttf": "font/ttf",
538
652
  ".webp": "image/webp",
539
653
  ".xml": "application/xml",
540
- ".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"
541
660
  };
542
661
  async function fetchStaticFile(pathname, frontendDir) {
543
662
  const normalizedBase = path.resolve(frontendDir);
@@ -550,13 +669,15 @@ async function fetchStaticFile(pathname, frontendDir) {
550
669
  const contentType = MIME_TYPES[ext] || "application/octet-stream";
551
670
  const isHashed = /[-][a-zA-Z0-9_-]{6,}\./.test(path.basename(candidate));
552
671
  const cacheControl = isHashed ? "public, max-age=31536000, immutable" : "public, max-age=3600";
553
- return new Response(new Uint8Array(content), {
554
- headers: {
555
- "Content-Type": contentType,
556
- "Content-Length": String(content.length),
557
- "Cache-Control": cacheControl
558
- }
559
- });
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 });
560
681
  } catch {
561
682
  }
562
683
  }
@@ -632,6 +753,7 @@ export {
632
753
  isActionRequest as d,
633
754
  registerActionsFromModules as e,
634
755
  fetchActionRequest as f,
756
+ renderErrorPage as g,
635
757
  initializeServices as i,
636
758
  loadPagePartsFromConfig as l,
637
759
  matchRequest as m,
@@ -1,5 +1,6 @@
1
1
  import * as _jay_framework_ssr_runtime from '@jay-framework/ssr-runtime';
2
- import { JayHtmlHeadMeta, DevServerPagePart, HeadlessInstanceComponent, ForEachHeadlessInstance, ActionRegistry } from '@jay-framework/stack-server-runtime';
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';
@@ -32,6 +77,8 @@ interface RouteEntry {
32
77
  cssImports?: string[];
33
78
  /** Head metadata from jay-html <head> (title, meta tags). */
34
79
  headMeta?: JayHtmlHeadMeta;
80
+ /** Passthrough `<script jay-script="allow">` tags from the jay-html template (DL#149 Phase 2). */
81
+ scripts?: JayHtmlScript[];
35
82
  /** True when page has static <meta name="robots" content="noindex"> */
36
83
  noIndex?: boolean;
37
84
  instances: InstanceEntry[];
@@ -80,6 +127,14 @@ interface BuildOptions {
80
127
  minify?: boolean;
81
128
  /** Site base URL for sitemap generation (e.g. "https://example.com"). */
82
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
+ };
83
138
  }
84
139
  interface ServerElementModule {
85
140
  renderToStream: (vs: object, ctx: _jay_framework_ssr_runtime.ServerRenderContext) => void;
@@ -202,4 +257,4 @@ interface PreImportedPlugin {
202
257
  declare function initializeServicesFromModules(plugins: PreImportedPlugin[], label: string): Promise<void>;
203
258
  declare function initializeServices(buildDir: string, projectRoot: string, label: string): Promise<void>;
204
259
 
205
- 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-C2Tfaz1E.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-CVQ-U9iX.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.0",
3
+ "version": "0.25.2",
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.0",
34
- "@jay-framework/logger": "^0.25.0",
35
- "@jay-framework/ssr-runtime": "^0.25.0",
36
- "@jay-framework/stack-server-runtime": "^0.25.0",
37
- "@jay-framework/view-state-merge": "^0.25.0"
33
+ "@jay-framework/fullstack-component": "^0.25.2",
34
+ "@jay-framework/logger": "^0.25.2",
35
+ "@jay-framework/ssr-runtime": "^0.25.2",
36
+ "@jay-framework/stack-server-runtime": "^0.25.2",
37
+ "@jay-framework/view-state-merge": "^0.25.2"
38
38
  },
39
39
  "devDependencies": {
40
- "@jay-framework/dev-environment": "^0.25.0",
40
+ "@jay-framework/dev-environment": "^0.25.2",
41
41
  "@types/node": "^22.15.21",
42
42
  "rimraf": "^5.0.5",
43
43
  "tsup": "^8.0.1",