Jump to content

Frontend

Build system, asset pipeline, JavaScript/SCSS architecture, and best practices.

MP-Core logo

Requirements

  • Node.js >=22 (Node 24 recommended)
  • npm >=10

Technology Stack

  • Vite 8 -- Build tool with HMR
  • Vue.js 3.5 -- Interactive components (TodoList, GallerySwiper, SwiperSlider)
  • Bootstrap 5.3 -- UI framework
  • Sass 1.99 -- CSS preprocessing (modern-compiler API)
  • PostCSS -- preset-env, pxtorem
  • ESLint 10 / Stylelint 17 -- Code quality
  • Swiper 12 -- Touch sliders (integrated via Vue components)
  • Jarallax 3 -- Parallax scrolling

Quick Start

 
cd Build
npm ci
npm run watch   # Auto-rebuild on file changes
 

In an mpc monorepo with DDEV running, you can also build from the site root: ddev mp-core-build (same as npm run build inside libs/mp-core/Build/).

Output goes to Resources/Public/ (JavaScripts, StyleSheets, Fonts, Icons, Images, Favicons, BackendLayouts).

ScriptDescription
buildLint + production build (minified, optimized) + bundle-size gate
build:analyzeProduction build with rollup-plugin-visualizer (writes reports/bundle-stats.html)
devLint + development build with source maps
watchDevelopment build with file watcher
check-sizeRun the bundle-size budget gate against Resources/Public/
lintRun ESLint + Stylelint
eslint / eslint.fixJavaScript linting
stylelint / stylelint.fixCSS/SCSS linting

Clean build: rm -rf node_modules Resources/Public && npm ci && npm run build

Project Structure

Build Directory

 
Build/
├── Assets/
│   ├── Fonts/                  # Web fonts (WOFF2)
│   ├── Images/                 # Source images, Icons/
│   ├── Scripts/                # JavaScript/Vue
│   │   ├── code/               # Feature modules
│   │   │   ├── Utils/          # Shared utilities (domUtils.js, …)
│   │   │   ├── Vue/            # vue-initialisation.js (component registry)
│   │   │   └── Navigation/     # Primary / Secondary / Tertiary
│   │   └── components/         # Vue SFCs
│   ├── Scss/                   # SCSS (ITCSS layers)
│   │   ├── Base/               # Variables, fonts
│   │   ├── Elements/           # Base elements
│   │   ├── Mixins/             # SCSS mixins
│   │   ├── Modules/            # UI components
│   │   ├── Templates/          # Layout helpers
│   │   └── Extensions/         # TYPO3 extension overrides
│   └── Static/                 # Copied as-is (BackendLayouts, Favicons)
├── vite.config.js
├── eslint.config.js
├── stylelint.config.js
└── postcss.config.js
 

Resources Directory

 
Resources/
├── Private/                    # Fluid templates (not web-accessible)
│   ├── Backend/, Language/, Layouts/, Partials/, Templates/
├── Extensions/                 # Extension template overrides
│   ├── fluid_styled_content/, form/, indexed_search/, news/
└── Public/                     # Compiled assets (web-accessible)
    ├── Fonts/, Icons/, Images/, JavaScripts/, StyleSheets/, Favicons/
 

Bundle Budgets

Every npm run build finishes by invoking scripts/check-bundle-size.js. The script reads each compiled JS/CSS file from Resources/Public/, computes gzip (level 9) and brotli (quality 11) sizes, compares them against scripts/bundle-budgets.json, and exits non-zero if any per-file or combined-total budget is exceeded. A WARN (orange light) is emitted when a bundle is within 10% of its budget -- that is the signal to refactor before the next feature pushes us over.

If a bundle grew legitimately (new component, intentional dependency upgrade), update scripts/bundle-budgets.json in the same commit as the size change. Never raise a budget just to silence the gate.

Baseline (2026-06-03)

Captured against Vite 8, Bootstrap 5.3.8, Vue 3.5, Swiper 12.

BundleRawGzipBrotli
bootstrap.js65.9 KiB20.1 KiB17.9 KiB
screen.js33.0 KiB9.6 KiB8.6 KiB
vue.js239.8 KiB72.5 KiB63.8 KiB
navigationPrimary.js1.7 KiB662 B560 B
navigationSecondary.js5.6 KiB1.3 KiB1.2 KiB
navigationTertiary.js4.4 KiB1.3 KiB1.1 KiB
paginationTruncate.js1.7 KiB733 B579 B
theme-init.js222 B183 B129 B
bootstrap.css174.1 KiB24.2 KiB17.5 KiB
screen.css67.4 KiB10.6 KiB9.1 KiB
vue.css24.2 KiB3.9 KiB3.4 KiB
navigationPrimary.css7.8 KiB1.8 KiB1.6 KiB
navigationSecondary.css25.2 KiB3.6 KiB3.2 KiB
navigationTertiary.css17.3 KiB3.2 KiB2.8 KiB
ckeditor.css18.3 KiB2.7 KiB2.3 KiB
print.css1.3 KiB559 B432 B
total--158.7 KiB135.8 KiB

