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.
This commit is contained in:
David Hill
2026-03-04 22:07:49 +00:00
parent 40fc406424
commit 0b825ca383
3 changed files with 242 additions and 160 deletions

View File

@@ -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.",

View File

@@ -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;
}
}
}

View File

@@ -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">