Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/nuejs/nue/llms.txt

Use this file to discover all available pages before exploring further.

Nue’s routing model has one rule: your file system is your URL structure. A Markdown file at blog/hello-world.md becomes the page at /blog/hello-world. An index.md or index.html at any level becomes that directory’s root URL. There is no router configuration file to maintain and no framework-specific conventions to learn.

File-based routing

The getURL function in file.js implements the URL derivation rule directly:
// From file.js
export function getURL(file) {
  let { name, base, ext, dir } = file

  if (['.md', '.html'].includes(ext)) {
    if (name == 'index') name = ''
    ext = ''
  }

  if (ext == '.ts') ext = '.js'
  const els = dir.split(sep)
  els.push(name + ext)

  return `/${ els.join('/') }`.replace('//', '/')
}
The rules are:
  • .md and .html files drop their extension in the URL
  • Files named index produce the directory URL (e.g., blog/index.md/blog/)
  • TypeScript files (.ts) are served as .js
  • All other files keep their extension

Directory structure example

my-site/
├── index.md          → /
├── about.md          → /about
├── blog/
│   ├── index.md      → /blog/
│   ├── hello.md      → /blog/hello
│   └── launch.md     → /blog/launch
├── docs/
│   ├── index.md      → /docs/
│   ├── quickstart.md → /docs/quickstart
│   └── api/
│       └── index.md  → /docs/api/
└── styles/
    └── main.css      → /styles/main.css
The URL for each page is determined entirely by its position in the file tree. Renaming or moving a file changes its URL automatically.

How index files become page routes

An index.md or index.html at any directory level becomes the page for that directory path. When Nue serves a URL without a file extension (e.g., /blog/), it looks for index.html or <dirname>/index.html among the site assets:
// From cmd/serve.js
if (!ext) {
  const app = url.split('/')[1]
  const spa = assets.find(asset =>
    ['index.html', `${app}/index.html`].includes(asset.path)
  )
  if (spa) return (await spa.render()).html
}
This means each top-level directory can have its own index.html that acts as the application shell — important for multi-application sites where different sections may have different layouts or navigation.

Multi-page app organization

Nue supports organizing multiple applications within one site. Each top-level directory is treated as a separate application with its own configuration:
my-site/
├── site.yaml         ← global configuration
├── @shared/          ← shared assets (layouts, data, components)
│   ├── layout.html
│   └── data/
├── marketing/        ← marketing app
│   ├── index.html
│   └── app.yaml
├── blog/             ← blog app
│   ├── index.md
│   └── app.yaml
└── app/              ← SPA dashboard
    ├── index.html    ← SPA shell
    └── app.yaml
Each directory can have its own app.yaml file for per-application configuration (layouts, CSS, collections) that merges with the global site.yaml. The @shared/ directory holds assets available to all applications.

SPA routing with nuestate

Single-page application routing is handled by nuestate. Rather than a dedicated router package, nuestate treats the URL itself as the source of truth for application state. The autolink feature intercepts <a href> clicks and converts them to SPA navigation without a full page reload. Enable autolink in your state setup:
import { state } from 'state'

state.setup({
  route: '/app/:section/:id',
  query: ['search', 'filter', 'page'],
  autolink: true
})
With autolink: true, nuestate attaches a click listener to the document. When a user clicks an <a href> that matches the configured route pattern, it calls api.set() instead of following the link, updates the URL via history.pushState, and fires any registered state listeners:
// From state.js — autolink implementation
function autolink(root=document) {
  root.addEventListener('click', e => {
    const link = e.target.closest('a[href]')
    if (!link || e.defaultPrevented || e.metaKey || e.ctrlKey ||
        !getPathData(opts.route, link.pathname)) return
    api.set(getURLData(link))
    e.preventDefault()
  })
}
Links that don’t match the route pattern behave normally. Modifier keys (metaKey, ctrlKey) are respected so that “open in new tab” still works.

Dynamic route parameters

Route parameters are path segments prefixed with : in the route pattern. Nuestate parses them from the URL and makes them available as state properties:
state.setup({
  route: '/products/:category/:id',
  query: ['sort', 'filter']
})

// URL: /products/shoes/123?sort=price
// state.category → 'shoes'
// state.id       → '123'
// state.sort     → 'price'
The getPathData function matches a URL pathname against a route template and extracts the named parameters:
// From state.js
export function getPathData(route, pathname) {
  const tokens = route.split('/')
  const els = pathname.split('/')
  const data = {}

  for (let i = 1; i < tokens.length; i++) {
    const token = tokens[i]
    const el = els[i]
    if (token[0] == ':') { data[token.slice(1)] = el || null }
    else if (token != el) return   // static segment mismatch → no match
  }
  return data
}
If a static segment in the route doesn’t match the URL, the function returns undefined, signaling no match. This is what the autolink handler uses to decide whether to intercept a click.

Rendering path-based URLs

When state changes cause route parameters to update, nuestate re-renders the URL using renderPath:
// From state.js
export function renderPath(route, data={}) {
  const els = route.split('/').map(
    token => token[0] == ':' ? data[token.slice(1)] : token
  )
  const i = els.indexOf(undefined)
  return (i > 0 ? els.slice(0, i + 1) : els).join('/').replace('//', '/')
}
Undefined parameters truncate the path at that point, so navigating to a category without an ID produces /products/shoes rather than /products/shoes/undefined.

Server routes

Server-side route handlers live in the @shared/server/ directory by default (configurable via server.dir in site.yaml). The entry point is @shared/server/index.js, which defines routes using the nue-edgeserver API.
// @shared/server/index.js
import { get, post } from 'nue-edgeserver'

get('/api/posts', async (req, env) => {
  const posts = await env.posts.getAll()
  return Response.json(posts)
})

post('/api/contact', async (req, env) => {
  const data = await req.formData()
  // handle form submission
  return Response.json({ ok: true })
})
Data models live in @shared/server/data/ as JSON files. The createEnv function in model.js loads each JSON file as a typed model:
@shared/server/
├── index.js       ← route definitions
└── data/
    ├── posts.json ← becomes env.posts
    └── users.json ← becomes env.users (with login/logout)
A users.json file automatically gets a UserModel with login, logout, and authenticate methods. All other JSON files get a basic CRUD model with getAll, get, create, and update/remove via the returned item object.
Server routes are reloaded automatically during development when you edit files in @shared/server/. The createWorker function accepts a reload option that re-imports the route module on each request.

State storage contexts

Beyond the URL, nuestate supports multiple storage contexts for different kinds of state:

path_params

URL path segments like /app/:section. Changes trigger history.pushState with a full path update.

query

URL query parameters like ?search=shoes. Changes trigger history.replaceState on the search string.

session

sessionStorage — persists for the browser tab session. Cleared when the tab closes.

local

localStorage — persists across sessions. Use for user preferences like theme or language.
state.setup({
  route: '/app/:section',
  query: ['search', 'page'],
  session: ['user', 'token'],
  local: ['theme', 'language']
})
State reads aggregate all contexts in priority order: localStoragesessionStorage → URL search → URL path → in-memory. Writing to any property automatically routes to the correct storage.

Build docs developers (and LLMs) love