Fresh logo

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.

Typescript main.ts
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.

Typescript  
// 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 an assets/ folder)
  • Files referenced by URL path (favicon.ico, fonts, robots.txt, PDFs, etc.): place in static/
Tip

Always use root-relative URLs (starting with /) when referencing static files in HTML. For example, use src="/image/photo.png" instead of src="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.

Multiple static directories

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.

Typescript vite.config.ts
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 staticDir option accepts a string or an array of strings.

Caching headers

By default, Fresh adds caching headers for the src and srcset attributes on <img> and <source> tags.

Typescript  
// 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.

Typescript  
// 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.

Typescript routes/about.tsx
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():

Typescript routes/gallery.tsx
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:

Typescript  
deno add -D npm:vite-imagetools
Typescript vite.config.ts
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:

Typescript  
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 srcset and sizes attributes
  • Set width and height on <img> tags to prevent layout shift
  • Use loading="lazy" for below-the-fold images
  • Use asset() / assetSrcSet() for cache-busted URLs