All Articles

Updating GitLab's Pricing Page

mockup of pricing page

Building a Dynamic Pricing Page for GitLab’s Website

As a front-end developer working on GitLab’s Digital Experience(web marketing) team, I recently completed work on a new pricing page implementation. Our team is in the process of migrating from a Nuxt 2 to Nuxt 3 webstack, so this project provided a key opporunity to migrate this page over. My responsibilities include migrating the exiting page, and updating it for our upcoming GitLab 18 annual major release. This project involved taking GitLab’s existing Vue 2 page, and migrating it to using the Composition API. Let me walk through the key aspects of this process!

Project Overview

The goals:

  • Provides interactive elements for users to explore pricing options
  • Adapts to different deployment types (SaaS/self-managed/Decidated)
  • Is fully responsive across all device sizes
  • Maintains strong SEO and accessibility compliance

Technical Implementation

State Management using the useState Compostable

One of the key features of our pricing page is the ability to switch between different deployment models (SaaS vs. self-managed).

// Tooltip.vue
<script lang="ts" setup>
  // if exists, set application state based on query parameters on page load
  // for example, `/pricing/?deployment=saas`
  const activeDeployment = useState<string>('active-deployment', () => {
    return (route.query.deployment as string) || 'saas';
  });

  function onChange(index: number, id: string): void {
    activeDeployment.value = id;
    router.push({
      query: {
        ...route.query,
        deployment: id,
      },
    });
  watch(
    () => route.query.deployment,
    (newDeployment) => {
      if (newDeployment) {
        activeDeployment.value = newDeployment as string;
      }
    },
    { immediate: true },
  );
}
</script>
<template>
  <div class="pricing-toggle">
    <div class="pricing-toggle__toggle">
      <div
        v-for="(tier, index) in props.tooltips"
        :key="tier.config.id"
        :class="{
          'pricing-toggle__button': true,
          'pricing-toggle__button--active': activeTypeIndex === index,
        }"
        @click="onChange(index, tier.config.id)"
      >
        <SlpTypography variant="body2">{{ tier.label }}</SlpTypography>
      </div>
    </div>
  </div>
</template>

TypeScript Interfaces for Pricing Data

This page has complex business logic that is hard to keep track of when managing a 1500 line YML file. This helps catch errors in incorrect YML formatting, failing CI builds if the improper YML is supplied.

// components/pricing/types.ts
export interface PricingTier {
  header: string;
  description: string;
  price: PricingPrice | string;
  buttons?: BaseLink[];
  features: PricingList;
  config: {
    id: string;
    promo: string;
  };
}

export interface PricingDeployment {
  tooltip: PricingTooltip;
  tiers: PricingTier[];
  email?: {
    placeholder: string;
    primaryCTA: CTALink;
    secondaryCTA: CTALink;
  };
}

Layout Adaptations for Mobile Devices

The mobile experience needed special handling. I created a dedicated layout component that adjusts the display of certain elements:

// layouts/hide-mobile-free-trial.vue
<script setup lang="ts">
provide('HideMobileFreeTrial', true);
</script>

<template>
  <div class="grid-wrapper">
    <!-- Hides mobile CTA for pages that inherit this layout -->
    <NavigationMainNavigation v-if="navigationData" :navigation-data="navigationData?.data" />
    <NavigationBanner v-if="bannerData" v-bind="bannerData" class="banner" />
    <slot />
    <NavigationFooter :footer-data="footerData?.data" />
  </div>
</template>

SEO Enhancements

I improved the SEO for the pricing page by adding schema.org markup for product information on the pricing page, while providing defaults for other pages:

let productSchema = {
  name: 'GitLab DevSecOps Platform',
  description:
    'The most comprehensive AI-powered DevSecOps Platform that enables organizations to develop, secure, and operate software in a single application.',
  image:
    'https://images.ctfassets.net/xz1dnu24egyd/1hnQd13UBU7n5V0RsJcbP3/769692e40a6d528e334b84f079c1f577/gitlab-logo-100.png',
  brand: {
    '@type': 'Corporation',
    name: 'GitLab',
    logo: 'https://images.ctfassets.net/xz1dnu24egyd/KpJoqcRLUFBL1AqKt9x0R/4fd439c21cecca4881106dd0069aa39c/gitlab-logo-extra-whitespace.png',
  },
};

if (seoConfig?.config?.schema) {
    productSchema = {
      ...productSchema,
      ...seoConfig?.config?.schema,
    };
  }

const schemaOrgs: Array<object> = [defineProduct(productSchema)];

useSchemaOrg(schemaOrgs);

Challenges (and Solutions)

Different deployment types (SaaS, self-managed) with complex business logic

Since we expect this page to get updated frequently, and price offerings to shift, allowing state to be controlled by YAML using a key called config.deployments make it easy to know which components to display based on the current activeDeployment.

  componentName: PricingCard
  componentContent: 
    config: 
      deployments: ['self-managed'] # will only display this component when activeDeployment exists within this list

The same could be done in Vue templates if necessary:

<section v-if="activeDeployment === 'saas'">
  Display SaaS content :fire:
</section>
<SlpButton v-else>
  When not on SaaS, this will be shown instead
</SlpButton>

Confidentiality in a public repo

We had a whole team of engineers making updates to various different pages to align with the release. With some changes being confidential, we needed to create a private fork to manage this requirement.

This was exasperated as newly created Vue components needed to be shared between engineers in a forked repository and public repository. While creating duplicate components was not ideal, it allowed us to implement a similar component multiple times, which would minimize merge conflicts and allow us to meet our due dates. Additionally, this informs what functionality should be shared among a single component, or when composition of smaller components made more sense.

Conclusions and future considerations

At the start, I briefly considered using presentational components where a smart container would write and resolve changes in state, where presentational components read that data passed down. While I didn’t apply this concept originally due to time constraints and fearing overengineering, this could be applied to a future iteration.

The page is now live! If you have any feedback, respond in the issue.

Published May 15, 2025

Thoughts about techology, and other things.