Skip to content

Commit 3a7a257

Browse files
authored
docs: add cloud plans & pricing page (medusajs#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
1 parent 69b9268 commit 3a7a257

File tree

17 files changed

+767
-14
lines changed

17 files changed

+767
-14
lines changed

.github/workflows/docs-test.yml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ jobs:
2222
- name: Setup Node.js environment
2323
uses: actions/setup-node@v4
2424
with:
25-
node-version: 20
25+
node-version: 22
2626
cache: "yarn"
2727

2828
- name: Install dependencies
@@ -43,6 +43,9 @@ jobs:
4343
NEXT_PUBLIC_DOCS_URL: "https://medusa-docs.vercel.app"
4444
NODE_ENV: production
4545
NEXT_PUBLIC_RESOURCES_URL: "http://medusa-types-nine.vercel.app"
46+
NEXT_PUBLIC_SANITY_PROJECT_ID: temp
47+
NEXT_PUBLIC_SANITY_DATASET: temp
48+
NEXT_PUBLIC_ENV: CI
4649
# TODO change once we have actual URLs
4750
NEXT_PUBLIC_USER_GUIDE_URL: "http://example.com"
4851

www/apps/cloud/app/faq/page.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ Yes, your code is hosted on your GitHub repository. You can also export a dump o
3636

3737
## Can I deploy multiple projects on Cloud?
3838

39-
Yes, if your plan supports multiple projects. Learn more in the [Pricing](https://medusajs.com/pricing/) page.
39+
Yes, if your plan supports multiple projects. Learn more in the [Plans & Pricing](../pricing/page.mdx) guide.
4040

4141
---
4242

www/apps/cloud/app/organizations/page.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ In this window, you can view the members of the organization, their roles, and t
119119

120120
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.
121121

122-
If you need more seats, learn more about [other available plans](https://medusajs.com/pricing).
122+
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).
123123

124124
![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)
125125

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
"use server"
2+
3+
import { sanityClient } from "../../utils/sanity-client"
4+
import { PricingQueryResult } from "../../utils/types"
5+
import HeroPricing from "../../components/Pricing/HeroPricing"
6+
import { notFound } from "next/navigation"
7+
import FeatureSections from "../../components/Pricing/FeatureSections"
8+
import { H2, Hr, Loading } from "docs-ui"
9+
import { cache, Suspense } from "react"
10+
11+
export default async function PricingPage() {
12+
if (process.env.NEXT_PUBLIC_ENV === "CI") {
13+
return <div>Pricing page is not available in the CI environment.</div>
14+
}
15+
const data = await loadPricingData()
16+
17+
const heroPricingData = data.find((item) => item._type === "heroPricing")
18+
const featureTableData = data.find((item) => item._type === "featureTable")
19+
20+
// Ensure both data pieces are present
21+
if (
22+
!featureTableData?.featureTableFields ||
23+
!heroPricingData?.heroPricingFields
24+
) {
25+
return notFound()
26+
}
27+
28+
return (
29+
<Suspense fallback={<Loading />}>
30+
<H2 id="cloud-plans">Cloud Plans</H2>
31+
<HeroPricing data={heroPricingData.heroPricingFields} />
32+
<Hr />
33+
<H2 id="plans-features">Plans Features</H2>
34+
<FeatureSections
35+
featureSections={featureTableData.featureTableFields.featureSections}
36+
columnCount={featureTableData.featureTableFields.columnHeaders.length}
37+
columns={featureTableData.featureTableFields.columnHeaders}
38+
/>
39+
</Suspense>
40+
)
41+
}
42+
43+
const loadPricingData = cache(async () => {
44+
const data: PricingQueryResult = await sanityClient.fetch(
45+
`*[
46+
(_type == "featureTable" && _id == "9cb4e359-786a-4cdb-9334-88ad4ce44f05") ||
47+
(_type == "heroPricing" && _id == "8d8f33e1-7f18-4b2f-8686-5bc57da697db")
48+
]{
49+
_type,
50+
_id,
51+
// For featureTable
52+
"featureTableFields": select(
53+
_type == "featureTable" => {
54+
columnHeaders,
55+
featureSections,
56+
links
57+
}
58+
),
59+
// For heroPricing
60+
"heroPricingFields": select(
61+
_type == "heroPricing" => {
62+
options
63+
}
64+
)
65+
}`
66+
)
67+
return data
68+
})
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
generate_toc: true
3+
---
4+
5+
import Page from "./content"
6+
7+
export const metadata = {
8+
title: `Plans & Pricing`,
9+
}
10+
11+
# {metadata.title}
12+
13+
In this guide, find details about the different Cloud plans and pricing.
14+
15+
<Page />
Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
import React from "react"
2+
import clsx from "clsx"
3+
import {
4+
FeatureTableFields,
5+
Block,
6+
Span,
7+
TooltipBlock,
8+
} from "../../../utils/types"
9+
import { BorderedIcon, H3, MarkdownContent, MDXComponents } from "docs-ui"
10+
import slugify from "slugify"
11+
import {
12+
CodePullRequest,
13+
CurrencyDollar,
14+
ServerStack,
15+
Shopping,
16+
WIP,
17+
} from "@medusajs/icons"
18+
19+
const P = MDXComponents.p
20+
21+
interface FeatureSectionsProps {
22+
featureSections: FeatureTableFields["featureSections"]
23+
columnCount: number
24+
columns: string[]
25+
}
26+
27+
const featureLinks: Record<string, string> = {
28+
Orders: "https://docs.medusajs.com/commerce-modules/order",
29+
Products: "https://docs.medusajs.com/commerce-modules/product",
30+
"Sales Channels": "https://docs.medusajs.com/commerce-modules/sales-channels",
31+
"Regions & currencies": "https://docs.medusajs.com/commerce-modules/region",
32+
"GitHub integration":
33+
"https://docs.medusajs.com/cloud/projects#2-create-project-from-an-existing-application",
34+
"Push-to-deploy flow":
35+
"https://docs.medusajs.com/cloud/deployments#how-are-deployments-created",
36+
Previews: "https://docs.medusajs.com/cloud/environments/preview",
37+
"Auto configuration:":
38+
"https://docs.medusajs.com/cloud/projects#prerequisite-medusa-application-configurations",
39+
Postgres: "https://docs.medusajs.com/cloud/database",
40+
Redis: "https://docs.medusajs.com/cloud/redis",
41+
S3: "https://docs.medusajs.com/cloud/s3",
42+
"Environment variables":
43+
"https://docs.medusajs.com/cloud/environments/environment-variables",
44+
"Data import/export":
45+
"https://docs.medusajs.com/cloud/database#importexport-database-dumps",
46+
Logs: "https://docs.medusajs.com/cloud/logs",
47+
"Multiple Long-Lived Environments":
48+
"https://docs.medusajs.com/cloud/environments/long-lived",
49+
"Cloud seats":
50+
"https://docs.medusajs.com/cloud/organizations#view-organization-members",
51+
}
52+
53+
const featureIcons: Record<string, React.FC> = {
54+
"Commerce features": Shopping,
55+
"Development Platform": CodePullRequest,
56+
"Hosting & Deployment": ServerStack,
57+
"Compute & Resources": WIP,
58+
"Organization & Billing": CurrencyDollar,
59+
}
60+
61+
// Helper function to render Block content (Sanity rich text)
62+
const renderBlockContent = (blocks: Block[]) => {
63+
if (!blocks || blocks.length === 0) {
64+
return ""
65+
}
66+
67+
return blocks
68+
.map((block) => {
69+
if (block._type === "block" && block.children) {
70+
return block.children
71+
.map((child: Span | TooltipBlock) => {
72+
if (child._type === "span") {
73+
const key = child.text.trim()
74+
return featureLinks[key]
75+
? "[" + child.text + "](" + featureLinks[key] + ")"
76+
: child.text
77+
}
78+
return ""
79+
})
80+
.join(" \n")
81+
}
82+
return ""
83+
})
84+
.join(" \n")
85+
.replaceAll("-", "\\-")
86+
}
87+
88+
const FeatureSections: React.FC<FeatureSectionsProps> = ({
89+
featureSections,
90+
columnCount,
91+
columns,
92+
}) => {
93+
if (!featureSections || featureSections.length === 0) {
94+
return null
95+
}
96+
97+
// Calculate consistent column widths
98+
// Use fractional units to ensure all grids have matching column sizes
99+
const featureNameFraction = 2 // Feature name gets 2 units
100+
const featureColumnFraction = 1 // Each feature column gets 1 unit
101+
const gridTemplate = `${featureNameFraction}fr repeat(${columnCount}, ${featureColumnFraction}fr)`
102+
103+
return (
104+
<div className="w-full flex flex-col rounded shadow-elevation-card-rest dark:shadow-elevation-card-rest-dark">
105+
{/* Header */}
106+
<div
107+
className="w-full grid gap-0 rounded-t"
108+
style={{
109+
gridTemplateColumns: gridTemplate,
110+
}}
111+
>
112+
{/* Features label column */}
113+
<div className="flex items-center justify-start px-1.5 py-1 border-solid border-r border-medusa-border-base">
114+
<p className="txt-large text-medusa-fg-subtle">Features</p>
115+
</div>
116+
117+
{/* Column headers */}
118+
{columns.map((column, index) => (
119+
<div
120+
key={index}
121+
className={clsx(
122+
"flex items-center justify-center px-1 py-1 bg-medusa-bg-base",
123+
index !== columns.length - 1 &&
124+
"border-solid border-r border-medusa-border-base"
125+
)}
126+
>
127+
<p className="txt-large text-medusa-fg-base text-left w-full">
128+
{column}
129+
</p>
130+
</div>
131+
))}
132+
</div>
133+
{/* Feature Sections */}
134+
{featureSections.map((section) => (
135+
<div key={section._key} className="w-full">
136+
{/* Section Header */}
137+
<div className="w-full p-1.5 bg-medusa-bg-component flex gap-1 border-medusa-border-base border-y items-center">
138+
{featureIcons[section.header.subtitle] && (
139+
<BorderedIcon
140+
IconComponent={featureIcons[section.header.subtitle]}
141+
wrapperClassName="p-[7.5px] bg-medusa-bg-component rounded-[5px]"
142+
/>
143+
)}
144+
<div>
145+
<H3
146+
id={slugify(section.header.subtitle, { lower: true })}
147+
className="!my-0"
148+
>
149+
{section.header.subtitle}
150+
</H3>
151+
{/* @ts-expect-error this is a React component */}
152+
<P className="text-medusa-fg-subtle">{section.header.title}</P>
153+
</div>
154+
</div>
155+
156+
{/* Section Rows */}
157+
<div className="w-full">
158+
{section.rows.map((row, index) => (
159+
<React.Fragment key={row._key}>
160+
<div
161+
className={clsx(
162+
"w-full grid gap-0 border-solid border-medusa-border-base",
163+
index !== section.rows.length - 1 && "border-b"
164+
)}
165+
style={{
166+
gridTemplateColumns: gridTemplate,
167+
}}
168+
>
169+
{/* Feature name column */}
170+
<div className="px-1 py-1 flex items-center justify-start border-solid border-r border-medusa-border-base">
171+
<p className="txt-medium-plus text-medusa-fg-base">
172+
<MarkdownContent
173+
allowedElements={["br", "a"]}
174+
unwrapDisallowed
175+
>
176+
{renderBlockContent(row.column1)}
177+
</MarkdownContent>
178+
</p>
179+
</div>
180+
181+
{/* Feature value columns */}
182+
{Array.from({ length: columnCount }, (_, colIndex) => {
183+
const columnKey = `column${
184+
colIndex + 2
185+
}` as keyof typeof row
186+
const columnData = row[columnKey] as Block[]
187+
188+
return (
189+
<div
190+
key={colIndex}
191+
className={clsx(
192+
"px-1 py-1 flex items-center justify-center",
193+
colIndex !== columnCount - 1 &&
194+
"border-solid border-r border-medusa-border-base"
195+
)}
196+
>
197+
<p className="txt-medium text-medusa-fg-base text-left w-full">
198+
<MarkdownContent
199+
allowedElements={["br", "a"]}
200+
unwrapDisallowed
201+
>
202+
{renderBlockContent(columnData)}
203+
</MarkdownContent>
204+
</p>
205+
</div>
206+
)
207+
})}
208+
</div>
209+
</React.Fragment>
210+
))}
211+
</div>
212+
</div>
213+
))}
214+
</div>
215+
)
216+
}
217+
218+
export default FeatureSections

0 commit comments

Comments
 (0)