68 Commits

Author SHA1 Message Date
Ben Senescu
03fd3588ef
refactor: clean up complex code (#229) 2026-05-28 16:37:33 -04:00
Ben Senescu
e7eb895d46
fix redirect loop (again) (#227) 2026-05-27 00:08:59 -04:00
Ben Senescu
4a5a1eb523
Redirect unverified users to email verification (#226) 2026-05-26 23:36:35 -04:00
Ben Senescu
8b74c7da61
Improve onboarding questions and sign-up routing (#225) 2026-05-26 23:05:20 -04:00
Ben Senescu
f95c66b7f2
Switch domain sorting defaults and rank-tracking suggestions to traffic (#223)
* Default domain keyword sorting to traffic

* Align keyword suggestion preselection with traffic sort
2026-05-26 18:11:14 -04:00
Ben Senescu
45234e6945
Fix backlink tab states (#218)
* Fix tab active state wiring across result views

* Add spam filters to referring domains
2026-05-22 22:49:05 -04:00
Ben Senescu
90f369ad46
readme: update roadmap + add MCP and skills (#213) 2026-05-22 09:55:42 -04:00
Ben Senescu
3456af12fe
bump to reddit v3 api (#209) 2026-05-21 00:08:50 -04:00
Ben Senescu
4f638db702
Add Reddit conversion tracking (#208) 2026-05-20 23:36:30 -04:00
Ben Senescu
2484f93ce6
Refactor pages to improve performance and consistency (#205) 2026-05-20 17:23:09 -04:00
Ben Senescu
7a0ab0de2c
Add more mcp tool metadara + ChatGPT domain verification route (#197) 2026-05-14 13:02:08 -04:00
Ben Senescu
0ce787889a
Redirect signups to the homepage (#196) 2026-05-14 08:17:44 -04:00
Ben Senescu
701ace3dff
Add Google sign in and two-step auth flow (#193) 2026-05-13 20:55:20 -04:00
Ben Senescu
570fe2aa62
Add keyword tags, server-side saved filtering, and filtered exports (#179) 2026-05-12 00:02:43 -04:00
Ben Senescu
b633da8844
Mark Claude Desktop connector approval optional (#175) 2026-05-10 16:36:53 -04:00
Ben Senescu
0dd818e114
Replace better-auth with Cloudflare for oauth (#174) 2026-05-08 22:19:55 -04:00
Ben Senescu
db10ffa250
Fix Claude Desktop MCP OAuth registration and resume flows (#173) 2026-05-08 18:07:05 -04:00
Ben Senescu
103b6fe91c
fix: add additional well known for claude.ai + Claude Desktop (#170)
* Simplify MCP discovery metadata and lazy auth flow

* Fix ci check for MCP discovery branch
2026-05-08 15:59:43 -04:00
Ben Senescu
36b60f240f
Merge pull request #165 from bensenescu/codex/update-ai-tab
Update the AI tab for MCP setup and usage guidance
2026-05-08 15:30:18 -04:00
Ben Senescu
38fbac159f ci: run prettier on AI page 2026-05-08 15:27:12 -04:00
Ben Senescu
239a5a9e2b Add Claude and Codex desktop setup guides with brand logos
- Drop redundant codex mcp login step from the CLI flow
- Add Claude Desktop and Codex Desktop accordion entries
- Lead each setup-guide row with its product logo
2026-05-08 15:20:42 -04:00
Ben Senescu
93ee343c63 Polish AI & MCP page and move out of project scope
- Move /p/$projectId/ai to /ai so the page can be linked directly
- Lead with a copyable MCP server URL and trim the surrounding copy
- Make Claude Code and Codex setup guides collapsible accordions
- Add an Available tools section grouped into Keywords and Domain
2026-05-08 14:56:26 -04:00
Ben Senescu
02fcfb9834 Update AI page for MCP setup guidance 2026-05-08 13:40:51 -04:00
Ben Senescu
4cd02e36f4 Advertise offline_access in MCP scope metadata 2026-05-08 13:40:26 -04:00
Ben Senescu
0ff7bc96b2
Add stateless MCP server (#162) 2026-05-07 23:38:00 -04:00
Ben Senescu
6231424e88
feat: add personal access tokens (#159)
* feat: add personal access tokens

* feat: replace MCP tokens with OAuth foundation

* fix: keep OAuth constants private in auth foundation

* fix: clean up mcp oauth branch scope

* fix: expose oauth metadata endpoints

* fix: trim mcp oauth config to non-default options

Drop OIDC scopes, the org-id JWT claim, and the openid-configuration
metadata endpoint since the MCP integration is OAuth-only and the org
gets resolved server-side. Also remove options that just duplicated
better-auth defaults.

* fix: drop redundant oauth metadata helpers

Remove `session.storeSessionInDatabase: true` since better-auth only
enforces it when secondaryStorage is configured. Inline the
`getHostedBaseUrlForOAuthMetadata` alias and skip the async
`getOAuthServerConfig()` call in the protected-resource metadata handler
— the issuer is just `baseURL` without a custom jwt.issuer override.

* docs: explain cache headers on mcp metadata response

* Use escaped file routes for OAuth metadata

* save
2026-05-06 22:41:02 -04:00
Ben Senescu
def9390cea
feat: Implement server-side pagination and filtering for domain keywords (#143) 2026-05-04 15:26:36 -04:00
Ben Senescu
c1a9c1f57d
Replace mutation-based search with URL-driven state management (#145)
* fix: preserve cmd+click on tracked domain rows

Tracked-domain rows used <div role="button" onClick={navigate(...)}>,
which prevented standard browser open-in-new-tab behavior (cmd+click,
middle-click, right-click → open). Use TanStack Router's <Link> as a
stretched overlay so the row is a real <a href> while the archive
button stays interactive.

* fix: use <Link> for tab toggles and history navigation

Replace onClick={() => navigate(...)} / setSearchParams patterns with
TanStack Router's <Link> across surfaces that change the URL on click.
<Link> renders a real <a href> and only intercepts plain left-clicks,
so cmd/ctrl+click, middle-click, and right-click → open-in-new-tab
all work natively.

- Audit: history "View" button + Pages/Performance tab toggles.
- Domain overview: Top Keywords / Top Pages tab toggles. The keyword-
  only sort fallback now happens in the Link's search updater.
- Backlinks: history items, Backlinks/Domains/Pages tab toggles, and
  "Recent searches" back-link. Removes now-unused
  navigateToBacklinksHistory / navigateToBacklinksTab helpers.

Keyword research and domain history items, and AI search histories,
are not converted: those pages don't trigger their data fetch from
URL params alone, so a plain link target wouldn't reproduce the
current click behavior without a deeper refactor.

* refactor: unify search-page state around URL-driven fetching

Drive Keyword Research, Brand Lookup, and Prompt Explorer from URL
search params so a search is reproducible from a link alone. With
that, all three history surfaces become <Link>s and cmd+click /
right-click → "open in new tab" work natively.

- Shared SearchHistorySection now takes a renderItemLink slot so
  callers wrap history items in a <Link> with the right destination.
- Prompt Explorer: added URL search params (q, models, web, cc, hb)
  via promptExplorerSearchSchema; switched the explore mutation to
  useQuery keyed on the URL params; addSearch now fires from a
  success effect; "Recent searches" back-button is a <Link>.
- Brand Lookup: history items + "Recent searches" back-button are
  <Link>s; local form state stays in sync with URL via an effect.
- Keyword Research: form submit still navigates+kicks off a search
  for the same-URL re-submit case, but the controller also runs an
  URL-driven search trigger (with a dedup ref against the form path).
  Direct URLs, cmd+click on history, and browser back/forward all
  reproduce the same fetch. "Recent searches" back-button and the
  history items are <Link>s; the bespoke resetView path is gone.

Also drops now-unused clearKeywordSearchParams,
navigateToBacklinksHistory/Tab helpers' last consumers, and the
PromptExplorerPage's onQueryChange/onSelectHistoryItem callbacks.

* format

* refactor: replace useMutation with manual state in keyword research

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-04 13:00:37 -04:00
Ben Senescu
ab50e59ff8
feat: add 'Export to Google Sheets' to every data table (#144) 2026-05-03 15:25:25 -04:00
Ben Senescu
2130dccc81 hosted: add posthog opt out + session replay 2026-04-28 20:56:06 -04:00
Ben Senescu
faf7a294a3
feat: AI Visibility — Brand Lookup and Prompt Explorer (#128) 2026-04-23 10:48:38 -04:00
Ben Senescu
ed65d6976c
feat: support all 94 DataForSEO countries across features (#127) 2026-04-20 15:23:25 -04:00
Ben Senescu
5c2b71d340
feat: bulk delete for saved keywords (#122) 2026-04-16 19:14:50 -04:00
Ben Senescu
aeafa5649d
feat: polish rank tracking UI (#115) 2026-04-16 00:42:10 -04:00
Ben Senescu
4595080356
Redesign AI page (#114) 2026-04-15 11:41:04 -04:00
Ben Senescu
0024691da8 Improve rank tracking empty state and add config route (#112) 2026-04-14 20:29:43 -04:00
Ben Senescu
53a83996ef feat: Rank Tracking MVP (#91) 2026-04-14 20:29:38 -04:00
Ben Senescu
1d9a503335
hosted: add free trial plan and onboarding (#104) 2026-04-08 19:06:35 -04:00
Ben Senescu
f7ccdfee38 feat: add backlink search history (#90)
* feat: add backlink search history

Save recent backlink searches so users can quickly rerun previous lookups instead of starting from an empty state each time. Also widen the keyword research empty state layout to better use the available space.

* update dev tools

* update recent search navigation across SEO pages

* refine search layouts and recent search navigation

* simplify recent search resets and history storage

* save

* fix recent search hydration mismatch

* fix ci lint issues
2026-04-08 14:09:02 -04:00
Ben Senescu
d2fc85342a hosted: track successful subscription checkout (#92) 2026-04-08 14:09:02 -04:00
Ben Senescu
ad3b732f60 hosted: add product analytics (#83)
* track core product analytics flows

Track auth, search, export, audit, and credit-consumption events with canonical route IDs so PostHog funnels and usage dashboards stay low-noise and privacy-safe.

* fix: keep auth actions usable after session loss

* refactor: simplify analytics and auth helpers

- Replace isRecord/getActiveOrganizationId type guards with simple cast
- Refactor getAnalyticsRouteContext from if/return chain to route tables
- Replace toVerificationIssueType switch with zod enum
- Merge duplicate credits_consume events into single event per API call
- Merge two PostHogBootstrap useEffects into one

* refactor: add projectId to middleware context to reduce boilerplate

The requireProjectContext middleware now includes projectId directly,
eliminating repeated manual construction of BillingCustomerContext
objects across all server function handlers.

* remove unused BILLING_* env var fallbacks from cost profile script

* remove before_send event enrichment to preserve native PostHog URL tracking

The before_send hook was stripping $pathname, $current_url, $referrer and
other URL properties, which breaks PostHog web analytics dashboards, paths
analysis, session replay, and attribution. The route_id/route_group injection
it provided is unnecessary since PostHog already captures $pathname natively.

* remove route mapping layer, pass raw redirect paths to analytics events

The route ID registry (STATIC_ROUTES, PROJECT_ROUTES, getAnalyticsRouteContext,
getRedirectRouteId) duplicated what PostHog already captures via $pathname.
Replace redirect_route_id with redirect_to containing the raw path, and remove
~80 lines of route mapping infrastructure.

* clean up analytics events: drop redundant submit events and derived properties

- Remove search_submit events for keywords, domain overview, and backlinks
  (the search_complete events capture the meaningful outcome data)
- Remove target_type from backlinks events (derived 1:1 from search_scope)
- Remove result_limit from keyword research (requested limit, not useful
  alongside actual result_count)
- Remove export_format from data:export events (always "csv")

* refactor: inline wrappers, colocate helpers, deduplicate getActiveOrganizationId

- Inline toVerificationIssueType into verify-email.tsx (single-use wrapper)
- Move mapDataforseoPathToCreditFeature into dataforseoClient.ts (only consumer)
- Extract shared getActiveOrganizationId into lib/auth-session.ts (was
  duplicated in __root.tsx and middleware/ensure-user/hosted.ts)
- Rename shared/analytics.ts → shared/internal-user.ts (only email helpers
  remain after removing route mapping, verification, and dataforseo helpers)

* remove internal user tracking and email domain properties

Drop is_internal_user super property, email_domain person property, and all
supporting code (shared/internal-user.ts, getEmailDomain, isInternalUserEmail).
Simplifies initPostHog and identifyAnalyticsUser signatures.

* remove backlinks:search_complete effect-based tracking

The reactive useEffect + useRef dedup pattern added ~30 lines of plumbing
inside a data hook for a single analytics event. Not worth the complexity.

* simplify: replace manual type guards with zod, deduplicate posthog and sign-out helpers

- Replace hand-rolled typeof checks in getActiveOrganizationId and
  isAuthenticatedServerFunctionContext with zod safeParse
- Extract withPostHogClient helper to deduplicate client posthog wrapper
- Move apiKey guard into getServerPostHogClient factory
- Extract signOutAndRedirect to avoid duplicated sign-out logic
- Drop derivable has_results from analytics events
- Remove unnecessary path normalization in mapDataforseoPathToCreditFeature

* fix: strip email from pageview URLs, restore sign-out guard, harden server posthog, fix path mapper

- Sanitize $current_url on pageviews to remove email query param (PII)
- Restore onSuccess for sign-out redirect to avoid bounce-back on failure
- Swallow shutdown() errors so PostHog outages can't fail billed work
- Rewrite mapDataforseoPathToCreditFeature to match real API path structure
  (path[1] = module, path[3] = endpoint) instead of scanning all segments

* simplify: remove redundant refs in verify-email, infer middleware context type

- Remove unnecessary useRef guards in verify-email effects (deps already prevent re-firing)
- Use z.ZodType<EnsuredUserContext> annotation to infer return type instead of casting
- Add comment explaining one-shot PostHog client on Workers

* fix: reset PostHog identity on sign-out before redirect

* fix: require POSTHOG_HOST env var instead of defaulting to us.i.posthog.com

* fix: annotate url as unknown to satisfy no-unsafe-assignment

* format
2026-04-08 14:09:02 -04:00
Ben Senescu
5e9ebaf19f fix ci (#79) 2026-04-08 14:09:02 -04:00
Ben Senescu
d773105e8d fix: use backlinks history for default trends (#69)
* fix: use backlinks history for default trends

* simplify backlinks: remove filters, always use history endpoint

Remove the filter UI (status, subdomains, indirect links, exclude internal)
and hardcode defaults across the stack. Replace the conditional
timeseries_summary + timeseries_new_lost_summary fallback with a single
backlinks/history/live call for trend data. This reduces the overview from
5 parallel API calls to 3 and removes all conditional branching.

* improve charts

* fix: refresh backlinks cost docs
2026-04-08 14:09:02 -04:00
Ben Senescu
8a4e5f429d fix: add legal links to hosted sign-up footer (#77) 2026-04-08 14:09:02 -04:00
Ben Senescu
3081bc624a feat: add theme switcher with system/light/dark segmented control (#75)
* feat: add theme switcher with system/light/dark segmented control

Add a three-way theme toggle (system, light, dark) as a segmented
icon control in the account dropdown menu. Rename placeholder daisyUI
theme names from "todo" to "openseo".

* refactor: always resolve data-theme explicitly, remove duplicated dark CSS

Instead of removing data-theme in "system" mode and relying on
@media (prefers-color-scheme: dark) CSS, always resolve to an
explicit theme name via matchMedia. This eliminates the duplicated
dark-mode CSS overrides.
2026-04-08 14:09:01 -04:00
Ben Senescu
bd10955a8d fix: set default app page title (#72) 2026-04-08 14:09:01 -04:00
Ben Senescu
5d093c1ebb feat: add sign out button to /subscribe 2026-04-08 14:09:01 -04:00
Ben Senescu
a85f67c298 fix: remove better auth background tasks (#67)
* Improve verify email page copy and design

Remove redundant green success alert, update title to "Verify your email",
rename button to "Resend email", and change footer link to "Back to sign in".

* Fix verification emails not sending in production

Remove the backgroundTasks/waitUntil handler so better-auth awaits
email sending inline. Previously, errors thrown inside waitUntil
promises were silently dropped by Cloudflare Workers, preventing
emails from sending with no visible error logs.
2026-04-08 14:09:01 -04:00
Ben Senescu
570b995248 Refactor billing + onboarding (#61)
* fix: use full page reload after email verification

Client-side navigation via TanStack Router during the auth→app
transition can race with Vite HMR, causing "action is not a function"
server function errors.

* feat: add minimal /subscribe onboarding page

New post-auth subscribe page at /subscribe using the same centered
layout as auth pages. Shows plan details and a single Subscribe CTA.
Redirects PAYMENT_REQUIRED users here instead of /billing.

* redesign: rewrite billing page with usage chart and cleaner layout

Delete the sprawling multi-component billing page and replace it with a
single-file implementation. Two cards sit side by side at the top
(subscription summary + buy credits), with a 30-day usage bar chart
below powered by Autumn's useAggregateEvents hook and recharts.

Removed BillingRouteParts.tsx, HostedBillingContent.tsx, and trimmed
HostedBillingContentUtils to only parseTopUpAmount.

* polish: billing page improvements and OpenSEO nav link

- Two-column layout with subscription summary and buy credits side by side
- Usage bar chart using ResizeObserver instead of ResponsiveContainer
- Input validation with inline error message
- Full-page redirect state when navigating to Stripe
- Make OpenSEO logo in navbar link to /

* fix: guard app routes and include top-up usage

* fix: restore billing onboarding guards

Keep unpaid orgs on /subscribe and avoid misleading billing states when Autumn customer lookups fail.

* fix: split billing usage chart for ci checks
2026-04-08 14:09:01 -04:00
Ben Senescu
5617c6b9f3 redesign: auth pages (#60)
Remove card/shadow layout in favor of a clean centered design.
Add tree logo, simplify inputs (placeholder-only, no labels),
use soft buttons, narrow form width, and clean up footer links.

* fix: remove unused AUTUMN_SEO_DATA_USAGE_FEATURE_ID export

This constant became unused after the billing buckets separation
in #57 which replaced it with separate balance/topup feature IDs.

* fix: resolve oxlint errors from billing buckets PR

Add eslint-disable for max-lines in HostedBillingContent.tsx.
Type trackMock properly in dataforseoClient.test.ts to eliminate
unsafe type assertions without needing eslint-disable.
2026-04-08 14:09:01 -04:00