Skip to content

User-Center Items

The shop widget user (the user center at /usercenter) ships with a plugin registry: other plugins can register menu entries there without touching the shop widget. This page covers the UserCenterItem interface, registration via registerUserCenterItem(), and the Nuxt alias #extensions for cross-plugin imports.

What for?

By default, the user center shows shop-specific menu entries: overview, profile, addresses, orders, subscriptions. Other plugins often need their own sections:

PluginItemPurpose
elearning"My courses"Course progress + episode list
elearning"My certificates"Download BZAEK certificates as PDF
Custom plugin"My favorites"Wishlist, bookmarks, …
Custom plugin"Support tickets"Integration with a helpdesk system

Without the registry, the shop user widget would have to be forked every time — with the registry, each plugin registers its own items.

The UserCenterItem interface

The composable lives at _public/extensions/core/backend/shop/widgets/user/template/useUserCenter.ts:

ts
export interface UserCenterItem {
    id:           string                  // URL segment, e.g. 'profil', 'my-courses'
    label:        string                  // Menu label
    description?: string                  // Subtitle under the label
    icon?:        string                  // Font Awesome class (e.g. 'fa-light fa-award')
    order:        number                  // Numeric sort order (ascending)
    component:    Component               // Vue component rendered in the content area
    isVisible?:   () => boolean           // Optional: reactive visibility check
}
FieldRequiredPurpose
idyesURL segment — generates the route /usercenter/{id}. Only a-z0-9- allowed
labelyesVisible menu entry in the sidebar
descriptionnoSubtitle under the label
iconnoFont Awesome class including the variant (fa-light fa-award)
orderyesSort order (steps of 10 recommended so plugins can slot themselves in between)
componentyesVue component rendered in the content area
isVisiblenoFunction returning true/false — reactive against stores

Registering from a plugin file

Each plugin places a file under widgets/{widget-name}/template/plugins/userCenterItems.ts. The file exports a default function that gets called explicitly when the user-center widget mounts — not a side-effect import.

Real example from elearning/widgets/elearning/template/plugins/userCenterItems.ts:

ts
import { registerUserCenterItem } from '#extensions/shop/widgets/user/template/useUserCenter'
import MyCoursesUserCenter from '../MyCoursesUserCenter.vue'
import MyCertificates      from '../MyCertificates.vue'

export default () => {
    registerUserCenterItem({
        id:          'my-courses',
        label:       'My courses',
        description: 'Your booked courses',
        icon:        'fa-light fa-graduation-cap',
        order:       60,
        component:   MyCoursesUserCenter,
    })

    registerUserCenterItem({
        id:          'my-certificates',
        label:       'My certificates',
        description: 'Download certificates',
        icon:        'fa-light fa-award',
        order:       70,
        component:   MyCertificates,
    })
}

Default function instead of a top-level call — why?

A side-effect import with a top-level registerUserCenterItem(...) would be removed by tree-shaking — the import has no used bindings, so the bundler optimizes it away. The default function forces the import graph to keep the module alive and additionally lets you control the lifecycle (register on mount, not on module load).

The #extensions alias

#extensions points to _public/extensions/core/backend/ — configured in _theme/vue-base/nuxt.config.ts:

ts
alias: {
    '#extensions': resolve(process.cwd(), '../../_public/extensions/core/backend'),
    // ...
}

It lets you write cross-plugin imports without fragile relative paths:

ts
// Instead of:
import { registerUserCenterItem } from '../../../shop/widgets/user/template/useUserCenter'

// Better:
import { registerUserCenterItem } from '#extensions/shop/widgets/user/template/useUserCenter'

Details on the alias: Themes › Nuxt aliases.

Rendering inside the user-center widget

The shop user widget entry point (shop/widgets/user/template/index.vue) imports every registrator function on mount and calls them once. Only then does the reactive registry fill up:

vue
<!-- shop/widgets/user/template/index.vue (schematic) -->
<script setup lang="ts">
import { onMounted } from 'vue'
import { useUserCenter } from './useUserCenter'
import registerShopItems      from './plugins/userCenterItems'
import registerElearningItems from '#extensions/elearning/widgets/elearning/template/plugins/userCenterItems'

