Skip to content

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

The 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 MANUALLY

Widget 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:

ComponentPurpose
NsTableLists with PrimeVue DataTable, pagination, search
NsFormDynamic form rendering from plugin config
NsModalModals with language tabs, Bootstrap 5-based
NsSelectTomSelect wrapper for searchable dropdowns
NsModuleListSub-lists inside a modal (the show_module_list button)
NsTemplateLoader for template-construct plugins
NsDetailDetail view for content_construct=table
NsCustomContentRenderer for the show_custom_content button
MediaManagerFile picker with tree navigation + upload

Details on these components: the php-backend-check skill (references/vue-components.md).

plugins/ — Nuxt plugins

PluginClient/serverPurpose
ab-tracking.client.tsclientA/B test impression/conversion tracking
admin-css.client.tsclientCSS isolation for /admin (loads admin styles only there)
admin-i18n.tsbothAdmin translations (also runs in SSR so the first paint is localized)
ai-admin.client.tsclientAI feature injection into admin forms (data-aifeature attributes)
ckeditor.client.tsclientCKEditor 5 setup for textarea fields with rich text
cookieconsent.client.tsclientCookie consent banner (4 categories)
gsap-animations.client.tsclientGSAP scroll animations, lazy-loaded
primevue.tsbothPrimeVue setup (foundation for the NsTable DataTable)
widgets.client.tsclientPlugin 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

StorePurpose
admin.tsActive plugin, navigation, systemVersion
auth.tsFrontend auth session (login status, user profile)
layout.tsMobile 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.txt

Files 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:

ts
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

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-base

Project-specific safelist additions also go into .env — see Environment variables › Project-specific PurgeCSS safelist.

package.json — scripts

json
{
    "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.vue

For example, a project theme can override the categoryBannerNoLink widget:

_theme/projekt01/app/custom/shop/widgets/categoryBannerNoLink/template/index.vue

The 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

Aspectvue-baseprojekt01
PurposeDefault/production themeProject fork for customer customizing
app/Fully equippedCopy + adjustments
nuxt.config.tsSystem defaultProject safelist, custom aliases
custom.less (via /admin/design)Default tokensProject tokens
Custom overridesnoneapp/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