Ben Senescu 8405564925
feat: Add Backlinks page (#23)
* feat: add backlinks setup and overview workflow

* save

* fix backlinks timeseries and refine overview layout

* update backlinks table guidance

* refactor backlinks CI cleanup

* refactor backlinks page state colocation

* fix backlinks access error handling

Differentiate DataForSEO subscription access failures from billing issues and preserve exact page hosts for page-level backlink lookups.

* fix backlinks project scoping and error states

* simplify backlinks billing and lazy loading

* harden backlinks profiling and access flow

* refine backlinks access status typing

* update backlinks scope selection and access handling

* fix backlinks scope fallback and cache scoping

* fix backlinks ci checks

* docs: simplify backlinks pricing

* refine backlinks search: move scope toggle below input, remove redundant description

* refine backlinks summaries and help text

* refine backlinks table sorting

* fix backlinks invalid target handling

* delete docs

* fix backlinks scope inference and domain normalization

* docs: mark backlinks as a supported workflow

* fix backlinks links badge wrapping

* test: split backlinks test suites
2026-03-16 17:48:13 -04:00

89 lines
3.3 KiB
TypeScript

import { createServerFn } from "@tanstack/react-start";
import {
buildBacklinksDisabledAccessStatus,
setBacklinksAccessStatus,
} from "@/server/features/backlinks/backlinksAccess";
import { assertBacklinksProjectAccess } from "@/server/features/backlinks/backlinksProjectAccess";
import { authenticatedServerFunctionMiddleware } from "@/serverFunctions/middleware";
import { BacklinksService } from "@/server/features/backlinks/services/BacklinksService";
import { AppError } from "@/server/lib/errors";
import { backlinksOverviewInputSchema } from "@/types/schemas/backlinks";
export const getBacklinksOverview = createServerFn({ method: "POST" })
.middleware(authenticatedServerFunctionMiddleware)
.inputValidator((data: unknown) => backlinksOverviewInputSchema.parse(data))
.handler(async ({ data, context }) => {
await assertBacklinksProjectAccess(context.userId, data.projectId);
try {
return await BacklinksService.getOverview({
target: data.target,
scope: data.scope,
includeSubdomains: data.includeSubdomains,
includeIndirectLinks: data.includeIndirectLinks,
excludeInternalBacklinks: data.excludeInternalBacklinks,
status: data.status,
});
} catch (error) {
if (error instanceof AppError && error.code === "BACKLINKS_NOT_ENABLED") {
const checkedAt = new Date().toISOString();
await setBacklinksAccessStatus(
buildBacklinksDisabledAccessStatus(checkedAt, error.code),
);
}
throw error;
}
});
export const getBacklinksReferringDomains = createServerFn({ method: "POST" })
.middleware(authenticatedServerFunctionMiddleware)
.inputValidator((data: unknown) => backlinksOverviewInputSchema.parse(data))
.handler(async ({ data, context }) => {
await assertBacklinksProjectAccess(context.userId, data.projectId);
try {
return await BacklinksService.getReferringDomains({
target: data.target,
scope: data.scope,
includeSubdomains: data.includeSubdomains,
includeIndirectLinks: data.includeIndirectLinks,
excludeInternalBacklinks: data.excludeInternalBacklinks,
status: data.status,
});
} catch (error) {
await updateBacklinksAccessStatusOnError(error);
throw error;
}
});
export const getBacklinksTopPages = createServerFn({ method: "POST" })
.middleware(authenticatedServerFunctionMiddleware)
.inputValidator((data: unknown) => backlinksOverviewInputSchema.parse(data))
.handler(async ({ data, context }) => {
await assertBacklinksProjectAccess(context.userId, data.projectId);
try {
return await BacklinksService.getTopPages({
target: data.target,
scope: data.scope,
includeSubdomains: data.includeSubdomains,
includeIndirectLinks: data.includeIndirectLinks,
excludeInternalBacklinks: data.excludeInternalBacklinks,
status: data.status,
});
} catch (error) {
await updateBacklinksAccessStatusOnError(error);
throw error;
}
});
async function updateBacklinksAccessStatusOnError(error: unknown) {
if (error instanceof AppError && error.code === "BACKLINKS_NOT_ENABLED") {
const checkedAt = new Date().toISOString();
await setBacklinksAccessStatus(
buildBacklinksDisabledAccessStatus(checkedAt, error.code),
);
}
}