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:
parent
a65c5fb00a
commit
add176c3cc
@ -10,6 +10,7 @@
|
||||
|
||||
import { Route as rootRouteImport } from './routes/__root'
|
||||
import { Route as PrivacyRouteImport } from './routes/privacy'
|
||||
import { Route as PricingRouteImport } from './routes/pricing'
|
||||
import { Route as IndexRouteImport } from './routes/index'
|
||||
import { Route as BlogsIndexRouteImport } from './routes/blogs/index'
|
||||
import { Route as JsScriptDotjsRouteImport } from './routes/js/script[.]js'
|
||||
@ -22,6 +23,11 @@ const PrivacyRoute = PrivacyRouteImport.update({
|
||||
path: '/privacy',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const PricingRoute = PricingRouteImport.update({
|
||||
id: '/pricing',
|
||||
path: '/pricing',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const IndexRoute = IndexRouteImport.update({
|
||||
id: '/',
|
||||
path: '/',
|
||||
@ -55,6 +61,7 @@ const ApiEventRoute = ApiEventRouteImport.update({
|
||||
|
||||
export interface FileRoutesByFullPath {
|
||||
'/': typeof IndexRoute
|
||||
'/pricing': typeof PricingRoute
|
||||
'/privacy': typeof PrivacyRoute
|
||||
'/api/event': typeof ApiEventRoute
|
||||
'/api/subscribe': typeof ApiSubscribeRoute
|
||||
@ -64,6 +71,7 @@ export interface FileRoutesByFullPath {
|
||||
}
|
||||
export interface FileRoutesByTo {
|
||||
'/': typeof IndexRoute
|
||||
'/pricing': typeof PricingRoute
|
||||
'/privacy': typeof PrivacyRoute
|
||||
'/api/event': typeof ApiEventRoute
|
||||
'/api/subscribe': typeof ApiSubscribeRoute
|
||||
@ -74,6 +82,7 @@ export interface FileRoutesByTo {
|
||||
export interface FileRoutesById {
|
||||
__root__: typeof rootRouteImport
|
||||
'/': typeof IndexRoute
|
||||
'/pricing': typeof PricingRoute
|
||||
'/privacy': typeof PrivacyRoute
|
||||
'/api/event': typeof ApiEventRoute
|
||||
'/api/subscribe': typeof ApiSubscribeRoute
|
||||
@ -85,6 +94,7 @@ export interface FileRouteTypes {
|
||||
fileRoutesByFullPath: FileRoutesByFullPath
|
||||
fullPaths:
|
||||
| '/'
|
||||
| '/pricing'
|
||||
| '/privacy'
|
||||
| '/api/event'
|
||||
| '/api/subscribe'
|
||||
@ -94,6 +104,7 @@ export interface FileRouteTypes {
|
||||
fileRoutesByTo: FileRoutesByTo
|
||||
to:
|
||||
| '/'
|
||||
| '/pricing'
|
||||
| '/privacy'
|
||||
| '/api/event'
|
||||
| '/api/subscribe'
|
||||
@ -103,6 +114,7 @@ export interface FileRouteTypes {
|
||||
id:
|
||||
| '__root__'
|
||||
| '/'
|
||||
| '/pricing'
|
||||
| '/privacy'
|
||||
| '/api/event'
|
||||
| '/api/subscribe'
|
||||
@ -113,6 +125,7 @@ export interface FileRouteTypes {
|
||||
}
|
||||
export interface RootRouteChildren {
|
||||
IndexRoute: typeof IndexRoute
|
||||
PricingRoute: typeof PricingRoute
|
||||
PrivacyRoute: typeof PrivacyRoute
|
||||
ApiEventRoute: typeof ApiEventRoute
|
||||
ApiSubscribeRoute: typeof ApiSubscribeRoute
|
||||
@ -130,6 +143,13 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof PrivacyRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/pricing': {
|
||||
id: '/pricing'
|
||||
path: '/pricing'
|
||||
fullPath: '/pricing'
|
||||
preLoaderRoute: typeof PricingRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/': {
|
||||
id: '/'
|
||||
path: '/'
|
||||
@ -177,6 +197,7 @@ declare module '@tanstack/react-router' {
|
||||
|
||||
const rootRouteChildren: RootRouteChildren = {
|
||||
IndexRoute: IndexRoute,
|
||||
PricingRoute: PricingRoute,
|
||||
PrivacyRoute: PrivacyRoute,
|
||||
ApiEventRoute: ApiEventRoute,
|
||||
ApiSubscribeRoute: ApiSubscribeRoute,
|
||||
|
||||
206
web/src/routes/pricing.tsx
Normal file
206
web/src/routes/pricing.tsx
Normal 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">
|
||||
—
|
||||
</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">
|
||||
—
|
||||
</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">→</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>
|
||||
);
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user