docs/latest/concepts/static-files.md
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());
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:
static/ (e.g.,
in an assets/ folder)static/[tip]: Always 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.
You can serve files from more than one directory by passing an array to the
staticDir option. When the same filename exists in multiple directories, the
first directory in the array takes precedence.
import { defineConfig } from "vite";
import { fresh } from "@fresh/plugin-vite";
export default defineConfig({
plugins: [
fresh({
staticDir: ["static", "generated"],
}),
],
});
This is useful when you have a build step that generates assets into a separate directory and you want to keep them apart from hand-authored static files.
[info]: If you're using the Builder API instead of Vite, the same
staticDiroption accepts a string or an array of strings.
By default, Fresh adds caching headers for the src and srcset attributes on
`` and <source> tags.
// Caching headers will be automatically added
app.get("/user", (ctx) => ctx.render());
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(),
);
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 `` tags with a srcset attribute, use assetSrcSet():
import { assetSrcSet } from "fresh/runtime";
export default function Gallery() {
return (
);
}
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.
vite-imagetools lets you
import images with query parameters to resize, convert formats, and generate
srcset at build time:
deno add -D npm:vite-imagetools
import { 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 ;
}
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.
<picture> fallbackssrcset and sizes attributeswidth and height on `` tags to prevent layout shiftloading="lazy" for below-the-fold imagesasset() / assetSrcSet() for cache-busted URLs