154 Commits

Author SHA1 Message Date
Ben Senescu
4cd02e36f4 Advertise offline_access in MCP scope metadata 2026-05-08 13:40:26 -04:00
Ben Senescu
57790b930f
feat: add OpenSEO MCP tools (#161) 2026-05-08 00:34:29 -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
fb265e3a3a ci: run prettier 2026-05-06 15:45:09 -04:00
Ben Senescu
0c356fac19 fix: change google sheets export copy 2026-05-06 15:43:26 -04:00
Ben Senescu
ca260f544b
feat: improve mobile UX (#155) 2026-05-06 12:52:05 -04:00
Ben Senescu
b9ad31704f
Clarify keyword intent badges (#157) 2026-05-06 12:30:33 -04:00
Ben Senescu
9e5afc9d48
feat: Add shared table component (#154)
* fix rank tracking unranked filters

* improve modal dismissal and range selection

* refactor: create shared table component
2026-05-06 11:23:07 -04:00
Ben Senescu
7f892ca433
Merge pull request #19 from every-app/split-modal-dismissal
fix: exit closes modal + accessible labels
2026-05-06 10:46:45 -04:00
Ben Senescu
6f50850b5a
fix rank tracking unranked filters (#18)
Co-authored-by: Granata005 <granata005@gmail.com>
2026-05-06 10:46:33 -04:00
Ben Senescu
6b7d0464f7
feat: add shift range table selection (#20)
* add shift range table selection

* format keyword suggestion selection

* credit original draft author

Co-authored-by: Granata005 <granata005@gmail.com>

---------

Co-authored-by: Granata005 <granata005@gmail.com>
2026-05-06 10:46:21 -04:00
Ben Senescu
89091ca9d5
refactor: simplify rank tracking backend (#125) 2026-05-05 22:18:35 -04:00
Ben Senescu
db1a1d723f
fix: use Tanstack Query for Keyword Research (#150)
* keywords: use query cache for research requests

* keywords: simplify research request dedupe
2026-05-05 14:11:28 -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
a42fe4d350
keywords: consolidate export buttons into dropdown (#147) 2026-05-04 14:04:44 -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
11252f8088
fix: improve spreadsheet exports (#146) 2026-05-04 12:49:42 -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
17298da7ee
fix: capture DataForSEO errors with context (#138) 2026-04-24 00:16:20 -04:00
Ben Senescu
ebc9e0b4fa
fix: prompt explorer bugs (#136) 2026-04-23 18:25:11 -04:00
Ben Senescu
1108cdd9a7
feat: gate llm mentions in self hosted product (#134) 2026-04-23 18:02:24 -04:00
Ben Senescu
4c8a3305ed
fix: preserve query case in brand lookup cache key (#133)
Drop the .toLowerCase() call when building the cache key's targetValue so
it matches whatever detectTarget returns. detectTarget already normalizes
domains to lowercase; keywords are passed through as-is, so lowercasing
again folded otherwise-distinct keyword queries into the same cache
entry.
2026-04-23 11:41:44 -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
ed27b64eb4
fix: pass only required fields in workflow billingCustomer params (#123)
The callers of startAudit and beginRankCheckRun were forwarding the
incoming billingCustomer object directly into workflow.create params.
At runtime the object can be an EnsuredUserContext with extra fields
beyond BillingCustomerContext, which pollutes the workflow instance's
persisted params payload. Explicitly pick the four required fields so
only the intended data is serialized into the workflow.
2026-04-16 16:24:27 -04:00
Ben Senescu
59d1dd24cb
fix: use VALIDATION_ERROR for rank tracking config validation errors (#119)
INTERNAL_ERROR was showing "An unexpected error occurred" to users and
triggering PostHog error monitoring for expected validation cases
(duplicate domain+country, max configs reached).
2026-04-16 12:23:52 -04:00
Ben Senescu
bc0bf894cd
fix: avoid D1 param limit in getEarliestSnapshotsForKeywords (#121) 2026-04-16 12:21:08 -04:00
Ben Senescu
9a9a9a2a6c
feat: add keyword metrics (volume, difficulty, CPC) to rank tracking (#118) 2026-04-16 10:41:31 -04:00
Ben Senescu
2b7a3eae05
Make domain overview URL paths clickable in tables (#120) 2026-04-16 10:30:49 -04:00
Ben Senescu
e78ef5e3dd
fix: accept full URLs in rank tracking domain input (#117)
Replace DOMAIN_REGEX with URL-based normalization so users can paste
full URLs (e.g. https://example.com/path) into the domain config form
without getting a ZodError. The URL class handles protocol stripping,
hostname extraction, and validation natively.
2026-04-16 02:58:34 -04:00
Ben Senescu
86d3714846
feat: allow free plan users to set up rank tracking (#116) 2026-04-16 02:19:23 -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
1b6bf8fd94
refactor: convert rank tracking table to Tanstack Table (#111) 2026-04-15 10:42:28 -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
e68ced7203 feat: group navigation into dropdown menus (#110)
* feat: group navigation into Keywords, Domain, and AI dropdowns

Reorganize the top nav from 7 flat items into grouped dropdowns:
- Keywords: Keyword Research, Saved Keywords, Rank Tracking
- Domain: Domain Overview, Backlinks, Site Audit
- AI: standalone link

Mobile sidebar uses section headers for the same groupings.
Active dropdown items are highlighted with primary tint.

* fix: use matchSegment lookup instead of hardcoded array indices in nav groups
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
fb1291537c
fix: treat empty DataForSEO backlinks results as valid empty data (#109) 2026-04-09 16:44:34 -04:00
Ben Senescu
53505c0a5b
fix: fix runtime error when clearing backlinks filter (#105) 2026-04-09 16:43:47 -04:00
Ben Senescu
937665ef95
fix: remove unused view transitions CSS causing navigation errors (#107) 2026-04-09 14:21:47 -04:00
Ben Senescu
1d9a503335
hosted: add free trial plan and onboarding (#104) 2026-04-08 19:06:35 -04:00
Ben Senescu
0b7811d882
fix: sanitize CSV exports against formula injection (#103) 2026-04-08 14:18:27 -04:00
Ben Senescu
c9d212b8b8 fix: explicitly pick billingCustomer fields for workflow serialization (#97)
The full middleware context object passed as billingCustomer contained
non-serializable properties (Drizzle project result, TanStack internals)
that caused a DataCloneError in Miniflare's workflow binding during
Docker self-hosting.
2026-04-08 14:09:02 -04:00
Ben Senescu
fd3d9838fe fix: default backlinks search to site-wide scope for root URLs (#96)
* fix: default backlinks search to site-wide scope for root URLs

URLs with an explicit protocol but no path (e.g. https://example.com/)
were incorrectly auto-selecting "Exact page" scope. Now only URLs with
an actual path beyond "/" trigger page scope auto-selection.

* test: update backlinks scope tests for root URL behavior change
2026-04-08 14:09:02 -04:00
Ben Senescu
93c95dc8f7 feat: add structured filters to backlinks tables (#94)
* feat: add structured filters to backlinks tables

Add per-tab filter panels (Backlinks, Referring Domains, Top Pages) with
include/exclude text search, numeric range filters, link type selector,
and visibility toggles. Filter values persist in localStorage across
searches and page reloads.

* fix: improve backlinks filter layout and hide domain overview filters on pages tab

- Add w-full to text filter inputs so they fill their grid columns
- Restructure backlinks results card to use border-separated sections
  matching the domain overview pattern
- Hide filter button and panel on domain overview's Top Pages tab since
  keyword filters don't apply there

* fix: move backlinks filter button to its own toolbar row below tabs

Match the domain overview layout: tabs in their own header row, then a
separate toolbar row with the Filters button underneath.

* style: fix prettier formatting in backlinks filter files

* fix: resolve all ci:check issues

- Remove unused BacklinksResultsHeader.tsx and useBacklinksSpamPreferences.ts
- Un-export DEFAULT_BACKLINKS_SPAM_THRESHOLD and normalizeBacklinksSpamThreshold
  (only used internally)
- Remove unused BacklinksAllFilterValues type
- Fix oxlint unsafe type assertions in useBacklinksFilters.ts

* fix: wire spam filter options through to server and remove spam filters from non-backlinks tabs

The client was not passing hideSpam/spamThreshold to the server, so the
server always defaulted to hideSpam:true with threshold 40. This made
the new client-side spam score filters on the backlinks tab silently
filter an already-truncated dataset. Now the client sends hideSpam:false
so the server returns all rows and client-side filtering works correctly.

Removed spam score filters from the Referring Domains tab since spam
filtering is not needed there.

* fix: restore backlinks spam filtering and safe filter hydration

* simplify: remove server-side spam filter wiring, keep client-side only

All backlinks filtering (including spam score) is now done client-side.
Server-side spam filtering adds complexity without meaningful benefit
at current data volumes. This removes the bridging code added in the
last two commits and simplifies filter hydration.
2026-04-08 14:09:02 -04:00
Ben Senescu
b3d543d206 feat: add top-level persistent filters for keyword research (#93)
* feat: add top-level persistent filters for keyword research

Add a collapsible filter bar below the search bar so users can set
default filters (volume, CPC, difficulty, include/exclude terms) that
persist across searches and sessions via localStorage.

* simplify: remove top-level filter UI, keep only localStorage persistence

Revert the search bar to its original state. The existing in-table
filters already provide good UX — all we needed was persistence.
Auto-expand the filter panel when persisted filters are active on load.

* feat: show filtered keyword count as "Showing X of Y keywords"

When filters are active, display "Showing 14 of 150 keywords" instead
of just "14 keywords". When no filters are active, show "Showing X
keywords". Applied to both desktop and mobile views.

* style: fix prettier formatting
2026-04-08 14:09:02 -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