Skip to content

[Detail Bug] CSS relative asset URLs are not rewritten to absolute URLs when rewriteUrls is enabled #236

@detail-app

Description

@detail-app

Detail Bug Report

https://app.detail.dev/org_06887db3-bf54-40ab-976d-46c66ab2b840/bugs/bug_6f16bb34-8cbb-4a04-a83a-e4179f6d3839

Summary

  • Context: The replaceCssUrls function is responsible for converting relative CSS URLs to absolute URLs when rewriteUrls: true is enabled, ensuring that images and assets referenced in stylesheets work when the HTML is used in different contexts.
  • Bug: The function only converts root-relative URLs (starting with /) to absolute URLs, completely ignoring other types of relative URLs like images/bg.jpg, ./images/bg.jpg, and ../images/bg.jpg.
  • Actual vs. expected: When rewriteUrls: true is set, the README states that "CSS/HTML relatives URLs present in the HTML markup" will be rewritten to absolutes, but non-root-relative CSS URLs remain unchanged while HTML relative URLs are correctly converted.
  • Impact: CSS background images and other assets referenced with relative paths will break when the HTML is saved to disk, displayed in an iframe, or used in any context different from the original source URL.

Code with Bug

const replaceCssUrls = (url, stylesheet) => {
  const cssUrls = Array.from(execall(cssUrl(), stylesheet)).reduce(
    (acc, match) => {
      match.subMatches.forEach(match => acc.add(match))
      return acc
    },
    new Set()
  )

  cssUrls.forEach(cssUrl => {
    if (cssUrl.startsWith('/')) {  // <-- BUG 🔴 Only handles root-relative URLs, ignores other relative paths
      try {
        const absoluteUrl = new URL(cssUrl, url).toString()
        stylesheet = stylesheet.replaceAll(
          `url(${cssUrl})`,
          `url(${absoluteUrl})`
        )
      } catch (_) {}
    }
  })

  return stylesheet
}

Explanation

new URL(cssUrl, url) correctly resolves all relative URL forms (e.g., images/x.png, ./images/x.png, ../images/x.png, and /images/x.png) against a base URL. However, replaceCssUrls gates rewriting behind cssUrl.startsWith('/'), so only root-relative paths are rewritten; other relative paths are left unchanged, causing broken asset references when the HTML is used outside its original URL context.

Codebase Inconsistency

HTML attribute URL rewriting uses new URL(attr, url) without restricting to root-relative values, so HTML relative URLs are rewritten as documented, but CSS URLs are not:

const urlObj = new URL(attr, url)  // Handles ALL relative URLs correctly

Recommended Fix

Remove the cssUrl.startsWith('/') guard and resolve all CSS URLs via new URL(cssUrl, url), while skipping already-absolute http(s) URLs.

History

This bug was introduced in commit a9aecd6. The original change attempted to prevent CSS selectors and data URIs from being incorrectly rewritten as URLs, but the fix was overly restrictive: it changed from checking isRelativeUrl(cssUrl) to only checking cssUrl.startsWith('/'), inadvertently breaking support for images/..., ./..., and ../... CSS URL patterns.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions