Svelte runtime

Warning

This feature is experimental. No extensive tests were done to ensure it works in all conditions, but it should work in most cases.

Frugal comes with an optional integration with Svelte. You can write your static markup with Svelte and declare island for stateful components that need to be hydrated client-side.

Configuration

First, you'll need an Import Map. Frugal uses bare specifiers internally to avoid locking you with a specific version of peer dependencies: import * as preact from 'preact'. Those kinds of imports need to be "mapped" to actual URLs where you can choose the specific version you wish to use.

import_map.json
{
    "imports": {
        "svelte/": "npm:/svelte@3.58.9/",
        "svelte": "npm:/svelte@3.58.9",
    }
}
1
2
3
4
5
6

You'll also need a deno.json config file to configure the Import Map :

deno.json
{
    "importMap": "./import_map.json"
}
1
2
3

Now that deno is configured, we need to configure Frugal with the svelte plugin :

frugal.config.ts
import { Config } from "https://deno.land/x/frugal@0.9.6/mod.ts"
import { svelte } from "https://deno.land/x/frugal@0.9.6/plugins/svelte.ts"

export default {
    ...
    plugin: [svelte()]
    ...
} satisfies Config;
1
2
3
4
5
6
7
8

Now frugal is ready to process Svelte components.

Server-only runtime

Those functions can only be used in server-side components. They can't be used inside an island.

getRenderFrom

The only thing that changes in page descriptors is the render function. Instead of defining it ourselves, the Svelte runtime compute a render function for us from a Svelte component :

page.tsx
import { getRenderFrom } from "https://deno.land/x/frugal@0.9.6/runtime/preact.server.ts"
import App from './App.svelte'

export const render = getRenderFrom(App)
1
2
3
4

Parameters

The getRenderFrom takes two parameters, the root Svelte Component and an optional config object RenderConfig

type RenderConfig = {
    document?: (head: string, body: string) => string;
    embedData?: boolean;
};
1
2
3
4
document

The root Svelte Component only describes markup inside the body. To modify the rest of the document, you can pass a function taking the content of the <head> tag and the content of the <body> tag that returns the entire document.

Tip

If you want to modify the <head> of the document, use the <svelte:head> component instead.

embedData

By default, Frugal outputs static pages without any client-side script. But if you have client-side island, you might need access to the data object that was used to render the page in the server. The embedData parameter instructs Frugal to embed the data object in an inline script for you to access via getData

Warning

You will get an error if you call the function getData inside an island with embedData: false. You must have embedData: true for the function to work client-side.

Client-safe runtime

Every components, hooks, or method described here are usable inside server components or island. You can use them everywhere.

<Island>

Wrapping your stateful client-side component in the <Island> component will create an island. The <Island> component will output all the necessary markup to hydrate the component client-side.

MyComponentIsland.svelte
<script>
    import { Island } from "https://deno.land/x/frugal@0.9.6/runtime/svelte.client.ts"
    import MyComponent from './MyComponent.svelte'
    import { NAME } from "./MyComponentIsland.script.ts"
</script>

<Island name={NAME} component={Counter} {$$props}>
    <slot />
</Island>
1
2
3
4
5
6
7
8
9
Warning

The <Island> component does not perform any hydration; it only generates the markup necessary for hydration. The hydration is done via a client-side call to the hydrate function.

The component accepts the following props :

strategy

This prop selects the hydration strategy for the island :

  • "load" will hydrate the island on page load (default behavior)
  • "idle" will defer hydration until the browser is idle (via requestIdleCallback or setTimeout for browsers not supporting it)
  • "visible" will defer hydration until the island enters the viewport (via InsersectionObserver)
  • "media-query" will hydrate the island if the viewport matches a given media query on load
  • "never" will turn off hydration for this island

query

The media query to match if you chose the "media-query" strategy.

name

A unique name for your island. This name will be used as a selector to find the DOM node to hydrate.

Component

Your stateful client-side component inside the island.

props

The props passed to your component.

Warning

The props of your component will be serialized and embedded in the HTML markup, so the props must be serializable

Moreover, if you have multiple instances of the same island on the page and the props are derived from the page data object, it might be more efficient to have an island without props and to use useData with embedData:true to compute the props from the data object. Instead of having a JSON of the props embedded for each instance of your island, you'll have a single JSON of the data object embedded. You'll have to decide what option is the best fit for your case.

hydrate

This is the function to call client-side (inside a script) to hydrate an <Island> :

MyComponentIsland.script.ts
import { hydrate } from "https://deno.land/x/frugal@0.9.6/runtime/svelte.client.ts"
import MyComponent from "./MyComponent.svelte"

export const NAME = "MyComponent";

if (import.meta.environment === 'client') {
    hydrate(NAME, () => MyComponent)
}
1
2
3
4
5
6
7
8

This function will find every island with a matching name and hydrate them with the component MyComponent.

Parameters

The hydrate function takes two parameters. The name of the island to hydrate and a function (sync or async) returning a component.

Dynamic import and deferred hydration

The second parameter of the hydrate function can be an async callback to enable dynamic loading of components with code-splitting. Since the hydrate function calls the callback parameter only during actual hydration, for Islands with deferred hydration ("idle", "visible", "media-query") you can use this pattern with code splitting enabled (via esbuild config):

MyComponentIsland.script.ts
import { hydrate } from "https://deno.land/x/frugal@0.9.6/runtime/preact.client.ts"

export const NAME = "MyComponent";

if (import.meta.environment === 'client') {
    hydrate(NAME, () => (await import('./MyComponent.svelte')).default)
}
1
2
3
4
5
6
7

The JS chunk containing your component will not be loaded immediately. Instead, it will be deferred until the island is hydrated.

getData

This function gives you access to the data object of the page within any Svelte component.

Warning

For this function to work client-side, you need embedData:true in the getRenderFrom function.

getPathname

This function gives you access to the current pathname (the route of the page, compiled with the current path object).