Directory-Based Routing

Directory-Based Routing

Directory-Based Routing

DeveloperDeveloper 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):

  1. Static segments (exact match) — highest priority
  2. [param] — required dynamic param
  3. [[param]] — optional param
  4. [...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.query is Record<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.cookies is Record<string, string> — empty {} when no cookies
  • Not available in the slow phase (compile error) — same as query params
  • In the interactive phase, use document.cookie directly
  • To set response headers (e.g. Cache-Control: no-store), use responseHeaders in phaseOutput() 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 →