Syntax Highlighting
Syntax highlighting lets a reader take in the shape of a program at a glance: strings in one color, comments in another, keywords in a third. But before anything else, one misconception has to be cleared away — syntax highlighting is not part of the Markdown specification. The spec only says that a fence may be followed by an info string, and that the first word of that string becomes a language- class name on the code element. What happens next is entirely up to the renderer. The same source file can look completely different on GitHub, in a documentation site, and in your own blog, and the only reason is which highlighter the renderer happens to use.
This page takes that apart. It explains the exact chain from the word you type to the color on the screen, gives you a table of the identifiers that are safe to use, describes what happens when the renderer has never heard of the language you named, and closes with the cases where the language label does more than you would expect — and the one place where the position of the code matters far more than its label.
To ask for highlighting, write the language name right after the opening backticks:
```python
def hello():
print("hi")
```
<pre><code class="language-python">def hello():
print("hi")
</code></pre>
Look at the rendered element and you will find the entire Markdown contribution to this page: one class attribute, class="language-python". The parser took the first word of the info string, prefixed it with language-, and attached the result to the code element. That is all it did. The parser has no idea what Python is, and it never looks at the contents of the block.
Everything else happens afterwards, in a component usually called a highlighter. The renderer receives class="language-python", looks that name up in a grammar registry, and if it finds one, rewrites the code text into a set of <span> elements with color classes. The three implementations you are most likely to meet are Shiki (used by VitePress and many modern documentation sites), Prism (used by many Jekyll and Gatsby themes), and highlight.js. Each ships its own registry of grammars, and their contents overlap heavily but are not identical.
This is why “I set the language and nothing happened” is such a common experience. The identifier only produces a class name; if no grammar in the registry answers to that class name, the class is still there but nobody acts on it, and the code is displayed as plain text.
Common Language Identifiers
The accepted values are defined by the highlighter, not by any standard, so the table below covers the names that nearly every registry recognizes:
| Content type | Common identifiers |
|---|---|
| JavaScript | js, javascript |
| TypeScript | ts, typescript |
| Shell scripts | bash, sh, shell |
| Python | python, py |
| JSON / YAML | json, yaml |
| HTML / CSS | html, css |
| Markdown itself | markdown, md |
| Plain text, highlighting off | text, plaintext |
The aliases matter because the registries genuinely disagree about them. Prism and Shiki both accept js for JavaScript, but Shiki’s alias set is not identical to Prism’s, and a renderer that only loads a subset of grammars — many sites load just the languages their docs use — will happily reject a name that another site accepts. When you are unsure, the canonical name is the safer bet: javascript and python are the stable spellings, and it is the short aliases — js, py, md — that come and go between registries.
When a block genuinely has no syntax to speak of — a directory listing, a log excerpt, a terminal transcript — say so explicitly with text rather than leaving the info string empty. An empty info string is not a request for no highlighting; it is an absence of instruction, and some renderers will guess a default language and paint plain prose in whatever colors that grammar uses.
The fallback behavior is not standardized, but the range of outcomes is small, and all of them are harmless:
- The class name is still generated, but no grammar claims it, so the block renders as plain monospace text.
- The renderer falls back to a default grammar, which sometimes looks better and sometimes looks wrong — a directory listing colored as if it were JavaScript.
- A strict renderer raises an error at build time and refuses the page.
In every case the content is untouched. A wrong or missing identifier costs you colors, never the code. That asymmetry is worth remembering when you are unsure: it is always better to write text than to gamble on a name that might not exist.
Most renderers and documentation themes also offer a set of extras that are not part of Markdown, and they are driven by the extra words of the info string rather than by anything in the body:
- File names. Many themes read a
title="..."word and display it as a caption above the block. The language is still taken from the first word only. - Highlighted lines. A number of themes understand a range such as
{1,3-4}and emphasize those lines. The braces are part of the info string, so they never reach the page as text. - Diff blocks. A block labeled
diffis colored by the highlighter according to the leading-and+on each line. This is a grammar, not a Markdown feature.
- pnpm install
+ pnpm install --frozen-lockfile
Because all of these live in the info string, they are progressive enhancements. The same Markdown file moved to another site simply loses the caption, the highlighted lines, and the diff colors, and that is normal rather than broken.
MDX and the Difference Between a Label and a Location
Some publishing pipelines do not parse Markdown at all — they parse MDX, in which a Markdown document may also contain React-style components and JavaScript expressions. This changes the risk profile of a stray character in a surprising way, and it is worth being precise about it.
In MDX, a fenced code block is inert. Whatever you label it — jsx, html, mdx, anything — its contents are never parsed as markup or evaluated. The label is a note to the highlighter, never a switch that turns on parsing. The prose around the fence is the part that is parsed: a bare { there is read as a JavaScript expression, and a bare < is read as the start of a JSX element, which fails the build outright when the tag is never closed.
The practical rule is therefore the opposite of what the language label suggests. The label decides the colors; the location decides what the parser will try to do. Put anything that looks like markup inside a fence and it is safe, whatever you call it; put it in a paragraph and it is parsed, whatever you meant by it.
| Written as | What actually results | Why |
|---|---|---|
```python | class="language-python" | The identifier took effect and the highlighter took over |
```Python | class="language-Python" | Identifiers are usually case sensitive |
```js title="a.js" | Still class="language-js" | Only the first word is the language |
| No identifier at all | No class name | No language was requested, so none was recorded |
```text | class="language-text" | An explicit request for no highlighting |
Do not treat highlighting as part of the content. Highlighting is a gift from the renderer, and the specification does not promise it. Your readers may open the file in a plain text editor, read it in an email client, or paste it into a ticket system, and in all of those places it is just lines of monospace text. That is exactly why code must be delivered as text in a code block and never as a screenshot — the formatting is a bonus, the text is the product.
Rewrite the following plain text as Markdown. Requirements: represent the command line section and the JSON section as fenced code blocks with the correct language identifiers; and for the directory listing, which is not code in any language, use text to switch highlighting off explicitly.
First run pnpm install, then execute the following command:
pnpm build --filter @kuaiyizhi/kit-ui-astro
The output directory looks like this:
dist/
index.mjs
index.d.ts
Finally, check the scripts field in package.json:
{
"scripts": {
"build": "astro build"
}
}
Approach:
Give every section its own fenced code block, and choose the identifier from the kind of content rather than from the program that produced it. The command line is a shell script, so it takes bash; the JSON fragment is JSON, so it takes json; the directory listing has no syntax at all, so it takes text, which is more predictable than leaving the info string empty and letting the renderer guess.
Steps:
- Open and close each block with three backticks, putting the closing fence on a line of its own, and write the identifier immediately after the opening fence.
- Put
pnpm installandpnpm build --filter @kuaiyizhi/kit-ui-astroin onebashblock. They are two consecutive shell commands, so they belong together and the reader can run them in order. - Give the directory listing the identifier
text. Left empty, a renderer may assume a default language and colordist/andindex.mjsas if they were keywords. - Give the
package.jsonfragment the identifierjson, so the reader immediately sees thatscriptsholds abuildentry. - Check the result: the first block renders with
class="language-bash", the last withclass="language-json", and the directory listing withclass="language-text".
Answer:
First run `pnpm install`, then execute the following command:
```bash
pnpm build --filter @kuaiyizhi/kit-ui-astro
```
The output directory looks like this:
```text
dist/
index.mjs
index.d.ts
```
Finally, check the `scripts` field in `package.json`:
```json
{
"scripts": {
"build": "astro build"
}
}
```You should now be able to say that syntax highlighting is decided by the renderer rather than by the Markdown specification, to explain exactly where the language- class name comes from, to pick a safe identifier for any common file type, and to recognize that an unrecognized name costs you colors and nothing else. You should also be able to tell the difference between a label that the highlighter reads and an extension that the theme reads, and to understand why, in an MDX pipeline, the position of a fragment matters more than the language you call it. The next time you paste code into a document, you will reach for the identifier out of habit — and you will know that if the site renders it in flat gray, the code is still perfectly readable.