onMounted(() => {
    registerShopItems()         // Shop items (overview, profile, addresses, orders, subscriptions)
    registerElearningItems()    // E-learning items (my-courses, my-certificates)
})

const { items } = useUserCenter()
const activeId = computed(() => route.params.slug?.[0] || items.value[0]?.id)
const active   = computed(() => items.value.find(i => i.id === activeId.value))
</script>

UserMenu.vue renders the sidebar navigation from items.value; the content area loads the active component via <component :is="active.component" />.

The plugin file has to be imported

Simply placing a userCenterItems.ts file is not enough. The shop user widget has to import and call the default function. Without the import, the registration code never runs.

order — sort order

The default shop items use values 10-50, e-learning 60-70. Convention: 10-step increments leave gaps for items to slot between.

PluginItemorder
shopoverview10
shopprofil20
shopadressen30
shopbestellungen40
shopabonnements50
elearningmy-courses60
elearningmy-certificates70

New plugin items: pick a free 10-step slot. Every item is sorted by order ascending inside useUserCenter().

isVisible — reactive visibility

Items can show or hide themselves conditionally — e.g. "My courses" only when the user has actually bought courses:

ts
import { useShopStore } from '#extensions/shop/widgets/user/template/useShopStore'

const shopStore = useShopStore()

registerUserCenterItem({
    id:        'my-subscriptions',
    label:     'Subscriptions',
    order:     50,
    component: SubscriptionsView,
    isVisible: () => shopStore.hasSubscriptions,
})

isVisible has to be reactive

useUserCenter() evaluates isVisible inside a computed — the function is called again on every access. It has to read reactive sources (Pinia store, useState, ref) — static values or snapshot reads won't work correctly.

Component style (namespace)

Every user-center component gets a unique root class as a CSS namespace:

vue
<template>
  <div class="ns-my-certificates">
    <h2>My certificates</h2>
    <!-- ... -->
  </div>
</template>

<style scoped>
.ns-my-certificates {
    padding: 1.5rem;
}
.ns-my-certificates h2 {
    margin-bottom: 1rem;
}
</style>

Convention: ns-{feature-name}. All default shop items follow it (ns-user-panel-edit, ns-user-orders, ns-user-addresses, ns-my-courses, ns-my-certificates). Avoid global CSS — scoping matters because many user-center components exist in parallel and must not interfere with each other.

URL routing

id is simultaneously the URL segment. The shop user widget uses route.params.slug:

/usercenter                  → items[0] (overview)
/usercenter/profil           → id = "profil"
/usercenter/my-courses       → id = "my-courses"
/usercenter/my-certificates  → id = "my-certificates"

id must be URL-safe — only lowercase letters, digits, and hyphens. No slashes, no umlauts, no whitespace.

Common issues

Plugin file not imported

The shop user widget has to import and call the registrator function actively. If the import is missing from shop/widgets/user/template/index.vue, the registration code never runs — the item doesn't appear in the menu.

id not URL-safe

id: 'My Courses' breaks routing. Always a-z0-9- — e.g. my-courses, meine-kurse.

component: MyView, but MyView isn't imported

The import has to live at the top of the file explicitly. component expects the Vue component, not the file name.

isVisible reads a non-reactive source

isVisible: () => localStorage.getItem('flag') === '1' doesn't work — the value isn't re-evaluated when localStorage changes. Only Pinia store, useState, ref, computed.

Tree-shaking removes side-effect imports

ts
// WRONG — removed by the bundler:
import './plugins/userCenterItems'
registerUserCenterItem(...)   // at the top level

// RIGHT — default function + explicit call:
import registerItems from './plugins/userCenterItems'
onMounted(() => registerItems())

CSS namespace for your own items

Every default component uses ns-{name} as the root class for scoped CSS. New items should follow the pattern — otherwise CSS from neighboring items can overlap.

See also

  • Widget Anatomy — the user-center widget context (shop/widgets/user/)
  • Themes › Nuxt aliases — the #extensions alias in detail
  • Example: Testimonial Widget — full widget build
  • _public/extensions/core/backend/shop/widgets/user/template/useUserCenter.ts — composable source
  • _public/extensions/core/backend/elearning/widgets/elearning/template/plugins/userCenterItems.ts — real e-learning registration pattern