Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 76 additions & 0 deletions docs/getting-started/apache-configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,82 @@ the class that writes it. An `.htaccess` already present in the directory is lef
these headers to it yourself. When `document_cache_dir_from_web_root` moves the document cache,
the file goes in that directory.

## Theme assets

`php Thelia asset-map:compile` writes the theme files to `public/assets/` with a digest in
their names. In production it also writes a `.br`, a `.zst` and a `.gz` copy next to each text
file (see [Precompressed assets](./nginx-configuration.md#precompressed-assets)). From Thelia 3.3,
the `public/.htaccess` of the shop serves the `.br` and `.gz` copies when the browser accepts
them (it needs `mod_rewrite` and `mod_headers`; without `mod_headers` the original file is sent).
On an older `.htaccess`, add these rules to it. If the virtual host does not read `.htaccess`
files (`AllowOverride None`), put them in the `<Directory>` block of `public/` instead: while
the `.htaccess` is read, its rewrite rules replace those of the `<Directory>` block, which are
then ignored.

```apache
<IfModule mod_rewrite.c>
<IfModule mod_headers.c>
RewriteEngine On

# Brotli, if the browser accepts it and the original file exists
RewriteCond %{HTTP:Accept-Encoding} br
RewriteCond %{REQUEST_FILENAME} -f
RewriteCond %{REQUEST_FILENAME}.br -s
RewriteRule ^(assets/.*)\.(css|js|json|svg)$ $1.$2.br [QSA]

# Gzip, if the browser accepts it and the original file exists
RewriteCond %{HTTP:Accept-Encoding} gzip
RewriteCond %{REQUEST_FILENAME} -f
RewriteCond %{REQUEST_FILENAME}.gz -s
RewriteRule ^(assets/.*)\.(css|js|json|svg)$ $1.$2.gz [QSA]

# Right content type, and no second compression by mod_brotli or mod_deflate
RewriteRule \.css\.(br|gz)$ - [T=text/css,E=no-brotli:1,E=no-gzip:1]
RewriteRule \.js\.(br|gz)$ - [T=text/javascript,E=no-brotli:1,E=no-gzip:1]
RewriteRule \.json\.(br|gz)$ - [T=application/json,E=no-brotli:1,E=no-gzip:1]
RewriteRule \.svg\.(br|gz)$ - [T=image/svg+xml,E=no-brotli:1,E=no-gzip:1]

<FilesMatch "\.(css|js|json|svg)\.br$">
Header append Content-Encoding br
Header append Vary Accept-Encoding
</FilesMatch>
<FilesMatch "\.(css|js|json|svg)\.gz$">
Header append Content-Encoding gzip
Header append Vary Accept-Encoding
</FilesMatch>
</IfModule>
</IfModule>
```

The rules follow the precompressed content pattern of the Apache manual
([mod_brotli](https://httpd.apache.org/docs/2.4/mod/mod_brotli.html#precompressed) and
[mod_deflate](https://httpd.apache.org/docs/2.4/mod/mod_deflate.html#precompressed)). Two
details differ:

- The `public/.htaccess` of Thelia already compresses text on the fly with `mod_deflate` and
`mod_brotli`. `E=no-brotli:1,E=no-gzip:1` stops them from compressing a file that is already
compressed.
- `RewriteCond %{REQUEST_FILENAME} -f` requires the original file to exist, so a compressed copy
is never served for a path whose original was removed.

The rules only apply under `assets/`. Thelia's `.htaccess` sends every other request that is not
a file to `index.php`, and it does not touch a file that exists.

## HTTP/2

The Flexy theme serves one JavaScript file per module through an import map: the home page
references about 80 module URLs and 15 `modulepreload` links. Over HTTP/1.1 these requests queue
on six connections per host. A server without HTTP/2 slows this model down noticeably, so enable
it on the HTTPS vhost:

```apache
Protocols h2 http/1.1
```

This needs `mod_http2` (`a2enmod http2` on Debian) and an MPM that supports it: `mpm_event` or
`mpm_worker`, not `mpm_prefork`. With `mpm_prefork`, switch to `mpm_event` and run PHP through
PHP-FPM (`proxy_fcgi`). Browsers only use HTTP/2 over TLS, so the shop needs HTTPS.

## Required writable directories

Apache needs write access to these directories:
Expand Down
7 changes: 5 additions & 2 deletions docs/getting-started/cdn.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ URLs do not change, the back office works as before, and Thelia needs three sett

### 1. Cache headers on the origin

Add to the shop's server block (see the [nginx page](./nginx-configuration.md)):
Add to the shop's server block (see the [nginx page](./nginx-configuration.md), which extends
the `/assets/` location below with `gzip_static` and `brotli_static`: keep a single `/assets/`
location):

```nginx
# Theme files: the name carries a digest of the content.
Expand Down Expand Up @@ -131,7 +133,8 @@ the shop domain.
- A deploy needs no purge: a changed theme file gets a new name. Run
`php Thelia asset-map:compile` on each deploy (`bin/install` does not), again after
`thelia:cache:clear` (it empties `public/assets` unless `--without-assets`), and
`importmap:install` after updating the theme package.
`importmap:install` after updating the theme package. In production, this command also
writes the minified CSS and the precompressed `.br`, `.zst` and `.gz` files.
- Never set `liip_imagine.webp.generate: true`: every image then goes through a
private `302` that no CDN caches. To check, the second load of a product page must show no
`/media/cache/resolve/` in `src` or `srcset` (the zoom link `data-zoom-src` may keep one):
Expand Down
124 changes: 124 additions & 0 deletions docs/getting-started/nginx-configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,130 @@ not read it, so a shop served by nginx needs this block. It is required from The
3.1.2 on: add it to the server block of an existing shop when you update. If
`document_cache_dir_from_web_root` moves the document cache, change the path of the location.

## Theme assets

`php Thelia asset-map:compile` writes the theme files to `public/assets/` with a digest in
their names. In production it also writes a `.br`, a `.zst` and a `.gz` copy next to each text
file (see [Precompressed assets](#precompressed-assets) below). Serve them with a dedicated
location:

```nginx
# Theme files: the name carries a digest of the content.
location ^~ /assets/ {
gzip_static on;
brotli_static on; # needs the ngx_brotli module, remove the line without it
gzip_vary on;

add_header Cache-Control "public, max-age=31536000, immutable" always;
add_header Access-Control-Allow-Origin "*" always;

# The original file must exist: gzip_static and brotli_static do not check it
try_files $uri =404;
}
```

The `^~` prefix makes this location win over the static files location above, so the cache
headers are repeated inside it. This is the same `/assets/` location as in
[Serving files from a CDN](./cdn.md): keep a single one in the server block.

`gzip_static` is part of the standard nginx build on Debian. `brotli_static` comes from the
ngx_brotli module, packaged as `libnginx-mod-http-brotli-static` on Debian. nginx
does not serve `.zst` files natively: those are for servers and proxies that support them.

`gzip_static on` and `brotli_static on` answer with `$uri.gz` or `$uri.br` when the browser
accepts the encoding, and they do so without checking that `$uri` exists. Without
`try_files $uri =404`, a request for a path whose original file was removed would still receive
its stale compressed copy. With it, a missing original is a 404 and no compressed file is ever
served alone. `gzip_vary on` adds `Vary: Accept-Encoding`, so a cache keeps the compressed and
the plain versions apart.

## HTTP/2

The Flexy theme serves one JavaScript file per module through an import map: the home page
references about 80 module URLs and 15 `modulepreload` links. Over HTTP/1.1 these requests queue
on six connections per host. A server without HTTP/2 slows this model down noticeably, so enable
it on the TLS listener of the shop:

```nginx
server {
listen 443 ssl;
http2 on; # nginx 1.25.1 and later
# before 1.25.1: listen 443 ssl http2;
server_name domain.tld;
# ...
}
```

Browsers only use HTTP/2 over TLS, so the shop needs HTTPS. If a CDN or a proxy terminates TLS,
HTTP/2 has to be enabled there.

## Precompressed assets

In production, the Flexy and `default-twig` themes enable `framework.asset_mapper.precompress`
(Symfony 7.3 and later). `php Thelia asset-map:compile` then writes `.br`, `.zst` and `.gz`
copies next to each compiled text asset: CSS, JavaScript, JSON, SVG, TTF, OTF, XML, HTML and
a few other text formats, which is Symfony's default list. WOFF2 fonts and images are already compressed and are
left alone.

The formats are detected on the machine that runs the compile:

| Format | Needs |
| --- | --- |
| Brotli (`.br`) | the `brotli` command or the PHP `brotli` extension |
| Zstandard (`.zst`) | the `zstd` command or the PHP `zstd` extension |
| Gzip (`.gz`) | nothing, zlib is always available |

A missing tool is skipped with a warning and the compile goes on. Install `brotli` (and
optionally `zstd`) on the build machine to get those formats. The same compile rebuilds the
Flexy Tailwind stylesheet minified and the back-office Sass stylesheet compressed. Nothing
changes in development: the AssetMapper dev server serves the sources and never compresses.

To turn precompression off, or to restrict it, set it in a file of your project, for example
`config/packages/asset_mapper.yaml`. Each list replaces the default one, so give it in full:

```yaml
framework:
asset_mapper:
precompress:
enabled: false
```

```yaml
framework:
asset_mapper:
precompress:
formats: [brotli, gzip]
extensions: [css, js, json, svg]
```

### Minifying JavaScript

The default theme does not minify its JavaScript: the first-party code is small, and the vendor
files downloaded from jsDelivr are already minified, so precompression covers most of the gain.
On the demo shop, minifying the JavaScript a page loads saves about 7 KB on the home page
and about 10 KB on a product page once Brotli has compressed it, about 4 KB of
which come from `live_controller.js`.
A project that ships heavier JavaScript can add `sensiolabs/minify-bundle`, a standalone binary
that needs no Node. It hooks into `asset-map:compile` and minifies the assets while they are
copied:

```bash
composer require sensiolabs/minify-bundle
```

Its options live in `config/packages/sensiolabs_minify.yaml`. By default it minifies CSS and
JavaScript, and ignores vendor files (`ignore_vendor: true`). It downloads the binary from
GitHub in debug mode only (`download_binary: '%kernel.debug%'`), so on a build machine, install
`minify` and set `local_binary: 'auto'` so the bundle finds it:

```yaml
sensiolabs_minify:
minify:
local_binary: 'auto'
```

See the [bundle README](https://github.com/SensioLabs/minify-bundle) for the other options.

## Required writable directories

Ensure Nginx (www-data) has write access to:
Expand Down
6 changes: 6 additions & 0 deletions docs/upgrading/update.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,12 @@ The first two rebuild the front-office assets, the last one the back-office styl
any command the console does not carry: each comes from a package the corresponding template
requires.

In production, also run `php Thelia asset-map:compile`. It writes the files served from
`public/assets/`, with the minified CSS and the precompressed `.br`, `.zst` and `.gz` copies.
Install `brotli` (and optionally `zstd`) on the machine that runs it to get those formats, and
see [Nginx configuration](../getting-started/nginx-configuration.md#precompressed-assets) for the
server side.

## 5. Update the modules

Modules keep their own version numbers, so they update separately:
Expand Down
Loading