Theme structure
Every theme under _theme/ is a standalone Nuxt 4 project. This page walks through the full directory structure using _theme/vue-base/ as the example, explains each area, and shows custom overrides for plugin layouts and widgets.
Top level of a theme
_theme/vue-base/
├── app/ # Nuxt app directory (Nuxt 4 convention)
├── public/ # Static assets (served at root URL)
├── server/ # Server-side middleware, API routes
├── node_modules/ # npm dependencies
├── .nuxt/ # Nuxt build output (git-ignored)
├── .output/ # Production build output (git-ignored)
├── .env # Theme-specific env variables
├── .htaccess # Apache rules (if the theme is served via Apache)
├── nuxt.config.ts # Nuxt configuration
├── package.json # Dependencies, build scripts
├── package-lock.json
├── tsconfig.json
├── app.cjs # Legacy entry point (if present)
├── deploy.sh # Project deploy script (if present)
└── README.mdThe crucial part is app/ (Nuxt app) — that's where all the theme code lives.
app/ — the Nuxt app
app/
├── app.vue # Root component (layout wrapper)
├── components/ # Vue components
│ ├── AppFooter.vue # Frontend footer
│ ├── AppHeader.vue # Frontend header
│ ├── AppMenuEditor.vue # Menu editor frontend
│ ├── AppPagebuilder.vue # Pagebuilder entry point (loads rows → cols → widgets)
│ ├── admin/ # Ns* admin UI (NsModal, NsForm, NsTable, MediaManager, ...)
│ └── pagebuilder/ # Pagebuilder rendering
│ ├── PagebuilderRow.vue
│ ├── PagebuilderColumn.vue
│ └── PagebuilderWidget.vue
├── composables/ # Composables (useSeoHead, useSchemaOrg, useUserCenter, ...)
├── layouts/ # Nuxt layouts (admin.vue, admin-login.vue, default.vue)
├── middleware/ # Route middleware
├── pages/ # File-based routing
│ ├── admin/
│ │ └── [...slug].vue # The ONLY admin route — no index.vue!
│ └── [...slug].vue # Frontend catch-all
├── plugins/ # Nuxt plugins (see table below)
├── stores/ # Pinia stores (admin.ts, auth.ts, layout.ts)
├── utils/ # Utility functions
└── widgets/ # Pagebuilder widget registry (auto-generated!)
└── plugin-registry.js # DO NOT EDIT MANUALLYWidget components don't live here
Pagebuilder widget components (testimonials, banners, shop listings, …) live not in components/widgets/, but under _public/extensions/core/backend/{plugin}/widgets/{widget_template}/template/index.vue. See Widgets › Widget Anatomy.
No admin/index.vue
The admin routing is a single catch-all route: app/pages/admin/[...slug].vue. No index.vue underneath — the Nuxt router would otherwise produce conflicts. All admin plugins are loaded dynamically into this route via import.meta.glob.
components/admin/ — backend UI building blocks
The admin UI uses a shared set of reusable components with the Ns prefix:
| Component | Purpose |
|---|---|
NsTable | Lists with PrimeVue DataTable, pagination, search |
NsForm | Dynamic form rendering from plugin config |
NsModal | Modals with language tabs, Bootstrap 5-based |
NsSelect | TomSelect wrapper for searchable dropdowns |
NsModuleList | Sub-lists inside a modal (the show_module_list button) |
NsTemplate | Loader for template-construct plugins |
NsDetail | Detail view for content_construct=table |
NsCustomContent | Renderer for the show_custom_content button |
MediaManager | File picker with tree navigation + upload |
Details on these components: the php-backend-check skill (references/vue-components.md).
plugins/ — Nuxt plugins
| Plugin | Client/server | Purpose |
|---|---|---|
ab-tracking.client.ts | client | A/B test impression/conversion tracking |
admin-css.client.ts | client | CSS isolation for /admin (loads admin styles only there) |
admin-i18n.ts | both | Admin translations (also runs in SSR so the first paint is localized) |
ai-admin.client.ts | client | AI feature injection into admin forms (data-aifeature attributes) |
ckeditor.client.ts | client | CKEditor 5 setup for textarea fields with rich text |
cookieconsent.client.ts | client | Cookie consent banner (4 categories) |
gsap-animations.client.ts | client | GSAP scroll animations, lazy-loaded |
primevue.ts | both | PrimeVue setup (foundation for the NsTable DataTable) |
widgets.client.ts | client | Plugin widget registration (user-center items etc.) |
The .client.ts suffix means: run in the browser only, not in SSR. Plugins without the suffix (admin-i18n.ts, primevue.ts) also run in SSR — required for PrimeVue to produce server-side DataTable markup, and for i18n to localize the first paint.
stores/ — Pinia stores
| Store | Purpose |
|---|---|
admin.ts | Active plugin, navigation, systemVersion |
auth.ts | Frontend auth session (login status, user profile) |
layout.ts | Mobile menu state, menu-editor state |
Plugin-specific stores (e.g. sw6Store.ts for Shopware 6 cart/customer/config) live not in the theme, but in the respective plugin under _public/extensions/core/backend/{plugin}/widgets/.../. That keeps the store and its logic bundled with the plugin and carried along on install/uninstall.
public/ — static assets
public/
├── backend/
│ ├── css/
│ │ ├── style.less # Admin light theme (glassmorphism)
│ │ ├── style-darkmode.less # Admin dark theme
│ │ └── index.less # LESS entry point
│ └── js/
│ └── pagebuilder/ # Preview overlay scripts (postMessage)
├── css/ # Frontend-specific styles
├── fonts/ # Font files
├── favicon.ico
└── robots.txtFiles in here are served from the root URL path — public/favicon.ico → /favicon.ico.
Admin styling details: the NewSemantics-Backend-Layout skill.
server/ — server side
Nuxt server middleware and API routes. In the CMS today, it's lightweight — most API calls go directly to the PHP backend (/api/*), not to Nuxt server routes.
nuxt.config.ts
The central Nuxt configuration. Key sections:
export default defineNuxtConfig({
// Nuxt modules (@pinia/nuxt, @nuxt/image, @vueuse/nuxt, ...)
modules: [ /* ... */ ],
// Runtime config (env variables)
runtimeConfig: {
apiKey: process.env.NUXT_API_KEY,
public: {
apiBase: process.env.NUXT_PUBLIC_API_BASE,
siteUrl: process.env.NUXT_PUBLIC_SITE_URL || '',
theme: process.env.NUXT_PUBLIC_THEME || 'vue-base',
}
},
// Cross-plugin aliases
alias: {
'#extensions': resolve(process.cwd(), '../../_public/extensions/core/backend'),
'#widgets/plugin-registry': resolve(process.cwd(), 'app/widgets/plugin-registry.js'),
},
// Vite + PurgeCSS safelist
vite: {
plugins: [
purgecss({
safelist: {
standard: [ /^swiper/, /^fa-/, /^ns-/, /* ... */ ],
greedy: [ /^row_/, /^col_/, /^widget_/, /* ... */ ],
deep: [ /data-animation/ ]
}
})
]
}
})Details on env variables: Environment variables. Details on aliases: Nuxt aliases.
.env
NUXT_API_KEY=123456
NUXT_PUBLIC_API_BASE=https://demo.new-semantics.com/api
NUXT_PUBLIC_SITE_URL=https://demo.new-semantics.com
NUXT_PUBLIC_THEME=vue-baseProject-specific safelist additions also go into .env — see Environment variables › Project-specific PurgeCSS safelist.
package.json — scripts
{
"scripts": {
"dev": "nuxt dev",
"build": "nuxt build",
"generate": "nuxt generate",
"preview": "nuxt preview"
}
}Every theme has its own build — npm install and npm run build happen in the theme folder, not at the project root. Details: Deployment.
Custom overrides (theme-specific)
Themes can override plugin layouts and widget templates without touching plugin code:
_theme/{theme}/app/custom/{plugin}/widgets/{widget_template}/template/index.vueFor example, a project theme can override the categoryBannerNoLink widget:
_theme/projekt01/app/custom/shop/widgets/categoryBannerNoLink/template/index.vueThe vueRegistry rebuild (/admin/cache) prefers this file over the original from _public/extensions/core/backend/shop/widgets/categoryBannerNoLink/template/index.vue. Details: Widgets › Widget Anatomy › Custom theme overrides.
Similarly for menu styling: every theme can have its own public/css/menu.less, loaded by the menu editor via the compiled_css endpoint.
Theme variants: vue-base vs. projekt01
| Aspect | vue-base | projekt01 |
|---|---|---|
| Purpose | Default/production theme | Project fork for customer customizing |
app/ | Fully equipped | Copy + adjustments |
nuxt.config.ts | System default | Project safelist, custom aliases |
custom.less (via /admin/design) | Default tokens | Project tokens |
| Custom overrides | none | app/custom/* overrides plugin widgets |
A new project fork copies vue-base/ in full and renames the folder — the build process and every convention stay identical.
Common issues
Creating app/pages/admin/index.vue
Breaks the Nuxt router convention — a catch-all plus an index leads to colliding routes. Use only [...slug].vue.
.env not loaded
Nuxt loads .env automatically from the theme root (not from the project root). Env variables must live in the respective theme folder, not in a central place.
Shared code imported from _theme/base/ but not resolved
_theme/base/ isn't an npm package — it's pulled in via relative paths or a Nuxt alias. Details on aliases: Nuxt aliases.
plugin-registry.js edited manually
The file is regenerated on cache rebuild from every widget folder. Changes are lost. For widget changes, always edit the widget template itself.
Theme switch without npm install
Each theme has its own node_modules/. Switching from vue-base to projekt01 requires npm install in the new folder.
See also
- Themes overview — theme concept and
NUXT_PUBLIC_THEME - Environment variables — every env variable with an explanation
- Design tokens — LESS tokens + admin UI
- Nuxt aliases —
#extensions,#widgets/plugin-registry - Deployment — production build, nginx, Update Manager
- Widgets › Widget Anatomy — theme widget overrides
- Plugins › Plugin Anatomy — admin plugin layouts under
_public/extensions/