Directory-Based Routing
Directory-Based Routing
Designer Agent Kit — documentation written for AI agents, readable by humans.
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)
└── 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 (most specific first):
- Static segments (exact match) — highest priority
[param]— required dynamic param[[param]]— optional param[...param]— catch-all — lowest priority
Static Route Overrides
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:
src/pages/products/
├── [slug]/page.jay-html # dynamic: /products/:slug
└── ceramic-flower-vase/page.jay-html # static override for this specific product
The static ceramic-flower-vase/ route takes priority over [slug]/ for that URL, but all other product URLs still use the dynamic route.
Static Override Params and Headless Component Props
Static override routes use the same headless component as the dynamic route they override. Since the static route has no dynamic directory segment, the params must be declared in the headless component's YAML body:
<!-- src/pages/products/ceramic-flower-vase/page.jay-html -->
<html>
<head>
<script
type="application/jay-headless"
plugin="wix-stores"
contract="product-page"
key="product"
>
slug: ceramic-flower-vase
</script>
</head>
<body>
<h1>{product.productName}</h1>
</body>
</html>
The script body is YAML. Values are passed to the component as props alongside route params. This same mechanism is used for any per-component configuration:
<script
type="application/jay-headless"
plugin="@jay-framework/markdown"
contract="markdown-pages"
key="post"
>
contentDir: ./content
</script>
Note:
<script type="application/jay-params">is deprecated. Move param values into the headless component's script tag body.
Page Files
Each page directory can contain:
| File | Purpose |
|---|---|
page.jay-html |
Template (required for rendering) |
page.jay-contract |
Page-level data contract (optional) |
page.jay-contract
Defines the page's own ViewState — data that the page's server-side code provides:
name: Page
tags:
- tag: title
type: data
dataType: string
phase: slow
- tag: items
type: sub-contract
repeated: true
trackBy: id
tags:
- tag: id
type: data
dataType: string
- tag: name
type: data
dataType: string
Dynamic Routes and Contract Params
When a component on the page — whether the page contract, a headless component, or a headfull full-stack component — declares params, the page should be placed in a dynamic route directory that provides those params.
For example, if a headless component's contract declares:
name: product-page
params:
slug: string
tags:
- ...
Then the page using this component should live at a route that provides a slug param:
src/pages/products/[slug]/page.jay-html
Multiple components on the same page can each declare params. The route directory must provide all required params across all components. For example, if the page contract requires lang and a headless component requires slug, the page should live at src/pages/[lang]/products/[slug]/page.jay-html.
Passing Route Params to Nested Components
Route params flow automatically to keyed headless components that declare them as params in their contract. Instance-based headless components and headfull components do not receive route params directly — they receive props from the template.
Direct binding with jay.params (no page.ts needed)
Use jay.params.X to bind route params directly to nested component props:
<!-- src/pages/docs/[role]/[slug]/page.jay-html -->
<jay:DocsSidebar activeRole="{jay.params.role}" activePage="{jay.params.slug}" />
No page.ts, no page contract needed for param passing. jay.params is available at all render phases. Use jay.url.path for the full URL pathname:
<jay:Sidebar currentPath="{jay.url.path}" />
See jay-html-template-syntax.md for the full list of jay. bindings and navigation-patterns.md for active menu patterns.
Passing via page.ts (when you need data transformation)
When route params need processing before reaching the component (e.g., fetching data, computing derived values), use the page.ts passthrough pattern:
1. Page contract exposes the param as ViewState:
# page.jay-contract
name: Page
params:
slug: string
tags:
- tag: activePage
type: data
dataType: string
phase: slow
2. page.ts passes the param into ViewState:
.withSlowlyRender(async (props) =>
phaseOutput({ activePage: props.slug }, {})
)
3. Template binds ViewState to the nested component prop:
<jay:SideNav activePage="{activePage}" />
The same pattern works with keyed headless data — if a keyed component already provides the value, bind directly: <jay:SideNav activePage="{product.slug}" />.
Discovering Param Values
For SSG with dynamic routes, the plugin component provides a loadParams generator that yields all valid param combinations. Use it to discover what routes will be generated:
jay-stack params wix-stores/product-page
# Output: [{"slug": "blue-shirt"}, {"slug": "red-hat"}, ...]
Params are always strings (URL params). Routes are case-sensitive — a slug of My-Page produces the URL /my-page only if the param value is exactly my-page. Use lowercase for all param values and filenames that become URL segments.
Query Parameters
URL query parameters (?page=2&sort=price) are available in the fast render phase only via props.query:
.withFastRender(async (props, carryForward, dbService) => {
const page = parseInt(props.query.page || '1');
const sort = props.query.sort || 'name';
const products = await dbService.getProducts({ page, sort });
return phaseOutput({ products, currentPage: page }, {});
})
props.queryisRecord<string, string>— empty{}when no query string- Not available in the slow phase (compile error) — slow results are cached by path params only
- In the interactive phase, use
new URLSearchParams(window.location.search)directly
About this document
This page is part of the Jay Stack Agent Kit — documentation generated from the framework source and written primarily for AI agents. The language and structure are optimized for machine consumption — expect precise, specification-style prose rather than narrative documentation. Learn more about the Agent Kit →