@jay-framework/jay-stack-cli 0.25.1 → 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.
@@ -46,6 +46,7 @@ There is no standalone "interactive" phase. Any tag with `type: interactive` (re
46
46
  | [design-system-guide.md](design-system-guide.md) | Design-system elements — **create** (author template + contract, `jay-content` slots) and **use** (flatten with `template=`, drift, `override` facets, `sync`) |
47
47
  | [jay-html-styling.md](jay-html-styling.md) | Styling: inline, external, dynamic style bindings, class bindings |
48
48
  | [routing.md](routing.md) | Directory-based routing: page structure, dynamic routes, route priority |
49
+ | [error-pages-guide.md](error-pages-guide.md) | Error pages: reserved `404`/`5xx` status folders, the `error` headless, bindings, exact→family resolution |
49
50
  | [navigation-patterns.md](navigation-patterns.md) | Active menu/sidebar patterns using `jay.url.path`, `===`, and `^=` operators |
50
51
  | [contracts-and-plugins.md](contracts-and-plugins.md) | Reading contracts, plugin.yaml, .jay-action files, and the materialized indexes |
51
52
  | [Contract Authoring Guide](../contracts/GUIDE.md) | Writing contracts: syntax, page/component/linked contracts, examples |
@@ -0,0 +1,109 @@
1
+ # Error Pages
2
+
3
+ Error pages render your site's chrome when a request fails — a 404 for an unknown URL, a 5xx for a
4
+ server error — instead of a bare plain-text message. You author them exactly like any other page,
5
+ in a reserved top-level folder named for the HTTP status they cover.
6
+
7
+ ## Where they live
8
+
9
+ Create error pages in `src/pages/` in a folder whose name is an HTTP **status token**:
10
+
11
+ ```
12
+ src/pages/
13
+ 404/page.jay-html # exact: 404 Not Found
14
+ 5xx/page.jay-html # family: any 500–599
15
+ 502/page.jay-html # exact: 502 Bad Gateway (optional, overrides 5xx for 502)
16
+ ```
17
+
18
+ A status token is either:
19
+
20
+ - **Exact** — three digits, `100`–`599` (e.g. `404`, `502`).
21
+ - **Family** — a leading digit then `xx` (e.g. `4xx`, `5xx`), covering that whole class.
22
+
23
+ v1 renders only **4xx** and **5xx**. Reserve others with a folder, but they are not served yet.
24
+
25
+ These folders are **not navigable routes** — visiting `/404` or `/5xx` directly returns a normal 404,
26
+ and they never appear in the sitemap or internal-link checks. The error page is selected by the
27
+ response **status**, not by its URL.
28
+
29
+ ## Resolution: exact → family → plain text
30
+
31
+ For a given status, Jay picks the most specific page:
32
+
33
+ 1. **Exact** match — `pages/502/` for a 502.
34
+ 2. **Family** fallback — `pages/5xx/` for a 502 when there's no `pages/502/`.
35
+ 3. **Plain text** — if neither exists, a built-in plain-text response.
36
+
37
+ So a single `pages/5xx/` covers every server error, and you add `pages/502/` only where you want
38
+ bespoke copy. Minimum recommended set: **`404` and `5xx`** (the `validate` command warns if either is
39
+ missing — see below).
40
+
41
+ ## Authoring an error page
42
+
43
+ Import the built-in `error` headless contract and bind its fields. No plugin install is needed —
44
+ `jay-stack`/`error` is built in:
45
+
46
+ ```html
47
+ <html>
48
+ <head>
49
+ <script
50
+ type="application/jay-headless"
51
+ plugin="jay-stack"
52
+ contract="error"
53
+ key="error"
54
+ ></script>
55
+ <script type="application/jay-data">
56
+ data:
57
+ </script>
58
+ <title>{error.title}</title>
59
+ <meta name="description" content="{error.message}" />
60
+ </head>
61
+ <body>
62
+ <main class="error-page">
63
+ <h1>{error.status} — {error.title}</h1>
64
+ <p>{error.message}</p>
65
+ <ul forEach="error.details" trackBy="key">
66
+ <li>{key}: {value}</li>
67
+ </ul>
68
+ <a href="/">Go home</a>
69
+ </main>
70
+ </body>
71
+ </html>
72
+ ```
73
+
74
+ ### Available bindings
75
+
76
+ | Binding | Type | Meaning |
77
+ | ----------------- | ------------------ | --------------------------------------------------------- |
78
+ | `{error.status}` | number | The HTTP status (e.g. `404`, `503`). |
79
+ | `{error.title}` | string | Short headline — supplied by logic or a sensible default. |
80
+ | `{error.message}` | string | Longer explanation — supplied or defaulted. |
81
+ | `{error.code}` | string | App-specific error code; `''` when none. |
82
+ | `error.details` | `{ key, value }[]` | Key/value pairs for `forEach` — empty when none. |
83
+
84
+ `error.title` and `error.message` always have a value: when the developer's code supplies none, Jay
85
+ fills a per-status default (e.g. `404 → "Page Not Found"`). You can rely on them being present.
86
+
87
+ ## No fast/slow data phase
88
+
89
+ An error page is a **pass-through** of the injected `error` data — it runs **no** author fast or slow
90
+ phase and cannot fetch its own request-time data (rendering an error page must never itself fail). Its
91
+ static chrome (layout, nav, copy) is still pre-rendered at build. Keep the page to static markup plus
92
+ the `error.*` bindings.
93
+
94
+ ## Validation
95
+
96
+ `jay-stack validate` warns when the minimum pages (`404` and `5xx`) are absent — a `4xx` family page
97
+ counts as covering `404`. If your site intentionally accepts the plain-text fallback, suppress the
98
+ nudge in `.jay`:
99
+
100
+ ```yaml
101
+ validations:
102
+ allow-no-error-pages: true
103
+ ```
104
+
105
+ ## See also
106
+
107
+ - Developer side — returning enriched errors (`notFound`, `serverError5xx`, title/details):
108
+ `../developer/error-pages-guide.md`.
109
+ - Template syntax (`forEach`, bindings, expression limits): `jay-html-template-syntax.md`.
@@ -39,6 +39,13 @@ Static routes match before dynamic routes (most specific first):
39
39
  3. **`[[param]]`** — optional param
40
40
  4. **`[...param]`** — catch-all — lowest priority
41
41
 
42
+ ## Reserved Status Folders (Error Pages)
43
+
44
+ Top-level folders named for an HTTP **status token** — `404`, `5xx`, `502`, `4xx`, … — are reserved
45
+ for **error pages**, not navigable routes. They are selected by response status (not URL), never
46
+ appear in the sitemap, and visiting `/404` directly returns a normal 404. See
47
+ [error-pages-guide.md](error-pages-guide.md).
48
+
42
49
  ## Static Route Overrides
43
50
 
44
51
  A static route can override a dynamic route for a specific URL — giving one particular page a custom layout while the dynamic route handles everything else:
@@ -35,6 +35,7 @@ The developer sets up the project, configures plugins, creates page-level compon
35
35
  | [component-refs.md](component-refs.md) | Refs, collection refs, element types |
36
36
  | [component-data.md](component-data.md) | Immutable data, JSON Patch, patching |
37
37
  | [render-results.md](render-results.md) | phaseOutput, RenderPipeline, errors, redirects |
38
+ | [error-pages-guide.md](error-pages-guide.md) | Returning enriched errors (title/code/details) and which error page renders |
38
39
  | [seo-guide.md](seo-guide.md) | SEO head tags: title, meta, OG, canonical via phaseOutput |
39
40
  | [cli-commands.md](cli-commands.md) | CLI commands: setup, validate, dev, agent-kit |
40
41
  | `../references/<plugin>/` | Plugin reference data |
@@ -0,0 +1,86 @@
1
+ # Error Pages (Developer Side)
2
+
3
+ When a page or plugin render returns an error result, Jay renders the matching **error page** (see the
4
+ designer guide for authoring them) with the error data injected as the `error` ViewState. This guide
5
+ covers how to return enriched errors from `page.ts` so they surface well on the rendered page.
6
+
7
+ ## Returning errors
8
+
9
+ Return an error result from any render phase to stop rendering and show an error page:
10
+
11
+ ```typescript
12
+ import {
13
+ notFound,
14
+ badRequest,
15
+ unauthorized,
16
+ forbidden,
17
+ serverError5xx,
18
+ clientError4xx,
19
+ } from '@jay-framework/fullstack-component';
20
+
21
+ if (!product) return notFound('The product you are looking for may have been removed.');
22
+
23
+ if (!input.query) return badRequest('Query is required');
24
+
25
+ if (!session) return unauthorized('Please log in');
26
+
27
+ if (!canAccess) return forbidden('Access denied');
28
+
29
+ return clientError4xx(429, 'Rate limit exceeded');
30
+
31
+ return serverError5xx(503, 'The inventory service is temporarily unavailable');
32
+ ```
33
+
34
+ An **uncaught exception** in a render phase becomes a 500 and renders the `5xx` page — the stack is
35
+ logged, never shown.
36
+
37
+ ## Enriching the error page
38
+
39
+ Every error constructor takes an optional options bag after the message: `{ title?, code?, details? }`.
40
+ These map directly onto the `error.*` bindings the error page can render.
41
+
42
+ ```typescript
43
+ return notFound('The product you are looking for may have been removed.', {
44
+ title: 'Product not found',
45
+ });
46
+ // → error.title = "Product not found", error.message = "The product…"
47
+
48
+ return serverError5xx(504, 'Upstream timed out', {
49
+ title: 'This is taking too long',
50
+ code: 'UPSTREAM_TIMEOUT',
51
+ details: { upstream: 'inventory-service', waitedMs: '10000' },
52
+ });
53
+ // → renders pages/504 if present, else pages/5xx;
54
+ // error.details lists the key/value pairs via forEach="error.details".
55
+ ```
56
+
57
+ | Field | Binding | Notes |
58
+ | --------- | ----------------- | ----------------------------------------------------------- |
59
+ | status | `{error.status}` | The numeric status you pass (`notFound` → 404). |
60
+ | message | `{error.message}` | First positional arg; defaulted per status when omitted. |
61
+ | `title` | `{error.title}` | Short headline; defaulted per status when omitted. |
62
+ | `code` | `{error.code}` | App-specific code; `''` on the page when omitted. |
63
+ | `details` | `error.details` | `Record<string, unknown>` → stringified `{ key, value }[]`. |
64
+
65
+ ## Which page renders
66
+
67
+ Resolution is by status, most specific first: exact (`pages/504/`) → family (`pages/5xx/`) → built-in
68
+ plain text. So `serverError5xx(503, …)` renders `pages/503/` if it exists, otherwise `pages/5xx/`.
69
+ `notFound(…)` renders `pages/404/`, or `pages/4xx/` as a family fallback.
70
+
71
+ ## RenderPipeline
72
+
73
+ The pipeline factory methods mirror the constructors and accept the same options bag:
74
+
75
+ ```typescript
76
+ const P = RenderPipeline.for<SlowViewState, CarryForward>();
77
+
78
+ return P.try(() => db.getProduct(props.slug))
79
+ .map((product) => product ?? P.notFound('Product not found', { title: 'Product not found' }))
80
+ .toPhaseOutput((product) => ({ viewState: { title: product.name }, carryForward: {} }));
81
+ ```
82
+
83
+ ## See also
84
+
85
+ - Full render-result reference (success, redirects, pipeline): `render-results.md`.
86
+ - Authoring the error page markup and bindings: `../designer/error-pages-guide.md`.
@@ -87,6 +87,22 @@ return clientError4xx(429, 'Rate limit exceeded');
87
87
  return serverError5xx(500, 'Database connection failed');
88
88
  ```
89
89
 
90
+ Each error constructor takes an optional options bag — `{ title?, code?, details? }` — after the
91
+ message. These surface as `error.title` / `error.code` / `error.details` on the error page:
92
+
93
+ ```typescript
94
+ return notFound('This product may have been removed.', { title: 'Product not found' });
95
+
96
+ return serverError5xx(504, 'Upstream timed out', {
97
+ title: 'This is taking too long',
98
+ details: { upstream: 'inventory-service', waitedMs: '10000' },
99
+ });
100
+ ```
101
+
102
+ The matching **error page** (`src/pages/404/`, `src/pages/5xx/`, …) renders these fields. See
103
+ [error-pages-guide.md](error-pages-guide.md) for the full field reference and status resolution, and
104
+ `../designer/error-pages-guide.md` for authoring the page markup.
105
+
90
106
  ## Redirects
91
107
 
92
108
  ```typescript
@@ -53,10 +53,69 @@ interface HeadTag {
53
53
  // Canonical URL
54
54
  { tag: 'link', attrs: { rel: 'canonical', href: 'https://example.com/products/slug' } }
55
55
 
56
- // JSON-LD structured data
56
+ // JSON-LD structured data — prefer the jsonLd() helper (see below) over hand-building this tag
57
57
  { tag: 'script', attrs: { type: 'application/ld+json' }, children: JSON.stringify(structuredData) }
58
58
  ```
59
59
 
60
+ ## Structured Data (JSON-LD)
61
+
62
+ JSON-LD tells search engines and AI agents what a page _is_ (a product, article, recipe…). There are three ways to emit it — pick by where the data lives:
63
+
64
+ | Where the data lives | How to emit | Who writes it |
65
+ | ------------------------------------------- | ----------------------------------------------------------------------- | ------------------- |
66
+ | Site-wide identity (org name, logo, social) | `.jay` `site.*` config — auto-emitted as `Organization` + `WebSite` | Developer (config) |
67
+ | Per-page dynamic data (product, article) | `jsonLd(data)` from `page.ts`, returned in `headTags` | Developer (page.ts) |
68
+ | Static, hand-authored for one page | `<script type="application/ld+json">` directly in the jay-html `<head>` | Designer (jay-html) |
69
+
70
+ ### 1. Site-wide (`.jay` config)
71
+
72
+ Set site identity once; the production build auto-emits `Organization` + `WebSite` JSON-LD into every page's `<head>`:
73
+
74
+ ```yaml
75
+ # .jay
76
+ site:
77
+ baseUrl: https://example.com # also drives sitemap.xml
78
+ name: Example Inc # required to emit Organization/WebSite JSON-LD
79
+ logo: /logo.png # relative paths are resolved against baseUrl
80
+ description: What we do
81
+ sameAs:
82
+ - https://twitter.com/example
83
+ - https://github.com/example
84
+ ```
85
+
86
+ Without `site.name`, no site-level JSON-LD is emitted (validation warns). Site-level JSON-LD is a **production** artifact (like `sitemap.xml`) — the dev server does not inject it.
87
+
88
+ ### 2. Per-page (`jsonLd()` helper)
89
+
90
+ Use the `jsonLd()` helper from `@jay-framework/fullstack-component` for dynamic, per-page structured data — it builds the correct `HeadTag` and the framework escapes it breakout-safe:
91
+
92
+ ```typescript
93
+ import { jsonLd } from '@jay-framework/fullstack-component';
94
+
95
+ return phaseOutput(viewState, carryForward, {
96
+ headTags: [
97
+ { tag: 'title', children: `${product.name} | Store` },
98
+ jsonLd({
99
+ '@context': 'https://schema.org',
100
+ '@type': 'Product',
101
+ name: product.name,
102
+ description: product.description,
103
+ offers: { '@type': 'Offer', price: product.price, priceCurrency: 'USD' },
104
+ }),
105
+ ],
106
+ });
107
+ ```
108
+
109
+ ### 3. Static (jay-html `<head>`)
110
+
111
+ A `<script type="application/ld+json">` written directly in a jay-html `<head>` is **first-class** — no `jay-script="allow"` needed, no inline-script warning. The body is emitted breakout-safe. See the designer SEO guide.
112
+
113
+ ### Validation
114
+
115
+ - `jay-stack validate` errors if a template `application/ld+json` block is not valid JSON or is missing `@context`/`@type`.
116
+ - `jay-stack validate` warns if `.jay` `site.name` is absent (no site-level JSON-LD).
117
+ - Never HTML-escape a JSON-LD body yourself — the framework escapes `<`, `>`, `&` as `\uXXXX` so the JSON stays valid and cannot break out of the `<script>`.
118
+
60
119
  ## Mapping Generic SEO Data
61
120
 
62
121
  If the data source provides a generic structure (array of tags with type/props/children), map it:
@@ -91,3 +150,53 @@ return phaseOutput(viewState, carryForward, { headTags });
91
150
 
92
151
  - Head tags from components inside `forEach` are ignored
93
152
  - The framework handles HTML escaping automatically
153
+
154
+ ## Agent-Discovery Files (DL#214)
155
+
156
+ A Jay production build emits the conventional files agents and crawlers look for. Most are generated from the route manifest + `.jay` `site` config — you mostly configure `site.*`; one file (`ard.json`) you author.
157
+
158
+ ### Generated automatically (override by placing your own file in `public/`)
159
+
160
+ | File | When generated | Source |
161
+ | -------------- | ------------------------------- | ------------------------------------------------------------------------ |
162
+ | `/sitemap.xml` | `site.baseUrl` set | route manifest |
163
+ | `/llms.txt` | `site.name` set | `site.name`/`site.description` + per-page `<title>`/`<meta description>` |
164
+ | `/robots.txt` | always (if you didn't ship one) | default allow-all + `Sitemap:` line when `site.baseUrl` set |
165
+
166
+ `llms.txt` is a markdown index of the site for LLMs: an H1 (`site.name`), a summary (`site.description`), and per-section links with each page's title + description. An author file at `public/llms.txt` / `public/robots.txt` always wins — the build never clobbers it.
167
+
168
+ ### `/.well-known/*` — just works in `public/`
169
+
170
+ Drop any `.well-known` file (agent/tool manifests, `security.txt`, etc.) into `public/.well-known/` and it is served at `/.well-known/*` in **both** dev and production. (The dev server opts into serving dotdirs; production already did.)
171
+
172
+ ### `/.well-known/ard.json` — you author this
173
+
174
+ Agentic Resource Discovery ([ARD v0.91](https://agenticresourcediscovery.org/spec)) lets agents discover a site's resources/APIs. The framework does **not** generate it — `jay-stack validate` warns when it's absent and validates its shape when present. Author `public/.well-known/ard.json`:
175
+
176
+ ```json
177
+ {
178
+ "entries": [
179
+ {
180
+ "identifier": "urn:air:acme:weather:node",
181
+ "displayName": "Weather Data Node",
182
+ "type": "application/mcp-server-card+json",
183
+ "url": "https://api.acme.com/mcp/weather.json",
184
+ "representativeQueries": ["weather in Paris", "forecast for tomorrow"],
185
+ "description": "Live weather data and forecasts",
186
+ "tags": ["weather", "forecast"]
187
+ }
188
+ ]
189
+ }
190
+ ```
191
+
192
+ Each entry **requires**: `identifier` (URN `urn:air:<publisher>:<namespace>:<name>`), `displayName`, `type` (an IANA media type), and **exactly one** of `url` or `data`. `representativeQueries` (2–5) is strongly recommended — agents use it to find your resource. `ard.json` is pinned to v0.91 ("Proposal" status — expect the schema to move).
193
+
194
+ **Predecessor alias:** ARD renamed `ai-catalog.json` → `ard.json`. For older clients, also write a copy at `public/.well-known/ai-catalog.json`. `ard.json` is canonical; validation warns if only the alias exists or the two differ.
195
+
196
+ **No agent resources?** If the site exposes nothing agent-callable, suppress the presence nudge instead of authoring an empty file — add to `.jay` (it governs the project-level `ard.json`, so it lives in project config, not a page's `application/jay-validations` script):
197
+
198
+ ```yaml
199
+ # .jay
200
+ validations:
201
+ allow-no-ard: true
202
+ ```
@@ -42,6 +42,15 @@ jay-stack validate-plugin -v
42
42
  - Contract files referenced in `plugin.yaml` exist
43
43
  - Component export names are valid strings (not file paths)
44
44
  - Action metadata files (`.jay-action`) exist
45
+ - Each `routes[].jayHtml` file **parses and compiles** (same generate stage as `jay-stack validate` — catches invalid template expressions before the dev server)
46
+
47
+ During plugin development, also compile-check **source** templates (paths under `lib/`, not only `dist/`):
48
+
49
+ ```bash
50
+ jay-stack validate -p lib/pages/my-route --compile-only
51
+ ```
52
+
53
+ `--compile-only` skips project rules (broken links, DOM lint, design system) so it is safe inside a plugin package `validate` script alongside `validate-plugin`.
45
54
 
46
55
  ### Capability-aware structure
47
56