Frontend

Pages & Routing

Page structure, definePageMeta, and routing conventions.

File Conventions

Pages live in app/pages/. Nuxt file-based routing applies:

FileRoute
app/pages/index.vue/
app/pages/login.vue/login
app/pages/bookings/index.vue/bookings
app/pages/bookings/[id].vue/bookings/:id
app/pages/[organization]/index.vue/:organization (dynamic org slug)

Page Structure

Pages follow the same <script setup> convention as components:

<script lang="ts" setup>
// 1. Page meta — always first in <script setup>
definePageMeta({
  layout: 'default',
  middleware: ['auth'],
})

// 2. Composables
const { getBookings, items, page, pages } = useBooking()
const { handleApiAction, loading }        = useFormHandler()

// 3. Reactive state
const dialogs = reactive({ view: false, cancel: false })
const selected = ref<Record<string, any> | null>(null)

// 4. Data fetching — called manually (SPA, no SSR)
onMounted(async () => {
  await fetchBookings()
})

async function fetchBookings() {
  await handleApiAction(
    async () => {
      const res = await getBookings({ page: page.value })
      items.value = res.bookings
      pages.value = res.pages
    },
    { skipAlert: true },
  )
}
</script>

<template>
  <div class="space-y-6">
    <h1 class="text-2xl font-bold">My Bookings</h1>

    <BookingTable
      :bookings="items"
      @view="(b) => { selected = b; dialogs.view = true }"
      @cancel="(b) => { selected = b; dialogs.cancel = true }"
    />

    <BookingViewModal
      v-if="dialogs.view"
      :booking="selected"
      @close="dialogs.view = false"
    />
  </div>
</template>

definePageMeta

Always declared at the top of <script setup>:

definePageMeta({
  layout: 'default',          // matches app/layouts/default.vue
  middleware: ['auth'],       // named middleware file(s) to run
})

Common middleware values:

ValueFileEffect
'auth'app/middleware/auth.tsRedirects to /login if no token
'admin'app/middleware/admin.tsThrows 403 if not admin role
'org'app/middleware/01.org.tsLoads org into state

Dynamic Routes

Dynamic segments use [param] in the file name and useRoute() to access:

// app/pages/bookings/[id].vue
const route = useRoute()
const bookingId = route.params.id as string

Data Fetching Pattern

All data fetching is done manually inside onMounted — not with useAsyncData or useFetch at the top level — because SSR is disabled and we want explicit control:

onMounted(async () => {
  await loadData()
})

async function loadData() {
  await handleApiAction(
    async () => {
      const res = await someComposable.getData()
      items.value = res.items
    },
    { skipAlert: true },   // suppress toast on initial load
  )
}

Pair with a watch when a filter or page number changes:

watch(page, () => loadData())
watch(searchQuery, useDebounceFn(() => loadData(), 400))