@jay-framework/jay-stack-cli 0.19.4 → 0.19.6

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.
@@ -21,6 +21,11 @@ A `.jay-html` file is standard HTML with jay-specific extensions.
21
21
  <!-- Headfull component imports -->
22
22
  <script type="application/jay-headfull" src="..." names="..." contract="..."></script>
23
23
 
24
+ <!-- SEO head tags (support {binding} syntax) -->
25
+ <title>{productPage.name} | My Store</title>
26
+ <meta name="description" content="{productPage.description}" />
27
+ <link rel="canonical" href="https://mystore.com/products/{productPage.slug}" />
28
+
24
29
  <!-- Styles -->
25
30
  <style>
26
31
  /* inline CSS */
@@ -34,6 +39,22 @@ A `.jay-html` file is standard HTML with jay-specific extensions.
34
39
  </html>
35
40
  ```
36
41
 
42
+ ## Head Tag Bindings
43
+
44
+ `<title>`, `<meta>`, and `<link>` in `<head>` support `{binding}` syntax — the same expressions used in `<body>`. Bindings resolve against the merged ViewState at SSR time.
45
+
46
+ ```html
47
+ <head>
48
+ <title>{productPage.name} | My Store</title>
49
+ <meta name="description" content="{productPage.description}" />
50
+ <link rel="canonical" href="https://mystore.com/products/{productPage.slug}" />
51
+ </head>
52
+ ```
53
+
54
+ If a headless component also provides head tags via `phaseOutput({ headTags })`, the **template wins** — template head tags override component-provided ones. This lets you customize the head while components provide defaults.
55
+
56
+ Canonical URLs must be absolute (`https://...`). The `{binding}` syntax can be used for the dynamic part (e.g., slug).
57
+
37
58
  ## Data Binding
38
59
 
39
60
  Use `{expression}` to bind contract data:
