Slick is a small, simple, and ultrafast web framework built on Web Standards for Deno.
- Ultrafast & lightweight Built on
@webtools/expressapiand the native Deno HTTP stack with minimal overhead. - Server-side rendering Pages and templates are rendered to HTML with Preact for great SEO and fast first paint.
- Typed from end to end
req.datafilled by a template's middleware is typed in itshead,bodyandfavicon, and in thetitle,head,body,onrequestandonpostof every page that uses it. URL params are typed too. - Islands architecture Ship static HTML by default, then hydrate only the interactive components you mark as islands.
- Zero-config asset pipeline CSS, JS and TS files in
static/are transpiled and minified on the fly with esbuild, cached in production and served with anETag. - Hot reload Optional dev mode that watches your workspace and reloads the pages, templates and islands affected by a change, without restarting the server.
- SPA navigation Optional client mode (
@webtools/slick-client) swaps pages overfetchwithout full reloads, while keeping SSR for the first request. - Bring your own npm libraries Use any npm/JSR package inside islands through automatic, shared, deduplicated vendor bundles and an import map.
- Simple by default Sensible defaults, a tiny configuration surface, and a predictable folder convention.
- How It Works
- Installation
- Quick Start
- Project Structure
- Configuration
- Hot Reload
- Environment Variables
- Templates
- Pages
- Typed Request Data
- Dynamic Rendering
- Static Files
- Islands
- Shared Libraries & Vendors
- Client Mode (SPA)
- Routing & Reserved Paths
- API Reference
- Migrating from 0.6
- License
Slick is built around three simple building blocks and a convention-based file system.
ββββββββββββββββββββββββββββββββββββββββββββββββ
HTTP GET β β Template + Page β SSR (Preact) β HTML β β Browser
ββββββββββββββββββββββββββββββββββββββββββββββββ
β
ββ Islands are detected during SSR and tagged in the HTML
β
Browser β loads the island hydration script
β
ββ Each tagged element is hydrated with its Preact component,
pulling shared libraries from a generated import map.
- Templates describe the shared shell of your site (header, footer,
<head>content). A template must contain an element withid="app"where page content is injected. - Pages describe a single route. Each page references a template and provides its own
title,head,body, styles, scripts and handlers. - Islands are interactive Preact components that are rendered on the server and hydrated on the client, only the islands ship JavaScript, the rest stays static HTML.
When client mode is enabled, navigation between pages happens over fetch: the server returns a JSON payload (only
the parts that changed) and the client patches the DOM instead of reloading the page.
deno add jsr:@webtools/slick-serverJSX must be configured for Preact, otherwise pages and templates won't compile. Preact must be in your imports.
preact-root-fragment too, as soon as the workspace has an islands/ directory: the hydration script imports it, and
Slick bundles the version you declare.
{
"imports": {
"@webtools/slick-server": "jsr:@webtools/slick-server@^0.7.0",
"preact": "npm:preact@^10.29.2",
"preact-root-fragment": "npm:preact-root-fragment@^0.3.1"
},
"compilerOptions": {
"jsxImportSource": "preact",
"jsx": "react-jsx"
}
}Slick targets Preact 10. Preact 11 is not supported yet.
Client mode (client: true) bundles @webtools/slick-client from the version declared in your project's deno.json:
"@webtools/slick-client": "jsr:@webtools/slick-client@^0.3.0"my-slick-app/
βββ pages/
β βββ index.tsx
βββ templates/
β βββ app.tsx
βββ static/
β βββ styles/
β βββ scripts/
βββ server.ts
import { Slick } from "@webtools/slick-server";
const app = new Slick(import.meta.dirname!, {
env: {
API_URL: Deno.env.get("API_URL") ?? "http://localhost:3000",
},
port: 5000,
lang: "en",
r404: "/",
client: true, // Enable SPA navigation
hotReload: true, // Reload pages, templates and islands on file change
});
await app.start();import type { Template } from "@webtools/slick-server";
export default {
name: "app",
favicon: "/assets/favicon.ico",
styles: ["/styles/reset.css", "/styles/app.css"],
scripts: ["/scripts/app.ts"],
head: <meta name="description" content="My Slick App" />,
body: (
<>
<header>
<nav>
<a href="/">Home</a>
<a href="/about">About</a>
</nav>
</header>
<main id="app">{/* Page content is injected here */}</main>
</>
),
} satisfies Template;import type { Page } from "@webtools/slick-server";
import app from "../templates/app.tsx";
export default {
url: "/",
template: app,
title: "Home - My App",
styles: ["/styles/pages/index.css"],
scripts: ["/scripts/pages/index.ts"],
head: <meta property="og:title" content="Home" />,
body: (
<>
<h1>Welcome to Slick Server!</h1>
<p>This is your home page.</p>
</>
),
} satisfies Page<typeof app>;deno run -A server.tsSlick relies on esbuild (for transpiling assets, islands and vendor bundles) and dynamically imports your pages, templates and islands. The
-Aflag is the simplest way to grant the required permissions. For a tighter sandbox you need at least--allow-net --allow-read --allow-env --allow-run --allow-write.
Slick uses a strict, convention-based layout. The templates/, pages/ and static/ directories are required; the
islands/ directory is optional.
project-root/
βββ pages/ # Route definitions (required)
β βββ index.tsx
β βββ about.tsx
βββ templates/ # Page shells (required)
β βββ app.tsx
β βββ admin.tsx
βββ islands/ # Interactive components (optional)
β βββ Counter.tsx
βββ static/ # Public assets (required)
βββ styles/
β βββ reset.css
β βββ app.css
βββ scripts/
β βββ app.ts
βββ assets/
βββ favicon.ico
| Directory | Required | Description |
|---|---|---|
pages/ |
β | Each file (.ts, .tsx, .js, .jsx) must export default a Page. Subdirectories are scanned. |
templates/ |
β | Each file must export default a Template. Subdirectories are scanned. |
static/ |
β | Served at the site root. CSS/JS/TS are transpiled and minified; everything else is served as-is. |
islands/ |
β¬ | Each file must export default a Preact function component to be hydrated on the client. |
Pages and templates are discovered recursively, so you can organize them in nested folders freely. Two pages cannot share a
url, and two templates cannot share aname: Slick refuses to start.
The Slick constructor takes the workspace path and a partial configuration object:
new Slick(workspace: string, config: Partial<Config> = {})interface Config {
env: Record<string, string>; // Compile-time constants (SSR + client)
port: number; // HTTP port
lang: string; // <html lang="..."> attribute
r404: string; // Fallback URL when no route/asset matches
client: boolean; // SPA mode: bundles @webtools/slick-client from the project's deno.json
hotReload: boolean; // Dev mode: reload pages, templates, islands and static assets on change
trustProxy: boolean; // Trust X-Forwarded-For (behind exactly one reverse proxy)
sharedLibs: readonly string[]; // Extra libraries made available to islands
}| Option | Type | Default | Description |
|---|---|---|---|
env |
Record<string, string> |
{} |
Compile-time constants injected via esbuild define. Available in pages, templates, islands (SSR and client), static scripts and vendor bundles. See Environment Variables. |
port |
number |
5000 |
Port the HTTP server listens on. |
lang |
string |
"en" |
Value of the HTML lang attribute. |
r404 |
string |
"/" |
Page URL to redirect to when a route or asset is not found. Must match an existing page. |
client |
boolean |
false |
When true, enables SPA navigation. @webtools/slick-client is bundled from the version in the project's deno.json imports and exposed through the import map. |
hotReload |
boolean |
false |
When true, enables development hot reload. See Hot Reload. |
trustProxy |
boolean |
false |
Reads the client IP from X-Forwarded-For (req.ip). Only enable it behind exactly one trusted reverse proxy. |
sharedLibs |
readonly string[] |
[] |
Libraries bundled once globally and shared across all islands (avoids duplicating them in every island bundle). Merged with the built-in Preact libs. |
Set hotReload: true during development to pick up file changes without restarting the server.
const app = new Slick(import.meta.dirname!, {
hotReload: Deno.env.get("DENO_ENV") !== "production",
});Slick watches the whole workspace (except static/, node_modules/ and hidden folders). When a source file is created,
modified or removed, Slick invalidates it and every module that imports it, then reloads the pages, templates and
islands concerned and rebuilds the routes at once.
| What changes | What happens |
|---|---|
| A page or a template | It is reloaded. A page with a new url gets its route, a removed file loses its route. |
| An island | It is reloaded and its client bundle rebuilt; the pages and templates importing it are reloaded. |
| A shared module (utils, componentsβ¦) | Every page, template and island importing it, directly or not, is reloaded. |
A file in static/ |
Recompiled on every request (no cache, no ETag). |
| A broken file (syntax errorβ¦) | The error is logged and the previous version stays served, nothing is half-applied. |
Changes are visible on the next HTTP request (refresh the browser to re-hydrate islands).
Restart still required when you change
config.env,port,sharedLibsor other server options, or when you create theislands/directory for the first time.
Keys from config.env are injected at compile time via esbuild define. Reference them as bare global identifiers
β no process.env, no Deno.env:
const app = new Slick(import.meta.dirname!, {
env: {
API_URL: Deno.env.get("API_URL") ?? "http://localhost:3000",
},
});They are available everywhere Slick compiles your code, including during SSR:
| Surface | SSR | Client (browser) |
|---|---|---|
| Pages | β | β |
| Templates | β | β |
| Islands | β | β |
Static .js / .ts |
β | β |
| Vendor bundles | β | β |
// pages/index.tsx β evaluated at SSR
export default {
url: "/",
template: app,
title: `Home β ${API_URL}`,
body: <p>Connected to {API_URL}</p>,
} satisfies Page<typeof app>;// islands/Status.tsx β SSR render + hydrated client bundle
export default function Status() {
return <p>API: {API_URL}</p>;
}// static/scripts/app.ts β served to the browser
console.log("API:", API_URL);Values are baked in when Slick compiles a module (at startup, on hot reload, or when a static asset is served). Change
config.env and restart the server to pick up new values.
A Template defines the structure shared across the pages that use it. Only name and body are required.
interface Template<TData extends object = {}> {
name: string; // Unique template name
favicon?: Render<string>; // Favicon URL (static or dynamic)
styles?: readonly string[]; // Global stylesheet URLs
scripts?: readonly string[]; // Global module script URLs
head?: Render<VNode>; // Extra <head> content
body: Render<VNode>; // Shell markup, must contain an element with id="app"
onrequest?: Middleware<TData>; // Runs before every page using this template, fills req.data
}import type { Template } from "@webtools/slick-server";
export default {
name: "app",
favicon: "/assets/favicon.ico",
styles: ["/styles/reset.css", "/styles/global.css"],
scripts: ["/scripts/global.ts"],
head: (
<>
<meta name="theme-color" content="#000000" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
</>
),
body: (
<>
<header>
<nav>
<a href="/">Home</a>
<a href="/about">About</a>
</nav>
</header>
<main id="app">{/* Page content injected here */}</main>
<footer>
<p>© 2026 My App</p>
</footer>
</>
),
} satisfies Template;Required: the template
bodymust contain exactly one element withid="app", written in the markup itself (not returned by a component). The page'sbodyis injected as the children of that element. Slick throws an explicit error when it is missing.
A Page defines a single route, the template it uses, and its content. Only url and template are required.
interface Page<TTemplate extends Template, TExtra = {}> {
url: string; // Route path (must start with "/", ":params" are supported)
template: TTemplate; // The template object, imported from templates/
title?: Render<string>; // Document <title>
styles?: readonly string[]; // Page-specific stylesheet URLs
scripts?: readonly string[]; // Page-specific module script URLs
head?: Render<VNode>; // Extra <head> content for this page
body?: Render<VNode>; // Page content (injected into the template's #app)
onpost?: RequestListener; // Handler for POST requests to this URL
onrequest?: Middleware; // Runs after the template's onrequest, before rendering
}The second type parameter is optional, see Typed Request Data.
import type { Page } from "@webtools/slick-server";
import app from "../templates/app.tsx";
export default {
url: "/about",
template: app,
title: "About Us",
styles: ["/styles/pages/about.css"],
scripts: ["/scripts/pages/about.ts"],
head: <meta name="description" content="Learn more about us" />,
body: (
<>
<h1>About Us</h1>
<p>We are a great company!</p>
</>
),
onpost: (req, res) => res.json({ success: true }),
onrequest: (req, res) => {
if (!req.headers.has("authorization")) return res.redirect("/login");
},
} satisfies Page<typeof app>;onrequest runs before the page is rendered (and before onpost), first the template's (template.onrequest) then
the page's (page.onrequest). Return a response (e.g. res.redirect(...)) to short-circuit; return nothing to
continue.
onpost is called for POST requests to the page URL, with the middlewares above already applied (req.data is
filled). Its req.body is parsed according to the content type. In client (SPA) mode, POST requests carrying the
X-Slick-Template header are intercepted by Slick to perform SPA navigation, so use onpost for your own form
submissions and API endpoints on standard POST requests. Without onpost, such a request answers 405 in client mode
and 404 otherwise.
Slick uses the typed middleware of @webtools/expressapi. What a template's
onrequest puts in req.data is typed in its own head, body and favicon, and in every page using it.
Declare the data in the template's type parameter:
// templates/app.tsx
import type { Template } from "@webtools/slick-server";
export default {
name: "app",
onrequest: async (req) => {
req.data.user = await findUser(req.cookies.session);
},
body: (req) => (
<>
<header>{req.data.user?.name ?? "Guest"}</header>
<main id="app" />
</>
),
} satisfies Template<{ user: User | null }>;Pages import the template and pass its type: req.data.user is typed everywhere, with no cast.
// pages/dashboard.tsx
import type { Page } from "@webtools/slick-server";
import app from "../templates/app.tsx";
export default {
url: "/dashboard",
template: app,
title: (req) => `Dashboard of ${req.data.user?.name}`,
body: (req) => <h1>Hello {req.data.user?.name}</h1>,
} satisfies Page<typeof app>;A page can add its own data, and type its params and query, with the second type parameter. Its onrequest can read
what the template provided and fills the page data:
// pages/products.tsx
import type { Page } from "@webtools/slick-server";
import app from "../templates/app.tsx";
export default {
url: "/products/:id",
template: app,
onrequest: async (req, res) => {
const product = await findProduct(req.params.id);
if (!product) return res.status(404).json({ success: false });
req.data.product = product;
},
title: (req) => req.data.product.name,
body: (req) => (
<>
<h1>{req.data.product.name}</h1>
<p>Page {req.query.page ?? "1"}, seen by {req.data.user?.name}</p>
</>
),
onpost: (req, res) => res.json({ id: req.params.id }),
} satisfies Page<
typeof app,
{
data: { product: Product };
params: { id: string };
query: { page?: string };
}
>;Page second parameter |
Effect | Default |
|---|---|---|
data |
Added to the template's data in req.data |
Only the template's data |
params |
Type of req.params |
Record<string, string> |
query |
Type of req.query |
Record<string, string | undefined> |
satisfiesdoes not infer type parameters, so the second parameter is written by hand when you use it. The first one is alwaystypeofthe template you set intemplate: using another template is a compile error.
If you would rather not write any type, wrap the module in template() / page(). They return their argument as is,
and TypeScript infers the rest, the same way it does with middleware() in expressapi: the data comes from the
middleware<TAdds, TNeeds>() you give to onrequest, and the params come from the literal url.
// templates/app.tsx
import { middleware, template } from "@webtools/slick-server";
export default template({
name: "app",
onrequest: middleware<{ user: User | null }>(async (req) => {
req.data.user = await findUser(req.cookies.session);
}),
// `req.data.user` is typed here, with nothing written by hand
body: (req) => (
<>
<header>{req.data.user?.name ?? "Guest"}</header>
<main id="app" />
</>
),
});// pages/products.tsx
import { middleware, page, type TemplateData } from "@webtools/slick-server";
import app from "../templates/app.tsx";
export default page({
url: "/products/:id", // req.params is { id: string }
template: app,
// Needs the data of the template (second parameter), adds `product` (first parameter)
onrequest: middleware<{ product: Product }, TemplateData<typeof app>>(async (req, res) => {
const product = await findProduct(req.params.id);
if (!product) return res.status(404).json({ success: false });
req.data.product = product;
}),
title: (req) => req.data.product.name,
body: (req) => <h1>{req.data.product.name} seen by {req.data.user?.name}</h1>,
});Both styles produce the same types and can be mixed in one workspace. A typo in req.data or req.params, or a
template that does not provide what a page reads, is a compile error either way.
title, favicon, head and body accept a Render<T> value: either a static value, or a (possibly async) function
of the request. The request is the typed HttpRequest of expressapi: req.path, req.params, req.query,
req.cookies, req.headers, req.data...
import type { Page } from "@webtools/slick-server";
import app from "../templates/app.tsx";
export default {
url: "/products/:id",
template: app,
title: (req) => `Product ${req.params.id}`,
body: async (req) => {
const product = await fetchProduct(req.params.id);
return (
<>
<h1>{product.name}</h1>
<p>{product.description}</p>
<p>Price: ${product.price}</p>
</>
);
},
} satisfies Page<typeof app>;Everything inside static/ is served from the site root (e.g. static/styles/app.css β /styles/app.css). Paths are
URL-decoded, and anything resolving outside static/ is refused.
| File type | Handling |
|---|---|
.css |
Minified, served as text/css. |
.js / .ts / .jsx / .tsx |
Transpiled (TS/JSX β JS), minified, served as ES modules (text/javascript). |
| Everything else | Served as-is (images, fonts, JSON, etc.). |
Every static response carries an ETag and Cache-Control: no-cache: browsers revalidate and get a 304 when nothing
changed. Compiled CSS/JS/TS results are cached in memory after the first request. With hotReload: true, static assets
are recompiled on every request and never cached.
Static scripts also receive
config.envvalues β see Environment Variables.
Islands are interactive Preact components that are rendered on the server and hydrated on the client. Only islands ship JavaScript to the browser, the rest of the page stays static HTML. This keeps pages fast while still allowing rich interactivity exactly where you need it.
Each island file must export default a Preact function component.
import { useSignal } from "@preact/signals";
interface Props {
start: number;
label?: string;
}
export default function Counter({ start, label = "Counter" }: Props) {
const count = useSignal(start);
return (
<div style="display:flex;align-items:center;gap:12px">
<strong>{label}</strong>
<button type="button" onClick={() => count.value--}>β</button>
<span>{count}</span>
<button type="button" onClick={() => count.value++}>+</button>
</div>
);
}Import the island like any component and use it in your JSX. Slick automatically detects it during SSR, tags it in the HTML, and hydrates it on the client.
import type { Page } from "@webtools/slick-server";
import Counter from "../islands/Counter.tsx";
import app from "../templates/app.tsx";
export default {
url: "/",
template: app,
title: "Home",
body: (
<section>
<h1>Interactive counters</h1>
<Counter start={0} label="A" />
<Counter start={100} label="B" />
</section>
),
} satisfies Page<typeof app>;That's it, no manual registration, no extra script tags. The hydration runtime is injected automatically whenever at least one island exists, and it even hydrates islands added later via SPA navigation.
- Props must be JSON-serializable. Island props are serialized on the server and parsed on the client to re-create
the component. Plain data (numbers, strings, booleans, arrays, objects) works; functions, class instances and
childrendo not survive hydration (childrenis not sent). - An island must render at least one root element. Several root elements are fine (a
Fragmentof elements is hydrated as one island), but an island that renders nothing (null) or only text throws an explicit error: there would be nothing to hydrate. Do not mix text with elements at the root. - Islands can use other islands. An island imported by another island is just a regular component of its parent: only the outermost island is hydrated, and the inner one is part of its bundle.
- During SSR, a Preact hook tags every root element of an island with
data-slick-island(its file name) anddata-slick-island-id(one id per island instance); the first one also getsdata-slick-props(its serialized props). An island rendered inside another island is not tagged. - A tiny hydration script scans the DOM, groups the tagged elements by id, lazily imports each island from
/~islands/<name>, and hydrates it in place with Preact. - A
MutationObserverre-runs hydration when new islands appear (e.g. after SPA navigation). - Each island is bundled individually with esbuild; shared libraries are kept external and loaded once (see below).
Each island is bundled individually with esbuild. By default, esbuild inlines every dependency an island imports
directly into that island's bundle. So if three islands all import chart.js, the entire chart.js library gets
duplicated three times once in each bundle, and the browser downloads and parses it three times.
sharedLibs solves this. A shared library is bundled once, globally, into a standalone vendor bundle served
from /~vendors/<lib>. It is then kept external to every island bundle: instead of inlining the code, islands simply
import it, and a generated <script type="importmap"> redirects that import to the single shared vendor bundle. The
result: each shared library is downloaded, parsed and cached once, no matter how many islands use it.
Without sharedLibs With sharedLibs
βββββββββββββββββ βββββββββββββββ
island A [ chart.js β¨ full copy ] island A ββ
island B [ chart.js β¨ full copy ] island B ββΌββ import map ββ /~vendors/chart.js (one copy)
island C [ chart.js β¨ full copy ] island C ββ
When the islands/ directory exists, the Preact ecosystem is always treated as shared, so these are available to
every island out of the box (you don't need to add them to sharedLibs):
preactpreact/hookspreact/jsx-runtimepreact-root-fragment(the version in yourdeno.jsonimports; required as soon asislands/exists)@preact/signals(only if it is in yourdeno.jsonimports, otherwise it is skipped)
With client: true, @webtools/slick-client is also bundled, from the version in your deno.json imports, and
served from /~vendors/@webtools/slick-client.
sharedLibs is merged with this built-in list, so anything you declare is added on top of the defaults. Without an
islands/ directory and without client, nothing is bundled and no import map is emitted unless you declare
sharedLibs.
Rule of thumb: any third-party library imported by more than one island (or any large library imported even once) should be added to
sharedLibsto avoid duplicating it across bundles.
To use any other npm/JSR package inside an island, add it in two places:
- The
importsof your project'sdeno.json(so Deno and esbuild can resolve it). - The
sharedLibsconfig option (so Slick exposes it through the import map).
Slick resolves imports from the closest deno.json above the workspace directory, and from the Deno workspace it
belongs to. In a monorepo you can therefore declare the libraries in the deno.json of the project that owns the Slick
workspace: there is no need to repeat them in the root deno.json, nor in the one of the directory you run deno from.
// server.ts
const app = new Slick(import.meta.dirname!, {
client: true,
sharedLibs: ["chart.js", "canvas-confetti", "marked"],
});
await app.start();// deno.json
{
"imports": {
"@webtools/slick-server": "jsr:@webtools/slick-server@^0.7.0",
"chart.js": "npm:chart.js@^4.5.1",
"canvas-confetti": "npm:canvas-confetti@^1.9.4",
"marked": "npm:marked@^18.0.5"
},
"compilerOptions": {
"jsxImportSource": "preact",
"jsx": "react-jsx"
}
}You can now import those libraries directly in your islands:
// islands/BarChart.tsx
import { useEffect, useRef } from "preact/hooks";
import { BarController, BarElement, CategoryScale, Chart, LinearScale } from "chart.js";
Chart.register(BarController, BarElement, CategoryScale, LinearScale);
export default function BarChart({ labels, values }: { labels: string[]; values: number[] }) {
const canvas = useRef<HTMLCanvasElement>(null);
useEffect(() => {
if (!canvas.current) return;
const chart = new Chart(canvas.current, {
type: "bar",
data: { labels, datasets: [{ data: values }] },
});
return () => chart.destroy();
}, []);
return <canvas ref={canvas} />;
}When client is set, Slick turns your multi-page app into a single-page app without giving up SSR:
- The first request of every page is fully server-rendered (great for SEO and first paint).
- Subsequent navigations are performed over
fetch: the server returns a JSON payload describing only what changed (the page, and the template only if it differs), and the client patches the DOM.
const app = new Slick(import.meta.dirname!, { client: true });
await app.start();Add @webtools/slick-client to the imports of the project's deno.json. Slick bundles that version; it does not pin
one of its own.
"@webtools/slick-client": "jsr:@webtools/slick-client@^0.3.0"- With
client: true, Slick bundles@webtools/slick-client(the version from that import) and exposes it through the generated import map (/~vendors/@webtools/slick-client). - The client initializes with the current template name and intercepts navigations.
- On navigation, the server is asked (via the
X-Slick-Templateheader) for a JSON payload containing the new page, and the template too, only when it changed, which the client applies in place. The template and page middlewares run as for a normal request. Navigating to an unknown page follows the redirect tor404. stylesandscriptsmarked with aslick-typeattribute let the client swap page/template assets correctly between navigations.- The document keeps a
<div id="root">, a<title>and a<link rel="shortcut icon">(an emptydata:,icon when the template has no favicon): the client relies on them.
For each registered page, Slick exposes:
GET <page.url>server-rendered HTML (or SPA JSON in client mode).HEADis answered too.POST <page.url>routed toonpost, or handled by Slick for SPA navigation when theX-Slick-Templateheader is present.
URLs support :params (typed in req.params); static segments win over params (/users/me before /users/:id).
The following path prefixes are reserved by Slick and should not be used for your own routes or static files:
| Path | Purpose |
|---|---|
/~islands/* |
Per-island client bundles. |
/~vendors/* |
Shared library (vendor) bundles for islands. |
Bundles and static files are served with an ETag. Any GET/HEAD request that matches no page, reserved path, or
static file is redirected to r404.
class Slick {
constructor(workspace: string, config?: Partial<Config>);
load(): Promise<void>;
start(): Promise<Deno.HttpServer<Deno.NetAddr>>;
readonly fetch: (request: Request, info?: Deno.ServeHandlerInfo<Deno.NetAddr>) => Promise<Response>;
}workspaceAbsolute path to your project root. Useimport.meta.dirname!.configA partialConfig; any omitted option falls back to its default.
Validates the workspace, builds the vendor bundles, loads islands, templates and pages and registers all routes, without listening. It throws if:
- the workspace or a required directory (
templates/,static/,pages/) is missing, - a page or template module is invalid (wrong default export, missing
url/name/body, atemplatethat is not a template object), - two pages share a
url, or two templates share aname, - the
r404URL does not match any page, - an island file does not
export defaulta function component.
Calls load(), starts the hot reload watcher if enabled, and listens on config.port. The returned server can be
stopped with await server.shutdown().
const server = await app.start();The request handler. Once load() was called, it can be used without opening a port, which makes the app easy to test:
const app = new Slick(import.meta.dirname!);
await app.load();
const response = await app.fetch(new Request("http://localhost/about"));| Export | Description |
|---|---|
Slick |
The server class. |
Config |
The configuration interface. |
Page |
The shape of a default-exported page module. |
Template |
The shape of a default-exported template module. |
Render |
A static value or a function of the request. |
SlickRequest |
The typed request received by renders and handlers, useful for components. |
TemplateData |
The req.data a template provides: TemplateData<typeof app>. |
UrlParams |
The :params of a literal URL: UrlParams<"/users/:id"> is { id: string }. |
template |
Declares a template and infers its req.data from its onrequest. |
page |
Declares a page and infers its data and params. |
middleware |
Re-export of the typed middleware of expressapi. |
import { type Config, middleware, page, Slick, type SlickRequest, template } from "@webtools/slick-server";Distributed under the MIT License. See LICENSE for more information.