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.

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