@@ -71,11 +71,51 @@ const headTags = seoData.tags.map((tag) => ({
71
71
  return phaseOutput(viewState, carryForward, { headTags });
72
72
  ```
73
73
 
74
+ ## Declaring Head Tags in plugin.yaml
75
+
76
+ If your component provides head tags dynamically via `phaseOutput`, declare them in `plugin.yaml` so the SEO validator knows not to warn about missing title/description on pages using your component:
77
+
78
+ ```yaml
79
+ name: my-plugin
80
+ contracts:
81
+ - name: product-page
82
+ contract: product-page.jay-contract
83
+ component: productPage
84
+ headTags:
85
+ - title
86
+ - meta:description
87
+ - link:canonical
88
+ ```
89
+
90
+ Values: `title`, `meta:<name>` (e.g., `meta:description`, `meta:og:title`), `link:<rel>` (e.g., `link:canonical`).
91
+
92
+ ## Priority: Template Wins
93
+
94
+ When both the jay-html template and a component provide the same head tag, the **template wins**:
95
+
96
+ 1. Component `phaseOutput({ headTags })` — defaults
97
+ 2. Jay-html `<head>` tags — **highest priority, overrides component**
98
+
99
+ This lets designers customize head content in the template while components provide sensible defaults.
100
+
101
+ The jay-html `<head>` supports `{binding}` syntax for dynamic values:
102
+
103
+ ```html
104
+ <head>
105
+ <title>{productPage.name} | My Store</title>
106
+ <meta name="description" content="{productPage.description}" />
107
+ <link rel="canonical" href="https://mystore.com/products/{productPage.slug}" />
108
+ </head>
109
+ ```
110
+
111
+ Bindings are resolved against the merged ViewState at SSR time.
112
+
74
113
  ## Phase Rules
75
114
 
76
115
  - Return headTags from **slow** phase for build-time SEO data (product name, description)
77
116
  - Return headTags from **fast** phase for per-request data (pricing, availability)
78
117
  - Fast phase headTags **replace** slow phase entirely (no merge)
118
+ - Template head tags override both (if present)
79
119
  - No interactive phase — head tags are SSR-only
80
120
 
81
121
  ## Collision Rules
@@ -91,3 +131,4 @@ return phaseOutput(viewState, carryForward, { headTags });
91
131
 
92
132
  - Head tags from components inside `forEach` are ignored
93
133
  - The framework handles HTML escaping automatically
134
+ - Canonical URLs must be absolute (`https://...`)
@@ -100,6 +100,25 @@ component: ./lib/components/product-page.ts
100
100
  component: productPage
101
101
  ```
102
102
 
103
+ ## Head Tags Declaration
104
+
105
+ If your component provides head tags dynamically (via `phaseOutput({ headTags })`), declare them in `plugin.yaml` so validators don't warn about missing tags on pages using your component:
106
+
107
+ ```yaml
108
+ contracts:
109
+ - name: product-page
110
+ contract: product-page.jay-contract
111
+ component: productPage
112
+ headTags:
113
+ - title
114
+ - meta:description
115
+ - link:canonical
116
+ ```
117
+
118
+ Values: `title`, `meta:<name>` (e.g., `meta:description`), `link:<rel>` (e.g., `link:canonical`).
119
+
120
+ Validators access this via `ctx.headlessImports[].providedHeadTags`.
121
+
103
122
  ## Plugin Validators
104
123
 
105
124
  Plugins can provide custom jay-html validation rules that run during `jay-stack validate`. Declare validators in `plugin.yaml`:
@@ -122,9 +141,11 @@ export const validate: JayHtmlValidatorFn = (ctx) => {
122
141
  const findings: JayHtmlValidationFinding[] = [];
123
142
 
124
143
  // ctx.body — parsed DOM tree (HTMLElement from node-html-parser)
144
+ // ctx.head — parsed <head> metadata (title, meta tags, link tags)
125
145
  // ctx.filePath — relative path to the jay-html file
126
146
  // ctx.contract — page contract (if any), with tags including meta
127
147
  // ctx.headlessImports — headless components used in this file
148
+ // .providedHeadTags — head tags the component declares in plugin.yaml
128
149
  // ctx.projectRoot — absolute project root path
129
150
 
130
151
  return findings;
package/dist/index.js CHANGED
@@ -9,7 +9,7 @@ import fs, { promises } from "fs";
9
9
  import YAML from "yaml";
10
10
  import { getLogger, setDevLogger, createDevLogger } from "@jay-framework/logger";
11
11
  import { parseJayFile, JAY_IMPORT_RESOLVER, generateElementDefinitionFile, ContractTagType, parseContract, generateElementFile, generateServerElementFile, htmlElementTagNameMap, loadLinkedContract, getLinkedContractDir } from "@jay-framework/compiler-jay-html";
12
- import { JAY_CONTRACT_EXTENSION, JAY_EXTENSION, resolvePluginManifest, LOCAL_PLUGIN_PATH, JayAtomicType, JayEnumType, loadPluginManifest, RuntimeMode, GenerateTarget } from "@jay-framework/compiler-shared";
12
+ import { JAY_CONTRACT_EXTENSION, JAY_EXTENSION, resolvePluginManifest, LOCAL_PLUGIN_PATH, JayAtomicType, JayEnumType, loadPluginManifest, RuntimeMode, GenerateTarget, findDynamicContract } from "@jay-framework/compiler-shared";
13
13
  import { scanPlugins as scanPlugins$1, listContracts, materializeContracts } from "@jay-framework/stack-server-runtime";
14
14
  import { listContracts as listContracts2, materializeContracts as materializeContracts2 } from "@jay-framework/stack-server-runtime";
15
15
  import { Command } from "commander";
@@ -4340,6 +4340,21 @@ async function runPluginValidators(projectRoot, parsedFiles, errors, warnings) {
4340
4340
  } : void 0,
4341
4341
  headlessImports: parsed.headlessImports.map((imp) => {
4342
4342
  const resolvedContract = imp.contract ? resolveContractLinks(imp.contract, imp.contractPath) : void 0;
4343
+ let providedHeadTags;
4344
+ for (const [, p] of scannedPlugins) {
4345
+ const entry = p.manifest.contracts?.find(
4346
+ (c) => c.name === imp.contractName
4347
+ );
4348
+ if (entry?.headTags) {
4349
+ providedHeadTags = entry.headTags;
4350
+ break;
4351
+ }
4352
+ const dynEntry = findDynamicContract(p.manifest, imp.contractName);
4353
+ if (dynEntry?.headTags) {
4354
+ providedHeadTags = dynEntry.headTags;
4355
+ break;
4356
+ }
4357
+ }
4343
4358
  return {
4344
4359
  key: imp.key,
4345
4360
  contractName: imp.contractName,
@@ -4348,7 +4363,8 @@ async function runPluginValidators(projectRoot, parsedFiles, errors, warnings) {
4348
4363
  tags: resolvedContract.tags,
4349
4364
  props: resolvedContract.props,
4350
4365
  params: resolvedContract.params
4351
- } : void 0
4366
+ } : void 0,
4367
+ providedHeadTags
4352
4368
  };
4353
4369
  }),
4354
4370
  projectRoot
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jay-framework/jay-stack-cli",
3
- "version": "0.19.4",
3
+ "version": "0.19.6",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -24,15 +24,15 @@
24
24
  "test:watch": "vitest"
25
25
  },
26
26
  "dependencies": {
27
- "@jay-framework/compiler-jay-html": "^0.19.4",
28
- "@jay-framework/compiler-shared": "^0.19.4",
29
- "@jay-framework/dev-server": "^0.19.4",
30
- "@jay-framework/editor-server": "^0.19.4",
31
- "@jay-framework/fullstack-component": "^0.19.4",
32
- "@jay-framework/logger": "^0.19.4",
33
- "@jay-framework/plugin-validator": "^0.19.4",
34
- "@jay-framework/production-server": "^0.19.4",
35
- "@jay-framework/stack-server-runtime": "^0.19.4",
27
+ "@jay-framework/compiler-jay-html": "^0.19.6",
28
+ "@jay-framework/compiler-shared": "^0.19.6",
29
+ "@jay-framework/dev-server": "^0.19.6",
30
+ "@jay-framework/editor-server": "^0.19.6",
31
+ "@jay-framework/fullstack-component": "^0.19.6",
32
+ "@jay-framework/logger": "^0.19.6",
33
+ "@jay-framework/plugin-validator": "^0.19.6",
34
+ "@jay-framework/production-server": "^0.19.6",
35
+ "@jay-framework/stack-server-runtime": "^0.19.6",
36
36
  "chalk": "^4.1.2",
37
37
  "commander": "^14.0.0",
38
38
  "express": "^5.0.1",
@@ -43,7 +43,7 @@
43
43
  "yaml": "^2.3.4"
44
44
  },
45
45
  "devDependencies": {
46
- "@jay-framework/dev-environment": "^0.19.4",
46
+ "@jay-framework/dev-environment": "^0.19.6",
47
47
  "@types/express": "^5.0.2",
48
48
  "@types/node": "^22.15.21",
49
49
  "nodemon": "^3.0.3",