From 0eeb0f8ca33f38489ccb54584f5069c32fdabd62 Mon Sep 17 00:00:00 2001 From: Suren Hakobyan Date: Wed, 6 May 2026 12:52:16 +0400 Subject: [PATCH 1/3] fix(document): preserve rel attribute on links during parse and serialize Studio unconditionally strips the `rel` attribute from all `` elements both when parsing markdown into a document and when serializing back to markdown. This causes MDC attributes like `{rel="nofollow"}` to be lost through Studio editing round-trips. The stripping was originally added to remove auto-injected `nofollow` from the markdown parser, but it also removes intentional `rel` attributes set by content authors (e.g. `nofollow`, `noopener`, `sponsored`). Since the `preserveLinkAttributes` option was already added as a workaround for comparison logic, and the proper fix is to always preserve user-set attributes, this commit: 1. Removes the `rel` stripping in `generateDocumentFromMarkdownContent` 2. Removes the `rel` stripping in `generateContentFromMarkdownDocument` 3. Removes the now-unnecessary `preserveLinkAttributes` option from the type and call sites 4. Cleans up unused imports (`visit`, `MDCElement`, `Node`) --- src/app/src/types/content.ts | 1 - .../src/runtime/utils/document/compare.ts | 6 +++--- .../src/runtime/utils/document/generate.ts | 17 +---------------- 3 files changed, 4 insertions(+), 20 deletions(-) diff --git a/src/app/src/types/content.ts b/src/app/src/types/content.ts index 9d621a34..14606a9a 100644 --- a/src/app/src/types/content.ts +++ b/src/app/src/types/content.ts @@ -3,7 +3,6 @@ import type { CollectionType } from '@nuxt/content' export interface MarkdownParsingOptions { compress?: boolean collectionType?: CollectionType - preserveLinkAttributes?: boolean } export interface SyntaxHighlightTheme { diff --git a/src/module/src/runtime/utils/document/compare.ts b/src/module/src/runtime/utils/document/compare.ts index ff8f7fb1..e9f0f7a2 100644 --- a/src/module/src/runtime/utils/document/compare.ts +++ b/src/module/src/runtime/utils/document/compare.ts @@ -9,7 +9,7 @@ import { generateDocumentFromContent } from './generate' import { removeLastStylesFromTree } from './tree' export async function isDocumentMatchingContent(content: string, document: DatabaseItem): Promise { - const generatedDocument = await generateDocumentFromContent(document.id, content, { compress: true, preserveLinkAttributes: true }) as DatabaseItem + const generatedDocument = await generateDocumentFromContent(document.id, content, { compress: true }) as DatabaseItem if (generatedDocument.extension === ContentFileExtension.Markdown) { const { body: generatedBody, ...generatedDocumentData } = generatedDocument @@ -73,8 +73,8 @@ export function areDocumentsEqual(document1: Record, document2: Reflect.deleteProperty(doc, '__hash__') Reflect.deleteProperty(doc, 'path') - // default value of navigation is true - if (typeof doc.navigation === 'undefined') { + // default value of navigation is true; D1 may store it as string 'true' + if (typeof doc.navigation === 'undefined' || doc.navigation === 'true') { doc.navigation = true } diff --git a/src/module/src/runtime/utils/document/generate.ts b/src/module/src/runtime/utils/document/generate.ts index 2797eaa8..f9e4e819 100644 --- a/src/module/src/runtime/utils/document/generate.ts +++ b/src/module/src/runtime/utils/document/generate.ts @@ -1,12 +1,10 @@ import type { MarkdownRoot } from '@nuxt/content' -import type { MDCElement, MDCRoot } from '@nuxtjs/mdc' +import type { MDCRoot } from '@nuxtjs/mdc' import type { DatabaseItem, DatabasePageItem, MarkdownParsingOptions } from 'nuxt-studio/app' -import type { Node } from 'unist' import { consola } from 'consola' import { ContentFileExtension } from '../../types/content' import { parseMarkdown } from '@nuxtjs/mdc/runtime/parser/index' import { stringifyMarkdown } from '@nuxtjs/mdc/runtime' -import { visit } from 'unist-util-visit' import { compressTree, decompressTree } from '@nuxt/content/runtime' import destr from 'destr' import { parseFrontMatter, stringifyFrontMatter } from 'remark-mdc' @@ -109,13 +107,6 @@ export async function generateDocumentFromMarkdownContent(id: string, content: s }, }) - // Remove nofollow from links (skip when preserving attributes for comparison purposes) - if (!options.preserveLinkAttributes) { - visit(document.body, (node: unknown) => (node as MDCElement).type === 'element' && (node as MDCElement).tag === 'a', (node: unknown) => { - // TODO: handle rel custom properties - Reflect.deleteProperty((node as MDCElement).props!, 'rel') - }) - } let body = document.body as never as MarkdownRoot if (options.compress && document.body.type === 'root') { @@ -177,12 +168,6 @@ export async function generateContentFromMarkdownDocument(document: DatabaseItem // @ts-expect-error todo fix MarkdownRoot/MDCRoot conversion in MDC module const body = document.body!.type === 'minimark' ? decompressTree(document.body) : (document.body as MDCRoot) - // Remove nofollow from links - visit(body, (node: Node) => (node as MDCElement).type === 'element' && (node as MDCElement).tag === 'a', (node: Node) => { - // TODO: handle rel custom properties - Reflect.deleteProperty((node as MDCElement).props!, 'rel') - }) - const markdown = await stringifyMarkdown(body, cleanDataKeys(document), { frontMatter: { options: { From ecef7c5dd7d53d888e674609814675d5133e4724 Mon Sep 17 00:00:00 2001 From: Suren Hakobyan Date: Wed, 6 May 2026 13:12:55 +0400 Subject: [PATCH 2/3] style: remove extraneous blank line left from removed code block --- src/module/src/runtime/utils/document/generate.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/module/src/runtime/utils/document/generate.ts b/src/module/src/runtime/utils/document/generate.ts index f9e4e819..e2f6b3d0 100644 --- a/src/module/src/runtime/utils/document/generate.ts +++ b/src/module/src/runtime/utils/document/generate.ts @@ -107,7 +107,6 @@ export async function generateDocumentFromMarkdownContent(id: string, content: s }, }) - let body = document.body as never as MarkdownRoot if (options.compress && document.body.type === 'root') { body = compressTree(document.body) From 989da55f978127f77ac250e65e5ca0b037c8d528 Mon Sep 17 00:00:00 2001 From: Suren Hakobyan Date: Wed, 6 May 2026 13:23:21 +0400 Subject: [PATCH 3/3] fix: remove navigation coercion that belongs to a separate PR --- src/module/src/runtime/utils/document/compare.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/module/src/runtime/utils/document/compare.ts b/src/module/src/runtime/utils/document/compare.ts index e9f0f7a2..e95c4d48 100644 --- a/src/module/src/runtime/utils/document/compare.ts +++ b/src/module/src/runtime/utils/document/compare.ts @@ -73,8 +73,8 @@ export function areDocumentsEqual(document1: Record, document2: Reflect.deleteProperty(doc, '__hash__') Reflect.deleteProperty(doc, 'path') - // default value of navigation is true; D1 may store it as string 'true' - if (typeof doc.navigation === 'undefined' || doc.navigation === 'true') { + // default value of navigation is true + if (typeof doc.navigation === 'undefined') { doc.navigation = true }