Advanced Routing

404 Page Handling

It's possible for any user to visit a URL that doesn't exist on your site. For example, if a user visits https://example.com/does-not-exist, then the server should respond with a 404 HTTP status code, and the page should have at least some sort of explanation, rather than just a blank page.

For any given route, Qwik City can choose how to best handle the 404 response for the user, whether it's with the default 404 page, a custom 404 page, or a dynamically generated 404 page.

Default 404 Page

Rather than showing a blank page, by default Qwik City provides a generic 404 page for any route that isn't handled. The default 404 page is rendered as a fallback when a custom 404 page was not found. Qwik City's default page is alright and all, but we recommend providing a custom 404 so the user can be given a better experience, such as providing the common header and navigation so the user can find the page they're looking for.

Custom 404 Page

Instead of showing the generic (boring) 404 response, it's possible to provide a custom 404 page using the same familar layouts as the rest of your site.

To create a custom 404 page, add a 404.tsx file to the root src/routes directory.

src/
โ””โ”€โ”€ routes/
    โ”œโ”€โ”€ 404.tsx            # Custom 404
    โ”œโ”€โ”€ layout.tsx         # Default layout
    โ””โ”€โ”€ index.tsx          # https://example.com/

In the example above, the 404.tsx page will also use the layout.tsx layout, since it is a sibling of the layout in the same directory.

Additionally, using Qwik City's directory-based routing allows for custom 404 pages to be created at different paths. For example, if src/routes/account/404.tsx was also added to the structure, then the custom account 404 page would only be applied to the /account/* routes, while all other 404s would use the root 404.tsx page.

Note: During development and in preview mode, the custom 404 pages will be not render, but instead the default Qwik City 404 page will show. However, when building the app for production, the custom 404 page will be statically generated as a static 404.html file.

src/
โ””โ”€โ”€ routes/
    โ”œโ”€โ”€ account/
    โ”‚   โ””โ”€โ”€ 404.tsx        # Custom Account 404
    โ”‚   โ””โ”€โ”€ index.tsx      # https://example.com/account/
    โ”œโ”€โ”€ 404.tsx            # Custom 404
    โ”œโ”€โ”€ layout.tsx         # Default layout
    โ””โ”€โ”€ index.tsx          # https://example.com/

It's worth noting that custom 404 pages are statically generated at build time, resulting in a static 404.html file, rather than individually server-side rendered pages. This strategy reduces the load on your HTTP server, avoiding server-side rendering of 404 pages, and thus preserving resources.

Dynamic 404 Page

When rendering a page, the default is to always respond with a 200 HTTP status code, which tells the browser all is good and the route exists. However, it's also possible to handle rendering the page, but manually setting the response status code to something other than 200, such as 404.

For example, let's say we have a product page with a URL such as https://example.com/product/abc. The product page would be handled using src/routes/product/[id]/index.tsx directory-based route, and [id] is a dynamic param in the URL.

In this example, id is used as a key to load the product data from the database. When the product data is found, great, we'll correctly render data. However, if the product data is not found in the database, we can still handle rendering this page, but instead responding with a 404 HTTP status code.

import { component$ } from '@builder.io/qwik';
import { routeLoader$ } from '@builder.io/qwik-city';
 
export const useProductLoader = routeLoader$(async ({ params, status }) => {
  // Example database call using the id param
  // The database could return null if the product is not found
  const data = await productDatabase.get(params.id);
 
  if (!data) {
    // Product data was not found
    // Set the status code to 404
    status(404);
  }
 
  // return the data (which may be null)
  return data;
});
 
export default component$(() => {
  // get the product data from the loader
  const product = useProductLoader();
 
  if (!product.value) {
    // no product data found
    // so render our own custom product 404
    return <p>Sorry, looks like we don't have this product.</p>;
  }
 
  // product data was found, so let's render it
  return (
    <div>
      <h1>{product.value.name}</h1>
      <p>{product.value.price}</p>
      <p>{product.value.description}</p>
    </div>
  );
});

Grouped Layouts

Common purpose routes are often placed into directories so they can share layouts, and so related source files are logically grouped next to each other. However, it may be desirable that the directory, which was used to group similar files and share layouts, is excluded from the public-facing URL. This is where "grouped" layouts come in (also referred to as a "pathless" layout route).

By surrounding any directory name with parentheses, such as (name), then the directory name itself will not be included in the URL pathname.

For example, let's say an app placed all account routes together in a directory, however, /account/ could be dropped from the URL for cleaner, shorter URLs. In the example below, notice that the paths are within the src/routes/(account)/ directory. However, the URL paths exclude (account)/.

