Skip to content

Inconsistent HTML escaping of & in head — & in attributes vs & in text nodes #16657

@jsparkdev

Description

@jsparkdev

Astro Info

Astro                    v6.3.1
Node                     v24.15.0
System                   Linux (x64)
Package Manager          pnpm
Output                   static
Adapter                  none
Integrations             @astrojs/mdx
                         @astrojs/sitemap

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

All browsers

Describe the Bug

When rendering HTML <head> elements, the & character is escaped inconsistently depending on where it appears:

  • In text nodes (e.g. <title>{title}</title>), & is escaped to &amp; via the html-escaper library (escapeHTML).
  • In attribute values (e.g. <meta name="..." content={title} />), & is escaped to &#38; via the internal toAttributeString() function in packages/astro/src/runtime/server/render/util.ts.

Both &amp; and &#38; are equivalent per the HTML spec, but mixing them produces inconsistent raw HTML output. This is visible when inspecting the page source or fetching it via curl -sL.

Root cause:
The toAttributeString function uses a manual regex replacement (&&#38;, "&#34;) instead of delegating to the same escapeHTML function used for text nodes.

// packages/astro/src/runtime/server/render/util.ts
export const toAttributeString = (value: any, shouldEscape = true) =>
    shouldEscape
        ? String(value).replace(AMPERSAND_REGEX, '&#38;').replace(DOUBLE_QUOTE_REGEX, '&#34;')
        : value;

What's the expected result?

The & character should be consistently escaped as &amp; across both text nodes and attribute values in the rendered HTML output.

Suggested fix in packages/astro/src/runtime/server/render/util.ts:

export const toAttributeString = (value: any, shouldEscape = true) =>
    shouldEscape
        ? String(value).replace(AMPERSAND_REGEX, '&amp;').replace(DOUBLE_QUOTE_REGEX, '&quot;')
        : value;

Link to Minimal Reproducible Example

  1. Create any Astro page that passes a string containing & to both <title>{title}</title> and <meta content={title} />.
  2. Run curl -sL http://localhost:4321 and inspect the <head> output.
  3. Observe that <title> contains &amp; while <meta content> contains &#38;.

Participation

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    - P2: nice to haveNot breaking anything but nice to have (priority)- P3: minor bugAn edge case that only affects very specific usage (priority)feat: renderingRelated to prop serialization, html escaping, and framework components (scope)pkg: astroRelated to the core `astro` package (scope)

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions