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.
go-page
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">