app/assets/images + favicon_link_tag

Rails Favicon Generator

Rails gives you two homes for these files with genuinely different behaviour: the asset pipeline, which fingerprints them and needs a helper to build the URL, and public/, which serves them raw at the path browsers expect.

Drop an image
Drop a logo or image
PNG, JPG, WebP · square 512×512+ works best · non-square images are padded, never cropped · processed in your browser

Favicon Setup for Ruby on Rails — Free, Fast & Private

The split above is deliberate, not indecision. Assets under app/assets/images get a content digest in their filename, which is exactly what you want for cache headers set to a year — change the image and the URL changes with it. But /favicon.ico is a path browsers hit by convention with no tag to guide them, and a digested filename cannot satisfy a hardcoded request. So the .ico lives in public/, undigested, and everything else goes through the pipeline.

Where each generated file goes in Ruby on Rails

The generator above emits seven files with fixed names. Static assets in Ruby on Rails live in app/assets/images/ or public/, and the markup is written in app/views/layouts/application.html.erb. This is the mapping between the two.

Generated fileGoes toNotes
favicon.icopublic/favicon.icoMust be undigested at the root — browsers request this literal path.
favicon-16x16.pngapp/assets/images/favicon-16x16.pngServed fingerprinted via favicon_link_tag.
favicon-32x32.pngapp/assets/images/favicon-32x32.pngServed fingerprinted via favicon_link_tag.
favicon-48x48.pngapp/assets/images/favicon-48x48.pngOptional — the .ico carries this size.
apple-touch-icon.pngapp/assets/images/apple-touch-icon.pngFingerprinted; some older iOS versions probe the root, so a public/ copy is cheap insurance.
android-chrome-192x192.pngapp/assets/images/android-chrome-192x192.pngReferenced from the manifest via image_path.
android-chrome-512x512.pngapp/assets/images/android-chrome-512x512.pngReferenced from the manifest via image_path.

favicon_link_tag and what it defaults to

Rails ships a dedicated helper for this. favicon_link_tag takes a source, defaults it to "favicon.ico", resolves it through the asset pipeline, and emits a link element. Its default options are rel: "icon" and type: "image/x-icon", which are right for an .ico and wrong for a PNG — pass type: "image/png" explicitly for every PNG entry or you will declare a MIME type that does not match the bytes.

The other option worth passing is rel: "apple-touch-icon" for the 180px file, since the helper will otherwise mark it as a regular icon. Everything you pass beyond the recognised options becomes an HTML attribute, so sizes works the way you would hope.

The reason to use the helper rather than writing raw link tags is the asset host and digest handling that comes with it. Set config.asset_host to a CDN and every helper-generated URL picks it up with no further edits; hand-written paths do not. In a Rails app that already serves assets from a CDN, hardcoding /favicon-32x32.png quietly bypasses it.

app/views/layouts/application.html.erb

<head>
  <title><%= content_for(:title) || "Your App" %></title>
  <%= csrf_meta_tags %>
  <%= csp_meta_tag %>

  <%= favicon_link_tag "favicon-32x32.png", rel: "icon", type: "image/png", sizes: "32x32" %>
  <%= favicon_link_tag "favicon-16x16.png", rel: "icon", type: "image/png", sizes: "16x16" %>
  <%= favicon_link_tag "apple-touch-icon.png", rel: "apple-touch-icon", type: "image/png", sizes: "180x180" %>
  <link rel="icon" href="/favicon.ico" sizes="16x16 32x32 48x48">
  <%= tag.link rel: "manifest", href: site_manifest_path %>

  <%= stylesheet_link_tag :app %>
  <%= javascript_importmap_tags %>
</head>

Propshaft and Sprockets disagree about manifests

Rails 8 defaults to Propshaft, which indexes every file under the configured asset paths and needs no declaration step. Drop a PNG into app/assets/images and it is immediately resolvable. Sprockets, which Rails 7 and earlier used and which plenty of applications still run, works the opposite way: an asset must be reachable from app/assets/config/manifest.js before it will be compiled into public/assets in production.

The stock manifest.js has a link_tree line covering the images directory, so a fresh app is fine. Applications that have been through a few upgrades are frequently not — someone trimmed that file, and the result is a favicon that resolves perfectly in development, where Sprockets compiles on demand, and raises Sprockets::Rails::Helper::AssetNotPrecompiled in production. If your icons work locally and blow up after deploy, open that file first.

On either pipeline, remember that precompilation happens at deploy time. Adding an image and restarting the server is not enough in production; the assets:precompile task has to run.

