{"title":"Asset Pipeline","description":"Build and serve fingerprinted Amber V2 assets with exact file and deployment boundaries","section":"guides","version":"v2","path":"guides/assets","canonical_url":"https://amberframework.org/docs/v2/guides/assets","markdown_url":"https://amberframework.org/docs/v2/guides/assets.md","inherited":false,"content_markdown":"# Asset Pipeline\n\n> **Supported web path:** Amber `2.0.0-beta.5`, Amber CLI `2.0.6`, and\n> asset_pipeline `0.37.0` are release-gated together. A new CLI web application\n> already contains every file and command shown below.\n\nAsset Pipeline turns application-authored CSS, JavaScript, images, fonts, and\nother static files into one deterministic release artifact. It preserves each\nlogical path, adds a SHA-256 content fingerprint to the emitted filename, writes\nsubresource-integrity metadata, rewrites local CSS `url(...)` references, and\nrecords the result in `public/assets/manifest.json`.\n\nThe important boundary is build time. A production process must never compile\nassets on its first request or require a writable application directory.\n\n## Where the examples go\n\nComplete these steps from the application root, the directory containing\n`shard.yml`.\n\n**Reference file map:**\n\n```text\nmy_app/\n├── shard.yml                                      # dependency versions\n├── config/assets.cr                               # runtime manifest resolver\n├── scripts/build_assets.cr                        # create for an existing app\n├── app/assets/                                    # authored source; edit\n│   ├── stylesheets/app.css\n│   ├── javascript/app.js\n│   ├── images/amber-mark.svg\n│   ├── fonts/Manrope-Variable.woff2\n│   └── files/getting-started.pdf\n├── public/assets/                                 # generated; never hand-edit\n│   ├── manifest.json\n│   └── ...fingerprinted files...\n└── src/views/layouts/application.ecr              # edit\n```\n\n`app/assets/` belongs to source control. `public/assets/` is build output. Build\nand deploy the entire output directory together; a manifest from one build must\nnever be paired with files from another.\n\nEvery non-hidden regular file discovered below `app/assets/` is copied or\ncompiled and fingerprinted, including CSS; JavaScript and source maps; JSON, web\nmanifests, XML, text, HTML, and CSV; SVG, PNG, JPEG, GIF, WebP, AVIF, and icons;\nWOFF, WOFF2, TTF, OTF, and EOT fonts; PDF, ZIP, and WebAssembly; and common audio\nand video formats. An unknown extension is still fingerprinted and recorded as\n`application/octet-stream`. Dotfiles and files inside dot-directories are\nignored; symlinks and references may not escape the source root.\n\nCompressible text, JSON-family formats (including web manifests), XML, SVG, and\nWebAssembly also receive deterministic `.gz` companions. The manifest verifier\nchecks that each companion expands to the recorded bytes.\n\n## 1. Confirm the compiler dependency\n\n**File: `shard.yml` — generated apps already contain this entry. Add it under\nthe existing `dependencies:` key only when upgrading an older app.**\n\n```yaml\ndependencies:\n  asset_pipeline:\n    github: amberframework/asset_pipeline\n    version: 0.37.0\n```\n\nKeep the Amber, Grant, database-driver, and other existing entries. Do not add a\nsecond top-level `dependencies:` key.\n\n**Run from: the application root.**\n\n```bash\nshards install\n```\n\n## 2. Configure the runtime resolver\n\n**File: `config/assets.cr` — create this complete file.**\n\n```crystal\nAmber::Assets.configure(\n  manifest_path: \"public/assets/manifest.json\"\n)\n```\n\n**File: `scripts/build_assets.cr` — create this complete build wrapper for an\nexisting pre-2.0.5 application. New CLI applications use `amber assets`.**\n\n```crystal\nrequire \"asset_pipeline/static_assets\"\n\nmanifest = AssetPipeline::StaticAssets::Compiler.new(\n  source_root: Path[\"app/assets\"],\n  output_root: Path[\"public/assets\"],\n  public_path: \"/assets\"\n).build\nputs \"Built #{manifest.assets.size} assets\"\n```\n\n**Run from: the application root, before compiling or packaging the app.**\n\n```bash\ncrystal run scripts/build_assets.cr\n```\n\nThis command is the build boundary. Run it in development after authored assets\nchange and in every release build. It emits the fingerprinted tree and\n`public/assets/manifest.json`; it does not wait for an HTTP request.\n\nAmber CLI `2.0.6` exposes this compiler as `amber assets build` and adds\n`amber assets check` for strict manifest verification. Use those commands in a\ngenerated app. Keep the wrapper only when migrating an older app that cannot\nyet invoke the new CLI in its build environment.\n\n## 3. Add authored assets\n\n**File: `app/assets/stylesheets/app.css` — create or move the application\nstylesheet here.**\n\n```css\n@font-face {\n  font-family: \"Manrope\";\n  src: url(\"../fonts/Manrope-Variable.woff2\") format(\"woff2\");\n  font-display: swap;\n}\n\n.brand-mark {\n  background: url(\"../images/amber-mark.svg\") center / contain no-repeat;\n}\n```\n\n**Files referenced by that stylesheet — place the real bytes at these paths.**\n\n```text\napp/assets/fonts/Manrope-Variable.woff2\napp/assets/images/amber-mark.svg\n```\n\nThe compiler resolves local CSS URLs relative to the stylesheet, fingerprints\nthe referenced files, and writes their final public URLs into emitted CSS. A\nreference to a missing local file fails the build. External, absolute, fragment,\nand `data:` URLs pass through unchanged.\n\n**File: `app/assets/javascript/app.js` — move browser-ready ESM here.**\n\n```javascript\nconst menuButton = document.querySelector(\"[data-menu-button]\")\n\nmenuButton?.addEventListener(\"click\", () => {\n  const open = menuButton.getAttribute(\"aria-expanded\") !== \"true\"\n  menuButton.setAttribute(\"aria-expanded\", String(open))\n})\n```\n\nAsset Pipeline fingerprints browser-ready files; it is not a TypeScript, Sass,\nor JSX compiler. Keep a necessary upstream compiler as an earlier build stage\nand feed its reviewed browser output into `app/assets/`.\n\n## 4. Confirm the configuration load boundary\n\n**File: `src/my_app.cr` — the generated V2 entry point loads every top-level\nconfiguration file with this line. Keep it before controllers and models.**\n\n```crystal\nrequire \"../config/*\"\n```\n\nReplace `my_app` with the application's target name when locating the file. If a\nmigrated application does not load `config/*`, explicitly require\n`../config/assets` from its existing entry point after the file that requires\nAmber. Creating `config/assets.cr` without requiring it does nothing.\n\nAmber loads the manifest when an asset helper first resolves a logical path. A\nlogical path absent from the manifest raises an error instead of silently\nproducing a broken production URL. Absolute paths, external URLs, fragments,\nand `data:` URLs pass through.\n\n## 5. Use logical paths in the layout\n\n**File: `src/views/layouts/application.ecr` — replace literal asset URLs with\nmanifest-aware helpers.**\n\n```ecr\n<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n    <%= stylesheet_link_tag(\"stylesheets/app.css\") %>\n    <%= favicon_tag(\"images/amber-mark.svg\") %>\n    <%= javascript_importmap_tag(\n      {\"app\" => \"javascript/app.js\"},\n      preload: [\"javascript/app.js\"]\n    ) %>\n  </head>\n  <body>\n    <%= content %>\n    <script type=\"module\">import \"app\";</script>\n  </body>\n</html>\n```\n\nUse `asset_path(\"images/amber-mark.svg\")` when no semantic tag helper fits.\n`image_tag`, `stylesheet_link_tag`, `javascript_include_tag`, `favicon_tag`, and\n`javascript_importmap_tag` resolve logical paths through the same manifest.\n`asset_integrity(\"javascript/app.js\")` exposes the recorded SRI value when a\ncustom tag needs it. Stylesheet, script, and module-preload helpers add the\nmanifest's integrity value and anonymous CORS mode for logical assets unless the\ncaller explicitly supplies those attributes.\n\n**File: an ECR view, for example `src/views/home/index.ecr` — refer to the\nlogical image, not its generated digest.**\n\n```ecr\n<%= image_tag(\"images/amber-mark.svg\", alt: \"Amber Framework\") %>\n<a href=\"<%= asset_path(\"files/getting-started.pdf\") %>\">Download the guide</a>\n```\n\nNever paste a generated fingerprint into an ECR file. Source code stays stable;\nthe manifest changes when bytes change.\n\n## 6. Verify the build before launch\n\n**Run from: the application root.**\n\n```bash\namber assets build\namber assets check\ncrystal spec\ncrystal build src/my_app.cr -o bin/my_app\namber watch\n```\n\nFor an upgraded older app using the wrapper, replace the first two lines with\n`crystal run scripts/build_assets.cr` and a verification program as shown in\n[Configuration](configuration/).\n\nOpen a rendered page and verify all of these signals:\n\n1. `public/assets/manifest.json` exists and contains every logical asset used by\n   the page;\n2. HTML references fingerprinted `/assets/` URLs rather than query versions;\n3. emitted CSS references fingerprinted font and image URLs that return `200`;\n4. JavaScript, CSS, image, font, and download responses have correct content\n   types;\n5. editing a source file and rebuilding changes that file's URL; and\n6. the compiled application can run with its release directory read-only.\n\nDo not enable year-long immutable caching until the server or reverse proxy\napplies it only to fingerprinted output. HTML and `manifest.json` must remain\nrevalidatable so a deployment can point clients at the new release.\n\n## Authored assets are not uploads\n\nThe manifest is for files reviewed and shipped with the application. Files\nreceived from users at runtime have a separate security, persistence, privacy,\nand cache lifecycle. Keep uploads outside `app/assets/` and\n`public/assets/manifest.json`; use [Gemma storage](../uploads/storage/) or an\napplication-owned delivery path instead.\n\n## Next steps\n\n- [Configuration](configuration/) — compiler, manifest, and cache boundaries\n- [Import Maps](import-maps/) — map local ESM through the manifest\n- [Stimulus Integration](stimulus/) — optional controller organization\n- [Webpack migration](../../migration-guide/webpack-to-esm/) — migrate in\n  reviewable stages without deleting the working build too early"}