# Run a redirect experiment

Split traffic between two pages and compare the results.

A redirect experiment sends part of your traffic to a different URL. Everyone arrives at the same page, and a share of the visitors is immediately forwarded to a different one, usually a redesigned version of that page or an entirely new conversion flow.

Because most testing tools work this way, redirect experiments are usually the first format teams are exposed to. One thing worth keeping in mind is that the redirected group experiences two page loads before seeing any content, so it is a good idea to weigh that tradeoff before choosing this approach.

In this tutorial, you will decide whether a redirect experiment is the right tool for the change you want to test, and then run one from the first component to a published experiment.

## Choose the right approach

Before implementing a redirect, you need to be sure the change actually calls for a second URL. There are three ways to compare two versions of a page, and they differ in where the split happens and what it costs the visitor:

| Approach            | Where the split happens                    | User experience            |
| ------------------- | ------------------------------------------ | -------------------------- |
| Content experiment  | On the server, while the page renders      | Seamless, one page load    |
| CTA experiment      | On click, in the destination of the link   | Seamless, one page load    |
| Redirect experiment | On arrival, before or right after the load | Extra wait, two page loads |

### Content experiment

Everyone stays on the same URL, and the parts that differ come from a slot: a headline, a CTA label, an image, a whole section, or the layout itself. Because we resolve content on the server, the assigned variant is already in the first HTML response, so the visitor gets a single page load with no redirect.

### CTA experiment

The versions live on different URLs, but the split happens when the visitor clicks, not when they arrive. The slot holds the destination of the link, so each variant sends the visitor to a different page and nobody is redirected.

### Redirect experiment

The entry point itself is the page under test, and the variant lives at another URL. The redirected group pays for two page loads, so reach for it when the two versions are genuinely separate pages, such as a page rebuilt from scratch, a different template, or a landing page produced by another tool.

> **Most redirect experiments do not need a redirect**
>
> If the two versions differ only in what the page says or shows, model the difference as content and keep a single URL. You learn the same thing without a second page load, and the results are not skewed by the visitors who leave while the redirect happens.

## How it works

Every request follows the same path:

1. The visitor requests the page, and your application resolves the redirect slot along with the rest of the content.

2. We evaluate the audience, allocate the visitor to a variant, and return the URL configured for that variant, or nothing for the group that stays.

3. Your application redirects when the slot has a URL, and renders the page as usual when it does not.

4. We track the exposure, with no extra call on your side.

5. When the visitor converts, your application tracks a goal event on whichever page the conversion happens.

The slot is the only moving part. Your code asks for a URL and acts on it, so adding a variant, changing a destination, or ending the experiment never requires a deployment.

## Choose where the redirect happens

The redirect can run on the server or in the browser, and the choice decides what the visitor sees while the assignment is resolved:

| Aspect                | Server-side redirect                 | Client-side redirect                           |
| --------------------- | ------------------------------------ | ---------------------------------------------- |
| Decision point        | Before any HTML is sent              | After the page loads and the content arrives   |
| What the visitor sees | Only the final page                  | A hidden page until the decision arrives       |
| Flicker risk          | None                                 | Real, if the mask fails or a script is blocked |
| Crawlers and bots     | Follow a standard HTTP redirect      | Never run the experiment                       |
| Requirement           | A server or edge runtime per request | Only the browser SDK                           |

Redirect on the server whenever your stack renders per request. It costs the visitor nothing beyond the redirect itself, and the original page is never rendered for the group that leaves it.

Redirect in the browser only when there is no server to decide, such as a fully static site served from a CDN, or a page you cannot change on the server. To avoid showing the old page before the new one, you have to hide the page until the answer arrives, which delays the first paint for everyone, including the visitors who stay.

## Set up the experiment

Running a redirect experiment follows the same flow as any other AB test, with a slot that carries a URL instead of visible content.

### Create the slot

The slot is what your code reads on every request, and the component defines what it can hold:

