@jay-framework/jay-stack-cli 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.
@@ -60,11 +60,11 @@ See [syntax.md](syntax.md) for the full YAML format: tag types, phases, data typ
60
60
 
61
61
  Start with the simplest example that matches your use case, then add complexity as needed:
62
62
 
63
- | Example | Complexity | Key patterns |
64
- | ------------------------------------------------------ | -------------- | ----------------------------------------------------------- |
65
- | [mini-cart](examples/mini-cart.md) | Trivial | Variant + interactive refs |
66
- | [cart-indicator](examples/cart-indicator.md) | Simple | Flat data, variants, phase choices |
67
- | [category-list](examples/category-list.md) | Medium | Props, repeated sub-contracts |
68
- | [product-card](examples/product-card.md) | Medium-complex | Linked sub-contracts, variants, multiple ref types |
69
- | [product-page](examples/product-page.md) | Complex | Params, nested repeated sub-contracts, linked sub-contracts |
70
- | [composing-contracts](examples/composing-contracts.md) | Pattern | How contracts link together into a hierarchy |
63
+ | Example | Complexity | Key patterns |
64
+ | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------- | ----------------------------------------------------------- |
65
+ | [mini-cart](https://github.com/jay-framework/jay/blob/main/packages/jay-stack/stack-cli/agent-kit-template/contracts/examples/mini-cart.md) | Trivial | Variant + interactive refs |
66
+ | [cart-indicator](https://github.com/jay-framework/jay/blob/main/packages/jay-stack/stack-cli/agent-kit-template/contracts/examples/cart-indicator.md) | Simple | Flat data, variants, phase choices |
67
+ | [category-list](https://github.com/jay-framework/jay/blob/main/packages/jay-stack/stack-cli/agent-kit-template/contracts/examples/category-list.md) | Medium | Props, repeated sub-contracts |
68
+ | [product-card](https://github.com/jay-framework/jay/blob/main/packages/jay-stack/stack-cli/agent-kit-template/contracts/examples/product-card.md) | Medium-complex | Linked sub-contracts, variants, multiple ref types |
69
+ | [product-page](https://github.com/jay-framework/jay/blob/main/packages/jay-stack/stack-cli/agent-kit-template/contracts/examples/product-page.md) | Complex | Params, nested repeated sub-contracts, linked sub-contracts |
70
+ | [composing-contracts](https://github.com/jay-framework/jay/blob/main/packages/jay-stack/stack-cli/agent-kit-template/contracts/examples/composing-contracts.md) | Pattern | How contracts link together into a hierarchy |
@@ -72,4 +72,4 @@ The linked contract must have a tag matching the `trackBy` field.
72
72
 
73
73
  ## See Also
74
74
 
75
- - [composing-contracts example](examples/composing-contracts.md) — Real-world composition hierarchy from wix-stores
75
+ - [composing-contracts example](https://github.com/jay-framework/jay/blob/main/packages/jay-stack/stack-cli/agent-kit-template/contracts/examples/composing-contracts.md) — Real-world composition hierarchy from wix-stores
@@ -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 |
@@ -69,8 +69,15 @@ jay-stack validate -v
69
69
 
70
70
  # JSON output
71
71
  jay-stack validate --json
72
+
73
+ # Tier 2: deeper checks against an existing build (needs `jay-stack build` first)
74
+ jay-stack validate --tier-2
72
75
  ```
73
76
 
77
+ The default run is **Tier 1** (fast, template-only). `--tier-2` adds deep checks against a build's output
78
+ (dynamic-slug links, rendered-content links, per-instance meta) — see
79
+ [validation-tier-2-guide.md](validation-tier-2-guide.md).
80
+
74
81
  Example output:
75
82
 
76
83
  ```
@@ -327,4 +327,4 @@ For repeated lists, add a **boolean variant** (e.g. `hasItems`, `hasCategories`)
327
327
  <div if="hasCategories" forEach="categories" trackBy="_id">...</div>
328
328
  ```
329
329
 
330
- Alternatively, expose a **number** data tag (`itemCount`) and use numeric comparison: `if="itemCount===0"`. See [jay-html-template-syntax.md](jay-html-template-syntax.md#expression-limits-important) and [contracts/examples/category-list.md](../contracts/examples/category-list.md).
330
+ Alternatively, expose a **number** data tag (`itemCount`) and use numeric comparison: `if="itemCount===0"`. See [jay-html-template-syntax.md](jay-html-template-syntax.md#expression-limits-important) and [contracts/examples/category-list.md](https://github.com/jay-framework/jay/blob/main/packages/jay-stack/stack-cli/agent-kit-template/contracts/examples/category-list.md).
@@ -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`.
@@ -240,7 +240,7 @@ Jay-html expressions resolve **contract or page tag names only**. They are **not
240
240
  | Empty list hint | `hasItems: boolean` variant | `if="!hasItems"` |
241
241
  | Count-based UI | `itemCount: number` data tag | `if="itemCount===0"` |
242
242
 
243
- See [contracts/examples/category-list.md](../contracts/examples/category-list.md) (`hasCategories`).
243
+ See [contracts/examples/category-list.md](https://github.com/jay-framework/jay/blob/main/packages/jay-stack/stack-cli/agent-kit-template/contracts/examples/category-list.md) (`hasCategories`).
244
244
 
245
245
  ### Common Errors
246
246
 
@@ -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:
@@ -27,6 +27,13 @@ So a warning about an `<img>` might come from a full-stack component's template,
27
27
  - **Errors** block the build. They must be fixed — there is no way to suppress them.
28
28
  - **Warnings** must be either fixed or explicitly suppressed. Do not ignore warnings — each one has a clear resolution path (add the missing attribute, or suppress via `<script type="application/jay-validations">`).
29
29
 
30
+ **Broken internal links are errors** (DL#210). `validate` resolves every `<a href>` against the project's
31
+ **static routes + public assets**. An href that resolves to nothing is an error: a placeholder (`href="#"` or
32
+ empty), a typo, or a link to a page/asset that does not exist. Links that match a **dynamic** route pattern
33
+ (e.g. `/blog/[slug]`) are **not** checked here — the concrete slug list isn't known at validate time — so they
34
+ never false-error. External URLs, `mailto:`/`tel:`, in-page `#fragments`, and `{binding}` hrefs are skipped.
35
+ Fix the path, use `<button>` for JS-driven anchors, or suppress per-page (see the `jay-stack` table below).
36
+
30
37
  One design-system rule is a **hard error**, not a warning: `REGION-CSS-NO-REF` (DL#209) — a `<jay:X>` region
31
38
  whose `template=` ships CSS but which has no `ref=`. Without a `ref` the CSS has no scope anchor, so `sync`
32
39
  silently drops it; the fix is to add a `ref` (see `design-system-guide.md` → _The shape of a region's CSS_).
@@ -126,6 +133,7 @@ The core `jay-stack validate` emits four warnings that nudge you to reuse UI thr
126
133
  | `REGION-OVERRIDE-NON-CONTENT` | `jay-validations="REGION-OVERRIDE-NON-CONTENT"` on the import |
127
134
  | `COMPONENT-NO-TEMPLATE` | `jay-stack: allow-no-template: [Contract]` |
128
135
  | `NO-DESIGN-SYSTEM` | `jay-stack: allow-no-design-system: true` |
136
+ | Broken internal link (DL#210) | `jay-stack: allow-broken-links: true` (per-page — skips all link checks for that page) |
129
137
 
130
138
  ```html
131
139
  <script type="application/jay-validations">
@@ -167,12 +175,28 @@ By default the scorecard prints **one-line totals** for the whole project:
167
175
  Run with `-v` / `--verbose` to expand the **per-page** breakdown (coverage per file, which tags are unused,
168
176
  per-template reuse counts, and `⚠ low` flags on pages under 50% coverage).
169
177
 
178
+ ## Two validation tiers
179
+
180
+ Validation comes in two tiers. This guide covers **Tier 1**, the always-on gate.
181
+
182
+ **Tier 1 — always on (`jay-stack validate`)** is the fast, template-only gate. It reads your `.jay-html` files and
183
+ route scan only — **no build needed** — so it's cheap enough for the hot agent loop and runs on every `validate`.
184
+ Everything described in this guide is Tier 1. For links, Tier 1 (DL#210) resolves every hand-authored `<a href>`
185
+ against the project's static routes + public assets and **errors** on broken internal links (typos, missing
186
+ pages), degenerate `#` / empty placeholder links, and self-links. Links matching a **dynamic** route pattern
187
+ (`/design-log/[...slug]`) are deferred — the concrete slug set isn't known without a build.
188
+
189
+ **Tier 2 — opt-in, against a build (`jay-stack validate --tier-2`)** adds deeper checks that need a build:
190
+ dynamic-slug links, broken links in rendered content, and per-instance meta/SEO. It's documented separately to
191
+ keep this guide focused — see **`validation-tier-2-guide.md`**. Run it pre-deploy / in CI.
192
+
170
193
  ## Running Validation
171
194
 
172
195
  ```bash
173
- jay-stack validate # validate all pages (+ one-line scorecard totals)
174
- jay-stack validate -v # add per-page tag coverage + scorecard detail
175
- jay-stack validate --strict # treat warnings as errors
196
+ jay-stack validate # Tier 1: all pages (+ one-line scorecard totals)
197
+ jay-stack validate -v # add per-page tag coverage + scorecard detail
198
+ jay-stack validate --strict # treat warnings as errors
199
+ jay-stack validate --tier-2 # also run Tier 2 — see validation-tier-2-guide.md
176
200
  ```
177
201
 
178
202
  Validation runs automatically during `jay-stack build`. Warnings don't block the build; errors do (with `--strict`).
@@ -0,0 +1,64 @@
1
+ # Tier 2 Validation — Deep Checks Against a Build
2
+
3
+ `jay-stack validate` runs **Tier 1**: the fast, template-only gate (links, placeholders, coverage) that needs no
4
+ build. See `validation-guide.md` for Tier 1.
5
+
6
+ **Tier 2** (`jay-stack validate --tier-2`) adds deeper checks that are only possible once a build exists. This
7
+ guide covers Tier 2 end to end.
8
+
9
+ ## What Tier 2 is
10
+
11
+ Deep validation against an **existing build's output** (DL#211). It reads
12
+ `build/v<version>/backend/route-manifest.json` plus the per-instance `*.cache.json` slow-render results. It does
13
+ **not** run any render, so it's cheap — but it **requires a prior `jay-stack build`**. If no build is found, Tier 2
14
+ reports a single actionable error (run `jay-stack build` first, or drop `--tier-2`).
15
+
16
+ Run it **pre-deploy / in CI**, not in the hot authoring loop.
17
+
18
+ ```bash
19
+ jay-stack validate --tier-2 # validate against the highest build/v* output
20
+ jay-stack validate --t2 # short alias
21
+ jay-stack validate --tier-2 --build-dir <dir> # point at a specific build backend dir or build root
22
+ ```
23
+
24
+ Tier 2 findings print under their own `📦 Tier 2 — build-output validation (--tier-2)` section, separate from the
25
+ Tier 1 `📦 jay-stack (core)` section.
26
+
27
+ ## The three checks
28
+
29
+ All run against the concrete URLs the build actually produced (routes × instances).
30
+
31
+ 1. **Deferred dynamic-slug links** (ERROR) — the template links Tier 1 could only defer (because they matched a
32
+ dynamic pattern like `/design-log/[...slug]`) are resolved against the real URL set. A link to a dynamic route
33
+ with a slug the build never generated (e.g. `/design-log/wix/index`) is caught here.
34
+
35
+ 2. **Broken links in rendered content** (WARNING) — `<a href>` found _inside_ the slow-rendered ViewState
36
+ (markdown bodies, descriptions) that resolve to a URL the build does not produce. Relative hrefs are resolved
37
+ against the instance's own URL. These are **warnings**, not errors, because content often doubles as repo/GitHub
38
+ docs where repo-relative links like `../pkg/foo.ts` are legitimately correct.
39
+
40
+ 3. **Per-instance meta/SEO** (mostly WARNING) — resolves each route's `headMeta` template against each instance's
41
+ slow ViewState and validates the concrete `<title>` / `<meta name="description">`:
42
+ - Empty title/description, a title > 60 chars, or a description > 160 chars → **warning**.
43
+ - A leftover unresolved `{binding}` (a field never produced at any phase) → **error**.
44
+ - Note (DL#189): a field bound to the fast/interactive phase is legitimately empty at slow render, so
45
+ **emptiness is only ever a warning, never an error**.
46
+
47
+ If the build is older than your current source files, a **staleness warning** is emitted ("validated against a
48
+ build from &lt;timestamp&gt;; source has changed since") suggesting a rebuild.
49
+
50
+ ## Suppressing Tier 2 findings
51
+
52
+ Per-page, via the same `<script type="application/jay-validations">` block used for Tier 1, under the `jay-stack:`
53
+ key:
54
+
55
+ - `allow-broken-links: true` — suppresses link findings (both template and rendered-content links) for that page.
56
+ - `allow-meta-issues: true` — suppresses meta/SEO findings for that page.
57
+
58
+ ```html
59
+ <script type="application/jay-validations">
60
+ jay-stack:
61
+ allow-broken-links: true
62
+ allow-meta-issues: true
63
+ </script>
64
+ ```
@@ -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