Jump to content

Front end

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 preprocessor (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 running DDEV, you can also start the build from the website’s root directory: ddev mp-core-build (equivalent to npm run build within libs/mp-core/Build/).

The output is generated in Resources/Public/ (JavaScripts, stylesheets, fonts, icons, images, favicons, backend layouts).

ScriptDescription
buildLint + production build (minified, optimised) + bundle size check
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 for 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

Each npm run build ends with a call to scripts/check-bundle-size.js. The script reads each compiled JS/CSS file from Resources/Public/, calculates the sizes using gzip (level 9) and Brotli (quality 11), compares these with scripts/bundle-budgets.jsonand terminates execution with a non-zero value if a file-specific or combined total budget is exceeded. A WARN message (orange warning) is issued if a bundle is within 10 per cent of its budget – this is the signal to carry out a refactoring before the next function pushes us over the limit.

If a bundle has grown for valid reasons (new component, intended upgrade of a dependency), update scripts/bundle-budgets.json in the same commit as the size change . Never increase a budget simply to bypass the gate.

Baseline (3 June 2026)

Captured with 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

Following the refactoring of ‘manual-chunk’ and ‘dynamic-import’ (see ‘Vendor separation’ below), it is expected that vue.js to shrink considerably, as the Vue runtime environment, Swiper and the three Vue SFCs will be moved to long-lived vendor-* / per-SFC chunks. Run the process npm run check-size once after the next build and reduce the affected budgets in scripts/bundle-budgets.json.

‘Vendor Splitting’

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

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

Provider blocks only change if the specified dependency changes; therefore, they remain in the HTTP cache across deployments, whilst our own application code changes.

Code splitting for Vue components

code/Vue/vue-initialisation.js uses dynamic import() per component, so that each .vue SFC is compiled into its own chunk. A page that only SwiperSlider is never downloaded TodoList or GallerySwiper. Add new components by using the componentLoaders map in vue-initialisation.js.

On-demand provider: Jarallax

code/jarallax.js loads the Jarallax provider bundle on a delayed basis via dynamic import('jarallax'), subject to a document.querySelectorAll('.grid-parallax') presence check. The .grid-parallax Wrapper is only rendered fluid_styled_content/Layouts/Container.html if an editor has the parallax tick box (grid_parallax = 1) for a ce_container-style content element.

Impact: Pages without a parallax container request the vendor-jarallax-*.js chunk (~26 KiB in raw format / ~7 KiB gzip / ~6 KiB brotli). Thanks to the manualChunks map; only the network request changes from ‘eager’ to ‘deferred’.

Visualisation of the bundle

 
npm run build:analyze
 

Writes Build/reports/bundle-stats.html (Treemap, gzip- and Brotli-compatible, git-ignored). Open directly in the browser; no server required.

Vite entry points

Defined in Build/vite.config.js:

BundlePurpose
bootstrap.jsInitialisation of the Bootstrap framework
screen.jsMain front-end (sticky header, theme, etc.)
vue.jsVue.js 3 components (including 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.

 

Add a new entry

  1. Create a JS file in Build/Assets/Scripts/
  2. Register under vite.config.js
  3. Execute npm run watch
  4. Integrate into 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

Function 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 — Auto-complete for indexed_search (header and /suche form)

Shared utility functions (code/Utils/domUtils.js)

  • debounce(func, wait) -- Performance-optimised handling of resizing and scrolling
  • toggleNavState(...) -- open/closed state of the navigation
  • handleDropdownVisibility(element, showCb, hideCb) -- Bootstrap dropdown events
  • toggleAriaLabelAndTitle(element, openLabel, closeLabel) -- Accessible toggling of labels

Vue.js components

Located in Build/Assets/Scripts/components/:

ComponentDescription
TodoList.vueInteractive to-do list using localStorage, registered as a CType mpcore_todolist
GallerySwiper.vueSwiper-based gallery carousel for the gallery content element
SwiperSlider.vueGeneric Swiper slider for container slider elements

The components are registered in code/Vue/vue-initialisation.js.

Vue mounts on elements with data-container="vue" and data-component="ComponentName" (see VueComponents.typoscript and templates for content elements). Optional data-* attributes are passed via props (e.g. data-card-title in ‘TodoList’).

Creating a new component

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

SCSS architecture (ITCSS)

Levels ranging from low to high specificity:

  1. Settings (Base/) – variables, fonts, colour palettes
  2. Tools (Mixins/) – functions, mixins (no CSS output)
  3. General – Reset, Normalise (from Bootstrap)
  4. Elements (Elements/) – Basic HTML elements
  5. Objects -- Layout patterns
  6. Components (Modules/) – Styled UI components
  7. Utility functions -- Utility classes

Bootstrap customisation

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

Asset Management

Asset typeTemplates
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
}
 

Priority of template paths: Higher numbers take precedence over lower ones (0 = core, 10 = extension, 20+ = project).

Overrides caused by extensions

ExtensionPathNotes
fluid_styled_contentResources/Extensions/fluid_styled_content/Private/In Bootstrap 5 style
FormResources/Extensions/form/Bootstrap forms + YAML configuration
PostsResources/Extensions/news/List, detail and category views
Indexed searchResources/Extensions/indexed_search/Bootstrap search results + drop-down menu with autocomplete

mp-core replaces the default ‘indexed_search’ templates and adds an accessible combo box suggestion list for the search field in the header as well as for the dedicated search page (/suche).

Website settings

Configure under ‘Website management’ → ‘Websites’ → ‘Settings’ → ‘Search’ (or config/sites/<id>/settings.yaml):

SettingDefaultEffect
search.headerSearchtrueShow search box in the header (all navigation variants)
search.autosuggesttrueSuggestions whilst typing for header and /suche forms

If the automatic suggestion function is deactivated, a simple search field is displayed in both forms.

Behaviour

  • Suggestions are retrieved as JSON from SearchSuggestMiddleware / SearchSuggestService — indexed base words and matching page titles, limited to the current website, language and front-end user access (no Solr required).
  • The drop-down list follows the WAI-ARIA listbox pattern with keyboard navigation and polite status messages.
  • The top results link to the same detail URLs as the full search results (including media library entries, which are resolved via indexer route arguments).
  • The header search is sent via POST to the “indexed_search” search (cHash-safe). For desktop navigation types 1/2/3, the field appears either in the meta bar flyout or inline in the mobile hamburger menu, depending on the breakpoint.

Front-end module: Build/Assets/Scripts/code/searchAutosuggest.js (included in screen.js).

Best practices

JavaScript: Modular code in code/, common patterns in Utils/, debounce on resize/scroll, event delegation, ARIA labels, lint before committing.

SCSS: Observe ITCSS levels, CSS variables for theming, logical properties for RTL, maximum of 3 nesting levels, mobile-first min-width queries.

Vue.js: Single-file components, scope-specific styles, prop validation, Composition API for complex logic.

Troubleshooting

ProblemSolution
404 error with fontsCheck ../ Segments from SCSS for Fonts/
Missing images in CSSPath in the Assets/Images/
bundle is too largeImport only the required Bootstrap components
Changes are not displayedClear the browser and TYPO3 cache

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

Further information

Share page