* 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
89 lines
3.3 KiB
TypeScript
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),
|
|
);
|
|
}
|
|
}
|