Skip to content
Orbix Nuxt · Bootstrap 5 v1.0.3

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 fieldMeaning
layoutapp, auth (plus variant) or blank: the file in src/layouts/
idPage id: marks the current menu entry (the menu entry with the same id)
titleTranslation key of the document title (… · Orbix)
variantAuth layout style: basic, cover or illustrated
attrsOptional 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 in onMounted, 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' }],
    },
  ],
},
KeyMeaning
idUnique navigation id; the page with the same id marks the entry as current and opens its group
toRoute path of the page
keyTranslation key of the label
iconIcon id in the sprite: lc-* (Lucide), tb-* (Tabler), fd-i-* or ox-i-* (Orbix)
badgeOptional counter: { count: 4, tone: 'primary' }

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).