From d59c0c5cfc2689c4307fa4e0064ef71f7205b8ed Mon Sep 17 00:00:00 2001 From: Romain Lanz Date: Sun, 4 Oct 2026 08:27:16 +0000 Subject: [PATCH 1/3] docs(frontend): document CDN deployment with Inertia SSR Explain how to configure the same asset URL at build time and runtime, and upload client assets without publishing manifests or SSR bundles. Keep the server files local and retain old hashed client assets during rolling deployments. Document the opt-in static file index and its restart requirement. --- content/guides/basics/static_file_server.md | 20 +++++++++- content/guides/frontend/inertia.md | 18 +++++++-- content/guides/frontend/vite.md | 43 +++++++++++++++++++-- 3 files changed, 72 insertions(+), 9 deletions(-) diff --git a/content/guides/basics/static_file_server.md b/content/guides/basics/static_file_server.md index 4c2f92e..10b731e 100644 --- a/content/guides/basics/static_file_server.md +++ b/content/guides/basics/static_file_server.md @@ -118,6 +118,24 @@ When a browser requests a file it has cached, it sends the ETag value. If the fi ::: +:::option{name="indexFiles"} + +When enabled, the middleware scans `public/` once when it is constructed and keeps an index of files and directories in memory. GET and HEAD requests for paths absent from the index continue to your routes without a filesystem lookup. Existing files still use the normal static server, including ETags, HEAD and range responses, custom headers, and dotfile rules. Symlinks and their descendants retain filesystem lookups. + +This option defaults to `false`. Enable it only when public files are fixed for the lifetime of the application process. + +```ts title="config/static.ts" +export default defineConfig({ + indexFiles: true, +}) +``` + +Restart the application after adding files or directories. Leave indexing disabled during development and when serving uploads. Building the index requires a synchronous directory scan and memory proportional to the size of `public/`. + +The index covers all public files, not just entries in the Vite manifest. It reduces filesystem operations for missing paths, but it does not remove file reads when serving an existing asset or guarantee a reduction in physical disk reads when metadata is already cached by the operating system. + +::: + :::option{name="lastModified"} The `lastModified` property enables the [Last-Modified](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Last-Modified) header. The server uses the file's modification time from the file system (the [stat.mtime](https://nodejs.org/api/fs.html#statsmtime) property) as the header value. @@ -317,7 +335,7 @@ The rule for copying public files is defined in the `adonisrc.ts` file: The `pattern` property uses glob syntax to match all files inside the `public` directory. The `reloadServer: false` setting indicates that changes to these files during development don't require restarting the development server. -If you add files to the `public` directory while your development server is running, you don't need to restart. The static middleware will serve them immediately. However, if you modify the `config/static.ts` file, you will need to restart the server for the configuration changes to take effect. +With the default configuration, files added to `public` are served immediately without restarting the server. If you enable `indexFiles`, restart the application after adding files so the index includes them. Changes to `config/static.ts` also require a restart. ## See also diff --git a/content/guides/frontend/inertia.md b/content/guides/frontend/inertia.md index eb89004..99061e4 100644 --- a/content/guides/frontend/inertia.md +++ b/content/guides/frontend/inertia.md @@ -1603,7 +1603,7 @@ See [history encryption](https://inertiajs.com/history-encryption) on the Inerti Server-side rendering (SSR) generates the initial HTML on the server, improving perceived performance and SEO. -The starter kit pre-wires everything SSR needs: the `inertia/ssr.tsx` entrypoint exists, `vite.config.ts` declares it under `serverEntrypoints`, and `config/inertia.ts` points at it. Enabling SSR is a single switch. +The starter kit pre-wires everything SSR needs: the `inertia/ssr.tsx` entrypoint exists, `vite.config.ts` declares it under `serverEntryPoints`, and `config/inertia.ts` points at it. Enabling SSR is a single switch. ```ts title="config/inertia.ts" import { defineConfig } from '@adonisjs/inertia' @@ -1618,22 +1618,32 @@ const inertiaConfig = defineConfig({ export default inertiaConfig ``` -If you are adding SSR to an existing application, add the same entrypoint to `serverEntrypoints` in `vite.config.ts`. +If you are adding SSR to an existing application, add the same entrypoint to `serverEntryPoints` in `vite.config.ts`. ```ts title="vite.config.ts" import { defineConfig } from 'vite' import adonisjs from '@adonisjs/vite/client' +import react from '@vitejs/plugin-react' export default defineConfig({ plugins: [ adonisjs({ - entrypoints: ['inertia/app.tsx'], - serverEntrypoints: ['inertia/ssr.tsx'], // [!code highlight] + entryPoints: ['inertia/app.tsx'], + serverEntryPoints: ['inertia/ssr.tsx'], // [!code highlight] }), + react(), ], }) ``` +### Deploying client assets to a CDN + +SSR does not require the client bundles to exist on the application server. The server renders pages from the local SSR bundle and uses the local manifests to resolve client asset URLs. You can upload the hashed client assets to a CDN and omit them from the running application's filesystem. + +With the default output directories, keep `public/assets/.vite/manifest.json` and the entire `public/assets/server/` directory in the application artifact. The server directory contains the SSR manifest and bundles. Never upload the manifests or anything under `public/assets/server` to the CDN. The [Vite CDN deployment guide](./vite.md#deploying-assets-to-a-cdn) shows how to configure one URL for both builds and runtime, and how to exclude these files without selecting client filenames manually. + +The Inertia SSR renderer loads its local server bundle directly, so it does not need the AdonisJS static middleware to serve `public/assets`. This only applies to Vite assets. Continue to serve other public files, such as `favicon.ico`, uploaded images, or `robots.txt`, with the static middleware, a web server, or separate object storage. + ## Understanding the Inertia request lifecycle Inertia requests follow two flows: the **initial page visit** and **subsequent Inertia visits**. Both flows use the same AdonisJS routes, middleware, controllers, and `inertia.render()` calls. The response format changes based on whether the request includes the `X-Inertia` header. diff --git a/content/guides/frontend/vite.md b/content/guides/frontend/vite.md index cb63847..49284a0 100644 --- a/content/guides/frontend/vite.md +++ b/content/guides/frontend/vite.md @@ -544,7 +544,14 @@ export default defineConfig({ ## Deploying assets to a CDN -To serve bundled assets from a CDN in production, configure the `assetsUrl` option in both configuration files. This ensures that URLs in the manifest file and lazy-loaded chunks point to your CDN server. +To serve bundled assets from a CDN in production, use the same CDN URL at build time and at runtime. The build-time value makes lazy-loaded chunks point to the CDN. The runtime value makes helpers such as `@vite` use the same origin. + +Export the URL before you build the application. Set the same environment variable on the production server. + +```sh title="Terminal" +export CDN_ASSETS_URL=https://cdn.example.com/assets/ +node ace build +``` ```ts title="vite.config.ts" import { defineConfig } from 'vite' @@ -556,23 +563,51 @@ export default defineConfig({ entryPoints: ['resources/js/app.js'], reload: ['resources/views/**/*.edge'], // [!code ++:1] - assetsUrl: 'https://cdn.example.com/', + assetsUrl: process.env.CDN_ASSETS_URL, }), ] }) ``` ```ts title="config/vite.ts" +import env from '#start/env' import { defineConfig } from '@adonisjs/vite' export default defineConfig({ buildDirectory: 'public/assets', // [!code ++:1] - assetsUrl: 'https://cdn.example.com/', + assetsUrl: env.get('CDN_ASSETS_URL'), }) ``` -After building your application with `node ace build`, upload the contents of `public/assets` to your CDN. +Add `CDN_ASSETS_URL` to the environment validation in `start/env.ts`. + +```ts title="start/env.ts" +{ + CDN_ASSETS_URL: Env.schema.string(), +} +``` + +Set `CDN_ASSETS_URL=/assets` in your development environment. Set the CDN URL in both the build environment and the production server environment. The environment schema requires a value, but it cannot check that these two environments use the same URL. + +Upload from the completed deployment artifact, not from the source checkout. With the default standalone build, `node ace build` copies the Vite output to `build/public/assets`. The following command uploads every client asset without selecting filenames by hand. + +```sh title="Terminal" +aws s3 sync build/public/assets s3://my-assets-bucket/assets/ \ + --exclude '.vite/*' \ + --exclude 'server/*' \ + --cache-control 'public,max-age=31536000,immutable' +``` + +For an S3-compatible service, add its `--endpoint-url`. Do not add `--delete`. Old hashed files must remain available for browsers with an open page and for application instances during a rolling deployment. Upload the assets before switching traffic to the new application release. + +The command excludes Vite manifests and server bundles. Keep `.vite/manifest.json`, `server/.vite/manifest.json`, and files such as `server/ssr.mjs` in the application artifact. The AdonisJS process reads these files locally, and they must never be publicly accessible from the CDN. + +If another static file server still serves `public/assets`, exclude `server/**` there too. These files contain server-side code, not browser assets. + +The default Vite output uses content hashes for client assets, so the command gives them an immutable cache policy. Do not apply that policy to any unhashed files you add to the upload source. Configure the CDN or object store to return the correct JavaScript and CSS MIME types and an `Access-Control-Allow-Origin` header for cross-origin module and font requests. Public assets can use `Access-Control-Allow-Origin: *`; allow `GET` and `HEAD` in the object store's CORS configuration. If your Content Security Policy restricts scripts, styles, fonts, or images, add the CDN origin to the relevant directives. + +For an Inertia application with SSR, see [Deploying client assets to a CDN](./inertia.md#deploying-client-assets-to-a-cdn). ## Common issues From 954d80d0fe1c15c0408392868b75beab0a294db2 Mon Sep 17 00:00:00 2001 From: Romain Lanz Date: Sun, 4 Oct 2026 10:32:27 +0200 Subject: [PATCH 2/3] docs(frontend): address CDN deployment review feedback Fix the SSR bundle filename, document the build failure when CDN_ASSETS_URL is missing, and move the upload restrictions into warnings with a solution each. Align the deployment guide and the assetsUrl reference example with the new CDN workflow. Move the indexFiles option to the end of the static options list and fix the entryPoints casing in the API client and Edge reference. --- content/guides/basics/static_file_server.md | 36 ++++++++--------- content/guides/frontend/api_client.md | 2 +- content/guides/frontend/vite.md | 44 ++++++++++++++------- content/reference/edge.md | 2 +- content/start/deployment.md | 2 +- 5 files changed, 51 insertions(+), 35 deletions(-) diff --git a/content/guides/basics/static_file_server.md b/content/guides/basics/static_file_server.md index 10b731e..8e4885d 100644 --- a/content/guides/basics/static_file_server.md +++ b/content/guides/basics/static_file_server.md @@ -118,24 +118,6 @@ When a browser requests a file it has cached, it sends the ETag value. If the fi ::: -:::option{name="indexFiles"} - -When enabled, the middleware scans `public/` once when it is constructed and keeps an index of files and directories in memory. GET and HEAD requests for paths absent from the index continue to your routes without a filesystem lookup. Existing files still use the normal static server, including ETags, HEAD and range responses, custom headers, and dotfile rules. Symlinks and their descendants retain filesystem lookups. - -This option defaults to `false`. Enable it only when public files are fixed for the lifetime of the application process. - -```ts title="config/static.ts" -export default defineConfig({ - indexFiles: true, -}) -``` - -Restart the application after adding files or directories. Leave indexing disabled during development and when serving uploads. Building the index requires a synchronous directory scan and memory proportional to the size of `public/`. - -The index covers all public files, not just entries in the Vite manifest. It reduces filesystem operations for missing paths, but it does not remove file reads when serving an existing asset or guarantee a reduction in physical disk reads when metadata is already cached by the operating system. - -::: - :::option{name="lastModified"} The `lastModified` property enables the [Last-Modified](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Last-Modified) header. The server uses the file's modification time from the file system (the [stat.mtime](https://nodejs.org/api/fs.html#statsmtime) property) as the header value. @@ -255,6 +237,24 @@ The function should return an object where keys are header names and values are ::: +:::option{name="indexFiles"} + +When enabled, the middleware scans `public/` once when it is constructed and keeps an index of files and directories in memory. GET and HEAD requests for paths absent from the index continue to your routes without a filesystem lookup. Existing files still use the normal static server, including ETags, HEAD and range responses, custom headers, and dotfile rules. Symlinks and their descendants retain filesystem lookups. + +This option defaults to `false`. Enable it only when public files are fixed for the lifetime of the application process. + +```ts title="config/static.ts" +export default defineConfig({ + indexFiles: true, +}) +``` + +Restart the application after adding files or directories. Leave indexing disabled during development and when serving uploads. Building the index requires a synchronous directory scan and memory proportional to the size of `public/`. + +The index covers all public files, not just entries in the Vite manifest. It reduces filesystem operations for missing paths, but it does not remove file reads when serving an existing asset or guarantee a reduction in physical disk reads when metadata is already cached by the operating system. + +::: + :::: ## Serving static files diff --git a/content/guides/frontend/api_client.md b/content/guides/frontend/api_client.md index 1897012..fd5ac2f 100644 --- a/content/guides/frontend/api_client.md +++ b/content/guides/frontend/api_client.md @@ -99,7 +99,7 @@ export default defineConfig({ plugins: [ react(), inertia({ ssr: { enabled: false, entrypoint: 'inertia/ssr.tsx' } }), - adonisjs({ entrypoints: ['inertia/app.tsx'], reload: ['resources/views/**/*.edge'] }), + adonisjs({ entryPoints: ['inertia/app.tsx'], reload: ['resources/views/**/*.edge'] }), ], resolve: { diff --git a/content/guides/frontend/vite.md b/content/guides/frontend/vite.md index 49284a0..8f51b81 100644 --- a/content/guides/frontend/vite.md +++ b/content/guides/frontend/vite.md @@ -140,7 +140,7 @@ URL prefix for generating asset links in production. Set this to your CDN URL wh ```ts { - assetsUrl: 'https://cdn.example.com/' + assetsUrl: 'https://cdn.example.com/assets/' } ``` :::: @@ -546,12 +546,7 @@ export default defineConfig({ To serve bundled assets from a CDN in production, use the same CDN URL at build time and at runtime. The build-time value makes lazy-loaded chunks point to the CDN. The runtime value makes helpers such as `@vite` use the same origin. -Export the URL before you build the application. Set the same environment variable on the production server. - -```sh title="Terminal" -export CDN_ASSETS_URL=https://cdn.example.com/assets/ -node ace build -``` +Read the URL from a `CDN_ASSETS_URL` environment variable in both configuration files, so that a single value drives the build and the running application. ```ts title="vite.config.ts" import { defineConfig } from 'vite' @@ -580,7 +575,7 @@ export default defineConfig({ }) ``` -Add `CDN_ASSETS_URL` to the environment validation in `start/env.ts`. +Add `CDN_ASSETS_URL` to the environment validation in `start/env.ts`. The variable is required, so you must also set `CDN_ASSETS_URL=/assets` in your `.env`, `.env.test`, and `.env.example` files. ```ts title="start/env.ts" { @@ -588,9 +583,22 @@ Add `CDN_ASSETS_URL` to the environment validation in `start/env.ts`. } ``` -Set `CDN_ASSETS_URL=/assets` in your development environment. Set the CDN URL in both the build environment and the production server environment. The environment schema requires a value, but it cannot check that these two environments use the same URL. +Export the CDN URL before you build the application, and set the same value on the production server. The environment schema requires a value, but it cannot check that the build environment and the production server use the same URL. -Upload from the completed deployment artifact, not from the source checkout. With the default standalone build, `node ace build` copies the Vite output to `build/public/assets`. The following command uploads every client asset without selecting filenames by hand. +```sh title="Terminal" +export CDN_ASSETS_URL=https://cdn.example.com/assets/ +node ace build +``` + +:::warning +`CDN_ASSETS_URL` must be defined in every environment that runs `node ace build`, including your CI pipeline. When the variable is missing, the plugin receives `undefined` instead of falling back to `/assets`, and the build fails with a `TypeError`. + +Export `CDN_ASSETS_URL=/assets` for builds that do not use a CDN. +::: + +Upload from the completed deployment artifact, not from the source checkout. With the default standalone build, `node ace build` copies the Vite output to `build/public/assets`. + +The following command uploads every client asset without selecting filenames by hand. It excludes the Vite manifests and the server bundles, because the AdonisJS process reads `.vite/manifest.json`, `server/.vite/manifest.json`, and files such as `server/ssr.js` from the local filesystem and browsers never request them. The default Vite output uses content hashes for client assets, so the command also gives them an immutable cache policy. For an S3-compatible service, add its `--endpoint-url`. ```sh title="Terminal" aws s3 sync build/public/assets s3://my-assets-bucket/assets/ \ @@ -599,13 +607,21 @@ aws s3 sync build/public/assets s3://my-assets-bucket/assets/ \ --cache-control 'public,max-age=31536000,immutable' ``` -For an S3-compatible service, add its `--endpoint-url`. Do not add `--delete`. Old hashed files must remain available for browsers with an open page and for application instances during a rolling deployment. Upload the assets before switching traffic to the new application release. +Upload the assets before switching traffic to the new application release, and do not apply the immutable cache policy to any unhashed files you add to the upload source. + +:::warning +Do not add `--delete` to the command. Old hashed files must remain available for browsers with an open page and for application instances that still run the previous release during a rolling deployment. Deleting them breaks lazy-loaded chunks for those users. -The command excludes Vite manifests and server bundles. Keep `.vite/manifest.json`, `server/.vite/manifest.json`, and files such as `server/ssr.mjs` in the application artifact. The AdonisJS process reads these files locally, and they must never be publicly accessible from the CDN. +Remove old assets with a separate cleanup job or an object lifecycle rule that only targets files older than your longest deployment window. +::: -If another static file server still serves `public/assets`, exclude `server/**` there too. These files contain server-side code, not browser assets. +:::warning +Do not upload the manifests or anything under `server/` to the CDN. The server bundles contain server-side code, not browser assets, and publishing them exposes that code for no benefit. + +The static middleware and a reverse proxy that serves the `public` directory also expose everything under `public/assets`. If your server entry points contain code you want to keep private, block the `/assets/server/` path in your reverse proxy. +::: -The default Vite output uses content hashes for client assets, so the command gives them an immutable cache policy. Do not apply that policy to any unhashed files you add to the upload source. Configure the CDN or object store to return the correct JavaScript and CSS MIME types and an `Access-Control-Allow-Origin` header for cross-origin module and font requests. Public assets can use `Access-Control-Allow-Origin: *`; allow `GET` and `HEAD` in the object store's CORS configuration. If your Content Security Policy restricts scripts, styles, fonts, or images, add the CDN origin to the relevant directives. +Configure the CDN or object store to return the correct JavaScript and CSS MIME types and an `Access-Control-Allow-Origin` header for cross-origin module and font requests. Public assets can use `Access-Control-Allow-Origin: *`, and you must allow `GET` and `HEAD` in the object store's CORS configuration. If your Content Security Policy restricts scripts, styles, fonts, or images, add the CDN origin to the relevant directives. For an Inertia application with SSR, see [Deploying client assets to a CDN](./inertia.md#deploying-client-assets-to-a-cdn). diff --git a/content/reference/edge.md b/content/reference/edge.md index 24e1aed..5c3f49f 100644 --- a/content/reference/edge.md +++ b/content/reference/edge.md @@ -247,7 +247,7 @@ export default defineConfig({ plugins: [ adonisjs({ // highlight-start - entrypoints: ['resources/js/app.js'], + entryPoints: ['resources/js/app.js'], // highlight-end }), ] diff --git a/content/start/deployment.md b/content/start/deployment.md index 6440c8a..82a1558 100644 --- a/content/start/deployment.md +++ b/content/start/deployment.md @@ -162,7 +162,7 @@ For the best performance, upload your compiled assets to a CDN. This requires up } ``` -After each build, deploy the contents of `build/public` to your CDN. Your CI/CD pipeline can automate this step. +After each build, upload the client assets from `build/public/assets` to your CDN, and keep the Vite manifests and server bundles on the application server. The [Vite CDN deployment guide](../guides/frontend/vite.md#deploying-assets-to-a-cdn) covers the build-time configuration and an upload command your CI/CD pipeline can run. :::tip You can combine both approaches. Use a reverse proxy to serve files like `favicon.ico` and `robots.txt` from the `public` directory, and a CDN for Vite-compiled assets (JavaScript, CSS, images) that benefit from global distribution. From d592c149b7cb7338cfb6e11697f3ab4ca6bb9eb2 Mon Sep 17 00:00:00 2001 From: Romain Lanz Date: Mon, 12 Oct 2026 13:20:49 +0200 Subject: [PATCH 3/3] docs(static): document prefixes instead of indexFiles adonisjs/static#11 replaced the public files index with a list of URL path prefixes. Document the new option, and state that a public file with no matching prefix is not served. --- content/guides/basics/static_file_server.md | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/content/guides/basics/static_file_server.md b/content/guides/basics/static_file_server.md index 8e4885d..e7bd9de 100644 --- a/content/guides/basics/static_file_server.md +++ b/content/guides/basics/static_file_server.md @@ -237,21 +237,25 @@ The function should return an object where keys are header names and values are ::: -:::option{name="indexFiles"} +:::option{name="prefixes"} -When enabled, the middleware scans `public/` once when it is constructed and keeps an index of files and directories in memory. GET and HEAD requests for paths absent from the index continue to your routes without a filesystem lookup. Existing files still use the normal static server, including ETags, HEAD and range responses, custom headers, and dotfile rules. Symlinks and their descendants retain filesystem lookups. - -This option defaults to `false`. Enable it only when public files are fixed for the lifetime of the application process. +The `prefixes` property limits static file serving to a list of URL paths. By default, the middleware looks for a file in `public/` on every request, including the requests your routes handle. When you define `prefixes`, a request with no matching prefix continues to your routes without a filesystem lookup. ```ts title="config/static.ts" export default defineConfig({ - indexFiles: true, + prefixes: ['/assets', '/favicon.ico', '/robots.txt'], }) ``` -Restart the application after adding files or directories. Leave indexing disabled during development and when serving uploads. Building the index requires a synchronous directory scan and memory proportional to the size of `public/`. +A request matches when its path equals a prefix or is nested under it. In the example above, `/assets/app.js` matches `/assets`, and `/assets-old/app.js` does not. The prefixes do not change the file path, so `/assets/app.js` still serves `public/assets/app.js`. + +The property is not defined by default, and every request is looked up in the `public` directory. -The index covers all public files, not just entries in the Vite manifest. It reduces filesystem operations for missing paths, but it does not remove file reads when serving an existing asset or guarantee a reduction in physical disk reads when metadata is already cached by the operating system. +:::warning +A file in `public/` with no matching prefix is not served, even when it exists. Add an entry for each file or directory you keep outside the listed prefixes, such as `/uploads`. + +The middleware compares the prefixes as-is with the request URL. It does not decode the path or ignore the case, so `/Assets/app.js` and `/%61ssets/app.js` do not match `/assets`. +::: ::: @@ -335,7 +339,7 @@ The rule for copying public files is defined in the `adonisrc.ts` file: The `pattern` property uses glob syntax to match all files inside the `public` directory. The `reloadServer: false` setting indicates that changes to these files during development don't require restarting the development server. -With the default configuration, files added to `public` are served immediately without restarting the server. If you enable `indexFiles`, restart the application after adding files so the index includes them. Changes to `config/static.ts` also require a restart. +If you add files to the `public` directory while your development server is running, you don't need to restart. The static middleware will serve them immediately, provided their URL matches the `prefixes` option when you have defined it. However, if you modify the `config/static.ts` file, you will need to restart the server for the configuration changes to take effect. ## See also