Skip to content

A staged move

Don't throw away what you know about Next.js.

Most of the concepts you already use have a smaller counterpart here. Move pages one at a time while traffic keeps flowing.

page.tsx → .jsk Client Component → Island ISR → HTML TTL
Next.js JSkelet
app/page.tsx A route entry plus a .jsk page template
generateMetadata() The metadata field your controller returns
layout.tsx A .jsk layout plus a layout context hook
not-found.tsx A not-found hook in the config
redirect() / notFound() The same names, imported from jskelet
next.config redirects/rewrites/headers The same syntax inside jskelet.config.mjs
export const revalidate = 60 A revalidate option on the route
React client component An island element plus a mount function
Suspense-deferred section A fragment endpoint rendered on demand
next/image, next/link The image() and link() helpers (plus optional remote optimize)
opengraph-image / ImageResponse ogImage / ogHandler / ImageResponse (PNG when sharp is installed)
React Context / zustand createStore() for small cross-island state

The full plan and the reasoning behind it live in the migration chapter of the docs.

A safe route

Move the easiest win first.

Two applications can live side by side behind a reverse proxy. Keep the risk small, measure, then take the next page.

  1. 01 Start with your highest-traffic, least interactive page — usually a list or a detail view.
  2. 02 Move the layout and your metadata defaults into hooks.
  3. 03 Reduce client components to islands: most become a button and a fetch.
  4. 04 Split per-user sections into fragment endpoints and mark them no-store.
  5. 05 Measure your TTLs: the cache header and the dev overlay are enough.
  6. 06 Mark dashboards and per-visitor pages with private: true; keep sessions, CSRF and live regions on fragments.
jskelet.config.mjs
export default {
  async redirects() {
    return [
      { source: "/post/:slug", destination: "/blog/:slug", permanent: true },
    ];
  },

  async cache() {
    return {
      html: { "/": 3600, "/blog/:slug": 300 },
      prewarm: { enabled: true, max: 200, concurrency: 4 },
    };
  },

  hooks: {
    metadata() {
      return { titleTemplate: "%s | Site", siteUrl: SITE_URL };
    },
  },
};

A broken config never takes the site down: a failing hook or header rule logs a warning and falls back to the default.

FAQ

Asked most often while migrating

Why is there no React?

Most pages are not interactive. React's cost is fixed: the runtime downloads, a tree is built, hydration runs — and the result is the same HTML the server already produced. JSkelet removes that fixed cost and gives interaction only to the elements that need it.

Can I use TypeScript?

The framework source is plain JavaScript with JSDoc and ships .d.ts for imports from jskelet. Client entries and islands may be .ts (esbuild strips types; manifest keys stay *.js). Server routes, hooks and jskelet.config.mjs stay JavaScript — use checkJs in jsconfig for editor checking there.

The cache lives in process memory. What happens with multiple instances?

Each instance keeps its own L1 cache, so a cold boot still needs a warm-up and TTLs can drift. Early refresh and prewarm close most of that gap; an optional Redis tier shares HTML and broadcasts invalidateHtmlCache() / clearHtmlCache() to every replica over pub/sub. Targeted invalidation itself is first-class either way.

Does it run without a build?

Yes. Without a manifest the asset helper falls back to unhashed paths and the layout simply prints no stylesheet tag. Forgetting the build step gives you an unstyled but working page, not an error.

Are Tailwind, sharp and the icon set required?

None of them. Drop SVGs in icons/ and the sprite builds from that folder alone; otherwise @phosphor-icons/core is the optional peer. Tailwind and sharp skip themselves when missing. A broken config warns and falls back instead of taking the site down.

How does theming work if the HTML is identical for everyone?

It is never decided on the server. Because cached HTML goes out identically to every visitor, per-person choices like theme and language are made in the browser; the theme button on this page is an eager island. Shared cookies across subdomains are a separate, opt-in path when you need a short session id on several hosts.