Assets @ stwoo.net

Gohyde’s asset pipeline (internal/assets) walks the configured source directories, processes each file (Liquid, Sass, fingerprint), writes the result to the output directory, and records a manifest.json so Liquid tags and filters resolve to the final URL.

The pipeline runs before the page render pass, so asset_path / asset_url and the {% asset %} family always see the current digest in the same build.

Configuration

assets:
  digest: false            # content-hash fingerprinting (true for production)
  destination: assets      # output subdir under the site root
  sources:                 # directories scanned, in order
    - _assets
    - _assets/css
    - _assets/js
    - _assets/images
    - assets
  cdn:
    url: "https://cdn.example.com"   # absolute CDN base; overrides baseurl

URLs, baseurl, and CDN

Asset URLs include the site baseurl so sub-path deploys resolve correctly:

baseurl: "/routeway"
{{ "logo.svg" | asset_path }}      → /routeway/assets/logo.svg
{% asset "app.css" @path %}        → /routeway/assets/app.css

Precedence:

  1. If assets.cdn.url is set, the URL is <cdn.url>/assets/<file> (baseurl is not applied — the CDN URL is already absolute).
  2. Otherwise the URL is <baseurl>/<destination>/<file>.

Asset URLs carry only the baseurl, never site.url. For an absolute URL, prepend {{ site.url }} in the template:

<meta property="og:image"
      content="{{ site.url }}{% asset {{ site.mainlogo }} @path %}" />
<!-- → https://example.com/routeway/assets/logo.svg -->

Fingerprinting & the dev server

digest (default true) appends a content hash to asset filenames (style.css → style-a1b2c3d4.css) for cache-busting. It is forced off under gohyde serve regardless of config, so dev URLs stay stable across live reloads — no churn while you edit. gohyde build fingerprints as configured. Treat digests as a production concern.

Liquid filters

{{ "css/style.css" | asset_path }}   → /assets/css/style-a1b2c3d4.css   (when digest on)
{{ "css/style.css" | asset_url }}    → same (alias)
{{ "css/style.css" | asset_digest }} → a1b2c3d4

The manifest is written to <destination>/assets/manifest.json.

Asset tags

Four tags emit the right HTML for the asset type:

{% asset "app.css" %}
→ <link rel="stylesheet" href="/assets/app-a1b2c3d4.css">

{% asset "app.js" defer %}
→ <script src="/assets/app-a1b2c3d4.js" defer></script>

{% asset "logo.png" alt="Logo" class="brand" %}
→ <img src="/assets/logo-a1b2c3d4.png" width="200" height="80" alt="Logo" class="brand">

{% javascript "main" async %}      → <script src="/assets/js/main-….js" async></script>
{% stylesheet "main" media="print" %} → <link rel="stylesheet" href="/assets/css/main-….css" media="print">
{% image "hero.jpg" alt="Hero" %}  → <img src="/assets/images/hero-….jpg" …>

URL-only flags

Append any of @uri, @src, @href, @path, @url to get just the resolved URL instead of a full HTML tag:

<meta property="og:image" content="{{ site.url }}{% asset site.mainlogo @path %}" />
<div style="background:url({% asset 'bg.svg' @url %})"></div>

Dynamic asset names

The first argument can be quoted, a variable, or a {{ … }} expression:

{% asset "logo.svg" %}              ← string literal
{% asset item.cover %}              ← resolved against the loop/page variable
{% asset {{ site.mainlogo }} @path %}   ← config value via {{ … }}

Missing assets

A missing asset logs a warning to stderr and renders an empty string. It never injects an HTML comment like <!-- asset error --> into the output, and it never hard-fails the build:

  Warning: asset "missing.svg" not found

Source file processing

The processor is keyed by the file’s source extension:

Source file Output Processing
main.js.liquid main.js Liquid-rendered (with site in scope), .liquid stripped
style.scss style.css Compiled by Dart Sass
style.scss.liquid style.css Liquid first, then Sass
app.css app.css Copied (fingerprinted if digest: true)

Unprocessed files that aren’t fingerprinted (images, video, fonts on a site without digest) are streamed, not loaded into memory, and skipped when the output already has the same size and is at least as new as the source. Processed or fingerprinted outputs are skipped when their bytes are unchanged. Either way the kept file’s modification time is refreshed.

Files prefixed with _ (Sass partials) are never emitted standalone.

CMS media library: the CMS serves its image library from the first assets.sources entry ending in images (e.g. _assets/images), falling back to assets/images. Picking an image from a pipeline source inserts the bare asset key (for {% asset %}), not an absolute path.

Stale output: a full build deletes destination files it didn’t produce (keep extra files with keep_files:), so leftovers such as raw *.liquid / *.scss from an older build disappear on their own; with --incremental, clean the destination yourself.

Sass

Gohyde compiles .scss/.sass via Dart Sass when a real sass/dart-sass binary is on PATH. It verifies the binary with --version and rejects the legacy Ruby sass gem (which can’t accept --no-source-map). With no real compiler available, sources pass through unchanged and a warning is printed — the build still succeeds.

sass:
  sass_dir: _sass
  style: expanded      # default, as in jekyll-sass-converter; or compressed

Tailwind

tailwind:
  enabled: true
  input: assets/css/input.css
  output: assets/css/tailwind.css
  bundles:                          # optional: extra css compilations
    - input: _assets/css/admin.css
      output: assets/admin.min.css

Each bundles entry gets its own CLI run — and its own --watch process under gohyde serve. A failing bundle doesn’t stop the others.

gohyde serve starts tailwindcss --watch only when enabled: true or the input file exists — so sites that disable Tailwind don’t get spammed with input.css does not exist.

gohyde build runs Tailwind while the pages render, since Tailwind scans source templates. If the input CSS (or a tailwind.config.*) mentions the destination directory — e.g. a v3 content: ["./_site/**/*.html"] — Tailwind runs after the pages are written instead.