SPB Git forge

spb/fetcha

Public
11commits 1branches 0releases
1.5 MBsize
maindefault branch
16 days agolast push
TypeScript 97.5% SQL 1.4% Python 0.8%
24.1 KB · 367 lines tsx
Raw Blame History
1import type { Metadata } from "next";2import Link from "next/link";3import {4  Activity,5  ArrowRight,6  Braces,7  Building2,8  Check,9  Clock,10  EyeOff,11  Fingerprint,12  Globe,13  Layers,14  MapPin,15  MonitorSmartphone,16  Network,17  RefreshCw,18  ShieldCheck,19  Terminal,20  Waypoints,21} from "lucide-react";22import { Button } from "@/components/ui/button";23import { Badge } from "@/components/ui/badge";24import { CodeBlock } from "@/components/ui/code-block";25import { HeroCodeTabs } from "@/components/marketing/hero-code-tabs";26import { ArchitectureDiagram } from "@/components/marketing/architecture-diagram";27import { Reveal } from "@/components/marketing/reveal";28import { cn } from "@/lib/utils";2930export const metadata: Metadata = {31  title: { absolute: "Fetcha — The web, accessible through one API." },32  description:33    "Fetcha intelligently routes every request across premium proxy networks, sessions and a managed browser to deliver reliable web access at scale. Private, invitation-only platform.",34  alternates: { canonical: "/" },35};3637/* ------------------------------------------------------------------ */38/* Data                                                                */39/* ------------------------------------------------------------------ */4041const SCORE_WEIGHTS = [42  { label: "Historical success", weight: 35, tone: "bg-accent" },43  { label: "Cost", weight: 20, tone: "bg-accent/80" },44  { label: "Latency", weight: 15, tone: "bg-accent/65" },45  { label: "Network health", weight: 15, tone: "bg-accent/50" },46  { label: "Geography", weight: 10, tone: "bg-accent/35" },47  { label: "Session stability", weight: 5, tone: "bg-accent/20" },48] as const;4950const ESCALATION: ReadonlyArray<{ step: string; title: string; body: string; cost: string; state: "live" | "soon" }> = [51  { step: "Attempt 1", title: "Best-scored route", body: "The cheapest route with a strong success history for this domain, from the requested geography.", cost: "~$0.0005", state: "live" },52  { step: "Attempt 2", title: "Alternate route, fresh IP", body: "Blocked or timed out? Fetcha rotates the exit IP and switches to an alternate route in the same class.", cost: "~$0.0009", state: "live" },53  { step: "Attempt 3", title: "Premium network", body: "Escalates to a premium residential route pinned to the target country, with tuned headers and pacing.", cost: "~$0.002", state: "live" },54  { step: "Attempt 4", title: "Managed browser rendering", body: "When the block is a JavaScript challenge, a managed headless Chromium renders the page on the same route, solves the challenge and returns the rendered DOM.", cost: "~$0.004", state: "live" },55];5657const FEATURES: Array<{ icon: React.ComponentType<{ className?: string }>; title: string; body: string; soon?: boolean; detail?: string }> = [58  { icon: Globe, title: "Global proxy network", body: "Residential exits in 45 target countries, aggregated behind one endpoint. Fetcha manages upstream capacity, so you never juggle vendors or IP pools.", detail: "residential · live" },59  { icon: Layers, title: "Session management", body: "Sticky sessions keep the same exit IP for up to 30 minutes. Create one with POST /v1/sessions and pass its id on every follow-up request.", detail: "ttl 60–1800 s" },60  { icon: MapPin, title: "Geo targeting", body: "Pin requests to a country, region or city with ISO codes. Prices, availability and search results as your users actually see them.", detail: "country · region · city" },61  { icon: RefreshCw, title: "Automatic retries", body: "Blocks, CAPTCHAs, challenges and timeouts trigger retries on a new IP, a new header profile or an alternate network, with jittered backoff, within the request timeout.", detail: "up to 5 retries" },62  { icon: MonitorSmartphone, title: "Managed browser rendering", body: "Render JavaScript-heavy pages in a managed headless Chromium with the same routing, geo and session controls. Blocked HTTP attempts escalate to it automatically; screenshots on request.", detail: "browser: true · auto-escalation" },63  { icon: Network, title: "Crawl & map", body: "Turn a whole site into Markdown, text or HTML with one asynchronous job, or list its URLs from the sitemap and links. Every page is a normal, logged request.", detail: "/v1/crawl · /v1/map" },64  { icon: Terminal, title: "Developer playground", body: "Run real requests from the dashboard, inspect attempts, modes and timings, then copy production-ready code for your language.", detail: "dashboard" },65  { icon: Activity, title: "Observability", body: "Every call carries a request id. Logs show status, network class, mode, attempts, latency and bytes, with sensitive headers redacted.", detail: "X-Fetcha-Request-ID" },66  { icon: Braces, title: "Structured extraction", body: "Turn fetched pages into typed JSON with a schema you define. The /v1/extract endpoint is on the roadmap and not yet callable.", soon: true },67  { icon: Building2, title: "Private platform", body: "Access is granted by invitation. One unlimited plan: no request quota, 200 concurrent requests, all network classes, 90-day retention, browser and crawl included. No billing.", detail: "hello@fetcha.co" },68];6970const STEPS = [71  { title: "Get invited", body: "An administrator adds your email." },72  { title: "Log in", body: "Set your password, confirm your email." },73  { title: "Create a key", body: "Scoped fch_live_ or fch_test_ keys." },74  { title: "Open the Playground", body: "Try a URL and a country." },75  { title: "Run", body: "Inspect attempts, timing, bytes." },76  { title: "Copy code", body: "Generated cURL, JavaScript or Python." },77];7879const SECURITY = [80  { icon: ShieldCheck, title: "SSRF protection", body: "Only http and https. Localhost, private ranges, link-local, metadata and internal hosts are refused, and every redirect is re-validated." },81  { icon: EyeOff, title: "Redacted logs", body: "Authorization, Cookie and other sensitive headers are stripped before anything is written. Response bodies are not stored by default." },82  { icon: Fingerprint, title: "Request IDs", body: "A unique req_ id on every response header and every log line, so support and audits point at exactly one call." },83  { icon: Clock, title: "90-day retention", body: "Request metadata and crawl results are kept 90 days, then deleted. Response bodies are not stored unless you enable it per project." },84];8586const SESSION_SNIPPET = `# 1. Open a sticky session pinned to Canada87curl -X POST https://www.fetcha.co/v1/sessions \\88  -H "Authorization: Bearer fch_live_..." \\89  -d '{ "country": "CA", "ttl": 900, "label": "checkout-flow" }'90# → { "id": "sess_01j8x5...", "status": "active", "expires_at": "..." }9192# 2. Reuse the same exit IP across requests93curl -X POST https://www.fetcha.co/v1/fetch \\94  -H "Authorization: Bearer fch_live_..." \\95  -d '{ "url": "https://shop.example/cart", "session": "sess_01j8x5..." }'`;9697/* ------------------------------------------------------------------ */98/* Small presentational helpers                                        */99/* ------------------------------------------------------------------ */100101function Eyebrow({ children, className }: { children: React.ReactNode; className?: string }) {102  return <p className={cn("text-[11.5px] font-semibold uppercase tracking-[0.12em] text-accent", className)}>{children}</p>;103}104105function SectionHeading({ eyebrow, title, body, id, align = "left" }: { eyebrow: string; title: string; body?: string; id?: string; align?: "left" | "center" }) {106  return (107    <Reveal className={cn("max-w-2xl", align === "center" && "mx-auto text-center")}>108      <Eyebrow>{eyebrow}</Eyebrow>109      <h2 id={id} className="mt-2 text-[28px] font-semibold leading-[1.15] tracking-tight sm:text-[34px]">110        {title}111      </h2>112      {body ? <p className="mt-3 text-[15.5px] leading-relaxed text-fg-muted">{body}</p> : null}113    </Reveal>114  );115}116117/* ------------------------------------------------------------------ */118/* Page                                                                */119/* ------------------------------------------------------------------ */120121export default function HomePage() {122  return (123    <>124      {/* ---------------------------------------------------------------- Hero */}125      <section className="relative overflow-hidden border-b border-border" aria-labelledby="hero-title">126        <div className="absolute inset-x-0 top-0 -z-10 h-[520px] grid-bg" aria-hidden />127        <div className="container-page grid items-center gap-12 py-16 lg:grid-cols-[minmax(0,1fr)_minmax(0,1.05fr)] lg:gap-14 lg:py-24">128          <div className="animate-fade-up">129            <Eyebrow>Intelligent web access infrastructure</Eyebrow>130            <h1 id="hero-title" className="mt-4 text-[38px] font-semibold leading-[1.05] tracking-[-0.03em] sm:text-[52px] lg:text-[56px]">131              The web, accessible through one API.132            </h1>133            <p className="mt-5 max-w-xl text-[16.5px] leading-relaxed text-fg-muted sm:text-[17.5px]">134              Fetcha intelligently routes every request across premium proxy networks, sticky sessions and a managed browser to deliver reliable web access at scale. Fetch, render, crawl.135            </p>136            <div className="mt-8 flex flex-wrap items-center gap-3">137              <Button asChild size="lg">138                <Link href="/login">139                  Log in <ArrowRight aria-hidden />140                </Link>141              </Button>142              <Button asChild size="lg" variant="outline">143                <a href="mailto:hello@fetcha.co?subject=Fetcha%20access">Request access</a>144              </Button>145              <Link href="/docs" className="text-[14px] font-medium text-fg-muted underline-offset-4 hover:text-fg hover:underline">146                View documentation147              </Link>148            </div>149            <dl className="mt-10 grid max-w-md grid-cols-3 gap-4 border-t border-border pt-6">150              {[151                ["Unlimited", "requests, one plan"],152                ["45", "target countries"],153                ["1 call", "to get a page"],154              ].map(([v, l]) => (155                <div key={l}>156                  <dt className="sr-only">{l}</dt>157                  <dd className="font-mono text-[20px] font-semibold tabular tracking-tight text-fg">{v}</dd>158                  <dd className="mt-0.5 text-[12.5px] text-fg-muted">{l}</dd>159                </div>160              ))}161            </dl>162          </div>163          <div className="min-w-0 animate-fade-up [animation-delay:120ms]">164            <HeroCodeTabs />165          </div>166        </div>167      </section>168169      {/* ------------------------------------------------------------ Platform */}170      <section id="platform" className="scroll-mt-16 border-b border-border py-20 lg:py-28" aria-labelledby="platform-title">171        <div className="container-page">172          <SectionHeading eyebrow="Platform" id="platform-title" title="One request in. The right route out." body="You send a URL. Fetcha authenticates, validates and scores every available route for that domain, then fetches through the network class most likely to succeed at the lowest cost." />173          <Reveal className="mt-12" delay={0.05}>174            <ArchitectureDiagram />175          </Reveal>176        </div>177      </section>178179      {/* -------------------------------------------------------- Routing split */}180      <section className="border-b border-border py-20 lg:py-28" aria-labelledby="routing-title">181        <div className="container-page grid gap-12 lg:grid-cols-2 lg:items-center lg:gap-20">182          <SectionHeading eyebrow="Intelligent routing" id="routing-title" title="Every route gets a score. The best one gets your request." body="Fetcha keeps a profile for every domain it has seen: which routes succeeded, how fast, at what price, from where. In auto mode each request is scored on six weighted signals and sent down the winning path. Circuit breakers take unhealthy routes out of rotation automatically." />183          <Reveal delay={0.05}>184            <figure className="rounded-lg border border-border bg-bg-elevated p-5 shadow-xs sm:p-6">185              <figcaption className="flex items-center justify-between text-[12px] text-fg-subtle">186                <span className="font-medium uppercase tracking-wide">Routing score weights</span>187                <span className="font-mono tabular">Σ = 100</span>188              </figcaption>189              <div className="mt-4 flex h-3 w-full overflow-hidden rounded-sm bg-bg-muted" role="img" aria-label="Score weights: historical success 35%, cost 20%, latency 15%, network health 15%, geography 10%, session stability 5%">190                {SCORE_WEIGHTS.map((w) => (191                  <div key={w.label} className={cn("h-full border-r border-bg last:border-r-0", w.tone)} style={{ width: `${w.weight}%` }} />192                ))}193              </div>194              <ol className="mt-5 grid gap-2.5 sm:grid-cols-2">195                {SCORE_WEIGHTS.map((w) => (196                  <li key={w.label} className="flex items-center justify-between gap-3 text-[13.5px]">197                    <span className="flex items-center gap-2.5 text-fg-muted">198                      <span className={cn("size-2.5 rounded-[3px]", w.tone)} aria-hidden />199                      {w.label}200                    </span>201                    <span className="font-mono tabular font-medium text-fg">{w.weight}%</span>202                  </li>203                ))}204              </ol>205              <p className="mt-5 border-t border-border pt-4 text-[12.5px] leading-relaxed text-fg-subtle">206                Scores are recomputed continuously from your own traffic and platform-wide health. Force a class with <code className="font-mono text-fg-muted">{'network: "residential"'}</code> when you need to.207              </p>208            </figure>209          </Reveal>210        </div>211      </section>212213      {/* ----------------------------------------------------- Escalation split */}214      <section className="border-b border-border bg-bg-subtle/50 py-20 lg:py-28" aria-labelledby="escalation-title">215        <div className="container-page grid gap-12 lg:grid-cols-[minmax(0,1.1fr)_minmax(0,0.9fr)] lg:items-start lg:gap-20">216          <Reveal>217            <ol className="relative border-l border-border pl-6" aria-label="How a request escalates">218              {ESCALATION.map((e, i) => (219                <li key={e.step} className={cn("relative pb-8 last:pb-0", e.state === "soon" && "opacity-80")}>220                  <span className={cn("absolute -left-[31px] top-0.5 flex size-[22px] items-center justify-center rounded-full border bg-bg text-[11px] font-mono font-medium tabular", e.state === "soon" ? "border-border-strong text-fg-subtle" : "border-accent text-accent")} aria-hidden>221                    {i + 1}222                  </span>223                  <div className="flex flex-wrap items-center gap-x-3 gap-y-1">224                    <span className="font-mono text-[11.5px] uppercase tracking-wide text-fg-subtle">{e.step}</span>225                    <h3 className="text-[15.5px] font-semibold tracking-tight">{e.title}</h3>226                    {e.state === "soon" ? <Badge variant="outline">Coming soon</Badge> : null}227                  </div>228                  <p className="mt-1.5 max-w-lg text-[14px] leading-relaxed text-fg-muted">{e.body}</p>229                  <p className="mt-2 font-mono text-[12.5px] tabular text-fg-subtle">230                    Illustrative cost: <span className={e.state === "soon" ? "" : "text-fg"}>{e.cost}</span>231                  </p>232                </li>233              ))}234            </ol>235          </Reveal>236          <div className="lg:sticky lg:top-24">237            <SectionHeading eyebrow="Escalation" id="escalation-title" title="Cheap first. Premium and browser only when they earn their keep." body="A block is not a failure, it is a signal. Fetcha starts on the most economical route and escalates step by step, up to a full browser render for JavaScript challenges, so heavy infrastructure is used only for the requests that need it. A blocked target still returns 200 with success: false, the upstream status, the attempt count and the block reason, never a silent retry loop." />238            <Reveal delay={0.1} className="mt-6">239              <p className="text-[12.5px] leading-relaxed text-fg-subtle">Costs shown are illustrative upstream averages per request for a typical 200 KB HTML page, for transparency. Fetcha is a private platform with a single unlimited plan; there is no per-request billing.</p>240            </Reveal>241          </div>242        </div>243      </section>244245      {/* -------------------------------------------------------- Feature grid */}246      <section className="border-b border-border py-20 lg:py-28" aria-labelledby="features-title">247        <div className="container-page">248          <SectionHeading eyebrow="Capabilities" id="features-title" title="Everything between your code and the page." body="Built for engineers who would rather ship features than babysit proxies. What is live is live; what is coming is labelled." />249          <ul className="mt-12 grid gap-px overflow-hidden rounded-lg border border-border bg-border sm:grid-cols-2 lg:grid-cols-3" role="list">250            {FEATURES.map((f) => (251              <li key={f.title} className={cn("relative flex flex-col bg-bg p-6", f.soon && "bg-bg-subtle/60")}>252                <div className="flex items-center justify-between">253                  <f.icon className={cn("size-[18px]", f.soon ? "text-fg-subtle" : "text-accent")} aria-hidden />254                  {f.soon ? <Badge variant="outline">Coming soon</Badge> : f.detail ? <span className="font-mono text-[11px] text-fg-subtle">{f.detail}</span> : null}255                </div>256                <h3 className={cn("mt-4 text-[15.5px] font-semibold tracking-tight", f.soon && "text-fg-muted")}>{f.title}</h3>257                <p className="mt-1.5 text-[13.5px] leading-relaxed text-fg-muted">{f.body}</p>258              </li>259            ))}260          </ul>261        </div>262      </section>263264      {/* ------------------------------------------------------- Sessions split */}265      <section className="border-b border-border py-20 lg:py-28" aria-labelledby="sessions-title">266        <div className="container-page grid gap-12 lg:grid-cols-2 lg:items-center lg:gap-20">267          <div className="order-2 lg:order-1">268            <Reveal>269              <CodeBlock code={SESSION_SNIPPET} lang="bash" title="sticky-session.sh" />270            </Reveal>271          </div>272          <div className="order-1 lg:order-2">273            <SectionHeading eyebrow="Sessions and geo" id="sessions-title" title="Same IP, same country, across a whole flow." body="Multi-step journeys need a consistent identity: log in, paginate, add to cart, read the price. Sticky sessions hold one residential exit for up to 30 minutes, pinned to the country, region or city you choose, and expire on their own when you are done." />274            <Reveal delay={0.05} className="mt-6">275              <ul className="grid gap-2 text-[14px] text-fg-muted">276                {["ISO country, region and city targeting on every request", "Sessions are scoped to your project and visible in the dashboard", "Idempotency-Key supported on session creation", "Expired sessions return SESSION_EXPIRED, never a silent new IP"].map((t) => (277                  <li key={t} className="flex items-start gap-2.5">278                    <Check className="mt-0.5 size-4 shrink-0 text-success" aria-hidden />279                    {t}280                  </li>281                ))}282              </ul>283            </Reveal>284          </div>285        </div>286      </section>287288      {/* -------------------------------------------------- Developer experience */}289      <section className="border-b border-border bg-bg-subtle/50 py-20 lg:py-28" aria-labelledby="dx-title">290        <div className="container-page">291          <SectionHeading eyebrow="Developer experience" id="dx-title" title="From invitation to a working request in a few minutes." align="center" />292          <Reveal className="mt-12" delay={0.05}>293            <ol className="grid gap-px overflow-hidden rounded-lg border border-border bg-border sm:grid-cols-3 lg:grid-cols-6">294              {STEPS.map((s, i) => (295                <li key={s.title} className="flex flex-col gap-2 bg-bg p-5">296                  <span className="font-mono text-[12px] font-medium tabular text-accent">0{i + 1}</span>297                  <span className="text-[14.5px] font-semibold tracking-tight">{s.title}</span>298                  <span className="text-[12.5px] leading-snug text-fg-muted">{s.body}</span>299                </li>300              ))}301            </ol>302          </Reveal>303          <Reveal className="mt-8 flex flex-wrap items-center justify-center gap-x-6 gap-y-2 text-[13px] text-fg-muted" delay={0.1}>304            <span className="flex items-center gap-2"><Waypoints className="size-4 text-fg-subtle" aria-hidden /> SDKs for JavaScript and Python (install from source until published)</span>305            <span className="flex items-center gap-2"><Terminal className="size-4 text-fg-subtle" aria-hidden /> OpenAPI-style reference with every error code</span>306          </Reveal>307        </div>308      </section>309310      {/* -------------------------------------------------------------- Security */}311      <section className="border-b border-border py-16 lg:py-20" aria-labelledby="security-title">312        <div className="container-page">313          <div className="flex flex-col gap-8 lg:flex-row lg:items-start lg:gap-16">314            <Reveal className="lg:w-72 lg:shrink-0">315              <Eyebrow>Security and compliance</Eyebrow>316              <h2 id="security-title" className="mt-2 text-[24px] font-semibold leading-tight tracking-tight">317                Defaults you do not have to think about.318              </h2>319              <p className="mt-3 text-[14px] leading-relaxed text-fg-muted">320                Read the <Link href="/legal/privacy" className="text-accent underline-offset-4 hover:underline">Privacy Policy</Link>, <Link href="/legal/acceptable-use" className="text-accent underline-offset-4 hover:underline">Acceptable Use</Link> and <Link href="/legal/dpa" className="text-accent underline-offset-4 hover:underline">DPA</Link>.321              </p>322            </Reveal>323            <ul className="grid flex-1 gap-6 sm:grid-cols-2">324              {SECURITY.map((s, i) => (325                <Reveal as="li" key={s.title} delay={0.04 * i} className="flex gap-3.5">326                  <s.icon className="mt-0.5 size-[18px] shrink-0 text-fg" aria-hidden />327                  <div>328                    <h3 className="text-[14.5px] font-semibold tracking-tight">{s.title}</h3>329                    <p className="mt-1 text-[13.5px] leading-relaxed text-fg-muted">{s.body}</p>330                  </div>331                </Reveal>332              ))}333            </ul>334          </div>335        </div>336      </section>337338      {/* -------------------------------------------------------------- Final CTA */}339      <section id="about" className="scroll-mt-16 py-20 lg:py-28" aria-labelledby="cta-title">340        <div className="container-page">341          <Reveal className="relative overflow-hidden rounded-xl border border-border bg-bg-elevated px-6 py-14 text-center shadow-xs sm:px-12">342            <div className="absolute inset-0 -z-10 grid-bg opacity-60" aria-hidden />343            <Eyebrow>Get started</Eyebrow>344            <h2 id="cta-title" className="mx-auto mt-3 max-w-2xl text-[30px] font-semibold leading-[1.1] tracking-tight sm:text-[40px]">345              Stop building proxy plumbing. Start fetching.346            </h2>347            <p className="mx-auto mt-4 max-w-xl text-[15.5px] leading-relaxed text-fg-muted">348              Fetcha is built and operated from Québec, Canada, by engineers who spent too many years maintaining scrapers. One endpoint, honest limits, and a private platform for invited teams.349            </p>350            <div className="mt-8 flex flex-wrap items-center justify-center gap-3">351              <Button asChild size="lg">352                <Link href="/login">353                  Log in <ArrowRight aria-hidden />354                </Link>355              </Button>356              <Button asChild size="lg" variant="outline">357                <a href="mailto:hello@fetcha.co?subject=Fetcha%20access">Request access</a>358              </Button>359            </div>360            <p className="mt-5 text-[13px] text-fg-subtle">Invitation-only. One unlimited plan, no billing.</p>361          </Reveal>362        </div>363      </section>364    </>365  );366}367