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' | relative_url }}">
<script src="{{ '/assets/js/main.js' | relative_url }}" defer></script>
<img src="{{ '/assets/images/logo.png' | relative_url }}" 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="{{ '/assets/images/social-card.png' | absolute_url }}">
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="{{ post.image | relative_url }}" alt="{{ post.title }}" 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' | relative_url }}" 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' | relative_url }}">
<link rel="icon" type="image/png" sizes="32x32" href="{{ '/assets/favicon-32x32.png' | relative_url }}">
<link rel="icon" type="image/png" sizes="16x16" href="{{ '/assets/favicon-16x16.png' | relative_url }}">
<link rel="apple-touch-icon" sizes="180x180" href="{{ '/assets/apple-touch-icon.png' | relative_url }}">
<link rel="manifest" href="{{ '/assets/site.webmanifest' | relative_url }}">
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' | relative_url }}?v={{ site.version }}">
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.
Organising your assets folder for maintainability
A well-organised assets/ directory makes finding and updating files fast. The conventional structure is: assets/css/ for compiled stylesheets, assets/js/ for JavaScript files, assets/images/ for site-wide images, assets/fonts/ for self-hosted typefaces, and assets/icons/ for SVG icons or favicon variants. Post-specific images often go in a post-images subfolder organised by year or post slug to prevent the root images directory from becoming unmanageable.
Naming conventions matter more as the asset library grows. Use lowercase hyphenated names for all files (hero-background.webp rather than HeroBackground.WebP). Include dimensions in image filenames when you serve multiple sizes (logo-200w.webp, logo-400w.webp). Prefix JavaScript files that are page-specific rather than sitewide (post.js, home.js, theme-detail.js) to distinguish them from libraries. These conventions make the assets directory self-documenting and reduce the time spent hunting for a specific file.
Version your asset files explicitly when they change significantly. Jekyll does not have a built-in asset fingerprinting system (unlike Webpack or Vite), so browser caching can cause visitors to see stale CSS or JavaScript after an update. The simplest workaround is a version query string in _config.yml:
asset_version: "2025-12"
Reference it in your layout: main.css?v={{ site.asset_version }}. When you update CSS significantly, bump the version string and the new filename breaks the browser cache for all visitors. This is not as elegant as hash-based fingerprinting but is simple, reliable, and sufficient for most Jekyll sites.
Handling images efficiently in Jekyll
Images are the largest assets in most Jekyll sites and the primary target for performance optimisation. The goal is to serve each image at the smallest file size that maintains acceptable visual quality, in the correct format, at exactly the dimensions needed for the display context.
WebP is the correct format for photographs and complex images in 2025. It is smaller than JPEG at equivalent quality and supported by all modern browsers. PNG remains appropriate for logos, icons, and graphics with transparency or hard edges where lossless compression matters. Convert JPEG originals to WebP using ImageMagick or Squoosh before adding them to the assets folder β do not rely on on-the-fly conversion during builds, which is slow and increases build times.
For images that appear in multiple sizes β a cover image shown as a thumbnail on the blog index and full-width on the post page β provide multiple source files and use the srcset attribute in your HTML to let the browser select the appropriate size. The <picture> element with multiple <source> children gives you full control: WebP for modern browsers, JPEG fallback for older ones, and different crop ratios for different breakpoints if needed.
Store images in a dedicated folder rather than at the root of _posts/. Images co-located with post files work (Jekyll processes files in _posts/ subdirectories) but create clutter and make bulk operations on all images harder. A single assets/images/posts/ directory or an organisation by year (assets/images/2025/) keeps images findable and manageable.
JavaScript in Jekyll: the right amount
Jekyllβs JavaScript story is deliberate minimalism. The framework provides no JavaScript of its own β your theme and your customisations provide whatever JavaScript the site needs. This is a feature, not a limitation: it means you only load JavaScript you have consciously chosen to include.
Audit your themeβs JavaScript dependencies before using it. Open the network tab in browser DevTools and load the live demo. Check what scripts are loaded, how large they are, and whether they are render-blocking. A theme that loads jQuery (87KB gzipped) for dropdown menus that could be handled with 15 lines of vanilla JavaScript is carrying avoidable weight. A theme that loads a 50KB animation library for a hero section that animates only on first load is optimisable.
Modularise your JavaScript into page-specific files and load them conditionally. A post page should load post.js (table of contents, back-to-top button, copy-to-clipboard). The homepage loads home.js (carousel, testimonial rotation). The theme listing page loads themes.js (filtering, sorting, search). Loading all JavaScript on every page increases parse and execution time on pages that do not need it. Conditional loading β {% if page.layout == 'post' %}<script src="/assets/js/post.js" defer></script>{% endif %} in your layout β is a structural performance improvement that compounds across every page visit.
Static files as a deployment advantage
One of Jekyllβs most underappreciated properties is that every output file β HTML, CSS, JavaScript, images, fonts β is a static file that can be cached aggressively at the CDN edge. There are no API calls, no database queries, no server-side rendering that must complete before a response can be sent. Every resource the browser requests is pre-computed, pre-cached, and pre-positioned at an edge node near the visitor.
This architecture means a Jekyll siteβs performance does not degrade under traffic spikes. The thousandth concurrent visitor gets the same response time as the first, because the response is a cached file delivered from a CDN node, not a dynamically generated page competing for server resources. For content that occasionally goes viral β a post that gets linked from a popular newsletter, a theme that gets featured on a high-traffic developer site β the static file architecture handles the traffic surge without additional cost or configuration.
Treat this architectural advantage as a reason to build your site correctly the first time: choose the right CDN host, configure caching headers properly, and optimise your build pipeline. The investment in static file architecture pays dividends every time a visitor loads a page, every time a search crawler indexes a URL, and every time traffic spikes beyond what a dynamic hosting tier would handle comfortably.