Directory-Based Routing
Directory-Based Routing
Developer 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.
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 — use lowercase for all param values and filenames that become URL segments.
When page.ts is not needed for params
The designer can bind route params directly to nested component props using jay.params in the template — no page.ts or page contract needed:
<jay:DocsSidebar activeRole="{jay.params.role}" activePage="{jay.params.slug}" />
<jay:Sidebar currentPath="{jay.url.path}" />
Only create a page.ts for params when you need to transform them (fetch data, compute derived values, combine with service calls). If the page just passes params through to components, jay.params is sufficient.
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
Cookies
HTTP cookies are available in the fast render phase only via props.cookies:
.withFastRender(async (props, carryForward, memberService) => {
const token = props.cookies['session-token'];
if (!token) return redirect3xx(302, '/login');
const member = await memberService.validate(token);
if (!member) return redirect3xx(302, '/login');
return phaseOutput(
{ memberName: member.name },
{},
{ responseHeaders: { 'Cache-Control': 'no-store' } },
);
})
props.cookiesisRecord<string, string>— empty{}when no cookies- Not available in the slow phase (compile error) — same as query params
- In the interactive phase, use
document.cookiedirectly - To set response headers (e.g.
Cache-Control: no-store), useresponseHeadersinphaseOutput()options
Plugin Routes
Plugins can provide their own pages via routes in plugin.yaml. These are backoffice tools, admin dashboards, or editors with boxed designs that don't need per-site customization.
Plugin routes appear alongside project routes. If your project defines the same route path in src/pages/, your page takes precedence — the plugin's page is skipped.
To override a plugin route, simply create a page at the same path:
# Plugin provides /admin/products
# To customize, create:
src/pages/admin/products/page.jay-html
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 →