From 03fd3588efcd65949e17eddf734f4715cf652894 Mon Sep 17 00:00:00 2001 From: Ben Senescu <44480372+bensenescu@users.noreply.github.com> Date: Thu, 28 May 2026 16:37:33 -0400 Subject: [PATCH] refactor: clean up complex code (#229) --- src/client/components/SafeExternalLink.tsx | 13 +- src/client/components/table/AppDataTable.tsx | 13 +- src/client/components/table/url.tsx | 2 +- src/client/features/ai-mcp/SetupControls.tsx | 4 +- .../features/ai-search/BrandLookupPage.tsx | 8 +- .../features/ai-search/PromptExplorerPage.tsx | 8 +- .../components/AiSearchSetupGate.tsx | 9 +- .../components/BrandLookupCitationTables.tsx | 1 - .../components/BrandLookupResults.tsx | 23 ++- .../audit/launch/useLaunchController.ts | 21 +- .../results/AuditResultsTableFilterLogic.ts | 2 +- .../features/audit/results/ResultsTables.tsx | 14 +- .../features/audit/results/ResultsView.tsx | 2 +- src/client/features/audit/results/export.ts | 11 +- src/client/features/auth/AuthPage.tsx | 12 -- .../backlinks/BacklinksPageCharts.tsx | 4 +- .../backlinks/BacklinksPageContent.tsx | 4 +- .../features/backlinks/BacklinksPageLinks.tsx | 14 +- .../backlinks/BacklinksPageStates.tsx | 9 +- .../backlinks/BacklinksSearchCard.tsx | 11 +- .../backlinks/ReferringDomainsTable.tsx | 4 +- .../features/backlinks/TopPagesTable.tsx | 4 +- .../features/backlinks/backlinksPageUtils.ts | 10 - .../features/domain/DomainOverviewPage.tsx | 8 +- .../domain/components/DomainFilterPanel.tsx | 7 +- .../features/domain/domainFilterUtils.ts | 26 +-- .../features/domain/domainRouteState.ts | 4 +- .../keywords/components/KeywordUi.tsx | 16 +- .../state/useKeywordResearchController.ts | 182 +++++------------- .../issues/LighthouseIssuesScreen.tsx | 15 +- .../features/lighthouse/issues/utils.tsx | 17 -- .../onboarding/PostSignupOnboarding.tsx | 7 - .../rank-tracking/AddKeywordsPanel.tsx | 6 +- .../rank-tracking/RankTrackingColumns.tsx | 19 +- .../RankTrackingDomainDetail.tsx | 1 - .../rank-tracking/RankTrackingDomainList.tsx | 12 +- .../rank-tracking/RankTrackingTableParts.tsx | 7 - src/client/lib/download.ts | 9 + src/middleware/ensureUser.ts | 6 +- src/routes/_app/ai.tsx | 6 +- src/routes/_auth.sign-in.tsx | 3 +- src/routes/_auth.sign-up.tsx | 3 +- src/routes/forgot-password.tsx | 3 +- src/routes/reset-password.tsx | 3 +- src/routes/verify-email.tsx | 6 +- src/server.ts | 6 +- .../auth/default-hosted-organization.ts | 18 +- src/server/auth/delegated-organization.ts | 18 +- src/server/auth/org-slug.ts | 16 ++ src/server/email/loops.ts | 19 +- .../services/backlinksServiceData.ts | 14 +- .../keywords/services/research/research.ts | 14 +- .../services/RankTrackingService.ts | 2 +- .../services/rankTrackingResults.ts | 4 +- src/server/lib/audit/discovery.ts | 6 +- src/server/lib/audit/page-analyzer.ts | 6 - src/server/lib/audit/progress-kv.ts | 12 -- src/server/lib/errors.ts | 6 +- src/server/lib/lighthouseStoredPayload.ts | 120 ++++-------- src/server/lib/reddit-conversions.ts | 9 +- src/server/mcp/context.ts | 4 - src/server/mcp/formatters.ts | 16 +- src/server/mcp/project-auth.ts | 4 +- .../mcp/tools/dataforseo-research-tools.ts | 25 +-- src/server/mcp/tools/list-projects.ts | 8 +- src/server/workflows/rankCheckPaths.ts | 16 +- web/src/components/feature-page.tsx | 48 ++--- web/src/lib/content.functions.ts | 12 +- web/src/lib/feature-pages.ts | 12 +- web/src/lib/seo.ts | 4 +- 70 files changed, 270 insertions(+), 718 deletions(-) create mode 100644 src/client/lib/download.ts create mode 100644 src/server/auth/org-slug.ts diff --git a/src/client/components/SafeExternalLink.tsx b/src/client/components/SafeExternalLink.tsx index 9e3c9a3..e7162a0 100644 --- a/src/client/components/SafeExternalLink.tsx +++ b/src/client/components/SafeExternalLink.tsx @@ -1,5 +1,7 @@ import { ExternalLink } from "lucide-react"; +import { getSafeExternalUrl } from "./table/url"; + export function SafeExternalLink({ url, label, @@ -21,14 +23,3 @@ export function SafeExternalLink({ ); } - -function getSafeExternalUrl(value: string) { - try { - const parsed = new URL(value); - return parsed.protocol === "http:" || parsed.protocol === "https:" - ? parsed.toString() - : null; - } catch { - return null; - } -} diff --git a/src/client/components/table/AppDataTable.tsx b/src/client/components/table/AppDataTable.tsx index f5b5362..24c349d 100644 --- a/src/client/components/table/AppDataTable.tsx +++ b/src/client/components/table/AppDataTable.tsx @@ -182,11 +182,7 @@ export function AppDataTable({ .join(" ")} > {row.getVisibleCells().map((cell) => { - const rawMeta: unknown = cell.column.columnDef.meta; - const meta = isAppColumnMeta(rawMeta) - ? rawMeta - : undefined; - const metaClass = meta?.cellClassName; + const metaClass = cell.column.columnDef.meta?.cellClassName; return ( ({ fixedLayout?: boolean; stickyHeader?: boolean; }) { - const rawMeta: unknown = header.column.columnDef.meta; - const meta = isAppColumnMeta(rawMeta) ? rawMeta : undefined; + const meta = header.column.columnDef.meta; return ( ({ ); } - -function isAppColumnMeta(value: unknown): value is AppColumnMeta { - return typeof value === "object" && value !== null; -} diff --git a/src/client/components/table/url.tsx b/src/client/components/table/url.tsx index a34e6f2..882b9d0 100644 --- a/src/client/components/table/url.tsx +++ b/src/client/components/table/url.tsx @@ -73,7 +73,7 @@ function getUrlDisplayLabel( return formatUrlForDisplay(value); } -function getSafeExternalUrl(value: string) { +export function getSafeExternalUrl(value: string) { try { const parsed = new URL(value); return parsed.protocol === "http:" || parsed.protocol === "https:" diff --git a/src/client/features/ai-mcp/SetupControls.tsx b/src/client/features/ai-mcp/SetupControls.tsx index c9acdfb..87542f4 100644 --- a/src/client/features/ai-mcp/SetupControls.tsx +++ b/src/client/features/ai-mcp/SetupControls.tsx @@ -77,12 +77,10 @@ export function CodeBlock({ code }: { code: string }) { export function CopyButton({ value, successMessage, - label, iconOnly = false, }: { value: string; successMessage: string; - label?: string; iconOnly?: boolean; }) { const [copied, setCopied] = useState(false); @@ -130,7 +128,7 @@ export function CopyButton({ ) : ( )} - {label ?? "Copy"} + Copy ); } diff --git a/src/client/features/ai-search/BrandLookupPage.tsx b/src/client/features/ai-search/BrandLookupPage.tsx index c331b36..43c2141 100644 --- a/src/client/features/ai-search/BrandLookupPage.tsx +++ b/src/client/features/ai-search/BrandLookupPage.tsx @@ -19,10 +19,8 @@ import { BrandLookupSearchCard } from "@/client/features/ai-search/components/Br import { BrandLookupHistorySection } from "@/client/features/ai-search/components/BrandLookupHistorySection"; import { AiSearchLoadingState } from "@/client/features/ai-search/components/AiSearchLoadingState"; import { AiSearchPaidPlanGate } from "@/client/features/ai-search/components/AiSearchPaidPlanGate"; -import { - AiSearchAccessLoadingState, - AiSearchSetupGate, -} from "@/client/features/ai-search/components/AiSearchSetupGate"; +import { AiSearchSetupGate } from "@/client/features/ai-search/components/AiSearchSetupGate"; +import { AccessGateLoadingState } from "@/client/features/access-gate/AccessGate"; import { useAiSearchAccess } from "@/client/features/ai-search/useAiSearchAccess"; import { useBrandLookupSearchHistory } from "@/client/hooks/useBrandLookupSearchHistory"; import { BRAND_LOOKUP_MAX_INPUT_LENGTH } from "@/types/schemas/ai-search"; @@ -151,7 +149,7 @@ function BrandLookupPageInner({ {access.isLoading ? ( - + ) : !access.enabled ? ( {access.isLoading ? ( - + ) : !access.enabled ? ( ; -} +import { AccessGate } from "@/client/features/access-gate/AccessGate"; export function AiSearchSetupGate({ errorMessage, diff --git a/src/client/features/ai-search/components/BrandLookupCitationTables.tsx b/src/client/features/ai-search/components/BrandLookupCitationTables.tsx index 1a3a69f..5f63959 100644 --- a/src/client/features/ai-search/components/BrandLookupCitationTables.tsx +++ b/src/client/features/ai-search/components/BrandLookupCitationTables.tsx @@ -149,7 +149,6 @@ function BrandLookupTable({ table.getColumn(columnId)?.getCanSort() ?? false, ) } - getRowClassName={() => ""} /> ); } diff --git a/src/client/features/ai-search/components/BrandLookupResults.tsx b/src/client/features/ai-search/components/BrandLookupResults.tsx index a9896aa..fe7a8a5 100644 --- a/src/client/features/ai-search/components/BrandLookupResults.tsx +++ b/src/client/features/ai-search/components/BrandLookupResults.tsx @@ -40,14 +40,14 @@ const PLATFORM_DOT_CLASS: Record = { }; export function BrandLookupResults({ result }: Props) { - const erroredPlatforms = result.perPlatform.filter( - (p) => p.status === "error", - ); - const allPlatformsErrored = - erroredPlatforms.length === result.perPlatform.length && - result.perPlatform.length > 0; - if (!result.hasData) { + const erroredPlatforms = result.perPlatform.filter( + (p) => p.status === "error", + ); + const allPlatformsErrored = + erroredPlatforms.length === result.perPlatform.length && + result.perPlatform.length > 0; + if (allPlatformsErrored) { return (
@@ -63,7 +63,10 @@ export function BrandLookupResults({ result }: Props) {
{erroredPlatforms.length > 0 ? (

- Note: {formatPlatformList(erroredPlatforms.map((p) => p.platform))}{" "} + Note:{" "} + {erroredPlatforms + .map((p) => formatPlatformLabel(p.platform)) + .join(" and ")}{" "} {erroredPlatforms.length === 1 ? "was" : "were"} unavailable — some mentions may be missing.

@@ -88,10 +91,6 @@ export function BrandLookupResults({ result }: Props) { ); } -function formatPlatformList(platforms: PlatformRow["platform"][]): string { - return platforms.map(formatPlatformLabel).join(" and "); -} - function BrandHeader({ result }: { result: BrandLookupResult }) { return (
diff --git a/src/client/features/audit/launch/useLaunchController.ts b/src/client/features/audit/launch/useLaunchController.ts index 7d64203..b7859c9 100644 --- a/src/client/features/audit/launch/useLaunchController.ts +++ b/src/client/features/audit/launch/useLaunchController.ts @@ -131,24 +131,15 @@ function useLaunchMutations({ return { startMutation, deleteMutation }; } -function applyMaxPages( - launchForm: { - setFieldValue: (field: "maxPagesInput", value: string) => void; - }, - value: number, -) { +function commitMaxPagesInput(launchForm: { + state: { values: { maxPagesInput: string } }; + setFieldValue: (field: "maxPagesInput", value: string) => void; +}) { + const maxPagesInput = launchForm.state.values.maxPagesInput; + const value = maxPagesInput ? Number.parseInt(maxPagesInput, 10) : MIN_PAGES; const safeValue = Number.isFinite(value) ? Math.max(MIN_PAGES, Math.min(MAX_PAGES_LIMIT, Math.round(value))) : MIN_PAGES; launchForm.setFieldValue("maxPagesInput", String(safeValue)); return safeValue; } - -function commitMaxPagesInput(launchForm: { - state: { values: { maxPagesInput: string } }; - setFieldValue: (field: "maxPagesInput", value: string) => void; -}) { - const maxPagesInput = launchForm.state.values.maxPagesInput; - if (!maxPagesInput) return applyMaxPages(launchForm, MIN_PAGES); - return applyMaxPages(launchForm, Number.parseInt(maxPagesInput, 10)); -} diff --git a/src/client/features/audit/results/AuditResultsTableFilterLogic.ts b/src/client/features/audit/results/AuditResultsTableFilterLogic.ts index 8df925b..de97bf9 100644 --- a/src/client/features/audit/results/AuditResultsTableFilterLogic.ts +++ b/src/client/features/audit/results/AuditResultsTableFilterLogic.ts @@ -7,7 +7,7 @@ export type PerformanceRowData = PerformanceResultRow & { pagePath: string | null; }; -export type LighthouseFailureFields = { +type LighthouseFailureFields = { errorMessage: string | null; performanceScore: number | null; accessibilityScore: number | null; diff --git a/src/client/features/audit/results/ResultsTables.tsx b/src/client/features/audit/results/ResultsTables.tsx index 5390726..2b3869f 100644 --- a/src/client/features/audit/results/ResultsTables.tsx +++ b/src/client/features/audit/results/ResultsTables.tsx @@ -30,24 +30,15 @@ import { EMPTY_PERFORMANCE_FILTERS, filterPages, filterPerformanceRows, - isLighthouseFailure as getIsLighthouseFailure, + isLighthouseFailure, nullableNumberSort, nullableStringSort, - type LighthouseFailureFields, type PageRow, type PagesFilters, type PerformanceFilters, type PerformanceRowData, } from "@/client/features/audit/results/AuditResultsTableFilterLogic"; -export function isLighthouseFailure(row: LighthouseFailureFields) { - return getIsLighthouseFailure(row); -} - -function getLighthouseFailureMessage(row: LighthouseFailureFields) { - return row.errorMessage ?? "Lighthouse returned no category scores"; -} - const pageColumnHelper = createColumnHelper(); const performanceColumnHelper = createColumnHelper(); @@ -275,7 +266,8 @@ function buildPerformanceColumns({ header: ({ column }) => , cell: ({ row }) => { const isFailed = isLighthouseFailure(row.original); - const failureMessage = getLighthouseFailureMessage(row.original); + const failureMessage = + row.original.errorMessage ?? "Lighthouse returned no category scores"; return isFailed ? ( ; + return ; } if (accessGate.statusErrorMessage) { diff --git a/src/client/features/backlinks/BacklinksPageLinks.tsx b/src/client/features/backlinks/BacklinksPageLinks.tsx index e31f6a0..575c6eb 100644 --- a/src/client/features/backlinks/BacklinksPageLinks.tsx +++ b/src/client/features/backlinks/BacklinksPageLinks.tsx @@ -1,18 +1,6 @@ import { SafeExternalLink } from "@/client/components/SafeExternalLink"; import { extractUrlPath, truncateMiddle } from "./backlinksPageUtils"; -export function BacklinksExternalLink({ - url, - label, - className, -}: { - url: string; - label: string; - className: string; -}) { - return ; -} - export function BacklinksSourceLink({ url, maxLength, @@ -23,7 +11,7 @@ export function BacklinksSourceLink({ muted?: boolean; }) { return ( - ; -} +import { AccessGate } from "@/client/features/access-gate/AccessGate"; export function BacklinksSetupGate({ errorMessage, diff --git a/src/client/features/backlinks/BacklinksSearchCard.tsx b/src/client/features/backlinks/BacklinksSearchCard.tsx index 9f42205..800d31e 100644 --- a/src/client/features/backlinks/BacklinksSearchCard.tsx +++ b/src/client/features/backlinks/BacklinksSearchCard.tsx @@ -8,7 +8,10 @@ import { shouldValidateFieldOnChange, } from "@/client/lib/forms"; import type { BacklinksSearchState } from "./backlinksPageTypes"; -import { resolveBacklinksSearchScope } from "./backlinksSearchScope"; +import { + inferBacklinksSearchScopeFromTarget, + resolveBacklinksSearchScope, +} from "./backlinksSearchScope"; type SearchDraft = Pick; @@ -124,11 +127,7 @@ export function BacklinksSearchCard({ if (!userSelectedScope) { form.setFieldValue( "scope", - resolveBacklinksSearchScope({ - target: nextTarget, - selectedScope: form.state.values.scope, - userSelectedScope: false, - }), + inferBacklinksSearchScopeFromTarget(nextTarget), ); } }} diff --git a/src/client/features/backlinks/ReferringDomainsTable.tsx b/src/client/features/backlinks/ReferringDomainsTable.tsx index 7367717..76b3f37 100644 --- a/src/client/features/backlinks/ReferringDomainsTable.tsx +++ b/src/client/features/backlinks/ReferringDomainsTable.tsx @@ -4,6 +4,7 @@ import { type SortingState, } from "@tanstack/react-table"; import { useState } from "react"; +import { SafeExternalLink } from "@/client/components/SafeExternalLink"; import { AppDataTable, useAppTable, @@ -23,7 +24,6 @@ import { formatDecimal, formatNumber, } from "./backlinksPageUtils"; -import { BacklinksExternalLink } from "./BacklinksPageLinks"; type ReferringDomainRow = BacklinksOverviewData["referringDomains"][number]; @@ -60,7 +60,7 @@ const columns = [ const domain = getValue(); if (!domain) return "-"; return ( - { const page = getValue(); return page ? ( - { - const update = getLocationSearchUpdate(nextLocationCode); - if (update) setSearchParams(update); + setSearchParams(getLocationSearchUpdate(nextLocationCode)); }, [setSearchParams], ); diff --git a/src/client/features/domain/components/DomainFilterPanel.tsx b/src/client/features/domain/components/DomainFilterPanel.tsx index f297e4d..c733e22 100644 --- a/src/client/features/domain/components/DomainFilterPanel.tsx +++ b/src/client/features/domain/components/DomainFilterPanel.tsx @@ -60,9 +60,6 @@ export function DomainFilterPanel({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [appliedKey]); - const onValueChange = useCallback((key: keyof TValues, value: string) => { - setDraftFilters((current) => ({ ...current, [key]: value })); - }, []); const meta = useMemo( () => getFilterMeta({ @@ -114,9 +111,9 @@ export function DomainFilterPanel({ field: String(key), valueLength: value.length, }); - onValueChange(key, value); + setDraftFilters((current) => ({ ...current, [key]: value })); }, - [debugName, onValueChange], + [debugName], ); return ( diff --git a/src/client/features/domain/domainFilterUtils.ts b/src/client/features/domain/domainFilterUtils.ts index 6b78db8..cc51a50 100644 --- a/src/client/features/domain/domainFilterUtils.ts +++ b/src/client/features/domain/domainFilterUtils.ts @@ -29,7 +29,7 @@ export const PAGE_FILTER_FIELDS = [ "maxVol", ] as const satisfies ReadonlyArray; -const PAGE_SEARCH_PARAM_BY_FIELD = { +export const PAGE_SEARCH_PARAM_BY_FIELD = { include: "pInclude", exclude: "pExclude", minTraffic: "pMinTraffic", @@ -42,23 +42,10 @@ type SearchUpdate = Partial; type FilterValues = Record; type FilterKey = Extract; -export function getPageFilterSearchParam( - key: PageFilterKey, -): (typeof PAGE_SEARCH_PARAM_BY_FIELD)[PageFilterKey] { - return PAGE_SEARCH_PARAM_BY_FIELD[key]; -} - export function countKeywordFilterConditions( values: KeywordsFilterValues, ): number { - let n = 0; - for (const term of values.include.split(/[,+]/)) if (term.trim()) n += 1; - for (const term of values.exclude.split(/[,+]/)) if (term.trim()) n += 1; - for (const key of KEYWORD_FILTER_FIELDS) { - if (key === "include" || key === "exclude") continue; - if (values[key].trim() !== "") n += 1; - } - return n; + return countFilterConditions(values, KEYWORD_FILTER_FIELDS); } export function countPageFilterConditions(values: PagesFilterValues): number { @@ -81,14 +68,14 @@ export function buildPagesSearchUpdate( return buildFilterSearchUpdate( values, PAGE_FILTER_FIELDS, - (key) => getPageFilterSearchParam(key), + (key) => PAGE_SEARCH_PARAM_BY_FIELD[key], ); } export function buildPagesClearSearchUpdate(): SearchUpdate { return buildFilterClearSearchUpdate( PAGE_FILTER_FIELDS, - (key) => getPageFilterSearchParam(key), + (key) => PAGE_SEARCH_PARAM_BY_FIELD[key], ); } @@ -99,8 +86,9 @@ export function buildDomainFiltersClearSearchUpdate(): SearchUpdate { ); Object.assign( update, - buildFilterClearSearchUpdate(PAGE_FILTER_FIELDS, (key) => - getPageFilterSearchParam(key), + buildFilterClearSearchUpdate( + PAGE_FILTER_FIELDS, + (key) => PAGE_SEARCH_PARAM_BY_FIELD[key], ), ); return update; diff --git a/src/client/features/domain/domainRouteState.ts b/src/client/features/domain/domainRouteState.ts index 8410a18..7e74e46 100644 --- a/src/client/features/domain/domainRouteState.ts +++ b/src/client/features/domain/domainRouteState.ts @@ -18,7 +18,7 @@ import { import { KEYWORD_FILTER_FIELDS, PAGE_FILTER_FIELDS, - getPageFilterSearchParam, + PAGE_SEARCH_PARAM_BY_FIELD, } from "@/client/features/domain/domainFilterUtils"; import { resolveSortOrder, toSortMode, toSortOrder } from "./utils"; @@ -95,6 +95,6 @@ function hasKeywordSearchFilters(search: DomainSearchParams): boolean { function hasPageSearchFilters(search: DomainSearchParams): boolean { return PAGE_FILTER_FIELDS.some( - (key) => search[getPageFilterSearchParam(key)] != null, + (key) => search[PAGE_SEARCH_PARAM_BY_FIELD[key]] != null, ); } diff --git a/src/client/features/keywords/components/KeywordUi.tsx b/src/client/features/keywords/components/KeywordUi.tsx index b2ce64b..5e8da35 100644 --- a/src/client/features/keywords/components/KeywordUi.tsx +++ b/src/client/features/keywords/components/KeywordUi.tsx @@ -17,7 +17,7 @@ export function OverviewStats({ keyword }: { keyword: KeywordResearchRow }) { {keyword.keyword} - +
@@ -47,24 +47,14 @@ export function OverviewStats({ keyword }: { keyword: KeywordResearchRow }) { ); } -function ScoreBadge({ - value, - size = "sm", -}: { - value: number | null; - size?: "sm" | "lg"; -}) { +function ScoreBadge({ value }: { value: number | null }) { if (value == null) return null; const tierClass = scoreTierClass(value); - const sizeClasses = - size === "lg" - ? "size-9 text-sm font-bold" - : "size-6 text-[10px] font-semibold"; return ( {value} diff --git a/src/client/features/keywords/state/useKeywordResearchController.ts b/src/client/features/keywords/state/useKeywordResearchController.ts index f3da608..4718ef1 100644 --- a/src/client/features/keywords/state/useKeywordResearchController.ts +++ b/src/client/features/keywords/state/useKeywordResearchController.ts @@ -58,121 +58,6 @@ export type KeywordResearchControllerInput = { export function useKeywordResearchController( input: KeywordResearchControllerInput, ) { - const state = useKeywordControllerState(input); - const controlsForm = state.controlsForm; - const setSearchParams = state.setSearchParams; - const retryResearch = state.retryResearch; - - const onSearch = useCallback( - (overrides?: Partial<{ keyword: string; locationCode: number }>) => { - if (overrides?.keyword !== undefined) { - controlsForm.setFieldValue("keyword", overrides.keyword); - } - - if (overrides?.locationCode !== undefined) { - controlsForm.setFieldValue("locationCode", overrides.locationCode); - } - - void controlsForm.handleSubmit(); - }, - [controlsForm], - ); - - const retrySearch = useCallback(() => { - void retryResearch(); - }, [retryResearch]); - - const handleSearchSubmit = useCallback( - (event: FormEvent) => { - event.preventDefault(); - void controlsForm.handleSubmit(); - }, - [controlsForm], - ); - - const toggleSort = useCallback( - (field: SortField) => { - setSearchParams(getNextSortParams(input.sortField, input.sortDir, field)); - }, - [input.sortDir, input.sortField, setSearchParams], - ); - - const { handleSaveKeywords, confirmSave, exportCsv, sheetsExportRows } = - useSaveAndExportActions({ - selectedRows: state.selectedRows, - rows: state.rows, - filteredRows: state.filteredRows, - input, - saveKeywordsMutate: state.saveMutation.mutate, - setShowSaveDialog: state.setShowSaveDialog, - }); - - const handleToggleAllRows = () => { - state.toggleAllRows(state.filteredRows.map((row) => row.keyword)); - }; - - const handleRowClick = (row: KeywordResearchRow) => { - captureClientEvent("keyword_research:serp_open"); - state.setSelectedKeyword(row); - state.setSerpKeyword(row.keyword); - state.setSerpPage(0); - }; - - return { - activeFilterCount: state.activeFilterCount, - activeSerpKeyword: state.activeSerpKeyword, - confirmSave, - controlsForm: state.controlsForm, - exportCsv, - sheetsExportRows, - filteredRows: state.filteredRows, - filtersForm: state.filtersForm, - handleRowClick, - handleSaveKeywords, - handleSearchSubmit, - hasSearched: state.hasSearched, - history: state.history, - historyLoaded: state.historyLoaded, - isLoading: state.isLoading, - lastResultSource: state.lastResultSource, - lastSearchError: state.lastSearchError, - lastSearchKeyword: state.lastSearchKeyword, - lastSearchLocationCode: state.lastSearchLocationCode, - lastUsedFallback: state.lastUsedFallback, - mobileTab: state.mobileTab, - onSearch, - overviewKeyword: state.overviewKeyword, - removeHistoryItem: state.removeHistoryItem, - researchError: state.researchError, - researchMutationError: state.researchMutationError, - retrySearch, - resetFilters: state.resetFilters, - rows: state.rows, - searchedKeyword: state.searchedKeyword, - selectedRows: state.selectedRows, - serpError: state.serpError, - serpLoading: state.serpLoading, - serpPage: state.serpPage, - serpQuery: state.serpQuery, - serpResults: state.serpResults, - setMobileTab: state.setMobileTab, - setSelectedRows: state.setSelectedRows, - setSerpPage: state.setSerpPage, - setShowFilters: state.setShowFilters, - setShowSaveDialog: state.setShowSaveDialog, - showApproximateMatchNotice: state.showApproximateMatchNotice, - showFilters: state.showFilters, - showSaveDialog: state.showSaveDialog, - sortDir: input.sortDir, - sortField: input.sortField, - toggleAllRows: handleToggleAllRows, - toggleRowSelection: state.toggleRowSelection, - toggleSort, - SERP_PAGE_SIZE: state.SERP_PAGE_SIZE, - }; -} - -function useKeywordControllerState(input: KeywordResearchControllerInput) { const { locationCode, setPreferredLocationCode } = useResolvedKeywordLocation(input); const { @@ -277,11 +162,6 @@ function useKeywordControllerState(input: KeywordResearchControllerInput) { previousSearchKeyRef.current = activeSearchKey; handledSerpSearchKeyRef.current = null; - if (!activeSearchKey) { - clearActiveKeywordResult(); - return; - } - clearActiveKeywordResult(); }, [activeSearchKey, clearActiveKeywordResult]); @@ -319,13 +199,58 @@ function useKeywordControllerState(input: KeywordResearchControllerInput) { keywordMode: input.keywordMode, }); + const retrySearch = useCallback(() => { + void retryResearch(); + }, [retryResearch]); + + const handleSearchSubmit = useCallback( + (event: FormEvent) => { + event.preventDefault(); + void controlsForm.handleSubmit(); + }, + [controlsForm], + ); + + const toggleSort = useCallback( + (field: SortField) => { + setSearchParams(getNextSortParams(input.sortField, input.sortDir, field)); + }, + [input.sortDir, input.sortField, setSearchParams], + ); + + const { handleSaveKeywords, confirmSave, exportCsv, sheetsExportRows } = + useSaveAndExportActions({ + selectedRows, + rows, + filteredRows, + input, + saveKeywordsMutate: saveMutation.mutate, + setShowSaveDialog: uiState.setShowSaveDialog, + }); + + const handleToggleAllRows = () => { + toggleAllRows(filteredRows.map((row) => row.keyword)); + }; + + const handleRowClick = (row: KeywordResearchRow) => { + captureClientEvent("keyword_research:serp_open"); + uiState.setSelectedKeyword(row); + setSerpKeyword(row.keyword); + setSerpPage(0); + }; + return { activeFilterCount, activeSerpKeyword, - clearSelection, + confirmSave, controlsForm, + exportCsv, + sheetsExportRows, filteredRows, filtersForm, + handleRowClick, + handleSaveKeywords, + handleSearchSubmit, hasSearched, history, historyLoaded, @@ -340,32 +265,29 @@ function useKeywordControllerState(input: KeywordResearchControllerInput) { removeHistoryItem, researchError, researchMutationError, - retryResearch, + retrySearch, resetFilters, rows, searchedKeyword, - selectedKeyword: uiState.selectedKeyword, selectedRows, - setSelectedRows, - saveMutation, - setPreferredLocationCode, - setSelectedKeyword: uiState.setSelectedKeyword, - setSearchParams, - setSerpKeyword, serpError, serpLoading, serpPage, serpQuery, serpResults, setMobileTab: uiState.setMobileTab, + setSelectedRows, setSerpPage, setShowFilters: uiState.setShowFilters, setShowSaveDialog: uiState.setShowSaveDialog, showApproximateMatchNotice, showFilters: uiState.showFilters, showSaveDialog: uiState.showSaveDialog, - toggleAllRows, + sortDir: input.sortDir, + sortField: input.sortField, + toggleAllRows: handleToggleAllRows, toggleRowSelection, + toggleSort, SERP_PAGE_SIZE, }; } diff --git a/src/client/features/lighthouse/issues/LighthouseIssuesScreen.tsx b/src/client/features/lighthouse/issues/LighthouseIssuesScreen.tsx index 0caf3dc..44d3fb3 100644 --- a/src/client/features/lighthouse/issues/LighthouseIssuesScreen.tsx +++ b/src/client/features/lighthouse/issues/LighthouseIssuesScreen.tsx @@ -5,15 +5,10 @@ import { exportAuditLighthouseIssues, getAuditLighthouseIssues, } from "@/serverFunctions/lighthouse"; +import { downloadFile } from "@/client/lib/download"; import { exportTableToSheets } from "@/client/lib/exportToSheets"; import type { CategoryTab, ExportPayload, LighthouseIssue } from "./types"; -import { - categoryLabel, - categorySlug, - downloadTextFile, - issuesToCsv, - issuesToTable, -} from "./utils"; +import { categoryLabel, issuesToCsv, issuesToTable } from "./utils"; import { LighthouseIssueList, LighthouseIssuesHeader, @@ -170,7 +165,7 @@ function useLighthouseIssuesActions({ const runExport = async (data: ExportPayload) => { try { const exported = await exportMutation.mutateAsync(data); - downloadTextFile(exported.filename, exported.content, "application/json"); + downloadFile(exported.content, exported.filename, "application/json"); toast.success("Download started"); } catch (error) { const message = @@ -183,8 +178,8 @@ function useLighthouseIssuesActions({ rows: LighthouseIssue[], variant: "all" | "current", ) => { - const filename = `lighthouse-${variant}-${categorySlug(category)}-issues.csv`; - downloadTextFile(filename, issuesToCsv(rows), "text/csv"); + const filename = `lighthouse-${variant}-${category}-issues.csv`; + downloadFile(issuesToCsv(rows), filename, "text/csv"); toast.success("CSV download started"); }; diff --git a/src/client/features/lighthouse/issues/utils.tsx b/src/client/features/lighthouse/issues/utils.tsx index 1ee2885..41bc236 100644 --- a/src/client/features/lighthouse/issues/utils.tsx +++ b/src/client/features/lighthouse/issues/utils.tsx @@ -37,23 +37,6 @@ export function categoryLabel(category: CategoryTab) { return `${category.charAt(0).toUpperCase()}${category.slice(1)}`; } -export function categorySlug(category: CategoryTab) { - return category === "all" ? "all" : category; -} - export function issuesToCsv(issues: LighthouseIssue[]) { return buildCsv(ISSUE_HEADERS, issuesToRows(issues)); } - -export function downloadTextFile( - filename: string, - content: string, - mimeType: string, -) { - const blob = new Blob([content], { type: mimeType }); - const link = document.createElement("a"); - link.href = URL.createObjectURL(blob); - link.download = filename; - link.click(); - URL.revokeObjectURL(link.href); -} diff --git a/src/client/features/onboarding/PostSignupOnboarding.tsx b/src/client/features/onboarding/PostSignupOnboarding.tsx index f5a3c12..6dbb17b 100644 --- a/src/client/features/onboarding/PostSignupOnboarding.tsx +++ b/src/client/features/onboarding/PostSignupOnboarding.tsx @@ -234,7 +234,6 @@ function McpRecommendation({ function OnboardingChoiceGroup({ title, description, - reason, options, selectedValues, onToggle, @@ -246,7 +245,6 @@ function OnboardingChoiceGroup({ }: { title: string; description?: string; - reason?: string; options: string[]; selectedValues: string[]; onToggle: (value: string) => void; @@ -275,11 +273,6 @@ function OnboardingChoiceGroup({ {description ? (

{description}

) : null} - {reason ? ( -

- {reason} -

- ) : null}
diff --git a/src/client/features/rank-tracking/AddKeywordsPanel.tsx b/src/client/features/rank-tracking/AddKeywordsPanel.tsx index b353b01..8264aaa 100644 --- a/src/client/features/rank-tracking/AddKeywordsPanel.tsx +++ b/src/client/features/rank-tracking/AddKeywordsPanel.tsx @@ -13,11 +13,7 @@ export function AddKeywordsPanel({ }: { configId: string; projectId: string; - onSuccess: (result: { - added: number; - addedIds: string[]; - checkTriggered: boolean; - }) => void; + onSuccess: (result: { added: number; checkTriggered: boolean }) => void; onCancel: () => void; }) { const [keywordInput, setKeywordInput] = useState(""); diff --git a/src/client/features/rank-tracking/RankTrackingColumns.tsx b/src/client/features/rank-tracking/RankTrackingColumns.tsx index 020d67c..b996753 100644 --- a/src/client/features/rank-tracking/RankTrackingColumns.tsx +++ b/src/client/features/rank-tracking/RankTrackingColumns.tsx @@ -4,7 +4,6 @@ import type { ColumnDef, SortingFn } from "@tanstack/react-table"; import { makeSelectionColumn } from "@/client/components/table/AppDataTable"; import type { RankTrackingRow } from "@/types/schemas/rank-tracking"; import { - comparePositions, CpcCell, DeviceRankCell, DeviceUrlCell, @@ -101,20 +100,6 @@ const cpcColumn: ColumnDef = { sortingFn: nullsLastNumeric, }; -const positionSort: SortingFn = (rowA, rowB, columnId) => { - const device = columnId === "desktopPosition" ? "desktop" : "mobile"; - return comparePositions( - rowA.original[device].position, - rowB.original[device].position, - ); -}; - -function makeSelectColumn( - anchorRef: MutableRefObject, -): ColumnDef { - return makeSelectionColumn(anchorRef); -} - const keywordColumn: ColumnDef = { id: "keyword", accessorKey: "keyword", @@ -140,7 +125,7 @@ function makeDeviceColumn( size: 120, maxSize: 140, cell: ({ row }) => , - sortingFn: positionSort, + sortingFn: nullsLastNumeric, }; } @@ -196,7 +181,7 @@ export function useRankTrackingColumns( ): ColumnDef[] { return useMemo(() => { const cols: ColumnDef[] = [ - makeSelectColumn(selectAnchorRef), + makeSelectionColumn(selectAnchorRef), keywordColumn, ]; if (showDesktop) { diff --git a/src/client/features/rank-tracking/RankTrackingDomainDetail.tsx b/src/client/features/rank-tracking/RankTrackingDomainDetail.tsx index 632b003..21ba854 100644 --- a/src/client/features/rank-tracking/RankTrackingDomainDetail.tsx +++ b/src/client/features/rank-tracking/RankTrackingDomainDetail.tsx @@ -122,7 +122,6 @@ function RankTrackingDomainDetailInner({ const handleKeywordsAdded = (result: { added: number; - addedIds: string[]; checkTriggered: boolean; }) => { void queryClient.invalidateQueries({ diff --git a/src/client/features/rank-tracking/RankTrackingDomainList.tsx b/src/client/features/rank-tracking/RankTrackingDomainList.tsx index e7ffdc1..7959b5d 100644 --- a/src/client/features/rank-tracking/RankTrackingDomainList.tsx +++ b/src/client/features/rank-tracking/RankTrackingDomainList.tsx @@ -14,10 +14,7 @@ import { getRankTrackingConfigSummaries, updateRankTrackingConfig, } from "@/serverFunctions/rank-tracking"; -import { - devicesLabel as getDevicesLabel, - scheduleLabel as getScheduleLabel, -} from "@/shared/rank-tracking"; +import { devicesLabel, scheduleLabel } from "@/shared/rank-tracking"; import { Modal } from "@/client/components/Modal"; type ConfigSummary = Awaited< @@ -139,9 +136,6 @@ function DomainRow({ summary: ConfigSummary; onArchive: () => void; }) { - const dl = getDevicesLabel(summary.devices); - const sl = getScheduleLabel(summary.scheduleInterval); - return (

{summary.domain}

- {LOCATIONS[summary.locationCode] ?? "US"} · {dl} · {sl} + {LOCATIONS[summary.locationCode] ?? "US"} ·{" "} + {devicesLabel(summary.devices)} ·{" "} + {scheduleLabel(summary.scheduleInterval)} {summary.lastRunCompletedAt && ( <> {" "} diff --git a/src/client/features/rank-tracking/RankTrackingTableParts.tsx b/src/client/features/rank-tracking/RankTrackingTableParts.tsx index d1df669..cc80431 100644 --- a/src/client/features/rank-tracking/RankTrackingTableParts.tsx +++ b/src/client/features/rank-tracking/RankTrackingTableParts.tsx @@ -159,13 +159,6 @@ export function CpcCell({ value }: { value: number | null }) { return ${value.toFixed(2)}; } -export function comparePositions(a: number | null, b: number | null): number { - if (a === null && b === null) return 0; - if (a === null) return 1; // nulls sort last - if (b === null) return -1; - return a - b; -} - /** Numeric change for CSV export — numbers bypass the CSV formula-injection sanitizer */ function csvChange( current: number | null, diff --git a/src/client/lib/download.ts b/src/client/lib/download.ts new file mode 100644 index 0000000..6c82e56 --- /dev/null +++ b/src/client/lib/download.ts @@ -0,0 +1,9 @@ +export function downloadFile(content: string, filename: string, mime: string) { + const blob = new Blob([content], { type: `${mime};charset=utf-8;` }); + const url = URL.createObjectURL(blob); + const link = document.createElement("a"); + link.href = url; + link.download = filename; + link.click(); + URL.revokeObjectURL(url); +} diff --git a/src/middleware/ensureUser.ts b/src/middleware/ensureUser.ts index 41406b6..4fa6dcd 100644 --- a/src/middleware/ensureUser.ts +++ b/src/middleware/ensureUser.ts @@ -23,14 +23,10 @@ function extractProjectId(data: unknown) { : null; } -function getRuntimeAuthMode() { - return getAuthMode(import.meta.env.AUTH_MODE ?? env.AUTH_MODE); -} - export const ensureUserMiddleware = createMiddleware({ type: "function", }).server(async ({ next, data }) => { - const authMode = getRuntimeAuthMode(); + const authMode = getAuthMode(import.meta.env.AUTH_MODE ?? env.AUTH_MODE); const headers = getRequest().headers; let context: EnsuredUserContext; diff --git a/src/routes/_app/ai.tsx b/src/routes/_app/ai.tsx index c1cb1bd..82fa898 100644 --- a/src/routes/_app/ai.tsx +++ b/src/routes/_app/ai.tsx @@ -59,11 +59,7 @@ function AiPage() {

MCP server URL

- +
{mcpUrl} diff --git a/src/routes/_auth.sign-in.tsx b/src/routes/_auth.sign-in.tsx index 4640c12..4ec362e 100644 --- a/src/routes/_auth.sign-in.tsx +++ b/src/routes/_auth.sign-in.tsx @@ -6,10 +6,9 @@ import { AuthPageCard, AuthMethodChooser, authRedirectSearchSchema, - getFieldError, - getFormError, useAuthPageState, } from "@/client/features/auth/AuthPage"; +import { getFieldError, getFormError } from "@/client/lib/forms"; import { captureClientEvent } from "@/client/lib/posthog"; import { authClient } from "@/lib/auth-client"; import { getSignInSearch } from "@/lib/auth-redirect"; diff --git a/src/routes/_auth.sign-up.tsx b/src/routes/_auth.sign-up.tsx index 5f015f9..c3bf066 100644 --- a/src/routes/_auth.sign-up.tsx +++ b/src/routes/_auth.sign-up.tsx @@ -5,10 +5,9 @@ import { AuthPageCard, AuthMethodChooser, authRedirectSearchSchema, - getFieldError, - getFormError, useAuthPageState, } from "@/client/features/auth/AuthPage"; +import { getFieldError, getFormError } from "@/client/lib/forms"; import { captureClientEvent } from "@/client/lib/posthog"; import { authClient } from "@/lib/auth-client"; import { getSignInSearch, getVerifyEmailSearch } from "@/lib/auth-redirect"; diff --git a/src/routes/forgot-password.tsx b/src/routes/forgot-password.tsx index e854d72..e16e351 100644 --- a/src/routes/forgot-password.tsx +++ b/src/routes/forgot-password.tsx @@ -4,9 +4,8 @@ import { AuthPageCard, AuthPageShell, authRedirectSearchSchema, - getFieldError, - getFormError, } from "@/client/features/auth/AuthPage"; +import { getFieldError, getFormError } from "@/client/lib/forms"; import { authClient } from "@/lib/auth-client"; import { isHostedClientAuthMode } from "@/lib/auth-mode"; import { getSignInSearch, normalizeAuthRedirect } from "@/lib/auth-redirect"; diff --git a/src/routes/reset-password.tsx b/src/routes/reset-password.tsx index 6d439bc..d098e7b 100644 --- a/src/routes/reset-password.tsx +++ b/src/routes/reset-password.tsx @@ -4,9 +4,8 @@ import { AuthPageCard, AuthPageShell, authRedirectSearchSchema, - getFieldError, - getFormError, } from "@/client/features/auth/AuthPage"; +import { getFieldError, getFormError } from "@/client/lib/forms"; import { authClient } from "@/lib/auth-client"; import { isHostedClientAuthMode } from "@/lib/auth-mode"; import { getSignInSearch, normalizeAuthRedirect } from "@/lib/auth-redirect"; diff --git a/src/routes/verify-email.tsx b/src/routes/verify-email.tsx index e139053..cd4615a 100644 --- a/src/routes/verify-email.tsx +++ b/src/routes/verify-email.tsx @@ -230,11 +230,7 @@ function VerifyEmailPage() { {isResending ? "Sending email..." : "Resend email"}
- ) : isPending ? ( -
- -
- ) : isVerified ? ( + ) : isPending || isVerified ? (
diff --git a/src/server.ts b/src/server.ts index 621b23a..6993b14 100644 --- a/src/server.ts +++ b/src/server.ts @@ -17,9 +17,7 @@ import { handleSelfHostedOpenSeoMcpRequest } from "@/server/mcp/transport"; import { computeNextCheckAt } from "@/shared/rank-tracking"; const appFetch = createStartHandler(defaultStreamHandler); -const handleAppFetch = (request: Request): Response | Promise => - appFetch(request); -const openSeoOAuthProvider = createOpenSeoOAuthProvider(handleAppFetch); +const openSeoOAuthProvider = createOpenSeoOAuthProvider(appFetch); function fetch( request: Request, @@ -44,7 +42,7 @@ function fetch( return handleSelfHostedOpenSeoMcpRequest(publicRequest, authMode, env, ctx); } - return handleAppFetch(request); + return appFetch(request); } // Export Workflow classes as named exports diff --git a/src/server/auth/default-hosted-organization.ts b/src/server/auth/default-hosted-organization.ts index 2419806..036395c 100644 --- a/src/server/auth/default-hosted-organization.ts +++ b/src/server/auth/default-hosted-organization.ts @@ -1,6 +1,7 @@ import { asc, eq } from "drizzle-orm"; import { db } from "@/db"; import { member, user as authUser } from "@/db/better-auth-schema"; +import { slugify, toHex } from "./org-slug"; type HostedUser = { id: string; @@ -18,23 +19,6 @@ type HostedOrganizationCreator = ( input: HostedOrganizationCreateInput, ) => Promise<{ id: string }>; -function slugify(value: string) { - const slug = value - .toLowerCase() - .trim() - .replace(/[^a-z0-9]+/g, "-") - .replace(/^-+|-+$/g, "") - .slice(0, 48); - - return slug || "workspace"; -} - -function toHex(value: string) { - return Array.from(new TextEncoder().encode(value), (byte) => - byte.toString(16).padStart(2, "0"), - ).join(""); -} - function getDefaultHostedOrganizationName(user: HostedUser) { const name = user.name?.trim() || user.email.split("@")[0] || "OpenSEO"; return `${name}'s workspace`; diff --git a/src/server/auth/delegated-organization.ts b/src/server/auth/delegated-organization.ts index 276acfc..9aa0daf 100644 --- a/src/server/auth/delegated-organization.ts +++ b/src/server/auth/delegated-organization.ts @@ -1,22 +1,6 @@ import { db } from "@/db"; import { organization } from "@/db/better-auth-schema"; - -function slugify(value: string) { - const slug = value - .toLowerCase() - .trim() - .replace(/[^a-z0-9]+/g, "-") - .replace(/^-+|-+$/g, "") - .slice(0, 48); - - return slug || "workspace"; -} - -function toHex(value: string) { - return Array.from(new TextEncoder().encode(value), (byte) => - byte.toString(16).padStart(2, "0"), - ).join(""); -} +import { slugify, toHex } from "./org-slug"; function getDelegatedOrganizationId(userId: string) { return `delegated-${userId}`; diff --git a/src/server/auth/org-slug.ts b/src/server/auth/org-slug.ts new file mode 100644 index 0000000..ede1ff5 --- /dev/null +++ b/src/server/auth/org-slug.ts @@ -0,0 +1,16 @@ +export function slugify(value: string) { + const slug = value + .toLowerCase() + .trim() + .replace(/[^a-z0-9]+/g, "-") + .replace(/^-+|-+$/g, "") + .slice(0, 48); + + return slug || "workspace"; +} + +export function toHex(value: string) { + return Array.from(new TextEncoder().encode(value), (byte) => + byte.toString(16).padStart(2, "0"), + ).join(""); +} diff --git a/src/server/email/loops.ts b/src/server/email/loops.ts index 86a3c40..1b6c4c4 100644 --- a/src/server/email/loops.ts +++ b/src/server/email/loops.ts @@ -3,15 +3,21 @@ import { env } from "cloudflare:workers"; const LOOPS_TRANSACTIONAL_URL = "https://app.loops.so/api/v1/transactional"; const LOOPS_CONTACT_UPDATE_URL = "https://app.loops.so/api/v1/contacts/update"; -function getRequiredEnv(name: string) { +function getOptionalEnv(name: string) { const value: unknown = Reflect.get(env, name); const trimmed = typeof value === "string" ? value.trim() : ""; - if (!trimmed) { + return trimmed || null; +} + +function getRequiredEnv(name: string) { + const value = getOptionalEnv(name); + + if (!value) { throw new Error(`${name} is required in hosted mode`); } - return trimmed; + return value; } function getHostedAuthEmailConfig() { @@ -68,13 +74,6 @@ async function sendLoopsTransactionalEmail({ ); } -function getOptionalEnv(name: string) { - const value: unknown = Reflect.get(env, name); - const trimmed = typeof value === "string" ? value.trim() : ""; - - return trimmed || null; -} - function getContactNameParts(name: string | null | undefined) { const trimmedName = name?.trim(); diff --git a/src/server/features/backlinks/services/backlinksServiceData.ts b/src/server/features/backlinks/services/backlinksServiceData.ts index 6ece1b3..f359903 100644 --- a/src/server/features/backlinks/services/backlinksServiceData.ts +++ b/src/server/features/backlinks/services/backlinksServiceData.ts @@ -1,7 +1,6 @@ import { z } from "zod"; import type { BillingCustomerContext } from "@/server/billing/subscription"; import { - type BacklinksRequest, type fetchBacklinksHistoryRaw, type fetchBacklinksRowsRaw, type fetchBacklinksSummaryRaw, @@ -159,9 +158,10 @@ export async function profileTopPagesRows( const dataforseo = createDataforseoClient(billingCustomer); - const request = buildBacklinksRequest( - normalizeBacklinksTarget(input.target, { scope: input.scope }).apiTarget, - ); + const request = { + target: normalizeBacklinksTarget(input.target, { scope: input.scope }) + .apiTarget, + }; const response = await dataforseo.backlinks.domainPages({ ...request, limit: 100, @@ -173,17 +173,13 @@ export async function profileTopPagesRows( return { rows }; } -function buildBacklinksRequest(target: string): BacklinksRequest { - return { target }; -} - function buildBacklinksListRequest( target: string, limit: number, options?: BacklinksSpamFilterOptions, ) { return { - ...buildBacklinksRequest(target), + target, limit, ...normalizeBacklinksSpamFilterOptions(options), }; diff --git a/src/server/features/keywords/services/research/research.ts b/src/server/features/keywords/services/research/research.ts index 05fcbeb..7308c2d 100644 --- a/src/server/features/keywords/services/research/research.ts +++ b/src/server/features/keywords/services/research/research.ts @@ -83,10 +83,6 @@ const cachedResultSchema = z.object({ const CACHE_VERSION = 2; -function getMode(input: ResearchKeywordsInput): KeywordMode { - return input.mode ?? "auto"; -} - async function fetchRowsFromSource( source: KeywordSource, input: ResearchKeywordsInput, @@ -195,12 +191,6 @@ async function fetchManualRows( }; } -function isUsableCachedResult(cached: CachedResult): boolean { - if (cached.rows.length === 0) return false; - - return true; -} - async function buildResearchCacheKey( input: ResearchKeywordsInput, normalizedKeywords: string[], @@ -254,7 +244,7 @@ export async function research( } const seedKeyword = uniqueKeywords[0]; - const mode = getMode(input); + const mode = input.mode ?? "auto"; const cacheKey = await buildResearchCacheKey( input, uniqueKeywords, @@ -268,7 +258,7 @@ export async function research( ? cachedResult.data : null; - if (cached && isUsableCachedResult(cached)) { + if (cached && cached.rows.length > 0) { return cached; } diff --git a/src/server/features/rank-tracking/services/RankTrackingService.ts b/src/server/features/rank-tracking/services/RankTrackingService.ts index a30fc9e..937c95e 100644 --- a/src/server/features/rank-tracking/services/RankTrackingService.ts +++ b/src/server/features/rank-tracking/services/RankTrackingService.ts @@ -69,7 +69,7 @@ async function createConfig(input: { id: configId, projectId: input.projectId, domain: normalizedDomain, - locationCode: input.locationCode ?? 2840, + locationCode, languageCode: input.languageCode ?? "en", devices: input.devices ?? "both", serpDepth: input.serpDepth, diff --git a/src/server/features/rank-tracking/services/rankTrackingResults.ts b/src/server/features/rank-tracking/services/rankTrackingResults.ts index 16659f7..0e94427 100644 --- a/src/server/features/rank-tracking/services/rankTrackingResults.ts +++ b/src/server/features/rank-tracking/services/rankTrackingResults.ts @@ -124,9 +124,7 @@ export async function getLatestResults( } return { - rows: activeKeywords - .map((keyword) => rows.get(keyword.id)) - .filter((row): row is RankTrackingRow => row != null), + rows: [...rows.values()], run: latestRunId && latestStartedAt ? { id: latestRunId, lastCheckedAt: latestStartedAt } diff --git a/src/server/lib/audit/discovery.ts b/src/server/lib/audit/discovery.ts index c0cfc73..f37b0a4 100644 --- a/src/server/lib/audit/discovery.ts +++ b/src/server/lib/audit/discovery.ts @@ -119,10 +119,6 @@ function isTimeoutError(error: unknown): boolean { return "name" in error && error.name === "TimeoutError"; } -function parseXmlDocument(body: string): unknown { - return xmlParser.parse(body) as unknown; -} - async function fetchSitemapDocumentWithRetry(sitemapUrl: string): Promise<{ nestedSitemaps: string[]; pageUrls: string[]; @@ -156,7 +152,7 @@ async function fetchSitemapDocumentWithRetry(sitemapUrl: string): Promise<{ return { nestedSitemaps: [], pageUrls: [], timedOut: false }; } - const parsed = parseXmlDocument(body); + const parsed = xmlParser.parse(body) as unknown; const sections = getParsedSitemapSections(parsed); const nestedSitemaps = getSitemapLocations(sections.sitemap) .map((loc) => normalizeUrl(loc, finalUrl)) diff --git a/src/server/lib/audit/page-analyzer.ts b/src/server/lib/audit/page-analyzer.ts index e44baf2..e853ae0 100644 --- a/src/server/lib/audit/page-analyzer.ts +++ b/src/server/lib/audit/page-analyzer.ts @@ -21,17 +21,13 @@ export function analyzeHtml( ): PageAnalysis { const $ = cheerio.load(html); - // --- Title --- const title = $("title").first().text().trim(); - // --- Meta description --- const metaDescription = $('meta[name="description"]').first().attr("content")?.trim() ?? ""; - // --- Canonical --- const canonical = $('link[rel="canonical"]').first().attr("href") ?? null; - // --- Robots meta --- const robotsMeta = $('meta[name="robots"]').first().attr("content") ?? null; // --- Open Graph --- @@ -67,7 +63,6 @@ export function analyzeHtml( const bodyText = bodyClone.text().replace(/\s+/g, " ").trim(); const wordCount = bodyText ? bodyText.split(/\s+/).length : 0; - // --- Images --- const images: Array<{ src: string | null; alt: string | null }> = []; $("img").each((_, el) => { images.push({ @@ -103,7 +98,6 @@ export function analyzeHtml( hasStructuredData = true; }); - // --- Hreflang --- const hreflangTags: string[] = []; $('link[rel="alternate"][hreflang]').each((_, el) => { const hreflang = $(el).attr("hreflang"); diff --git a/src/server/lib/audit/progress-kv.ts b/src/server/lib/audit/progress-kv.ts index 244c6cb..d3ce160 100644 --- a/src/server/lib/audit/progress-kv.ts +++ b/src/server/lib/audit/progress-kv.ts @@ -37,17 +37,6 @@ function key(auditId: string): string { return `${KV_PREFIX}${auditId}`; } -/** - * Append a crawled URL entry to the progress list. - * Newest entries are prepended so the array is sorted newest-first. - */ -async function pushCrawledUrl( - auditId: string, - entry: CrawledUrlEntry, -): Promise { - await pushCrawledUrls(auditId, [entry]); -} - /** * Append multiple crawled URL entries in one KV write. * New entries are prepended and the list is capped. @@ -85,7 +74,6 @@ async function clear(auditId: string): Promise { } export const AuditProgressKV = { - pushCrawledUrl, pushCrawledUrls, getCrawledUrls, clear, diff --git a/src/server/lib/errors.ts b/src/server/lib/errors.ts index 27f142b..610a69a 100644 --- a/src/server/lib/errors.ts +++ b/src/server/lib/errors.ts @@ -19,11 +19,7 @@ export function asAppError(error: unknown): AppError | null { return null; } -function toErrorCode(error: unknown): ErrorCode { - return asAppError(error)?.code ?? "INTERNAL_ERROR"; -} - export function toClientError(error: unknown): Error { const appError = asAppError(error); - return new Error(appError?.code ?? toErrorCode(error)); + return new Error(appError?.code ?? "INTERNAL_ERROR"); } diff --git a/src/server/lib/lighthouseStoredPayload.ts b/src/server/lib/lighthouseStoredPayload.ts index 744b0e5..03376d5 100644 --- a/src/server/lib/lighthouseStoredPayload.ts +++ b/src/server/lib/lighthouseStoredPayload.ts @@ -1,62 +1,5 @@ import { z } from "zod"; -import { - LIGHTHOUSE_CATEGORIES, - type LighthouseCategory, -} from "@/shared/lighthouse"; - -export type StoredLighthouseIssue = { - category: LighthouseCategory; - auditKey: string; - title: string; - description: string; - score: number | null; - scoreDisplayMode: string | null; - displayValue: string | null; - impactMs: number | null; - impactBytes: number | null; - severity: "critical" | "warning" | "info"; - items: string[]; -}; - -type StoredLighthouseMetric = { - score: number | null; - displayValue: string | null; - numericValue: number | null; -}; - -export type StoredLighthouseMetrics = { - firstContentfulPaint: StoredLighthouseMetric; - largestContentfulPaint: StoredLighthouseMetric; - totalBlockingTime: StoredLighthouseMetric; - cumulativeLayoutShift: StoredLighthouseMetric; - speedIndex: StoredLighthouseMetric; - timeToInteractive: StoredLighthouseMetric; - interactionToNextPaint: StoredLighthouseMetric; - serverResponseTime: StoredLighthouseMetric; -}; - -export type StoredLighthousePayload = { - version: 2; - source: "dataforseo-lighthouse"; - hasIssueDetails: boolean; - metadata: { - requestedUrl: string; - finalUrl: string; - strategy: "mobile" | "desktop"; - fetchedAt: string; - lighthouseVersion: string | null; - taskId: string | null; - cost: number | null; - }; - scores: { - performance: number | null; - accessibility: number | null; - "best-practices": number | null; - seo: number | null; - }; - metrics: StoredLighthouseMetrics; - issues: StoredLighthouseIssue[]; -}; +import { LIGHTHOUSE_CATEGORIES } from "@/shared/lighthouse"; export type RawLighthouseAudit = { title?: string; @@ -85,6 +28,31 @@ const storedLighthouseMetricSchema = z.object({ numericValue: z.number().nullable(), }); +const storedLighthouseMetricsSchema = z.object({ + firstContentfulPaint: storedLighthouseMetricSchema, + largestContentfulPaint: storedLighthouseMetricSchema, + totalBlockingTime: storedLighthouseMetricSchema, + cumulativeLayoutShift: storedLighthouseMetricSchema, + speedIndex: storedLighthouseMetricSchema, + timeToInteractive: storedLighthouseMetricSchema, + interactionToNextPaint: storedLighthouseMetricSchema, + serverResponseTime: storedLighthouseMetricSchema, +}); + +const storedLighthouseIssueSchema = z.object({ + category: z.enum(LIGHTHOUSE_CATEGORIES), + auditKey: z.string(), + title: z.string(), + description: z.string(), + score: z.number().nullable(), + scoreDisplayMode: z.string().nullable(), + displayValue: z.string().nullable(), + impactMs: z.number().nullable(), + impactBytes: z.number().nullable(), + severity: z.enum(["critical", "warning", "info"]), + items: z.array(z.string()), +}); + export const storedLighthousePayloadSchema = z.object({ version: z.literal(2), source: z.literal("dataforseo-lighthouse"), @@ -104,33 +72,17 @@ export const storedLighthousePayloadSchema = z.object({ "best-practices": z.number().nullable(), seo: z.number().nullable(), }), - metrics: z.object({ - firstContentfulPaint: storedLighthouseMetricSchema, - largestContentfulPaint: storedLighthouseMetricSchema, - totalBlockingTime: storedLighthouseMetricSchema, - cumulativeLayoutShift: storedLighthouseMetricSchema, - speedIndex: storedLighthouseMetricSchema, - timeToInteractive: storedLighthouseMetricSchema, - interactionToNextPaint: storedLighthouseMetricSchema, - serverResponseTime: storedLighthouseMetricSchema, - }), - issues: z.array( - z.object({ - category: z.enum(LIGHTHOUSE_CATEGORIES), - auditKey: z.string(), - title: z.string(), - description: z.string(), - score: z.number().nullable(), - scoreDisplayMode: z.string().nullable(), - displayValue: z.string().nullable(), - impactMs: z.number().nullable(), - impactBytes: z.number().nullable(), - severity: z.enum(["critical", "warning", "info"]), - items: z.array(z.string()), - }), - ), + metrics: storedLighthouseMetricsSchema, + issues: z.array(storedLighthouseIssueSchema), }); +type StoredLighthouseMetric = z.infer; +type StoredLighthouseMetrics = z.infer; +export type StoredLighthouseIssue = z.infer; +export type StoredLighthousePayload = z.infer< + typeof storedLighthousePayloadSchema +>; + export function scoreToPercent( score: number | null | undefined, ): number | null { @@ -244,7 +196,7 @@ export function buildStoredLighthouseIssues(input: { const isPass = score == null || - (score != null && score >= 90) || + score >= 90 || scoreDisplayMode === "notApplicable" || scoreDisplayMode === "informative" || scoreDisplayMode === "manual" || diff --git a/src/server/lib/reddit-conversions.ts b/src/server/lib/reddit-conversions.ts index 7a74fe0..d146172 100644 --- a/src/server/lib/reddit-conversions.ts +++ b/src/server/lib/reddit-conversions.ts @@ -82,12 +82,7 @@ async function upsertAttribution(args: CaptureRedditConversionArgs) { updatedAt: now, }); } -} -async function hasSentConversion(args: CaptureRedditConversionArgs) { - const existing = await db.query.redditAttributions.findFirst({ - where: eq(redditAttributions.userId, args.userId), - }); return args.eventType === "SIGN_UP" ? Boolean(existing?.signupSentAt) : Boolean(existing?.purchaseSentAt); @@ -112,8 +107,8 @@ export async function captureRedditConversion( ) { if (!hasRedditAttribution(args.attribution)) return "skipped" as const; - await upsertAttribution(args); - if (await hasSentConversion(args)) return "already_sent" as const; + const alreadySent = await upsertAttribution(args); + if (alreadySent) return "already_sent" as const; const config = getRedditConfig(); if (!config) return "stored" as const; diff --git a/src/server/mcp/context.ts b/src/server/mcp/context.ts index ac595df..65552e9 100644 --- a/src/server/mcp/context.ts +++ b/src/server/mcp/context.ts @@ -90,10 +90,6 @@ export function getAuth(extra: ToolExtra): McpAuth { return auth; } -export function getBaseUrl(extra: ToolExtra): string { - return requireMcpToolAuthContext(extra).baseUrl; -} - export function buildBillingCustomer( auth: McpAuth, projectId: string, diff --git a/src/server/mcp/formatters.ts b/src/server/mcp/formatters.ts index f5a7762..c7623b4 100644 --- a/src/server/mcp/formatters.ts +++ b/src/server/mcp/formatters.ts @@ -24,18 +24,16 @@ export function mcpResponse(opts: { if (value !== undefined) meta[key] = value; } } + const hasMeta = meta != null && Object.keys(meta).length > 0; if (opts.structuredContent) { - result.structuredContent = - meta && Object.keys(meta).length > 0 - ? { ...opts.structuredContent, meta } - : opts.structuredContent; - } else if (meta && Object.keys(meta).length > 0) { + result.structuredContent = hasMeta + ? { ...opts.structuredContent, meta } + : opts.structuredContent; + } else if (hasMeta) { result.structuredContent = { meta }; } - if (meta) { - if (Object.keys(meta).length > 0) { - result._meta = meta; - } + if (hasMeta) { + result._meta = meta; } return result; } diff --git a/src/server/mcp/project-auth.ts b/src/server/mcp/project-auth.ts index 11a488a..665df47 100644 --- a/src/server/mcp/project-auth.ts +++ b/src/server/mcp/project-auth.ts @@ -9,8 +9,8 @@ type ProjectScopedArgs = { projectId: string; }; -async function requireProjectAccess(_extra: ToolExtra, projectId: string) { - const { baseUrl, ...auth } = requireMcpToolAuthContext(_extra); +async function requireProjectAccess(extra: ToolExtra, projectId: string) { + const { baseUrl, ...auth } = requireMcpToolAuthContext(extra); // This lookup enforces that the project belongs to the authenticated org. await ProjectService.getProjectForOrganization( diff --git a/src/server/mcp/tools/dataforseo-research-tools.ts b/src/server/mcp/tools/dataforseo-research-tools.ts index 07b2c88..b2edafb 100644 --- a/src/server/mcp/tools/dataforseo-research-tools.ts +++ b/src/server/mcp/tools/dataforseo-research-tools.ts @@ -1,6 +1,5 @@ /* eslint-disable max-lines */ import { z } from "zod"; -import { AppError } from "@/server/lib/errors"; import { createDataforseoClient } from "@/server/lib/dataforseoClient"; import { buildProjectMeta } from "@/server/mcp/context"; import { mcpResponse } from "@/server/mcp/formatters"; @@ -85,7 +84,6 @@ const rankedTargetSchema = z "Use a domain without protocol/www or an absolute page URL.", ); -const looseRecordSchema = z.record(z.string(), z.unknown()); const getRankedKeywordsInputSchema = { projectId: projectIdSchema, target: rankedTargetSchema, @@ -180,18 +178,10 @@ type GetGoogleBusinessQuestionsArgs = z.infer< const QUESTIONS_ANSWERS_MIN_RADIUS = 200; const QUESTIONS_ANSWERS_MAX_RADIUS = 199999; -function resolveMarketLocationCode(market: Market | undefined): number { - const country = market?.country?.trim().toLowerCase(); - if ( - !country || - ["us", "usa", "united states", "united states of america"].includes(country) - ) { - return DEFAULT_LOCATION_CODE; - } - throw new AppError( - "VALIDATION_ERROR", - "Only United States country targeting is supported by this MCP tool today.", - ); +function resolveMarketLocationCode(_market: Market | undefined): number { + // The Zod enum on market.country already restricts values to United States + // variants, so no other country can reach this code path. + return DEFAULT_LOCATION_CODE; } function formatCoordinate(value: number): string { @@ -263,9 +253,12 @@ function buildRankedKeywordFilters(args: { return filters.length > 0 ? filters : undefined; } +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + function asRecord(value: unknown): Record | undefined { - const parsed = looseRecordSchema.safeParse(value); - return parsed.success ? parsed.data : undefined; + return isRecord(value) ? value : undefined; } function displayValue(value: unknown): string { diff --git a/src/server/mcp/tools/list-projects.ts b/src/server/mcp/tools/list-projects.ts index e187a0e..f407959 100644 --- a/src/server/mcp/tools/list-projects.ts +++ b/src/server/mcp/tools/list-projects.ts @@ -1,6 +1,9 @@ import { ProjectService } from "@/server/features/projects/services/ProjectService"; import { mcpResponse } from "@/server/mcp/formatters"; -import { getAuth, getBaseUrl, type ToolExtra } from "@/server/mcp/context"; +import { + requireMcpToolAuthContext, + type ToolExtra, +} from "@/server/mcp/context"; import { optionalMetaOutputSchema } from "@/server/mcp/output-schemas"; import { buildDashboardUrl } from "@/server/mcp/urls"; import { z } from "zod"; @@ -32,8 +35,7 @@ export const listProjectsTool = { }, }, handler: async (_args: Record, extra: ToolExtra) => { - const auth = getAuth(extra); - const baseUrl = getBaseUrl(extra); + const { baseUrl, ...auth } = requireMcpToolAuthContext(extra); const projects = await ProjectService.listProjects(auth.organizationId); const lines = projects.length === 0 diff --git a/src/server/workflows/rankCheckPaths.ts b/src/server/workflows/rankCheckPaths.ts index 969c2d8..ab5a632 100644 --- a/src/server/workflows/rankCheckPaths.ts +++ b/src/server/workflows/rankCheckPaths.ts @@ -51,17 +51,16 @@ interface CheckContext { export async function runLiveCheck( step: WorkflowStep, ctx: CheckContext, -): Promise<{ totalFailed: number }> { +): Promise { const deviceList: Array<"desktop" | "mobile"> = ctx.devices === "both" ? ["desktop", "mobile"] : [ctx.devices]; let checked = 0; - let totalFailed = 0; for (let i = 0; i < ctx.keywords.length; i += KEYWORDS_PER_BATCH) { const batch = ctx.keywords.slice(i, i + KEYWORDS_PER_BATCH); const batchIndex = Math.floor(i / KEYWORDS_PER_BATCH); - const batchResults = await step.do( + await step.do( `live-batch-${batchIndex}`, SINGLE_ATTEMPT_STEP_CONFIG, async () => { @@ -82,12 +81,10 @@ export async function runLiveCheck( ); const settled = await Promise.allSettled(promises); const results: RankCheckResultWithDevice[] = []; - let batchFailed = 0; for (const outcome of settled) { if (outcome.status === "fulfilled") { results.push(outcome.value); } else { - batchFailed++; console.error("Rank check call failed:", outcome.reason); } } @@ -101,16 +98,7 @@ export async function runLiveCheck( mapResultsToSnapshotRows(ctx.runId, results), ); } - return { batchFailed }; }, ); - - totalFailed += batchResults.batchFailed; } - - if (totalFailed > 0) { - console.warn(`Rank check completed with ${totalFailed} failed API call(s)`); - } - - return { totalFailed }; } diff --git a/web/src/components/feature-page.tsx b/web/src/components/feature-page.tsx index 6f88586..a991b41 100644 --- a/web/src/components/feature-page.tsx +++ b/web/src/components/feature-page.tsx @@ -20,7 +20,7 @@ export function FeaturePageTemplate({ page }: FeaturePageProps) { href="https://app.openseo.so/sign-up" className="inline-flex h-10 items-center justify-center rounded-md bg-neutral-900 px-5 text-sm font-medium text-white transition-colors hover:bg-neutral-800" > - {page.ctaLabel ?? "Try OpenSEO"} + Try OpenSEO @@ -142,7 +142,7 @@ export function FeaturePageTemplate({ page }: FeaturePageProps) { href="https://app.openseo.so/sign-up" className="inline-flex h-9 items-center justify-center rounded-md bg-neutral-900 px-4 text-sm font-medium text-white transition-colors hover:bg-neutral-800" > - {page.ctaLabel ?? "Try OpenSEO"} + Try OpenSEO
@@ -151,43 +151,19 @@ export function FeaturePageTemplate({ page }: FeaturePageProps) { } function FeatureImage({ page }: FeaturePageProps) { - if (page.imageSrc) { - return ( -
- {page.imageAlt} -
- {page.eyebrow} in OpenSEO. -
-
- ); - } - return (
-
-
-

- {page.imagePlaceholder} -

-

- Replace with final product screenshot or short demo asset. -

-
-
+ {page.imageAlt}
- Placeholder for final OpenSEO product imagery. + {page.eyebrow} in OpenSEO.
); diff --git a/web/src/lib/content.functions.ts b/web/src/lib/content.functions.ts index 9260993..ea9d0b7 100644 --- a/web/src/lib/content.functions.ts +++ b/web/src/lib/content.functions.ts @@ -28,8 +28,8 @@ export const getGuidePosts = createServerFn({ method: "GET" }).handler( }, ); -function getContentPost(source: typeof docsSource, slugs: string[]) { - const page = source.getPage(slugs); +function getContentPost(slugs: string[]) { + const page = docsSource.getPage(slugs); if (!page) throw notFound(); return { @@ -40,12 +40,12 @@ function getContentPost(source: typeof docsSource, slugs: string[]) { }; } -function getContentPosts(source: typeof docsSource) { +function getContentPosts() { const topLevelOrder = new Map([ ["mcp", 0], ["skills", 1], ]); - const pages = source.getPages(); + const pages = docsSource.getPages(); return pages .map((page: (typeof pages)[number]) => ({ @@ -69,10 +69,10 @@ function getContentPosts(source: typeof docsSource) { export const getDocsPost = createServerFn({ method: "GET" }) .inputValidator((slugs: string[]) => slugs) - .handler(async ({ data: slugs }) => getContentPost(docsSource, slugs)); + .handler(async ({ data: slugs }) => getContentPost(slugs)); export const getDocsPosts = createServerFn({ method: "GET" }).handler( - async () => getContentPosts(docsSource), + async () => getContentPosts(), ); export const getDocsPageTree = createServerFn({ method: "GET" }).handler( diff --git a/web/src/lib/feature-pages.ts b/web/src/lib/feature-pages.ts index 6da1d20..cb0190c 100644 --- a/web/src/lib/feature-pages.ts +++ b/web/src/lib/feature-pages.ts @@ -9,9 +9,7 @@ export type FeaturePage = { primaryKeyword: string; secondaryKeywords: string[]; imageAlt: string; - imagePlaceholder: string; - imageSrc?: string; - ctaLabel?: string; + imageSrc: string; workflows: Array<{ title: string; description: string; @@ -47,7 +45,6 @@ export const featurePages = { "keyword research tools", ], imageAlt: "OpenSEO keyword research dashboard", - imagePlaceholder: "Keyword research screenshot placeholder", imageSrc: "https://imagedelivery.net/ysLOa6bzFaM49Jxok-TAlw/d77077d0-cdf4-4523-0c41-56a7b4861300/public", workflows: [ @@ -123,7 +120,6 @@ export const featurePages = { "seo audit tools", ], imageAlt: "OpenSEO site audit report", - imagePlaceholder: "Site audit screenshot placeholder", imageSrc: "https://imagedelivery.net/ysLOa6bzFaM49Jxok-TAlw/53149e87-0027-4fa8-5d13-bcaab60c7100/public", workflows: [ @@ -196,7 +192,6 @@ export const featurePages = { "google backlink checker", ], imageAlt: "OpenSEO backlinks report", - imagePlaceholder: "Backlink checker screenshot placeholder", imageSrc: "https://imagedelivery.net/ysLOa6bzFaM49Jxok-TAlw/d97206ed-bd64-447c-2b9e-1b9f07c5ec00/public", workflows: [ @@ -272,7 +267,6 @@ export const featurePages = { "competitor analysis seo tool", ], imageAlt: "OpenSEO domain overview", - imagePlaceholder: "Domain overview screenshot placeholder", imageSrc: "https://imagedelivery.net/ysLOa6bzFaM49Jxok-TAlw/189e22b8-fdf8-46b4-198c-e912beef2300/public", workflows: [ @@ -348,7 +342,6 @@ export const featurePages = { "google rank tracker", ], imageAlt: "OpenSEO rank tracking table", - imagePlaceholder: "Rank tracking screenshot placeholder", imageSrc: "https://imagedelivery.net/ysLOa6bzFaM49Jxok-TAlw/4a0f8508-1527-46a8-c91c-086456f21c00/public", workflows: [ @@ -424,7 +417,6 @@ export const featurePages = { "keyword planning", ], imageAlt: "OpenSEO saved keywords list", - imagePlaceholder: "Saved keywords screenshot placeholder", imageSrc: "https://imagedelivery.net/ysLOa6bzFaM49Jxok-TAlw/8938a529-b443-4d4f-9869-c972f3cef900/public", workflows: [ @@ -497,7 +489,6 @@ export const featurePages = { "answer engine optimization", ], imageAlt: "OpenSEO AI brand visibility report", - imagePlaceholder: "AI brand visibility screenshot placeholder", imageSrc: "https://imagedelivery.net/ysLOa6bzFaM49Jxok-TAlw/cde3e4f8-079f-4890-cb17-371087107400/public", workflows: [ @@ -570,7 +561,6 @@ export const featurePages = { "answer engine optimization tool", ], imageAlt: "OpenSEO prompt explorer", - imagePlaceholder: "Prompt explorer screenshot placeholder", imageSrc: "https://imagedelivery.net/ysLOa6bzFaM49Jxok-TAlw/9f3d38f2-aa97-417c-ca74-ae378654d700/public", workflows: [ diff --git a/web/src/lib/seo.ts b/web/src/lib/seo.ts index c87c45d..4567e24 100644 --- a/web/src/lib/seo.ts +++ b/web/src/lib/seo.ts @@ -25,7 +25,6 @@ type BuildSeoParams = { description?: string; titleSuffix?: string; ogType?: "website" | "article"; - imagePath?: string; imageAlt?: string; }; @@ -35,12 +34,11 @@ export function buildPageSeo({ description, titleSuffix, ogType = "website", - imagePath = DEFAULT_SOCIAL_IMAGE_PATH, imageAlt = DEFAULT_SOCIAL_IMAGE_ALT, }: BuildSeoParams) { const fullTitle = titleSuffix ? `${title} - ${titleSuffix}` : title; const canonicalUrl = toCanonicalUrl(path); - const socialImageUrl = toCanonicalUrl(imagePath); + const socialImageUrl = toCanonicalUrl(DEFAULT_SOCIAL_IMAGE_PATH); return { meta: [