---
title: "Mostly-Static Sites"
description: "Ship near-zero JavaScript for content and marketing sites while keeping islands of interactivity."
canonical_url: "https://nuxt.com/docs/4.x/guide/recipes/mostly-static-sites"
---
# Mostly-Static Sites

> Ship near-zero JavaScript for content and marketing sites while keeping islands of interactivity.

By default, Nuxt server-renders a page and then hydrates all of it. For a page that is 95% static text and images, that hydration JavaScript is mostly overhead: it delays interactivity and competes with your content for bandwidth and CPU, which can hurt metrics like mobile PageSpeed scores.

Nuxt has the pieces to avoid paying that cost where you don't need it. This recipe combines them:

1. **Prerender** your content routes at build time.
2. **Drop the scripts** on those routes with the `noScripts` route rule.
3. Use **server components** for widgets that need server rendering but no interactivity.
4. Use **lazy hydration** on routes that keep their scripts, so interactivity is paid for only when needed.

## Prerender and Drop Scripts

For routes that have no client-side interactivity at all, combine `prerender` and `noScripts` in [`routeRules`](https://nuxt.com/docs/4.x/guide/concepts/rendering#hybrid-rendering):

```ts [nuxt.config.ts]twoslash
export default defineNuxtConfig({
  routeRules: {
    '/blog/**': { prerender: true, noScripts: true },
    '/about': { prerender: true, noScripts: true },
  },
})
```

These routes are rendered to plain HTML at build time, and the rendered pages omit the Nuxt entry scripts, import map, payload script and JS resource hints. CSS is still included. The result is a page with effectively zero JavaScript cost.

<read-more to="https://nuxt.com/docs/4.x/guide/going-further/features#noscripts">

Read more about exactly what `noScripts` omits, and the app-wide `features.noScripts` option.

</read-more>

<important>

A `noScripts` page does not hydrate. Nothing on it is interactive: no `@click` handlers, no `<NuxtLink>` prefetching (links still work as plain `<a>` tags), no client-side navigation away from the page. Only use it on routes where that is what you want.

</important>

## Navigating to and from Script-Free Routes

Because a `noScripts` page has no client-side router, every navigation to or from one is a full-page load. Nuxt handles both directions of that for you:

- Navigating **from** a scripted route **to** a `noScripts` route (with `navigateTo`, `<NuxtLink>`, or `router.push`) triggers a document load, so the target is served script-free by the server rather than rendered by the client-side router.
- Because those routes are never rendered on the client, their page components are dropped from the client bundle when the route rules covering them can be resolved at build time.
- Both scripted and script-free pages emit a declarative [speculation rules](https://developer.mozilla.org/en-US/docs/Web/API/Speculation_Rules_API) tag so supporting browsers prefetch and prerender the target before the user follows a link. This runs no JavaScript on the page itself, and the rules are scoped to your page routes, so a link to a server route such as `/logout` is never speculatively fetched.

With [experimental view transitions](https://nuxt.com/docs/4.x/getting-started/transitions#view-transitions-api-experimental) enabled, these pages also opt into cross-document view transitions, so a full-page navigation animates rather than flashing:

```ts [nuxt.config.ts]twoslash
export default defineNuxtConfig({
  experimental: {
    viewTransition: true,
  },
})
```

## Islands for Server-Rendered Widgets

Some parts of a static page still benefit from being components with real logic behind them: rendered markdown, syntax-highlighted code, CMS content. Use [server components](https://nuxt.com/docs/4.x/guide/concepts/server-components) for these. They render on the server, and their dependencies never reach the client bundle:

```bash [Directory Structure]
-| app/
---| components/
-----| HighlightedMarkdown.server.vue
```

```vue [app/pages/blog/[slug].vue]
<template>
  <article>
    <h1>{{ post.title }}</h1>
    <HighlightedMarkdown :markdown="post.body" />
  </article>
</template>
```

Static islands like this work fine on `noScripts` routes, since their HTML is embedded at render time.

<warning>

Fully interactive widgets inside islands (components marked with [`nuxt-client`](https://nuxt.com/docs/4.x/guide/concepts/server-components#selective-hydration-with-nuxt-client), or interactive island slots) do **not** work on a `noScripts` route. Island teleports are relocated into place by a small inline script before hydration; with scripts disabled, that relocation cannot run and nothing hydrates. If a page needs even one interactive widget, keep scripts on that route and use lazy hydration instead.

</warning>

## Lazy Hydration for the Rest

For routes that need some interactivity, keep scripts enabled but control *when* components hydrate using [lazy hydration](https://nuxt.com/docs/4.x/directory-structure/app/components#delayed-or-lazy-hydration):

```vue [app/pages/index.vue]
<template>
  <div>
    <HeroSection />
    <!-- static content that never needs to become interactive -->
    <LazyTestimonials hydrate-never />
    <!-- hydrate only when scrolled into view -->
    <LazyNewsletterSignup hydrate-on-visible />
    <!-- hydrate when the browser is idle -->
    <LazyCookieBanner hydrate-on-idle />
  </div>
</template>
```

`hydrate-never` is particularly useful on content sites: the component's HTML is server-rendered, but it is never hydrated, so its JavaScript is never executed (though prop changes will still trigger hydration).

## Putting It Together

A typical mostly-static site ends up with:

```ts [nuxt.config.ts]twoslash
export default defineNuxtConfig({
  routeRules: {
    // pure content: prerendered, zero JS
    '/': { prerender: true, noScripts: true },
    '/blog/**': { prerender: true, noScripts: true },
    // pages with an interactive widget: prerendered, scripts kept
    '/contact': { prerender: true },
  },
})
```

- content routes ship plain HTML and CSS, and navigation to them is a speculatively prefetched document load
- islands (`.server.vue` components) handle server-rendered widgets everywhere, keeping heavy dependencies out of the client bundle
- the few interactive routes keep scripts, with `hydrate-on-visible` / `hydrate-on-idle` / `hydrate-never` limiting how much work the browser does up front

<read-more to="https://nuxt.com/docs/4.x/getting-started/prerendering">

Read more about prerendering, including selective prerendering and payload extraction.

</read-more>

<read-more to="https://nuxt.com/docs/4.x/guide/best-practices/performance">

Read more general performance best practices for Nuxt apps.

</read-more>


## Sitemap

See the full [sitemap](https://nuxt.com/sitemap.md) for all pages.
