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.
| Next.js | JSkelet |
|---|---|
| app/page.tsx | A route entry plus an EJS page template |
| generateMetadata() | The metadata field your controller returns |
| layout.tsx | The layout template 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 |
| 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.
- 01 Start with your highest-traffic, least interactive page — usually a list or a detail view.
- 02 Move the layout and your metadata defaults into hooks.
- 03 Reduce client components to islands: most become a button and a fetch.
- 04 Split per-user sections into fragment endpoints and mark them no-store.
- 05 Measure your TTLs: the cache header and the dev overlay are enough.
- 06 Leave the dashboard where it is. This is not the right tool for those pages.
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 itself is plain JavaScript with JSDoc and has no compile step. On the application side you can enable checked JavaScript and keep most of the same type safety without a build; for .ts files you would add a step to the pipeline yourself.
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. Prewarming closes 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. They are optional peer dependencies: if a package is missing, its build step is skipped silently. A broken config behaves the same way — it 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.