Skip to main content
Fresh lemon

Fresh 1.6: Tailwind CSS plugin, simplified typings and more


In this cycle we’ve worked on making first class Tailwind CSS support a reality and expanding our plugin API. Apart from that we spent time on various developer experience improvements.

Remember: you can start a new Fresh project by running

deno run -Ar https://fresh.deno.dev

or update an existing project by running

deno run -Ar https://fresh.deno.dev/update

in your project folder.

Here’s an overview of what’s new in 1.6. Read on for all the details, plus what to expect in the next iteration of Fresh.

Fresh 1.6 at a glance

  • 🌊 First class Tailwind CSS plugin We’re moving from Twind to Tailwind CSS. It features better Editor integrations and moves the CSS generation out of the rendering path.
  • 📨 Partials with forms Partials are now supported on Form elements.
  • 🧯 Partials on error pages Partials can now be used to render error pages.
  • 🕵️‍♀️ Closable error overlay The custom error overlay that is shown during development can now be closed, so that you can see the projects 500 page.
  • ⚡️ Improved islands bundling strategy Islands are now bundled together in a smarter way that leads to less files and allows you to group related islands together.
  • ⚔️ Less manifest merge conflicts The identifiers in the manifest file are now derived from the file name instead of being an incremented number. This reduces the chance for merge conflicts greatly.
  • 📦 Support for pre-generated assets Fresh will give generated assets priority when serving them when it encouters the same file in _fresh/static.
  • 🧩 Plugin API enhancements The Plugin API now supports adding islands from plugins, adding <link>-elements and has a new configResolved hook.
  • 🏁 Faster route matching The route matching performance has been improved, especially when a project has many static files.
  • 🛣️ Serving from a sub path Fresh projects can now be served under a sub path like `https://example.com/foo/bar` instead of the root of the domain.
  • 🏆 Simplified typings The number of different context types has been reduced from 6 down to 2 and there is now only one props type instead of four.

First class Tailwind CSS plugin

It has been a long time coming and Fresh now ships with a proper Tailwind CSS plugin. It’s using the exact same npm package as you would do in Node. This means we’re putting our Twind plugin on life support and mark it as deprecated.

Tailwind CSS has many benefits over Twind, with the most important one being that Tailwind CSS is actively maintained and supports a much more polished editor experience. You’ll get some nice performance benefits too by switching over to Tailwind CSS as it only needs to generate CSS once on deployment, vs on the fly on every request with Twind.

To switch from Twind to Tailwind CSS, follow this guide in our documentation. Note that this plugin requires ahead of time builds to be set up for your project.

Tailwind CSS vscode extension in action on a Fresh project.

Partials with forms

We’ve expanded the capabilities for Partials to work with <form> -elements. Similar to the existing Partials support for <a>-elements, Fresh will opt into partials with forms when the parent element of the submitter has a “truthy” f-client-nav attribute. This even works when the submit button is outside of the enclosing form element.

<form id="foo">
  <Partial name="slot-1">
    <input type="text" value={value} name="name" />
  </Partial>
</form>
<button
  type="submit"
  form="foo"
  formaction="/form"
  f-partial="/form"
  formmethod="POST"
>
  submit
</button>

Partials on error pages

With Fresh 1.6 you can now use Partials for error pages too. Previously, a partial navigation would error when the response status code was not ok. This limitation has been removed and we only check if the response content type is HTML.

While we were working on that part of the code, we also added support for following redirects automatically for partial navigations.

Closable error overlay

With Fresh 1.5 we added an error overlay during development that shows detailed information about where the error occurred. Problem was that this overrode the user’s _500.tsx error page. We fixed that and now the error overlay is a proper error overlay that can be closed.

Improved islands bundling strategy

In the browser we noticed that we could improve the way we serve the JavaScript code for islands. Previously, every island component would be moved into a separate bundle, which would lead to many .js files being generated that are less than <1kb. The new strategy adheres to the original files to give you much more control on how best to bundle islands for your project. By respecting the original files you can group related islands together and include them in the same bundle.

Less manifest merge conflicts

Whilst we want to get rid of the manifest in the long term, we felt that we can improve the current pain points of frequently running into merge conflicts when adding new routes or renaming them. Previously, identifiers would be composed of $<number> where the number part was merely incrementing. The new approach converts the file name to a valid JavaScript identifier and only appends a number if an existing identifier with the same name is found.

Thanks to Reed von Redwitz for bringing this over the finish line!

Screenshot of a fresh.gen.ts manifest file where import names are serialized file names instead of a `$` with an incremented number

Support for pre-generated assets

There are many scenarios where you’d want to generate asset files for deployment. These could include file optimisations or generating CSS like for the Tailwind CSS plugin. The way it works is that Fresh gives static files residing in <project>/_fresh/static higher priority over the default <project>/static directory.

Plugin API enhancements

