Adding a page and a menu item
Add a page
1. Create the page src/pages/reports/sales.vue (Nuxt file-based routing: the file path is the URL,
/reports/sales):
<script setup lang="ts">
import { definePageMeta } from '#imports';
import PageHeader from '@/components/shell/PageHeader.vue';
import { useI18n } from '@/composables/useI18n';
import { usePageScript } from '@/composables/usePageScript';
definePageMeta({ layout: 'app', id: 'reports-sales', title: 'reports.sales.title' });
const { t } = useI18n();
usePageScript();
</script>
<template>
<PageHeader title="reports.sales.title" subtitle="reports.sales.subtitle" section="nav.reports" sectionHref="/reports/sales" />
<div class="fd-page">
<section class="fd-card">
<div class="fd-card__body">
<p data-i18n="reports.sales.empty">{{ t('reports.sales.empty') }}</p>
</div>
</section>
</div>
</template>definePageMeta field | Meaning |
|---|---|
layout | app, auth (plus variant) or blank: the file in src/layouts/ |
id | Page id: marks the current menu entry (the menu entry with the same id) |
title | Translation key of the document title (… · Orbix) |
variant | Auth layout style: basic, cover or illustrated |
attrs | Optional attributes set on <html> while the page is shown |
Nuxt route params work too: src/pages/reports/[id].vue is /reports/:id (read it with useRoute().params.id).
2. Register the page in src/config/pages.ts, with the same data and its path:
{ path: '/reports/sales', id: 'reports-sales', title: 'reports.sales.title', layout: 'app' },The static generation prerenders every path of this list (nitro.prerender.routes in nuxt.config.ts), and the
tests check that every page file matches its line. For a page with a route param, list each path you want
prerendered (/reports/2026, /reports/2025) or use the Node.js server build, which renders any path on request.
3. Add the texts to every src/locales/<locale>/ folder, for example a new reports.json (see
Languages):
{
"sales": {
"title": "Sales report",
"subtitle": "Revenue by channel and region",
"empty": "No sales in this period yet."
}
}4. Optional page script. Code that works on the rendered markup in the browser (third-party widgets, charts,
timers) goes in a function passed to usePageScript(). It runs once the page is mounted and the app has started
(never on the server); listeners on document/window added with listen(), timers added with every() and
charts are released when the page unmounts:
// src/scripts/reports.ts
import { listen } from '../core/scope';
export function init(): void {
listen(document, 'keydown', (event) => {
if (event.key === 'r' && event.altKey) document.querySelector<HTMLButtonElement>('[data-report-refresh]')?.click();
});
}import { init } from '@/scripts/reports';
// ...
usePageScript(init);For state that belongs to Vue (filters, lists, forms), use regular ref, computed, v-model and composables
inside the component. Remember that the page is also rendered on the server: read window, document or
localStorage only in onMounted (or in a page script), or wrap the part that needs them in <ClientOnly>
(import { ClientOnly } from '#components').
Visit http://localhost:3000/reports/sales.
Pages that need real data fetch it from your API with Nuxt's
useFetch/useAsyncData(rendered on the server with the Node.js build, or at generation time with the static build) or in the browser inonMounted, and render it with the same markup (v-for,v-if).
Add a menu item
The sidebar, the horizontal menu, the two-column rail and the command palette are all built from
src/config/menu.ts. Sections hold items and groups; groups hold children:
{
id: 'reports',
key: 'nav.reports', // section title (translation key)
icon: 'lc-chart-column',
items: [
{ id: 'reports-sales', to: '/reports/sales', key: 'nav.sales_report', icon: 'lc-chart-line' },
{
id: 'reports-more',
key: 'nav.more_reports',
icon: 'lc-folder-open',
children: [{ id: 'reports-stock', to: '/reports/stock', key: 'nav.stock_report' }],
},
],
},| Key | Meaning |
|---|---|
id | Unique navigation id; the page with the same id marks the entry as current and opens its group |
to | Route path of the page |
key | Translation key of the label |
icon | Icon id in the sprite: lc-* (Lucide), tb-* (Tabler), fd-i-* or ox-i-* (Orbix) |
badge | Optional counter: { count: 4, tone: 'primary' } |
Links
Use <AppLink to="/reports/sales"> (src/components/shell/AppLink.vue) for links between pages: a plain <a>
that navigates through the Nuxt router without adding classes of its own, so the fd-* markup stays exactly as
designed. <NuxtLink> works too (import { NuxtLink } from '#components'). From code, use
useRouter().push('/reports/sales') or navigateTo('/reports/sales') from #imports. Plain <a href> links
created at runtime (for example inside a data grid cell) are handled by the router as well, so the app never
reloads.
Icons
Icons of the design system (fd-i-*) are a component:
<script setup lang="ts">
import { FdIcon } from '@foundry/ui/vue';
</script>
<template>
<FdIcon name="search" size="sm" />
</template>Any icon of the bundled sets is an SVG <use> reference:
<svg class="fd-icon" aria-hidden="true"><use href="#lc-chart-line" /></svg>Browse the sets on the Icons pages of the template. Only the icons you reference are included in the sprite:
after adding a new lc-* or tb-* icon, run npm run assets (it also runs with npm run dev, npm run build
and npm test).