Routing

The file system is the router.

Every .alpine file under pages/ is a route. The path on disk becomes the URL — with index collapsing to the parent and [param] segments matching dynamically.

The mapping #

scanPages walks pages/**/*.alpine and turns each file into a route. A trailing index is dropped, and [name] becomes a :name parameter:

FileURLKind
pages/index.alpine/static
pages/about.alpine/aboutstatic
pages/blog/index.alpine/blogstatic
pages/blog/[slug].alpine/blog/:slugdynamic

Nesting works to any depth — folders become path segments. Generate a page with apex make page blog/hello.

Index routes #

index.alpine represents the folder it sits in. pages/index.alpine is the site root /; pages/blog/index.alpine is /blog.

Dynamic [param] segments #

Wrap a segment in square brackets to capture it. The matched value is passed to the page's loader() as params:

pages/blog/[slug].alpine

<script server lang="ts">
  export function loader({ params }: { params: Record<string, string> }) {
    return { slug: params.slug }
  }
</script>

<template x-data>
  <main>
    <h1 x-text="'Post: ' + slug"></h1>
    <a href="/">← Home</a>
  </main>
</template>

Requesting /blog/hello-world calls the loader with params.slug === "hello-world". Segment values are URL-decoded for you.

Dynamic routes need a server at build time

apex dev and apex start render dynamic routes per request. A plain apex build / apex build --islands prerenders only static routes and reports the dynamic ones it skipped — build with --server to serve them in production. See Build & deploy.

Precedence #

Routes are matched by segment count and then literal-vs-param. Static routes take precedence over dynamic ones, so pages/blog/new.alpine (/blog/new) wins over pages/blog/[slug].alpine for the exact path /blog/new.

404s #

A request that matches no route returns a 404. Keep URLs relative in your own links (Apex is happy under a sub-path), and let unmatched paths fall through to the framework's not-found response.