Skip to content

Commit 09644f5

Browse files
fix(post-publish): PricingPage Anonymous storage cell, Team price label, Storage(S3) → Object storage; docs code-block lang badges (#116)
Five small UI polish items from the PB04 post-publish bug-hunt: 1. PricingPage Anonymous "Storage" cell rendered '—' but plans.yaml has storage_storage_mb=10 for anonymous; backend really does return a 10 MB 24h-TTL bucket. Cell now reads "10 MB / 24h TTL". 2. Homepage SERVICES grid label "Storage (S3)" implied AWS S3, but the live backend is DigitalOcean Spaces (S3-compatible, not AWS). Renamed to "Storage (S3-compatible)". 3. PricingPage Team tier headline price was "coming soon" with no number, which read as vaporware. Now shows "$199/mo" (per plans.yaml team tier) with the existing "soon" badge as the availability signal. CTA stays empty until launch. 4. The first two code blocks on /docs (quickstart.md, services.md curl examples in the InstaNode-dev/content repo) author their fences without a language tag (```\ncurl ...\n```), so they render monochrome with no badge. Added a conservative sniffLang pass in markdown.tsx that flags unlabelled fences as 'bash' only when the first non-empty line starts with curl/kubectl/npm/etc. Explicit fences are never overridden; the existing "no language class when fence has no language tag" test still passes because "plain text" isn't shell-ish. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent ef33325 commit 09644f5

3 files changed

Lines changed: 57 additions & 10 deletions

File tree

src/lib/markdown.tsx

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,17 @@ export function renderMarkdown(md: string, opts: RenderOptions = {}): ReactNode
7272
// The CodeBlock component handles syntax highlighting + the
7373
// "Copy" affordance (BugBash B3-P2-1, B3-P2-2).
7474
const langMatch = block.match(/^```(\w+)?/)
75-
const lang = langMatch?.[1] ?? null
75+
const explicitLang = langMatch?.[1] ?? null
7676
const inner = block.replace(/^```\w*\r?\n?/, '').replace(/\r?\n?```$/, '')
77+
// PB04 P3 (2026-05-21): the docs content repo authors curl examples
78+
// with an unlabelled fence (```\ncurl …\n```) — that lands at
79+
// CodeBlock with lang=null, which renders monochrome with no badge.
80+
// The first two code blocks on /docs (quickstart, services) are
81+
// both curl examples, so the first thing readers see is a blank
82+
// black block. Auto-detect the shell-curl shape and treat it as
83+
// bash so the badge + syntax highlight kick in. Explicit fences
84+
// win — this branch only fires when the author omitted the lang.
85+
const lang = explicitLang ?? sniffLang(inner)
7786
return <CodeBlock key={key} lang={lang} code={inner} />
7887
}
7988

@@ -126,6 +135,36 @@ export function renderMarkdown(md: string, opts: RenderOptions = {}): ReactNode
126135
})
127136
}
128137

138+
/* sniffLang — last-resort language guess for a fenced code block whose
139+
* author omitted the language tag. Only fires when the explicit fence
140+
* label is empty (```\n…) — never overrides ```bash / ```json / etc.
141+
*
142+
* Conservative: returns a language only when the first non-empty line
143+
* is unambiguously shell-ish (starts with curl / kubectl / npm / a flag
144+
* pattern / a `$` prompt). Anything else stays null so the monochrome
145+
* fallback path still works — never wrong-coloured.
146+
*
147+
* Background: the /docs content lives in InstaNode-dev/content. The
148+
* first two code blocks on /docs (quickstart.md, services.md) are curl
149+
* examples with an unlabelled fence, which left the first impression
150+
* of the docs as two blank-black code blocks. Fixing the markdown in
151+
* content+ requires a cross-repo PR and a content-repo redeploy; the
152+
* cheaper, safer fix is to sniff at render time inside the existing
153+
* markdown pipeline so unfenced curl examples get the bash badge +
154+
* syntax highlighting they should have had all along. */
155+
function sniffLang(code: string): string | null {
156+
const firstLine = code.split(/\r?\n/).find((l) => l.trim().length > 0)
157+
if (!firstLine) return null
158+
const trimmed = firstLine.trim()
159+
// curl / kubectl / npm / docker / git / brew / sudo / a $-prompt all
160+
// start unambiguously shell-ish. Keep this list short and obvious —
161+
// anything weird stays monochrome.
162+
if (/^(curl|kubectl|npm|docker|git|brew|sudo|cd|ls|cat|echo|export|node|go|make|bash|sh|\$)\b/.test(trimmed)) {
163+
return 'bash'
164+
}
165+
return null
166+
}
167+
129168
function headingTag(level: number, key: string, content: string): ReactNode {
130169
const clamped = Math.min(Math.max(level, 1), 6)
131170
const Tag = `h${clamped}` as Heading

src/pages/MarketingPage.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,10 @@ const SERVICES: Service[] = [
9999
{ id: 'rd', name: 'Redis', curl: 'POST /cache/new', liveIn: '0.9s' },
100100
{ id: 'mg', name: 'MongoDB', curl: 'POST /nosql/new', liveIn: '1.2s' },
101101
{ id: 'qu', name: 'Queue (NATS)', curl: 'POST /queue/new', liveIn: '0.7s' },
102-
{ id: 'st', name: 'Storage (S3)', curl: 'POST /storage/new', liveIn: '0.8s' },
102+
// PB04 P2 (2026-05-21): label was 'Storage (S3)' which implied AWS;
103+
// the live backend is DO Spaces (S3-compatible API, not AWS S3).
104+
// Renamed to avoid the brand-tie-in confusion.
105+
{ id: 'st', name: 'Storage (S3-compatible)', curl: 'POST /storage/new', liveIn: '0.8s' },
103106
{ id: 'wh', name: 'Webhook', curl: 'POST /webhook/new', liveIn: '0.3s' },
104107
{ id: 'dp', name: 'Deploy', curl: 'POST /deploy/new', liveIn: '<10s' },
105108
]

src/pages/PricingPage.tsx

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -79,16 +79,18 @@ const TIERS: {
7979
ctaHrefYearly: '/app/checkout?plan=pro&frequency=yearly',
8080
highlighted: true,
8181
},
82-
// Team tier — not launched yet. We intentionally show NO pricing, NO
83-
// detail, NO CTA. A single quiet "coming soon" placeholder is enough to
84-
// signal the tier exists without making any commitment customers can
85-
// hold us to. The per-row SOON markers in the matrix below render as
86-
// empty cells for the same reason.
82+
// Team tier — not launched yet, but PB04 P3 (2026-05-21) surfaced
83+
// the price from plans.yaml ($199/mo) so the "soon" badge reads as an
84+
// availability label rather than a vaporware signal. The "soon" badge
85+
// (from t.comingSoon below) renders next to the tier name; the price
86+
// itself stays a real number sourced from plans.yaml. Matches the
87+
// existing Hobby/Pro {price, sub} pattern. The CTA stays empty until
88+
// launch — interested customers can email support.
8789
{
8890
key: 'team',
8991
name: 'Team',
90-
monthly: { price: 'coming soon', sub: '' },
91-
// No yearly — there's nothing to bill annually for an unlaunched tier.
92+
monthly: { price: '$199', sub: '/ mo' },
93+
// No yearly — annual billing wires up at launch.
9294
cta: '',
9395
ctaHrefMonthly: '',
9496
comingSoon: true,
@@ -128,7 +130,10 @@ const ROWS: Row[] = [
128130
// moves to the field we actually enforce. Numbers mirror plans.yaml
129131
// queue_storage_mb (anonymous=1024, hobby=5120, pro=10240).
130132
{ label: 'Queue', sub: 'NATS storage', values: ['1 GB / 24h TTL', '5 GB', '10 GB', SOON] },
131-
{ label: 'Storage', values: [{ mark: 'dash' }, '512 MB', '50 GB', SOON] },
133+
// Anonymous storage: plans.yaml storage_storage_mb=10 (anonymous tier).
134+
// PB04 P1 (2026-05-21): cell used to render '—' which contradicted the
135+
// shipped backend — anonymous /storage/new returns a real 10 MB bucket.
136+
{ label: 'Storage', values: ['10 MB / 24h TTL', '512 MB', '50 GB', SOON] },
132137
{ label: 'Webhook stored', values: ['100', '1 000', '10k', SOON] },
133138
{ label: 'Deploy apps', values: [{ mark: 'dash' }, '1 small', '10 medium', SOON] },
134139
{ label: 'Domains', values: [{ mark: 'dash' }, '*.deployment.instanode.dev', 'custom domain', SOON] },

0 commit comments

Comments
 (0)