# React SPA on uat-beam.page

> **The SPA must be built for the `/app/` URL prefix.** Set Vite's
> `base: '/app/'` in `vite.config.js`, and let React Router pick up the
> same prefix with `<BrowserRouter basename={import.meta.env.BASE_URL}>`
> (Vite populates `import.meta.env.BASE_URL` from `base`). Without
> these, every asset and route resolves to the wrong URL on uat-beam.page.

Ship a React + Vite + React Router build to a uat-beam.page project. One
project, one sub-page, one ZIP upload, one redirect. The SPA lives
under `/app/`; the bare subdomain redirects to it.

This is the right pattern when:

- The app is client-rendered (React Router, React Query, Zustand, etc.)
- The build output is a `dist/` folder with `index.html` and an
  `assets/` sub-folder
- The user wants a real SPA with clean URLs, not a bunch of static
  HTML files

The key platform fact: ZIP uploads require `index.html` at the **root**
of the archive. So you upload the SPA to a sub-page named `app` and ZIP
from inside `dist/` — the archive root is the build root, and the page
slug `app` is what maps the files to `/app/...` on the subdomain.

## 1. Configure Vite and React Router for `/app/`

Edit `vite.config.js`:

```js
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";

export default defineConfig({
  plugins: [react()],
  base: "/app/",
});
```

In your router setup:

```jsx
import { createBrowserRouter } from "react-router-dom";

const router = createBrowserRouter(routes, { basename: "/app" });
```

## 2. Build and stage the ZIP

```bash
npm run build                       # emits dist/
cd dist && zip -r ../build.zip .    # ZIP entries are root-level
cd ..
```

Verify with `unzip -l build.zip` — you should see `index.html` (not
`app/index.html`) and `assets/...` at the top level.

## 3. Create the project and the `app` sub-page

```python
import requests, time
API = "https://api.uat-beam.page"
H = {"Authorization": f"Bearer {TOKEN}", "Content-Type": "application/json"}
slug = "my-react-app"

requests.post(f"{API}/projects", headers=H, json={
    "slug": slug,
    "context": "React + Vite SPA under /app/.",
})
time.sleep(2)

requests.post(f"{API}/projects/{slug}/pages", headers=H, json={
    "slug": "app",
    "notes": "React SPA build. Do not hand-edit — regenerate from source.",
})
time.sleep(1)

r = requests.post(
    f"{API}/projects/{slug}/pages/app/upload-zip-url",
    headers=H,
).json()
```

Then PUT the ZIP (MCP agents: ask the user to run the `curl`; chat-only
MCP cannot PUT binary bytes):

```bash
curl -X PUT --data-binary @build.zip \
  -H "Content-Type: application/zip" \
  -H "x-amz-meta-via: presigned-zip-upload" \
  -H "x-amz-meta-tenant-id: <from response.headers>" \
  -H "x-amz-meta-user-sub: <from response.headers>" \
  -H "x-amz-meta-entity: PAGE" \
  -H "x-amz-meta-project-id: my-react-app" \
  -H "x-amz-meta-slug: app" \
  "<response.uploadUrl>"
```

Echo every header from `response.headers` verbatim — mismatch fails
with `SignatureDoesNotMatch`.

## 4. Redirect the bare subdomain to the SPA

```python
requests.post(f"{API}/projects/{slug}/redirects", headers=H, json={
    "from": "/",
    "to": "/app/",
})
```

Wait ~5 seconds.

## 5. Verify

```bash
curl -sI https://my-react-app.uat-beam.page/
# → HTTP/2 301 ... location: /app/

curl -s https://my-react-app.uat-beam.page/app/ | head
# → your index.html

curl -s https://my-react-app.uat-beam.page/app/any/deep/route | head
# → same index.html (router takes over client-side)
```

If a deep route 404s, check that Vite's `base: "/app/"` matches React
Router's `basename: "/app"` (no trailing slash on the router side).
