# SXO routes.json Manifest Structure and Asset Injection: A Complete Technical Guide

> Understand SXO routes.json manifest structure and asset injection. Learn how URL paths map to modules and assets, with automatic CSS and JS injection in production.

- Repository: [Víctor García/sxo](https://github.com/gc-victor/sxo)
- Tags: deep-dive
- Published: 2026-03-02

---

**The SXO framework generates a [`routes.json`](https://github.com/gc-victor/sxo/blob/main/routes.json) manifest during the build process to map URL paths to their server-side modules and client assets, with the production runtime injecting CSS `<link>` tags before `</head>` and JavaScript `<script type="module">` tags before `</body>` via the `injectAssets` utility.**

The SXO framework (gc-victor/sxo) relies on a build-time manifest to bridge the gap between esbuild's asset bundling and the server's HTML rendering pipeline. Located at [`dist/server/routes.json`](https://github.com/gc-victor/sxo/blob/main/dist/server/routes.json), this manifest describes every page route's server module, entry points, and associated static assets, enabling precise **asset injection** during both production SSR and development hot-reloading.

## Understanding the routes.json Manifest Structure

Each element in the [`routes.json`](https://github.com/gc-victor/sxo/blob/main/routes.json) array describes one page route and contains everything the runtime needs to render it and load its associated client-side resources.

### Core Manifest Properties

Every route entry includes the following fields:

- **`filename`** – The path to the server-side JSX module that implements the page.
- **`entryPoints`** – An array of client-side entry files (e.g., [`client/index.js`](https://github.com/gc-victor/sxo/blob/main/client/index.js)) that belong to the route.
- **`jsx`** – The relative path that the SSR bundle imports to obtain the page's HTML output.
- **`hash`** – A boolean indicating cache-busting behavior; `true` in development and `false` in production.
- **`assets`** – An object containing **`css`** and **`js`** arrays with client-relative paths of the built files belonging to the route.
- **`path`** (optional) – The URL path derived from the file system that maps to this route.
- **`generated`** (optional) – Set to `true` by `sxo generate` for pre-rendered routes, causing the production server to serve static HTML instead of performing SSR.

## Build-Time Asset Discovery via esbuild

The manifest generation involves two stages: initial route mapping and asset augmentation. While the initial structure defines the route paths and modules, the **asset lists** are populated by analyzing esbuild's build output.

### The esbuild Metafile Plugin

The **esbuild metafile plugin** ([`src/js/esbuild/esbuild-metafile.plugin.js`](https://github.com/gc-victor/sxo/blob/main/src/js/esbuild/esbuild-metafile.plugin.js)) augments [`routes.json`](https://github.com/gc-victor/sxo/blob/main/routes.json) during the client build. This plugin extracts the list of assets that each route produced from esbuild's metafile and writes them into the `assets` field of the corresponding entry without modifying the HTML output. The test suite in [`src/js/esbuild/esbuild-metafile.plugin.test.js`](https://github.com/gc-victor/sxo/blob/main/src/js/esbuild/esbuild-metafile.plugin.test.js) verifies this behavior, confirming that the plugin correctly maps output files to their respective routes.

## Runtime Asset Injection Process

When the production server handles a request, it loads the manifest and conditionally injects assets into the rendered HTML based on the route configuration.

### Loading the Manifest

The server loads the manifest via `loadRoutesManifest`, implemented in [`src/js/server/shared/routes-loader.js`](https://github.com/gc-victor/sxo/blob/main/src/js/server/shared/routes-loader.js). This utility includes retry logic for file system operations and validates the manifest structure before returning the route array to the request handler.

### The Injection Flow in Production

In [`src/js/server/prod/core-handler.js`](https://github.com/gc-victor/sxo/blob/main/src/js/server/prod/core-handler.js), the request handler executes the following sequence for **non-generated** routes:

1. Renders the page using the SSR module (`module.default || module.jsx`).
2. Checks `route.assets` and verifies the response is HTML using the `isHtml` utility.
3. Calls `injectAssets(page, route.assets, normalizedPublicPath)` to modify the HTML string before sending the response.

The `publicPath` value originates from the `PUBLIC_PATH` configuration and is normalized by `normalizePublicPath` (exported from [`src/js/server/utils/inject-assets.js`](https://github.com/gc-victor/sxo/blob/main/src/js/server/utils/inject-assets.js)) to ensure consistent path formatting.

### The injectAssets Utility

Located in [`src/js/server/utils/inject-assets.js`](https://github.com/gc-victor/sxo/blob/main/src/js/server/utils/inject-assets.js), the `injectAssets` function performs the actual DOM manipulation on the HTML string:

- Inserts `<link rel="stylesheet" href="{publicPath}{cssFile}">` before the closing `</head>` tag.
- Inserts `<script type="module" src="{publicPath}{jsFile}"></script>` before the closing `</body>` tag.

Unit tests in [`src/js/server/utils/tests/inject-assets.runtime.test.js`](https://github.com/gc-victor/sxo/blob/main/src/js/server/utils/tests/inject-assets.runtime.test.js) verify that CSS links appear before `</head>` and module scripts appear before `</body>` when given a specific `PUBLIC_PATH` configuration.

## Hot-Reload Asset Handling in Development

During development, SXO uses **Server-Sent Events (SSE)** to push updates to the browser without full page reloads. The development server sends a payload containing the new HTML body, asset lists, and public path.

### Client-Side Asset Reconstruction

The client script ([`src/js/server/hot-replace.client.js`](https://github.com/gc-victor/sxo/blob/main/src/js/server/hot-replace.client.js)) consumes this payload and reconstructs the asset tags dynamically. The payload structure `{ body, assets, publicPath }` mirrors the production manifest's asset structure, allowing the client to create new `<link>` and `<script>` tags with the correct public path prefix. The logic follows the same injection pattern as the server-side utility, ensuring consistency between development and production asset loading.

## Summary

- The **manifest** at [`dist/server/routes.json`](https://github.com/gc-victor/sxo/blob/main/dist/server/routes.json) contains per-route metadata including server modules, entry points, and asset references.
- The **esbuild metafile plugin** ([`src/js/esbuild/esbuild-metafile.plugin.js`](https://github.com/gc-victor/sxo/blob/main/src/js/esbuild/esbuild-metafile.plugin.js)) populates the `assets` field during the client build by analyzing bundler output.
- The **production server** loads routes via `loadRoutesManifest` and injects assets using `injectAssets` from [`src/js/server/utils/inject-assets.js`](https://github.com/gc-victor/sxo/blob/main/src/js/server/utils/inject-assets.js).
- **Asset injection** places CSS links before `</head>` and module scripts before `</body>`, using the normalized `PUBLIC_PATH` prefix.
- **Development hot-reload** uses the same asset list structure in its SSE payload, with the client script dynamically recreating tags to match the new build output.

## Frequently Asked Questions

### What is the exact location of the routes.json file in an SXO project?

The build pipeline generates [`routes.json`](https://github.com/gc-victor/sxo/blob/main/routes.json) at [`dist/server/routes.json`](https://github.com/gc-victor/sxo/blob/main/dist/server/routes.json) relative to the project root. The production server loads this file via `loadRoutesManifest` from [`src/js/server/shared/routes-loader.js`](https://github.com/gc-victor/sxo/blob/main/src/js/server/shared/routes-loader.js), which accepts the absolute path to this manifest as its primary argument.

### How does SXO determine which CSS and JS files belong to a specific route?

During the client build, the esbuild metafile plugin ([`src/js/esbuild/esbuild-metafile.plugin.js`](https://github.com/gc-victor/sxo/blob/main/src/js/esbuild/esbuild-metafile.plugin.js)) analyzes the bundler's output graph to correlate generated assets with their entry points. It then writes these client-relative paths into the [`assets.css`](https://github.com/gc-victor/sxo/blob/main/assets.css) and [`assets.js`](https://github.com/gc-victor/sxo/blob/main/assets.js) arrays of the corresponding route entry in [`routes.json`](https://github.com/gc-victor/sxo/blob/main/routes.json).

### What is the difference between generated and non-generated routes in the manifest?

Routes marked with `"generated": true` have been pre-rendered to static HTML files using `sxo generate`. The production server serves these static files directly without executing SSR. Non-generated routes require server-side rendering on each request, triggering the `injectAssets` flow to dynamically insert the latest CSS and JavaScript into the rendered HTML.

### How does the development hot-reload mechanism handle asset injection differently from production?

In development, the server sends asset lists via Server-Sent Events as part of the hot-replace payload (`{ body, assets, publicPath }`). The client-side script in [`src/js/server/hot-replace.client.js`](https://github.com/gc-victor/sxo/blob/main/src/js/server/hot-replace.client.js) receives this data and manually constructs new DOM elements to inject styles and scripts, whereas production performs this injection on the server using `injectAssets` before the HTML reaches the browser.