BETA

Example Deployment with Vercel

This deployment example refers to the Vercel serverless deployment, formerly ZEIT Now.

Prerequisites

Before you get started, you need to have:

We're going to deploy the application under the domain mc-examples-starter.vercel.app.

The mc-examples-starter.vercel.app domain is already in use. Pick a different one if you choose to deploy using Vercel.

Configuration

To start, we need to create an env.prod.json file with the following JSON:

{
"applicationName": "mc-examples-starter",
"frontendHost": "mc-examples-starter.vercel.app",
"mcApiUrl": "https://mc-api.europe-west1.gcp.commercetools.com",
"location": "gcp-eu",
"env": "production",
"cdnUrl": "http://mc-examples-starter.vercel.app",
"servedByProxy": true
}

We also need a headers.prod.json to configure the Content Security Policy to allow the required hostnames:

{
"csp": {
"script-src": ["mc-examples-starter.vercel.app"],
"connect-src": [
"mc-examples-starter.vercel.app",
"mc-api.europe-west1.gcp.commercetools.com",
"mc-api.commercetools.com"
],
"style-src": ["mc-examples-starter.vercel.app"]
}
}

To configure Vercel (formerly Zeit Now) deployments, we can use a vercel.json file. It looks something like this:

{
"version": 2,
"public": true,
"name": "mc-examples-starter.vercel.app",
"alias": "mc-examples-starter.vercel.app",
"regions": ["bru"],
"builds": [
{
"src": "public/**",
"use": "@now/static"
}
],
"routes": [
{
"src": "/(.*).(js.map|js|css|txt|html|png)",
"dest": "/public/$1.$2",
"headers": {
"cache-control": "s-maxage=31536000,immutable"
}
},
{
"src": "/(.*)",
"dest": "/public/index.html",
"headers": {
"Cache-Control": "no-cache"
}
}
]
}

Some fields may vary based on your setup and requirements, for example public, regions, etc.

However, that won't work just yet as the Custom Application does not have an index.html after building the production bundles. To make it work, we need to compile the application first.

Compile the application

The Merchant Center Custom Applications are available by default with a built-in HTTP server, which takes care of preparing the index.html according to the env.json and headers.json configuration (see Runtime configuration).

To be able to deploy the Custom Application to the version 2 of the Vercel platform, the application needs to be configured and built statically. This is possible using the compile-html command.

mc-scripts compile-html

The command requires to provide the runtime configuration files so that the index.html can be properly compiled.

mc-scripts compile-html \
--headers=$(pwd)/headers.prod.json \
--config=$(pwd)/env.prod.json \
--use-local-assets

The --use-local-assets option is required for the sake of this example. See Serving static assets.

The command above does what we need: it compiles the index.html using the JavaScript bundle references (after running mc-scripts build) and the runtime configuration. At this point the index.html file is ready for production usage.

However, the Custom Application needs to instruct the User-Agent (the browser) to enforce certain security measures, using HTTP headers. The HTTP headers are also compiled together with the index.html, as they rely on the runtime configuration headers.json.

Because of that, the now.json file cannot be defined statically. Instead, it needs to be generated programmatically when the Custom Application is built and compiled. To achieve that, we need to implement a transformer function.

Generate now.json using a transformer function

The compile-html command accepts an option transformer which we can use to pass the filesystem path to our transformer function.

We assume that the transformer function is defined at the following location: ./config/transformer-now.js.

mc-scripts compile-html \
--headers=$(pwd)/headers.prod.json \
--config=$(pwd)/env.prod.json \
--use-local-assets \
--transformer $(pwd)/config/transformer-now.js

The purpose of the transformer function is to generate the final now.json given the compiled values passed to the function.

// Function signature using TypeScript
type TransformerFunctionOptions = {
// The content of the `env.json` file.
env: Json;
// The compiled HTTP headers, including CSP (see `loadHeaders` from `@commercetools-frontend/mc-html-template`).
headers: Json;
// The final HTML content of the `index.html`.
indexHtmlContent: string;
}
type TransformerFunction = (options: TransformerFunctionOptions) => void;

The main export of the file should be the transformer function.

transformer-now.jsJavaScript
module.exports = function transformer(options) {
// ...
}

With that in mind, we can implement the transformer function and write the now.json config into the filesystem.

./config/transformer-now.jsJavaScript
const fs = require('fs');
const path = require('path');
const rootPath = path.join(__dirname, '..');
module.exports = function transformer({ headers }) {
const config = {
version: 2,
public: true,
name: 'mc-examples-starter.vercel.app',
alias: 'mc-examples-starter.vercel.app',
regions: ['bru'],
builds: [
{ src: 'public/**', use: '@now/static' },
],
routes: [
{
src: '/(.*).(js.map|js|css|txt|html|png)',
dest: '/public/$1.$2',
headers: { 'Cache-Control': 's-maxage=31536000,immutable' },
},
{
src: '/(.*)',
dest: '/public/index.html',
headers: { 'Cache-Control': 'no-cache', ...headers },
},
],
};
fs.writeFileSync(
path.join(rootPath, 'now.json'),
JSON.stringify(config, null, 2),
{ encoding: 'utf8' }
);
};

Adding fallback routes

This step is optional and does not prevent the application to be used within the Merchant Center. However, it's recommended to do so to avoid unexpected behaviors in case the URL, where the Custom Application is hosted, is accessed directly.

Accessing the Custom Application directly at https://mc-examples-starter.vercel.app won't work, as the application requires the user to log in and thus tries to redirect to the /login route at the same domain.

To prevent that, we can add a dummy fallback route for the login|logout routes. This is only meant to inform the user that the Custom Application cannot be used standalone.

./config/fallback-route.jsJavaScript
module.exports = function fallbackRoute(request, response) {
response.end(
'This is not a real route. If you are seeing this, you most likely are accessing the custom application\n' +
'directly from the hosted domain. Instead, you need to access the custom application from within the Merchant Center\n' +
'domain, as custom applications are served behind a proxy router.\n' +
'To do so, you need to first register the custom application in Merchant Center > Settings > Custom Applications.'
);
};

This route will be used as a serverless function:

./config/transformer-now.jsJavaScript
const fs = require('fs');
const path = require('path');
const rootPath = path.join(__dirname, '..');
module.exports = function transformer({ headers }) {
const config = {
version: 2,
public: true,
name: 'mc-examples-starter.vercel.app',
alias: 'mc-examples-starter.vercel.app',
regions: ['bru'],
builds: [
{ src: 'public/**', use: '@now/static' },
{ src: 'config/fallback-route.js', use: '@now/node' },
],
routes: [
{
src: '/(.*).(js.map|js|css|txt|html|png)',
dest: '/public/$1.$2',
headers: { 'Cache-Control': 's-maxage=31536000,immutable' },
},
{ src: '/(login|logout)', dest: '/config/fallback-route.js' },
{
src: '/(.*)',
dest: '/public/index.html',
headers: { 'Cache-Control': 'no-cache', ...headers },
},
],
};
fs.writeFileSync(
path.join(rootPath, 'now.json'),
JSON.stringify(config, null, 2),
{ encoding: 'utf8' }
);
};

Deployment

Finally, we can trigger the deployment using the Vercel CLI:

yarn build
mc-scripts compile-html \
--headers=$(pwd)/headers.prod.json \
--config=$(pwd)/env.prod.json \
--use-local-assets \
--transformer $(pwd)/config/transformer-now.js
now

Now you're ready to Register your Custom Application and start using it!