web-meta-framework-nuxt-2a020a — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited web-meta-framework-nuxt-2a020a (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Quick Guide:useFetchfor an API call in a component,useAsyncDatafor a custom source or several fetches combined — both transfer the server's result to the client so nothing is fetched twice. Server routes live inserver/api/, shared state inuseState, and composables and components are auto-imported. Two facts change the answers below:datais ashallowRef, so replace the object rather than mutating into it (or passdeep: true), anddataanderrordefault toundefinedrather thannull.
Detailed Resources:
useState composables, cookie persistence, server-initialised stateuseFetch versus useAsyncData, and everything elseis options on them; follow examples/data-fetching.md.
server/api/ handlers run under Nitro with their ownutilities; follow examples/server-routes.md.
<critical_requirements>
Fetch through `useFetch` or `useAsyncData` rather than a bare `$fetch` in `<script setup>`. Both carry the server's payload into hydration; a bare $fetch there runs twice, once per environment.
Put API routes in `server/api/` and export a `defineEventHandler()` as the default. The file suffix (.get.ts, .post.ts) is what restricts the method, and the directory is what adds the /api prefix.
Attach middleware and page options through `definePageMeta`. It is a compile-time macro, so its argument has to be statically analysable — no variables, no computed keys.
Set metadata with `useHead` or `useSeoMeta`. Both render server-side and merge with the defaults in nuxt.config.ts, which hand-written <head> markup does not.
Keep `useState` values JSON-serializable. The value is serialised into the HTML and revived on the client, so a function, class instance or Symbol breaks hydration.
Read `to` and `from` inside route middleware rather than calling `useRoute()`. The route object has not been committed yet at that point, so useRoute() answers with the previous route.
</critical_requirements>
Auto-detection: nuxt.config.ts, defineNuxtConfig, useFetch, useAsyncData, useState, useCookie, useRuntimeConfig, useNuxtApp, defineEventHandler, definePageMeta, defineNuxtRouteMiddleware, defineNuxtPlugin, navigateTo, abortNavigation, createError, clearError, showError, useHead, useSeoMeta, NuxtLayout, NuxtPage, NuxtLink, NuxtErrorBoundary, server/api, $fetch, h3, import.meta.client, import.meta.server
Applies to:
pages/, with layouts, dynamic segments and catch-all routesHandled elsewhere:
here
readBody through a schema; which library defines it is aseparate choice
useStatedeliberately does not provide
<philosophy>
Nuxt is a meta-framework for Vue 3: file-based routing, automatic code splitting, server-side rendering, and a data-fetching layer that knows about hydration. It runs on the Nitro server engine, so API routes live in the same project as the pages that call them.
Five ideas explain most of the API surface:
an unfamiliar useX is usually Nuxt's own
pages/, server/, layouts/, middleware/ each mean somethingdata is a shallowRef; deep tracking is a cost Nuxt does not pay bydefault
</philosophy>
<patterns>
File names in pages/ become URL paths, and bracket depth chooses the kind of segment.
| File | URL |
|---|---|
pages/index.vue | / |
pages/about.vue | /about |
pages/blog/[slug].vue | /blog/:slug |
pages/users/[...slug].vue | /users/* |
pages/posts/[[id]].vue | /posts or /posts/:id |
pages/users/[id]/posts.vue | /users/:id/posts |
<!-- pages/blog/[slug].vue -->
<script setup lang="ts">
const route = useRoute();
const { data: post, error } = await useFetch(`/api/posts/${route.params.slug}`);
if (error.value) {
throw createError({ statusCode: 404, statusMessage: "Post not found" });
}
</script>Full code: examples/core.md
useFetch is useAsyncData plus $fetch, with the URL as the cache key.
// URL is the cache key; pass `key` when two calls share a URL
const { data, error, status, refresh, clear } = await useFetch("/api/users");
// Reactive query params, refetched when `page` changes
const page = ref(1);
const { data: users } = await useFetch("/api/users", {
query: { page, limit: 20 },
watch: [page],
});
// A POST the user triggers, rather than one that fires on mount
const { execute, status } = useFetch("/api/users", {
method: "POST",
body: form,
immediate: false,
watch: false,
});Reach for useAsyncData when the source is not a single HTTP call:
const { data } = await useAsyncData("dashboard", async () => {
const [users, stats] = await Promise.all([
$fetch("/api/users"),
$fetch("/api/stats"),
]);
return { users, stats };
});Full code: examples/data-fetching.md
server/api/ prefixes the URL with /api; server/routes/ does not. The file suffix restricts the method.
// server/api/users.get.ts
export default defineEventHandler(async (event) => {
const query = getQuery(event);
return listUsers({ page: Number(query.page) || 1 });
});
// server/api/users.post.ts
export default defineEventHandler(async (event) => {
const body = await readBody(event);
setResponseStatus(event, 201);
return createUser(body);
});Full code: examples/server-routes.md. The full file-to-URL table is in reference.md.
An SSR-friendly ref keyed by a string, so every caller of the same key gets the same state.
// composables/use-user.ts
export function useUser() {
const user = useState<User | null>("user", () => null);
const isLoggedIn = computed(() => user.value !== null);
async function login(credentials: Credentials) {
user.value = await $fetch<User>("/api/auth/login", {
method: "POST",
body: credentials,
});
}
return { user: readonly(user), isLoggedIn, login };
}The initializer runs once per key, so wrapping mutations in the composable is what keeps them in one place. Export readonly(user) so callers change it through login rather than by assignment.
Full code: examples/state-management.md
Middleware runs before navigation commits, which is what makes it the right place for an auth check.
// middleware/auth.ts
export default defineNuxtRouteMiddleware((to, from) => {
const { isLoggedIn } = useUser();
if (!isLoggedIn.value) {
return navigateTo(`/login?redirect=${encodeURIComponent(to.fullPath)}`);
}
});| Type | File | Runs |
|---|---|---|
| Named | middleware/auth.ts | When definePageMeta opts in |
| Global | middleware/auth.global.ts | On every navigation |
| Inline | A function in definePageMeta | For that page only |
Attach with definePageMeta({ middleware: "auth" }), or an array to run several in order.
Full code: examples/middleware.md
A layout wraps pages and renders them through <slot />. layouts/default.vue applies with no opt-in.
<!-- layouts/default.vue -->
<template>
<div>
<header>
<nav><!-- navigation --></nav>
</header>
<main><slot /></main>
</div>
</template>Choose per page with definePageMeta({ layout: "admin" }). definePageMeta is static, so a layout that depends on runtime values needs either setPageLayout("admin") from a script or <NuxtLayout :name="computedLayout"> in app.vue.
Full code: examples/core.md
useSeoMeta takes flat, type-checked property names; pass getters so the values track the data.
<script setup lang="ts">
useSeoMeta({
title: () => post.value?.title ?? "Blog Post",
description: () => post.value?.excerpt ?? "",
ogImage: () => post.value?.coverImage ?? "/default-og.png",
twitterCard: "summary_large_image",
});
</script>useHead is the lower-level form, and app.head in nuxt.config.ts sets the defaults these override.
Full code: examples/core.md
A plugin runs before the Vue app is created — the place to build a configured client once.
// plugins/api.client.ts — .client = browser only, .server = server only, no suffix = both
export default defineNuxtPlugin(() => {
const config = useRuntimeConfig();
const api = $fetch.create({ baseURL: config.public.apiBase });
return { provide: { api } };
});Reach it as useNuxtApp().$api.
Full code: examples/core.md
createError works on both sides of the boundary; where the error surfaces depends on where it is thrown.
// A server route, or a page that checks useFetch's error
throw createError({
statusCode: 404,
statusMessage: "Not found",
data: { id },
});NuxtErrorBoundary with an #error slot isolates one component's failure; a root error.vue catches everything else, and clearError({ redirect: "/" }) is how the user gets out.
Full code: examples/core.md
</patterns>
<red_flags>
Breaks at runtime:
$fetch in <script setup> for initial data — the request fires on the server and again onthe client
useState — it cannot be serialised, so hydration mismatchesuseRoute() inside middleware — the navigation has not committed, so the values are the previousroute's
key on useAsyncData for data that varies — two routes share one cache entryruntimeConfig's private keys — anything under public reaches the browserawait in setup — the component instance is no longer currentSurprising behaviour:
data from useFetch and useAsyncData is a shallowRef, so mutating a nested property changesnothing on screen — replace the object, or pass deep: true
data and error default to undefined, not null, so a === null check never fireskey
useState's initializer runs once per key; later calls return the existing value and ignore thefunction they were given
import.meta.server /import.meta.client
definePageMeta is a macro rather than a function call, so a variable in its argument fails tocompile
NuxtLink needs the external prop for an off-site URLwatch on useFetch only reacts to reactive sources; a plain variable never triggers a refetchawait before useFetch renders the component before the data existsonMounted instead of a composable runs it only in the browser, so theserver renders the empty state and that is what a crawler indexes
</red_flags>
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.