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.tsx
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.tsx
โ/
pages/blog/index.tsx
โ/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.tsx
โ/blog/first-post
pages/dashboard/settings/username.tsx
โ/dashboard/settings/username
Pages with Dynamic Routes
Brisa supports pages with dynamic routes. For example, if you create a file called pages/posts/[id].tsx
, 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.
src/layout/index.tsx
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
src/layout/index.tsx
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:
src/layout/index.tsx
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:
middleware
response headerslayout
response headers (can crush the middleware response headers)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",
};
}
Head
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
.
src/layout/index.tsx
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>
);
}
src/components/some-component.tsx
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.