Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Referencing assets in CSS #22

Closed
ryanb opened this issue Sep 17, 2021 · 29 comments
Closed

Referencing assets in CSS #22

ryanb opened this issue Sep 17, 2021 · 29 comments

Comments

@ryanb
Copy link
Contributor

ryanb commented Sep 17, 2021

Is it possible to reference other assets such as images and fonts in CSS? In the asset pipeline you'd normally do image-url("image.png"), but from my understanding that is handled by sass-rails and requires an scss file.

Is there a recommended way for doing this through cssbundling-rails and the asset pipeline?

Thanks for your work on this gem!

@rmacklin
Copy link

rmacklin commented Sep 17, 2021

Are you using PostCSS to process your CSS? If so, can https://github.com/postcss/postcss-url solve your use case?
I don't have personal experience with that plugin (currently still using sass), but the docs seemed promising. Perhaps the generated PostCSS configuration could be set up with it.

@chloerei
Copy link

chloerei commented Sep 18, 2021

I try to add a postprocessor to replace asset path:

class AssetUrlProcessor
  def self.call(input)
    # don't know why, copy from other processor
    context = input[:environment].context_class.new(input)

    data = input[:data].gsub(/url\((.+?)\)/) do |match|
      path = context.asset_path($1)
      "url(#{path})"
    end

    { data: data }
  end
end

Sprockets.register_postprocessor 'text/css', AssetUrlProcessor

It works for me. Need an official solution.

@dhh
Copy link
Member

dhh commented Sep 18, 2021

Yes, we need an official solution to this. That asset url processor looks in the ballpark. Here's the one from sassy-rails: https://github.com/sass/sassc-rails/blob/master/lib/sassc/rails/functions.rb

@deepj
Copy link

deepj commented Sep 18, 2021

I have a bit different "ask" for official solution. But I guess still related to this. How to reference and "import" assets from npm packages (in my case font-awesome). Here is how to do it in Phoenix/esbuild/sass https://gist.github.com/ks2211/75af6cddc051f5e261a29fc25eed5789

It would be wonderful some official solution for Rails 7/cssbuilding-rails

@stevestmartin
Copy link

stevestmartin commented Sep 20, 2021

I try to add a postprocessor to replace asset path:

class AssetUrlProcessor
  def self.call(input)
    # don't know why, copy from other processor
    context = input[:environment].context_class.new(input)

    data = input[:data].gsub(/url\((.+?)\)/) do |match|
      path = context.asset_path($1)
      "url(#{path})"
    end

    { data: data }
  end
end

Sprockets.register_postprocessor 'text/css', AssetUrlProcessor

This works great, just ran into this.

@stevestmartin
Copy link

stevestmartin commented Sep 20, 2021

Updating to better handle asset URLs that contain quotes or if they use a data URI / full URL so that we do not attempt to overwrite with a wrong value.

Thanks for the starting point @chloerei

config/initializers/assets.rb

