--- title: Create a page description: Add a page to a Lux app. A page is a Lit web component that Lux registers as a route based on where you put its file. locale: en-US canonical_url: https://www.servicenow.com/docs/r/australia/application-development/create-a-page.html release: australia topic_type: task last_updated: "2026-09-29" reading_time_minutes: 3 keywords: [Create a page, Goal, Prerequisites, Verify, See also] breadcrumb: [Build with Lux, Lux Development, Building pro-code applications, Developing your application, Building applications] --- # Create a page Add a page to a Lux app. A page is a Lit web component that Lux registers as a route based on where you put its file. ## Before you begin - Role required: admin - A scaffolded Lux app \(see the getting-started topic for your app; out of scope here\). You need an existing `pages/` directory and a working `pnpm build`/dev server setup. - Familiarity with Lit \(`html`, `customElement`, reactive properties\). ## About this task Create a working page that renders at a URL in your app, optionally fetching data on the server before it renders. ## Procedure 1. Create a directory under `pages/` Lux uses file-based routing: the directory path under `pages/` becomes the URL path. Create one directory per route segment. ``` mkdir -p pages/risk ``` A nested path becomes a nested route: ``` mkdir -p pages/settings/preferences ``` `pages/settings/preferences/page.js` maps to `/settings/preferences`. 2. Add the page file. Inside the directory, create `page.js` \(or `page.ts`\). The build looks for a file named `page.js`/`page.ts` to terminate a route; anything else in that directory is ignored. Older applications may name the page file `index.js` instead. Both work: `page.js` is preferred for new code, and an existing app can migrate gradually. ``` // pages/risk/page.js import {html} from 'lit'; import {customElement} from 'lit/decorators.js'; import {AIUXElement} from '@servicenow/aiux-components-core'; @customElement('my-app-risk-page') export default class RiskPage extends AIUXElement { static async loader(ctx) { return {title: 'Risk'}; } render() { return html`

${this.loaderData?.title}

`; } } ``` A page file must meet the following requirements: - Extend `AIUXElement`, not `LitElement` directly: it supplies Tailwind/DaisyUI styling, the `loaderData` accessor, and SSR safety. - `@customElement(...)` registers the tag. Prefix it with your app name so it doesn't collide with another app's tags \(`my-app-risk-page`, not `risk-page`\). - `export default`: the build's page discovery looks for the default export. Any other component you define in the same directory should use a named export instead. - `render()` must be pure and deterministic: it runs during server-side rendering and in the browser, so it can't touch `window`, `document`, or `localStorage`. 3. Fetch data with a loader. `static async loader(ctx)` runs on the server before the page renders. Its return value is JSON-serialized into the page and becomes available as `this.loaderData`. ``` import {AIUXElement, getHeaders} from '@servicenow/aiux-components-core'; @customElement('my-app-risk-page') export default class RiskPage extends AIUXElement { static async loader(ctx) { const res = await fetch( `${ctx.protocol}://${ctx.hostname}/api/now/table/sn_risk_risk`, {headers: getHeaders(ctx)} ); return {items: (await res.json()).result ?? []}; } render() { const {items} = this.loaderData; return html`
${items.map(item => html`
${item.number}
`)}
`; } } ``` Use `getHeaders(ctx)` from `@servicenow/aiux-components-core` when calling ServiceNow table APIs: it forwards the session cookie and CSRF token in one call. `loader` runs on the server, so no browser globals here either. **Note:** Route parameters and query parameters on `ctx` are strings: cast them yourself if you need a number or boolean. 4. Add a dynamic route segment. Wrap a directory or file name in square brackets to declare a path parameter: ``` mkdir -p "pages/risk/[riskId]" ``` `pages/risk/[riskId]/page.js` maps to the route pattern `/risk/:riskId`. The value lands on `ctx.params` in the loader: ``` static async loader(ctx) { const {riskId} = ctx.params; // a string // fetch the record identified by riskId ... return {riskId}; } ``` You can nest dynamic segments, for example `pages/risk/[riskId]/controls/[controlId]/page.js` for `/risk/:riskId/controls/:controlId`. 5. Build and check the route. ``` pnpm build ``` The build walks `pages/` and writes a route manifest mapping each route pattern to its bundle and tag name. Your app's basename \(from `aiux.json`\) is prepended to every route to form the production URL: `/aiux/`. For a basename of `my-app`, `pages/risk/page.js` serves at `/aiux/my-app/risk`. 6. Link to the page. A page isn't reachable until something links to it. Either use a normal anchor tag: ``` render() { return html`Go to Risk`; } ``` or navigate programmatically from an event handler: ``` import {navigate} from '@servicenow/aiux-client'; _openRecord(e) { const {riskId} = e.currentTarget.dataset; navigate(`/risk/${riskId}`); } ``` The router intercepts a same-origin `` select automatically and swaps in the new page client-side, without a full reload. It intercepts only a plain left click, with no modifier keys, no `target="_blank"`. ## Verify - `pnpm build` succeeds. - Open the route in a browser \(or check the Network tab's response preview during SSR\) and confirm your rendered content and loader data appear. - If the page uses `navigate()` or a sidebar link, select it and confirm client-side navigation works without a full page reload. ## What to do next - [Page collections](https://raw.githubusercontent.com/ServiceNow/ServiceNowDocs/australia/markdown/australia/application-development/ui-builder/page-collections.md): serving different page implementations from the same route - : wiring a page into the app's shell and navigation - : the full loader contract and `ctx` shape