src/
โ””โ”€โ”€ routes/
    โ””โ”€โ”€ (account)/             # Notice the parentheses
        โ”œโ”€โ”€ layout.tsx         # Shared account layout
        โ””โ”€โ”€ profile/
            โ””โ”€โ”€ index.tsx      # https://example.com/profile
        โ””โ”€โ”€ settings/
            โ””โ”€โ”€ index.tsx      # https://example.com/settings

Named Layout

At times related routes need to have drastically different layouts from their siblings. It is possible to define multiple layouts for different sibling routes. A single default layout and any number of named layouts. The child route can then request a specific named-layout.

Qwik City defines the convention that layouts are within src/routes and the filename starts with layout. That's why the default layout is named layout.tsx. A named layout also starts with layout followed by a dash - and a unique name, such as layout-narrow.tsx.

src/
โ””โ”€โ”€ routes/
    โ”œโ”€โ”€ contact/
    โ”‚   โ””โ”€โ”€ index@narrow.tsx      # https://example.com/contact (Layout: layout-narrow.tsx)
    โ”œโ”€โ”€ layout.tsx                # Default layout
    โ”œโ”€โ”€ layout-narrow.tsx         # Default named layout
    โ””โ”€โ”€ index.tsx                 # https://example.com/ (Layout: layout.tsx)
  • https://example.com/
    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
    โ”‚       src/routes/layout.tsx                      โ”‚
    โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”‚
    โ”‚  โ”‚    src/routes/index.tsx                    โ”‚  โ”‚
    โ”‚  โ”‚                                            โ”‚  โ”‚
    โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ”‚
    โ”‚                                                  โ”‚
    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
    
  • https://example.com/contact
    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
    โ”‚       src/routes/layout-narrow.tsx               โ”‚
    โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”‚
    โ”‚  โ”‚    src/routes/contact/index@narrow.tsx     โ”‚  โ”‚
    โ”‚  โ”‚                                            โ”‚  โ”‚
    โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ”‚
    โ”‚                                                  โ”‚
    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
    

Nested Layout

Most times it is desirable to nest layouts into each other. A page's content can be nested in numerous wrapping layouts, which is determined by the directory structure.

src/
โ””โ”€โ”€ routes/
    โ”œโ”€โ”€ layout.tsx           # Parent layout
    โ””โ”€โ”€ about/
        โ”œโ”€โ”€ layout.tsx       # Child layout
        โ””โ”€โ”€ index.tsx        # https://example.com/about

In the above example, there are two layouts that apply themselves around the /about page component.

  1. src/routes/layout.tsx
  2. src/routes/about/layout.tsx

In this case, the layouts will nest each other with the page enveloped within each of them.

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚       src/routes/layout.tsx                    โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”‚
โ”‚  โ”‚    src/routes/about/layout.tsx           โ”‚  โ”‚
โ”‚  โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”‚  โ”‚
โ”‚  โ”‚  โ”‚ src/routes/about/index.tsx         โ”‚  โ”‚  โ”‚
โ”‚  โ”‚  โ”‚                                    โ”‚  โ”‚  โ”‚
โ”‚  โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ”‚  โ”‚
โ”‚  โ”‚                                          โ”‚  โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ”‚
โ”‚                                                โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
src/routes/layout.tsx
import { component$, Slot } from '@builder.io/qwik';
 
export default component$(() => {
  return (
    <main>
      <Slot /> {/* <== Child layout/route inserted here */}
    </main>
  );
});
src/routes/about/layout.tsx
import { component$, Slot } from '@builder.io/qwik';
 
export default component$(() => {
  return (
    <section>
      <Slot /> {/* <== Child layout/route inserted here */}
    </section>
  );
});
src/routes/about/index.tsx
import { component$ } from '@builder.io/qwik';
 
export default component$(() => {
  return <h1>About</h1>;
});

The above example would render the html:

<main>
  <section>
    <h1>About</h1>
  </section>
</main>

plugin.ts

plugin.ts and plugin@<name>.ts files can be created in the root of the src/routes directory to handle any incoming request before even the root layout executes.

You can have multiple plugin.ts files, each with a different name. For example, plugin@auth.ts and plugin@security.ts. The @<name> is optional and and it's only used for developers to help identify the plugin.

Requests handlers like onRequest, onGet, onPost, etc. are called before server$ functions are executed.

Contributors

Thanks to all the contributors who have helped make this documentation better!

  • manucorporat
  • adamdbradley
  • cunzaizhuyi
  • the-r3aper7
  • mhevery