Skip to main content
A wildcard route lets one page answer many URLs. You put {placeholders} in the page’s slug, and every URL that fits the pattern renders that page with the URL’s segments handed to your template as route_params. One “guided course” page can then serve /app/guided-courses/foundations/module-1, /app/guided-courses/advanced/lesson-5, and every other course and module.
Prefer a wildcard route over a query string. For product, course, article and category detail pages, /shop/product/blue-widget beats /shop/product?id=42 on every axis that matters — see Use paths, not ?id= below.

How a URL is matched

When a request arrives, the runtime resolves it in three steps and stops at the first hit:
  1. Exact slug. A page whose slug is literally the requested path.
  2. Wildcard patterns. Pages whose slug contains {…}, tried longest URL prefix first.
  3. Folder index. /<path> and /<path>/ fall back to a page saved as <path>/index. This is a last resort, so ecom/{slug} still wins over ecom/index for /ecom/cart.
A pattern matches only when both of these hold:
  • Its static prefix — everything before the first { — is exactly the URL prefix being tested.
  • Its number of placeholders equals the number of segments left over.
A page whose slug has no braces is never wildcard-matched. A plain page at app will not catch /app/anything; only app/{slug} will.

Setting up a wildcard route

1. Create the page

Give the page a slug that contains the placeholders, e.g.
  • Fixed part: app/guided-courses
  • Dynamic parts: {course} and {module}

2. What matches

The bare prefix /app/guided-courses does not match this page — two placeholders require exactly two trailing segments. If you want a landing page at the prefix, create a separate page with the slug app/guided-courses or app/guided-courses/index.

Rules and limits

Placeholders must all be trailing. The static prefix stops at the first {, so any literal segment written after a placeholder is ignored when matching: To keep a literal segment in the middle, put it in the fixed part instead: blog/tech/posts/{id}. A slug cannot start with a placeholder. Matching always tests at least one literal leading segment, so a slug like {hl}/about never matches any URL. Start the pattern with a real segment (docs/{topic}). Sibling patterns are disambiguated by prefix. blog/{slug} and blog/category/{slug} can coexist: /blog/hello picks the first, /blog/category/news picks the second, because each pattern’s static prefix must equal the URL prefix exactly. Offline and deleted pages are skipped, and at most 50 candidate patterns are considered per prefix. Wildcard pages are never listed in sitemap.xml or llms.txt — a {param} slug stands for many URLs, not one. Link to concrete URLs from a listing page you do publish.

Reading the segments

Two values are available once a wildcard route matches:
  • route_params — an object keyed by your placeholder names, e.g. { course: 'foundations', module: 'module-1' }.
  • route_slug — the whole remainder as one string, e.g. foundations/module-1.
Both are null when the page was reached by an exact slug, the domain index, or the folder-index fallback.
The keys of route_params are always your placeholder names. There is no segment_0 / segment_1 — name the placeholder whatever you want to read ({slug}route_params.slug).

In a template

In a backend script

route_params arrives on the request object:

Guarding

On a non-wildcard URL route_params is null, so guard before reading it:

Use paths, not ?id=

For any page that renders one of many records — a product, a course, a blog post, a category — put the identifier in the path via a wildcard route rather than in a query string. Query strings still make sense for things that modify a view rather than identify it: ?page=2, ?sort=price, ?hl=en, and UTM tags.

Product detail pages

Build the page once with a placeholder:
  • Page slug: shop/product/{code}
  • Template:
getProduct() accepts a product id or code, so the segment can be either. Link to it from your catalog with the productUrl filter, which emits /<base>/<slug>-<id>:
That produces /shop/product/blue-widget-42, so pair it with the slug shop/product/{handle} and split the trailing id in a backend script:
See Products and shipping for the full product API.

Course detail pages

  • Page slug: app/guided-course/{course}
  • Template:
getCourseBySlug() also falls back to the URL on its own when you call it with no argument: it reads ?slug=, then route_params.course, then route_slug. Passing the value explicitly is clearer, and required when your placeholder is named anything other than course.

Examples

One placeholder — record by slug

  • Page slug: app/guided-course/{slug}
  • URLs: /app/guided-course/foundations, /app/guided-course/advanced
  • In template: route_params.slug (or route_slug, same value here)

Two placeholders — course + module

  • Page slug: app/guided-courses/{course}/{module}
  • URLs: /app/guided-courses/foundations/module-1
  • In template: route_params.course, route_params.module

Three placeholders

  • Page slug: learn/{topic}/{level}/{lesson}
  • URL: /learn/javascript/beginner/01-variables
  • In template: route_params.topic, route_params.level, route_params.lesson

Summary

Use {name} in the slug for each dynamic part, then read route_params.name (or route_slug) in the template or request.route_params in a backend script.