Add pricing page (#58)

* refactor: move lighthouse audits to dataforseo (#43)

* refactor: move lighthouse audits to dataforseo

* chore: remove obsolete audit settings modal

* refactor: rename psi flows to lighthouse

* save

* refactor: simplify audit lighthouse storage flow

* fix: separate lighthouse metrics from actionable audits

* refactor: remove redundant audit project inputs

* feat: redesign lighthouse issues screen with score gauges and table layout

Replace flat score cards with circular SVG gauges, condense metrics into
a compact grid, and switch issue list from cards to an expandable table
with fixed column widths.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* test: harden lighthouse regression coverage

* fix: restore project-scoped audit inputs

* refactor: simplify lighthouse payload handling

* refactor: inline lighthouse server handlers

* refactor: share audit workflow types

* refactor: simplify lighthouse payload flows

* save

* refactor: drop project pagespeed api key

* fix: restore lighthouse issues loading with resilient project context

* fix: restore audit issues back navigation

* refactor: simplify project context and lighthouse error handling

* fix: tolerate DataForSEO lighthouse payload drift

* refactor: route audit lighthouse through dataforseo client

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor: move DataForSEO response cache from KV to R2 (#46)

* refactor: move lighthouse audits to dataforseo (#43)

* refactor: move lighthouse audits to dataforseo

* chore: remove obsolete audit settings modal

* refactor: rename psi flows to lighthouse

* save

* refactor: simplify audit lighthouse storage flow

* fix: separate lighthouse metrics from actionable audits

* refactor: remove redundant audit project inputs

* feat: redesign lighthouse issues screen with score gauges and table layout

Replace flat score cards with circular SVG gauges, condense metrics into
a compact grid, and switch issue list from cards to an expandable table
with fixed column widths.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* test: harden lighthouse regression coverage

* fix: restore project-scoped audit inputs

* refactor: simplify lighthouse payload handling

* refactor: inline lighthouse server handlers

* refactor: share audit workflow types

* refactor: simplify lighthouse payload flows

* save

* refactor: drop project pagespeed api key

* fix: restore lighthouse issues loading with resilient project context

* fix: restore audit issues back navigation

* refactor: simplify project context and lighthouse error handling

* fix: tolerate DataForSEO lighthouse payload drift

* refactor: route audit lighthouse through dataforseo client

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor: move DataForSEO response cache from KV to R2

KV TTL-based expiry is imprecise for cache freshness. Switch to R2 with
soft TTL via custom metadata (expiresAt) and a 7-day lifecycle rule for
cleanup. Cache objects live under the `dataforseo-cache/` prefix,
separate from durable audit payloads in `site-audit/`.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor: use Workers crypto for R2 cache keys

Keep the cache helper aligned with the Cloudflare runtime and avoid pulling Worker bindings into Vitest. Also clarify that R2 lifecycle cleanup is optional but recommended to control storage growth.

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor: move hosted billing to Autumn hooks (#48)

* refactor: move lighthouse audits to dataforseo (#43)

* refactor: move lighthouse audits to dataforseo

* chore: remove obsolete audit settings modal

* refactor: rename psi flows to lighthouse

* save

* refactor: simplify audit lighthouse storage flow

* fix: separate lighthouse metrics from actionable audits

* refactor: remove redundant audit project inputs

* feat: redesign lighthouse issues screen with score gauges and table layout

Replace flat score cards with circular SVG gauges, condense metrics into
a compact grid, and switch issue list from cards to an expandable table
with fixed column widths.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* test: harden lighthouse regression coverage

* fix: restore project-scoped audit inputs

* refactor: simplify lighthouse payload handling

* refactor: inline lighthouse server handlers

* refactor: share audit workflow types

* refactor: simplify lighthouse payload flows

* save

* refactor: drop project pagespeed api key

* fix: restore lighthouse issues loading with resilient project context

* fix: restore audit issues back navigation

* refactor: simplify project context and lighthouse error handling

* fix: tolerate DataForSEO lighthouse payload drift

* refactor: route audit lighthouse through dataforseo client

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor: move hosted billing to Autumn hooks

* fix: satisfy ci checks for hosted billing

* refactor: simplify hosted Autumn billing integration

Limit Autumn wiring to the billing route and remove billing-specific indirection so the hosted billing flow is easier to reason about. Stop sending organization IDs as fake customer names to keep customer identity data honest.

* fix: satisfy ci line-limit check for hosted billing

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Add pricing page

* merge

* Revert home page back to normal

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ben Senescu 2026-04-02 00:08:30 -04:00 committed by Ben Senescu
parent a65c5fb00a
commit add176c3cc
2 changed files with 227 additions and 0 deletions

View File

@ -10,6 +10,7 @@
import { Route as rootRouteImport } from './routes/__root' import { Route as rootRouteImport } from './routes/__root'
import { Route as PrivacyRouteImport } from './routes/privacy' import { Route as PrivacyRouteImport } from './routes/privacy'
import { Route as PricingRouteImport } from './routes/pricing'
import { Route as IndexRouteImport } from './routes/index' import { Route as IndexRouteImport } from './routes/index'
import { Route as BlogsIndexRouteImport } from './routes/blogs/index' import { Route as BlogsIndexRouteImport } from './routes/blogs/index'
import { Route as JsScriptDotjsRouteImport } from './routes/js/script[.]js' import { Route as JsScriptDotjsRouteImport } from './routes/js/script[.]js'
@ -22,6 +23,11 @@ const PrivacyRoute = PrivacyRouteImport.update({
path: '/privacy', path: '/privacy',
getParentRoute: () => rootRouteImport, getParentRoute: () => rootRouteImport,
} as any) } as any)
const PricingRoute = PricingRouteImport.update({
id: '/pricing',
path: '/pricing',
getParentRoute: () => rootRouteImport,
} as any)
const IndexRoute = IndexRouteImport.update({ const IndexRoute = IndexRouteImport.update({
id: '/', id: '/',
path: '/', path: '/',
@ -55,6 +61,7 @@ const ApiEventRoute = ApiEventRouteImport.update({
export interface FileRoutesByFullPath { export interface FileRoutesByFullPath {
'/': typeof IndexRoute '/': typeof IndexRoute
'/pricing': typeof PricingRoute
'/privacy': typeof PrivacyRoute '/privacy': typeof PrivacyRoute
'/api/event': typeof ApiEventRoute '/api/event': typeof ApiEventRoute
'/api/subscribe': typeof ApiSubscribeRoute '/api/subscribe': typeof ApiSubscribeRoute
@ -64,6 +71,7 @@ export interface FileRoutesByFullPath {
} }
export interface FileRoutesByTo { export interface FileRoutesByTo {
'/': typeof IndexRoute '/': typeof IndexRoute
'/pricing': typeof PricingRoute
'/privacy': typeof PrivacyRoute '/privacy': typeof PrivacyRoute
'/api/event': typeof ApiEventRoute '/api/event': typeof ApiEventRoute
'/api/subscribe': typeof ApiSubscribeRoute '/api/subscribe': typeof ApiSubscribeRoute
@ -74,6 +82,7 @@ export interface FileRoutesByTo {
export interface FileRoutesById { export interface FileRoutesById {
__root__: typeof rootRouteImport __root__: typeof rootRouteImport
'/': typeof IndexRoute '/': typeof IndexRoute
'/pricing': typeof PricingRoute
'/privacy': typeof PrivacyRoute '/privacy': typeof PrivacyRoute
'/api/event': typeof ApiEventRoute '/api/event': typeof ApiEventRoute
'/api/subscribe': typeof ApiSubscribeRoute '/api/subscribe': typeof ApiSubscribeRoute
@ -85,6 +94,7 @@ export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath fileRoutesByFullPath: FileRoutesByFullPath
fullPaths: fullPaths:
| '/' | '/'
| '/pricing'
| '/privacy' | '/privacy'
| '/api/event' | '/api/event'
| '/api/subscribe' | '/api/subscribe'
@ -94,6 +104,7 @@ export interface FileRouteTypes {
fileRoutesByTo: FileRoutesByTo fileRoutesByTo: FileRoutesByTo
to: to:
| '/' | '/'
| '/pricing'
| '/privacy' | '/privacy'
| '/api/event' | '/api/event'
| '/api/subscribe' | '/api/subscribe'
@ -103,6 +114,7 @@ export interface FileRouteTypes {
id: id:
| '__root__' | '__root__'
| '/' | '/'
| '/pricing'
| '/privacy' | '/privacy'
| '/api/event' | '/api/event'
| '/api/subscribe' | '/api/subscribe'
@ -113,6 +125,7 @@ export interface FileRouteTypes {
} }
export interface RootRouteChildren { export interface RootRouteChildren {
IndexRoute: typeof IndexRoute IndexRoute: typeof IndexRoute
PricingRoute: typeof PricingRoute
PrivacyRoute: typeof PrivacyRoute PrivacyRoute: typeof PrivacyRoute
ApiEventRoute: typeof ApiEventRoute ApiEventRoute: typeof ApiEventRoute
ApiSubscribeRoute: typeof ApiSubscribeRoute ApiSubscribeRoute: typeof ApiSubscribeRoute
@ -130,6 +143,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof PrivacyRouteImport preLoaderRoute: typeof PrivacyRouteImport
parentRoute: typeof rootRouteImport parentRoute: typeof rootRouteImport
} }
'/pricing': {
id: '/pricing'
path: '/pricing'
fullPath: '/pricing'
preLoaderRoute: typeof PricingRouteImport
parentRoute: typeof rootRouteImport
}
'/': { '/': {
id: '/' id: '/'
path: '/' path: '/'
@ -177,6 +197,7 @@ declare module '@tanstack/react-router' {
const rootRouteChildren: RootRouteChildren = { const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute, IndexRoute: IndexRoute,
PricingRoute: PricingRoute,
PrivacyRoute: PrivacyRoute, PrivacyRoute: PrivacyRoute,
ApiEventRoute: ApiEventRoute, ApiEventRoute: ApiEventRoute,
ApiSubscribeRoute: ApiSubscribeRoute, ApiSubscribeRoute: ApiSubscribeRoute,

206
web/src/routes/pricing.tsx Normal file
View File

@ -0,0 +1,206 @@
import { createFileRoute, Link } from "@tanstack/react-router";
import { buildPageSeo } from "@/lib/seo";
export const Route = createFileRoute("/pricing")({
head: () =>
buildPageSeo({
title: "Pricing",
description:
"OpenSEO is free to self-host. The managed service is $10/month and includes Usage Credits.",
path: "/pricing",
titleSuffix: "OpenSEO",
}),
component: Pricing,
});
function NavLink({ to, children }: { to: string; children: React.ReactNode }) {
return (
<Link
to={to}
className="text-sm text-neutral-600 hover:text-neutral-900 transition-colors"
>
{children}
</Link>
);
}
function Pricing() {
return (
<main
className="bg-white text-neutral-900 min-h-screen"
style={{
fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif",
}}
>
<div className="max-w-3xl mx-auto px-6 py-16 md:py-24">
{/* Nav */}
<nav className="flex items-center justify-between mb-20">
<Link
to="/"
className="text-sm font-semibold hover:opacity-80 transition-opacity"
>
OpenSEO
</Link>
<a
href="https://github.com/every-app/open-seo"
target="_blank"
rel="noopener noreferrer"
className="text-sm font-medium text-neutral-700 hover:text-neutral-900 transition-colors"
>
GitHub
</a>
</nav>
{/* Headline */}
<h1 className="text-3xl font-bold tracking-tight">Pricing</h1>
{/* Managed */}
<section className="mt-12">
<h2 className="text-xl font-semibold">Managed</h2>
<div className="mt-6 rounded-lg border border-neutral-200 overflow-hidden">
{/* Base Plan */}
<div className="px-5 py-5">
<div className="flex items-baseline justify-between gap-4">
<p className="font-semibold">Base Plan</p>
<p className="text-lg font-semibold tabular-nums">$10/month</p>
</div>
<ul className="mt-3 space-y-2">
{[
"Access to OpenSEO's managed service",
"Includes $10.00 of Usage Credits each billing cycle",
"Monthly included credits reset each cycle",
].map((item) => (
<li
key={item}
className="flex gap-2.5 text-sm text-neutral-700"
>
<span className="text-neutral-400 mt-[2px] shrink-0">
&mdash;
</span>
{item}
</li>
))}
</ul>
</div>
{/* Usage Credits — add-on, visually connected to Base Plan */}
<div className="border-t border-dashed border-neutral-200 px-5 py-5">
<div className="flex items-baseline gap-3">
<p className="text-sm font-semibold">Usage Credits</p>
<span className="text-xs text-neutral-400 font-medium">
add-on
</span>
</div>
<p className="mt-2 text-sm text-neutral-600 leading-relaxed">
Credits are consumed as you use the app. We use powerful
providers for AI and SEO data. We bill based on usage of those
APIs and charge a small premium.
</p>
<ul className="mt-3 space-y-2">
{[
"Purchase top-up credits if you use up your monthly credits",
"Top-up credits roll over and don't expire",
].map((item) => (
<li
key={item}
className="flex gap-2.5 text-sm text-neutral-700"
>
<span className="text-neutral-400 mt-[2px] shrink-0">
&mdash;
</span>
{item}
</li>
))}
</ul>
</div>
</div>
</section>
{/* Self-hosted */}
<section className="mt-12">
<div className="flex items-baseline justify-between gap-4">
<h2 className="text-xl font-semibold">Self-hosted</h2>
</div>
<p className="mt-2 text-sm text-neutral-600 leading-relaxed">
Deploy OpenSEO yourself via Docker or Cloudflare Workers. Bring your
own API keys and pay DataForSEO and other API providers directly.
</p>
<a
href="https://github.com/every-app/open-seo"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-1.5 mt-3 text-sm font-medium text-neutral-900 hover:text-neutral-600 transition-colors"
>
View on GitHub
<span aria-hidden="true">&rarr;</span>
</a>
</section>
{/* FAQ */}
<section className="mt-12">
<h2 className="text-xl font-semibold">FAQ</h2>
<dl className="mt-4 space-y-6">
<div>
<dt className="text-sm font-medium">
What features use credits vs. what's unlimited?
</dt>
<dd className="mt-1 text-sm text-neutral-600 leading-relaxed">
Credits are consumed by features that query DataForSEO's API
backlinks, keyword volume, competitor data, and site audits.
Your projects, settings, and any data already fetched don't cost
credits.
</dd>
</div>
<div>
<dt className="text-sm font-medium">
Do unused credits roll over?
</dt>
<dd className="mt-1 text-sm text-neutral-600 leading-relaxed">
Top-up credits roll over indefinitely. The Usage Credits
included with your Base Plan reset each billing cycle.
</dd>
</div>
<div>
<dt className="text-sm font-medium">Can I cancel anytime?</dt>
<dd className="mt-1 text-sm text-neutral-600 leading-relaxed">
Yes. Cancel from your billing portal at any time. Your access
continues through the end of the current billing period.{" "}
</dd>
</div>
<div>
<dt className="text-sm font-medium">
Do I need a subscription or just usage credits?
</dt>
<dd className="mt-1 text-sm text-neutral-600 leading-relaxed">
While top-up Usage Credits roll over and don't expire, you need
an active subscription in order to use OpenSEO.
</dd>
</div>
</dl>
</section>
{/* Footer */}
<div className="mt-16 pt-8 border-t border-neutral-200">
<div className="flex items-center gap-6 text-xs text-neutral-600 [&_a]:hover:text-neutral-900 [&_a]:transition-colors">
<a
href="https://github.com/every-app/open-seo"
target="_blank"
rel="noopener noreferrer"
>
GitHub
</a>
<a
href="https://discord.gg/c9uGs3cFXr"
target="_blank"
rel="noopener noreferrer"
>
Discord
</a>
<NavLink to="/pricing">Pricing</NavLink>
<NavLink to="/privacy">Privacy</NavLink>
</div>
</div>
</div>
</main>
);
}