Images, scripts, fonts and islands
These are plain functions and data, so they run the same way on the server and in the browser.
These are plain functions and data, so they run the same way on the server and in the browser. They are not components: a
Dusk source file is compiled without JSX, so the markup is written with the spread syntax of JSX (<img {...} />,
<div {...} />). That keeps the server and the browser output equal, which is what hydration needs.
Images. imageProps returns the attributes of an <img>: a srcset of the display width and each offered width up to twice it,
lazy loading (or eager with a high fetch priority for priority: true), decoding="async", and an aspect-ratio from the
width and height, so the box keeps its shape before the file arrives. The candidates point at /_dusk/image, the optimizer endpoint:
import { imageProps, preloadImageTag } from "@shadow-js/dusk";
export const hero = imageProps({ src: "/hero.jpg", width: 1200, height: 630, alt: "", priority: true, sizes: "100vw" });
export const heroPreload = preloadImageTag({ src: "/hero.jpg", width: 1200, height: 630, alt: "", sizes: "100vw" });
The optimizer answers GET /_dusk/image?url=&w=&q=&f= for the widths the app offers only: IMAGE_WIDTHS by default, and images.widths (or image.widths in dusk.config.ts) changes them. imageProps snaps every width it writes to one of them. It serves a file of the app
through the images.source option, and a remote image only from an origin in images.remotePatterns (no redirects, a size limit,
a timeout). Its processor is pluggable: the built-in passthroughProcessor changes nothing, and svgProcessor passes SVG through
(vector) and refuses raster sources. An SVG is served with a sandboxing Content Security Policy, because it can run script. The
package ships no native image code: an adapter or an app that can resize supplies an ImageProcessor.
Scripts. scriptTag returns the markup of a script for its strategy: beforeInteractive (blocking, in the head),
afterInteractive (async), or lazyOnload (nothing in the markup; loadScript loads it from the browser after the load event). Each
script carries the request's nonce, so a policy with script-src 'nonce-…' allows it and no other script. The head registry refuses
<script>, so the markup goes through the document.head option, which receives the request's event:
import { scriptsHtml } from "@shadow-js/dusk";
import type { RequestEvent } from "@shadow-js/dusk/server";
export const head = (event: RequestEvent): string =>
scriptsHtml([{ src: "/polyfill.js", strategy: "beforeInteractive" }, { src: "https://cdn.example/analytics.js" }], event.nonce);
Fonts. fontPreloadTag is the head link that starts a font's download early. fontFace writes the @font-face rule, with
font-display: swap by default; family names and URLs that could break out of the CSS are refused. fallbackFontFace describes a
local font that stands in while the web font loads, scaled (size-adjust) and given the web font's ascent, descent and line gap, so the
text does not jump when the web font arrives. Its inputs are the two fonts' metrics: xWidthAvg is the average width of a lowercase
letter, measured from the font, not guessed.
import { fallbackFontFace, fontFace, fontPreloadTag } from "@shadow-js/dusk";
export const inter = fontFace({
family: "Inter",
src: [{ url: "/fonts/inter.woff2", format: "woff2" }],
weight: "100 900",
display: "swap",
});
export const interFallback = fallbackFontFace({
family: "Inter fallback",
local: "Arial",
font: { unitsPerEm: 2816, xWidthAvg: 1100, ascender: 2728, descender: -680, lineGap: 0 },
fallback: { unitsPerEm: 2048, xWidthAvg: 904 },
});
export const interPreload = fontPreloadTag({ href: "/fonts/inter.woff2" });
Islands. A page part that does not need JavaScript on first paint is rendered by the server and hydrated later. Spread
islandAttributes on the element: its id, its strategy (visible, the default; idle; interaction; media with a query), and its
props, which must be plain data (functions, class instances, bigint, NaN and cycles are refused, and <, >, & are escaped in the
attribute). In the browser, hydrateIslands mounts each island when its strategy fires. The registry maps each id to a loader; a loader
written as () => import("./chart") gives the island a chunk of its own, which the browser downloads only when the island mounts:
import { islandAttributes } from "@shadow-js/dusk";
export const panel = islandAttributes({ id: "chart", client: "interaction", props: { series: [3, 1, 4], title: "Sales" } });
export default function Chart(props: Readonly<Record<string, unknown>>): unknown {
return props["title"];
}
import { hydrateIslands } from "@shadow-js/dusk/client";
const islands = hydrateIslands({
registry: { chart: () => import("./chart") },
onError: (error) => console.error(error),
});
void islands.hydrated;
An island inside another island is left to the outer one. The island's own markup is replaced by its component's output when it mounts (there is no DOM reuse in this version: the server's markup is what the page shows until then).