Home β€Ί Blog β€Ί Jekyll Static Files and Assets: How to Manage Images, CSS, and JS
Tutorial

Jekyll Static Files and Assets: How to Manage Images, CSS, and JS

How Jekyll handles static files and assets β€” the assets/ folder, referencing files in templates, image management, static_files, and optimising assets for production.

Jekyll Static Files and Assets: How to Manage Images, CSS, and JS

Jekyll processes some files (Markdown, Liquid templates, Sass) and copies others unchanged. Understanding which is which β€” and how to reference assets correctly in your templates β€” is fundamental to building Jekyll sites that work properly across all environments.

Processed files vs static files

Jekyll handles two categories of files:

Processed files β€” files with YAML front matter, or files in special directories (_posts/, _layouts/, _includes/, _sass/). Jekyll runs these through its build pipeline: Liquid templating, Markdown conversion, Sass compilation.

Static files β€” everything else. Jekyll copies them to _site/ unchanged. Images, fonts, JavaScript files, PDFs, videos β€” all copied as-is.

my-jekyll-site/
β”œβ”€β”€ _posts/           ← processed (has front matter)
β”œβ”€β”€ _sass/            ← processed (Sass compilation)
β”œβ”€β”€ assets/
β”‚   β”œβ”€β”€ images/       ← static (copied unchanged)
β”‚   β”œβ”€β”€ fonts/        ← static (copied unchanged)
β”‚   β”œβ”€β”€ js/           ← static (copied unchanged)
β”‚   └── css/
β”‚       └── main.scss ← processed (has front matter β†’ compiled to CSS)

The assets/ folder convention

While Jekyll has no hard requirement for folder naming, assets/ is the universal convention for static files. Most themes use:

assets/
β”œβ”€β”€ css/
β”‚   └── main.scss        ← entry point for Sass (has front matter)
β”œβ”€β”€ js/
β”‚   β”œβ”€β”€ main.js
β”‚   β”œβ”€β”€ bookmarks.js
β”‚   └── search.js
β”œβ”€β”€ images/
β”‚   β”œβ”€β”€ logo.png
β”‚   β”œβ”€β”€ social-card.png
β”‚   β”œβ”€β”€ favicon.ico
β”‚   └── blog/
β”‚       β”œβ”€β”€ post-cover.webp
β”‚       └── another-cover.webp
└── fonts/
    └── inter.woff2

Jekyll copies everything in assets/ to _site/assets/ during build. A file at assets/images/logo.png is served at https://example.com/assets/images/logo.png.

Referencing assets in templates

The relative_url filter

Always use relative_url when referencing assets in Liquid templates:

<link rel="stylesheet" href="/assets/css/main.css">
<script src="/assets/js/main.js" defer></script>
<img src="/assets/images/logo.png" alt="Logo">

relative_url prepends site.baseurl to the path. If your site has baseurl: "" (root domain), it changes nothing. If your site lives at a subdirectory (baseurl: "/my-project"), it correctly produces /my-project/assets/images/logo.png.

Without relative_url, assets break on sites with a non-empty baseurl.

The absolute_url filter

For Open Graph tags, sitemaps, or any place that needs a full URL:

<meta property="og:image" content="https://jekyllhub.com/assets/images/social-card.png">

absolute_url prepends both site.url and site.baseurl.

In Markdown content

![Hero image](/assets/images/blog/hero.webp)

[Download PDF](/assets/files/guide.pdf)

Note: Markdown links do not go through relative_url. If you have a baseurl, use the HTML <img> tag inside your Markdown, or ensure your permalink structure accounts for the base URL.

In SCSS/CSS

Reference assets from CSS using relative paths from the CSS file’s location. Since assets/css/main.scss is in assets/css/, fonts and images are reached with ../:

@font-face {
  font-family: "Inter";
  src: url("../fonts/inter.woff2") format("woff2");
}

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

Working with images

File formats in 2026

  • WebP β€” best default choice. 25–35% smaller than JPEG at equivalent quality. Supported by all modern browsers.
  • AVIF β€” even smaller than WebP but slower to encode. Good for images you generate offline.
  • JPEG β€” use for photographs when WebP is not an option.
  • PNG β€” use for images requiring transparency or exact colours (logos, icons).
  • SVG β€” use for logos, icons, and illustrations. Infinitely scalable, tiny file size.

Converting images to WebP

# Single file
cwebp -q 85 image.jpg -o image.webp

# Batch convert all JPEGs
find assets/images -name "*.jpg" -exec sh -c   'cwebp -q 85 "$1" -o "${1%.jpg}.webp"' _ {} \;

