Skip to main content

Someone wanted to learn this too, so Grasp built them a personal learning path.

You can keep exploring this one or create your own

Formatting Replies with Markdown and HTML

Recommended resources

The freely available resources this lesson will direct you to.

Hello! In our previous lesson, we taught your bot to handle a wide array of message types, from text and photos to documents, using grammY's powerful filter query system. You learned how to receive and send files, making your bot much more versatile.

This lesson focuses on presentation. So far, our bot's replies have been plain text. To build the kind of "tech UIs" you're interested in, we need to control the formatting of our messages. Today, we'll explore how to send messages with bolding, italics, links, and even code blocks. We will cover the two formatting standards supported by Telegram—Markdown and HTML—and I'll show you the robust, type-safe grammY plugin that makes this process a breeze.

The Two parse_mode Options

The Telegram Bot API allows you to style your text by embedding special formatting characters or tags directly into the message string. To tell Telegram how to interpret these markers, you must specify a parse_mode when you send the message. The two modes are MarkdownV2 and HTML.

Let's look at the basic grammY documentation for this.

Sending and Receiving Messages - grammY

This section of the grammY basics guide introduces the parse_mode option and provides simple examples for both Markdown and HTML.

Read the main section Sending Message With Formatting, and then review the short subsections for <tf start="Markdown" end="parse_mode: "MarkdownV2"">Markdown and <tf start="HTML" end="parse_mode: "HTML"">HTML. Pay attention to the options object passed to the send message call.

As you've just read, the process involves passing a second argument to ctx.reply() (or ctx.api.sendMessage()) with a parse_mode property.

MarkdownV2: Powerful but tricky

