Routing
@Page
Creating a page is easy! It's just a normal @Composable
method. To upgrade your composable to a page, all you need to do is:
- Define your composable in a file somewhere under the
pages
package in yourjsMain
source directory. - Annotate it with
@Page
Just from that, Kobweb will create a site entry for you automatically.
For example, if I create the following file:
this will create a page that I can then visit by going to mysite.com/admin/settings
.
The last part of a URL, here settings
, is called a slug.
By default, the slug comes from the file name, which is converted into kebab-case. For example, the file name AboutUs.kt
would transform into about-us
. However, this can be overridden on a case-by-case basis to whatever you want (more on that shortly).
In other words, the name of your method is not used when generating the URL. You can call it whatever you want, but as a convention, we suggest using a name that matches the name of the file, appended by the Page
suffix.
The file name Index.kt
is special. If a page is defined inside such a file, it will be treated as the default page under that URL. For example, a page defined in .../pages/admin/Index.kt
will be visited if the user visits mysite.com/admin/
.
Route Override
If you ever need to change the route generated for a page, you can set the Page
annotation's routeOverride
field:
The above would create a page you could visit by going to mysite.com/admin/config
.
routeOverride
can additionally contain slashes, and if the value begins and/or ends with a slash, that has a special meaning.
- Begins with a slash - represent the whole route from the root
- Ends with a slash - a slug will still be generated from the filename and appended to the route override.
And if you set the override to "index", that behaves the same as setting the file to Index.kt
as described above.
Some examples can clarify these rules (and how they behave when combined). Assuming we're defining a page for our site example.com
within the file a/b/c/Slug.kt
:
Annotation | Resulting URL |
---|---|
@Page |
example.com/a/b/c/slug |
@Page("other") |
example.com/a/b/c/other |
@Page("index") |
example.com/a/b/c/ |
@Page("d/e/f/") |
example.com/a/b/c/d/e/f/slug |
@Page("d/e/f/other") |
example.com/a/b/c/d/e/f/other |
@Page("d/e/f/index") |
example.com/a/b/c/d/e/f/ |
@Page("/d/e/f/") |
example.com/d/e/f/slug |
@Page("/d/e/f/other") |
example.com/d/e/f/other |
@Page("/d/e/f/index") |
example.com/d/e/f/ |
@Page("/") |
example.com/slug |
@Page("/other") |
example.com/other |
@Page("/index") |
example.com/ |
Despite the flexibility allowed here, you should not be using this feature frequently, if at all. A Kobweb project benefits from the fact that a user can easily associate a URL on your site with a file in your codebase, but this feature allows you to break those assumptions. It is mainly provided to enable dynamic routing ( Dynamic routes) or providing a valid URL name that uses characters which aren't allowed in Kotlin filenames.
Package
While the slug is derived from the filename, earlier parts of the route, called URL route segments, are derived from the file's package.
A package will be converted into a route segment by removing any leading or trailing underscores (as these are often used to work around limitations into what values and keywords are allowed in a package name, e.g. site.pages.blog._2022
and site.events.fun_
) and converting camelCase packages into hyphenated words (so site.pages.team.ourValues
generates the route /team/our-values/
).
@PackageMapping
If you'd like to override the route segment generated for a package, you can use the PackageMapping
annotation.
For example, let's say your team prefers not to use camelCase packages for aesthetic reasons. Or perhaps you intentionally want to add a leading underscore into your site's route segment for some emphasis (since earlier we mentioned that leading underscores get removed automatically), such as in the route /team/_internal/contact-numbers
. You can use package mappings for this.
You apply the package mapping annotation to the current file. Using it looks like this:
With the above package mapping in place, a file that lives at site/pages/team/values/Mission.kt
will be visitable at /team/our-values/mission
.
Page context
Every page method provides access to its PageContext
via the rememberPageContext()
method.
Critically, a page's context provides it access to a router, allowing you to navigate to other pages.
It also provides dynamic information about the current page's URL (discussed in the next section).
Query parameters
You can use the page context to check the values of any query parameters passed into the current page's URL.
So if you visit site.com/posts?id=12345&mode=edit
, you can query those values like so:
Dynamic routes
In addition to query parameters, Kobweb supports embedding arguments directly in the URL itself. For example, you might want to register the path users/{user}/posts/{post}
which would be visited if the site visitor typed in a URL like users/bitspittle/posts/20211231103156
.
How do we set it up? Thankfully, it's fairly easy.
But first, notice that in the example dynamic route users/{user}/posts/{post}
there are actually two different dynamic segments, one in the middle and one at the tail end. These can be handled by the PackageMapping
and Page
annotations, respectively.
@PackageMapping
Pay attention to the use of the curly braces in the mapping name! That lets Kobweb know that this is a dynamic package.
If you pass an empty "{}"
into the PackageMapping
annotation, it directs Kobweb to use the route segment name normally generated from the package (i.e. user
in this specific case).
@Page
Like PackageMapping
, the Page
annotation can also take curly braces to indicate a dynamic value.
An empty "{}"
tells Kobweb to use the name of the slug normally generated from the file name (i.e. post
in this specific case).
Remember that the Page
annotation allows you to rewrite the entire route. That value also accepts dynamic segments, so you could even do something like:
but with great power comes great responsibility. Tricks like this may be hard to find and/or update later, especially as your project gets larger. While it works, you should only use this format in cases where you absolutely need to (perhaps after a code refactor where you have to support legacy URL paths).
Querying dynamic route values
You query dynamic route values exactly the same as if you were requesting query parameters. That is, use ctx.params
:
You should avoid creating URL paths where the dynamic path and the query parameters have the same name, as in mysite.com/posts/{post}?post=...
, as this could be really tricky to debug in a complex project. If there is a conflict, then the dynamic route parameters will take precedence. (You can still access the query parameter value via ctx.route.queryParams
in this case if necessary.)
Catch-all dynamic routes
As seen above, dynamic routes so far capture a single part of the entire route, e.g. "/users/{user}/profile"
capturing "bitspittle"
in the URL "/users/bitspittle/profile"
.
Kobweb also supports catch-all dynamic routes, which capture the remainder of the URL, at which point the page can parse it and handle it as it sees fit.
To create a catch-all route, prepend your dynamic route name with an ellipsis.
For example, the catch-all route "/a/b/c/{...rest}"
would capture "x/y/z"
in the URL "/a/b/c/x/y/z"
.
In practice, using it looks like this:
It's not expected that many sites will ever use a catch-all route, but in the above case, you could use the captured value as a way to encode fluid details of a product, perhaps with sub-routes contextually depending on the product type.
For example, the above page could handle /store/products/home-and-garden/hoses/19528
, /store/products/electronics/phones/google/pixel/4a
, and whatever other scheme each department demands.
Of course, it is better to provide a more structured solution if you can (e.g. declaring a page route like /store/products/{category}/{subcategory}/{product}
), but reality can be messy sometimes.
Catch-all route segments MUST terminate the route. The following is not valid and will result in an exception being thrown: "/a/b/c/{...middle}/x/y/z"
.
Optional catch-all routes
While "a/b/c/{...rest}"
will match "/a/b/c/d"
, it will NOT match "/a/b/c/"
. If you want to additionally support the empty case, you can add a question mark to the end of the name, e.g. "/a/b/c/{...rest?}"
.
Using this feature, you could even discard Kobweb's routing logic entirely and handle everything yourself:
Static and dynamic siblings
If you have a site that registers both static and dynamic segments at the same level, then a static match will always take precedence.
For example, if you register routes /a
and /{else}
, then visiting /a
will trigger the /a
handler, while visiting anything else, such as /b
, will trigger the /{else}
handler.