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 like page.html and layout.html control 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:

FilenameBehavior
index.htmlServes the directory path (e.g., about/index.html serves /about)
page.htmlAlso 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 pathURL
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:

  1. products/[id]/page.html -- the page content
  2. products/layout.html -- wraps the page
  3. layout.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 nameURL segmentAccess 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 pathMatches
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