summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorDavid Hill <[email protected]>2026-03-04 22:07:49 +0000
committerDavid Hill <[email protected]>2026-03-04 22:07:49 +0000
commit0b825ca383f12fa6649d48165fb3f5e27cc4b049 (patch)
treea5169779591e78529e6c58e00fd10367e8ad66f5
parent40fc406424c27a00f0841af46f6ec96572c973f7 (diff)
downloadopencode-0b825ca383f12fa6649d48165fb3f5e27cc4b049.tar.gz
opencode-0b825ca383f12fa6649d48165fb3f5e27cc4b049.zip
docs: redesign Go pricing graph with horizontal bars and inline request labels
Improve visual clarity of request limits on the Go pricing page by replacing dot-based chart with animated horizontal bars that directly show model names and exact request counts. Add proper OpenGraph and Twitter Card meta tags for better social sharing discovery.
-rw-r--r--packages/console/app/src/i18n/en.ts2
-rw-r--r--packages/console/app/src/routes/go/index.css254
-rw-r--r--packages/console/app/src/routes/go/index.tsx146
3 files changed, 242 insertions, 160 deletions
diff --git a/packages/console/app/src/i18n/en.ts b/packages/console/app/src/i18n/en.ts
index 3f39fcd9d..9bf8ad43d 100644
--- a/packages/console/app/src/i18n/en.ts
+++ b/packages/console/app/src/i18n/en.ts
@@ -246,6 +246,8 @@ export const dict = {
"zen.privacy.exceptionsLink": "following exceptions",
"go.title": "OpenCode Go | Low cost coding models for everyone",
+ "go.meta.description":
+ "Go is a $10/month subscription with generous 5-hour request limits for GLM-5, Kimi K2.5, and MiniMax M2.5.",
"go.hero.title": "Low cost coding models for everyone",
"go.hero.body":
"Go brings agentic coding to programmers around the world. Offering generous limits and reliable access to the most capable open-source models, so you can build with powerful agents without worrying about cost or availability.",
diff --git a/packages/console/app/src/routes/go/index.css b/packages/console/app/src/routes/go/index.css
index 34be47f0d..99c61a70b 100644
--- a/packages/console/app/src/routes/go/index.css
+++ b/packages/console/app/src/routes/go/index.css
@@ -21,6 +21,13 @@
}
}
+@keyframes go-graph-bar {
+ to {
+ opacity: 1;
+ transform: scaleX(1);
+ }
+}
+
[data-page="go"] {
--color-background: hsl(0, 20%, 99%);
--color-background-weak: hsl(0, 8%, 97%);
@@ -424,13 +431,78 @@ body {
[data-component="limit-graph"] {
margin: 0 auto;
- max-width: calc(100% - (var(--padding) * 2));
+ width: calc(100% - 120px);
+ max-width: calc(100% - 120px);
border: none;
background: transparent;
- padding: 18px 18px 56px;
+ padding: 58px var(--padding) 56px;
+
+ @media (max-width: 48rem) {
+ width: 100%;
+ max-width: 100%;
+ }
[data-slot="plot"] {
position: relative;
+ overflow: visible;
+ width: 100%;
+ margin: 0 auto;
+ margin-left: -40px;
+ }
+
+ [data-slot="ylabels"] {
+ position: absolute;
+ inset: 0;
+ pointer-events: none;
+ }
+
+ [data-slot="ylabels"] [data-ylabel] {
+ position: absolute;
+ left: var(--x);
+ top: var(--y);
+ transform: translate(-100%, -50%);
+ color: var(--color-text-strong);
+ font-size: 16px;
+ font-weight: 700;
+ line-height: 1;
+ white-space: nowrap;
+ }
+
+ [data-slot="pills"] {
+ position: absolute;
+ inset: 0;
+ pointer-events: none;
+
+ [data-item] {
+ position: absolute;
+ left: var(--x);
+ top: var(--y);
+ transform: translate(12px, -50%);
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+ border: none;
+ background: transparent;
+ height: 20px;
+ padding: 0 8px;
+ border-radius: 2px;
+ max-width: calc(100% - 12px);
+ font-size: 13px;
+ line-height: 20px;
+ box-sizing: border-box;
+ opacity: 0;
+ }
+
+ [data-name] {
+ color: var(--color-text);
+ white-space: nowrap;
+ }
+
+ [data-value] {
+ color: var(--color-text-strong);
+ font-weight: 600;
+ white-space: nowrap;
+ }
}
[data-slot="plot-labels"] {
@@ -451,8 +523,7 @@ body {
svg {
width: 100%;
- height: auto;
- aspect-ratio: 720 / 220;
+ height: 220px;
display: block;
}
@@ -479,13 +550,44 @@ body {
font-weight: 600;
}
+ [data-row],
+ [data-val] {
+ opacity: 0;
+ }
+
+ &[data-visible] [data-row],
+ &[data-visible] [data-val] {
+ opacity: 1;
+ transition: opacity 240ms ease;
+ transition-delay: var(--d, 0ms);
+ }
+
[data-stub] {
stroke: var(--color-border);
- stroke-width: 2;
+ stroke-width: 1;
stroke-linecap: round;
opacity: 0.55;
}
+ [data-bar] {
+ transform-box: fill-box;
+ transform-origin: left center;
+ opacity: 0;
+ transform: scaleX(0.02);
+ fill: var(--color-go-2);
+ stroke: none;
+ }
+
+ [data-bar][data-kind="free"] {
+ fill: var(--color-text-strong);
+ }
+
+ [data-val] {
+ fill: var(--color-text-strong);
+ font-size: 13px;
+ font-weight: 650;
+ }
+
[data-range] {
stroke: var(--color-text-strong);
stroke-width: 2;
@@ -542,6 +644,17 @@ body {
animation-delay: var(--d, 0ms);
}
+ &[data-visible] [data-bar] {
+ animation: go-graph-bar 560ms cubic-bezier(0.2, 0.7, 0.2, 1) forwards;
+ animation-delay: var(--d, 0ms);
+ }
+
+ &[data-visible] [data-slot="pills"] [data-item] {
+ opacity: 1;
+ transition: opacity 240ms ease;
+ transition-delay: var(--d, 0ms);
+ }
+
@media (prefers-reduced-motion: reduce) {
[data-animate="line"] {
stroke-dashoffset: 0;
@@ -552,34 +665,49 @@ body {
transform: none;
animation: none;
}
+ [data-bar] {
+ opacity: 1;
+ transform: none;
+ animation: none;
+ }
+ [data-row],
+ [data-val] {
+ opacity: 1;
+ transition: none;
+ }
+
+ [data-slot="pills"] [data-item] {
+ opacity: 1;
+ transition: none;
+ }
}
figcaption {
margin-top: 34px;
display: flex;
- flex-direction: column;
- gap: 10px;
+ justify-content: center;
font-size: 13px;
+ text-align: center;
}
[data-slot="caption-row"] {
display: flex;
width: 100%;
+ justify-content: center;
}
[data-slot="caption-left"] {
- display: grid;
+ display: flex;
width: 100%;
- grid-template-columns: var(--start, 16.9%) minmax(0, 1fr);
- grid-template-rows: auto auto;
- align-items: center;
- column-gap: 0;
- row-gap: 0;
min-width: 0;
+ justify-content: center;
}
[data-slot="caption-meta"] {
- display: contents;
+ display: flex;
+ flex-direction: row;
+ gap: 16px;
+ align-items: baseline;
}
[data-slot="caption-label"] {
@@ -587,8 +715,6 @@ body {
font-weight: 650;
white-space: nowrap;
line-height: 1;
- grid-column: 1;
- grid-row: 1;
}
[data-slot="caption-link"] {
@@ -596,73 +722,6 @@ body {
text-decoration-thickness: 1px;
width: fit-content;
line-height: 1;
- grid-column: 1;
- grid-row: 2;
- align-self: start;
- }
-
- [data-slot="legend"] {
- display: flex;
- width: 100%;
- flex-wrap: nowrap;
- gap: 10px;
- min-width: 0;
- overflow-x: auto;
- -webkit-overflow-scrolling: touch;
- padding-bottom: 8px;
- margin-left: -12px;
- grid-column: 2;
- grid-row: 1;
- align-self: center;
-
- [data-item] {
- display: inline-flex;
- flex: 0 0 auto;
- align-items: center;
- gap: 8px;
- border: 1px solid var(--color-border-weak);
- background: var(--color-background);
- padding: 6px 10px;
- border-radius: 999px;
- max-width: 100%;
- }
-
- [data-dot] {
- width: 10px;
- height: 10px;
- border-radius: 999px;
- display: inline-block;
- border: 1px solid var(--color-text-strong);
- background: var(--color-background);
- flex: 0 0 auto;
- }
-
- [data-dot][data-kind="go"] {
- background: var(--color-background-interactive);
- }
-
- [data-dot][data-kind="go"][data-model="glm"] {
- background: var(--color-go-1);
- }
-
- [data-dot][data-kind="go"][data-model="kimi"] {
- background: var(--color-go-2);
- }
-
- [data-dot][data-kind="go"][data-model="minimax"] {
- background: var(--color-go-3);
- }
-
- [data-name] {
- color: var(--color-text);
- white-space: nowrap;
- }
-
- [data-value] {
- color: var(--color-text-strong);
- font-weight: 600;
- white-space: nowrap;
- }
}
[data-slot="caption-note"] {
@@ -671,35 +730,8 @@ body {
}
@media (max-width: 56.25rem) {
- [data-slot="caption-left"] {
- grid-template-columns: var(--start, 16.9%) minmax(0, 1fr);
- grid-template-rows: auto auto;
- align-items: start;
- }
-
- [data-slot="legend"] {
- grid-column: 2;
- grid-row: 1;
- }
-
[data-slot="caption-meta"] {
- display: flex;
- gap: 24px;
- align-items: baseline;
- grid-column: 2;
- grid-row: 2;
- margin-top: 12px;
- }
-
- [data-slot="caption-label"] {
- grid-column: auto;
- grid-row: auto;
- }
-
- [data-slot="caption-link"] {
- grid-column: auto;
- grid-row: auto;
- align-self: baseline;
+ gap: 14px;
}
}
}
diff --git a/packages/console/app/src/routes/go/index.tsx b/packages/console/app/src/routes/go/index.tsx
index 9e4f0eefd..6ae5e476c 100644
--- a/packages/console/app/src/routes/go/index.tsx
+++ b/packages/console/app/src/routes/go/index.tsx
@@ -10,6 +10,7 @@ import { Faq } from "~/component/faq"
import { Legal } from "~/component/legal"
import { Footer } from "~/component/footer"
import { Header } from "~/component/header"
+import { config } from "~/config"
import { getLastSeenWorkspaceID } from "../workspace/common"
import { IconMiniMax, IconZai } from "~/component/icon"
import { useI18n } from "~/context/i18n"
@@ -49,24 +50,50 @@ function LimitsGraph(props: { href: string }) {
{ id: "kimi", name: "Kimi K2.5", req: 1850, d: "240ms" },
{ id: "minimax", name: "MiniMax M2.5", req: 20000, d: "360ms" },
]
- const ratio = (n: number) => n / free
const w = 720
const h = 220
- const left = 88
- const right = 24
- const top = 22
- const bottom = 46
+ const left = 40
+ const right = 60
+ const top = 18
+ const bottom = 44
const plot = w - left - right
+
+ const ratio = (n: number) => n / free
const rmax = Math.max(1, ...models.map((m) => ratio(m.req)))
const log = (n: number) => Math.log10(Math.max(n, 1))
- const x = (r: number) => left + (log(r) / log(rmax)) * plot
+ const base = 24
+ const p = 2.2
+ const x = (r: number) => left + base + Math.pow(log(r) / log(rmax), p) * (plot - base)
const start = (x(1) / w) * 100
- const yFree = 74
- const yGo = 134
const ticks = [1, 2, 5, 10, 25, 50, 100].filter((t) => t <= rmax)
- const y = (n: number) => `${(n / h) * 100}%`
+ const labels = (() => {
+ const set = new Set<number>()
+ let last = -Infinity
+ for (const t of ticks) {
+ if (t === 1) {
+ set.add(t)
+ last = x(t)
+ continue
+ }
+ const pos = x(t)
+ if (pos - last < 44) continue
+ set.add(t)
+ last = pos
+ }
+ return set
+ })()
+ const bh = 8
+ const gap = 16
+ const step = bh + gap
+ const sep = bh + 40
+ const fy = top + 22
+ const gy = (i: number) => fy + sep + step * i
+ const my = models.length < 2 ? gy(0) : (gy(0) + gy(models.length - 1)) / 2
+ const px = (n: number) => `${(n / w) * 100}%`
+ const py = (n: number) => `${(n / h) * 100}%`
+ const lx = px(left - 16)
return (
<figure
@@ -77,52 +104,81 @@ function LimitsGraph(props: { href: string }) {
style={{ "--start": `${start}%` } as any}
>
<div data-slot="plot">
- <svg viewBox={`0 0 ${w} ${h}`} role="img" aria-hidden="true">
+ <svg
+ viewBox={`0 0 ${w} ${h}`}
+ preserveAspectRatio="none"
+ role="img"
+ aria-hidden="true"
+ style={{ height: `${h}px` }}
+ >
<g data-slot="grid">
<For each={ticks}>
{(t) => (
<g>
<line x1={x(t)} y1={top} x2={x(t)} y2={h - bottom} data-grid />
- <text x={x(t)} y={h - 18} text-anchor="middle" data-tick>
- {i18n.t("go.graph.tick", { n: t })}
- </text>
+ {labels.has(t) ? (
+ <text x={x(t)} y={h - 18} text-anchor="middle" data-tick>
+ {i18n.t("go.graph.tick", { n: t })}
+ </text>
+ ) : null}
</g>
)}
</For>
</g>
- <g data-slot="free" style={{ "--d": "0ms" } as any}>
- <circle cx={x(1)} cy={yFree} r={5.5} data-point data-kind="free" />
- </g>
+ <line x1={left} y1={top} x2={left} y2={h - bottom} data-stub />
+
+ <g data-slot="bars">
+ <g style={{ "--d": "0ms" } as any}>
+ <rect x={left} y={fy - bh / 2} width={Math.max(0, x(1) - left)} height={bh} data-bar data-kind="free" />
+ </g>
- <g data-slot="go">
- <line x1={x(1)} y1={yGo} x2={x(ratio(models[0]!.req))} y2={yGo} data-range data-animate="line" />
- <line
- x1={x(ratio(models[0]!.req))}
- y1={yGo}
- x2={x(ratio(models[2]!.req))}
- y2={yGo}
- data-range
- data-animate="line"
- />
<For each={models}>
- {(m) => (
+ {(m, i) => (
<g style={{ "--d": m.d } as any}>
- <circle cx={x(ratio(m.req))} cy={yGo} r={5.5} data-point data-kind="go" data-model={m.id} />
+ <rect
+ x={left}
+ y={gy(i()) - bh / 2}
+ width={Math.max(0, x(ratio(m.req)) - left)}
+ height={bh}
+ data-bar
+ data-kind="go"
+ data-model={m.id}
+ />
</g>
)}
</For>
</g>
</svg>
- <div data-slot="plot-labels">
- <span data-row-label style={{ "--y": y(yFree) } as any}>
+ <div data-slot="ylabels" aria-hidden="true">
+ <span data-ylabel style={{ "--x": lx, "--y": py(fy) } as any}>
{i18n.t("go.graph.free")}
</span>
- <span data-row-label style={{ "--y": y(yGo) } as any}>
+ <span data-ylabel style={{ "--x": lx, "--y": py(my) } as any}>
{i18n.t("go.graph.go")}
</span>
</div>
+
+ <div data-slot="pills" aria-hidden="true">
+ <span data-item data-kind="free" style={{ "--x": px(x(1)), "--y": py(fy), "--d": "0ms" } as any}>
+ <span data-name>{i18n.t("go.graph.free")}</span>
+ <span data-value>{free.toLocaleString()}</span>
+ </span>
+ <For each={models}>
+ {(m, i) => (
+ <span
+ data-item
+ data-kind="go"
+ data-model={m.id}
+ style={{ "--x": px(x(ratio(m.req))), "--y": py(gy(i())), "--d": m.d } as any}
+ >
+ <span data-name>{m.name}</span>
+ <span data-value>{m.req.toLocaleString()}</span>
+ </span>
+ )}
+ </For>
+ </div>
</div>
<figcaption>
@@ -134,22 +190,6 @@ function LimitsGraph(props: { href: string }) {
{i18n.t("go.graph.usageLimits")}
</a>
</div>
- <div data-slot="legend">
- <span data-item>
- <i data-dot data-kind="free" />
- <span data-name>{i18n.t("go.graph.free")}</span>
- <span data-value>{free.toLocaleString()}</span>
- </span>
- <For each={models}>
- {(m) => (
- <span data-item>
- <i data-dot data-kind="go" data-model={m.id} />
- <span data-name>{m.name}</span>
- <span data-value>{m.req.toLocaleString()}</span>
- </span>
- )}
- </For>
- </div>
</div>
</div>
</figcaption>
@@ -165,9 +205,17 @@ export default function Home() {
<main data-page="go">
{/*<HttpHeader name="Cache-Control" value="public, max-age=1, s-maxage=3600, stale-while-revalidate=86400" />*/}
<Title>{i18n.t("go.title")}</Title>
+ <Meta name="description" content={i18n.t("go.meta.description")} />
<LocaleLinks path="/go" />
- <Meta property="og:image" content="/social-share-zen.png" />
- <Meta name="twitter:image" content="/social-share-zen.png" />
+ <Meta property="og:type" content="website" />
+ <Meta property="og:url" content={`${config.baseUrl}${language.route("/go")}`} />
+ <Meta property="og:title" content={i18n.t("go.title")} />
+ <Meta property="og:description" content={i18n.t("go.meta.description")} />
+ <Meta property="og:image" content="/social-share-black.png" />
+ <Meta name="twitter:card" content="summary_large_image" />
+ <Meta name="twitter:title" content={i18n.t("go.title")} />
+ <Meta name="twitter:description" content={i18n.t("go.meta.description")} />
+ <Meta name="twitter:image" content="/social-share-black.png" />
<Meta name="opencode:auth" content={loggedin() ? "true" : "false"} />
<div data-component="container">