1. Open the [components page](https://app.croct.com/redirect/organizations/-organization-/workspaces/-workspace-/components) and create a component with the ID `redirect` and the following attribute:

   | Attribute     | Type       | Required |
   | ------------- | ---------- | -------- |
   | `redirectUrl` | Plain text | No       |

   Keep the attribute optional. An absent value is what tells your application that the visitor stays where they are.

2. Open the [slots page](https://app.croct.com/redirect/organizations/-organization-/workspaces/-workspace-/slots) and create a slot with the ID `redirect`, associated with the `redirect` component.

   Leave the [default content](/explanation/content/slot-default-content) empty. An empty URL means no redirect, so the page keeps behaving exactly as it does today until you publish the experiment, and it goes back to that the moment you stop it.

3. Add the slot to your project:

   ```sh
   croct add slot redirect
   ```

   The CLI [generates the type definitions](/reference/cli/type-generation) and [downloads the default content](/reference/cli/fallback-content) for use as an automatic fallback, so a failed request leaves the visitor on the current page instead of blocking the experiment.

### Redirect on the server

Resolve the slot where your application handles the request, and redirect when the slot carries a URL:

**Plug Next — App router**

```jsx
import {redirect} from 'next/navigation';
import {fetchContent} from '@croct/plug-next/server';
import {HomeHero} from '@/components/HomeHero';

export default async function HomePage() {
  const {content} = await fetchContent('redirect');

  if (content.redirectUrl) {
    redirect(content.redirectUrl);
  }

  return <HomeHero />;
}
```

**Plug Next — Page router**

```jsx
import {fetchContent} from '@croct/plug-next/server';
import {HomeHero} from '@/components/HomeHero';

export const getServerSideProps = async context => {
  const {content} = await fetchContent('redirect', {
    route: context,
  });

  if (content.redirectUrl) {
    return {
      redirect: {
        destination: content.redirectUrl,
        permanent: false,
      },
    };
  }

  return {props: {}};
};

export default function HomePage() {
  return <HomeHero />;
}
```

**Plug Hydrogen**

```jsx
import {redirect} from 'react-router';
import {fetchContent} from '@croct/plug-hydrogen/server';
import {HomeHero} from '~/components/HomeHero';

export async function loader({context}) {
  const {content} = await fetchContent('redirect', {
    scope: context,
  });

  if (content.redirectUrl) {
    return redirect(content.redirectUrl);
  }

  return {};
}

export default function Index() {
  return <HomeHero />;
}
```

**Plug Nuxt**

```js
export default defineEventHandler(async event => {
  if (getRequestURL(event).pathname !== '/') {
    return;
  }

  const {content} = await fetchContent('redirect');

  if (content.redirectUrl) {
    return sendRedirect(event, content.redirectUrl, 302);
  }
})
```

**Plug PHP**

```php
<?php

use Croct\Plug\Croct;

$croct = Croct::fromDotenv();
$content = $croct->fetchContent('redirect')->getContent();

Croct::emitCookies();

if (!empty($content['redirectUrl'])) {
    header('Location: ' . $content['redirectUrl'], true, 302);

    exit;
}
```

**Plug Symfony**

```php
<?php

namespace App\Controller;

use Croct\Plug\Plug;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;

final class HomeController extends AbstractController
{
    #[Route('/', name: 'home')]
    public function index(Plug $croct): Response
    {
        $content = $croct->fetchContent('redirect')->getContent();

        if (!empty($content['redirectUrl'])) {
            return $this->redirect($content['redirectUrl']);
        }

        return $this->render('home/index.html.twig');
    }
}
```

**Plug Laravel**

```php
<?php

use Croct\Plug\Plug;
use Illuminate\Support\Facades\Route;

Route::get('/', function (Plug $croct) {
    $content = $croct->fetchContent('redirect')->getContent();

    if (!empty($content['redirectUrl'])) {
        return redirect($content['redirectUrl']);
    }

    return view('home');
});
```

**Plug Drupal**

```php
<?php

declare(strict_types=1);

namespace Drupal\my_module\EventSubscriber;

use Croct\Plug\Plug;
use Drupal\Core\Routing\TrustedRedirectResponse;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\KernelEvents;

final class RedirectSubscriber implements EventSubscriberInterface
{
    private Plug $croct;

    public function __construct(Plug $croct)
    {
        $this->croct = $croct;
    }

    public static function getSubscribedEvents(): array
    {
        return [KernelEvents::REQUEST => 'onRequest'];
    }

    public function onRequest(RequestEvent $event): void
    {
        if (!$event->isMainRequest() || $event->getRequest()->getPathInfo() !== '/') {
            return;
        }

        $content = $this->croct->fetchContent('redirect')->getContent();

        if (empty($content['redirectUrl'])) {
            return;
        }

        $response = new TrustedRedirectResponse($content['redirectUrl']);

        // The destination is per visitor, so keep it out of the page cache.
        $response->getCacheableMetadata()->setCacheMaxAge(0);

        $event->setResponse($response);
    }
}
```

Because the decision happens before any HTML is sent, the visitor never sees the page they are about to leave, and crawlers follow an ordinary HTTP redirect.

> **Keep the redirect temporary**
>
> Use a temporary status, such as 302 or 307, not a permanent one. A permanent redirect is cached by browsers and search engines, so visitors would keep landing on the variant long after the experiment ends.

> **Good to know: How do I avoid a redirect loop?**
>
> A loop happens when the destination page resolves the same slot and is sent back to itself. Scoping the fetch to the page under test, as in the examples above, is usually enough. When the code runs on every request, such as in middleware or an event subscriber, compare the destination with the current URL and redirect only when they differ:
>
> ```js
> const target = new URL(content.redirectUrl, request.url);
>
> if (target.href !== request.url) {
>   // Redirect to the target.
> }
> ```
>
> Resolving the destination against the current URL also lets you configure relative paths, such as `/new-page`, in the platform.

### Redirect in the browser

> **Prefer the server-side redirect whenever possible**
>
> Hiding the page delays the first paint for all visitors, including the ones who stay. Reach for this approach only where there is no server to decide, and keep the mask on the page under test.

When no server is involved, the page has to hide itself until the content arrives, otherwise the visitor sees the current page and then jumps to another one.

Hide the page until the answer arrives, and reveal it as soon as it does, whatever the answer is:

**Plug JS**

**redirect.js**

```js
import croct from '@croct/plug';

croct.plug({appId: 'APPLICATION_ID'});

croct.fetch('redirect')
    .then(({content}) => {
        if (content.redirectUrl) {
            location.replace(new URL(content.redirectUrl, location.href));
        }
    })
    .finally(() => document.body.classList.remove('croct-pending'));
```

**index.html**

```html
<!DOCTYPE html>
<html>
<head>
    <title>My awesome application</title>
    <style>
        body.croct-pending {
            visibility: hidden;
        }
    </style>
    <script type="module" src="/redirect.js"></script>
</head>
<body class="croct-pending">
    <!-- Your page goes here -->
</body>
</html>
```

**Plug React**

**components/RedirectTest.jsx**

```jsx
import {useEffect} from 'react';
import {useContent} from '@croct/plug-react';

export function RedirectTest({children}) {
  // The `null` initial value marks the decision as pending while the content loads.
  const {content} = useContent('redirect', {
    initial: {redirectUrl: null},
  });

  const decided = content.redirectUrl !== null;

  useEffect(
    () => {
      if (content.redirectUrl) {
        location.replace(new URL(content.redirectUrl, location.href));
      }
    },
    [content.redirectUrl],
  );

  return decided && !content.redirectUrl ? children : null;
}
```

**src/App.jsx**

```jsx
import {CroctProvider} from '@croct/plug-react';
import {RedirectTest} from './components/RedirectTest';
import {HomePage} from './pages/HomePage';

export default function App() {
  return (
    <CroctProvider appId="APPLICATION_ID">
      <RedirectTest>
        <HomePage />
      </RedirectTest>
    </CroctProvider>
  );
}
```

**Plug Vue**

**components/RedirectTest.vue**

```vue
<script setup>
import {watch} from 'vue';
import {useContent} from '@croct/plug-vue';

const {data, isLoading} = useContent('redirect');

watch(
  data,
  content => {
    if (content?.url) {
      location.replace(new URL(content.redirectUrl, location.href));
    }
  },
  {immediate: true},
);
</script>

<template>
  <slot v-if="!isLoading && !data?.redirectUrl" />
</template>
```

**src/App.vue**

```vue
<script setup>
import RedirectTest from './components/RedirectTest.vue';
import HomePage from './pages/HomePage.vue';
</script>

<template>
  <RedirectTest>
    <HomePage />
  </RedirectTest>
</template>
```

Three details make this safe. The page is revealed in every outcome, including errors, so in the rare event of a failed request the visitor still sees the original page. The SDK aborts the request after its [default timeout](/reference/sdk/javascript/api/plug/plug#configuration-defaultfetchtimeout-prop), so the page cannot stay hidden indefinitely. Calling `replace` instead of assigning the location keeps the original page out of the history, so the back button takes the visitor to where they came from instead of triggering the redirect again.

### Configure the experiment

The experiment lives in the platform, not in your code:

1. Open the [experiences page](https://app.croct.com/redirect/organizations/-organization-/workspaces/-workspace-/experiences) and create an experience targeting the audience you want to test, then open the **Slot** tab and select the `redirect` slot.

2. Open the **Experiment** tab, name it, and select the goal that represents the conversion you want to compare.

3. Allocate the traffic that enters the experiment.

4. Create two variants, such as "Current page" and "New page", and split the traffic evenly between them.

5. Open the **Content** tab and set the URL for each variant. Leave it empty for "Current page", and set the address of the new page for "New page".

6. Click **Publish** and launch the experience.

Only the variant that moves the visitor carries a URL. Visitors outside the audience get the empty [default content](/explanation/content/slot-default-content) and stay where they are, while visitors who are eligible but not allocated get the [experience's content](/explanation/content/experience-content), so leave that one empty as well unless you want them redirected too. For a niche audience, allocate 100% of the traffic so the experiment collects enough data to reach statistical validity.

### Track the conversion

The goal you select in the experiment must match a [`GoalCompleted`](/reference/event/types/engagement/goal-completed) event tracked in your application. Without it, the experiment collects exposures but no conversions.

Track it on both destinations, since the conversion can happen on either page. The goal decides the winner, and tracking the event that matches the conversion adds revenue, lead, and sign-up figures to the same comparison:

**Plug JS**

**Goal**

```js
import croct from '@croct/plug';

document.querySelector('#signup').addEventListener('click', () => {
  croct.track('goalCompleted', {
    goalId: 'sign-up',
  });
});
```

**Order**

```js
import croct from '@croct/plug';

document.querySelector('#signup').addEventListener('click', () => {
  croct.track('goalCompleted', {
    goalId: 'sign-up',
  });

  croct.track('orderPlaced', {
    order: {
      orderId: 'AXJH-1234',
      currency: 'USD',
      total: 899.99,
      items: [
        {
          index: 0,
          product: {
            productId: '12345',
            name: 'Black iPhone 12',
            displayPrice: 899.99,
          },
          quantity: 1,
          total: 899.99,
        },
      ],
    },
  });
});
```

**Lead**

```js
import croct from '@croct/plug';

document.querySelector('#signup').addEventListener('click', () => {
  croct.track('goalCompleted', {
    goalId: 'sign-up',
  });

  croct.track('leadGenerated', {
    leadId: '1ed2fd65-a027-4f3a-a35f-c6dd97537392',
    lead: {
      firstName: 'Carol',
      lastName: 'Doe',
      email: 'carol@croct.com',
    },
  });
});
```

**Sign-up**

```js
import croct from '@croct/plug';

document.querySelector('#signup').addEventListener('click', () => {
  croct.track('goalCompleted', {
    goalId: 'sign-up',
  });

  croct.track('userSignedUp', {
    userId: '1ed2fd65-a027-4f3a-a35f-c6dd97537392',
    profile: {
      firstName: 'Carol',
      lastName: 'Doe',
      email: 'carol@croct.com',
    },
  });
});
```

**Plug React**

**Goal**

```jsx
import {useCroct} from '@croct/plug-react';

export function SignupButton() {
  const croct = useCroct();

  function handleClick() {
    croct.track('goalCompleted', {
      goalId: 'sign-up',
    });
  }

  return <a href="/signup" onClick={handleClick}>Sign up for free</a>;
}
```

**Order**

```jsx
import {useCroct} from '@croct/plug-react';

export function SignupButton() {
  const croct = useCroct();

  function handleClick() {
    croct.track('goalCompleted', {
      goalId: 'sign-up',
    });

    croct.track('orderPlaced', {
      order: {
        orderId: 'AXJH-1234',
        currency: 'USD',
        total: 899.99,
        items: [
          {
            index: 0,
            product: {
              productId: '12345',
              name: 'Black iPhone 12',
              displayPrice: 899.99,
            },
            quantity: 1,
            total: 899.99,
          },
        ],
      },
    });
  }

  return <a href="/signup" onClick={handleClick}>Sign up for free</a>;
}
```

**Lead**

```jsx
import {useCroct} from '@croct/plug-react';

export function SignupButton() {
  const croct = useCroct();

  function handleClick() {
    croct.track('goalCompleted', {
      goalId: 'sign-up',
    });

    croct.track('leadGenerated', {
      leadId: '1ed2fd65-a027-4f3a-a35f-c6dd97537392',
      lead: {
        firstName: 'Carol',
        lastName: 'Doe',
        email: 'carol@croct.com',
      },
    });
  }

  return <a href="/signup" onClick={handleClick}>Sign up for free</a>;
}
```

**Sign-up**

```jsx
import {useCroct} from '@croct/plug-react';

export function SignupButton() {
  const croct = useCroct();

  function handleClick() {
    croct.track('goalCompleted', {
      goalId: 'sign-up',
    });

    croct.track('userSignedUp', {
      userId: '1ed2fd65-a027-4f3a-a35f-c6dd97537392',
      profile: {
        firstName: 'Carol',
        lastName: 'Doe',
        email: 'carol@croct.com',
      },
    });
  }

  return <a href="/signup" onClick={handleClick}>Sign up for free</a>;
}
```

**Plug Next**

**Goal**

```jsx
'use client';

import {useCroct} from '@croct/plug-next';

export function SignupButton() {
  const croct = useCroct();

  function handleClick() {
    croct.track('goalCompleted', {
      goalId: 'sign-up',
    });
  }

  return <a href="/signup" onClick={handleClick}>Sign up for free</a>;
}
```

**Order**

```jsx
'use client';

import {useCroct} from '@croct/plug-next';

export function SignupButton() {
  const croct = useCroct();

  function handleClick() {
    croct.track('goalCompleted', {
      goalId: 'sign-up',
    });

    croct.track('orderPlaced', {
      order: {
        orderId: 'AXJH-1234',
        currency: 'USD',
        total: 899.99,
        items: [
          {
            index: 0,
            product: {
              productId: '12345',
              name: 'Black iPhone 12',
              displayPrice: 899.99,
            },
            quantity: 1,
            total: 899.99,
          },
        ],
      },
    });
  }

  return <a href="/signup" onClick={handleClick}>Sign up for free</a>;
}
```

**Lead**

```jsx
'use client';

import {useCroct} from '@croct/plug-next';

export function SignupButton() {
  const croct = useCroct();

  function handleClick() {
    croct.track('goalCompleted', {
      goalId: 'sign-up',
    });

    croct.track('leadGenerated', {
      leadId: '1ed2fd65-a027-4f3a-a35f-c6dd97537392',
      lead: {
        firstName: 'Carol',
        lastName: 'Doe',
        email: 'carol@croct.com',
      },
    });
  }

  return <a href="/signup" onClick={handleClick}>Sign up for free</a>;
}
```

**Sign-up**

```jsx
'use client';

import {useCroct} from '@croct/plug-next';

export function SignupButton() {
  const croct = useCroct();

  function handleClick() {
    croct.track('goalCompleted', {
      goalId: 'sign-up',
    });

    croct.track('userSignedUp', {
      userId: '1ed2fd65-a027-4f3a-a35f-c6dd97537392',
      profile: {
        firstName: 'Carol',
        lastName: 'Doe',
        email: 'carol@croct.com',
      },
    });
  }

  return <a href="/signup" onClick={handleClick}>Sign up for free</a>;
}
```

**Plug Hydrogen**

**Goal**

```jsx
import {useCroct} from '@croct/plug-hydrogen';

export function SignupButton() {
  const croct = useCroct();

  function handleClick() {
    croct.track('goalCompleted', {
      goalId: 'sign-up',
    });
  }

  return <a href="/signup" onClick={handleClick}>Sign up for free</a>;
}
```

**Order**

```jsx
import {useCroct} from '@croct/plug-hydrogen';

export function SignupButton() {
  const croct = useCroct();

  function handleClick() {
    croct.track('goalCompleted', {
      goalId: 'sign-up',
    });

    croct.track('orderPlaced', {
      order: {
        orderId: 'AXJH-1234',
        currency: 'USD',
        total: 899.99,
        items: [
          {
            index: 0,
            product: {
              productId: '12345',
              name: 'Black iPhone 12',
              displayPrice: 899.99,
            },
            quantity: 1,
            total: 899.99,
          },
        ],
      },
    });
  }

  return <a href="/signup" onClick={handleClick}>Sign up for free</a>;
}
```

**Lead**

```jsx
import {useCroct} from '@croct/plug-hydrogen';

export function SignupButton() {
  const croct = useCroct();

  function handleClick() {
    croct.track('goalCompleted', {
      goalId: 'sign-up',
    });

    croct.track('leadGenerated', {
      leadId: '1ed2fd65-a027-4f3a-a35f-c6dd97537392',
      lead: {
        firstName: 'Carol',
        lastName: 'Doe',
        email: 'carol@croct.com',
      },
    });
  }

  return <a href="/signup" onClick={handleClick}>Sign up for free</a>;
}
```

**Sign-up**

```jsx
import {useCroct} from '@croct/plug-hydrogen';

export function SignupButton() {
  const croct = useCroct();

  function handleClick() {
    croct.track('goalCompleted', {
      goalId: 'sign-up',
    });

    croct.track('userSignedUp', {
      userId: '1ed2fd65-a027-4f3a-a35f-c6dd97537392',
      profile: {
        firstName: 'Carol',
        lastName: 'Doe',
        email: 'carol@croct.com',
      },
    });
  }

  return <a href="/signup" onClick={handleClick}>Sign up for free</a>;
}
```

**Plug Vue**

**Goal**

```vue
<script setup>
import {useCroct} from '@croct/plug-vue';

const croct = useCroct();

function handleClick() {
  croct.track('goalCompleted', {
    goalId: 'sign-up',
  });
}
</script>

<template>
  <a href="/signup" @click="handleClick">Sign up for free</a>
</template>
```

**Order**

```vue
<script setup>
import {useCroct} from '@croct/plug-vue';

const croct = useCroct();

function handleClick() {
  croct.track('goalCompleted', {
    goalId: 'sign-up',
  });

  croct.track('orderPlaced', {
    order: {
      orderId: 'AXJH-1234',
      currency: 'USD',
      total: 899.99,
      items: [
        {
          index: 0,
          product: {
            productId: '12345',
            name: 'Black iPhone 12',
            displayPrice: 899.99,
          },
          quantity: 1,
          total: 899.99,
        },
      ],
    },
  });
}
</script>

<template>
  <a href="/signup" @click="handleClick">Sign up for free</a>
</template>
```

**Lead**

```vue
<script setup>
import {useCroct} from '@croct/plug-vue';

const croct = useCroct();

function handleClick() {
  croct.track('goalCompleted', {
    goalId: 'sign-up',
  });

  croct.track('leadGenerated', {
    leadId: '1ed2fd65-a027-4f3a-a35f-c6dd97537392',
    lead: {
      firstName: 'Carol',
      lastName: 'Doe',
      email: 'carol@croct.com',
    },
  });
}
</script>

<template>
  <a href="/signup" @click="handleClick">Sign up for free</a>
</template>
```

**Sign-up**

```vue
<script setup>
import {useCroct} from '@croct/plug-vue';

const croct = useCroct();

function handleClick() {
  croct.track('goalCompleted', {
    goalId: 'sign-up',
  });

  croct.track('userSignedUp', {
    userId: '1ed2fd65-a027-4f3a-a35f-c6dd97537392',
    profile: {
      firstName: 'Carol',
      lastName: 'Doe',
      email: 'carol@croct.com',
    },
  });
}
</script>

<template>
  <a href="/signup" @click="handleClick">Sign up for free</a>
</template>
```

**Plug Nuxt**

**Goal**

```vue
<script setup>
import {useCroct} from '@croct/plug-nuxt';

const croct = useCroct();

function handleClick() {
  croct.track('goalCompleted', {
    goalId: 'sign-up',
  });
}
</script>

<template>
  <a href="/signup" @click="handleClick">Sign up for free</a>
</template>
```

**Order**

```vue
<script setup>
import {useCroct} from '@croct/plug-nuxt';

const croct = useCroct();

function handleClick() {
  croct.track('goalCompleted', {
    goalId: 'sign-up',
  });

  croct.track('orderPlaced', {
    order: {
      orderId: 'AXJH-1234',
      currency: 'USD',
      total: 899.99,
      items: [
        {
          index: 0,
          product: {
            productId: '12345',
            name: 'Black iPhone 12',
            displayPrice: 899.99,
          },
          quantity: 1,
          total: 899.99,
        },
      ],
    },
  });
}
</script>

<template>
  <a href="/signup" @click="handleClick">Sign up for free</a>
</template>
```

**Lead**

```vue
<script setup>
import {useCroct} from '@croct/plug-nuxt';

const croct = useCroct();

function handleClick() {
  croct.track('goalCompleted', {
    goalId: 'sign-up',
  });

  croct.track('leadGenerated', {
    leadId: '1ed2fd65-a027-4f3a-a35f-c6dd97537392',
    lead: {
      firstName: 'Carol',
      lastName: 'Doe',
      email: 'carol@croct.com',
    },
  });
}
</script>

<template>
  <a href="/signup" @click="handleClick">Sign up for free</a>
</template>
```

**Sign-up**

```vue
<script setup>
import {useCroct} from '@croct/plug-nuxt';

const croct = useCroct();

function handleClick() {
  croct.track('goalCompleted', {
    goalId: 'sign-up',
  });

  croct.track('userSignedUp', {
    userId: '1ed2fd65-a027-4f3a-a35f-c6dd97537392',
    profile: {
      firstName: 'Carol',
      lastName: 'Doe',
      email: 'carol@croct.com',
    },
  });
}
</script>

<template>
  <a href="/signup" @click="handleClick">Sign up for free</a>
</template>
```

**Plug PHP**

**Goal**

```php
<a id="signup" href="/signup">Sign up for free</a>

<script>
  document.getElementById('signup').addEventListener('click', () => {
    croct.track('goalCompleted', {
      goalId: 'sign-up',
    });
  });
</script>
```

**Order**

```php
<a id="signup" href="/signup">Sign up for free</a>

<script>
  document.getElementById('signup').addEventListener('click', () => {
    croct.track('goalCompleted', {
      goalId: 'sign-up',
    });

    croct.track('orderPlaced', {
      order: {
        orderId: 'AXJH-1234',
        currency: 'USD',
        total: 899.99,
        items: [
          {
            index: 0,
            product: {
              productId: '12345',
              name: 'Black iPhone 12',
              displayPrice: 899.99,
            },
            quantity: 1,
            total: 899.99,
          },
        ],
      },
    });
  });
</script>
```

**Lead**

```php
<a id="signup" href="/signup">Sign up for free</a>

<script>
  document.getElementById('signup').addEventListener('click', () => {
    croct.track('goalCompleted', {
      goalId: 'sign-up',
    });

    croct.track('leadGenerated', {
      leadId: '1ed2fd65-a027-4f3a-a35f-c6dd97537392',
      lead: {
        firstName: 'Carol',
        lastName: 'Doe',
        email: 'carol@croct.com',
      },
    });
  });
</script>
```

**Sign-up**

```php
<a id="signup" href="/signup">Sign up for free</a>

<script>
  document.getElementById('signup').addEventListener('click', () => {
    croct.track('goalCompleted', {
      goalId: 'sign-up',
    });

    croct.track('userSignedUp', {
      userId: '1ed2fd65-a027-4f3a-a35f-c6dd97537392',
      profile: {
        firstName: 'Carol',
        lastName: 'Doe',
        email: 'carol@croct.com',
      },
    });
  });
</script>
```

**Plug Symfony**

**Goal**

```twig
<a id="signup" href="/signup">Sign up for free</a>

{% apply croct %}
document.getElementById('signup').addEventListener('click', () => {
    croct.track('goalCompleted', {
      goalId: 'sign-up',
    });
});
{% endapply %}
```

**Order**

```twig
<a id="signup" href="/signup">Sign up for free</a>

{% apply croct %}
document.getElementById('signup').addEventListener('click', () => {
    croct.track('goalCompleted', {
      goalId: 'sign-up',
    });

    croct.track('orderPlaced', {
      order: {
        orderId: 'AXJH-1234',
        currency: 'USD',
        total: 899.99,
        items: [
          {
            index: 0,
            product: {
              productId: '12345',
              name: 'Black iPhone 12',
              displayPrice: 899.99,
            },
            quantity: 1,
            total: 899.99,
          },
        ],
      },
    });
});
{% endapply %}
```

**Lead**

```twig
<a id="signup" href="/signup">Sign up for free</a>

{% apply croct %}
document.getElementById('signup').addEventListener('click', () => {
    croct.track('goalCompleted', {
      goalId: 'sign-up',
    });

    croct.track('leadGenerated', {
      leadId: '1ed2fd65-a027-4f3a-a35f-c6dd97537392',
      lead: {
        firstName: 'Carol',
        lastName: 'Doe',
        email: 'carol@croct.com',
      },
    });
});
{% endapply %}
```

**Sign-up**

```twig
<a id="signup" href="/signup">Sign up for free</a>

{% apply croct %}
document.getElementById('signup').addEventListener('click', () => {
    croct.track('goalCompleted', {
      goalId: 'sign-up',
    });

    croct.track('userSignedUp', {
      userId: '1ed2fd65-a027-4f3a-a35f-c6dd97537392',
      profile: {
        firstName: 'Carol',
        lastName: 'Doe',
        email: 'carol@croct.com',
      },
    });
});
{% endapply %}
```

**Plug Laravel**

**Goal**

```blade
<a id="signup" href="/signup">Sign up for free</a>

@croct
document.getElementById('signup').addEventListener('click', () => {
    croct.track('goalCompleted', {
      goalId: 'sign-up',
    });
});
@endcroct
```

**Order**

```blade
<a id="signup" href="/signup">Sign up for free</a>

@croct
document.getElementById('signup').addEventListener('click', () => {
    croct.track('goalCompleted', {
      goalId: 'sign-up',
    });

    croct.track('orderPlaced', {
      order: {
        orderId: 'AXJH-1234',
        currency: 'USD',
        total: 899.99,
        items: [
          {
            index: 0,
            product: {
              productId: '12345',
              name: 'Black iPhone 12',
              displayPrice: 899.99,
            },
            quantity: 1,
            total: 899.99,
          },
        ],
      },
    });
});
@endcroct
```

**Lead**

```blade
<a id="signup" href="/signup">Sign up for free</a>

@croct
document.getElementById('signup').addEventListener('click', () => {
    croct.track('goalCompleted', {
      goalId: 'sign-up',
    });

    croct.track('leadGenerated', {
      leadId: '1ed2fd65-a027-4f3a-a35f-c6dd97537392',
      lead: {
        firstName: 'Carol',
        lastName: 'Doe',
        email: 'carol@croct.com',
      },
    });
});
@endcroct
```

**Sign-up**

```blade
<a id="signup" href="/signup">Sign up for free</a>

@croct
document.getElementById('signup').addEventListener('click', () => {
    croct.track('goalCompleted', {
      goalId: 'sign-up',
    });

    croct.track('userSignedUp', {
      userId: '1ed2fd65-a027-4f3a-a35f-c6dd97537392',
      profile: {
        firstName: 'Carol',
        lastName: 'Doe',
        email: 'carol@croct.com',
      },
    });
});
@endcroct
```

**Plug Drupal**

**Goal**

```twig
<a id="signup" href="/signup">Sign up for free</a>

{% apply croct %}
document.getElementById('signup').addEventListener('click', () => {
    croct.track('goalCompleted', {
      goalId: 'sign-up',
    });
});
{% endapply %}
```

**Order**

```twig
<a id="signup" href="/signup">Sign up for free</a>

{% apply croct %}
document.getElementById('signup').addEventListener('click', () => {
    croct.track('goalCompleted', {
      goalId: 'sign-up',
    });

    croct.track('orderPlaced', {
      order: {
        orderId: 'AXJH-1234',
        currency: 'USD',
        total: 899.99,
        items: [
          {
            index: 0,
            product: {
              productId: '12345',
              name: 'Black iPhone 12',
              displayPrice: 899.99,
            },
            quantity: 1,
            total: 899.99,
          },
        ],
      },
    });
});
{% endapply %}
```

**Lead**

```twig
<a id="signup" href="/signup">Sign up for free</a>

{% apply croct %}
document.getElementById('signup').addEventListener('click', () => {
    croct.track('goalCompleted', {
      goalId: 'sign-up',
    });

    croct.track('leadGenerated', {
      leadId: '1ed2fd65-a027-4f3a-a35f-c6dd97537392',
      lead: {
        firstName: 'Carol',
        lastName: 'Doe',
        email: 'carol@croct.com',
      },
    });
});
{% endapply %}
```

**Sign-up**

```twig
<a id="signup" href="/signup">Sign up for free</a>

{% apply croct %}
document.getElementById('signup').addEventListener('click', () => {
    croct.track('goalCompleted', {
      goalId: 'sign-up',
    });

    croct.track('userSignedUp', {
      userId: '1ed2fd65-a027-4f3a-a35f-c6dd97537392',
      profile: {
        firstName: 'Carol',
        lastName: 'Doe',
        email: 'carol@croct.com',
      },
    });
});
{% endapply %}
```

See [Track events](/immersion/guides/tracking-events) for the full list of events and what each one adds to the analysis.

> **Keep both pages on the same domain**
>
> The visitor is recognized per domain, so a redirect to a different domain starts a new anonymous visitor whose conversion is never attributed to the variant. For a destination on a subdomain, share the session first, as described in [Identify users across subdomains](/immersion/tutorials/cross-subdomain-identification).

If your application authenticates users, the assignment follows the identified visitor across devices instead of being tied to a single browser. See [Experiment with feature flags](/immersion/tutorials/experiment-feature-flagging) for how identity feeds the assignment.

### Review and publish

Before you let the experiment run, confirm that both paths behave as expected:

1. **Preview each variant**

   Use [preview mode](/explanation/content/preview) to check that the variant with a URL redirects and the one without it renders the page untouched.

2. **Follow the redirect once**

   Land on the destination page and reload it. If you end up somewhere else, the destination is resolving the slot too, and the loop needs to be broken before publishing.

3. **Check the exposure count**

   In the [experiment dashboard](/reference/analytics/experiment/overview), confirm that the split between variants matches your configuration. A persistent imbalance means visitors are dropping out during the redirect, and no other metric is worth reading until that is resolved.

4. **Confirm that goals are firing**

   Complete the conversion on both pages and check that the event appears with the correct goal.

## Analyze the results

Once the data starts coming in, compare the variants in the [experiment dashboard](/reference/analytics/experiment/overview) and read the [performance per goal](/reference/analytics/experiment/widgets/performance-per-goal) widget to see which page converts better.

If you redirect in the browser, read the numbers with the cost of the mask in mind. The redirected group waits for the decision and then loads a second page, so part of any difference reflects the wait rather than the page itself. A new page that wins despite that handicap is a strong result, and one that loses narrowly deserves a second look at how much of the gap the delay explains. A server-side redirect costs the visitor a single round trip, so it does not skew the comparison this way.

When you have a winner, apply it for good. Point the original URL at the winning page, or move its content into the page you already serve, and stop the experiment. The slot goes back to its empty default, so no visitor is redirected while you clean up.

## Explore

- [Experiments](/explanation/experiment): Understand how experiments allocate traffic and pick a winner.
- [Analytics](/reference/analytics/experiment/overview): Understand how your experiments perform across key metrics.
