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

app/loaders/home.ts
									
										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)

app/page.vsk
										
											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

app/loaders/sidebar.ts
									
										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

app/loaders/products.ts
									
										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.

app/page.vsk
										
											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

app/actions/publish.ts
									
										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.