Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
2e45d89
feat(web): rewrite header with real helpbase nav
Codehagen Apr 19, 2026
12bd686
chore(web): install @magicui/terminal + 8 Tailark Pro blocks
Codehagen Apr 19, 2026
cba97a8
feat(web): marketing page v2 — hero terminal + narrative scroll + pri…
Codehagen Apr 19, 2026
115d18b
chore(deps): lock hugeicons + radix-tooltip installed for tailark blocks
Codehagen Apr 19, 2026
c0fb493
test(web): coverage for the three critical new surfaces
Codehagen Apr 19, 2026
22906e0
fix(sec): prefer cf-connecting-ip + scope cloudinary image allowlist
Codehagen Apr 19, 2026
23ba5e8
docs(changelog): unreleased — marketing page v2 + analytics + securit…
Codehagen Apr 19, 2026
91bc639
test(web): tighten non-null assertions on mock.calls access
Codehagen Apr 19, 2026
b0916cf
fix(ci): exclude marketing-only files from scaffold/registry sync
Codehagen Apr 19, 2026
c9bf189
fix(sec): drop marketing_events anon insert policy — edge fn owns writes
Codehagen Apr 19, 2026
c028983
feat(web): CodeRabbit fixes + hero copy + lazy-load + Pro price
Codehagen Apr 19, 2026
e35bf7c
feat(web): show llms.txt content instead of money illustration
Codehagen Apr 19, 2026
051aa08
chore: strip session-narrative comments from source
Codehagen Apr 19, 2026
e99cb3d
style(design): marketing page polish pass — typography, targets, anal…
Codehagen Apr 19, 2026
84eed16
copy(marketing): strip jargon — YC-style plain English across the page
Codehagen Apr 19, 2026
38f5e7d
copy(hero): agent-first positioning + ownership wedge
Codehagen Apr 19, 2026
6ccd72a
copy(hero): data-first H1 — flip Mintlify's Series B stat into our hook
Codehagen Apr 19, 2026
0bd865e
style(design): DR-W-001/002 — align comparator to 5xl, restore mobile…
Codehagen Apr 19, 2026
8dc53be
style(design): DR-W-003 — normalize FAQs mobile padding to px-6
Codehagen Apr 19, 2026
2ec783a
style(design): DR-W-004 — drop xl:px-0 on how-it-works
Codehagen Apr 19, 2026
711099c
style(design): DR-W-005 — pricing sub-paragraph max-w-xl \u2192 max-w…
Codehagen Apr 19, 2026
e1fdee9
style(design): remove floating hero decoration rails
Codehagen Apr 19, 2026
55bfb69
style(design): swap three weak-fit illustrations with Tailark Pro picks
Codehagen Apr 19, 2026
19e6146
style(design): swap 4 more illustrations with Tailark Pro picks (roun…
Codehagen Apr 19, 2026
e04c99c
style(design): custom MDX + Preview illustrations (LlmsTxtPreview pat…
Codehagen Apr 19, 2026
d170132
style(design): DR3-001 \u2014 shorten comparator feature names
Codehagen Apr 20, 2026
c6d30a8
style(design): DR3-002 \u2014 custom InstallCommandPreview replaces P…
Codehagen Apr 20, 2026
5968da1
style(design): DR3-003 \u2014 retheme FlowIllustration output from in…
Codehagen Apr 20, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,73 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added — web / marketing page v2 — 2026-04-19

- **New marketing page at `/`.** Terminal-hero with `pnpm dlx create-helpbase`
typing into a shadcn-framed Terminal (via `@magicui/terminal`), click-to-copy
primary CTA, `See the live demo →` secondary CTA. Announces OSS + live today.
- **Dual-front comparator.** A vertical table compares Helpbase against two
real status-quos side by side: rolling your own Next.js help center vs. a
hosted docs SaaS. Checks the ownership, MCP, llms.txt, cost, and "maintained
without you" rows.
- **How-it-works strip.** Scaffold → Preview → Deploy, 3 steps with
illustrations, mapped to the real CLI flow.
- **AI-native bento.** Five-cell grid explaining the MCP server, llms.txt,
structured agent output, `helpbase sync` (code-grounded doc diffs), and the
hosted tier escape hatch.
- **Demo cross-link.** A section that points visitors at `demo.helpbase.dev`
with a `curl /llms.txt` teaser of the structured agent output.
- **Pricing — open-core, three tiers.** Self-host (free, MIT), Hosted free
(one site on `{slug}.helpbase.dev`, hosted MCP included), and Hosted Pro
(coming soon — custom domain, team roles, analytics, priority support).
- **Grouped FAQ.** General / Hosted tier / MCP & AI, 9 questions covering OSS
vs hosted, ownership, MCP, and migration.
- **Real footer.** GitHub, Docs, Pricing, Privacy, newsletter form. Named the
built-on stack in the copyright line.

### Added — web / analytics

- **Supabase-native page analytics.** New `public.marketing_events` table
(insert-only for anon via RLS, service-role read only). New `track` edge
function validates an event allowlist, caps metadata at 2 KB, and derives a
session hash as `sha256(ip|ua|yyyy-mm-dd)` without storing raw PII.
- **Client helper `apps/web/lib/analytics.ts`.** `track(event, metadata)` uses
`keepalive: true` fetch and swallows every failure so analytics can never
break the page.

### Added — `@workspace/ui`

- **`CopyButton` primitive.** Wraps the button with `navigator.clipboard`
plus an `execCommand("copy")` fallback for older Safari / non-secure
contexts. Exposes an `onCopy` callback and a `data-copy-state` attribute
(`idle` / `copied` / `error`) for consumers.

### Changed — web / header

- **Killed placeholder nav.** The shadcn template left behind ten fake links
(`Automation`, `Scalability`, `Marketplace`, `Guides`, `Partnerships`, etc.)
and a `Continue` button pointing nowhere. Replaced with real helpbase nav:
Docs, Pricing (anchor), GitHub. Right-side: `Sign in` + `Deploy now`.

### Security

- **`cf-connecting-ip` precedence in edge analytics.** The `track` function
now prefers Cloudflare's authoritative IP header over the client-spoofable
`x-forwarded-for`, closing a trivial session-hash forgery.
- **Scoped Cloudinary allowlist in `next.config.mjs`.** Added pathname
constraint `/dohqjvu9k/**` so arbitrary Cloudinary content cannot be
proxied through `/_next/image`.

### For contributors

- **Shadcn add, from `apps/web`.** Registry lives in
`apps/web/components.json`, not the repo root. Document in your workflow:
`cd apps/web && pnpm dlx shadcn@latest add @tailark-pro/...`.
- **Test coverage + 26.** Net-new tests: `apps/web/test/analytics.test.ts`
(7), `apps/web/test/copy-button.test.tsx` (8), and
`apps/web/test/track-edge-handler.test.ts` (12). Handler refactored out of
the Deno `Deno.serve` entry so vitest can exercise it directly.

## [create-helpbase 0.5.0] — 2026-04-19

### Changed
Expand Down
4 changes: 2 additions & 2 deletions apps/web/app/(main)/docs/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,10 +73,10 @@ export default async function HomePage() {
<div className="absolute inset-0 bg-[radial-gradient(ellipse_80%_50%_at_50%_-20%,var(--muted),transparent)]" />
<div className="relative mx-auto max-w-6xl px-6 pb-20 pt-24 text-center">
<h1 className="animate-fade-in mx-auto max-w-3xl text-4xl font-bold tracking-tight sm:text-5xl">
The AI-native knowledge layer, as code you own.
The docs your AI tools can read.
</h1>
<p className="animate-fade-in-delay-1 mx-auto mt-4 max-w-2xl text-lg text-muted-foreground">
Helpbase ships MCP, <code className="rounded bg-muted px-1.5 py-0.5 font-mono text-base">llms.txt</code>, and codebase-grounded doc sync as primitives that run in your repo. Open source, self-hostable, built on shadcn/ui + Next.js.
Helpbase includes an MCP server, an <code className="rounded bg-muted px-1.5 py-0.5 font-mono text-base">llms.txt</code>, and a doc-sync tool that reads your source code. Open source, self-hostable, built on shadcn/ui + Next.js.
</p>

{/* Search bar in hero */}
Expand Down
41 changes: 39 additions & 2 deletions apps/web/app/(marketing)/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,42 @@
import HeroSection from "@/components/hero-section"
import dynamic from "next/dynamic"

import { Header } from "@/components/header"
import { Hero } from "@/components/marketing/hero"
import Comparator from "@/components/comparator-7"
import FooterSection from "@/components/footer"

// Below-fold sections load via next/dynamic so their JS code-splits out
// of the first-paint bundle. SSR stays on — the HTML ships pre-rendered;
// only client hydration defers.
const HowItWorks = dynamic(() => import("@/components/how-it-works-3"))
const FeaturesOwnIt = dynamic(() => import("@/components/features-1"))
const AiNativeBento = dynamic(() => import("@/components/bento-2"))
const DemoCrossLink = dynamic(() =>
import("@/components/marketing/demo-cross-link").then((m) => ({
default: m.DemoCrossLink,
})),
)
const Pricing = dynamic(() => import("@/components/pricing"))
const FAQs = dynamic(() => import("@/components/faqs-3"))

export default function LandingPage() {
return <HeroSection />
return (
<>
<Header />
<main
id="main"
role="main"
className="bg-background overflow-hidden">
<Hero />
<Comparator />
<HowItWorks />
<FeaturesOwnIt />
<AiNativeBento />
<DemoCrossLink />
<Pricing />
<FAQs />
</main>
<FooterSection />
</>
)
}
3 changes: 2 additions & 1 deletion apps/web/components.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"headers": {
"Authorization": "Bearer ${TAILARK_API_KEY}"
}
}
},
"@magicui": "https://magicui.design/r/{name}.json"
}
}
109 changes: 109 additions & 0 deletions apps/web/components/bento-2.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { AgentTasksIllustration } from "@/components/illustrations/agent-tasks"
import { ModelsIllustration } from "@/components/illustrations/models"
import { Card } from '@/components/ui/card'
import { UptimeIllustration } from "@/components/illustrations/uptime"
import { DocumentAnalysisIllustration } from "@/components/illustrations/document-analysis"

