---
title: "skill by agenticluke · skilld"
canonical_url: "https://skilld.dev/gh/agenticluke/nuxt-safe-ssr-plus"
meta:
  description: "Build and fix Nuxt 4 apps with safe SSR, matching hydration, route rules, lazy loading, and data fetching with useFetch and useAsyncData. Use for page data,… From agenticluke/nuxt-safe-ssr-plus."
  "og:description": "Build and fix Nuxt 4 apps with safe SSR, matching hydration, route rules, lazy loading, and data fetching with useFetch and useAsyncData. Use for page data,… From agenticluke/nuxt-safe-ssr-plus."
  "og:title": "skill by agenticluke"
  "twitter:description": "Build and fix Nuxt 4 apps with safe SSR, matching hydration, route rules, lazy loading, and data fetching with useFetch and useAsyncData. Use for page data,… From agenticluke/nuxt-safe-ssr-plus."
  "twitter:title": "skill by agenticluke"
---

`

[All skills](https://skilld.dev/skills)

[![agenticluke avatar](https://skilld.dev/_img/avatar?url=https%3A%2F%2Fgithub.com%2Fagenticluke.png%3Fsize%3D96)](https://skilld.dev/gh/agenticluke)

# **/skill**

[@549e337](https://github.com/agenticluke/nuxt-safe-ssr-plus/commit/549e3370e088376e9c737ed48b171a7d9d97deef "Your agent reads SKILL.md at commit 549e337")

by [agenticluke](https://skilld.dev/gh/agenticluke)· [agenticluke](https://skilld.dev/gh/agenticluke)/ [nuxt-safe-ssr-plus](https://skilld.dev/gh/agenticluke/nuxt-safe-ssr-plus)

Build and fix Nuxt 4 apps with safe SSR, matching hydration, route rules, lazy loading, and data fetching with useFetch and useAsyncData. Use for page data, dynamic routes, browser-only code, caching, and speed work.

- 1 file
- 6.8 KB
- Updated 2 weeks ago
- [GitHub](https://github.com/agenticluke/nuxt-safe-ssr-plus/blob/549e3370e088376e9c737ed48b171a7d9d97deef/skill/SKILL.md "View SKILL.md on GitHub")

## SKILL.md

6.8 KB

**≈56** tokens always: the name and description. **≈1.7k** when used: this file.

## Nuxt 4 Patterns

### Credit

**Original skill by ECC.**

Use this skill when you build or fix a Nuxt 4 app.

### Use This Skill For

- Server HTML that does not match the first browser render
- SSR and browser-only code
- Static pages, SWR, ISR, and route rules
- Slow pages or large page data
- Lazy loading and lazy hydration
- `useFetch`, `useAsyncData`, and `$fetch`
- Bugs tied to route params or middleware

### Safe Hydration

The server and browser must make the same HTML on the first render.

- Do not put `Date.now()` or `Math.random()` in server-rendered HTML.
- Do not read `window`, `document`, storage, or browser size during SSR.
- Move browser-only work into `onMounted()`.
- You may also use `import.meta.client`, `<ClientOnly>`, or a `.client.vue` file.
- Use Nuxt's `useRoute()`. Do not import `useRoute` from `vue-router`.
- Do not use `route.fullPath` to build server HTML. The URL hash only exists in the browser.
- Do not hide a mismatch with `ssr: false` unless the whole route needs the browser.
- Use one time zone and locale when text must match on both sides.
- Check for bad HTML, such as a `<div>` inside a `<p>`. The browser may change it.
- Give lists stable and unique `:key` values.
- Do not show user data from browser storage during the first render. Read it after mount.

### Fetch Data Safely

Use `await useFetch()` for a simple API read in a page or component. Nuxt can pass the server result to the browser. This helps stop a second request.

```
const { data, status, error } = await useFetch('/api/products')
```

Use `useAsyncData()` when you need:

- More than one data source
- A custom data function
- A stable cache key
- Data from a library other than `$fetch`

```
const route = useRoute()

const slug = computed(() => String(route.params.slug))

