File-Based Routing
Grove uses file-based routing for web pages. Files inside
apps/<name>/map directly to URL paths -- no router configuration required. Directory names become URL segments, and special filenames likepage.htmlandlayout.htmlcontrol what gets rendered and how it wraps.
Prerequisites: Concepts Overview, What is Grove?. What you'll learn: How directory structure maps to URLs, how layouts nest, and how dynamic segments work.
App Directory Structure
Each app lives under apps/ with its own directory tree:
apps/
main/
layout.html # root layout for the app
index.html # serves /
public/ # static assets
products/
layout.html # nested layout for /products/*
[id]/
page.html # serves /products/:id
page.jq # data loader for /products/:id
api/
todo/
route.grove # API route at /api/todo
Page Files
Two filenames are treated as page entry points:
| Filename | Behavior |
|---|---|
index.html | Serves the directory path (e.g., about/index.html serves /about) |
page.html | Also serves the directory path (e.g., products/[id]/page.html serves /products/:id) |
Any other .html filename becomes its own URL segment. For example, about.html in the root directory serves /about.
URL Computation Rules
The path from the app root to the file determines the URL:
| File path | URL |
|---|---|
index.html | / |
about.html | /about |
products/index.html | /products |
products/page.html | /products |
products/[id]/page.html | /products/:id |
docs/[...slug]/page.html | /docs/*slug |
Layout Files
A file named layout.html wraps all pages in the same directory and its subdirectories. Layouts receive rendered child content through the .children variable:
<html>
<head><title>{{ .page.title }}</title></head>
<body>{{ .children | trusted }}</body>
</html>
The | trusted filter is required when injecting rendered HTML to prevent double-escaping.
Nested Layouts
Layouts compose from the root inward. If both layout.html and products/layout.html exist, a request to /products/:id renders:
products/[id]/page.html-- the page contentproducts/layout.html-- wraps the pagelayout.html-- wraps everything
<!-- products/layout.html -->
<div class="products-layout">{{ .children | trusted }}</div>
The root layout receives the result of the inner layout as its .children.
Dynamic Segments
A directory name wrapped in brackets captures that portion of the URL as a parameter:
| Directory name | URL segment | Access in template |
|---|---|---|
[id] | :id | {{ .params.id }} |
[slug] | :slug | {{ .params.slug }} |
---
title: Product Detail
---
<h1>{{ .page.title }}</h1>
<p>Product ID: {{ .params.id }}</p>
Catch-All Segments
A directory named [...slug] matches any number of remaining URL segments. The captured value is the full remaining path.
| File path | Matches |
|---|---|
docs/[...slug]/page.html | /docs/a, /docs/a/b, /docs/a/b/c |
YAML Frontmatter
Pages support YAML frontmatter delimited by ---. The title field is available as .page.title in templates:
---
title: Home
---
<h1>{{ .page.title }}</h1>
See Also
- Templates & JQT -- Template syntax and data context
- Data Loaders -- Providing data to pages with
page.jq - Static Assets -- Serving CSS, JS, and images
- Subdomain Model -- How multi-app projects route requests
- Routes -- API routes using
route.grove