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:
| Plugin | Item | Purpose |
|---|---|---|
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:
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
}| Field | Required | Purpose |
|---|---|---|
id | yes | URL segment — generates the route /usercenter/{id}. Only a-z0-9- allowed |
label | yes | Visible menu entry in the sidebar |
description | no | Subtitle under the label |
icon | no | Font Awesome class including the variant (fa-light fa-award) |
order | yes | Sort order (steps of 10 recommended so plugins can slot themselves in between) |
component | yes | Vue component rendered in the content area |
isVisible | no | Function 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:
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:
alias: {
'#extensions': resolve(process.cwd(), '../../_public/extensions/core/backend'),
// ...
}It lets you write cross-plugin imports without fragile relative paths:
// 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:
<!-- 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.
| Plugin | Item | order |
|---|---|---|
| shop | overview | 10 |
| shop | profil | 20 |
| shop | adressen | 30 |
| shop | bestellungen | 40 |
| shop | abonnements | 50 |
| elearning | my-courses | 60 |
| elearning | my-certificates | 70 |
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:
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:
<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
// 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
#extensionsalias 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