const { data: article, status, error, refresh } = await useAsyncData(
  () => `article:${slug.value}`,
  () => $fetch(`/api/articles/${slug.value}`),
  {
    watch: [slug],
  },
)
```

Follow these rules:

- Make each cache key stable and unique.
- Put route params in the key when the data depends on them.
- Add `watch` when the same page can stay open while a param changes.
- Return real data from the handler. Do not return `undefined`.
- Keep the handler free of side effects. Do not send mail, write data, or track events in it.
- Use `$fetch()` for form saves, button actions, and other user actions.
- Do not use top-level `$fetch()` for page data that must come from SSR.
- Use `lazy: true`, `useLazyFetch()`, or `useLazyAsyncData()` for data that can load later.
- Show a loading state when `status === 'pending'`.
- Show a useful error state when a request fails.
- Use `server: false` only when the first page view and search tools do not need the data.
- Use `pick` to keep only fields the page needs.
- Use shallow data when deep updates are not needed.
- Use `runtimeConfig` for private values. Never send private keys to the browser.
- Read request headers and cookies with Nuxt server tools when SSR needs them.

### Route Rules

Set page render and cache rules in `nuxt.config.ts`.

```
export default defineNuxtConfig({
  routeRules: {
    '/': { prerender: true },
    '/products/**': { swr: 3600 },
    '/blog/**': { isr: true },
    '/admin/**': { ssr: false },
    '/api/**': { cache: { maxAge: 3600 } },
  },
})
```

- `prerender` makes HTML at build time.
- `swr` serves saved content, then updates it in the back end.
- `isr` rebuilds saved pages on hosts that support it.
- `ssr: false` renders the route in the browser.
- `cache` sets rules for server replies.
- `redirect` sends the user to another path.

Pick rules by route group. A shop page, blog, admin page, and API may each need a different rule.

Check these edge cases:

- Do not cache private or user-only data in a shared cache.
- Make sure the host supports the chosen ISR rules.
- Give content a clear refresh time.
- Keep admin routes out of static page builds when they need live user data.
- Test route patterns so broad rules do not catch the wrong pages.

### Lazy Loading

Nuxt already splits page code by route. Keep route boundaries clear before you split small components.

Use the `Lazy` name prefix for parts that are not needed at once.

```
<template>
  <LazyRecommendations v-if="showRecommendations" />
  <LazyProductGallery hydrate-on-visible />
</template>
```

- Use `v-if` so a lazy part is not loaded before it is needed.
- Use lazy hydration for parts below the first screen.
- Use `hydrate-on-visible` for a part that can wait until it is seen.
- Use `defineLazyHydrationComponent()` for a custom visible or idle rule.
- Lazy hydration works with Vue single-file components.
- A new prop can cause a lazy component to hydrate at once.
- Use `<NuxtLink>` for links inside the app.
- Keep key content and main actions out of delayed parts.
- Test that delayed controls still work with a keyboard.

### Concrete Example

This page loads a product on the server. Reviews load later in the browser.

```
<script setup lang="ts">
const route = useRoute()
const id = computed(() => String(route.params.id))

const {
  data: product,
  status: productStatus,
  error: productError,
} = await useAsyncData(
  () => `product:${id.value}`,
  () => $fetch(`/api/products/${id.value}`),
  {
    watch: [id],
    pick: ['id', 'name', 'price'],
  },
)

const {
  data: reviews,
  status: reviewStatus,
  error: reviewError,
} = await useFetch(
  () => `/api/products/${id.value}/reviews`,
  {
    lazy: true,
    server: false,
    watch: [id],
  },
)
</script>

<template>
  <main>
    <p v-if="productStatus === 'pending'">Loading product...</p>
    <p v-else-if="productError">Could not load this product.</p>

    <article v-else-if="product">
      <h1>{{ product.name }}</h1>
      <p>{{ product.price }}</p>
    </article>

    <section>
      <h2>Reviews</h2>
      <p v-if="reviewStatus === 'pending'">Loading reviews...</p>
      <p v-else-if="reviewError">Could not load reviews.</p>
      <ul v-else>
        <li v-for="review in reviews" :key="review.id">
          {{ review.text }}
        </li>
      </ul>
    </section>
  </main>
</template>
```

### Review List

Before you finish, check that:

- The server and first browser render make the same HTML.
- Browser-only code runs only in the browser.
- Page reads use `useFetch` or `useAsyncData`.
- Data keys include values that change the result.
- Loading, empty, and error states are shown.
- Private data is not put in a shared cache.
- Route rules match search and freshness needs.
- Heavy parts load or hydrate later.
- Main text and actions are ready at first load.
- Dynamic route changes load the right data.

Source: [SKILL.md on GitHub](https://github.com/agenticluke/nuxt-safe-ssr-plus/blob/549e3370e088376e9c737ed48b171a7d9d97deef/skill/SKILL.md)

## Third-party checks

No third-party reports yet.

## Provenance

[Signed by skilld at 549e337.](https://github.com/agenticluke/nuxt-safe-ssr-plus/commit/549e3370e088376e9c737ed48b171a7d9d97deef "549e3370e088376e9c737ed48b171a7d9d97deef") This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub 2 weeks ago.

Activeupdated 2 weeks ago

## Capability

<dl>

<dt>origin</dt>
<dd>ECC</dd>

</dl>

## README badge

![README badge for agenticluke/nuxt-safe-ssr-plus](https://skilld.dev/b/agenticluke/nuxt-safe-ssr-plus?theme=light&label=0)

## Related skills

-
-
-
-
-
-