Skip to content

Smartypants corrupts Unicode punctuation when rendering MDX content collections via render() on Windows #17555

Description

@HikeAndMap

Astro Info

Astro                    v7.1.6
Vite                     v8.1.5
Node                     v24.18.1
System                   Windows (x64)
Package Manager          npm
Output                   static
Adapter                  none
Integrations             @astrojs/mdx (v7.0.5)

If this issue only occurs in one browser, which browser is a problem?

Happens in FireFox and Chrome, didn't test others

Describe the Bug

Describe the bug

When rendering an MDX content collection entry using render() from astro:content, enabling markdown.smartypants causes typographic punctuation to be corrupted into mojibake on Windows.

For example:

  • Don’t becomes Don’
  • “Hello” becomes “Helloâ€�

This only occurs when rendering an entry from a content collection using render(). A direct .mdx page renders correctly.

Environment

  • Astro: 7.1.6
  • @astrojs/mdx: 7.0.5
  • @mdx-js/mdx: 3.1.1
  • Node.js: 24.18.1 (LTS)
  • npm
  • Windows 11

What I tested

I spent quite some time isolating the issue.

✅ Source file encoding

The .mdx file is saved as UTF-8.

I verified this by:

  • Saving the file explicitly as UTF-8.
  • Confirming the encoding in Notepad++.
  • Inspecting the file with Format-Hex.

The UTF-8 bytes are correct:

  • ASCII apostrophe (') → 27
  • Right single quotation mark () → E2 80 99

The source file itself is not corrupted.

✅ Content collection

getCollection() returns the correct content.

For example:

console.log(lesson.body);

prints:

I don’t like it

which proves the content is read correctly before rendering.

✅ Direct MDX pages

A page such as:

src/pages/direct.mdx

renders perfectly:

Don’t

“I don’t like it”

No corruption occurs.

❌ Rendering through astro:content

Rendering the same content using:

const { Content } = await render(lesson);

produces HTML containing:

Don’t

“I don’t like it�

Viewing the generated HTML source confirms the mojibake is already present before the browser renders the page.

✅ Disabling Smartypants fixes the problem

Changing:

markdown: {
  smartypants: false,
}

immediately fixes the issue.

The same content then renders correctly:

Don't

"I don't like it"

Summary

The issue appears to occur only when all of the following are true:

  • Rendering an MDX content collection entry using render() from astro:content
  • markdown.smartypants is enabled
  • Running on Windows

Direct .mdx pages do not exhibit this behavior, and the content returned by getCollection() is already correct before rendering.

This suggests the corruption occurs somewhere during the rendering pipeline rather than while reading the source file.

What's the expected result?

When rendering an MDX content collection entry using render() from astro:content, enabling markdown.smartypants should produce valid typographic punctuation without corrupting the output.

For example, the following Markdown:

Don't

Don’t

"I don't like it"

"I don’t like it"

should render as:

Don't

Don’t

“I don't like it”

“I don’t like it”

or, if Smartypants is expected to convert straight quotes:

Don’t

Don’t

“I don’t like it”

“I don’t like it”

In either case, the rendered HTML should contain valid Unicode punctuation (, , ) and should never contain mojibake such as ’, “, or �.

Link to Minimal Reproducible Example

I will create that later - right now not in a position to do

Participation

  • I am willing to submit a pull request for this issue.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions