AB testing skill based landing page top (#60753)

Co-authored-by: Mrugesh Mohapatra <1884376+raisedadead@users.noreply.github.com>
This commit is contained in:
Ahmad Abdolsaheb
2025-06-10 07:40:57 +03:00
committed by GitHub
parent 276cea53a9
commit 339e09961f
8 changed files with 246 additions and 227 deletions
@@ -5,17 +5,17 @@
"aa-test-in-component": {
"defaultValue": false
},
"landing-page-redesign": {
"landing-top-skill-focused": {
"defaultValue": false,
"rules": [
{
"coverage": 1,
"hashAttribute": "id",
"seed": "landing-page-redesign",
"seed": "landing-top-skill-focused",
"hashVersion": 2,
"variations": [false, true],
"weights": [0.5, 0.5],
"key": "landing-page-redesign",
"key": "landing-top-skill-focused",
"meta": [
{
"key": "0",
@@ -114,12 +114,12 @@
},
"landing": {
"big-heading-1": "Learn to code — for free.",
"big-heading-1-b": "Learn to code.",
"big-heading-2": "Build projects.",
"big-heading-1-b": "Build Your Skills for Free.",
"big-heading-3": "Earn certifications.",
"big-heading-4": "All for free.",
"h2-heading": "Since 2014, more than 40,000 freeCodeCamp.org graduates have gotten jobs at tech companies including:",
"h2-heading-b": "More than <strong>100,000</strong> freeCodeCamp.org graduates have gotten <strong>jobs</strong> at tech companies including:",
"advance-career": "Advance your career by learning in-demand skills in Programming, DevOps, Cybersecurity, AI Engineering, and English for Developers.",
"h2-heading": "More than <strong>100,000</strong> freeCodeCamp.org graduates have gotten <strong>jobs</strong> at tech companies including:",
"graduates-work": "More than <strong>100,000</strong> freeCodeCamp graduates work in companies such as",
"hero-img-description": "freeCodeCamp students at a local study group in South Korea.",
"hero-img-alt": "A group of people, including a White man, a Black woman, and an Asian woman, gathered around a laptop.",
"hero-img-uis": "A group of screenshots showing the freeCodeCamp editor interface on both a mobile and desktop device and a certification.",
@@ -14,79 +14,69 @@ import {
import BigCallToAction from './big-call-to-action';
import CampersImage from './campers-image';
const LogoRow = (): JSX.Element => {
function LandingTop(): JSX.Element {
const { t } = useTranslation();
const showChineseLogos = ['chinese', 'chinese-tradition'].includes(
clientLocale
);
return (
<>
<p
className='logo-row-title'
data-playwright-test-label='landing-h2-heading-b'
>
<Trans>landing.h2-heading-b</Trans>
</p>
<div
className='logo-row'
data-playwright-test-label='brand-logo-container'
>
<AppleLogo />
<GoogleLogo />
<MicrosoftLogo />
{showChineseLogos ? (
<>
<TencentLogo />
<AlibabaLogo />
</>
) : (
<>
<SpotifyLogo />
<AmazonLogo />
</>
)}
</div>
</>
);
};
function LandingTop(): JSX.Element {
const { t } = useTranslation();
return (
<Container fluid={true} className='gradient-container'>
<Container className='landing-top landing-top-b'>
<Spacer size='m' />
<Row>
<Col lg={8} lgOffset={2} sm={10} smOffset={1} xs={12}>
<Container
fluid={true}
className='landing-top lading-top-c gradient-container'
>
<Container>
<Row className='landing-top-two-column'>
<Spacer size='m' />
<Col className='landing-top-left'>
<h1
id='content-start'
className='mega-heading'
className='ultra-heading'
data-test-label='landing-header'
data-playwright-test-label='landing-big-heading-1'
data-playwright-test-label='big-heading-1-b'
>
{t('landing.big-heading-1')}
{t('landing.big-heading-1-b')}
</h1>
<p
className='mega-heading'
data-playwright-test-label='landing-big-heading-2'
>
{t('landing.big-heading-2')}
<p data-playwright-test-label='advance-career'>
{t('landing.advance-career')}
</p>
<p
className='mega-heading'
data-playwright-test-label='landing-big-heading-3'
>
{t('landing.big-heading-3')}
</p>
<LogoRow />
<Spacer size='m' />
<BigCallToAction />
<Spacer size='m' />
</Col>
<Col className='landing-top-right'>
<CampersImage />
<Spacer size='m' />
</Col>
</Row>
</Container>
<Container>
<Row>
<Col lg={8} lgOffset={2} sm={10} smOffset={1} xs={12}>
<CampersImage />
<Col sm={10} smOffset={1} xs={12} className='brands-container'>
<Spacer size='l' />
<p data-playwright-test-label='graduates-work'>
<Trans>landing.graduates-work</Trans>
</p>
<Spacer size='s' />
<div
className='logo-row'
data-playwright-test-label='brand-logo-container'
>
<AppleLogo />
<GoogleLogo />
<MicrosoftLogo />
{showChineseLogos ? (
<>
<TencentLogo />
<AlibabaLogo />
</>
) : (
<>
<SpotifyLogo />
<AmazonLogo />
</>
)}
</div>
<Spacer size='m' />
</Col>
</Row>
@@ -1,7 +1,6 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { Trans, useTranslation } from 'react-i18next';
import { Container, Col, Row, Spacer } from '@freecodecamp/ui';
import { clientLocale } from '../../../../config/env.json';
import {
AmazonLogo,
@@ -15,64 +14,80 @@ import {
import BigCallToAction from './big-call-to-action';
import CampersImage from './campers-image';
function LandingTop(): JSX.Element {
const { t } = useTranslation();
const LogoRow = (): JSX.Element => {
const showChineseLogos = ['chinese', 'chinese-tradition'].includes(
clientLocale
);
return (
<Container className='landing-top landing-top-a'>
<Row>
<>
<p className='logo-row-title' data-playwright-test-label='h2-heading'>
<Trans>landing.h2-heading</Trans>
</p>
<div
className='logo-row'
data-playwright-test-label='brand-logo-container'
>
<AppleLogo />
<GoogleLogo />
<MicrosoftLogo />
{showChineseLogos ? (
<>
<TencentLogo />
<AlibabaLogo />
</>
) : (
<>
<SpotifyLogo />
<AmazonLogo />
</>
)}
</div>
</>
);
};
function LandingTop(): JSX.Element {
const { t } = useTranslation();
return (
<Container fluid={true} className='gradient-container'>
<Container className='landing-top'>
<Spacer size='m' />
<Col lg={8} lgOffset={2} sm={10} smOffset={1} xs={12}>
<h1
id='content-start'
className='big-heading'
data-test-label='landing-header'
data-playwright-test-label='landing-big-heading-1'
>
{t('landing.big-heading-1')}
</h1>
<p
className='big-heading'
data-playwright-test-label='landing-big-heading-2'
>
{t('landing.big-heading-2')}
</p>
<p
className='big-heading'
data-playwright-test-label='landing-big-heading-3'
>
{t('landing.big-heading-3')}
</p>
<p data-playwright-test-label='landing-h2-heading'>
{t('landing.h2-heading')}
</p>
<div
className='logo-row'
data-playwright-test-label='brand-logo-container'
>
<AppleLogo />
<GoogleLogo />
<MicrosoftLogo />
{showChineseLogos ? (
<>
<TencentLogo />
<AlibabaLogo />
</>
) : (
<>
<SpotifyLogo />
<AmazonLogo />
</>
)}
</div>
<Spacer size='m' />
<BigCallToAction />
<CampersImage />
<Spacer size='m' />
</Col>
</Row>
<Row>
<Col lg={8} lgOffset={2} sm={10} smOffset={1} xs={12}>
<h1
id='content-start'
className='mega-heading'
data-test-label='landing-header'
data-playwright-test-label='landing-big-heading-1'
>
{t('landing.big-heading-1')}
</h1>
<p
className='mega-heading'
data-playwright-test-label='landing-big-heading-2'
>
{t('landing.big-heading-2')}
</p>
<p
className='mega-heading'
data-playwright-test-label='landing-big-heading-3'
>
{t('landing.big-heading-3')}
</p>
<LogoRow />
<Spacer size='m' />
<BigCallToAction />
</Col>
</Row>
<Row>
<Col lg={8} lgOffset={2} sm={10} smOffset={1} xs={12}>
<CampersImage />
<Spacer size='m' />
</Col>
</Row>
</Container>
</Container>
);
}
@@ -1,27 +0,0 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { Spacer } from '@freecodecamp/ui';
import Media from 'react-responsive';
import landingPageb from '../../../assets/images/landing/landing-page-b.svg';
import { LazyImage } from '../../helpers';
const LARGE_SCREEN_SIZE = 1200;
function UIImages(): JSX.Element {
const { t } = useTranslation();
return (
<Media minWidth={LARGE_SCREEN_SIZE}>
<figure
className='ui-images'
data-playwright-test-label='landing-page-figure'
>
<LazyImage alt={t('landing.hero-img-uis')} src={landingPageb} />
</figure>
<Spacer size='xl' />
</Media>
);
}
UIImages.displayName = 'UIImages';
export default UIImages;
+65 -7
View File
@@ -98,7 +98,6 @@ figcaption.caption {
.landing-top p {
font-size: 1.2rem;
font-weight: bold;
margin: 0.6rem 0;
}
@@ -218,33 +217,33 @@ figcaption.caption {
}
/* AB testing styles */
.landing-page-b .mega-heading {
.landing-top .mega-heading {
font-size: 2.2rem;
margin: 0px 0px 2rem;
font-weight: 700;
line-height: 2rem;
}
.landing-page-b .landing-top .btn-cta-big {
.landing-top .btn-cta-big {
max-width: fit-content;
padding: 12px 20px;
}
@media (min-width: 500px) {
.landing-page-b .mega-heading {
.landing-top .mega-heading {
font-size: 3rem;
line-height: 2.5rem;
}
.landing-page-b .landing-top .btn-cta-big {
.landing-top .btn-cta-big {
padding: 12px 40px;
}
}
.landing-page-b .logo-row-title {
.logo-row-title {
font-weight: normal;
}
.landing-page-b {
.landing-page {
overflow-x: hidden;
}
@@ -297,3 +296,62 @@ figure.ui-images img {
.benefits-container p {
color: var(--gray-15);
}
/* AB testing styles */
.landing-top-two-column {
display: flex;
flex-direction: column;
justify-content: center;
width: 100%;
gap: 20px;
}
.landing-top-two-column p {
font-size: 1.3rem;
font-weight: normal;
line-height: 1.5;
}
@media (min-width: 1200px) {
.landing-top-two-column {
flex-direction: row;
align-items: center;
}
.landing-top-two-column .landing-top-left {
flex: 0 0 50%;
max-width: 50%;
z-index: 1;
}
.landing-top-two-column .landing-top-right {
flex: 0 1 auto;
min-width: 0;
overflow: visible;
}
}
.landing-top .landing-top-two-column figure,
.landing-top .landing-top-two-column figure img {
margin-left: -100px;
margin-top: 0px;
height: 550px;
width: 850px;
max-width: none;
-webkit-mask-image: linear-gradient(to right, transparent 0%, black 20%);
mask-image: linear-gradient(to right, transparent 0%, black 20%);
}
.landing-top .ultra-heading {
font-size: 4rem;
}
.landing-top-right figcaption {
display: none;
}
.landing-top .brands-container p {
color: var(--quaternary-color);
font-weight: normal;
text-align: center;
}
+6 -21
View File
@@ -3,9 +3,8 @@ import { useTranslation } from 'react-i18next';
import { useGrowthBook } from '@growthbook/growthbook-react';
import SEO from '../components/seo';
import { Loader } from '../components/helpers';
import LandingTop from '../components/landing/components/landing-top';
import LandingTopB from '../components/landing/components/landing-top-b';
import AsSeenIn from '../components/landing/components/as-seen-in';
import LandingTop from '../components/landing/components/landing-top';
import Testimonials from '../components/landing/components/testimonials';
import Certifications from '../components/landing/components/certifications';
import Faq from '../components/landing/components/faq';
@@ -14,19 +13,12 @@ import '../components/landing/landing.css';
type LandingProps = {
showLandingPageRedesign: boolean;
showBenefitsSection: boolean;
};
const Landing = ({
showLandingPageRedesign,
showBenefitsSection
}: LandingProps) => (
<main
className={`landing-page ${showLandingPageRedesign ? 'landing-page-b' : ''}`}
>
const Landing = ({ showLandingPageRedesign }: LandingProps) => (
<main className={`landing-page`}>
{showLandingPageRedesign ? <LandingTopB /> : <LandingTop />}
{showBenefitsSection ? <Benefits /> : <AsSeenIn />}
<Benefits />
<Testimonials />
<Certifications />
<Faq />
@@ -38,21 +30,14 @@ function IndexPage(): JSX.Element {
const growthbook = useGrowthBook();
if (growthbook && growthbook.ready) {
const showLandingPageRedesign = growthbook.getFeatureValue(
'landing-page-redesign',
false
);
const showBenefitsSection = growthbook.getFeatureValue(
'show-benefits',
'landing-top-skill-focused',
false
);
growthbook.getFeatureValue('landing-aa-test', false);
return (
<>
<SEO title={t('metaTags:title')} />
<Landing
showLandingPageRedesign={showLandingPageRedesign}
showBenefitsSection={showBenefitsSection}
/>
<Landing showLandingPageRedesign={showLandingPageRedesign} />
</>
);
} else {
+50 -52
View File
@@ -51,70 +51,32 @@ test.describe('Landing Top - Variation B', () => {
await goToLandingPage(page);
});
test('The supporting copy renders correctly', async ({ page }) => {
const landingH2Heading = page.getByTestId('landing-h2-heading-b');
await expect(landingH2Heading).toHaveText(
translations.landing['h2-heading-b'].replace(/<\/?strong>/g, '')
test('Main heading copy renders correctly', async ({ page }) => {
const bigHeading = page.getByTestId('big-heading-1-b');
await expect(bigHeading).toHaveText(
translations.landing['big-heading-1-b']
);
});
});
test.describe('Second section - Variation B', () => {
test('The component Why learn with freeCodeCamp renders correctly', async ({
context,
page
}) => {
await addGrowthbookCookie({ context, variation: 'C' });
await goToLandingPage(page);
const h2Element = page.locator(
`h2:has-text("${translations.landing.benefits['heading']}")`
test('Supporting copy renders correctly', async ({ page }) => {
const bigHeading = page.getByTestId('advance-career');
await expect(bigHeading).toHaveText(translations.landing['advance-career']);
});
test('Logo row copy renders correctly', async ({ page }) => {
const landingH2Heading = page.getByTestId('graduates-work');
await expect(landingH2Heading).toHaveText(
translations.landing['graduates-work'].replace(/<\/?strong>/g, '')
);
await expect(h2Element).toBeVisible();
});
});
test.describe('Landing Top - Variation A', () => {
test.beforeEach(async ({ context, page }) => {
await addGrowthbookCookie({ context, variation: 'A' });
await addGrowthbookCookie({ context, variation: 'newA' });
await goToLandingPage(page);
});
test('The supporting copy renders correctly', async ({ page }) => {
const landingH2Heading = page.getByTestId('landing-h2-heading');
await expect(landingH2Heading).toHaveText(
translations.landing['h2-heading']
);
});
});
test.describe('Second section - Variation A', () => {
test('The component As Seen renders correctly', async ({ context, page }) => {
await addGrowthbookCookie({ context, variation: 'E' });
await goToLandingPage(page);
const h2Element = page.locator(
`h2:has-text("${translations.landing['as-seen-in']}")`
);
await expect(h2Element).toBeVisible();
});
});
test.describe('Landing Page', () => {
test.beforeEach(async ({ page }) => {
await goToLandingPage(page);
});
test('Call to action buttons should render correctly', async ({ page }) => {
const ctas = page.getByRole('link', {
name: translations.buttons['logged-in-cta-btn']
});
await expect(ctas).toHaveCount(4);
for (const cta of await ctas.all()) {
await expect(cta).toBeVisible();
}
});
test('The headline renders correctly', async ({ page }) => {
const landingHeading1 = page.getByTestId('landing-big-heading-1');
await expect(landingHeading1).toHaveText(
@@ -132,6 +94,42 @@ test.describe('Landing Page', () => {
);
});
test('Logo row copy renders correctly', async ({ page }) => {
const landingH2Heading = page.getByTestId('h2-heading');
await expect(landingH2Heading).toHaveText(
translations.landing['h2-heading'].replace(/<\/?strong>/g, '')
);
});
});
test.describe('Landing Page', () => {
test.beforeEach(async ({ page }) => {
await goToLandingPage(page);
});
test('The component Why learn with freeCodeCamp renders correctly', async ({
context,
page
}) => {
await addGrowthbookCookie({ context, variation: 'C' });
await goToLandingPage(page);
const h2Element = page.locator(
`h2:has-text("${translations.landing.benefits['heading']}")`
);
await expect(h2Element).toBeVisible();
});
test('Call to action buttons should render correctly', async ({ page }) => {
const ctas = page.getByRole('link', {
name: translations.buttons['logged-in-cta-btn']
});
await expect(ctas).toHaveCount(4);
for (const cta of await ctas.all()) {
await expect(cta).toBeVisible();
}
});
test('Hero image should have an alt and a description', async ({
isMobile,
page