export default function AiNativeBento() {
return (
<section
aria-labelledby="ai-native-heading"
className="@container bg-background py-24">
<div className="mx-auto w-full max-w-5xl px-6">
<div className="mb-12 text-center">
<h2
id="ai-native-heading"
className="text-foreground text-3xl font-semibold md:text-4xl">
Claude and Cursor can read your docs. Day one.
</h2>
<p className="text-muted-foreground mx-auto mt-4 max-w-2xl text-balance text-lg">
Every helpbase site includes an MCP server and an llms.txt. Claude, Cursor, and ChatGPT can answer from your real docs the moment you deploy. You don&apos;t pay us for this later.
</p>
</div>
<div className="not-dark:*:bg-card/50 @xl:grid-cols-2 @3xl:grid-cols-6 grid gap-3">
<Card className="@3xl:col-span-2 grid grid-rows-[1fr_auto] gap-y-12 overflow-hidden rounded-2xl p-8">
<div className="relative -m-8 flex items-center justify-center p-8">
<Stripes />
<ModelsIllustration />
</div>
<div>
<h3 className="text-foreground text-lg font-semibold">MCP server, built in</h3>
<p className="text-muted-foreground mt-3">
Every helpbase site runs a Model Context Protocol endpoint. Point Claude Code or Cursor at the URL and they answer from your real docs instead of guessing.
</p>
</div>
</Card>
<Card className="@3xl:col-span-2 grid grid-rows-[1fr_auto] gap-y-12 overflow-hidden rounded-2xl p-8">
<div className="relative -m-8 flex items-center justify-center p-8">
<Stripes />
<LlmsTxtPreview />
</div>
<div>
<h3 className="text-foreground text-lg font-semibold">llms.txt, always fresh</h3>
<p className="text-muted-foreground mt-3">
A fresh /llms.txt on every build. It&apos;s the manifest AI agents already look for. You don&apos;t have to remember to write it.
</p>
</div>
</Card>
<Card className="@xl:col-span-2 grid grid-rows-[1fr_auto] gap-y-12 overflow-hidden rounded-2xl p-8">
<div className="relative -m-8 flex items-center justify-center p-8">
<Stripes />
<AgentTasksIllustration />
</div>
<div>
<h3 className="text-foreground text-lg font-semibold">Structured agent output</h3>
<p className="text-muted-foreground mt-3">
The MCP server exposes your content as typed tool calls: list articles, read a specific slug, search. No scraping, no guesswork.
</p>
</div>
</Card>
<Card className="@xl:col-span-2 @3xl:col-span-3 grid grid-rows-[1fr_auto] gap-8 rounded-2xl p-8">
<div className="-m-8 flex items-center justify-center p-8">
<DocumentAnalysisIllustration />
</div>
<div>
<h3 className="text-foreground text-lg font-semibold">Docs that stay in sync with code</h3>
<p className="text-muted-foreground mt-3">
helpbase sync reads your source and proposes MDX edits based on the actual functions and types. You review, you merge. AI assists, it does not author.
</p>
</div>
</Card>
<Card className="@xl:col-span-2 @3xl:col-span-3 grid grid-rows-[1fr_auto] gap-8 rounded-2xl p-8">
<div className="-m-8 flex items-center justify-center p-8">
<UptimeIllustration />
</div>
<div>
<h3 className="text-foreground text-lg font-semibold">Hosted tier, if you want it</h3>
<p className="text-muted-foreground mt-3">
helpbase deploy pushes the same app to {'{'}slug{'}'}.helpbase.dev. No servers to manage. Hosted MCP at scale, custom domain, team roles, analytics when you upgrade.
</p>
</div>
</Card>
</div>
</div>
</section>
)
}

