359 Commits

Author SHA1 Message Date
Ben Senescu
2fc568f73c
release: v0.0.9 (#149) 2026-05-04 15:55:15 -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
368b3ce375 website: add faq 2026-04-28 21:03:17 -04:00
Ben Senescu
2130dccc81 hosted: add posthog opt out + session replay 2026-04-28 20:56:06 -04:00
Ben Senescu
3e39c65265
docs: clarify self-hosting troubleshooting (#140)
* docs: clarify self-hosting troubleshooting

* docs: simplify Docker env troubleshooting
2026-04-28 17:39:38 -04:00
Ben Senescu
17298da7ee
fix: capture DataForSEO errors with context (#138) 2026-04-24 00:16:20 -04:00
Ben Senescu
94d1ee7bd5
chore: release v0.0.8 (#132) 2026-04-23 18:25:37 -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
65d62f0eed
website: fix mobile header and footer layout (#135)
Hide Sign in and GitHub text label on small screens, wrap footer
links onto multiple rows, and clip any horizontal overflow globally.
2026-04-23 11:54:54 -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
da2509ec74
ci: use correct posthog-cli env var names (#131) 2026-04-22 17:49:44 -04:00
Ben Senescu
61cc64f2af hosted: switch non-secrets to vars 2026-04-22 17:32:30 -04:00
Ben Senescu
d23cad7abf
hosted: upload sitemaps to posthog in ci (#130) 2026-04-22 17:29:37 -04:00
Ben Senescu
c779d83cda
fix: fix cloudlfare self hosting (#129) 2026-04-22 15:21:30 -04:00
Ben Senescu
29a0c9cc43
website: add seo for startups guide (#33) 2026-04-22 13:30:07 -04:00
Ben Senescu
ed65d6976c
feat: support all 94 DataForSEO countries across features (#127) 2026-04-20 15:23:25 -04:00
Ben Senescu
021ec830af
website: add rank tracking + format (#126) 2026-04-16 19:19:21 -04:00
Ben Senescu
5c2b71d340
feat: bulk delete for saved keywords (#122) 2026-04-16 19:14:50 -04:00
Ben Senescu
a52cf0bf95 docs: tweak readme 2026-04-16 18:54:25 -04:00
Ben Senescu
bf58af3752
release: bump to 0.0.6 (#124)
* save

* save
2026-04-16 16:35:54 -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
4137b386ab
website: convert CTA to sign-up (#108)
* landing: convert main CTA to sign-up, add shared marketing layout

- Replace email waitlist form with "Try now" sign-up button as primary CTA
- Add shared _marketing layout route with consistent nav (Pricing, GitHub, Sign in)
- Move email form to bottom of homepage as "Stay in the loop" newsletter
- Update self-host card copy for live managed version
- Reorder self-host card below video on mobile via flex order
- Add /pricing to sitemap and SiteFooter

* change footer and add social card

* landing: move newsletter and footer to shared marketing layout

Move divider line above the newsletter form instead of between
the form and footer links.
2026-04-09 15:13:23 -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
01c11b0c5f
release: v0.0.5 (#102) 2026-04-08 14:18:46 -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