# Asset Pipeline configuration

> **Supported web path:** This is the asset contract generated by Amber CLI
> `2.0.6` for Amber `2.0.0-beta.5` and asset_pipeline `0.37.0`.

Complete [Asset Pipeline](../) first. Every filesystem path below is resolved
from the application root, beside `shard.yml`.

## Keep build-time and runtime responsibilities separate

**File: `config/assets.cr` — this complete generated file configures only the
runtime manifest resolver.**

```crystal
Amber::Assets.configure(
  manifest_path: "public/assets/manifest.json"
)
```

Do not require the compiler from `config/assets.cr`. That file is loaded into
the running server, which only needs Amber's resolver. Keeping the compiler in
the CLI or a build script prevents release tooling from becoming an accidental
runtime dependency.

The three paths have different owners:

| Setting | Value | Owner |
|---|---|---|
| `source_root` | `app/assets` | source files developers edit |
| `output_root` | `public/assets` | generated release files |
| `public_path` | `/assets` | URLs emitted into the manifest |

Never point `source_root` and `output_root` at the same directory. Never store
uploads in either directory. The build is allowed to replace generated output;
it must not delete application source or runtime data.

Nested logical paths are preserved.

**Reference structure — authored source files:**

```text
app/assets/stylesheets/app.css
app/assets/javascript/controllers/menu.js
app/assets/images/marketing/hero.webp
app/assets/fonts/Manrope-Variable.woff2
```

remain distinct logical entries with the same relative paths, even though the
emitted filenames include content digests. Directory preservation prevents two
files such as `admin/logo.svg` and `store/logo.svg` from colliding.

**File: `scripts/build_assets.cr` — an existing pre-2.0.5 application can use
this complete build-only wrapper.**

```crystal
require "asset_pipeline/static_assets"

AssetPipeline::StaticAssets::Compiler.new(
  source_root: Path["app/assets"],
  output_root: Path["public/assets"],
  public_path: "/assets"
).build
```

## Configure Amber's resolver

**File: `src/my_app.cr` — verify the application entry point loads top-level
configuration before controllers and models.**

```crystal
require "../config/*"
```

Replace `my_app` with the target name. The generated V2 entry point uses that
wildcard, so `config/assets.cr` is loaded. If a migrated app has a narrower
require list, add `require "../config/assets"` explicitly after the
configuration file that loads Amber. Do not put the setup in the empty
`config/initializers/` directory unless the app explicitly requires it.

The resolver is strict for logical paths. A missing entry is a build or deploy
failure to fix, not a reason to fall back silently to an unhashed URL. External
URLs, absolute application paths, fragments, and `data:` URLs pass through.

## Inspect the manifest directly

Application views should normally use Amber's helpers. Build tooling can load
the same manifest directly when it needs structured metadata.

**File: a build verification program, for example
`scripts/verify_assets.cr` — create this complete file.**

```crystal
require "asset_pipeline/static_assets"

manifest = AssetPipeline::StaticAssets::Manifest.load(
  Path["public/assets/manifest.json"]
)
manifest.verify(Path["public/assets"])

puts manifest.path("stylesheets/app.css")
puts manifest.integrity("stylesheets/app.css")
entry = manifest.entry("fonts/Manrope-Variable.woff2")
puts "#{entry.content_type} #{entry.bytes} bytes"
```

Each entry records its public path, full SHA-256 digest, SRI value, content type,
and byte count. `verify` checks those values against the emitted bytes and
deterministic gzip companions. `path`, `integrity`, and `entry` are strict
lookups; a miss stops release verification.

**Run from: the application root, after building assets.**

```bash
crystal run scripts/verify_assets.cr
```

## CSS references

**File: `app/assets/stylesheets/app.css` — use paths relative to this source
stylesheet for local authored files.**

```css
@font-face {
  font-family: "Manrope";
  src: url("../fonts/Manrope-Variable.woff2") format("woff2");
  font-display: swap;
}

.hero {
  background-image: url("../images/marketing/hero.webp");
}
```

The build rewrites those local references to the fingerprinted public paths.
Keep an external URL, root-absolute URL, fragment, or data URL only when that is
deliberately outside the manifest. A missing relative file is an error.

Relative CSS `@import` references are rewritten too. Query strings and fragments
on a local reference are preserved after the fingerprinted path. The compiler
also rewrites relative static imports, exports, dynamic imports, and source-map
references in browser-ready JavaScript. Bare module names stay unchanged so an
import map can resolve them.

Asset Pipeline copies the bytes supplied to it. Generate real responsive image
sizes and formats in an earlier deterministic build step if the application
needs them, then put every emitted variant under `app/assets/images/` and list
the real logical paths in `srcset` or `<picture>`. Query parameters such as
`?w=640` or `?format=webp` do not create an image variant.

## Development workflow

Rebuild assets after an authored source file changes, then let the Amber watcher
reload application code.

**Run from: the application root.**

```bash
amber assets build
amber watch
```

`amber watch` already runs the same compiler before application compilation
when `app/assets/**/*` changes. Running `amber assets build` explicitly is
useful before the initial watcher start and when diagnosing output. The compiler
is never a first-request hook.

## Production workflow

**Run from: the application root — build before compiling or packaging the
application.**

```bash
shards install --production
amber assets build
amber assets check
crystal spec
shards build my_app --release
```

For an older app that uses `scripts/build_assets.cr`, run that file instead of
`amber assets build`, then run `scripts/verify_assets.cr`. Both paths invoke the
same asset_pipeline `0.37.0` manifest contract.

Package `bin/my_app`, `config/`, and the complete generated `public/assets/`
tree. Start the runtime with a read-only release directory. A writable
`public/assets/` path or a warm-up request must never be required.

The compiler writes files atomically, publishes `manifest.json` last, and after
a successful rebuild removes stale files owned by the previous manifest. It
does not delete unrelated files under `public/assets/`. Deployment still must
copy or switch the complete generated tree as one unit.

Deploy atomically: place a complete release in a new directory, verify it, then
switch traffic. Rollback switches back to the prior complete directory. Do not
copy new files over an old asset tree, and do not share a manifest between
releases.

## Cache boundary

**Reference response header — apply only to fingerprinted asset URLs:**

```text
Cache-Control: public, max-age=31536000, immutable
```

HTML and `public/assets/manifest.json` must revalidate or use a short cache so
clients can discover a new deployment. Unfingerprinted aliases must never be
cached as immutable. Configure compression in Amber's static handler or the
reverse proxy, and verify `Content-Type`, `Content-Encoding`, and `Vary` rather
than assuming a CDN corrected them.

## Release verification

Verify at least one CSS file, JavaScript module, image, font, and other binary:

1. build assets from a clean checkout;
2. load `manifest.json` and perform strict lookups;
3. start the compiled app with the release directory read-only;
4. request every emitted URL and check bytes and content type;
5. confirm fingerprinted responses receive immutable caching;
6. confirm HTML and the manifest do not;
7. edit each source class, rebuild, and confirm its URL changes; and
8. switch back to the prior complete release and confirm its URLs still work.