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
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,16 @@ Create beautiful, interactive mini apps with zero setup. Your creations are auto
- Add your AI account key from [OpenRouter](https://openrouter.ai/settings/keys)
- Run `pnpm dev`

## Developer previews on the main domain (no redirects)

Opt into an experimental branch deploy on the primary site using Netlify Split Testing with a cookie. Use `?ab=<branch>` on the main domain, for example:

```
https://vibes.diy/?ab=feature-new-ui
```

Note: the underlying Netlify cookie is named `nf_ab` and is host‑scoped by default (not shared between `www` and apex). See [docs/split-testing.md](docs/split-testing.md) for details and scope options.

## Your Work is Always Safe

Every app you create is automatically saved, so you can:
Expand Down
7 changes: 7 additions & 0 deletions app/root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,13 @@ export function Layout({ children }: { children: React.ReactNode }) {
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
{/**
* Netlify Split Testing opt-in/out via query params (pre-mount)
*
* Moved to a small static file to keep CSP strict (no 'unsafe-inline').
* The script must execute before the app mounts; keep it first in <head>.
*/}
<script src="/nf-ab.cookie.js"></script>
<Meta data-testid="meta" />
<Links />
</head>
Expand Down
49 changes: 49 additions & 0 deletions docs/split-testing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
## Netlify Split Testing: Developer Opt‑in via Cookie (no redirects)

This app supports Netlify Split Testing so developers can opt into an alternate branch deploy while staying on the main domain. Because the origin does not change, browser storage (localStorage, sessionStorage, cookies) is preserved across versions.

### How it works

- Netlify routes traffic between eligible branch deploys at the CDN proxy layer.
- Visitors are bucketed by the `nf_ab` cookie. When present, Netlify serves all content for that visitor from the selected branch deploy on the main domain.
- We ship a tiny pre‑mount script that reads the `ab=` query parameter on arrival and sets/clears the `nf_ab` cookie before the app initializes, then reloads once so the CDN picks up the selection.

### Opt in

Open the primary site URL with:

- `?ab=<branch-name>` — sets the cookie to the exact branch name and reloads.

Example:

```
https://vibes.diy/?ab=feature-new-ui
```

After reload, Netlify will proxy all requests for that browser to the `feature-new-ui` branch deploy at `https://vibes.diy/...` (no redirect).

### Opt out / revert

Open the primary site URL with the following to clear the bucket:

- `?ab=clear` (aliases: `off`, `reset`, `none`)

This removes the cookie and reloads once. Without the cookie, Netlify serves the primary branch (e.g., `main`). If you want to force the primary branch explicitly, you can also set `?ab=main` (or your production branch name).

### Important limitations

- Split Testing only supports persistent branch deploys. Deploy Previews (per‑PR) cannot be targeted by the cookie‑based Split Testing mechanism (`nf_ab`).
- When Split Testing is enabled for a site, Netlify does not execute Edge Functions for that site. If you rely on Edge Functions, enable Split Testing only when acceptable for your routes.
- Storage schema compatibility: both branches share the same origin storage. If you change localStorage structure on one branch, ensure the other can tolerate or migrate it.

#### Cookie scope (apex vs subdomains)

By default, the `nf_ab` cookie is set without a `Domain` attribute. That means it is host‑scoped and will not be shared across hosts like `vibes.diy` and `www.vibes.diy`. If you need the same selection to apply across subdomains, consider setting a cookie `Domain` (for example, `Domain=.vibes.diy`). Doing so shares the bucket across all subdomains but also broadens the cookie’s reach. Evaluate privacy and collision trade‑offs before enabling.

This repository ships the default host‑scoped behavior. If cross‑subdomain behavior is desired, we can add an option to include a `Domain` attribute in the cookie setter; please confirm the desired scope on the PR.

### Configuration required in Netlify

Split Testing is configured at the site level in the Netlify UI. Include your production branch (e.g., `main`) and at least one experimental branch deploy. Do not add PR Deploy Previews. Traffic allocation can be 0/100 — the cookie opt‑in will still work.

<!-- The public docs intentionally document only the `ab=` query parameter. The code continues to accept other aliases for compatibility, but those are not documented. -->
71 changes: 71 additions & 0 deletions public/nf-ab.cookie.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/*
* Netlify Split Testing opt-in/out via query params (pre-mount)
*
* Reads `?nf_ab` or `?ab` on arrival to set/clear the `nf_ab` cookie before the app
* initializes, so Netlify can route this and subsequent requests to a specific branch
* deploy on the primary domain without redirects. Preserves origin-scoped storage.
*
* Usage examples (open on the main domain):
* - Set to a branch: ?nf_ab=my-experimental-branch
* - Clear the cookie: ?nf_ab=clear (aliases: off, reset, none)
* - Also accepts `ab=` as a synonym for `nf_ab=`
*/
(function () {
try {
var u = new URL(window.location.href);
var sp = u.searchParams;
var hasNf = sp.has('nf_ab');
var hasAb = sp.has('ab');
if (!hasNf && !hasAb) return;

var value = hasNf ? sp.get('nf_ab') : sp.get('ab');
var clear = value && /^(clear|off|reset|none)$/i.test(value);

var secure = window.location.protocol === 'https:' ? '; Secure' : '';
var cookieBase = 'nf_ab=';

// Make whitespace after semicolons optional to be robust across browsers
var currentMatch = document.cookie.match(/(?:^|;\s*)nf_ab=([^;]+)/);
var current = currentMatch ? decodeURIComponent(currentMatch[1]) : null;

// Apply changes if needed
if (clear) {
// Clear cookie if present
if (current) {
document.cookie = 'nf_ab=; Max-Age=0; Path=/; SameSite=Lax' + secure;
}
} else if (value && value !== current) {
// Long-ish expiration so the choice sticks for developers
var expires = new Date(Date.now() + 365 * 24 * 60 * 60 * 1000).toUTCString();
document.cookie =
cookieBase +
encodeURIComponent(value) +
'; Expires=' +
expires +
'; Path=/; SameSite=Lax' +
secure;
}

// Determine whether a reload is required (only if the effective value changed)
var shouldReload = false;
if (clear) {
if (current) shouldReload = true; // only reload if we actually removed an existing cookie
} else if (value && value !== current) {
shouldReload = true;
}

// Always remove our params to avoid loops and keep URLs clean
sp.delete('nf_ab');
sp.delete('ab');
var newUrl = u.origin + u.pathname + (sp.toString() ? '?' + sp.toString() : '') + u.hash;

if (shouldReload) {
window.location.replace(newUrl);
} else if (hasNf || hasAb) {
// Clean the URL without a reload if nothing changed
history.replaceState(null, '', newUrl);
}
} catch (_e) {
// Swallow errors to avoid blocking page load in edge cases
}
})();