Loading Local Images in Minimal Mistakes
I want to be able to load local images when the site is served locally. However,
the URL format file:///Users/<username>/archive/image/20260218184820.png
doesn’t work.
Why <img src="file:///Users/..."> won’t load?
Your page is being served over HTTP (e.g. http://localhost:4000). Modern
browsers block pages from http(s)://… from loading local files via file://…
for security (“cross-origin” / local file access). So even though the HTML is
“correctly parsed”, the browser refuses to fetch it.
That means: a file:// URL will not work on a website (local dev server or
deployed site). It only works when the page itself is also opened via file://
(and even then, policies vary).
Method 1: Copying files to project assets
The simplest method is to copy the images to <project-root>/assets/images/ and
then refer to them using the standard syntax:
---
gallery:
- url: /assets/images/20260218184820.png
image_path: /assets/images/20260218184820.png
---

<img src="https://josephtesfaye.com/josephs-blog/assets/images/filename.jpg" alt="">
{% include gallery %}
However, this has the overhead of managing files manually, which can be tedious. The following methods give you better experience.
Method 2: Using symlinks
-
Create a symlink to the target data inside the site root. Say, if the local images reside in
~/Downloads/temp/archive/image/foo/you can create a symlink to it under<project-root>/_site/assets/archive/image/foo:cd <project-root>/_site/ mkdir -p assets/archive/image/ ln -s "~/Downloads/temp/archive/image/foo" "assets/archive/image/foo" -
Then you can refer to the images using the symlink like the following:
--- gallery: - url: /assets/archive/image/foo/20260218184820.png image_path: /assets/archive/image/foo/20260218184820.png - url: /assets/archive/image/foo/20260218184820.png image_path: /assets/archive/image/foo/20260218184820.png ---  {% include gallery %} -
The symlinks created this way are cleared every time the site is re-built. To make them persist we can write a plugin
_plugins/symlink_external_assets.rbto create them automatically according to the following front matter of a post:--- symlinks: - /assets/archive/image/foo ~/Downloads/temp/archive/image/foo - /assets/archive/video/foo ~/Downloads/temp/archive/video/foo ---symlink_external_assets.rb:require 'fileutils' require 'shellwords' Jekyll::Hooks.register :site, :post_read do |site| return if Jekyll.env == 'production' # Store the intended symlinks so we can recreate them in the _site folder later site.config['dynamic_symlinks'] ||= {} docs = site.pages + site.collections.values.flat_map(&:docs) docs.each do |doc| next unless doc.data['symlinks'].is_a?(Array) doc.data['symlinks'].each do |symlink_def| next unless symlink_def.is_a?(String) parts = Shellwords.split(symlink_def) next unless parts.size == 2 link_path_raw, target_path_raw = parts target_path = File.expand_path(target_path_raw) relative_link_path = link_path_raw.sub(%r{^/}, '') link_path = File.join(site.dest, relative_link_path) # Verify target exists unless File.exist?(target_path) Jekyll.logger.warn "Symlink Plugin:", "Skipped. Target does not exist: #{link_path} -> #{target_path}" next end # Save for the post_write phase site.config['dynamic_symlinks'][relative_link_path] = { target: target_path, link: link_path } end end end # Inject the symlinks directly into the final build folder Jekyll::Hooks.register :site, :post_write do |site| return if Jekyll.env == 'production' symlinks = site.config['dynamic_symlinks'] || {} symlinks.each do |relative_path, link| target_path = link[:target] link_path = link[:link] needs_creation = false # If the link already exists but is broken, recreate it. If it's not broken # or is a file or directory, leave it. Otherwise, create the link. if File.symlink?(link_path) if !File.exist?(link_path) File.unlink(link_path) needs_creation = true elsif File.readlink(link_path) != target_path # Link has valid target Jekyll.logger.warn "Symlink Plugin: Symlink already exists: #{link_path}" end elsif !File.exist?(link_path) needs_creation = true else # Link path is taken by a file or directory Jekyll.logger.warn "Symlink Plugin: Symlink path is taken: #{link_path}" end if needs_creation FileUtils.mkdir_p(File.dirname(link_path)) begin File.symlink(target_path, link_path) # Jekyll.logger.info "Symlink Plugin:", "Symlink created: #{link_path} -> #{target_path}" rescue => e Jekyll.logger.warn "Symlink Plugin:", "Failed to create symlink #{link_path}: #{e.message}" end end end end
Method 3: Serving the files locally with a web server
For example, you can serve the images locally with Python’s built-in web server:
-
Serve your image folder
cd ~/Downloads/temp/archive/image/foo/ python3 -m http.server 8123 --bind 127.0.0.1If you want the server to run in the background:
nohup python3 -m http.server 8123 --bind 127.0.0.1 >/tmp/imgserver.log 2>&1 &To stop it later:
lsof -iTCP:8123 -sTCP:LISTEN kill <PID> -
Use that URL in your post

You can serve from the directory ~/Downloads/temp/archive/image/foo/ but use a
different mount path in the URL to refer to that directory, e.g.,
http://localhost:8123/igps/20260218184820.png. To achieve this we can write
and run this python script:
cd ~/Downloads/temp/archive/image/foo/
python3 server.py
server.py:
#!/usr/bin/env python3
from http.server import ThreadingHTTPServer, SimpleHTTPRequestHandler
from pathlib import Path
import os
import sys
# Directory you want to expose
ROOT = Path(os.path.expanduser("~/Downloads/temp/archive/image/foo")).resolve()
MOUNT = "/igps/"
HOST = "127.0.0.1"
PORT = 8123
class MountHandler(SimpleHTTPRequestHandler):
def translate_path(self, path: str) -> str:
"""
Map URLs under /igps/... to files under ROOT/...
Reject everything else with a 404.
"""
# Strip query/fragment
path = path.split("?", 1)[0].split("#", 1)[0]
if not path.startswith(MOUNT):
# Send 404 for non-mounted paths
return str(ROOT / "__nonexistent__")
rel = path[len(MOUNT):] # e.g. "20260218184820.png"
# Normalize and prevent path traversal
rel_path = Path(rel)
full = (ROOT / rel_path).resolve()
if ROOT not in full.parents and full != ROOT:
return str(ROOT / "__nonexistent__")
return str(full)
def log_message(self, fmt, *args):
# optional: quieter logs
sys.stderr.write("%s - - [%s] %s\n" % (self.client_address[0],
self.log_date_time_string(),
fmt % args))
if __name__ == "__main__":
if not ROOT.exists():
print(f"ERROR: directory does not exist: {ROOT}", file=sys.stderr)
sys.exit(1)
httpd = ThreadingHTTPServer((HOST, PORT), MountHandler)
print(f"Serving {ROOT} at http://{HOST}:{PORT}{MOUNT}")
httpd.serve_forever()
Comments