app/assets/config/manifest.js — Sprockets only

//= link_tree ../images
//= link_directory ../stylesheets .css
//= link_tree ../../javascript .js

The manifest, and the tags Rails already generated

A modern Rails scaffold writes icon link tags into the generated layout for you, pointing at files it placed in public/. Those are placeholders. Delete them when you add your own, or you will ship two competing sets of icon declarations and the browser will pick whichever it likes — usually not yours.

For the web app manifest, an ERB view rendered by a small controller action lets you use image_path so the manifest points at the same digested URLs as everything else. Serve it as application/manifest+json, add a route, and reference it from the layout. A static file in public/ works too, but then the manifest icon URLs bypass the digest and the asset host, and drift out of sync with the rest of your assets on the next redeploy.

app/views/site/manifest.json.erb

{
  "name": "Your App",
  "short_name": "App",
  "start_url": "/",
  "display": "standalone",
  "icons": [
    { "src": "<%= image_path('android-chrome-192x192.png') %>",
      "sizes": "192x192", "type": "image/png" },
    { "src": "<%= image_path('android-chrome-512x512.png') %>",
      "sizes": "512x512", "type": "image/png", "purpose": "any maskable" }
  ]
}

Turbo, caching, and one layout to rule them all

Turbo Drive replaces the body between visits and merges the head rather than reloading the document, so the icon declarations from your layout persist across an entire browsing session. One edit to application.html.erb covers every page the layout wraps. Watch for applications with more than one layout — an admin namespace or a marketing layout is a common place for the icons to be quietly missing.

The digested URLs let you serve those PNGs with a one-year immutable cache header safely, since a new image means a new filename. public/favicon.ico is the exception with no digest to protect it, so give that path a shorter max-age. It is a small file requested constantly, and a week of caching costs nothing while a year of it means your rebrand is invisible to returning users for a very long time.

Whatever you paste, the artwork itself never leaves your machine. The set above is rendered with the Canvas API and the .ico container is assembled in JavaScript on your device, so an unreleased logo stays unreleased. If the 16px preview looks like mush, that is the honest signal to simplify the mark before it ships to a Ruby on Rails tab near you.

How it works

  1. Generate the set: Drop a square 512px logo into the tool above; the whole set renders in your browser.
  2. Split the files: favicon.ico goes in public/ so the bare root request resolves. The PNGs go in app/assets/images so the pipeline digests them.
  3. Add the helper calls: Use favicon_link_tag in application.html.erb, passing type: "image/png" for PNGs since the helper defaults to image/x-icon.
  4. Precompile and verify: Run assets:precompile, then view source in production and confirm the URLs are digested and the placeholder tags Rails generated are gone.

Frequently asked questions

Should the favicon go in public/ or app/assets in Rails?
Both, for different files. favicon.ico belongs in public/ undigested, because browsers request that literal root path with no tag to guide them and a fingerprinted filename cannot answer it. The PNGs belong in app/assets/images so the pipeline digests them and they can be cached immutably.
What does favicon_link_tag default to?
A source of "favicon.ico", rel: "icon" and type: "image/x-icon". Those defaults suit an .ico and are wrong for PNGs, so pass type: "image/png" explicitly on every PNG call, and rel: "apple-touch-icon" on the 180px one.
Why do I get AssetNotPrecompiled in production but not development?
That is Sprockets. It only compiles assets reachable from app/assets/config/manifest.js, and development compiles on demand so the gap never shows. Confirm that file still has its link_tree line for images. Propshaft, the Rails 8 default, has no such requirement.
Can I just hardcode the paths in the layout?
You can, and it works until you put a CDN in front of the app. favicon_link_tag routes through config.asset_host and the digest table; a hardcoded path bypasses both, so it silently serves from the origin and never gets a fresh URL when the image changes.
Rails already generated icon link tags for me.
Modern scaffolds write placeholder icon tags into the layout pointing at files in public/. Delete them when you add yours, otherwise the page ships two competing sets of declarations and the browser chooses.
How do I serve the web app manifest?
Render it from an ERB view through a small controller action using image_path for the icon URLs, and serve it as application/manifest+json. A static public/ file works but its icon URLs bypass the digest and asset host, so they drift out of sync after the next deploy.
Does Turbo affect the favicon?
Only helpfully. Turbo Drive merges the head between visits rather than reloading the document, so the declarations from your layout persist across the session. The thing to watch is a second layout — admin or marketing namespaces are where icons usually go missing.

All Image Tools

Solutions by use case