class AssetUrlProcessor
  def self.call(input)
    # don't know why, copy from other processor
    context = input[:environment].context_class.new(input)
    data = input[:data].gsub(/url\(["']?(.+?)["']?\)/i) do |match|
      asset = $1
      if asset && asset !~ /(data:|http)/i
        path = context.asset_path(asset)
        "url(#{path})"
      else
        match
      end
    end

    { data: data }
  end
end

Sprockets.register_postprocessor 'text/css', AssetUrlProcessor

@ryanb
Copy link
Contributor Author

ryanb commented Sep 20, 2021

Thanks for the feedback everyone. I ended up going with a post processor that uses asset-url() with optional quotes so that it doesn't conflict with other urls such as in node modules.

# config/initializers/asset_url_processor.rb
class AssetUrlProcessor
  def self.call(input)
    context = input[:environment].context_class.new(input)
    data = input[:data].gsub(/asset-url\(["']?(.+?)["']?\)/) do |_match|
      "url(#{context.asset_path($1)})"
    end
    {data: data}
  end
end

Sprockets.register_postprocessor "text/css", AssetUrlProcessor

Keep in mind this only handles assets that are managed by sprockets. If we have other external assets that need to be included (such as within node modules), we could use postcss-url as @rmacklin mentioned. I'd probably configure it to output directly to public/assets so that it bypasses sprockets.

@stevestmartin
Copy link

Looks like Propshaft might be the official solution to this, although it seems to use an asset-path CSS function which mimics sprockets and @ryanb 's solution. However looking at @chloerei 's solution with minor improvements, I do like the idea that you could take a theme or library etc.. drop its assets into your assets_path and it just fixes everything without having to edit the CSS files to insert the asset-path calls, inevitably you always miss one.

This would make drop in updates of themes, libraries, or icon fonts much simpler by just copying files into folders under config.assets.paths I would assume we could just add path's from node_modules here as well.

@ryanb
Copy link
Contributor Author

ryanb commented Sep 20, 2021

@stevestmartin thanks for pointing out Propshaft. I replied to your issue there: rails/propshaft#1

@Petercopter
Copy link

I think I'm running into this problem with FontAwesome. I've got a Rails 6.x app, and I switched from Webpacker to cssbundling-rails and jsbundling-rails. Everything is working great locally as far as I can tell. Compilation is SUPER fast, and there's a LOT less JavaScript involved in the entire process.

I pushed to Heroku, and it seems the font URLs aren't being rewritten. Not a problem, this application isn't live yet, so I get to play with the new and shiny with limited consequences.

Will Propshaft be the right solution for Rails 6.x apps? I see it's locked to Rails 7 right now.

@ryanb
Copy link
Contributor Author

ryanb commented Oct 1, 2021

@Petercopter if the fonts are working in development but not production, they might not be included in the precompile. Try running rails assets:precompile and see if they are in the public/assets directory. If not, add them to the app/assets/config/manifest.js file similar to the images.

If they are included, you might need a post processor to remap the url, similar to the ones mentioned earlier.

@jcoyne
Copy link

jcoyne commented Oct 14, 2021

I'm also running cssbundling-rails on Rails 6.1 and I'm unable to make the Sprockets.register_postprocessor "text/css", AssetUrlProcessor solution work. Does anyone have a public repo with this working for them that they can share?

There is a flaw in the regex in @ryanb's solution. It should be /asset-url\(["']?(.+?)["']?\)/

@ryanb
Copy link
Contributor Author

ryanb commented Oct 15, 2021

@jcoyne thanks, I'll update it.

@brenogazzola
Copy link

brenogazzola commented Oct 15, 2021

You can also use propshaft's regex, which I've tested against all mdn uses cases: /url\(\s*["']?(?!(?:\#|data|http))([^"'\s)]+)\s*["']?\)/

It will ignore urls, anchors and data images, and will handle spaces or no " and '

@dhh
Copy link
Member

dhh commented Oct 18, 2021

Anyone want to take on bringing this to sprockets-rails proper? This should work out of the box with Rails 7 on both Sprockets and Propshaft (already works there).

@jcoyne
Copy link

jcoyne commented Oct 18, 2021

@dhh something like this: rails/sprockets-rails#476 ?

@dhh
Copy link
Member

dhh commented Nov 5, 2021

Solved via rails/sprockets-rails#476. Thanks @jcoyne 🙏

@MrHubble
Copy link

@Petercopter were you able to get font awesome deploying correctly to Heroku? If yes, what did you use in your manifest.js?

@Petercopter
Copy link

@MrHubble Yes! I added this to my manifest.js

//= link_tree ../../../node_modules/@fortawesome/fontawesome-free/webfonts

@itsyoshio
Copy link

MrHubble Yes! I added this to my manifest.js

//= link_tree ../../../node_modules/@fortawesome/fontawesome-free/webfonts

@Petercopter Not directly related to the topic at hand, but what did you do to get the webfonts in dev?
I tried working with postcss-url to rebase the path in the generated application.css, but it works in neither env.

@ianheggie
Copy link

MrHubble Yes! I added this to my manifest.js

//= link_tree ../../../node_modules/@fortawesome/fontawesome-free/webfonts

@Petercopter Not directly related to the topic at hand, but what did you do to get the webfonts in dev? I tried working with postcss-url to rebase the path in the generated application.css, but it works in neither env.

@itsyoshio - I added the following to config/initializers/assets.rb and that fixed up the dev side.
Rails.application.config.assets.paths << Rails.root.join("node_modules/@fortawesome/fontawesome-free/webfonts")

For context, this was in addition to

$fa-font-path: '/assets';
@import '@fortawesome/fontawesome-free/scss/fontawesome';
@import '@fortawesome/fontawesome-free/scss/brands';
@import '@fortawesome/fontawesome-free/scss/solid';
@import '@fortawesome/fontawesome-free/scss/regular';
@import '@fortawesome/fontawesome-free/scss/v4-shims';

My testing is with development and checking puiblic/assets after bin/rake tmp:clear assets:clobber assets:precompile

FYI, my package.json looks like:

{
  "name": "app",
  "private": "true",
  "dependencies": {
    "@fortawesome/fontawesome-free": "^6.1.0",
    "@hotwired/stimulus": "^3.0.1",
    "@hotwired/turbo-rails": "^7.1.1",
    "@popperjs/core": "^2.11.4",
    "bootstrap": "^5.1.3",
    "bootstrap-icons": "^1.8.1",
    "esbuild": "^0.14.27",
    "sass": "^1.49.9"
  },
  "scripts": {
    "build:css": "sass ./app/assets/stylesheets/application.bootstrap.scss ./app/assets/builds/application.css --no-source-map --load-path=node_modules",
    "build": "esbuild app/javascript/*.* --bundle --sourcemap --outdir=app/assets/builds"
  }
}

and my Procfile.dev has the following (I also have Procfile.watch that excludes web for running seperately whilst debugging under rubymine):

web: bin/rails server -p 3040 -b 0.0.0.0
js: yarn build --watch
css: yarn build:css --watch

@pezholio
Copy link

I'm still unclear as to how this works in practice. I have a file in app/assets/images, and I'm referencing it in my sass like so:

background: #fff url("image.png") repeat-x;

But my image is 404ing. Is there a step I'm missing here?

@GregSmith92
Copy link

@ianheggie Thanks for your set up help. Works great for me in development, but once pushed to Heroku I get errors in my logs like: ActionController::RoutingError (No route matches [GET] "/assets/fa-solid-900.ttf"): I actually get this for any 3rd party assets I try to load in production, other than scss files.

Any ideas on why this might be?

My set up is the same as yours, other than my css script:
"build:css": "sass ./app/assets/stylesheets/application.sass.scss ./app/assets/builds/application.css --no-source-map --load-path=node_modules"

@deepj
Copy link

deepj commented May 12, 2022

@GregSmith92

You might be in the same situation as I was. I see you're trying to use font-awesome.

In my case I use Propshaft, but the following wouldn't be different from Sprocket

config/initializers/assets.rb

Rails.application.config.assets.excluded_paths += [Rails.root.join('app/assets/stylesheets'), Rails.root.join('app/javascript')]

# Add additional assets to the asset load path.
Rails.application.config.assets.paths << Rails.root.join('node_modules/@fortawesome/fontawesome-free/webfonts')

application.css.scss

$fa-font-path: '';
@import '@fortawesome/fontawesome-free/scss/fontawesome';
@import '@fortawesome/fontawesome-free/scss/regular';
@import '@fortawesome/fontawesome-free/scss/solid';

@GregSmith92
Copy link

@deepj

Using $fa-font-path: ''; does not work for me in development.
Both $fa-font-path: '/assets/@fortawesome/fontawesome-free/webfonts'; and $fa-font-path: '/assets'; do work in development, but neither in production.

I don't understand why!! In both environments the assets are in the right place:

Dir.entries("public/assets/@fortawesome/fontawesome-free/webfonts")
=> 
["fa-solid-900-a0cc38b88839387e4451bb1ebdd9ecd821b2df0f7fcd5b26df75630f4171ee32.ttf.gz",
 "fa-brands-400-d6e9a2e4cad853a5a88711678db47de7456f36b30923f8350f55c41434809ba8.ttf.gz",
 "fa-v4compatibility-8351eb008a6fa9eca28be682c28af28e47d9d668e34128fb789072aa90d5c2e7.ttf",
 "fa-solid-900-addc97d14257b43232b89194f73bd3b862007d5eedcb4569362b8f26356d8db3.woff2",
 "fa-v4compatibility-8dc02461354e5f9713549bb76c84c6aa45a23c728534a8ce658a66a045f1cb76.woff2",
 "fa-brands-400-d6e9a2e4cad853a5a88711678db47de7456f36b30923f8350f55c41434809ba8.ttf",
 "fa-regular-400-79a39b18a5524be800ab200fbedebf7f737a316f01b874fe42b005fda10a6f11.ttf.gz",
 "fa-brands-400-e624f952dec1ac1e2673205a87513cab83c4a0d187b08528a5ff0c36d9b1e090.woff2",
 "fa-regular-400-6919b47939790fc2ab662fd09b22907c53f32d9a817eed0782e2fb3e7af24b5b.woff2",
 "..",
 "fa-v4compatibility-8351eb008a6fa9eca28be682c28af28e47d9d668e34128fb789072aa90d5c2e7.ttf.gz",
 "fa-regular-400-79a39b18a5524be800ab200fbedebf7f737a316f01b874fe42b005fda10a6f11.ttf",
 "fa-solid-900-a0cc38b88839387e4451bb1ebdd9ecd821b2df0f7fcd5b26df75630f4171ee32.ttf",
 "."]

@GregSmith92
Copy link

Incase someone stumbles upon this, I removed all fontawesome bootstrap refrences from application.scss, manifest.js and config/assets.rb and instead added import "@fortawesome/fontawesome-free/js/all" to my application.js and it now works fine in both environemnts. I'm sure there is a solution for just using scss but I couldn't find it.

@chloerei
Copy link

chloerei commented Jun 3, 2022

To fix fontawesome problem:

config/initializers/assets.rb

Rails.application.config.assets.paths << Rails.root.join('node_modules/@fortawesome/fontawesome-free/webfonts')

application.css.scss

$fa-font-path: ".";

@import "@fortawesome/fontawesome-free/scss/fontawesome.scss";
@import "@fortawesome/fontawesome-free/scss/solid.scss";

edwardloveall added a commit to edwardloveall/portfolio that referenced this issue Sep 1, 2022
Using esbuild to compile SCSS files works for the SCSS part, but not
for assets that are referenced inside the SCSS files. Images are
processed by the asset pipeline. The pipeline turns images like
`foo.png` into
`foo-138b348edbc780482d40f0abc5b57e487c0ecf24cfbde42d54007cfd8db0d7a4.pn
g`. SCSS files still refer to them as `url("foo.png")`, but the final
path to the image must include the hash in production. To make sure the
final CSS file can find these images, sprockets-rails [scans] all CSS
files using `AssetUrlProcessor`, which replaces `url("foo.png")` with
the hashed version.

Another complication was in a previous version of my asset pipeline
esbuild located assets relative to the CSS file the refernced them.
Sprockets doesn't look there. It [searches] any directory in
`app/assets`. All the asset urls in SCSS files needed to be changed to
be relative to those search paths instead, e.g. `../../images/foo.png`
to `foo.png`.

I have three base css files: application, blog, and admin. To support
them all I changed the `sass` command to look in
`./app/assets/stylesheets` and build to `./app/assets/builds` instead
of looking at each individual file ([sass reference]). It built the
`admin.css` file inside of an `admin/` directory, so the
`stylesheet_link_tag` had to change to reflect that.

Building asseets in development now either requires running the css,
js, _and_ rails server. You can use `bin/dev` to start all three, or
start them manually:

```
yarn build --watch
yarn build:css --watch
rails s
```

## Other cleanup

I wasn't able to get normalize.css imported from the node modules, but
I also removed it and it didn't seem to break anything. So I'm going to
keep it removed, as well as remove the `normalize-rails` gem.

Finally, I was able to remove the `esbuild-sass-plugin` node module
since I'm not using esbuild to bundle sass anymore.

[scans]: rails/cssbundling-rails#22
[searches]:
https://guides.rubyonrails.org/asset_pipeline.html#search-paths
[sass reference]:
https://sass-lang.com/documentation/cli/dart-sass#many-to-many-mode
edwardloveall added a commit to edwardloveall/portfolio that referenced this issue Sep 1, 2022
Using esbuild to compile SCSS files works for the SCSS part, but not
for assets that are referenced inside the SCSS files. Images are
processed by the asset pipeline. The pipeline turns images like
`foo.png` into
`foo-138b348edbc780482d40f0abc5b57e487c0ecf24cfbde42d54007cfd8db0d7a4.pn
g`. SCSS files still refer to them as `url("foo.png")`, but the final
path to the image must include the hash in production. To make sure the
final CSS file can find these images, sprockets-rails [scans] all CSS
files using `AssetUrlProcessor`, which replaces `url("foo.png")` with
the hashed version.

Another complication was in a previous version of my asset pipeline
esbuild located assets relative to the CSS file the refernced them.
Sprockets doesn't look there. It [searches] any directory in
`app/assets`. All the asset urls in SCSS files needed to be changed to
be relative to those search paths instead, e.g. `../../images/foo.png`
to `foo.png`.

I have three base css files: application, blog, and admin. To support
them all I changed the `sass` command to look in
`./app/assets/stylesheets` and build to `./app/assets/builds` instead
of looking at each individual file ([sass reference]). It built the
`admin.css` file inside of an `admin/` directory, so the
`stylesheet_link_tag` had to change to reflect that.

Building asseets in development now either requires running the css,
js, _and_ rails server. You can use `bin/dev` to start all three, or
start them manually:

```
yarn build --watch
yarn build:css --watch
rails s
```

## Other cleanup

I wasn't able to get normalize.css imported from the node modules, but
I also removed it and it didn't seem to break anything. So I'm going to
keep it removed, as well as remove the `normalize-rails` gem.

Finally, I was able to remove the `esbuild-sass-plugin` node module
since I'm not using esbuild to bundle sass anymore.

[scans]: rails/cssbundling-rails#22
[searches]:
https://guides.rubyonrails.org/asset_pipeline.html#search-paths
[sass reference]:
https://sass-lang.com/documentation/cli/dart-sass#many-to-many-mode
edwardloveall added a commit to edwardloveall/portfolio that referenced this issue Sep 1, 2022
Using esbuild to compile SCSS files works for the SCSS part, but not
for assets that are referenced inside the SCSS files. Images are
processed by the asset pipeline. The pipeline turns images like
`foo.png` into
`foo-138b348edbc780482d40f0abc5b57e487c0ecf24cfbde42d54007cfd8db0d7a4.pn
g`. SCSS files still refer to them as `url("foo.png")`, but the final
path to the image must include the hash in production. To make sure the
final CSS file can find these images, sprockets-rails [scans] all CSS
files using `AssetUrlProcessor`, which replaces `url("foo.png")` with
the hashed version.

Another complication was in a previous version of my asset pipeline
esbuild located assets relative to the CSS file the refernced them.
Sprockets doesn't look there. It [searches] any directory in
`app/assets`. All the asset urls in SCSS files needed to be changed to
be relative to those search paths instead, e.g. `../../images/foo.png`
to `foo.png`.

I have three base css files: application, blog, and admin. To support
them all I changed the `sass` command to look in
`./app/assets/stylesheets` and build to `./app/assets/builds` instead
of looking at each individual file ([sass reference]). It built the
`admin.css` file inside of an `admin/` directory, so the
`stylesheet_link_tag` had to change to reflect that.

Building asseets in development now either requires running the css,
js, _and_ rails server. You can use `bin/dev` to start all three, or
start them manually:

```
yarn build --watch
yarn build:css --watch
rails s
```

## Other cleanup

I wasn't able to get normalize.css imported from the node modules, but
I also removed it and it didn't seem to break anything. So I'm going to
keep it removed, as well as remove the `normalize-rails` gem.

Finally, I was able to remove the `esbuild-sass-plugin` node module
since I'm not using esbuild to bundle sass anymore.

[scans]: rails/cssbundling-rails#22
[searches]:
https://guides.rubyonrails.org/asset_pipeline.html#search-paths
[sass reference]:
https://sass-lang.com/documentation/cli/dart-sass#many-to-many-mode
edwardloveall added a commit to edwardloveall/portfolio that referenced this issue Sep 1, 2022
Using esbuild to compile SCSS files works for the SCSS part, but not
for assets that are referenced inside the SCSS files. Images are
processed by the asset pipeline. The pipeline turns images like
`foo.png` into
`foo-138b348edbc780482d40f0abc5b57e487c0ecf24cfbde42d54007cfd8db0d7a4.pn
g`. SCSS files still refer to them as `url("foo.png")`, but the final
path to the image must include the hash in production. To make sure the
final CSS file can find these images, sprockets-rails [scans] all CSS
files using `AssetUrlProcessor`, which replaces `url("foo.png")` with
the hashed version.

Another complication was in a previous version of my asset pipeline
esbuild located assets relative to the CSS file the refernced them.
Sprockets doesn't look there. It [searches] any directory in
`app/assets`. All the asset urls in SCSS files needed to be changed to
be relative to those search paths instead, e.g. `../../images/foo.png`
to `foo.png`.

I have three base css files: application, blog, and admin. To support
them all I changed the `sass` command to look in
`./app/assets/stylesheets` and build to `./app/assets/builds` instead
of looking at each individual file ([sass reference]). It built the
`admin.css` file inside of an `admin/` directory, so the
`stylesheet_link_tag` had to change to reflect that.

Building asseets in development now either requires running the css,
js, _and_ rails server. You can use `bin/dev` to start all three, or
start them manually:

```
yarn build --watch
yarn build:css --watch
rails s
```

[scans]: rails/cssbundling-rails#22
[searches]: https://guides.rubyonrails.org/asset_pipeline.html#search-paths
[sass reference]: https://sass-lang.com/documentation/cli/dart-sass#many-to-many-mode
@jasonfb
Copy link

jasonfb commented Feb 20, 2024

note: The solution that was brought into rails was to automatically pick up url references in your SCSS files. As of Rails 7.0/7.1 with the recent versions of this gem, you should no longer be using asset-path or asset-url in your SCSS files, instead, just use url and the built-in helpers will convert them to the pipeline paths.

note that if you seem to not getting your url markers in your CSS, triple check that the file name in app/assets/images/ matches what you have in your CSS.

@goulvench
Copy link

Thanks for the helpful hints above! I tried various combinations before finding something that works, so I'm sharing my setup when using FontAwesome as scss using cssbundling+propshaft:

app/assets/stylesheets/fontawesome.scss

$fa-font-path: ""; // or "."
@import "@fortawesome/fontawesome-free/scss/fontawesome";
@import "@fortawesome/fontawesome-free/scss/brands";
@import "@fortawesome/fontawesome-free/scss/solid";
@import "@fortawesome/fontawesome-free/scss/regular";

config/initializers/assets.rb

Rails.application.config.assets.version = "1.0"
Rails.application.config.assets.paths << Rails.root.join("node_modules/@fortawesome/fontawesome-free/webfonts")

I have no app/assets/config/manifest.js because it's only used by Sprockets, Propshaft doesn't care about this file.

package.json

"scripts": {
  "build": "esbuild app/javascript/*.* --bundle --sourcemap --format=esm --outdir=app/assets/builds --public-path=/assets",
  "build:css": "sass ./app/assets/stylesheets/:./app/assets/builds/ --no-source-map --load-path=node_modules"
}

I'm telling sass to compile the stylesheets folder (instead of specifying the stylesheet name) because I chose to generate application.css and fontawesome.css separately, since FontAwesome is bound to change much less often. The line is shorter and more flexible as well.

Gem versions:

rails: 7.1.3.2
css-bundling: 1.4.0
js-bundling: 1.3.0
propshaft: 0.8.0

If you want to see what is generated, run bin/rails assets:clobber && bin/rails assets:precompile and examine the contents of the public/assets/ folder.
When you're happy with the results, run bin/rails assets:clobber otherwise the compiled assets will take precedence and you won't see the CSS and JS changes you make.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests