The CoreShop Theme Bundle provides a standardized solution for building and managing themes in Pimcore - even without using CoreShop's eCommerce features. This makes the bundle ideal for a wide range of Pimcore projects, from content-driven websites to complex multi-site installations.
Why Use Different Themes in Pimcore?
In some of our projects, we operate up to 20 or 30 different websites within a single Pimcore instance. This is common when multiple sites or landing pages share the same data model - for example:
The CoreShop Theme Bundle helps ensure a clean and scalable architecture for managing such setups.
We aim for maximum flexibility and reusability. Our base theme contains shared functionality and styling, which we extend in project-specific themes. Here's how you can get started.
Install the bundle using Composer:
composer require coreshop/theme-bundle:^4.0
Then enable it in your Kernel.php:
public function registerBundlesToCollection(BundleCollection $collection)
{
$collection->addBundles([
new \CoreShop\Bundle\ThemeBundle\CoreShopThemeBundle(),
]);
}
CoreShop provides several built-in theme resolvers. For example, you can assign themes via Pimcore document properties:
core_shop_theme:
default_resolvers:
pimcore_document_property: true
Assign the theme using a document property named theme:
Then CoreShop tries to resolve the theme based on the name base.
Internally, CoreShop relies on the Sylius Theme Bundle and uses the filesystem configuration source. By default, it looks for composer.json files in the %kernel.project_dir%/themes directory.
Here's what a typical setup looks like:
Each theme contains all necessary assets - such as Twig templates, JavaScript, CSS, images, and fonts - neatly organized in its own folder.
The Sylius Theme Bundle also supports theme inheritance, allowing one theme to extend another - for instance, using a composer.json file:
{
"name": "cors/portal",
"description": "CORS portal theme",
"authors": [
{
"name": "CORS GmbH",
"email": "office@cors.gmbh",
"homepage": "https://www.cors.gmbh/",
"role": "TEAM"
}
],
"extra": {
"sylius-theme": {
"title": "portal",
"parents": [
"cors/base"
]
}
}
}
This setup allows full reuse of layouts, assets, and logic from the base theme.
We store all theme-based assets in their respective theme folders and configure Webpack Encore builds accordingly.
Each theme has its own build target for CSS and JS files.
webpack_encore:
output_path: '%kernel.project_dir%/public/build/base'
builds:
admin: '%kernel.project_dir%/public/build/admin'
base: '%kernel.project_dir%/public/build/base'
# Example for an additional theme
#portal: '%kernel.project_dir%/public/build/portal'
Symfony Asset Packages
framework:
assets:
json_manifest_path: '%kernel.project_dir%/public/build/base/manifest.json'
packages:
admin:
base_path: '/build/admin/'
json_manifest_path: '%kernel.project_dir%/public/build/admin/manifest.json'
base:
base_path: '/build/base/'
json_manifest_path: '%kernel.project_dir%/public/build/base/manifest.json'
# portal:
# base_path: '/build/portal/'
# json_manifest_path: '%kernel.project_dir%/public/build/portal/manifest.json'
With this configuration, asset paths are resolved dynamically using the appropriate Symfony asset package. For example, using a custom Twig extension:
<link rel="apple-touch-icon" sizes="180x180" href="{{ asset('images/favicons/apple-touch-icon.png','base') }}">
We define separate webpack config files per theme:
const Encore = require('@symfony/webpack-encore');
/**
*
* @param {string} name
* @param {string} outputPath
* @param {string} publicPath
* @param {Array.<{name:string, src:string}>} entries
* @param {Array.<{name:string, src:string}>} styleEntries
* @param {Array.<{from:string, to:string}>} copyFiles
* @param {Array.<Plugin>} plugins
* @param {boolean} defaults
* @param {boolean} mangle
* @returns {webpack.Configuration}
*/
function getConfig({
name,
outputPath,
publicPath,
entries = [],
styleEntries = [],
copyFiles = [],
plugins = [],
defaults = true,
mangle = true
}) {
Encore.reset();
if (!mangle) {
Encore.configureTerserPlugin((options) => {
return {
terserOptions: {
mangle: false,
}
};
})
}
Encore
// directory where all compiled assets will be stored
.setOutputPath(outputPath)
// what's the public path to this directory (relative to your project's document root dir)
.setPublicPath(publicPath)
.cleanupOutputBeforeBuild()
// allow sass/scss files to be processed
.enableSassLoader()
.enablePostCssLoader((options) => {
options.postcssOptions = {
path: 'postcss.config.js'
};
})
.enableSingleRuntimeChunk()
.enableSourceMaps(!Encore.isProduction())
.enableVersioning(Encore.isProduction())
.enableTypeScriptLoader()
;
// ##########################################################################################
// ### default integrations for all configs
// ##########################################################################################
if (defaults) {
// Put your default js includes here, if needed
/*Encore
.addEntry('js/base', './themes/base/assets/js/base.js')
;*/
}
entries.forEach(entry => {
if (entry.name && entry.src) {
Encore.addEntry(entry.name, entry.src);
}
})
styleEntries.forEach(entry => {
if (entry.name && entry.src) {
Encore.addStyleEntry(entry.name, entry.src);
}
})
if (defaults) {
// Put your default copyFiles here, if needed
/*copyFiles = [
...[
{
from: './themes/base/assets/images',
to: 'images/[path][name].[ext]',
}, {
from: './themes/base/assets/fonts',
to: 'fonts/[path][name].[ext]',
}
],
...copyFiles
];*/
}
Encore
.copyFiles(copyFiles)
plugins.forEach(plugin => {
Encore.addPlugin(plugin);
})
const config = Encore.getWebpackConfig();
config.name = name;
return config;
}
// export the final configuration
const admin = getConfig({
name: 'admin',
outputPath: 'public/build/admin',
publicPath: '/build/admin',
defaults: false,
mangle: false,
styleEntries: [
{
name: 'css/admin/edit',
src: './themes/base/admin/assets/css/edit.scss'
}
],
copyFiles: [
{
from: './themes/base/admin/assets/js',
to: 'js/[path][name].[ext]',
},
]
});
const base = getConfig(require('./themes/base/webpack-web.config.js'));
const portal = getConfig(require('./themes/portal/webpack-web.config.js'));
module.exports = [admin, base, portal];
A theme specific webpack config looks like this:
module.exports = {
name: 'base-web',
outputPath: 'public/build/base',
publicPath: '/build/base',
plugins: [
require('./purgecss-web.config.js')
],
entries: [
{
name: 'js/project',
src: './themes/base/assets/js/web.ts'
},
...
],
styleEntries: [
{
name: 'css/project',
src: './themes/base/assets/css/web.scss'
}
],
copyFiles: [
{
from: './themes/base/assets/images',
to: 'images/[path][name].[ext]',
}, {
from: './themes/base/assets/fonts',
to: 'fonts/[path][name].[ext]',
}
],
}
This helps us build each theme independently and manage theme-specific dependencies.
Using the CoreShop Theme Bundle, you can build scalable, modular, and reusable themes for your Pimcore projects- without needing any eCommerce functionality. This is ideal for multisite architectures, landing pages, portals, or content-driven applications.
If you need help setting up your Pimcore theme structure, feel free to reach out.
👉 More details can be found in the official documentation