Profile picture

Arkadiusz Kulpa

AI & ML Engineer

HomeAboutBlogChat
Profile picture

Arkadiusz Kulpa

AI & ML Engineer

HomeAboutBlogChat

Fixing the MDEditor Ghost Text and Adding a Media Gallery Download

2026-03-125 min read
cmsadmin-uitypescriptdevlog

Fixing the MDEditor Ghost Text and Adding a Media Gallery Download

The admin UI is a product too. Even though I am the only user, a buggy editing experience slows down content creation and erodes confidence in the tools. Two issues had been nagging me since the CMS build: a ghost text rendering bug in the markdown editor, and a missing download button in the media gallery.

The Ghost Text Bug

The symptom was strange. When editing a blog post, the text in the markdown editor appeared doubled — a faint, slightly offset copy of every character visible beneath the main text. The cursor position did not match where it appeared to be. Selecting text highlighted unexpected ranges. The whole editing experience felt broken.

How MDEditor Works Under the Hood

The @uiw/react-md-editor component uses a two-layer rendering approach:

  1. A <pre><code> layer — the visible, syntax-highlighted text. This is what you see and read.
  2. A <textarea> layer — an invisible textarea positioned exactly on top of the code layer. This captures your keystrokes and handles text input.

The textarea is made invisible using -webkit-text-fill-color: transparent. You type into it, but you see the formatted output from the <pre><code> layer below. It is a clever trick that gives you a rich editing experience with native textarea input handling.

The Root Cause

My PostForm.tsx component had a useEffect that forced colour values onto editor elements to match the site's dark/light theme. The selector was:

// The bug
container.querySelectorAll<HTMLElement>(
  '.w-md-editor-text-input, .w-md-editor-text-pre, .w-md-editor-text-pre > code'
).forEach(el => {
  el.style.setProperty('color', foregroundColor, 'important');
});

The problem is .w-md-editor-text-input — that is the invisible textarea. By setting color: #ffffff !important on it, I overrode the -webkit-text-fill-color: transparent that kept it invisible. Suddenly both layers were rendering visible text, creating the ghosting effect.

The Fix

Remove the textarea from the colour-forcing selector. Only apply colour to the visible layers:

// The fix
container.querySelectorAll<HTMLElement>(
  '.w-md-editor-text-pre, .w-md-editor-text-pre > code'
).forEach(el => {
  el.style.setProperty('color', foregroundColor, 'important');
});

One selector removed. The textarea goes back to being transparent. The ghost text disappears. The cursor aligns correctly. The editing experience is clean again.

The Lesson

When working with components that use layered rendering (editors, syntax highlighters, virtual scrollers), be extremely careful about applying global style overrides. The invisible layers are invisible for a reason. A well-intentioned theme adjustment can break the entire visual model.

Adding Media Gallery Downloads

The media gallery in the admin UI displayed uploaded images and PDFs with "Insert" and "Copy tag" buttons. But there was no way to download a file directly — if I wanted to check an image or share a PDF, I had to construct the URL manually or open the S3 console.

The Implementation

I added a "Download" button alongside the existing actions for each media item. The button uses the CDN URL (via getMediaUrl()) and opens the file in a new tab:

<button
  onClick={() => {
    const url = getMediaUrl(item.key);
    window.open(url, '_blank');
  }}
  className={styles.downloadBtn}
>
  Download
</button>

The button appears in the card actions for both images and PDFs. It is styled consistently with the existing "Insert" and "Copy tag" buttons — same size, same spacing, same hover behaviour. No visual disruption to the existing layout.

For images, the download opens the full-resolution file in a new tab (the browser's native "Save Image As" handles the actual download). For PDFs, it opens the PDF viewer in a new tab where the user can download via the browser's built-in PDF controls.

The Broader Point

These two fixes — the ghost text bug and the missing download button — are not glamorous features. They will never appear in a feature announcement or a product demo. But they represent something important about building software: the quality of the tools determines the quality of the output.

A broken editor means I hesitate to write. A missing download button means I waste time constructing URLs. These small frictions accumulate into reluctance, and reluctance means fewer blog posts and less content.

Fixing them took less than an hour combined. The return on that investment — in reduced friction and increased confidence — will pay off every time I open the admin UI.

What This Means for You

If you are reading this blog, the content was written in an editor that renders cleanly, with a cursor that goes where expected, using media that was managed through a gallery with proper upload, insert, and download capabilities. The admin experience is polished, which means I can focus on writing rather than fighting my tools.

Looking Back at the Journey

This is the eighth and final post in the development journey series for this portfolio site. From a blank Next.js project to an AI-powered portfolio with a full CMS, REST API, media pipeline, and polished admin UI — every feature was built from scratch, documented in plans, and implemented incrementally.

The series covered:

  1. The foundation — Next.js, TypeScript, AWS Amplify
  2. AI chat assistant — Bedrock, Pinecone, RAG
  3. Blog CMS — S3, DynamoDB, AppSync, admin UI
  4. REST API — Programmatic blog management for LLMs
  5. Responsive layout — Design tokens, fluid scaling
  6. Reading experience — TOC, scroll behaviour
  7. Media pipeline — S3 presigned URLs, CDN
  8. This post — Admin polish

Each post documents a real implementation decision, the skills required, and what it means for the end product. The portfolio is not just a showcase of projects — the portfolio itself is the project.


This post is part of a series documenting the development of arkadiuszkulpa.co.uk.

← Back to Blog