Multisite
v7.4+ Single-installation
Intro
Since v7.4+, XalokNext supports a simpler variant of multisite - a single installation can serve different sites. This benefits from a simplified configuration.
Configuration
The single-installation configuration is done through .env
files, to enable loading it from this file:
# app/config/parameters/local.yml
wf_cms_base:
multisite:
env: true
Then add a .env
file in the root directory with a WF_CMS_MULTISITE
variable that contains the json encoded definition of sites:
WF_CMS_MULTISITE='{
"defaultSite": "site1",
"sites": {
"site1":{
"title": "Site number 1",
"base_url": "https://admin.common.tld",
"public_url": "https://site1.tld",
"images_domain": "https://imagenes.site1.tld",
"imghandler_url": "https://imghandler.site1.url",
"environment": {
"S3_BUCKET": "site1-bucket",
"S3_BUCKET_THUMBS": "site1-bucket-thumbs"
}
},
"site2":{
"title": "Site number 2",
"base_url": "https://admin.common.tld",
"public_url": "https://site2.tld",
"images_domain": "https://imagenes.site2.tld",
"imghandler_url": "https://imghandler.site2.url",
"environment": {
"S3_BUCKET": "site2-bucket",
"S3_BUCKET_THUMBS": "site2-bucket-thumbs"
}
},
}
}'
Note: Since this file is not part of the normal configuration files for Symfony, every change to this file must be followed by clearing the Symfony cache. Note: If using a non-default port (e.g.: 8000), this must be part of the URLs configured above (e.g.: "base_url": "http://site1.localhost:8000"
)
defaultSite
: the name (key) of the site to be used when a site cannot be determined, e.g.: when using Symfony commands.sites
: an array with sites definition, indexed by a key.
For each site, its definition can include:
title
: the name of the site that is displayed in the admin site selector (dropdown menu next to the Xalok logo)base_url
: the base admin URL - note that for single-installations there is a unique admin domain for all sitespublic_url
: the base public URLimages_domain
: keeps the_domain
name for historical reasons, but it's actually the base URL for the images - used to prefix the images' URL.imghandler_url
: the base URL for the "imghandler" - the domain under which thumbnails that are not already created are being servedenvironment
: a list of environment variables that are site-specific. Read the Site-specific environment variables section for details.
Loading sites
The sites are also stored in the database, to allow separating the entities (e.g.: categories, articles) by site. After changing the sites in the WF_CMS_MULTISITE
environment variable, one needs to run:
./app/admin/console --env=prod wf:cms:multisite:load
Note: Only the site key is kept in the database, so any other changes don't require running this. Note: Remember to clear Symfony's cache before running this command. Note: This command adds the sites to the database only if they don't exist, so running it multiple times won't have negative consequences
Migrating data
If there's already a database that holds the data for one of the sites and the project needs to migrate to multisite single-installation, XalokNext offers a command to assign existing entities to one of the sites:
./app/admin/console --env=prod wf:cms:multisite:switch https://site1.tld
For performance reasons, this updates the database through SQL queries directly, that is - it doesn't hydrate entities. So one must follow this with a reindex of ElasticSearch.
Site-specific environment variables
Each site can define its own environment variables (see the sites[X].environment
configuration above. Note that in order to avoid duplication, all the main keys are also registered (uppercased) as environment variables, e.g.: BASE_URL
, PUBLIC_URL
, IMAGES_DOMAIN
- without the need to specify them (also) in sites[X].environment
.
To use the values of these variables as parameters:
# app/config/parameters/local.yml
parameters:
images_domain: '%env(IMAGES_DOMAIN)%' # this comes from `sites[X].images_domain
s3_bucket: '%env(S3_BUCKET)%' # this comes from `sites[X].environment.S3_BUCKET
s3_bucket_thumbs: '%env(S3_BUCKET_THUMBS)%'
Configuring the parameters using environment variables means that their value will be determined at request time, depending on which site is being served (public/images URLs) or depending on which site has been chosen to work on in the admin.
Note: The S3 buckets must be configured for each site independent of each-other in order to ease the redirection (imagenes.site1.tld -> imghandler.site1.tld) when a thumbnail wasn't previously created.
Rehubbing
The multisite single-installation was released in v7.4+. All sites running this version must replace their "rehubbing" deploy system with refreshing the config.json
. Details here.
Setting current site in web requests
- in public facing pages, the current site is added based on the host of the request, matched against the available sites. This is done by the
Wf\Bundle\CmsBaseBundle\Multisite\Listener\SiteListener
- in admin facing pages, the current site is added based on the value of the
wf_cms_site
cookie byWf\Bundle\CmsBaseAdminBundle\Listeners\MultisiteListener
Setting current site in commands
When running commands, the defaultSite
is automatically being set by Wf\Bundle\CmsBaseBundle\Multisite\Listener\CommandListener
.
Specify a SITE
environment variable to use a different site:
SITE=non-default-site-key ./app/admin/console some:command
Working with Varnish tags
See this section
Invalidating (purging) a tag across all sites
See this section
CI
Intro
Building the deployment artefact on CI must be run in single-site installation, to avoid further complications.
For this, one needs to setup a CI
fact, used inisde the local.yml.j2
template. Also, having a CI
environment variable in the runner can help with errors that occur during the make wf_assets
phase, as the wf-assets downloader takes this variable into account and dumps the error message if it's set.
CI fact
To be able to use single-site in CI and multi-site in the other environments, one must add a CI
fact to their provisioning repository. For the vagrant-provision
repository this has been already added in the feature/generalization
branch. If using a different branch, you must add this to the build_pre_tasks.yml
:
- name: Set CI variable
set_fact:
CI: true
This must be done anywhere before the task that's generating the local.yml
file based on the local.yml.j2
template.
local.yml.j2
Since the CI will run without access to the .env variables, the local.yml.j2
cannot use them. Use the previously added CI
fact to make the distinction:
{% if CI %}
s3_bucket: {{ parameters.s3_bucket }}
s3_bucket_thumbs: {{ parameters.s3_bucket_thumbs }}
cms_main_images_domain: {{ parameters.cms_main_images_domain }}
wf_cms.admin.publicUrl: https://{{ parameters.cms_main_domain }}
{% else %}
s3_bucket: '%env(S3_BUCKET)%'
s3_bucket_thumbs: '%env(S3_BUCKET_THUMBS)%'
cms_main_images_domain: '%env(IMAGES_DOMAIN)%'
wf_cms.admin.publicUrl: '%env(PUBLIC_URL)%'
{% endif %}
Note: The indentation is important, the Jinja blocks ({% if %}
, etc.), must be at the beginning of the line, while the rest of the lines respect the normal indentation.
The same must be used to disable multisite on the CI environment:
{% if CI == false %}
wf_cms_base:
multisite:
env: true
{% endif %}
CI environment variable
Make sure the project's provisioning repository is setting up the CI
environment variable. Details here. This has the benefit of outputting the error details if there's an error when running make wf_assets
.
Development
Autowiring the sites configuration service
If using autowire, implement the Wf\Bundle\CmsBaseBundle\Multisite\SitesAwareInterface
interface and use the Wf\Bundle\CmsBaseBundle\Multisite\SitesAwareTrait
trait. XalokNext will automatically inject the wf_cms.multisite.configuration
service.
public function someAction()
{
if ($this->isMultisite()) {
$currentSite = $this->sitesConfiguration->getCurrentSite();
}
}
Manually injecting the sites configuration service
If the code is not using autowire (e.g. code inside the group bundle, if any), one can tag their service with the wf_cms.multisite_aware
tag and use the same ``Wf\Bundle\CmsBaseBundle\Multisite\SitesAwareTrait` trait.
<service id="group_bundle.site_aware_service" class="Group\Bundle\CmsBundle\SiteAwareService">
<tag name="wf_cms.multisite_aware" />
</service>
namespaace Group\Bundle\CmsBundle;
class SiteAwareService
{
use Wf\Bundle\CmsBaseBundle\Multisite\SitesAwareTrait;
public function someMethod()
{
if ($this->isMultisite()) {
$currentSite = $this->sitesConfiguration->getCurrentSite();
}
}
}
Making entities site-specific
Use the Wf\Bundle\CmsBaseBundle\Entity\Traits\SiteTrait
in your entity to make that entity site-specific. The trait adds a site_id
and getSite()
/setSite(Site $site)
methods to your entity.
CurrentSiteListener
When using Wf\Bundle\CmsBaseBundle\Entity\Traits\SiteTrait
the current site is automatically injected into your entity when persisting (only, the site shouldn't change after the entity has been created). This is done by the Wf\Bundle\CmsBaseBundle\Multisite\Listener\CurrentSiteListener
.
If one wants to add an entity for a different site:
$entity->setSite($anotherSite);
If one wants to make that specific row "global" (available for all sites):
$entity->setSite(null);
Querying site-specific entities
If your entity has a site
column, Wf\Bundle\CmsBaseBundle\Multisite\Filter\MultisiteFilter
automatically adds a site_id = :current_site_id
where clause to the queries.
Querying global entities
The single example of global entities at the time of this writing are some settings. Some settings are global, non-site-specific - they have a single value for all the sites.
To get a global entity while using findBy
or findOneBy
repository methods, make sure your repository is extending Wf\Bundle\CmsBaseBundle\Entity\Repository\EntityRepository
and pass 'site' => null
in the criteria:
$globalAudio = $this-audioRepository->findOneBy([
'id' => 100,
'site' => null
]);
Adding a unique (slug, site_id) index
If the entity is using the Wf\Bundle\CmsBaseBundle\Entity\Traits\SiteTrait
trait, it's enough to add unique=true
to the slug, Wf\Bundle\CmsBaseBundle\Multisite\MultisiteMetadataSubscriber
is turning this into a (slug, site_id)
index. This was required so that single-site projects can still rely on (slug)
being unique.
Allowing same-slug per site entities
If one adds a custom sluggable entity in their project, one must use Wf\Bundle\CmsBaseBundle\Multisite\MultisiteAwareSlugHandler
slug handler:
/**
* @Gedmo\Slug(
* fields={"title"},
* handlers={
* @Gedmo\SlugHandler(
* class="Wf\Bundle\CmsBaseBundle\Multisite\MultisiteAwareSlugHandler"
* )
* }
* )
* @ORM\Column(name="slug", type="string", length=128, unique=true)
*/
protected $slug;
This makes sure the slug
is unique for each site, instead of globally.
Using site-specific assets (CSS/JS)
One can define site-specific entry points in public_webpack.config.js
by prefixing the entry's name with the site's short name:
const webpackConfig = {
entry: {
// ... other entry points
"home-css": "scss/home.scss", // will be used for all sites that don't have site-specific `home-css`
"site1/home-css": "scss/site1/home.scss", // will be used for `site1` site
},
// ... other public webpack configurations
};
Note: Defining the "global" home-css
entry is optional, don't do it if there aren't multiple sites that share the same resource. It's been added to the example above only to let one know it's possible.
With this configuration, wf_public_webpack_*
twig functions will automatically pick up the site-specific assets, if present.
Multi-installation
Intro
The multi-installation variant supposes that each project will get its own installation (different repositories/projects). This relies on the wf_user
tables being in sync across the different projects. That means that when starting a subsequent project, the wf_user
table must be copied as it is in the first project. Once the multisite multi-installation is setup, changes to the users of one site are synced automatically to other installations.
nginx
nginx
must be configured to switch the document root based on the value of wf_cms_site
cookie:
map $cookie_wf_cms_site $site {
default "site1dir";
site1 site1dir;
site2 site2dir;
}
server {
listen 80;
server_name admin.multisite.tld;
root /var/www/$site/web;
}
In addition to the admin.multisite.tld
, each of the installations must be accessible separately (admin.site1.tld
, admin.site2.tld
), for tools that don't send the cookie, like make wf_assets
.
.make-config
The ADMIN_HOST
is used for make wf_assets
, which doesn't send the wf_cms_site
cookie. Thus, it must be set to the individual CMS installation host (admin.site1.tld
, etc.)
CMS parameters
Sessions
All installations must be configured to read/write sessions in the same directory:
# app/admin/config/config_prod.yml
framework:
session:
save_path: /tmp/wfcms_session
admin_url
The admin_url
parameter is used by the client-side application, so it needs to point to the "joint" domain (https://admin.multisite.tld/
)
wf_cms_admin.multisite
wf_cms_admin.multisite
parameter must be configured on all sites with a list of available sites
# app/config/parameters/local.yml
wf_cms_admin.multisite:
multiinstallation: true
sites:
site1:
base_url: admin.site1.tld
public_url: site1.tld
images_domain: img.site1.tld
images_base_url: /files
site2:
base_url: admin.site2.tld
public_url: site2.tld
images_domain: img.site2.tld
images_base_url: /files
The base_url
is used to display (for example) in site1
, when searching for images from site2
. The user already has the wf_cms_site
set to site1
, so nginx would point to the /uploads
directory of site1
. To solve this, the images have the full path https://site2_base_url/uploads/...
Troubleshooting
Out of sync users table
If a site was added to the multisite config without importing the users table first, the easiest way to sync the users table is to load the fixtures and import the users table from an existing website:
make load_fixtures
Warning: This will delete the entire content from the DB - it should only be ran in DEV or PRE environments.
Note: this command refuses to run if the ADMIN_SF_ENV
variable is set to prod
in .make-config
. Change it to dev
temporarily, before running the command. Don't forget to set it back to prod
once the fixtures are reloaded.
After reloading the fixtures, import a dump of the wf_users
table from the currently running project.