Pages and Layouts

The Pages Router has a file-system based router built on the concept of pages (like Next.js pages folder).

When a file is added to the pages directory, it's automatically available as a route.

In Brisa framework, a page is a Brisa Server Component exported from a .js, .jsx, .ts, or .tsx file in the pages directory. Each page is associated with a route based on its file name.

Example: If you create pages/about.js that exports a Brisa component like below, it will be accessible at /about.

export default function About() {
  return <div>About</div>;
}

See the difference between Brisa Components and React Components here.

Index routes

The router will automatically route files named index to the root of the directory.

  • pages/index.js โ†’ /
  • pages/blog/index.js โ†’ /blog

Nested routes

The router supports nested files. If you create a nested folder structure, files will automatically be routed in the same way still.

  • pages/blog/first-post.js โ†’ /blog/first-post
  • pages/dashboard/settings/username.js โ†’ /dashboard/settings/username

Pages with Dynamic Routes

Brisa supports pages with dynamic routes. For example, if you create a file called pages/posts/[id].js, then it will be accessible at posts/1, posts/2, etc.

To learn more about dynamic routing, check the Dynamic Routing documentation.

Layout

The global layout is defined inside /src/layout/index. By default Brisa supports a default layout, but you can modify it here.

import { RequestContext } from "brisa";

export default function Layout(
  { children }: { children: JSX.Element },
  { route }: RequestContext
) {
  return (
    <html>
      <head>
        <title>My layout</title>
        <link rel="icon" href="favicon.ico" />
      </head>
      <body>{children}</body>
    </html>
  );
}

It must have the same structure: html, head and body. If for example you forget to put the head, you may have issues and you will be alerted with an error during development.

All the components of Brisa (pages and layouts included), apart from the props, receive a second argument which is the context of the request, apart from having access to the request, you have access to a series of extra information such as the route of the page. In the layouts, having access to the page route is very useful to create different layouts.

Once you have defined the layout, you don't have to import it into your pages, it will automatically be applied to all pages. To apply different layouts to different pages, take a look at multi-layouts.

Example of multi-layouts

import { type RequestContext } from "brisa";
import UserLayout from './user-layout'
import GlobalLayout from './global-layout'

export default function Layout({ children }: { children: JSX.Element }, { route }: RequestContext) {
  // pathname: /en/user/aralroca/settings or /es/usuario/pepe
  if(route.name.startsWith('/user/[username]')) {
    return <UserLayout>{children}<UserLayout>
  }

  return <GlobalLayout>{children}</GlobalLayout>
}

Data Fetching

Inside your layout, you can fetch data directly with fetch, in the same way that you can do it in pages:

import { RequestContext } from "brisa";

export default async function Layout(
  { children }: { children: JSX.Element },
  { route }: RequestContext
) {
  const data = await fetch(/* data url */).then((r) => r.json());

  return (
    <html>
      <head>
        <title>My layout</title>
        <link rel="icon" href="favicon.ico" />
      </head>
      <body>{children}</body>
    </html>
  );
}

The fetch is directly native and has no wrapper to control the cache. We recommend that you do not do the same fetch in several places, but use the store to store the data and consume it from any component.

async generators are also supported if you want to stream every item in a list for example:

async function* List() {
  yield <li>{await foo()}</li>;
  yield <li>{await bar()}</li>;
  yield <li>{await baz()}</li>;
}

This can be used as a server component:

return <List />;

And the HTML is resolved via streaming.

Response headers in layouts and pages

The responseHeaders function can be exported inside the layout and inside any page. In the same way that is possible to export it also in the middleware.

All responseHeaders will be mixed in this order:

  1. middleware response headers
  2. layout response headers (can crush the middleware response headers)
  3. page response headers (both middleware and layout response headers can be mixed).
import { type RequestContext } from "brisa";

export function responseHeaders(
  request: RequestContext,
  responseStatus: number,
) {
  return {
    "Cache-Control": "public, max-age=3600",
    "Content-Security-Policy": "script-src 'self' 'unsafe-inline';",
    "X-Example": "This header is added from layout",
  };
}
export function responseHeaders(request, responseStatus) {
  return {
    "Cache-Control": "public, max-age=3600",
    "Content-Security-Policy": "script-src 'self' 'unsafe-inline';",
    "X-Example": "This header is added from layout",
  };
}

The Head is a method that you can export in the pages to overwrite any element of the <head> tag.

If for example you have the title defined in the layout but in the page /about-us you want to put a different title. You can use the same id to override the title of the layout:

export function Head({}, { route }: RequestContext) {
  return (
    <>
      <title id="title">About us</title>
      <link rel="canonical" href="https://my-web.com" />
    </>
  );
}

export default function AboutUsPage() {
  // ...
}

If you want to mash existing head fields (title, link, meta, etc) because you already have them defined in the layout, you must use the id attribute in both parts, and only this one will be rendered. On pages that do not overwrite it, the one in the layout will be rendered.

Share data between middleware โ†’ layout โ†’ page โ†’ component โ†’ responseHeaders โ†’ Head โ†’ web-components

You can share data between different parts of the application using the request context.

import { type RequestContext } from "brisa";

export default async function Layout({}, request: RequestContext) {
  const data = await getData(request);

  request.store.set("data", data);

  return (
    <html>
      <head>
        <title>My page</title>
        <link rel="icon" href="favicon.ico" />
      </head>
      <body>{children}</body>
    </html>
  );
}
import { type RequestContext } from "brisa";

type Props = {
  name: string;
};

export default function SomeComponent(props: Props, { store }: RequestContext) {
  const data = store.get("data");

  return <h1>Hello {data[props.name]}</h1>;
}

Transfer data to client (web-components):

This data is only available on the server. So you can store sensitive data without worrying. However, you can transfer certain data to the client side (web-components) using store.transferToClient method.

import { type RequestContext } from "brisa";

export default async function Layout({}, request: RequestContext) {
  const data = await getData(request);

  request.store.set("data", data);

  // Transfer "data" from store to client
  request.store.transferToClient(["data"]);

  return (
    <html>
      <head>
        <title>My page</title>
        <link rel="icon" href="favicon.ico" />
      </head>
      <body>{children}</body>
    </html>
  );
}

This allows access to these values from the web components store.

This setup also enables subsequent server actions to access the same store, as the communication flows through the client:

server render โ†’ client โ†’ server action โ†’ client

It is a way to modify in a reactive way from a server action the web components that consume this store.

You can encrypt store data if you want to transfer sensitive data to the server actions so that it cannot be accessed from the client.

Consume data on client (web-components):

In the web-components instead of the RequestContext, there is the WebContext, where you have a different store, but if you have transferred the data from the RequestContext store, you will be able to consume it from the WebContext store.

import { WebContext } from "brisa";

export default function WebComponent({}, { store }: WebContext) {
  return <button onClick={() => alert(store.get("example"))}>Click</button>;
}

If you want to know more about store check this out.