Skip to content

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:

yaml
# 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:

shell
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 sites
  • public_url: the base public URL
  • images_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 served
  • environment: 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:

shell
./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:

shell
./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:

yaml
# 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 by Wf\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:

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

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

yaml
{% 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:

yaml
{% 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.

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

xml
        <service id="group_bundle.site_aware_service" class="Group\Bundle\CmsBundle\SiteAwareService">
            <tag name="wf_cms.multisite_aware" />
        </service>
php
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:

php
$entity->setSite($anotherSite);

If one wants to make that specific row "global" (available for all sites):

php
$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:

php
$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:

php
    /**
     * @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:

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

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

yml
# 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

yaml
# 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.