@koryla/next 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +91 -0
- package/dist/index.cjs +52 -0
- package/dist/index.d.cts +38 -0
- package/dist/index.d.ts +38 -0
- package/dist/index.js +27 -0
- package/package.json +34 -0
package/README.md
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# @koryla/next
|
|
2
|
+
|
|
3
|
+
A/B testing for Next.js with zero flicker. Runs on the Vercel Edge Network.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @koryla/next
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Setup
|
|
12
|
+
|
|
13
|
+
### 1. Add environment variables
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
# .env.local
|
|
17
|
+
KORYLA_API_KEY=sk_live_... # generated in Koryla → Settings → API Keys
|
|
18
|
+
KORYLA_API_URL=https://your-koryla-app.vercel.app
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
### 2. Create middleware.ts
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
// middleware.ts (project root, next to package.json)
|
|
25
|
+
import { korylaMiddleware } from '@koryla/next'
|
|
26
|
+
|
|
27
|
+
export default korylaMiddleware({
|
|
28
|
+
apiKey: process.env.KORYLA_API_KEY!,
|
|
29
|
+
apiUrl: process.env.KORYLA_API_URL!,
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
// Only run on the pages you're testing — keeps other routes fast
|
|
33
|
+
export const config = {
|
|
34
|
+
matcher: ['/', '/pricing', '/landing'],
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### 3. Create your variant pages
|
|
39
|
+
|
|
40
|
+
The middleware rewrites the URL server-side, so the variant page must exist in your app:
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
app/
|
|
44
|
+
page.tsx ← control (base_url: https://acme.com/)
|
|
45
|
+
variant-b/
|
|
46
|
+
page.tsx ← variant (target_url: https://acme.com/variant-b)
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### 4. Create an experiment in Koryla
|
|
50
|
+
|
|
51
|
+
Go to your Koryla dashboard → New experiment:
|
|
52
|
+
|
|
53
|
+
| Field | Value |
|
|
54
|
+
|-------|-------|
|
|
55
|
+
| Base URL | `https://acme.com/` |
|
|
56
|
+
| Variant A — Control | `https://acme.com/` · 50% |
|
|
57
|
+
| Variant B | `https://acme.com/variant-b` · 50% |
|
|
58
|
+
| Conversion URL | `https://acme.com/thank-you` |
|
|
59
|
+
|
|
60
|
+
Set it to **Active** and you're done.
|
|
61
|
+
|
|
62
|
+
## How it works
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
User visits /
|
|
66
|
+
│
|
|
67
|
+
▼
|
|
68
|
+
Next.js middleware (runs at Vercel edge, before rendering)
|
|
69
|
+
│
|
|
70
|
+
├── calls Koryla API once every 60s to get experiment config
|
|
71
|
+
├── reads sp_<experimentId> cookie
|
|
72
|
+
│ ├── cookie present → use existing variant (sticky)
|
|
73
|
+
│ └── no cookie → assign variant by traffic weight
|
|
74
|
+
│
|
|
75
|
+
├── rewrites request to /variant-b (server-side, no redirect)
|
|
76
|
+
└── sets cookie for 30 days
|
|
77
|
+
│
|
|
78
|
+
▼
|
|
79
|
+
Next.js renders /variant-b
|
|
80
|
+
Browser receives variant B HTML — never sees the rewrite
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Why this is better than VWO / Optimizely
|
|
84
|
+
|
|
85
|
+
| | VWO / Optimizely | @koryla/next |
|
|
86
|
+
|--|--|--|
|
|
87
|
+
| When variant is decided | In the browser, after JS loads | At the edge, before any HTML |
|
|
88
|
+
| Flicker | Yes (page hides while swapping) | No |
|
|
89
|
+
| Extra JS | ~80–150 KB | 0 KB |
|
|
90
|
+
| Blockable by ad blockers | Yes | No |
|
|
91
|
+
| Works without JS | No | Yes |
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
korylaMiddleware: () => korylaMiddleware
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(index_exports);
|
|
26
|
+
var import_server = require("next/server");
|
|
27
|
+
var import_core = require("@koryla/core");
|
|
28
|
+
var engine = null;
|
|
29
|
+
function korylaMiddleware(options) {
|
|
30
|
+
return async function middleware(request) {
|
|
31
|
+
if (!engine) engine = (0, import_core.createSplitEngine)(options);
|
|
32
|
+
const result = await engine.process(
|
|
33
|
+
request.url,
|
|
34
|
+
request.headers.get("cookie") ?? ""
|
|
35
|
+
);
|
|
36
|
+
if (!result) return import_server.NextResponse.next();
|
|
37
|
+
const response = import_server.NextResponse.rewrite(new URL(result.targetUrl));
|
|
38
|
+
if (result.isNewAssignment) {
|
|
39
|
+
response.cookies.set(result.cookieName, result.variantId, {
|
|
40
|
+
maxAge: 60 * 60 * 24 * 30,
|
|
41
|
+
// 30 days
|
|
42
|
+
sameSite: "lax",
|
|
43
|
+
path: "/"
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
return response;
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
50
|
+
0 && (module.exports = {
|
|
51
|
+
korylaMiddleware
|
|
52
|
+
});
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import { SplitEngineOptions } from '@koryla/core';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @koryla/next
|
|
6
|
+
*
|
|
7
|
+
* Next.js middleware adapter for Koryla A/B testing.
|
|
8
|
+
* Runs on the Vercel Edge Network — zero latency, zero flicker.
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* // middleware.ts (project root)
|
|
12
|
+
* import { korylaMiddleware } from '@koryla/next'
|
|
13
|
+
*
|
|
14
|
+
* export default korylaMiddleware({
|
|
15
|
+
* apiKey: process.env.KORYLA_API_KEY!,
|
|
16
|
+
* apiUrl: process.env.KORYLA_API_URL!, // e.g. https://your-app.koryla.com
|
|
17
|
+
* })
|
|
18
|
+
*
|
|
19
|
+
* export const config = {
|
|
20
|
+
* matcher: ['/', '/pricing', '/landing'], // pages to A/B test
|
|
21
|
+
* }
|
|
22
|
+
*
|
|
23
|
+
* How it works:
|
|
24
|
+
* 1. Middleware intercepts the request before Next.js renders anything
|
|
25
|
+
* 2. Calls Koryla API to get active experiments (cached 60s in memory)
|
|
26
|
+
* 3. Checks for an existing variant cookie — if found, sticks with it
|
|
27
|
+
* 4. If no cookie, randomly assigns a variant by traffic weight
|
|
28
|
+
* 5. Rewrites the request URL to the variant's target_url (server-side, no redirect)
|
|
29
|
+
* 6. Sets a 30-day cookie so the user always sees the same variant
|
|
30
|
+
*
|
|
31
|
+
* The browser never sees the rewrite — it only ever receives the assigned variant.
|
|
32
|
+
* This eliminates the flash of original content (FOOC) that client-side tools like
|
|
33
|
+
* VWO or Optimizely cause.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
declare function korylaMiddleware(options: SplitEngineOptions): (request: NextRequest) => Promise<NextResponse>;
|
|
37
|
+
|
|
38
|
+
export { korylaMiddleware };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import { SplitEngineOptions } from '@koryla/core';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @koryla/next
|
|
6
|
+
*
|
|
7
|
+
* Next.js middleware adapter for Koryla A/B testing.
|
|
8
|
+
* Runs on the Vercel Edge Network — zero latency, zero flicker.
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* // middleware.ts (project root)
|
|
12
|
+
* import { korylaMiddleware } from '@koryla/next'
|
|
13
|
+
*
|
|
14
|
+
* export default korylaMiddleware({
|
|
15
|
+
* apiKey: process.env.KORYLA_API_KEY!,
|
|
16
|
+
* apiUrl: process.env.KORYLA_API_URL!, // e.g. https://your-app.koryla.com
|
|
17
|
+
* })
|
|
18
|
+
*
|
|
19
|
+
* export const config = {
|
|
20
|
+
* matcher: ['/', '/pricing', '/landing'], // pages to A/B test
|
|
21
|
+
* }
|
|
22
|
+
*
|
|
23
|
+
* How it works:
|
|
24
|
+
* 1. Middleware intercepts the request before Next.js renders anything
|
|
25
|
+
* 2. Calls Koryla API to get active experiments (cached 60s in memory)
|
|
26
|
+
* 3. Checks for an existing variant cookie — if found, sticks with it
|
|
27
|
+
* 4. If no cookie, randomly assigns a variant by traffic weight
|
|
28
|
+
* 5. Rewrites the request URL to the variant's target_url (server-side, no redirect)
|
|
29
|
+
* 6. Sets a 30-day cookie so the user always sees the same variant
|
|
30
|
+
*
|
|
31
|
+
* The browser never sees the rewrite — it only ever receives the assigned variant.
|
|
32
|
+
* This eliminates the flash of original content (FOOC) that client-side tools like
|
|
33
|
+
* VWO or Optimizely cause.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
declare function korylaMiddleware(options: SplitEngineOptions): (request: NextRequest) => Promise<NextResponse>;
|
|
37
|
+
|
|
38
|
+
export { korylaMiddleware };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { NextResponse } from "next/server";
|
|
3
|
+
import { createSplitEngine } from "@koryla/core";
|
|
4
|
+
var engine = null;
|
|
5
|
+
function korylaMiddleware(options) {
|
|
6
|
+
return async function middleware(request) {
|
|
7
|
+
if (!engine) engine = createSplitEngine(options);
|
|
8
|
+
const result = await engine.process(
|
|
9
|
+
request.url,
|
|
10
|
+
request.headers.get("cookie") ?? ""
|
|
11
|
+
);
|
|
12
|
+
if (!result) return NextResponse.next();
|
|
13
|
+
const response = NextResponse.rewrite(new URL(result.targetUrl));
|
|
14
|
+
if (result.isNewAssignment) {
|
|
15
|
+
response.cookies.set(result.cookieName, result.variantId, {
|
|
16
|
+
maxAge: 60 * 60 * 24 * 30,
|
|
17
|
+
// 30 days
|
|
18
|
+
sameSite: "lax",
|
|
19
|
+
path: "/"
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
return response;
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
export {
|
|
26
|
+
korylaMiddleware
|
|
27
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@koryla/next",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./dist/index.cjs",
|
|
6
|
+
"module": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./dist/index.js",
|
|
11
|
+
"require": "./dist/index.cjs",
|
|
12
|
+
"types": "./dist/index.d.ts"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": ["dist"],
|
|
16
|
+
"publishConfig": {
|
|
17
|
+
"access": "public"
|
|
18
|
+
},
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "tsup src/index.ts --format esm,cjs --dts --clean",
|
|
21
|
+
"dev": "tsup src/index.ts --format esm,cjs --dts --watch"
|
|
22
|
+
},
|
|
23
|
+
"peerDependencies": {
|
|
24
|
+
"next": ">=13"
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"@koryla/core": "workspace:^"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"next": "^15.0.0",
|
|
31
|
+
"tsup": "^8.0.0",
|
|
32
|
+
"typescript": "^5"
|
|
33
|
+
}
|
|
34
|
+
}
|