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 ###.
Gotcha 2: Markdown Links Generate Embed Previews
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.