@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 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
+ });
@@ -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 };
@@ -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
+ }