The plugin API was an area we always wanted to improve but didn’t have the time for. With this release we set aside dedicated time to expand it with some new features users have been asking for.

Islands from plugins

Among the most exciting of them is the ability to add islands from plugins. By specifying file paths to your island file, Fresh will treat them the same way as if they had been placed inside the islands/ directory.

import { Plugin } from "$fresh/server.ts";
import * as path from "https://deno.land/std@0.208.0/path/mod.ts";

const __dirname = path.dirname(path.fromFileUrl(import.meta.url));

export default function myIslandPlugin(): Plugin {
  return {
    name: "my-island-plugin",
    islands: {
      baseLocation: import.meta.url,
      paths: [
        "./plugin-islands/MyIsland.tsx",
        "./plugin-islands/OtherPluginIsland.tsx",
      ],
    },
  };
}

Thanks to Reed von Redwitz for working out all the details.

Plugins can now add <link> -elements to add additional stylesheets or similar things.

import { Plugin } from "$fresh/server.ts";

function MyPlugin(): Plugin {
  return {
    name: "link-inject",
    render(ctx) {
      ctx.render();
      return {
        links: [{ rel: "stylesheet", href: "styles.css" }],
      };
    },
  };
}

Thanks to Adam Gregory for enabling this feature!

New configResolved hook

There are often scenarios where your plugin needs to apply different logic based on the Fresh configuration. We already pass the config to onBuildStart(config) but that is too late for other sections of the plugin API. Therefore we’ve added a new configResolved hook similar to vite which allows you to grab the fully resolved Fresh configuration.

import { Plugin, ResolvedFreshConfig } from "$fresh/server.ts";

function MyPlugin(): Plugin {
  let config: ResolvedFreshConfig;

  return {
    name: "my-cool-plugin",
    configResolved(resolvedConfig) {
      config = resolvedConfig;
    },
  };
}

Faster route matching

Another area we looked at to make Fresh faster, was our router. We’ve noticed some inefficiencies in the way we process and match routes, especially when a project has many static files. We added another optimisation that detects if the route has no dynamic parts and will fall back to a simple string comparisons.

Overall, with all optimisations applied we see a 4-10x speedup in route matching times on deno.com .

Serving from a sub path

Not every project is served at the root domain address and a popular request among users was to be able to serve Fresh from a sub path. Instead of hosted at https://example.com/ it should be available under https://example.com/foo/bar for example. This is now possible with the new router.basePath config option.

// fresh.config.ts
import { defineConfig } from "$fresh/server.ts";

export default defineConfig({
  router: {
    basePath: "/foo/bar",
  },
});

Thanks again to Reed von Redwitz for bringing this over the finish line!

Simplified typings

One thing that I always felt we could do better has to do with how we type our API that users interface this. Take a look at the various middleware and route context types for example:

  • MiddlewareHandlerContext
  • AppContext
  • ErrorHandlerContext
  • HandlerContext
  • LayoutContext
  • UnknownHandlerContext

That’s quite a lot of types to be aware of. With recent code cleanups and simplifications of our internals we realised that they are mostly the same type. So we reduced the number of context types down to just two.

  • FreshContext
  • RouteContext (for async routes/layouts/app wrapper)

In the future we might be even able to reduce it further to just one. Note, that we’re still keeping the old types around as aliases to the new ones to not make this a breaking change.

In the midst of simplifying our core we noticed that our props typings were similarly complex, more than they should’ve been.

  • ErrorPageProps
  • AppProps
  • UnknownPageProps
  • LayoutProps

Those four types have been reduced to a single type:

  • PageProps

We’re pretty happy with the simplifications so far as it eases the mental burden of working with Fresh.

Together with the type simplifications we made sure that the Fresh context object is exposed everywhere. It’s now passed to middlewares and routes as well. On top of we now pass the fully resolved Fresh config inside context, so that you can apply different logic based on it.

What’s on the horizon?

As with every release there is more that I would’ve loved to be able to include in this release. The truth is that these features need a little more time to polish. Curious Deno readers might have noticed the new “precompile” JSX transform in recent Deno release notes. It’s something I spent quite a bit of time on during this cycle and something I want to bring to Fresh as soon as possible. It’s mostly a performance change in how JSX is transpiled that will speed up most Fresh sites by 2-4x.

Another thing that I’ve worked on with Bartek is HMR for Deno. The initial prototype landed in Deno 1.38 as well, but there is more work to be done to wire it up in Fresh and make it feel smooth.

And of course, there is more work to be done surrounding Partials! I really want to have a proper way to specify pending states, do error handling and expose the functionality so that you can call it from an event listener. I don’t know exactly how all of this will look like, but it’s something I really want to have a good answer for in Fresh.

The December cycle will be a bit shorter than usual due to vacations around Christmas. It’s likely that we’ll skip a feature release and only do another patch release at the end of the year. The current iteration plan can be seen in the December iteration plan