docs: add cloud plans & pricing page (#13420)

* docs: add cloud plans & pricing page

* fix buttons

* simplify condition

* remove test frontmatter

* design fixes and changes

* styling fixes

* change node version for tests

* fix build error

* fix tests

* fix github pipeline
This commit is contained in:
Shahed Nasser authored and GitHub committed 2025-09-11 18:18:12 +03:00
1 parent 69b92681b9
commit 3a7a2571a6
17 files changed
+767 -14

No files matched your search

+1 -1
View File
@@ -36,7 +36,7 @@ Yes, your code is hosted on your GitHub repository. You can also export a dump o
## Can I deploy multiple projects on Cloud?
Yes, if your plan supports multiple projects. Learn more in the [Pricing](https://medusajs.com/pricing/) page.
Yes, if your plan supports multiple projects. Learn more in the [Plans & Pricing](../pricing/page.mdx) guide.
---
+1 -1
View File
@@ -119,7 +119,7 @@ In this window, you can view the members of the organization, their roles, and t
Based on your organization's plan, you may have a limited number of seats available for members. You can view the number of available seats at the top of the Team tab.
If you need more seats, learn more about [other available plans](https://medusajs.com/pricing).
If you need more seats, learn more about [other available plans](../pricing/page.mdx) or [purchasing add-on resources](../billing/page.mdx#purchase-add-on-resources).
![Window showing the organization members in the Team tab with the available seats highlighted](https://res.cloudinary.com/dza7lstvk/image/upload/v1749737803/Cloud/CleanShot_2025-06-12_at_17.16.13_2x_zk8n32.png)
+68
View File
@@ -0,0 +1,68 @@
"use server"
import { sanityClient } from "../../utils/sanity-client"
import { PricingQueryResult } from "../../utils/types"
import HeroPricing from "../../components/Pricing/HeroPricing"
import { notFound } from "next/navigation"
import FeatureSections from "../../components/Pricing/FeatureSections"
import { H2, Hr, Loading } from "docs-ui"
import { cache, Suspense } from "react"
export default async function PricingPage() {
if (process.env.NEXT_PUBLIC_ENV === "CI") {
return <div>Pricing page is not available in the CI environment.</div>
}
const data = await loadPricingData()
const heroPricingData = data.find((item) => item._type === "heroPricing")
const featureTableData = data.find((item) => item._type === "featureTable")
// Ensure both data pieces are present
if (
!featureTableData?.featureTableFields ||
!heroPricingData?.heroPricingFields
) {
return notFound()
}
return (
<Suspense fallback={<Loading />}>
<H2 id="cloud-plans">Cloud Plans</H2>
<HeroPricing data={heroPricingData.heroPricingFields} />
<Hr />
<H2 id="plans-features">Plans Features</H2>
<FeatureSections
featureSections={featureTableData.featureTableFields.featureSections}
columnCount={featureTableData.featureTableFields.columnHeaders.length}
columns={featureTableData.featureTableFields.columnHeaders}
/>
</Suspense>
)
}
const loadPricingData = cache(async () => {
const data: PricingQueryResult = await sanityClient.fetch(
`*[
(_type == "featureTable" && _id == "9cb4e359-786a-4cdb-9334-88ad4ce44f05") ||
(_type == "heroPricing" && _id == "8d8f33e1-7f18-4b2f-8686-5bc57da697db")
]{
_type,
_id,
// For featureTable
"featureTableFields": select(
_type == "featureTable" => {
columnHeaders,
featureSections,
links
}
),
// For heroPricing
"heroPricingFields": select(
_type == "heroPricing" => {
options
}
)
}`
)
return data
})
+15
View File
@@ -0,0 +1,15 @@
---
generate_toc: true
---
import Page from "./content"
export const metadata = {
title: `Plans & Pricing`,
}
# {metadata.title}
In this guide, find details about the different Cloud plans and pricing.
<Page />
@@ -0,0 +1,218 @@
import React from "react"
import clsx from "clsx"
import {
FeatureTableFields,
Block,
Span,
TooltipBlock,
} from "../../../utils/types"
import { BorderedIcon, H3, MarkdownContent, MDXComponents } from "docs-ui"
import slugify from "slugify"
import {
CodePullRequest,
CurrencyDollar,
ServerStack,
Shopping,
WIP,
} from "@medusajs/icons"
const P = MDXComponents.p
interface FeatureSectionsProps {
featureSections: FeatureTableFields["featureSections"]
columnCount: number
columns: string[]
}
const featureLinks: Record<string, string> = {
Orders: "https://docs.medusajs.com/commerce-modules/order",
Products: "https://docs.medusajs.com/commerce-modules/product",
"Sales Channels": "https://docs.medusajs.com/commerce-modules/sales-channels",
"Regions & currencies": "https://docs.medusajs.com/commerce-modules/region",
"GitHub integration":
"https://docs.medusajs.com/cloud/projects#2-create-project-from-an-existing-application",
"Push-to-deploy flow":
"https://docs.medusajs.com/cloud/deployments#how-are-deployments-created",
Previews: "https://docs.medusajs.com/cloud/environments/preview",
"Auto configuration:":
"https://docs.medusajs.com/cloud/projects#prerequisite-medusa-application-configurations",
Postgres: "https://docs.medusajs.com/cloud/database",
Redis: "https://docs.medusajs.com/cloud/redis",
S3: "https://docs.medusajs.com/cloud/s3",
"Environment variables":
"https://docs.medusajs.com/cloud/environments/environment-variables",
"Data import/export":
"https://docs.medusajs.com/cloud/database#importexport-database-dumps",
Logs: "https://docs.medusajs.com/cloud/logs",
"Multiple Long-Lived Environments":
"https://docs.medusajs.com/cloud/environments/long-lived",
"Cloud seats":
"https://docs.medusajs.com/cloud/organizations#view-organization-members",
}
const featureIcons: Record<string, React.FC> = {
"Commerce features": Shopping,
"Development Platform": CodePullRequest,
"Hosting & Deployment": ServerStack,
"Compute & Resources": WIP,
"Organization & Billing": CurrencyDollar,
}
// Helper function to render Block content (Sanity rich text)
const renderBlockContent = (blocks: Block[]) => {
if (!blocks || blocks.length === 0) {
return ""
}
return blocks
.map((block) => {
if (block._type === "block" && block.children) {
return block.children
.map((child: Span | TooltipBlock) => {
if (child._type === "span") {
const key = child.text.trim()
return featureLinks[key]
? "[" + child.text + "](" + featureLinks[key] + ")"
: child.text
}
return ""
})
.join(" \n")
}
return ""
})
.join(" \n")
.replaceAll("-", "\\-")
}
const FeatureSections: React.FC<FeatureSectionsProps> = ({
featureSections,
columnCount,
columns,
}) => {
if (!featureSections || featureSections.length === 0) {
return null
}
// Calculate consistent column widths
// Use fractional units to ensure all grids have matching column sizes
const featureNameFraction = 2 // Feature name gets 2 units
const featureColumnFraction = 1 // Each feature column gets 1 unit
const gridTemplate = `${featureNameFraction}fr repeat(${columnCount}, ${featureColumnFraction}fr)`
return (
<div className="w-full flex flex-col rounded shadow-elevation-card-rest dark:shadow-elevation-card-rest-dark">
{/* Header */}
<div
className="w-full grid gap-0 rounded-t"
style={{
gridTemplateColumns: gridTemplate,
}}
>
{/* Features label column */}
<div className="flex items-center justify-start px-1.5 py-1 border-solid border-r border-medusa-border-base">
<p className="txt-large text-medusa-fg-subtle">Features</p>
</div>
{/* Column headers */}
{columns.map((column, index) => (
<div
key={index}
className={clsx(
"flex items-center justify-center px-1 py-1 bg-medusa-bg-base",
index !== columns.length - 1 &&
"border-solid border-r border-medusa-border-base"
)}
>
<p className="txt-large text-medusa-fg-base text-left w-full">
{column}
</p>
</div>
))}
</div>
{/* Feature Sections */}
{featureSections.map((section) => (
<div key={section._key} className="w-full">
{/* Section Header */}
<div className="w-full p-1.5 bg-medusa-bg-component flex gap-1 border-medusa-border-base border-y items-center">
{featureIcons[section.header.subtitle] && (
<BorderedIcon
IconComponent={featureIcons[section.header.subtitle]}
wrapperClassName="p-[7.5px] bg-medusa-bg-component rounded-[5px]"
/>
)}
<div>
<H3
id={slugify(section.header.subtitle, { lower: true })}
className="!my-0"
>
{section.header.subtitle}
</H3>
{/* @ts-expect-error this is a React component */}
<P className="text-medusa-fg-subtle">{section.header.title}</P>
</div>
</div>
{/* Section Rows */}
<div className="w-full">
{section.rows.map((row, index) => (
<React.Fragment key={row._key}>
<div
className={clsx(
"w-full grid gap-0 border-solid border-medusa-border-base",
index !== section.rows.length - 1 && "border-b"
)}
style={{
gridTemplateColumns: gridTemplate,
}}
>
{/* Feature name column */}
<div className="px-1 py-1 flex items-center justify-start border-solid border-r border-medusa-border-base">
<p className="txt-medium-plus text-medusa-fg-base">
<MarkdownContent
allowedElements={["br", "a"]}
unwrapDisallowed
>
{renderBlockContent(row.column1)}
</MarkdownContent>
</p>
</div>
{/* Feature value columns */}
{Array.from({ length: columnCount }, (_, colIndex) => {
const columnKey = `column${
colIndex + 2
}` as keyof typeof row
const columnData = row[columnKey] as Block[]
return (
<div
key={colIndex}
className={clsx(
"px-1 py-1 flex items-center justify-center",
colIndex !== columnCount - 1 &&
"border-solid border-r border-medusa-border-base"
)}
>
<p className="txt-medium text-medusa-fg-base text-left w-full">
<MarkdownContent
allowedElements={["br", "a"]}
unwrapDisallowed
>
{renderBlockContent(columnData)}
</MarkdownContent>
</p>
</div>
)
})}
</div>
</React.Fragment>
))}
</div>
</div>
))}
</div>
)
}
export default FeatureSections
@@ -0,0 +1,162 @@
import React from "react"
import { CheckCircleSolid } from "@medusajs/icons"
import { HeroPricingFields } from "../../../utils/types"
import { H3, Button } from "docs-ui"
import clsx from "clsx"
import slugify from "slugify"
import Link from "next/link"
interface HeroPricingProps {
data: HeroPricingFields
}
const HeroPricing: React.FC<HeroPricingProps> = ({ data }) => {
if (!data?.options) {
return null
}
return (
<div className="relative w-full rounded shadow-elevation-card-rest dark:shadow-elevation-card-rest-dark">
{/* Header Row */}
<div className="flex items-start justify-start">
{/* Main content area */}
<div className="w-full flex items-center justify-start">
{data.options.map((option, index) => (
<React.Fragment key={option._key}>
<div
className={clsx(
`flex-1 min-w-0 p-1`,
index !== data.options.length - 1 &&
"border-solid border-r border-medusa-border-base",
index === data.options.length - 1 &&
"bg-medusa-bg-subtle rounded-tr"
)}
>
<div className="flex flex-col w-full">
<div className="flex flex-col items-start justify-start w-full">
<div className="flex flex-col justify-end w-full">
<H3
className="!mb-0 txt-xlarge"
id={slugify(option.title, {
lower: true,
})}
>
{option.title}
</H3>
</div>
<div className="flex flex-col justify-end w-full">
<p className="txt-xlarge text-medusa-fg-subtle">
{option.subtitle}
</p>
</div>
</div>
</div>
</div>
</React.Fragment>
))}
</div>
</div>
{/* Features and Buttons Row */}
<div
className="w-full grid gap-0"
style={{
gridTemplateColumns: `repeat(${data.options.length}, 1fr)`,
gridTemplateRows: "auto 1fr auto",
}}
>
{/* Description row - all descriptions at same height */}
{data.options.map((option, index) => (
<div
key={`description-${option._key}`}
className={clsx(
`p-1 flex items-start border-b border-medusa-border-base`,
index !== data.options.length - 1 &&
"border-solid border-r border-medusa-border-base",
index === data.options.length - 1 && "bg-medusa-bg-subtle"
)}
>
<div className="w-full flex items-start">
{option.description && (
<p className="text-medusa-fg-base txt-small-plus text-balance">
{option.description}
</p>
)}
</div>
</div>
))}
{/* Features row - all features sections at same level */}
{data.options.map((option, index) => (
<div
key={`features-${option._key}`}
className={clsx(
`p-1 flex flex-col gap-0.75`,
index !== data.options.length - 1 &&
"border-solid border-r border-medusa-border-base",
index === data.options.length - 1 && "bg-medusa-bg-subtle"
)}
>
{option.pre_features && (
<div className="flex items-center w-full">
<span className="text-medusa-fg-base txt-small-plus">
{option.pre_features}
</span>
</div>
)}
{option.features.map((feature, featureIndex) => (
<div
key={featureIndex}
className="flex gap-0.75 items-center w-full text-medusa-fg-base"
>
<div className="flex gap-0.5 items-center">
<CheckCircleSolid />
</div>
<span className="txt-medium-plus">{feature}</span>
</div>
))}
</div>
))}
{/* Buttons row - all buttons at same level */}
{data.options.map((option, index) => (
<div
key={`buttons-${option._key}`}
className={clsx(
`p-1`,
index !== data.options.length - 1 &&
"border-solid border-r border-medusa-border-base",
index === data.options.length - 1 &&
"bg-medusa-bg-subtle rounded-br"
)}
>
<div className="w-full">
{option.buttons.map((button) => (
<Link
key={button._key}
href={`https://medusajs.com${button.link.path}`}
target="_blank"
rel="noreferrer"
>
<Button
variant={
button.variant === "primary" || button.variant === "dark"
? "primary"
: "secondary"
}
className="w-full txt-compact-xsmall-plus"
>
{button.link.label}
</Button>
</Link>
))}
</div>
</div>
))}
</div>
</div>
)
}
export default HeroPricing
+4 -3
View File
@@ -4,7 +4,7 @@ export const generatedEditDates = {
"app/projects/page.mdx": "2025-09-09T06:45:36.785Z",
"app/environments/page.mdx": "2025-06-25T08:00:05.550Z",
"app/deployments/page.mdx": "2025-06-25T07:57:13.059Z",
"app/organizations/page.mdx": "2025-08-25T07:15:17.593Z",
"app/organizations/page.mdx": "2025-09-11T14:26:31.848Z",
"app/notifications/page.mdx": "2025-06-25T07:27:37.642Z",
"app/database/page.mdx": "2025-08-15T15:30:37.814Z",
"app/redis/page.mdx": "2025-06-25T07:57:23.246Z",
@@ -16,8 +16,9 @@ export const generatedEditDates = {
"app/environments/environment-variables/page.mdx": "2025-08-15T15:35:24.085Z",
"app/environments/long-lived/page.mdx": "2025-08-25T07:23:59.858Z",
"app/environments/preview/page.mdx": "2025-08-20T12:15:33.952Z",
"app/faq/page.mdx": "2025-08-15T14:32:23.372Z",
"app/faq/page.mdx": "2025-09-11T14:26:01.400Z",
"app/billing/page.mdx": "2025-09-04T15:25:50.586Z",
"app/usage/page.mdx": "2025-08-25T07:25:54.703Z",
"app/billing/manage/page.mdx": "2025-09-04T14:50:46.747Z"
"app/billing/manage/page.mdx": "2025-09-04T14:50:46.747Z",
"app/pricing/page.mdx": "2025-09-05T10:31:59.059Z"
}
+8
View File
@@ -171,6 +171,14 @@ export const generatedSidebars = [
"path": "/billing",
"children": []
},
{
"loaded": true,
"isPathHref": true,
"type": "link",
"title": "Plans & Pricing",
"path": "/pricing",
"children": []
},
{
"loaded": true,
"isPathHref": true,
+3 -1
View File
@@ -18,6 +18,7 @@
"@mdx-js/react": "^3.1.0",
"@medusajs/icons": "2.10.1",
"@next/mdx": "15.3.5",
"@sanity/client": "^7.11.0",
"@stefanprobst/rehype-extract-toc": "^3.0.0",
"clsx": "^2.1.0",
"docs-ui": "*",
@@ -28,7 +29,8 @@
"rehype-slug": "^6.0.0",
"remark-directive": "^3.0.0",
"remark-frontmatter": "^5.0.0",
"remark-rehype-plugins": "*"
"remark-rehype-plugins": "*",
"slugify": "^1.6.6"
},
"devDependencies": {
"@types/mdx": "^2.0.13",
+5
View File
@@ -118,6 +118,11 @@ export const sidebar = [
title: "Overview",
path: "/billing",
},
{
type: "link",
title: "Plans & Pricing",
path: "/pricing",
},
{
type: "link",
title: "Manage Billing Details",
+8
View File
@@ -0,0 +1,8 @@
import { createClient } from "@sanity/client"
export const sanityClient = createClient({
projectId: process.env.NEXT_PUBLIC_SANITY_PROJECT_ID || "",
dataset: process.env.NEXT_PUBLIC_SANITY_DATASET || "production",
apiVersion: "2025-02-19",
useCdn: false,
})
+90
View File
@@ -0,0 +1,90 @@
export type PricingQueryResult = Array<{
_id: string
_type: "heroPricing" | "featureTable"
featureTableFields: FeatureTableFields | null
heroPricingFields: HeroPricingFields | null
}>
export type HeroPricingFields = {
options: Array<{
_key: string
_type: "option"
buttons: Array<{
_key: string
_type: "button"
link: {
_type: "link"
label: string
path: string
type: string
}
size: string
variant: string
}>
description: string
features: string[]
subtitle: string
title: string
pre_features?: string
titleLine2?: string
}>
}
export type FeatureTableFields = {
columnHeaders: string[]
featureSections: Array<{
_key: string
header: {
_type: "sectionHeader"
icon: {
_type: "image"
asset: {
_ref: string
_type: "reference"
}
}
subtitle: string
title: string
}
rows: Array<{
_key: string
column1: Block[]
column2: Block[]
column3: Block[]
column4: Block[]
}>
}>
links: Array<{
_key: string
_type: "button"
link: {
_type: "link"
label: string
path: string
type: string
}
size: string
variant: string
}>
}
export type Block = {
_key: string
_type: "block"
children: Array<Span | TooltipBlock>
markDefs: any[]
style: string
}
export type Span = {
_key: string
_type: "span"
marks: string[]
text: string
}
export type TooltipBlock = {
_key: string
_type: "tooltipBlock"
text: string
}