Skip to content
Merged
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ netlify
netlify.toml
!packages/react-server/adapters/netlify
deno.lock
.azure
.azure-swa
.bun
.deno
*.pem
Expand Down
2 changes: 2 additions & 0 deletions docs/.gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
.vercel

.env
59 changes: 59 additions & 0 deletions docs/src/components/AdapterGrid.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
const adapters = [
{
name: "Vercel",
href: "/deploy/vercel",
description: "Serverless & edge functions",
},
{
name: "Netlify",
href: "/deploy/netlify",
description: "Serverless functions & edge CDN",
},
{
name: "Cloudflare",
href: "/deploy/cloudflare",
description: "Workers & Pages",
},
{
name: "Bun",
href: "/deploy/bun",
description: "Standalone Bun server",
},
{
name: "Deno",
href: "/deploy/deno",
description: "Standalone Deno server",
},
{
name: "Azure Functions",
href: "/deploy/azure",
description: "Functions v4 with streaming",
},
{
name: "Azure Static Web Apps",
href: "/deploy/azure-swa",
description: "Managed functions & CDN",
},
];

export default function AdapterGrid() {
return (
<div className="my-4 grid grid-cols-1 md:grid-cols-3 gap-4 not-prose">
{adapters.map(({ name, href, description }) => (
<a
key={href}
href={href}
className="adapter-card flex flex-col rounded-xl p-4 bg-gray-50 dark:bg-gray-800 text-xs shadow-lg dark:shadow-[rgba(255,255,255,0.1)] border border-gray-500 no-underline hover:no-underline transition-colors hover:border-gray-300 dark:hover:border-gray-400"
style={{ textDecoration: "none", color: "inherit" }}
>
<span className="adapter-card-title relative inline-block self-start font-semibold text-base text-black dark:text-gray-300 mb-1">
{name}
</span>
<span className="font-normal text-sm text-gray-500 dark:text-gray-400">
{description}
</span>
</a>
))}
</div>
);
}
4 changes: 3 additions & 1 deletion docs/src/pages/en/(pages)/deploy/adapters.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,14 @@ You can use adapters to configure your app for different deployment environments
- [x] Cloudflare Workers/Pages
- [x] Bun
- [x] Deno
- [x] Azure Functions
- [x] Azure Static Web Apps

<Link name="configuration">
## Configuration
</Link>

Add `adapter` entry to your `react-server.config.mjs` file. You can specify the name of a built-in adapter (`vercel`, `netlify`, `cloudflare`, `bun`, or `deno`) as a string, or use an external adapter package.
Add `adapter` entry to your `react-server.config.mjs` file. You can specify the name of a built-in adapter (`vercel`, `netlify`, `cloudflare`, `bun`, `deno`, `azure`, or `azure-swa`) as a string, or use an external adapter package.

> **Note:** When running a production build with **Bun** or **Deno**, the corresponding adapter is automatically detected and used without any configuration. You can override this with an explicit `adapter` setting in your config or via `--adapter <name>` on the CLI. Use `--no-adapter` to disable auto-detection.

Expand Down
21 changes: 18 additions & 3 deletions docs/src/pages/en/(pages)/deploy/api.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,14 @@ You need to pass adapter properties to the `createAdapter` function to configure

`deploy`: The deployment command and arguments. This is optional. When provided, the adapter will show what command the developer needs to run to deploy the application after it has been built. If the `--deploy` flag is provided during the build, the adapter will run this command. The `deploy` property can also be a function that will be called with the adapter options, CLI options and the handler result. This is useful if you need to customize the deployment command based on the adapter options or the handler result. If you don't provide a result with `command` and `args`, the default deployment handling spawning the command will be skipped. This is useful if you want to implement a custom deployment workflow in the adapter.

The deploy descriptor supports the following properties:

- [ ] `command`: The CLI command to run.
- [ ] `args`: The command arguments.
- [ ] `cwd`: The working directory for the command. Defaults to the project root.
- [ ] `message`: Help text shown to the user when `--deploy` is not used.
- [ ] `afterDeploy`: A callback invoked after successful deployment (e.g., to print the deployment URL).