MarkdownV2 supports a familiar syntax for formatting:

  • *bold text*
  • _italic text_
  • [link text](http://example.com)
  • `inline code`
  • pre-formatted block

However, its key drawback is that any of the characters _, *, [, ], (, ), ~, `, >, #, +, -, =, |, {, }, ., ! must be escaped with a backslash (\) if you want them to appear as literal characters. This is a very common source of bugs, as forgetting to escape a period in a URL, for instance, will break the entire message's formatting.

Here's an example of sending a MarkdownV2 message. Notice the escaped . and !:

bot.command("md", (ctx) => {
  ctx.reply(
    "This is *bold*, this is _italic_, and this is a [link to grammY](https://grammy.dev)\\. Don't forget to escape special characters\\!",
    { parse_mode: "MarkdownV2" },
  );
});

HTML: A More Familiar Choice

For a front-end developer, the HTML parse mode is often a more natural and robust choice. You use standard HTML tags to format your text. The supported tags are a small subset of HTML:

  • <b>bold</b>, <strong>bold</strong>
  • <i>italic</i>, <em>italic</em>
  • <u>underline</u>, <ins>underline</ins>
  • <s>strikethrough</s>, <strike>strikethrough</strike>, <del>strikethrough</del>
  • <a href="http://example.com">link</a>
  • <code>inline fixed-width code</code>
  • <pre>pre-formatted fixed-width code block</pre>

Crucially, with HTML mode, you only need to escape three characters: < becomes &lt;, > becomes &gt;, and & becomes &amp;. This is far more manageable than MarkdownV2's long list.

The <pre> tag is particularly powerful for creating the "tech UIs" you're aiming for. You can use it to display code or to create table-like layouts with monospaced fonts, ensuring that your spacing and alignment are preserved.

An example of a `<pre>` block used to display a snippet of JavaScript code within a Telegram message.
A more advanced use of the `<pre>` tag to create a neatly formatted, table-like view of data, perfect for reports or selection menus.

Here's how you'd send an HTML-formatted message:

bot.command("html", (ctx) => {
  ctx.reply(
    'This is <b>bold</b>, this is <i>italic</i>, and this is a <a href="https://grammy.dev">link to grammY</a>. Here is some <code>inline code</code>.',
    { parse_mode: "HTML" },
  );
});

The parse-mode Plugin: A Superior Abstraction

While using the parse_mode option directly works, it feels very low-level. You have to remember to add the option to every call, and you're still manually constructing strings, which is error-prone. This is a solved problem in front-end development, where libraries like React abstract away direct DOM manipulation. grammY offers a similar high-level abstraction with the @grammyjs/parse-mode plugin.

This plugin provides a type-safe, declarative API for building formatted messages. It handles the generation of the underlying entities array—Telegram's internal representation for formatted text—so you don't have to worry about parse_mode or escaping characters.

First, let's add it to the project:

bun add @grammyjs/parse-mode

Now, let's explore how to use it by reading its documentation.

Parse Mode Plugin (parse-mode) | grammY

This document covers the official grammY plugin for handling formatted text. It introduces the core concepts and provides examples for its two main APIs.

Please read through the following sections: Introduction: Understand the plugin's purpose. Two Approaches: Learn about the tagged template and class-based methods. Usage (using fmt): See the tagged template literal in action. This syntax may remind you of CSS-in-JS libraries. Usage (using FormattedString): Review the builder pattern approach. <tf start="Core Concepts" end="This is my response");">Core Concepts: This is the most important part. Understand how the plugin creates a unified, composable system for formatted text.

Implementing with the Plugin

Let's put what you've read into practice. The plugin offers two main ways to build formatted strings.

1. The fmt Tagged Template

This is often the most readable approach. It uses a tagged template literal, which is a standard JavaScript feature.

First, import the necessary functions from the plugin:

import { fmt, bold, italic, link } from "@grammyjs/parse-mode";

Now, let's create a new command that generates a formatted status report.

bot.command("status", async (ctx) => {
  const statusText = fmt(
    `${bold("Bot Status Report")}\n\n` +
    `Uptime: ${italic("42h 13m")}\n` +
    `CPU Load: <code>15%</code>\n` +
    `Memory: <code>256/1024 MB</code>\n\n` +
    `For more details, see the ${link("dashboard", "https://example.com")}.`
  );

  await ctx.reply(statusText);
});

Notice a few things here:

  • We're composing the final message with simple helper functions (bold, italic, link).
  • The fmt function returns an object that ctx.reply understands directly. We don't need to specify parse_mode or manually handle text and entities properties.
  • We can mix formatted and plain strings seamlessly.

2. The FormattedString Builder

This approach uses a builder pattern, which can be very useful when you need to construct a message programmatically, perhaps inside loops or conditional branches.

Let's create the same status report using this method.

import { FormattedString } from "@grammyjs/parse-mode";

bot.command("status_builder", async (ctx) => {
  const statusText = new FormattedString("Bot Status Report").bold().text("\n\n")
    .plain("Uptime: ").italic("42h 13m").text("\n")
    .plain("CPU Load: ").code("15%").text("\n")
    .plain("Memory: ").code("256/1024 MB").text("\n\n")
    .plain("For more details, see the ").link("dashboard", "https://example.com").plain(".");

  await ctx.reply(statusText);
});

The end result is identical. The choice between fmt and FormattedString is largely a matter of style and the specific use case. The fmt template is often clearer for static or lightly dynamic text, while the builder pattern excels at complex, conditional text construction.

The true power of this plugin, as highlighted in the "Core Concepts" you read, is its composability. You can even pass an existing message from ctx.msg (which already has text and entities) into fmt and build upon its formatting. This demonstrates a level of API design that you, as a senior developer, can appreciate. It treats formatted text as a first-class, composable object.

Conclusion

You've now learned how to elevate your bot's communication from plain text to richly formatted messages. This is a fundamental skill for building bots that are not only functional but also user-friendly and aesthetically pleasing.

Key Takeaways:

  • Telegram supports text formatting via two modes: MarkdownV2 and HTML.
  • The HTML mode is generally more reliable and easier to work with, especially given your background, due to its simpler escaping rules.
  • The <pre> tag is invaluable for creating code blocks and table-like layouts.
  • The official @grammyjs/parse-mode plugin is the recommended, type-safe, and declarative way to handle message formatting.
  • The plugin provides two powerful APIs: the fmt tagged template literal for readability and the FormattedString class for programmatic construction.

Now that your bot can present information clearly, the next logical step is to allow users to interact with that information directly through buttons. In our next lesson, we will dive into creating custom reply keyboards and inline keyboards, turning your bot's messages from static displays into truly interactive UI components.

End of Module,Exit lesson

Can't find a good explanation? Sign up and we'll make it for you

Sign up