# Using ImageMagick
mogrify -format webp -quality 85 assets/images/blog/*.jpg

Responsive images with srcset

For images that appear at different sizes across screen widths:

<img
  src="/assets/images/hero-800.webp"
  srcset="
    /assets/images/hero-400.webp 400w,
    /assets/images/hero-800.webp 800w,
    /assets/images/hero-1200.webp 1200w
  "
  sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 800px"
  alt="Hero image"
  width="800"
  height="450"
  loading="lazy"
>

Generate multiple sizes during your build process (using Pillow, ImageMagick, or a Jekyll plugin like jekyll-responsive-image).

Always specify width and height

<img src="" alt="" width="800" height="450">

Width and height attributes prevent Cumulative Layout Shift (CLS) β€” a Core Web Vitals metric β€” by letting the browser reserve the correct space before the image loads.

Lazy loading

<!-- Below-the-fold images β€” lazy load -->
<img src="..." alt="..." loading="lazy">

<!-- Above-the-fold images (hero, LCP element) β€” eager load -->
<img src="..." alt="..." loading="eager">

Use loading="lazy" on images that are not visible when the page loads. Never use it on your LCP (Largest Contentful Paint) element β€” the hero image or main post image.

The site.static_files variable

Jekyll provides a site.static_files variable that lists all static files (files copied without processing):


{% for file in site.static_files %}
  {{ file.path }}          β†’ /assets/images/logo.png
  {{ file.basename }}      β†’ logo
  {{ file.name }}          β†’ logo.png
  {{ file.extname }}       β†’ .png
  {{ file.modified_time }} β†’ last modified date
{% endfor %}

Filter by extension or path:


{% assign images = site.static_files | where_exp: "f", "f.extname == '.webp'" %}
{% for image in images %}
  <img src="{{ image.path | relative_url }}" alt="{{ image.basename }}">
{% endfor %}

This is useful for auto-generating image galleries from files in a folder.

JavaScript files

Jekyll copies JavaScript files unchanged β€” no bundling, no transpilation. For simple sites, this is fine:

assets/js/
β”œβ”€β”€ main.js         ← copied as-is
β”œβ”€β”€ bookmarks.js    ← copied as-is
└── search.js       ← copied as-is

Reference in your layout:

<script src="/assets/js/main.js" defer></script>

Using Liquid in JavaScript files

If you need Jekyll variables in JavaScript (e.g. site data), add front matter to the JS file:


---
---
// assets/js/search-data.js

var SITE_DATA = {
  url: "{{ site.url }}",
  posts: [
    {% for post in site.posts %}
    {
      title: {{ post.title | jsonify }},
      url: "{{ post.url | relative_url }}",
      excerpt: {{ post.excerpt | strip_html | truncatewords: 30 | jsonify }}
    }{% unless forloop.last %},{% endunless %}
    {% endfor %}
  ]
};

Jekyll processes this file through Liquid (because of the front matter) and outputs JavaScript with the values baked in at build time.

Minifying JavaScript

Jekyll does not minify JS. Options:

  1. Minify manually and commit the minified file
  2. Use a build step (esbuild, rollup, or webpack) before Jekyll runs
  3. Use a CDN for third-party libraries and serve your own JS unminified

For most Jekyll blogs, unminified JS is fine β€” the files are small and HTTP/2 handles multiple small requests efficiently.

Favicon files

Place favicon files in your project root or assets/ and reference in <head>:

assets/
β”œβ”€β”€ favicon.ico          ← legacy IE fallback
β”œβ”€β”€ favicon-16x16.png
β”œβ”€β”€ favicon-32x32.png
β”œβ”€β”€ apple-touch-icon.png  ← 180Γ—180px for iOS
└── site.webmanifest     ← PWA manifest
<!-- _includes/head.html -->
<link rel="icon" type="image/x-icon" href="/favicon.ico">
<link rel="icon" type="image/png" sizes="32x32" href="/assets/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/assets/favicon-16x16.png">
<link rel="apple-touch-icon" sizes="180x180" href="/assets/apple-touch-icon.png">
<link rel="manifest" href="/assets/site.webmanifest">

Excluding assets from the build

Use exclude: in _config.yml to prevent source files (like uncompressed originals) from appearing in _site/:

exclude:
  - assets/images/originals/   # source files, not served
  - assets/fonts/source/       # font source files
  - tools/                     # build scripts
  - "*.psd"                    # Photoshop files
  - "*.ai"                     # Illustrator files

Cache busting

Because browsers cache static assets aggressively, changing a CSS or JS file may not update for returning visitors. Options:

Query string versioning β€” append a version number:

<link rel="stylesheet" href="/assets/css/main.css?v=">

Update version: "1.2.3" in _config.yml after significant changes.

Filename hashing β€” include a hash in the filename (main.abc123.css). Requires a build pipeline; not supported natively by Jekyll.

Long cache headers with immutable assets β€” in _headers (Cloudflare/Netlify):

/assets/*
  Cache-Control: public, max-age=31536000, immutable

Use very long cache on assets with versioned filenames; use no-cache on HTML.

Summary: what goes where

File type Location How Jekyll handles it
Blog posts _posts/ Processed (Markdown β†’ HTML)
Page Markdown _pages/ or root Processed (Markdown β†’ HTML)
Layout HTML _layouts/ Used as template, not output
Include fragments _includes/ Used as template, not output
Sass partials _sass/ Compiled (via entry point)
Sass entry point assets/css/ Compiled to CSS
JavaScript assets/js/ Copied unchanged
Images assets/images/ Copied unchanged
Fonts assets/fonts/ Copied unchanged
Data files _data/ Loaded as site.data.*, not output

Understanding this table β€” what Jekyll processes vs what it copies β€” explains nearly every β€œwhy is my file not showing up” or β€œwhy is my template not working” issue you will encounter.

Share LinkedIn