| name | jay-html-authoring |
| description | Create and edit jay-html template files with data binding, headless components, conditional rendering, loops, and styling. Also covers directory-based routing in jay-stack. Use when creating pages, editing templates, or setting up routes. |
Jay-HTML Authoring & Routing
Jay-HTML File Structure
A .jay-html file is standard HTML with jay-specific extensions for data binding, conditions, loops, and headless component integration.
<html>
<head>
<script type="application/jay-data" contract="./page.jay-contract"></script>
<script type="application/jay-headless" plugin="..." contract="..." key="..."></script>
<style>
</style>
<link rel="stylesheet" href="../../styles/theme.css" />
</head>
<body>
<h1>{title}</h1>
</body>
</html>
Data Binding
Use {expression} to bind contract data:
<h1>{productName}</h1>
<span>{product.price}</span>
<div style="color: {textColor}">{msg}</div>
<a href="/products/{slug}">{name}</a>
Conditional Rendering
Use the if attribute:
<span if="inStock">In Stock</span>
<span if="!inStock">Out of Stock</span>
<div if="type===physical">Ships to your door</div>
<div if="type===virtual">Instant download</div>
For variant tags (enum): if="tagName===value" (no quotes around value).
Negation: if="!tagName" for boolean, if="tagName!==value" for enum.
Loops (forEach / trackBy)
Iterate over repeated sub-contracts:
<li forEach="products" trackBy="id">
<a href="/products/{slug}">
<div>{name}</div>
<div>{price}</div>
</a>
</li>
forEach — the repeated tag name
trackBy — stable unique key for each item (must match contract's trackBy)
- Inside the loop, bindings resolve to the current item's tags
Nested loops:
<div forEach="options" trackBy="_id">
<h3>{name}</h3>
<div forEach="choices" trackBy="choiceId">
<button ref="choiceButton">{name}</button>
</div>
</div>
Refs (Interactions)
Map elements to contract interactive tags using ref:
<button ref="addToCart">Add to Cart</button> <input value="{quantity}" ref="quantityInput" />
Key-based headless refs — prefix with the key:
<button ref="rating.submitButton">Submit</button> <button ref="mt.happy">+1 Happy</button>
Refs inside forEach:
<div forEach="options" trackBy="_id">
<div forEach="choices" trackBy="choiceId">
<button ref="choiceButton">{name}</button>
</div>
</div>
Page-level refs (not from a headless contract) — declare interactive tags on page.jay-contract and bind in jay-html:
<button ref="saveDraft">Save</button> <textarea ref="notesInput" value="{notes}"></textarea>
Handle them in page.ts via refs.saveDraft.onclick(...) — never document.querySelector for elements the page template renders.
DOM access from TypeScript
Jay Stack owns the DOM. In page.ts and headfull components:
- UI structure belongs in jay-html (
ref, if, forEach, {bindings}).
- Imperative DOM (focus, scroll, measure) goes through refs:
refs.myRef.exec$((el) => …) inside event handlers only.
- Do not use
document.querySelector, document.body.appendChild, or build UI with document.createElement in loops.
- Drags — pointer capture on the starting ref element, not
document mousemove/mouseup listeners.
See skill jay-dom-refs and agent-kit/developer/component-refs.md.
Headless Components
Pattern 1: Key-Based Import
Data merged into parent ViewState under a key. Use when you have one instance of a component per page.
<head>
<script
type="application/jay-headless"
plugin="wix-stores"
contract="product-page"
key="productPage"
></script>
</head>
<body>
<h1>{productPage.productName}</h1>
<span>{productPage.price}</span>
<button ref="productPage.addToCartButton">Add to Cart</button>
<div forEach="productPage.options" trackBy="_id">
<h3>{name}</h3>
<div forEach="choices" trackBy="choiceId">
<button ref="choiceButton">{name}</button>
</div>
</div>
</body>
Pattern 2: Instance-Based (jay: prefix)
Each instance has its own props and inline template. Use when you have multiple instances or need to pass props.
First, declare the headless import without a key:
<head>
<script
type="application/jay-headless"
plugin="product-widget"
contract="product-widget"
></script>
</head>
Then use <jay:contract-name> tags with props:
<jay:product-widget productId="prod-1">
<h3>{name}</h3>
<div>${price}</div>
<button ref="addToCart">Add</button>
</jay:product-widget>
<jay:product-widget productId="prod-2">
<h3>{name}</h3>
<button ref="addToCart">Add</button>
</jay:product-widget>
With forEach (dynamic props from parent data):
<div forEach="featuredProducts" trackBy="_id">
<jay:product-widget productId="{_id}">
<h3>{name}</h3>
<div>${price}</div>
<button ref="addToCart">Add</button>
</jay:product-widget>
</div>
Inside <jay:...>, bindings resolve to that instance's contract tags (not the parent).
Headfull Components
Import a local component (with its own rendering logic):
<script type="application/jay-headfull" src="./todo" names="TodoComponent"></script>
<jay:TodoComponent props="{todoProps}"></jay:TodoComponent>
Headfull Full-Stack Components (Jay Stack)
Adding a contract attribute makes a headfull import full-stack (SSR + three-phase rendering):
<script
type="application/jay-headfull"
src="./header/header"
contract="./header/header.jay-contract"
names="header"
></script>
<jay:header logoUrl="/logo.png" />
The component has its own jay-html with <head> (data contract, styles) and <body> (template). At compile time, the body is injected into each <jay:Name> tag.
Nesting Components Inside Headfull FS
Headfull FS components can import other components in their own <head>:
Headfull inside headfull (e.g., page → layout → header):
<head>
<script
type="application/jay-headfull"
src="../header/header"
contract="../header/header.jay-contract"
names="header"
></script>
</head>
<body>
<div class="layout">
<jay:header logoUrl="/logo.png" />
<aside>{sidebarLabel}</aside>
</div>
</body>
Headless inside headfull (e.g., header using a plugin widget):
<head>
<script type="application/jay-headless" plugin="my-plugin" contract="cart-indicator"></script>
</head>
<body>
<header>
<img src="{logoUrl}" />
<jay:cart-indicator>
<span>{itemCount}</span>
</jay:cart-indicator>
</header>
</body>
All nested imports are hoisted to page level. Nesting depth is unlimited. Key-based headless imports (key="...") are not allowed inside headfull FS components — use instance-based imports instead.
Styling
Inline <style>:
<head>
<style>
.product-card {
border: 1px solid #ccc;
padding: 16px;
}
.price {
font-weight: bold;
color: #2d7d2d;
}
</style>
</head>
External stylesheets:
<link rel="stylesheet" href="../../styles/theme.css" />
Dynamic style bindings:
<div style="color: {textColor}; width: {width}px">styled</div>
Page-Level Contract
A page can define its own data contract in page.jay-contract:
<script type="application/jay-data" contract="./page.jay-contract"></script>
This declares the page's own ViewState. Tags are bound directly (no key prefix).
Directory-Based Routing
Route Structure
Pages live under src/pages/. Directory names become URL segments.
src/pages/
├── page.jay-html → /
├── about/
│ └── page.jay-html → /about
├── products/
│ ├── page.jay-html → /products
│ └── [slug]/
│ └── page.jay-html → /products/:slug
├── blog/
│ ├── page.jay-html → /blog
│ └── [[slug]]/
│ └── page.jay-html → /blog/:slug (optional param)
└── files/
└── [...path]/
└── page.jay-html → /files/* (catch-all)
Dynamic Routes
| Syntax | Meaning | Example |
|---|
[param] | Required parameter | [slug] → /products/:slug |
[[param]] | Optional parameter | [[slug]] → /blog or /blog/my-post |
[...param] | Catch-all | [...path] → matches any sub-path |
Route Priority
Static routes match before dynamic routes:
- Static segments (exact match) — highest priority
[param] — required dynamic param
[[param]] — optional param
[...param] — catch-all — lowest priority
You can create a static override alongside a dynamic route:
src/pages/products/
├── [slug]/page.jay-html # dynamic: /products/:slug
└── ceramic-flower-vase/page.jay-html # static override: /products/ceramic-flower-vase
Page Files
Each page directory can contain:
| File | Purpose |
|---|
page.jay-html | Template (required for rendering) |
page.jay-contract | Page-level data contract |
page.conf.yaml | Configuration — which headless components to use |
page.conf.yaml example:
used_components:
- plugin: '@jay-framework/wix-stores'
contract: product-page
key: productPage
Dynamic Route with Load Params
For SSG with dynamic routes, the plugin component provides a loadParams generator. Discover available params:
yarn jay-stack params wix-stores/product-page
The contract may declare params:
name: product-page
params:
slug: string
tags:
- tag: productName
type: data
dataType: string
Complete Example
A product page at src/pages/products/[slug]/page.jay-html:
<html>
<head>
<script type="application/jay-data" contract="./page.jay-contract"></script>
<script
type="application/jay-headless"
plugin="product-rating"
contract="product-rating"
key="rating"
></script>
<link rel="stylesheet" href="../../../styles/product.css" />
</head>
<body>
<h1>{name}</h1>
<h2>${price}</h2>
<p>{sku}</p>
<div if="type===physical">Shipping available</div>
<div if="type===virtual">Digital download</div>
<div if="!inStock">Out of stock</div>
<div if="inStock">
<button ref="add-to-cart">Add to Cart</button>
</div>
<div>
<span>★ {rating.rating} ({rating.totalReviews} reviews)</span>
<button ref="rating.star1">★</button>
<button ref="rating.star2">★</button>
<button ref="rating.star3">★</button>
<button ref="rating.star4">★</button>
<button ref="rating.star5">★</button>
<p>Your rating: {rating.userRating}</p>
<button ref="rating.submitButton">Submit</button>
</div>
</body>
</html>