obi-jam fix beginner 15 minutes

Discord Markdown Gotchas for Bot Messages

You'd think markdown is markdown. Discord's version has opinions.

The Problem

When @Obi started posting formatted morning briefs to Discord — calendar events, task lists, suggested priorities — the messages looked broken. Headings rendered as literal # characters, link previews cluttered the channel with embed cards, and occasionally a brief just vanished entirely with no error in the bot logs.

Three separate issues, all related to Discord’s markdown implementation diverging from standard markdown.

Gotcha 1: Headings Need a Blank Line Before Them

Standard markdown doesn’t require a blank line before a heading. Discord does.

Before — renders as plain text:

Today's tasks:\n## Priority Items\n- Fix the scheduler

What the user sees:

Today's tasks:
## Priority Items
- Fix the scheduler

The ## renders as literal text. No heading formatting.

After — renders as a heading:

Today's tasks:\n\n## Priority Items\n- Fix the scheduler

The fix is a double newline (\n\n) before any heading. This applies to #, ##, and ###.

When your bot sends a message containing markdown links like [Meeting notes](https://docs.google.com/...), Discord auto-generates an embed preview for each URL. A morning brief with three calendar event links suddenly has three large preview cards appended to it, pushing actual content off-screen.

The fix: suppress embeds after sending.

const msg = await channel.send(briefContent);
await msg.suppressEmbeds(true);

Call suppressEmbeds(true) on the message object returned by channel.send(). This removes the embed previews while keeping the clickable markdown links intact.

Gotcha 3: Messages Over 2000 Characters Get Rejected

Discord has a hard 2000-character limit per message. The API returns an error, but if your bot doesn’t handle it, the message just disappears. A detailed morning brief with calendar events, task lists, and suggested priorities easily exceeds 2000 characters.

The fix: split before sending.

async function sendMessage(channel, content) {
  const chunks = splitMessage(content, 2000);
  let lastMsg;
  for (const chunk of chunks) {
    lastMsg = await channel.send(chunk);
    await lastMsg.suppressEmbeds(true);
  }
  return lastMsg;
}

function splitMessage(text, maxLength) {
  if (text.length <= maxLength) return [text];

  const chunks = [];
  let remaining = text;

  while (remaining.length > 0) {
    if (remaining.length <= maxLength) {
      chunks.push(remaining);
      break;
    }

    // Find the last paragraph break before the limit
    let splitIndex = remaining.lastIndexOf('\n\n', maxLength);
    if (splitIndex === -1) {
      // Fall back to last newline
      splitIndex = remaining.lastIndexOf('\n', maxLength);
    }
    if (splitIndex === -1) {
      // Hard split as last resort
      splitIndex = maxLength;
    }

    chunks.push(remaining.slice(0, splitIndex));
    remaining = remaining.slice(splitIndex).trimStart();
  }

  return chunks;
}

Split on paragraph boundaries (\n\n) to keep sections intact. Fall back to single newlines, then hard-split as a last resort. Suppress embeds on every chunk.

The Combined Wrapper

One function that handles all three gotchas:

async function sendFormattedMessage(channel, content) {
  // Fix heading formatting: ensure blank line before headings
  const fixed = content.replace(/([^\n])\n(#{1,3} )/g, '$1\n\n$2');

  // Split and send
  const chunks = splitMessage(fixed, 2000);
  let lastMsg;
  for (const chunk of chunks) {
    lastMsg = await channel.send(chunk);
    await lastMsg.suppressEmbeds(true);
  }
  return lastMsg;
}

Key Takeaway

Discord markdown is not standard markdown. If your bot sends formatted messages — morning briefs, reports, status updates — wrap your send logic in a helper that enforces blank lines before headings, splits on 2000-char boundaries, and suppresses embed previews. You’ll hit all three of these within the first week of building a bot that sends anything more complex than plain text.

FAQ

Why are my Discord bot headings showing as plain text with a hash symbol?

Discord requires a blank line (\n\n) before any heading. If your heading immediately follows other text with just a single newline, Discord renders the # as a literal character instead of a heading. Add \n\n before every #, ##, or ### in your bot messages.

How do I stop link previews from cluttering my Discord bot messages?

After sending a message that contains markdown links, call msg.suppressEmbeds(true) on the returned message object. This removes the auto-generated embed previews (link unfurling) without affecting the message text itself. Do this for any bot message that contains URLs — morning briefs, reports, or status updates.

What happens when a Discord bot message exceeds 2000 characters?

Discord silently rejects the message — the API returns an error and nothing is posted. You need to split long messages before sending. Split on paragraph boundaries (double newlines) to keep formatting intact, and send each chunk as a separate message.