Assets in Production

Compiling assets for a release, why the filenames carry a content hash, and the one setting that moves them onto a CDN.

Syntaxphp bin/console asset-map:compile

Browsers need your CSS, JavaScript and images as files behind a URL. In development Symfony can serve them straight out of the assets/ directory as you edit. For a release that indirection is removed: the files are written once, each with a hash of its contents in the filename, into public/assets/.

Two build paths

AssetMapper is part of the framework in Symfony 8.1. It maps each file under assets/ to a logical path, and php bin/console asset-map:compile writes every mapped file into public/assets/ along with manifest.json and importmap.json. Webpack Encore is the other path: a separate package where npm run build produces public/build/ and its own manifest.json, which you point the framework at with framework.assets.json_manifest_path. Choose one; they are not meant to be combined.

Why the hash matters

A compiled name such as /assets/images/duck-3c16d92m.png changes when the file's contents change. That lets you serve assets with a long cache lifetime and still ship a new version, because the new version has a different URL. There is nothing to purge.

CDN

A content delivery network serves those files from hosts near the user. Set framework.assets.base_urls and every asset() call is prefixed with that host. Publish the compiled directory to the CDN's origin before the new application code starts answering requests, otherwise a visitor can receive HTML that references a file nobody is serving yet.

Example

Example · php
// importmap.php
declare(strict_types=1);

// AssetMapper reads this file at the project root. Entries are written
// by "php bin/console importmap:require <package>", which pins the
// version rather than resolving it at build time.
return [
    // 'entrypoint' => true means this file is loaded by the browser
    // directly, and {{ importmap('app') }} in a template renders the
    // tags for it plus everything it imports.
    'app' => [
        'path' => './assets/app.js',
        'entrypoint' => true,
    ],

    // A third-party package. 'path' is absent, so the file is vendored
    // into assets/vendor/ by importmap:install.
    '@hotwired/stimulus' => [
        'version' => '3.2.2',
    ],
];

When to use it

  • Add an asset build step to a release pipeline so no request ever compiles anything.
  • Move images and JavaScript onto a CDN without changing a single template.
  • Work out why assets 404 in production but load fine on a development machine.
  • Set long cache lifetimes on static files without having to purge anything on deploy.

More examples

Compiling and what lands on disk

Run this once per release, in the same build that installs dependencies. The two JSON files are what let the framework render asset URLs without scanning the filesystem on each request. If this step is skipped, the application starts normally and then returns 404 for every asset, because the route that serves assets dynamically exists only in dev.

Example · bash
# build step of the release, with the target environment selected
APP_ENV=prod php bin/console asset-map:compile

# the compiled tree, hashes shortened here for readability:
#   public/assets/app-4a3f9c2e.js
#   public/assets/styles/app-7b10d4aa.css
#   public/assets/images/duck-3c16d92m.png
#   public/assets/manifest.json
#   public/assets/importmap.json

Serving the compiled files from a CDN

With this set, asset('images/duck.png') produces https://cdn.example.com/assets/images/duck-3c16d92m.png. Leave framework.assets.version unset: AssetMapper already puts a content hash in the filename, and version adds a query string that is a weaker form of the same idea. Give more than one base URL and the framework spreads requests across them, which only matters for browsers with low per-host connection limits.

Example · yaml
# config/packages/framework.yaml
framework:
    assets:
        base_urls:
            - 'https://cdn.example.com/'

Pointing the framework at an Encore build

This is the Encore side of the choice. npm run build writes hashed files into public/build/ and records the mapping in manifest.json; asset() then looks each logical name up in that file. strict_mode matters because the default behaviour for a name that is missing from the manifest is to return the original path, which produces a 404 that is easy to miss. Setting it to %kernel.debug% makes the mistake loud in development and harmless in production.

Example · yaml
# config/packages/framework.yaml
framework:
    assets:
        json_manifest_path: '%kernel.project_dir%/public/build/manifest.json'
        # throw instead of silently returning the unhashed path
        strict_mode: '%kernel.debug%'

Discussion

  • Be the first to comment on this lesson.