const Stripes = () => (
<div
aria-hidden
className="opacity-3 absolute -inset-x-6 inset-y-0 bg-[repeating-linear-gradient(-45deg,var(--color-foreground),var(--color-foreground)_1px,transparent_1px,transparent_6px)] [mask-image:radial-gradient(ellipse_50%_50%_at_50%_50%,#000_70%,transparent_100%)]"
/>
)

const LlmsTxtPreview = () => (
<div className="ring-border bg-card relative z-10 w-full max-w-sm rounded-xl border border-transparent p-4 font-mono text-xs ring-1">
<div className="text-muted-foreground mb-2 text-[0.65rem] uppercase tracking-wider">llms.txt</div>
<div className="space-y-1">
<div className="text-foreground"># Your Product docs</div>
<div className="text-muted-foreground">Docs: https://docs.yourco.com</div>
<div className="text-muted-foreground">MCP: https://docs.yourco.com/api/mcp</div>
<div className="text-foreground mt-2">## Articles</div>
<div className="text-muted-foreground">- getting-started</div>
<div className="text-muted-foreground">- authentication</div>
<div className="text-muted-foreground">- webhooks</div>
<div className="text-muted-foreground">- rate-limits</div>
</div>
</div>
)
92 changes: 92 additions & 0 deletions apps/web/components/code-block.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
'use client'