After the manual-chunk + dynamic-import refactor (see "Vendor splitting" below), expect vue.js itself to shrink considerably as the Vue runtime, Swiper, and the three Vue SFCs move into long-lived vendor-* / per-SFC chunks. Re-run npm run check-size once after the next build and lower the affected budgets in scripts/bundle-budgets.json.

Vendor splitting

vite.config.js declares a manualChunks map that pulls these node_modules paths into named vendor chunks:

ChunkContents
vendor-vuevue, @vue/*
vendor-swiperswiper
vendor-bootstrapbootstrap
vendor-popper@popperjs/core
vendor-jarallaxjarallax

Vendor chunks change only when the pinned dependency changes, so they stay in HTTP cache across deployments while our own application code rotates.

Code splitting Vue components

code/Vue/vue-initialisation.js uses dynamic import() per component, so each .vue SFC compiles to its own chunk. A page that mounts only SwiperSlider never downloads TodoList or GallerySwiper. Add new components by extending the componentLoaders map in vue-initialisation.js.

On-demand vendor: Jarallax

code/jarallax.js lazy-loads the Jarallax vendor bundle via dynamic import('jarallax'), gated on a document.querySelectorAll('.grid-parallax') presence check. The .grid-parallax wrapper is only emitted by fluid_styled_content/Layouts/Container.html when an editor toggles the Parallax checkbox (grid_parallax = 1) on a ce_container-style content element.

Effect: pages without a parallax container never request the vendor-jarallax-*.js chunk (~26 KiB raw / ~7 KiB gzip / ~6 KiB brotli). The chunk stays a separate, cacheable asset thanks to the manualChunks map; only the network request changes from eager to deferred.

Visualising the bundle

 
npm run build:analyze
 

Writes Build/reports/bundle-stats.html (treemap, gzip + brotli aware, gitignored). Open it directly in a browser; no server required.

Vite Entry Points

Defined in Build/vite.config.js:

BundlePurpose
bootstrap.jsBootstrap framework initialization
screen.jsMain frontend (sticky header, theme, etc.)
vue.jsVue.js 3 components (includes Swiper integration)
navigationPrimary/Secondary/Tertiary.jsNavigation levels
print.jsPrint-specific styles
backend.jsTYPO3 backend styles
ckeditor.jsCKEditor RTE styles
 

Note: Swiper is integrated into the vue.js bundle -- there is no separate swiper.js entry point.

 

Adding a New Entry

  1. Create JS file in Build/Assets/Scripts/
  2. Register in vite.config.js
  3. Run npm run watch
  4. Include in Fluid:
<f:asset.script identifier="myfeature" src="EXT:mp_core/Resources/Public/JavaScripts/myfeature.js" />
<f:asset.css identifier="myfeature" href="EXT:mp_core/Resources/Public/StyleSheets/myfeature.css" />
 

JavaScript Architecture

Feature Modules (Build/Assets/Scripts/code/)

Core: main.js, i18n.js, i18nLinkHelper.js

UI: jarallax.js, modalGallery.js, openAccordionAndTabs.js, pagination.js, sticky.js, totop.js

Navigation: nav-toggle.js, Navigation/Primary/navigation.js, Navigation/Secondary/navigation.js, Navigation/Tertiary/navigation.js

Layout: moveHeaderDate.js, moveMeta.js, theme.js

Search: searchAutosuggest.js — type-ahead for indexed_search (header and /suche form)

Shared Utilities (code/Utils/domUtils.js)

  • debounce(func, wait) -- Performance-safe resize/scroll handling
  • toggleNavState(...) -- Navigation open/closed state
  • handleDropdownVisibility(element, showCb, hideCb) -- Bootstrap dropdown events
  • toggleAriaLabelAndTitle(element, openLabel, closeLabel) -- Accessible label toggling

Vue.js Components

Located in Build/Assets/Scripts/components/:

ComponentDescription
TodoList.vueInteractive todo with localStorage, registered as CType mpcore_todolist
GallerySwiper.vueSwiper-based gallery carousel for the gallery content element
SwiperSlider.vueGeneric Swiper slider for container slider elements

Component registration is handled in code/Vue/vue-initialisation.js.

Vue mounts on elements with data-container="vue" and data-component="ComponentName" (see VueComponents.typoscript and content element templates). Optional data-* attributes pass props (e.g. data-card-title on TodoList).

Creating a New Component

  1. Create .vue file in Build/Assets/Scripts/components/
  2. Register in code/Vue/vue-initialisation.js
  3. Build and include via <f:asset.script> in Fluid (the vue.js entry point auto-mounts registered components).

SCSS Architecture (ITCSS)

Layers from low to high specificity:

  1. Settings (Base/) -- Variables, fonts, color maps
  2. Tools (Mixins/) -- Functions, mixins (no CSS output)
  3. Generic -- Reset, normalize (from Bootstrap)
  4. Elements (Elements/) -- Base HTML elements
  5. Objects -- Layout patterns
  6. Components (Modules/) -- Styled UI components
  7. Utilities -- Helper classes

Bootstrap Customization

  • Light theme: Build/Assets/Scss/Base/Bootstrap/_custom-variables.scss
  • Dark theme: Build/Assets/Scss/Base/Bootstrap/_custom-variables-dark.scss

Asset Handling

Asset TypePattern
Images in SCSSurl('../../Images/Icons/icon.png') (relative path)
Fonts@include font-face('Name', '../../Fonts/file', 400, normal, woff2)
Inline SVGsvg-load('../Images/Icons/arrow.svg')
Static filesBuild/Assets/Static/ -> copied to Resources/Public/

Template Integration

Fluid

 
<f:asset.css identifier="screen" href="EXT:mp_core/Resources/Public/StyleSheets/screen.css" />
<f:asset.script identifier="screen" src="EXT:mp_core/Resources/Public/JavaScripts/screen.js" />
 

TypoScript

 
page {
  includeCSS.screen = EXT:mp_core/Resources/Public/StyleSheets/screen.css
  includeJSFooter.screen = EXT:mp_core/Resources/Public/JavaScripts/screen.js
}
 

Template path precedence: higher numbers override lower (0 = core, 10 = extension, 20+ = project).

Extension Overrides

ExtensionPathNotes
fluid_styled_contentResources/Extensions/fluid_styled_content/Private/Bootstrap 5 styled
formResources/Extensions/form/Bootstrap forms + YAML config
newsResources/Extensions/news/List, detail, category views
indexed_searchResources/Extensions/indexed_search/Bootstrap search results + autosuggest combobox

mp-core replaces the default indexed_search templates and adds an accessible autosuggest combobox for the header search field and the dedicated search page (/suche).

Site settings

Configure in Site Management → Sites → Settings → Search (or config/sites/<id>/settings.yaml):

SettingDefaultEffect
search.headerSearchtrueShow the header search field (all navigation variants)
search.autosuggesttrueType-ahead suggestions for header and /suche forms

When autosuggest is off, both forms fall back to a plain search field.

Behaviour

  • Suggestions are fetched as JSON from SearchSuggestMiddleware / SearchSuggestService — indexed base words plus matching page titles, scoped to the current site, language, and frontend-user access (no Solr required).
  • The combobox follows the WAI-ARIA listbox pattern with keyboard navigation and polite status announcements.
  • Top results link to the same detail URLs as full search results (including mediathek entries resolved via indexer route arguments).
  • Header search submits via POST to the indexed_search search action route (cHash-safe). On desktop navType 1/2/3 the field appears in the meta-bar flyout or inline in the mobile hamburger menu depending on breakpoint.

Frontend module: Build/Assets/Scripts/code/searchAutosuggest.js (bundled in screen.js).

Best Practices

JavaScript: Modular code in code/, shared patterns in Utils/, debounce resize/scroll, event delegation, ARIA labels, lint before commit.

SCSS: Respect ITCSS layers, CSS variables for theming, logical properties for RTL, max 3 nesting levels, mobile-first min-width queries.

Vue.js: Single File Components, scoped styles, prop validation, Composition API for complex logic.

Troubleshooting

IssueSolution
404 on fontsCheck ../ segments from SCSS to Fonts/
Images missing in CSSVerify path in Assets/Images/
Bundle too largeImport only needed Bootstrap components
Changes not appearingClear browser + TYPO3 caches

Important: Never edit Resources/Public/ directly. Always edit in Build/Assets/ and run npm run build.

Further Reading

Share page