Server-Side Routing in LWR on Node.js
To set server-side routes for your app with LWR, use the lwr.config.json project configuration file. Server-side routes can be specified in lwr.config.json by providing either:
- a static path for each page
- a JavaScript file that defines a hook to dynamically generate server-side routes for pages at server startup (this is an advanced option)
Both static and dynamic server-side routes can also have route handler functions, which let you customize the page response at runtime.
Read on for details. We start with a basic static routing example.
Static Server-Side Routing
Let’s add a page to our site and do some basic (and fast!) routing.
- Now that you’ve got your
StaticSiteproject, make a copy of theabout.mdfile. Rename the new fileexplore.mdand leave it in thecontentdirectory. - Edit the
explore.mdfile. Change the first line from# About LWRto# Explore LWR. Save the file. - Open the
main_layout.njkfile. On a new line, add<li><a href="/explore">Explore</a>under theaboutline. Save the file.
- Finally, open the
lwr.config.jsonfile. Copy the entire block of code forabout, including the curly brackets, and paste it below theaboutblock. Changeabouttoexplorethroughout, and don’t forget to add the comma after the closing bracket in theaboutblock. Save the file.
- If you’re still in the
StaticSitedirectory from when you first made your site, just typenpm run startin the terminal to see your changes. Your updatedStaticSiteproject runs at http://localhost:3000/, with a new Explore LWR page and a working Explore button.
If you get a “Port 3000 is already in use” error after running npm run start, close any terminal tabs that run your site preview. Then, open a new terminal tab and run npm run start.
Tip
What just happened is that you:
- created a Markdown page (
explore.md) and gave it new heading text (“Explore LWR”) - updated the site navigation in the Nunjucks layout (
main_layout.njk) file to include a new Explore button - updated the JSON routing configuration file (
lwr.config.json) to let LWR know how to route to the new page using a route- you gave an
idfor the page (Explore) - you provided the
pathfor the page (/explore) - you provided a
contentTemplatefor the page (explore.md) - you told LWR the
layoutTemplateto use for the page; in this case, you usedmain_layout.njk, which is the same as for other pages in this example, though that’s not required
- you gave an
Error Routes
You can set up routes to serve if LWR encounters a 404 or 500 error during the bootstrap of a route. Error routes take a status code value instead of a path value.
1{
2 "errorRoutes": [
3 {
4 "id": "not_found",
5 "status": 404,
6 "rootComponent": "example/notFound"
7 },
8 {
9 "id": "server_error",
10 "status": 500,
11 "contentTemplate": "$contentDir/not-found.html"
12 }
13 ]
14}Dynamic Server-Side Routing with Configuration Hooks
This is an advanced topic.
Note
Configuration hooks let you dynamically generate server-side routes for your application. On server startup, they update the configuration and global data for your app.
To set up a configuration hook, start by creating a hooks section in your lwr.config.json file. In there, add the filepath that points to the hook.
1{
2 "hooks": ["$rootDir/src/hooks/my-config-hook.ts"]
3}The following is an example of a configuration hook:
1import path from 'path';
2import { DEFAULT_LWR_BOOTSTRAP_CONFIG } from '@lwrjs/config';
3import { slugify } from '@lwrjs/shared-utils';
4import type { HooksPlugin, NormalizedLwrGlobalConfig, NormalizedLwrRoute, GlobalData } from '@lwrjs/types';
5
6const DEFAULT_MAIN_LAYOUT = 'main_layout.njk';
7const CACHE_TTL = '30m';
8interface GuideItem {
9 label: string;
10 content: string;
11}
12interface GuideSidebarItem {
13 label: string;
14 id: string;
15 url: string;
16}
17interface SiteGlobalData extends GlobalData {
18 site?: {
19 guide: GuideItem[];
20 sidebar: GuideSidebarItem[];
21 };
22}
23
24function generateGuideSidebar(guide: GuideItem[]): GuideSidebarItem[] {
25 return guide.map(({ label, content }) => {
26 return {
27 label,
28 id: `guide_${slugify(label)}`,
29 url: `/${content.replace('.md', '')}`,
30 };
31 });
32}
33
34function generateGuideRoutes(
35 guide: GuideItem[],
36 { contentDir, layoutsDir }: NormalizedLwrGlobalConfig,
37): NormalizedLwrRoute[] {
38 return guide.map(({ label, content }) => {
39 return {
40 id: `guide_${slugify(label)}`,
41 path: `/${content.replace('.md', '')}`,
42 contentTemplate: path.join(contentDir, content),
43 layoutTemplate: path.join(layoutsDir, DEFAULT_MAIN_LAYOUT),
44 cache: { ttl: CACHE_TTL },
45 bootstrap: DEFAULT_LWR_BOOTSTRAP_CONFIG,
46 };
47 });
48}
49
50// Export an Application Configuration hook
51// Configured in lwr.config.json[hooks]
52export default class MyAppHooks implements HooksPlugin {
53 initConfigs(lwrConfig: NormalizedLwrGlobalConfig, globalData: SiteGlobalData): void {
54 if (!globalData.site) {
55 throw 'Expected site global data to be defined';
56 }
57
58 // The guide is an ordered list of files we want to display
59 // Hardcoded here: src/data/site/guide.json
60 const guide = globalData.site.guide;
61
62 // Generate sidebar add it to the globalData object
63 // The data is accessed in src/layouts/partials/guide-sidebar.njk
64 globalData.site.sidebar = generateGuideSidebar(guide);
65
66 // Dynamically add a new route for each guide
67 // Note: other routes are statically declared in lwr.config.json[routes]
68 lwrConfig.routes.push(...generateGuideRoutes(guide, lwrConfig));
69 }
70}Dynamic server-side routes can use route handler functions to customize the page response at runtime.
Route Handler Functions
This is an advanced topic.
Note
Route handler functions are part of the LWR context object. They’re a server-side way to alter the current route and customize the page response at runtime. You can use route handler functions with both static and dynamic server-side routing.
Route handler functions, which are used in LWR’s server-side routing, aren’t the same as route handler modules, which are used in client-side routing.
Tip
There are a couple of differences between configuration hooks and route handler functions that are worth noting:
- A configuration hook is called once on server startup, while a route handler is triggered with each incoming page request.
- A configuration hook applies to your entire app, whereas each route handler applies only to its specified path.
You provide the path to a route handler function in lwr.config.json, like this:
1{
2 "id": "custom_route_handler",
3 "path": "/custom/:param",
4 "routeHandler": "$rootDir/src/routeHandlers/custom.ts"
5}A route handler function follows this syntax. For more information about syntax and properties of RouteHandlerFunction and the RouteHandlerViewReponse that it returns, see the Server-Side Routing Reference.
1type RouteHandlerFunction = (
2 viewRequest: LocalizedViewRequest,
3 handlerContext: HandlerContext,
4) => Promise<RouteHandlerViewResponse>;Things to note:
- Cache size. All the
ViewDefinitionResponse.viewParamsare added to the cache key for a page view response. To control cache size, monitor the number of items added. - Static route properties. The
ViewDefinitionResponse.viewParamsreplace the static route properties, so if the static route properties are needed, you must merge them into theviewParamsin the route handler function. - Markdown. The dynamic
ViewDefinitionResponse.viewParamsare available in Markdown content templates. This is notable because in general context isn’t passed into Markdown templates, unless you use a custom route handler. - View. LWR merges the
ViewDefinitionResponse.viewwith theidandbootstrapvalues from the current route. - Time-to-live. The
CacheResponse.ttlis a number, in seconds, or a time string to use as themax-ageon theCache-Controlheader. - Supported languages. You can use both TypeScript and JavaScript to create your route handler.
Customized Page Response Example
The following is an example of using a route handler function to customize a page response. The LWR server constructs the page response from this function.
1import { HandlerContext, RouteHandlerViewResponse, ViewRequest } from '@lwrjs/types';
2
3// Return customized input, from which the LWR server constructs a response
4// viewRequest = { url, requestPath, params?, query? }
5// handlerContext = { route, viewApi: { hasViewResponse, getViewResponse } }
6export default function echoRouteHandler(
7 viewRequest: ViewRequest,
8 handlerContext: HandlerContext,
9): RouteHandlerViewResponse {
10 const routeProperties = handlerContext.route.properties || {};
11 const message = viewRequest.params.message;
12
13 // return a "ViewDefinitionResponse"
14 return {
15 // Required: customize the current route by setting:
16 // { rootComponent?, contentTemplate?, layoutTemplate? }
17 view: {
18 contentTemplate: '$contentDir/echo.html',
19 },
20 // Required: pass context to the templates
21 viewParams: {
22 message, // pass the "message" path param
23 ...routeProperties, // pass the static route properties
24 },
25 // Optional: rendering options { skipMetadataCollection?, freezeAssets?, skipCaching? }
26 renderOptions: {
27 freezeAssets: true,
28 },
29 // Optional: caching options { ttl? }
30 cache: {
31 ttl: 200,
32 },
33 };
34}Overridden Page Response Example
The following is an example of using a route handler function to completely override a page response. The LWR server constructs the page response from this function.
1import { ViewRequest, ViewResponse } from 'lwr';
2
3// Return a completely custom response
4// containing some JSON data, based on the current path and query parameters
5// viewRequest = { url, requestPath, params?, query? }
6export default function jsonRouteHandler(viewRequest: ViewRequest): ViewResponse {
7 const myPathParam = viewRequest.params.my;
8 const someQueryParams = viewRequest.query?.some;
9
10 // return a "ViewResponse": { status?, body, cache?, headers? }
11 return {
12 // Required: return the response body
13 body: {
14 my: myPathParam,
15 some: someQueryParams,
16 },
17 // Optional: HTTP header map
18 headers: {
19 'Content-Type': 'application/json',
20 },
21 };
22}Developer Preview Feature