import { cn } from '@workspace/ui/lib/utils'
import { toJsxRuntime } from 'hast-util-to-jsx-runtime'
import { JSX, useLayoutEffect, useState } from 'react'
import { Fragment, jsx, jsxs } from 'react/jsx-runtime'
import type { BundledLanguage } from 'shiki/bundle/web'

const highlightCache = new Map()

let shikiPromise: Promise<typeof import('shiki/bundle/web')> | null = null

function getShiki() {
if (!shikiPromise) {
shikiPromise = import('shiki/bundle/web')
}
return shikiPromise
}

export async function highlight(code: string, lang: BundledLanguage) {
const cacheKey = `${lang}:${code.length}:${code.slice(0, 50)}:${code.slice(-50)}`

const cached = highlightCache.get(cacheKey)
if (cached) return cached

const { codeToHast } = await getShiki()

const hast = await codeToHast(code, {
lang,
themes: {
light: 'github-light',
dark: 'vesper',
},
})

const result = toJsxRuntime(hast, {
Fragment,
jsx,
jsxs,
}) as JSX.Element

if (highlightCache.size > 100) {
const firstKey = highlightCache.keys().next().value
if (firstKey) highlightCache.delete(firstKey)
}
highlightCache.set(cacheKey, result)
Comment on lines +20 to +46
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Use a collision-free cache key for highlighted code.

The current key only fingerprints the length plus first/last 50 chars. Two different snippets with the same prefix/suffix/length will return the first snippet’s cached JSX, rendering incorrect code. Also, the cache can grow to 101 entries because eviction happens only when size > 100.

🐛 Proposed fix
 export async function highlight(code: string, lang: BundledLanguage) {
-    const cacheKey = `${lang}:${code.length}:${code.slice(0, 50)}:${code.slice(-50)}`
+    const cacheKey = `${lang}:${code}`
 
     const cached = highlightCache.get(cacheKey)
     if (cached) return cached
@@
-    if (highlightCache.size > 100) {
+    if (highlightCache.size >= 100) {
         const firstKey = highlightCache.keys().next().value
         if (firstKey) highlightCache.delete(firstKey)
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/components/code-block.tsx` around lines 20 - 46, The cache key in
highlight() is collision-prone and eviction off-by-one: replace the brittle
`${lang}:${code.length}:${code.slice(0,50)}:${code.slice(-50)}` with a
collision-free key (e.g., compute a stable hash of the full code plus lang, such
as SHA-1 or another fast hash, and use `${lang}:${hash}`) so different snippets
never collide, and enforce eviction deterministically (check highlightCache.size
>= 100 before inserting or trim after set to keep max 100 entries) around the
existing highlightCache.set(cacheKey, result) call.


return result
}

type Props = {
code: string | null
lang: BundledLanguage
initial?: JSX.Element
preHighlighted?: JSX.Element | null
maxHeight?: number
className?: string
theme?: string
lineNumbers?: boolean // ← added
}

export default function CodeBlock({ code, lang, initial, maxHeight=940, preHighlighted, theme, className }: Props) {
const [content, setContent] = useState(preHighlighted || initial || null)

useLayoutEffect(() => {
if (preHighlighted) {
return
}

let isMounted = true

if (code) {
highlight(code, lang).then((result) => {
if (isMounted) setContent(result)
})
}

return () => {
isMounted = false
}
}, [code, lang, theme, preHighlighted])

return content ? (
<div
className={cn('[&_pre]:no-scrollbar max-h-(--pre-max-height) [&_code]:text-[13px]/2 [&_code]:font-mono [&_pre]:border-l [&_pre]:p-2 [&_pre]:leading-snug', className)}
style={{ '--pre-max-height': `${maxHeight}px` } as React.CSSProperties}>
{content}
</div>
) : (
<pre className="rounded-lg p-4 text-xs">Loading...</pre>
)
}
Loading
Loading