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 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

[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:
- Minify manually and commit the minified file
- Use a build step (esbuild, rollup, or webpack) before Jekyll runs
- 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.