propshaft
Expert guidance for the Propshaft asset pipeline in Rails 8+. Use when working with assets, stylesheets, CSS organization, JavaScript assets, import maps, asset precompilation, fingerprinting, images, fonts, CDN setup, or migrating from Sprockets. Covers file structure, stylesheet_link_tag, image_tag, @layer ordering, @import, CSS custom properties, asset helpers, deployment, and common Propshaft pitfalls. Trigger on "propshaft", "asset pipeline", "assets", "stylesheet", "javascript assets", "import maps", "asset precompile", "CSS organization", "sprockets migration", "fingerprinting", "stylesheet_link_tag", "image_tag", "asset_path".
How do I install this agent skill?
npx skills add https://github.com/thinkoodle/rails-skills --skill propshaftIs this agent skill safe to install?
- Gen Agent Trust Hubpass
The skill provides expert guidance on the Rails Propshaft asset pipeline, including file structure, CSS organization using layers, and migration from Sprockets. It utilizes standard Rails command-line tools for asset management and does not contain any malicious patterns or safety violations.
- Socketpass
No alerts
- Snykpass
Risk: LOW · No issues
What does this agent skill do?
Rails Propshaft Asset Pipeline Expert
Manage assets in Rails 8+ applications using Propshaft — the modern, minimal asset pipeline that serves files directly without compilation or bundling.
When To Use This Skill
- Setting up or organizing assets in a Rails 8+ app
- Migrating from Sprockets to Propshaft
- Fixing broken asset paths, missing stylesheets, or fingerprinting issues
- Configuring CDN, import maps, or asset precompilation
- Organizing CSS/JS file structure with Propshaft conventions
Critical Mental Model
Propshaft is NOT Sprockets. Stop thinking in Sprockets patterns immediately:
| Sprockets (OLD — never use) | Propshaft (Rails 8 default) |
|---|---|
//= require directives | Not needed — all files auto-served |
//= require_tree . | Not needed — directory auto-included |
asset_path("image.png") in CSS | url("/image.png") in CSS |
image-url("bg.png") Sass helper | url("/bg.png") plain CSS |
manifest.js file | Not needed — no manifests |
| Sass/SCSS compilation | Plain CSS (or use cssbundling-rails) |
| Asset compilation step | No compilation — files served as-is |
config.assets.compile = true | Not applicable |
The #1 agent mistake: Using //= require or Sprockets-era helpers. Propshaft ignores these completely and they'll appear as literal text in your CSS/JS.
Philosophy
- Files are served directly — No compilation, no bundling, no transformation
- Fingerprinting is the core job — Propshaft digests files for cache busting
- CSS
@layercontrols cascade — Not file load order or manifest declarations - Browser-ready assets only — Propshaft expects CSS/JS the browser can consume
- Simplicity over power — Need bundling/transpilation? Add jsbundling-rails or cssbundling-rails
File Organization
Standard Directory Structure
app/assets/
├── stylesheets/
│ ├── application.css # Main entry — may just declare @layer order
│ ├── _global.css # Design tokens (CSS custom properties)
│ ├── reset.css # CSS reset
│ ├── base.css # Base element styles
│ ├── utilities.css # Utility classes
│ └── components/
│ ├── buttons.css
│ ├── cards.css
│ ├── forms.css
│ ├── alerts.css
│ └── navigation.css
├── images/
│ ├── logo.svg
│ └── icons/
│ ├── arrow.svg
│ └── check.svg
└── fonts/
├── inter-regular.woff2
└── inter-bold.woff2
Key Rules
- Every
.cssfile inapp/assets/stylesheets/is available to serve - Underscore prefix (
_global.css) is convention only — Propshaft doesn't treat it specially - No manifest file — Unlike Sprockets, no
manifest.jsorapplication.csswith//= require - Subdirectories work —
components/cards.cssis served ascomponents/cards.css
CSS Organization with @layer
Declare Layer Order (Critical)
In application.css or _global.css, declare the cascade order:
/* app/assets/stylesheets/application.css */
@layer reset, base, components, utilities;
This single line controls ALL cascade priority. Layers listed later win over earlier ones.
Wrap Each File in Its Layer
/* app/assets/stylesheets/reset.css */
@layer reset {
*, *::before, *::after { box-sizing: border-box; }
body { margin: 0; }
}
/* app/assets/stylesheets/components/cards.css */
@layer components {
.card {
padding: var(--space-4);
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
}
}
/* app/assets/stylesheets/utilities.css */
@layer utilities {
.hidden { display: none !important; }
.text-center { text-align: center; }
}
Why @layer Matters
Without @layer, CSS specificity depends on file load order, which Propshaft doesn't guarantee. With @layer, the declared order always wins regardless of which file loads first:
reset— lowest priority (CSS reset, box-sizing)base— element defaults (body, headings, links)components— UI components (cards, buttons, forms)utilities— highest priority (override anything)
Styles NOT in any @layer have the highest specificity — they beat all layers. Use this intentionally.
Design Tokens with CSS Custom Properties
/* app/assets/stylesheets/_global.css */
:root {
/* Colors */
--color-primary: #2563eb;
--color-surface: #ffffff;
--color-border: #e5e7eb;
--color-text: #111827;
--color-text-muted: #6b7280;
/* Spacing */
--space-1: 0.25rem;
--space-2: 0.5rem;
--space-4: 1rem;
--space-6: 1.5rem;
--space-8: 2rem;
/* Typography */
--text-sm: 0.875rem;
--text-base: 1rem;
--text-lg: 1.125rem;
--font-medium: 500;
--font-bold: 700;
/* Borders */
--radius-sm: 0.25rem;
--radius-md: 0.375rem;
--radius-lg: 0.5rem;
}
Dark mode with light-dark():
:root {
color-scheme: light dark;
--color-surface: light-dark(#ffffff, #1f2937);
--color-text: light-dark(#111827, #f9fafb);
}
Loading Stylesheets in Layouts
The :app Symbol (Recommended)
<%# Loads ALL stylesheets from app/assets/stylesheets/ %>
<%= stylesheet_link_tag :app, "data-turbo-track": "reload" %>
Loading Specific Files
<%# Load third-party CSS first, then app CSS %>
<%= stylesheet_link_tag "actiontext", "data-turbo-track": "reload" %>
<%= stylesheet_link_tag :app, "data-turbo-track": "reload" %>
NEVER Use :all
<%# BAD — loads engine CSS too (e.g., Bulma from mission_control-jobs) %>
<%= stylesheet_link_tag :all %>
<%# GOOD — only your app's stylesheets %>
<%= stylesheet_link_tag :app %>
Explicit File Order (When Needed)
<%= stylesheet_link_tag "reset", "data-turbo-track": "reload" %>
<%= stylesheet_link_tag "base", "data-turbo-track": "reload" %>
<%= stylesheet_link_tag "main", "data-turbo-track": "reload" %>
Asset References
In Views (ERB)
<%# Images %>
<%= image_tag "logo.svg", alt: "Company Logo" %>
<%= image_tag "icons/arrow.svg", alt: "Arrow", class: "icon" %>
<%# Stylesheets %>
<%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
<%# Favicon %>
<%= favicon_link_tag "favicon.ico" %>
<%# Generic asset path %>
<%= asset_path("document.pdf") %>
In CSS
/* Use root-relative paths — Propshaft rewrites these to digested URLs */
.hero {
background-image: url("/bg/pattern.svg");
}
.icon-check {
background-image: url("/icons/check.svg");
}
@font-face {
font-family: "Inter";
src: url("/fonts/inter-regular.woff2") format("woff2");
font-weight: 400;
}
Propshaft automatically rewrites url("/bg/pattern.svg") → url("/assets/bg/pattern-abc123.svg") during precompilation.
In JavaScript
Use the RAILS_ASSET_URL macro:
// Propshaft transforms this during precompilation
const trashIcon = RAILS_ASSET_URL("/icons/trash.svg");
// Becomes: "/assets/icons/trash-54g9cbef.svg"
JavaScript with Import Maps
Propshaft handles CSS/images/fonts. JavaScript is managed by importmap-rails (Rails 8 default):
app/javascript/
├── application.js # Entry point
└── controllers/ # Stimulus controllers
├── hello_controller.js
└── modal_controller.js
<%# In layout — this is importmap-rails, not Propshaft %>
<%= javascript_importmap_tags %>
# config/importmap.rb
pin "application"
pin "@hotwired/turbo-rails", to: "turbo.min.js"
pin "@hotwired/stimulus", to: "stimulus.min.js"
pin_all_from "app/javascript/controllers", under: "controllers"
Key distinction: Propshaft fingerprints JS files. Import maps resolve module names to URLs. They complement each other — Propshaft doesn't bundle or transform JS.
Images and Fonts
Images
Place in app/assets/images/. Reference with helpers:
<%= image_tag "logo.svg", alt: "Logo", width: 200 %>
<%= image_tag "icons/edit.svg", class: "icon", alt: "" %>
In CSS:
.logo { background-image: url("/logo.svg"); }
Fonts
Place in app/assets/fonts/. Declare with @font-face:
@font-face {
font-family: "Inter";
font-weight: 400;
font-style: normal;
font-display: swap;
src: url("/fonts/inter-regular.woff2") format("woff2");
}
@font-face {
font-family: "Inter";
font-weight: 700;
font-style: normal;
font-display: swap;
src: url("/fonts/inter-bold.woff2") format("woff2");
}
body {
font-family: "Inter", system-ui, sans-serif;
}
Deployment and Precompilation
Precompile Command
RAILS_ENV=production rails assets:precompile
Or without real secrets:
RAILS_ENV=production SECRET_KEY_BASE_DUMMY=1 rails assets:precompile
This:
- Copies all assets from load paths to
public/assets/ - Fingerprints filenames (e.g.,
application-a1b2c3.css) - Rewrites
url()references in CSS to digested paths - Generates
.manifest.jsonmapping original → digested filenames
CDN Configuration
# config/environments/production.rb
config.asset_host = ENV.fetch("CDN_HOST", nil)
# e.g., CDN_HOST=https://cdn.example.com
Cache Headers (Web Server)
Nginx:
location ~ ^/assets/ {
expires 1y;
add_header Cache-Control public;
add_header ETag "";
}
Apache:
<Location /assets/>
Header unset ETag
FileETag None
ExpiresActive On
ExpiresDefault "access plus 1 year"
</Location>
Production Cache-Control
# config/environments/production.rb
config.public_file_server.headers = {
"Cache-Control" => "public, max-age=31536000"
}
Adding New Stylesheets
-
Create the file:
app/assets/stylesheets/components/modal.css -
Wrap in appropriate
@layer:@layer components { .modal { /* ... */ } .modal-overlay { /* ... */ } } -
Done. Propshaft auto-includes it. No manifest to update, no require directive needed.
Third-Party / Engine CSS
For gems providing CSS (e.g., Action Text, Trix):
<%# Load third-party BEFORE :app so your styles can override %>
<%= stylesheet_link_tag "actiontext", "data-turbo-track": "reload" %>
<%= stylesheet_link_tag :app, "data-turbo-track": "reload" %>
Additional Asset Paths
# config/initializers/assets.rb
Rails.application.config.assets.paths << Rails.root.join("vendor/assets/stylesheets")
Rails.application.config.assets.paths << Emoji.images_path
Excluding Paths from Digestion
# config/initializers/assets.rb
# Useful when using cssbundling-rails with Sass source files
config.assets.excluded_paths = [Rails.root.join("app/assets/stylesheets")]
Debugging
# List all available assets
bin/rails assets:reveal
# In Rails console
Rails.application.assets.resolver.logical_paths.to_a
# Clear precompiled assets (fix stale development assets)
bin/rails assets:clobber
Common dev issue: If assets stop updating, you probably ran assets:precompile in development. The .manifest.json in public/assets/ tells Rails to use precompiled files. Fix with rails assets:clobber.
Common Mistakes
- Using
//= require— Propshaft ignores Sprockets directives entirely - Using
image-url()orasset-url()— These are Sass/Sprockets helpers. Use plainurl("/path") - Using
:allinstead of:app—:allloads engine CSS you don't want - Missing
@layerdeclarations — Without layers, cascade depends on unpredictable file load order - Expecting Sass compilation — Propshaft serves plain CSS. Use cssbundling-rails or dartsass-rails for Sass
- Precompiling in development — Creates
.manifest.jsonthat freezes assets. Runassets:clobber - Wrong URL format in CSS — Use
url("/icons/check.svg")noturl("icons/check.svg")(root-relative) - Confusing Propshaft and importmap-rails — Propshaft = CSS/images/fonts. Import maps = JavaScript modules
Migration from Sprockets
See reference.md in this skill directory for the complete migration checklist.
Quick summary:
bundle remove sprockets sprockets-rails sass-rails- Delete
config/assets.rbandassets/config/manifest.js bundle add propshaft(or upgrade to Rails 8 which includes it)- Replace
//= requirewith nothing — files auto-load - Replace
image-url()/asset-url()withurl("/path") - Replace
asset_path()in CSS withurl("/path") - Add
@layerdeclarations for cascade control - Convert Sass to plain CSS (or add dartsass-rails/cssbundling-rails)
How can the creator link this skill?
Add the canonical catalog link to the repository README so users can inspect current installs and available audits. The publishing guide covers the complete discovery path.
<a href="https://skillzs.dev/skills/thinkoodle/rails-skills/propshaft">View propshaft on skillZs</a>