Static files
Static assets placed in the static/ directory are served at the root of the
webserver via the staticFiles() middleware. They are streamed directly from
disk for optimal performance with
ETag
headers.
import { staticFiles } from "fresh";
const app = new App()
.use(staticFiles());Imported assets vs static files
When using Fresh with Vite (now the default), files
that you import in your JavaScript/TypeScript code should not be placed in the
static/ folder. This prevents file duplication during the build process.
// Don't import from static/
import "./static/styles.css";
// Import from outside static/ (e.g., assets/)
import "./assets/styles.css";Rule of thumb:
- Files imported in code (CSS, icons, etc.): place outside
static/(e.g., in anassets/folder) - Files referenced by URL path (favicon.ico, fonts, robots.txt, PDFs, etc.):
place in
static/
TipAlways use root-relative URLs (starting with
/) when referencing static files in HTML. For example, usesrc="/image/photo.png"instead ofsrc="image/photo.png". Relative paths resolve against the browser’s current URL, which breaks when navigating between routes.
When you import a file in your code, Vite processes it through its build
pipeline, optimizes it, and adds a content hash to the filename for cache
busting. Keeping these files outside static/ ensures they’re only included
once in your build output.
Caching headers
By default, Fresh adds caching headers for the src and srcset attributes on
<img> and <source> tags.
// Caching headers will be automatically added
app.get("/user", (ctx) => ctx.render(<img src="/user.png" />));You can always opt out of this behaviour per tag, by adding the
data-fresh-disable-lock attribute.
// Opt-out of automatic caching headers
app.get(
"/user",
(ctx) => ctx.render(<img src="/user.png" data-fresh-disable-lock />),
);Adding caching headers manually
Use the asset() function to add caching headers manually. It will be served
with a cache lifetime of one year.
import { asset } from "fresh/runtime";
export default function About() {
// Adding caching headers manually
return <a href={asset("/brochure.pdf")}>View brochure</a>;
}For <img> tags with a srcset attribute, use assetSrcSet():
import { assetSrcSet } from "fresh/runtime";
export default function Gallery() {
return (
<img
src="/photo.jpg"
srcset={assetSrcSet("/photo-640.jpg 640w, /photo-1280.jpg 1280w")}
/>
);
}Image optimization
Fresh does not include a built-in image optimization pipeline, but since Fresh 2 uses Vite, you can use Vite plugins or external services to optimize images.
Build-time optimization with Vite
vite-imagetools lets you
import images with query parameters to resize, convert formats, and generate
srcset at build time:
deno add -D npm:vite-imagetoolsimport { defineConfig } from "vite";
import { fresh } from "@fresh/plugin-vite";
import { imagetools } from "vite-imagetools";
export default defineConfig({
plugins: [fresh(), imagetools()],
});Then import optimized images directly:
import heroAvif from "../static/hero.jpg?format=avif&w=800";
export default function Page() {
return <img src={heroAvif} alt="Hero" width={800} />;
}CDN image services
For dynamic optimization without a build step, use a CDN image service that transforms images on-the-fly:
These services resize, compress, and convert images to modern formats (WebP, AVIF) based on URL parameters, with automatic caching at the edge.
Best practices
- Use modern formats (WebP, AVIF) with
<picture>fallbacks - Provide responsive images with
srcsetandsizesattributes - Set
widthandheighton<img>tags to prevent layout shift - Use
loading="lazy"for below-the-fold images - Use
asset()/assetSrcSet()for cache-busted URLs