diff --git a/app/assets/javascripts/content_editor/services/markdown_serializer.js b/app/assets/javascripts/content_editor/services/markdown_serializer.js
index 6c8c562af8e460ca5b2a767633b3f3aaf3119504..d665f24bba16dea63e56670e62f8f0447287c20f 100644
--- a/app/assets/javascripts/content_editor/services/markdown_serializer.js
+++ b/app/assets/javascripts/content_editor/services/markdown_serializer.js
@@ -48,7 +48,6 @@ import Text from '../extensions/text';
 import Video from '../extensions/video';
 import WordBreak from '../extensions/word_break';
 import {
-  isPlainURL,
   renderCodeBlock,
   renderHardBreak,
   renderTable,
@@ -62,36 +61,29 @@ import {
   renderHTMLNode,
   renderContent,
   preserveUnchanged,
+  bold,
+  italic,
+  link,
+  code,
 } from './serialization_helpers';
 
 const defaultSerializerConfig = {
   marks: {
-    [Bold.name]: defaultMarkdownSerializer.marks.strong,
-    [Italic.name]: { open: '_', close: '_', mixable: true, expelEnclosingWhitespace: true },
-    [Code.name]: defaultMarkdownSerializer.marks.code,
+    [Bold.name]: bold,
+    [Italic.name]: italic,
+    [Code.name]: code,
     [Subscript.name]: { open: '<sub>', close: '</sub>', mixable: true },
     [Superscript.name]: { open: '<sup>', close: '</sup>', mixable: true },
     [InlineDiff.name]: {
       mixable: true,
-      open(state, mark) {
+      open(_, mark) {
         return mark.attrs.type === 'addition' ? '{+' : '{-';
       },
-      close(state, mark) {
+      close(_, mark) {
         return mark.attrs.type === 'addition' ? '+}' : '-}';
       },
     },
-    [Link.name]: {
-      open(state, mark, parent, index) {
-        return isPlainURL(mark, parent, index, 1) ? '<' : '[';
-      },
-      close(state, mark, parent, index) {
-        const href = mark.attrs.canonicalSrc || mark.attrs.href;
-
-        return isPlainURL(mark, parent, index, -1)
-          ? '>'
-          : `](${state.esc(href)}${mark.attrs.title ? ` ${state.quote(mark.attrs.title)}` : ''})`;
-      },
-    },
+    [Link.name]: link,
     [MathInline.name]: {
       open: (...args) => `$${defaultMarkdownSerializer.marks.code.open(...args)}`,
       close: (...args) => `${defaultMarkdownSerializer.marks.code.close(...args)}$`,
diff --git a/app/assets/javascripts/content_editor/services/serialization_helpers.js b/app/assets/javascripts/content_editor/services/serialization_helpers.js
index 99aaee8f312814fc374b7902cf8a7b81c1f8754d..089d30edec7e595d4e303b11017bede1f29c5405 100644
--- a/app/assets/javascripts/content_editor/services/serialization_helpers.js
+++ b/app/assets/javascripts/content_editor/services/serialization_helpers.js
@@ -363,3 +363,120 @@ export function preserveUnchanged(render) {
     }
   };
 }
+
+const generateBoldTags = (open = true) => {
+  return (_, mark) => {
+    const type = /^(\*\*|__|<strong|<b).*/.exec(mark.attrs.sourceMarkdown)?.[1];
+
+    switch (type) {
+      case '**':
+      case '__':
+        return type;
+      // eslint-disable-next-line @gitlab/require-i18n-strings
+      case '<strong':
+      case '<b':
+        return (open ? openTag : closeTag)(type.substring(1));
+      default:
+        return '**';
+    }
+  };
+};
+
+export const bold = {
+  open: generateBoldTags(),
+  close: generateBoldTags(false),
+  mixable: true,
+  expelEnclosingWhitespace: true,
+};
+
+const generateItalicTag = (open = true) => {
+  return (_, mark) => {
+    const type = /^(\*|_|<em|<i).*/.exec(mark.attrs.sourceMarkdown)?.[1];
+
+    switch (type) {
+      case '*':
+      case '_':
+        return type;
+      // eslint-disable-next-line @gitlab/require-i18n-strings
+      case '<em':
+      case '<i':
+        return (open ? openTag : closeTag)(type.substring(1));
+      default:
+        return '_';
+    }
+  };
+};
+
+export const italic = {
+  open: generateItalicTag(),
+  close: generateItalicTag(false),
+  mixable: true,
+  expelEnclosingWhitespace: true,
+};
+
+const generateCodeTag = (open = true) => {
+  return (_, mark) => {
+    const type = /^(`|<code).*/.exec(mark.attrs.sourceMarkdown)?.[1];
+
+    if (type === '<code') {
+      return (open ? openTag : closeTag)(type.substring(1));
+    }
+
+    return '`';
+  };
+};
+
+export const code = {
+  open: generateCodeTag(),
+  close: generateCodeTag(false),
+  mixable: true,
+  expelEnclosingWhitespace: true,
+};
+
+const LINK_HTML = 'linkHtml';
+const LINK_MARKDOWN = 'linkMarkdown';
+
+const linkType = (sourceMarkdown) => {
+  const expression = /^(\[|<a).*/.exec(sourceMarkdown)?.[1];
+
+  if (!expression || expression === '[') {
+    return LINK_MARKDOWN;
+  }
+
+  return LINK_HTML;
+};
+
+export const link = {
+  open(state, mark, parent, index) {
+    if (isPlainURL(mark, parent, index, 1)) {
+      return '<';
+    }
+
+    const { canonicalSrc, href, title, sourceMarkdown } = mark.attrs;
+
+    if (linkType(sourceMarkdown) === LINK_MARKDOWN) {
+      return '[';
+    }
+
+    const attrs = { href: state.esc(href || canonicalSrc) };
+
+    if (title) {
+      attrs.title = title;
+    }
+
+    return openTag('a', attrs);
+  },
+  close(state, mark, parent, index) {
+    if (isPlainURL(mark, parent, index, -1)) {
+      return '>';
+    }
+
+    const { canonicalSrc, href, title, sourceMarkdown } = mark.attrs;
+
+    if (linkType(sourceMarkdown) === LINK_HTML) {
+      return closeTag('a');
+    }
+
+    return `](${state.esc(canonicalSrc || href)}${title ? ` ${state.quote(title)}` : ''})`;
+  },
+};
diff --git a/spec/frontend/content_editor/services/markdown_serializer_spec.js b/spec/frontend/content_editor/services/markdown_serializer_spec.js
index 6fa42ddbd2d610661bc648d790c0836c058ff0f4..25b7483f234be39cf2cf3a0e57f36f90ee2650a2 100644
--- a/spec/frontend/content_editor/services/markdown_serializer_spec.js
+++ b/spec/frontend/content_editor/services/markdown_serializer_spec.js
@@ -24,6 +24,7 @@ import Link from '~/content_editor/extensions/link';
 import ListItem from '~/content_editor/extensions/list_item';
 import OrderedList from '~/content_editor/extensions/ordered_list';
 import Paragraph from '~/content_editor/extensions/paragraph';
+import Sourcemap from '~/content_editor/extensions/sourcemap';
 import Strike from '~/content_editor/extensions/strike';
 import Table from '~/content_editor/extensions/table';
 import TableCell from '~/content_editor/extensions/table_cell';
@@ -32,6 +33,7 @@ import TableRow from '~/content_editor/extensions/table_row';
 import TaskItem from '~/content_editor/extensions/task_item';
 import TaskList from '~/content_editor/extensions/task_list';
 import markdownSerializer from '~/content_editor/services/markdown_serializer';
+import remarkMarkdownDeserializer from '~/content_editor/services/remark_markdown_deserializer';
 import { createTestEditor, createDocBuilder } from '../test_utils';
 
 jest.mock('~/emoji');
@@ -63,6 +65,7 @@ const tiptapEditor = createTestEditor({
     Link,
     ListItem,
     OrderedList,
+    Sourcemap,
     Strike,
     Table,
     TableCell,
@@ -1158,4 +1161,42 @@ Oranges are orange [^1]
       `.trim(),
     );
   });
+
+  it.each`
+    mark        | content                                    | modifiedContent
+    ${'bold'}   | ${'**bold**'}                              | ${'**bold modified**'}
+    ${'bold'}   | ${'__bold__'}                              | ${'__bold modified__'}
+    ${'bold'}   | ${'<strong>bold</strong>'}                 | ${'<strong>bold modified</strong>'}
+    ${'bold'}   | ${'<b>bold</b>'}                           | ${'<b>bold modified</b>'}
+    ${'italic'} | ${'_italic_'}                              | ${'_italic modified_'}
+    ${'italic'} | ${'*italic*'}                              | ${'*italic modified*'}
+    ${'italic'} | ${'<em>italic</em>'}                       | ${'<em>italic modified</em>'}
+    ${'italic'} | ${'<i>italic</i>'}                         | ${'<i>italic modified</i>'}
+    ${'link'}   | ${'[gitlab](https://gitlab.com)'}          | ${'[gitlab modified](https://gitlab.com)'}
+    ${'link'}   | ${'<a href="https://gitlab.com">link</a>'} | ${'<a href="https://gitlab.com">link modified</a>'}
+    ${'code'}   | ${'`code`'}                                | ${'`code modified`'}
+    ${'code'}   | ${'<code>code</code>'}                     | ${'<code>code modified</code>'}
+  `(
+    'preserves original $mark syntax when sourceMarkdown is available',
+    async ({ content, modifiedContent }) => {
+      const { document } = await remarkMarkdownDeserializer().deserialize({
+        schema: tiptapEditor.schema,
+        content,
+      });
+
+      tiptapEditor
+        .chain()
+        .setContent(document.toJSON())
+        // changing the document ensures that block preservation doesn’t yield false positives
+        .insertContent(' modified')
+        .run();
+
+      const serialized = markdownSerializer({}).serialize({
+        pristineDoc: document,
+        doc: tiptapEditor.state.doc,
+      });
+
+      expect(serialized).toEqual(modifiedContent);
+    },
+  );
 });