diff --git a/apps/server/src/collaboration/processors/history.processor.ts b/apps/server/src/collaboration/processors/history.processor.ts index d7e27f60..5374e745 100644 --- a/apps/server/src/collaboration/processors/history.processor.ts +++ b/apps/server/src/collaboration/processors/history.processor.ts @@ -11,6 +11,7 @@ import { import { extractMentions, extractPageMentions, + extractInternalLinkSlugIds, } from '../../common/helpers/prosemirror/utils'; import { PageHistoryRepo } from '@docmost/db/repos/page/page-history.repo'; import { PageRepo } from '@docmost/db/repos/page/page.repo'; @@ -77,12 +78,14 @@ export class HistoryProcessor extends WorkerHost implements OnModuleDestroy { const mentions = extractMentions(page.content); const pageMentions = extractPageMentions(mentions); + const internalLinkSlugIds = extractInternalLinkSlugIds(page.content); await this.generalQueue .add(QueueJob.PAGE_BACKLINKS, { pageId, workspaceId: page.workspaceId, mentions: pageMentions, + internalLinkSlugIds, } as IPageBacklinkJob) .catch((err) => { this.logger.error( diff --git a/apps/server/src/common/helpers/prosemirror/utils.ts b/apps/server/src/common/helpers/prosemirror/utils.ts index 7704306e..07b6828b 100644 --- a/apps/server/src/common/helpers/prosemirror/utils.ts +++ b/apps/server/src/common/helpers/prosemirror/utils.ts @@ -7,6 +7,10 @@ import { validate as isValidUUID } from 'uuid'; import { Transform } from '@tiptap/pm/transform'; import { TiptapTransformer } from '@hocuspocus/transformer'; import * as Y from 'yjs'; +import { + INTERNAL_LINK_REGEX, + extractPageSlugId, +} from '../../../integrations/export/utils'; export interface MentionNode { id: string; @@ -64,6 +68,27 @@ export function extractPageMentions(mentionList: MentionNode[]): MentionNode[] { return pageMentionList as MentionNode[]; } +export function extractInternalLinkSlugIds(prosemirrorJson: any): string[] { + const slugIds: string[] = []; + const doc = jsonToNode(prosemirrorJson); + + doc.descendants((node: Node) => { + for (const mark of node.marks) { + if (mark.type.name === 'link' && mark.attrs.internal && mark.attrs.href) { + const match = mark.attrs.href.match(INTERNAL_LINK_REGEX); + if (match) { + const slugId = extractPageSlugId(match[5]); + if (slugId && !slugIds.includes(slugId)) { + slugIds.push(slugId); + } + } + } + } + }); + + return slugIds; +} + export function extractUserMentionIdsFromJson(json: any): string[] { const userIds: string[] = []; diff --git a/apps/server/src/integrations/queue/constants/queue.interface.ts b/apps/server/src/integrations/queue/constants/queue.interface.ts index f0683a4e..58937419 100644 --- a/apps/server/src/integrations/queue/constants/queue.interface.ts +++ b/apps/server/src/integrations/queue/constants/queue.interface.ts @@ -4,6 +4,7 @@ export interface IPageBacklinkJob { pageId: string; workspaceId: string; mentions: MentionNode[]; + internalLinkSlugIds?: string[]; } export interface IAddPageWatchersJob { diff --git a/apps/server/src/integrations/queue/tasks/backlinks.task.ts b/apps/server/src/integrations/queue/tasks/backlinks.task.ts index 268adaae..9d707272 100644 --- a/apps/server/src/integrations/queue/tasks/backlinks.task.ts +++ b/apps/server/src/integrations/queue/tasks/backlinks.task.ts @@ -11,7 +11,7 @@ export async function processBacklinks( backlinkRepo: BacklinkRepo, data: IPageBacklinkJob, ): Promise { - const { pageId, mentions, workspaceId } = data; + const { pageId, mentions, workspaceId, internalLinkSlugIds = [] } = data; await executeTx(db, async (trx) => { const existingBacklinks = await trx @@ -20,7 +20,28 @@ export async function processBacklinks( .where('sourcePageId', '=', pageId) .execute(); - if (existingBacklinks.length === 0 && mentions.length === 0) { + const mentionTargetPageIds = mentions + .filter((mention) => mention.entityId !== pageId) + .map((mention) => mention.entityId); + + let resolvedLinkPageIds: string[] = []; + if (internalLinkSlugIds.length > 0) { + const resolvedPages = await trx + .selectFrom('pages') + .select('id') + .where('slugId', 'in', internalLinkSlugIds) + .where('workspaceId', '=', workspaceId) + .execute(); + resolvedLinkPageIds = resolvedPages + .map((p) => p.id) + .filter((id) => id !== pageId); + } + + const allTargetPageIds = [ + ...new Set([...mentionTargetPageIds, ...resolvedLinkPageIds]), + ]; + + if (existingBacklinks.length === 0 && allTargetPageIds.length === 0) { return; } @@ -28,16 +49,12 @@ export async function processBacklinks( (backlink) => backlink.targetPageId, ); - const targetPageIds = mentions - .filter((mention) => mention.entityId !== pageId) - .map((mention) => mention.entityId); - let validTargetPages = []; - if (targetPageIds.length > 0) { + if (allTargetPageIds.length > 0) { validTargetPages = await trx .selectFrom('pages') .select('id') - .where('id', 'in', targetPageIds) + .where('id', 'in', allTargetPageIds) .where('workspaceId', '=', workspaceId) .execute(); }