Add a page
Example used to test this page: an /about page that lists the latest
items with serverApi(), and a small form, shown to signed-in users, that
adds an item through a Server Action.
frontend/app/settings/ is the reference: an async Server Component in
page.tsx, a colocated actions.ts, a client island, and a colocated SASS
module. Copy it.
Steps
Create
frontend/app/<route>/page.tsxas an async Server Component (no'use client'). Fetch withserverApi(), the way the backoffice email page does:const client = await serverApi(); const { data, error } = await client.GET('/admin/email/stats');serverApi()attaches the session's bearer token on the server, so the token never reaches the browser. With no session it sends no header, so public routes still work. Always handleerroror a missingdataand render a message instead of throwing.To know who is signed in, call
auth()from@/auth, asapp/settings/page.tsxdoes (const session = await auth();).Set the title with
export const metadata: Metadata = { title: '... | CrabStack' }. A private page also setsrobots: { index: false, follow: false }, as settings does.Put mutations in a colocated
actions.tsthat starts with'use server'. Each action callsserverApi()itself, catches failures, and returns an error state, the wayopenBillingPortalActioninapp/settings/actions.tsdoes:try { const client = await serverApi(); const { data, error } = await client.POST('/billing/portal'); if (error || !data) { return { error: 'Could not open the billing portal. Try again.' }; }After a successful write, call
revalidatePath('/<route>')fromnext/cacheso the page shows the new data, asapp/backoffice/users/actions.tsdoes. Validate form input in the action too; the browser's checks are not a security boundary.Make the interactive part a client island: a separate file that starts with
'use client'and wires the action with React'suseActionState, asapp/settings/AccountSettings.tsxdoes:const [state, formAction, pending] = useActionState( deleteAccountAction, INITIAL_STATE, );The island gets data as props from the page and writes only through the action. Never import
serverApi()into it. The build still passes if you do; the call then throwsserverApi() must not be called from the browserat runtime.Style it with a colocated
<route>.module.scssthat starts with@use '../styles/palette' as *;, or reuseapp/styles/page-shell.module.scss, the shell the pricing and legal pages share.Link to it. There is no shared site nav component. The landing page's top nav and footer are inline in
frontend/app/page.tsx(<nav className={styles.nav}>and<footer>); add a<Link>there. Other pages carry only a logo link back to/. For a public page, add an entry tostaticEntriesinfrontend/app/sitemap.ts.Outside images and scripts are blocked until you allow them.
buildCsp()infrontend/proxy.tssets the Content-Security-Policy on every page:"img-src 'self' data: blob: https://images.unsplash.com",- An image through
next/imagefrom another host: add the host toimages.remotePatternsinfrontend/next.config.ts, never a wildcard. - A plain
<img>or CSSurl()from another host: add the host toimg-srcinbuildCsp(). - A third-party script:
script-srcis a per-request nonce plus'strict-dynamic', so a script runs only if it carries this request's nonce or was loaded by a script that does. Browsers that support'strict-dynamic'ignore host names inscript-src, so adding the script's host there does nothing. Next's own scripts carry the nonce, so a script loaded withnext/scriptruns, and so does everything it loads, with full access to the page. Nothing infrontend/proxy.tshas to change for that, so it is a security decision you make in your page code: load only a script you have chosen to trust. See Troubleshooting. - Browser requests go only to this site and the API origin
(
connect-src).
A blocked resource shows up as a CSP error in the browser console, not as a build or test failure.
- An image through
Pick the path with the gates in mind.
proxy.ts404s every/backoffice/*path for anyone who is not an admin, so do not put a user page there. For an admin page, see Add a backoffice screen.Run
./verify.sh. The frontend stage runs format, lint, knip, the tests andnext build; the build output lists the new route.
What the test run found
/aboutwithserverApi(), anactions.tsand a client island, plus the nav link and sitemap entry: verify all green, and the build listedƒ /about.- Importing
serverApiinto the client island did not fail the build, which is why step 3 is a rule to follow and not something verify catches.
Related
- Frontend NextAuth Setup - why the token stays server-side
- Consume from the frontend