Using Fresh Plugins
Javier RĂos
Featured on Deno News issue #57!
As you may or may not know, Fresh is the next-gen
web framework. Even though
it is relatively new, it has gained a lot of popularity, with an astonishing
10k
stars on GitHub. The main features
are its just-in-time rendering on the edge, island based client hydration and no
build step.
As a regular Fresh user, one thing I've learned is that to get the most out of
Fresh, types
are your best friend. Since all types are documented
, one can
explore them and learn a lot. This is for example the case of plugins
.
If you have used Fresh
+ twind
before, you may have noticed that in the
main.ts
file, the start
function takes a plugins
array:
/// <reference no-default-lib="true" />
/// <reference lib="dom" />
/// <reference lib="dom.iterable" />
/// <reference lib="dom.asynciterable" />
/// <reference lib="deno.ns" />
import { start } from "$fresh/server.ts";
import manifest from "./fresh.gen.ts";
import twindPlugin from "$fresh/plugins/twind.ts";
import twindConfig from "./twind.config.ts";
await start(manifest, { plugins: [twindPlugin(twindConfig)] });
What is a plugin?
As explained in the highly technical documentation:
Plugins can dynamically add new functionality to Fresh without exposing significant complexity to the user. Users can add plugins by importing and initializing them in their main.ts file:
This basically means that Fresh
offers us developers a way to hook custom
JavaScript
and CSS
to our site. Cool!
Usecases
If you are developing your website, you may have encountered one of the following problems:
Why do I have to use the special non-standard
_app.tsx
?
I want to add global styles without adding them to the
static
folder.
How do I add custom JavaScript in Fresh? Where are
script
tags?
The answer to all of those problems is: Write your own plugin!
Injecting CSS
Let's create a really simple plugin that adds smooth scrolling to our website.
import { Plugin } from "$fresh/server.ts";
const CUSTOM_STYLE_ID = "__JBL_INJECT";
export const smooth: Plugin = {
name: "smooth",
render: ({ render }) => {
render();
return {
// ...
styles: [{
cssText: "html {scroll-behavior: smooth;}",
id: CUSTOM_STYLE_ID,
}],
};
},
};
This as simple as it gets. We just export an Object of type Plugin
that
returns a render
function that itself calls the ctx
destructured render
function and returns an array named styles
.
Each of those objects of the styles
array will be injected into the head
of
our site, with the id
set to the specified one. After adding it to the
plugins
array in main.ts
, the generated head
would look like this,
provided that you are using the twind
plugin:
<style id="__FRSH_TWIND">...</style>
<style id="__JBL_INJECT">html {scroll-behavior: smooth;}</style>
Injecting JavaScript
Now that the main structure of a plugin
is clear, let's write one that appends
a link
to the head so that our site has a site.webmanifest
!
The structure of a script
plugin looks like this. Basically whatever you pass
as state
gets passed as an argument to the script
that you specify in the
entrypoint
key. The state
parameter must be JSON-serializable
, to pass it
as a prop in the client, and the script
itself must export default
a
function that takes that paremeter.
export interface PluginRenderScripts {
/** The "key" of the entrypoint (as specified in `Plugin.entrypoints`) for the
* script that should be loaded. The script must be an ES module that exports
* a default function.
*
* The default function is invoked with the `state` argument specified below.
*/
entrypoint: string;
/** The state argument that is passed to the default export invocation of the
* entrypoint's default export. The state must be JSON-serializable.
*/
state: unknown;
}
Let's code the injected
JavaScript function, which just appends an Element
of type type
with props props
to the head
of the site.
export default function append({ type, ...props }) {
const elem = document.createElement(type);
for (const [key, value] of Object.entries(props)) {
elem[key] = value;
}
document.head.append(elem);
}
Now comes the tricky part. We need to tell Fresh
which key
associates with
which source
, so we do that in the entrypoints
object to later refer in the
entrypoint
key.
import { Plugin } from "$fresh/server.ts";
export const inject: Plugin = {
name: "inject",
entrypoints: {
manifest:
`data:application/javascript,export default function e({type:e,...t}){let n=document.createElement(e);for(let[a,f]of Object.entries(t))n[a]=f;document.head.append(n)};`,
},
// ...
};
As you have realized, the code has been minimized
and put as a data URI
with
MIME type application/javascript
, so it can be used as a script.
The next step is to call the render
function and return our scripts
, with
the entrypoint
key being the one pointing to our script
source in the
entrypoints
object, and state
with the arguments to our append
function.
import { Plugin } from "$fresh/server.ts";
export const inject: Plugin = {
name: "inject",
entrypoints: {
manifest:
`data:application/javascript,export default function e({type:e,...t}){let n=document.createElement(e);for(let[a,f]of Object.entries(t))n[a]=f;document.head.append(n)};`,
},
render: ({ render }) => {
render();
return {
scripts: [{
entrypoint: "manifest",
state: {
type: "link",
rel: "manifest",
href: "/site.webmanifest",
},
}],
// ...
};
},
};
And that's it! Provided you have a valid site.webmanifest
file in the static
directory, your document head
should now have this, and the script
tag with
the __FRSH_STATE
id will have your provided state
too! This is because
Fresh
passes the props
as JSON
.
<link rel="manifest" href="/site.webmanifest">
Wrapping up
Plugins
allow us to avoid having to create a separate file in the static
directory, then reference it from the non-standard _app.tsx
file in order for
our website to have extra functionality. Now your main.ts
should look like
this:
/// <reference no-default-lib="true" />
/// <reference lib="dom" />
/// <reference lib="dom.iterable" />
/// <reference lib="dom.asynciterable" />
/// <reference lib="deno.ns" />
import { start } from "$fresh/server.ts";
import manifest from "./fresh.gen.ts";
import twindPlugin from "$fresh/plugins/twind.ts";
import twindConfig from "./twind.config.ts";
import { inject, smooth } from "./plugins.ts";
await start(manifest, { plugins: [twindPlugin(twindConfig), inject, smooth] });
Another interesting use case for plugins
is registering a serviceWorker
, but
that will be left as an exercise to the reader.