```js
export const adapter = createAdapter({
// ...
Expand All @@ -73,6 +81,10 @@ export const adapter = createAdapter({
return {
command: "vercel",
args: ["deploy", "--prebuilt"],
cwd: outDir,
afterDeploy: () => {
console.log("Deployment complete!");
},
};
},
});
Expand All @@ -88,11 +100,13 @@ The adapter handler function will receive the following properties:
- [ ] `files`: The files object contains the static files, assets, client files, public files, server files and the dependencies.
- [ ] `copy`: The copy object contains the functions to copy the files to the output directory.
- [ ] `config`: The configuration object contains the configuration of the application.
- [ ] `reactServerDir`: The path to the directory where the build output is located.
- [ ] `reactServerOutDir`: The directory name where the build output is located.
- [ ] `reactServerDir`: The absolute path to the directory where the build output is located.
- [ ] `reactServerOutDir`: The relative directory name where the build output is located (default `.react-server`, configurable via `outDir` in config).
- [ ] `root`: The entry point of the application.
- [ ] `options`: The options object contains the options passed from the CLI.

> **Important:** When referencing server files in generated code (e.g., import paths), always use `reactServerOutDir` instead of hardcoding `.react-server`. This ensures your adapter works when users customize the build output directory.

The `files` object contains the following functions:

- [ ] `static`: The function to get the static files.
Expand Down Expand Up @@ -215,10 +229,11 @@ const dependencies = await getDependencies(adapterFiles, reactServerDir);

### spawnCommand

Spawns a command in the current working directory.
Spawns a command. Accepts an optional options object with `cwd` to set the working directory.

```js
await spawnCommand("vercel", ["deploy", "--prebuilt"]);
await spawnCommand("func", ["azure", "functionapp", "publish", appName], { cwd: outDir });
```

### deepMerge
Expand Down
236 changes: 236 additions & 0 deletions docs/src/pages/en/(pages)/deploy/azure-swa.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
---
title: Azure Static Web Apps
category: Deploy
order: 7
---

import Link from "../../../../components/Link.jsx";

# Azure Static Web Apps

To deploy to Azure Static Web Apps (SWA), use the built-in `azure-swa` adapter. This adapter packages your app for SWA's managed functions and CDN-backed static hosting.

> **Note:** Azure Static Web Apps does **not** support response streaming. All responses are buffered before being sent to the client. If you need streaming (React Suspense, progressive HTML), use the [Azure Functions](/deploy/azure) adapter instead.

<Link name="installation">
## Installation
</Link>

You need the [Azure Static Web Apps CLI](https://azure.github.io/static-web-apps-cli/) installed:

```sh
npm install -g @azure/static-web-apps-cli
```

No additional packages are needed — the adapter is built into `@lazarv/react-server`.

Add the adapter to your `react-server.config.mjs` file:

```mjs
export default {
adapter: "azure-swa",
};
```

<Link name="configuration">
## Configuration
</Link>

You can customize the adapter by passing options:

```mjs
export default {
adapter: [
"azure-swa",
{
host: {}, // Extra host.json properties
routes: [], // Additional SWA route rules
platform: { // Platform configuration overrides
apiRuntime: "node:20",
},
staticwebapp: {}, // Extra staticwebapp.config.json properties
env: { // Extra environment variables
MY_API_KEY: "value",
},
},
],
};
```

### Configuration Options

- `host`: Additional properties to merge into the generated `host.json`.
- `routes`: Additional route rules to add to `staticwebapp.config.json`. These are placed after the default `/`, `/assets/*`, and `/client/*` rules.
- `platform`: Override the platform configuration in `staticwebapp.config.json` (default: `{ apiRuntime: "node:20" }`).
- `staticwebapp`: Additional top-level properties to merge into `staticwebapp.config.json`.
- `env`: Additional environment variables for `local.settings.json`.

<Link name="extending-config">
## Extending SWA configuration
</Link>

To extend the generated `staticwebapp.config.json`, create a `react-server.azure.json` file in your project root. The adapter will merge it with the generated config:

```json filename="react-server.azure.json"
{
"responseOverrides": {
"404": {
"rewrite": "/api/server"
}
},
"globalHeaders": {
"X-Frame-Options": "DENY"
}
}
```

<Link name="deploy">
## Deploy
</Link>

Build and deploy in one step:

```sh
pnpm react-server build --deploy
```

Or build first and deploy manually:

```sh
# Build
pnpm react-server build

# Deploy
swa deploy .azure-swa/static \
--api-location .azure-swa/functions \
--api-language node \
--api-version 20
```

Before deploying, make sure you have an Azure Static Web Apps resource created. You can create one in the [Azure portal](https://portal.azure.com) or using the Azure CLI:

```sh
az staticwebapp create \
--name my-app \
--resource-group my-rg \
--location "eastus2"
```

<Link name="how-it-works">
## How it works
</Link>

The adapter uses an **edge build** mode, bundling your server into a single file. At build time, it:

1. Bundles your server into `.azure-swa/functions/server/.react-server/server/edge.mjs`
2. Copies static assets into `.azure-swa/static/`
3. Generates a `functions/server/index.mjs` wrapper that bridges Azure Functions v3's `(context, req)` model to the standard fetch handler
4. Generates `function.json` (HTTP trigger), `host.json`, and `staticwebapp.config.json`
5. Creates a fallback `index.html` in the static directory (required by SWA)

### Static file routing

Static files are served by Azure SWA's built-in CDN. The `staticwebapp.config.json` configures:

- `/assets/*` and `/client/*` routes with immutable cache headers
- A `navigationFallback` that rewrites all non-static requests to the `/api/server` function
- The root `/` path rewrites to `/api/server`

This means static assets bypass the serverless function entirely, served directly from SWA's edge CDN.

<Link name="output-structure">
## Output Structure
</Link>

```
.azure-swa/
├── staticwebapp.config.json # SWA routing configuration
├── functions/
│ ├── host.json # Azure Functions host config
│ ├── local.settings.json # Local dev settings
│ ├── package.json # ESM support
│ └── server/
│ ├── function.json # HTTP trigger binding
│ ├── index.mjs # Request handler wrapper
│ └── .react-server/ # Bundled server (edge.mjs, manifests)
└── static/
├── staticwebapp.config.json
├── index.html # Fallback (required by SWA)
├── assets/ # Vite-built assets
├── client/ # Client component bundles
└── ... # Other static files
```

<Link name="azure-vs-azure-swa">
## Azure Functions vs. Azure SWA
</Link>

| Feature | `azure` (Functions v4) | `azure-swa` (Static Web Apps) |
|---|---|---|
| **Streaming** | Yes | No (responses are buffered) |
| **Static files** | Served by the function | Served by CDN |
| **Auto-provisioning** | Yes (via Bicep) | Manual (portal or CLI) |
| **Cold starts** | Consumption plan latency | Managed by SWA |
| **Custom domains** | Via Function App settings | Via SWA settings |
| **Functions version** | v4 (programming model) | v3 (function.json) |

Choose `azure` if you need streaming or want automatic resource provisioning. Choose `azure-swa` for simpler static-heavy apps where CDN-backed asset serving is more important than streaming.

<Link name="troubleshooting">
## Troubleshooting
</Link>

### SWA CLI not found

Install the CLI globally:

```sh
npm install -g @azure/static-web-apps-cli
```

### Responses are buffered / no streaming

This is a limitation of Azure Static Web Apps. SWA buffers all function responses before sending them to the client. If you need streaming, switch to the [Azure Functions](/deploy/azure) adapter.

### 404 errors on page navigation

Make sure the `navigationFallback` is configured in `staticwebapp.config.json`. The adapter generates this automatically. If you've customized the config via `react-server.azure.json`, ensure you haven't overwritten the `navigationFallback` section.

### Empty or broken page on root URL

Verify that the `/` route rewrite to `/api/server` is present in `staticwebapp.config.json`. The adapter creates this by default. If the page loads but shows only `<!doctype html>`, the function may not be running — check the function logs in the Azure portal.

### Function not triggering

Check that the function was deployed correctly:

```sh
swa deploy .azure-swa/static \
--api-location .azure-swa/functions \
--api-language node \
--api-version 20 \
--verbose
```

Ensure that `.azure-swa/functions/server/function.json` exists and defines the HTTP trigger binding.

### "x-ms-original-url" header issues

Azure SWA uses `navigationFallback` to rewrite requests to the API function. The original URL is passed via the `x-ms-original-url` header. The adapter's wrapper handles this automatically. If you see incorrect URLs in your app, check that your SWA configuration's `navigationFallback.exclude` patterns aren't matching routes that should go to the function.

### Local development

You can test locally using the SWA CLI:

```sh
swa start .azure-swa/static \
--api-location .azure-swa/functions \
--api-port 7071
```

Or use Azure Functions Core Tools directly for the API:

```sh
cd .azure-swa/functions
func start
```
Loading
Loading