docs / Runtime
Incremental Static Regeneration
Page-, component- and data-level ISR with stale-while-revalidate, page config via export const, and targeted invalidation by path, tag or component.
ISR renders pages, component snippets and data once and serves them cached, refreshing in the background after a TTL. The runtime caches are in-memory per server instance: a fresh entry is served until it expires; an expired entry is served immediately as `stale` while a background revalidation runs (stale-while-revalidate). If the background refresh fails, the stale entry stays in place. Setting `revalidate` to 0 (the default) disables caching for that call entirely.
Page-level ISR
import { pageIsr } from '@vesk/runtime';
export async function Loader() {
return pageIsr(
'homepage',
async () => {
const data = await fetchHomeData();
return {
html: renderHomeHtml(data),
headers: { 'content-type': 'text/html' },
};
},
{ tags: ['home'], revalidate: 60 },
);
}
`pageIsr(path, renderFn, opts?)` — the path is the cache key (trailing slashes collapse to `/`), `renderFn` returns `Promise<{ html, headers? }>`, and the result is `{ html, headers, stale }`. A cached hit serves `html` + `headers` without re-rendering. When expired, the stale HTML is served and `renderFn` runs again in the background.
Page-level config (export const)
export const revalidate = 60;
export const isrTags = ['home'];
component Home() {
<main>
<h1>Home</h1>
<p>This page is cached for 60 seconds and tagged "home".</p>
</main>
}
Declaring `export const revalidate = <seconds>` and `export const isrTags = [...]` in `page.vsk` enables page-level ISR for that route: the build records the TTL and tags, and the production server serves the page through `pageIsr` with the path as key. Configs that arrive as an object (platform manifests) are normalized with `isrConfigToRevalidate(config: unknown)`, which returns the number when given a number or a `{ revalidate }` object, else 0.
Component-level ISR
import { componentIsr } from '@vesk/runtime';
export async function getSidebarHtml() {
return componentIsr(
'sidebar',
async () => {
const links = await getNavLinks();
return `<ul>${links.map((l) => `<li><a href="${l.href}">${l.label}</a></li>`).join('')}</ul>`;
},
{ tags: ['nav'], revalidate: 120 },
);
}
`componentIsr(key, renderFn, opts?)` caches a component snippet by `key` in a dedicated component cache (separate from the data and page caches). `renderFn` may be sync or async and returns the HTML string; `revalidate <= 0` skips caching. Component entries are indexed under tags as `comp:<key>`, so `revalidateTag('nav')` invalidates them along with any matching data/page entries.
Data-level ISR
import { isr } from '@vesk/runtime';
const { data, stale } = await isr(
'product-list',
async () => {
const res = await fetch('https://api.store.com/products');
return res.json();
},
{ tags: ['products'], revalidate: 300 },
);
`isr(key, fetcher, opts?)` caches arbitrary data: `fetcher: () => Promise<unknown>` and the result is `{ data, stale }`. First call populates the cache; fresh calls return cached data with `stale: false`; after the TTL the cached data is returned with `stale: true` while the fetcher re-runs in the background.
export const revalidate = 60;
export const isrTags = ['home'];
component Home() {
<main>
<h1>Home</h1>
<p>This page is cached for 60 seconds and tagged "home".</p>
</main>
}
Wrap the `isr()` call in a `{#server}` block and mark the component `async component` so it only executes during server rendering — the block is stripped from the client bundle. Async data you only need on the server belongs here.
Revalidation
import { revalidatePath, revalidateTag, revalidateComponent, clearIsrCache } from '@vesk/runtime';
await revalidatePath('/products'); // drop keys equal to or starting with /products
await revalidateTag('products'); // drop every entry tagged 'products'
revalidateComponent('sidebar'); // drop just the 'sidebar' component entry
clearIsrCache(); // wipe data + page + component caches and the tag index
| Function | Signature | Purpose |
|---|---|---|
| pageIsr | pageIsr(path, renderFn, { tags?, revalidate? }) | Cache a whole page's HTML + headers |
| componentIsr | componentIsr(key, renderFn, { tags?, revalidate? }) | Cache a component HTML snippet |
| isr | isr(key, fetcher, { tags?, revalidate? }) | Cache arbitrary data |
| isrConfigToRevalidate | isrConfigToRevalidate(config) => number | TTL from a number or { revalidate } config |
| revalidatePath | revalidatePath(path) => Promise<void> | Invalidate by path (exact + prefix) |
| revalidateTag | revalidateTag(tag) => Promise<void> | Invalidate everything tagged |
| revalidateComponent | revalidateComponent(key) => void | Invalidate one component entry |
| clearIsrCache | clearIsrCache() => void | Clear data, page, component caches + tag index |
All opts are `{ tags?: string[], revalidate?: number }` with `revalidate` in seconds. `revalidatePath` normalizes the path (trailing slashes collapse) and deletes the exact key plus every key that starts with it. `revalidateTag` deletes data/page entries and component entries (`comp:<key>`) sharing the tag, then removes the tag index itself.