Migrating from Nuxt
Silgi has the shape of Nuxt: file-based pages, layouts, route middleware, a server/ folder, useFetch, useState, route rules, modules and layers. The names are often the same. Four things are different from the start:
- You pick the UI library. Nuxt is Vue. Silgi runs React, Vue, Preact, Solid, Svelte or plain HTML, so a Nuxt app moves most easily to
@silgi/vue. Hooks return the library's own values, such as aRefin Vue; see Adapters. - The server is Web standards. There is no h3 or Nitro. A handler gets a
SilgiEventthat holds a WebRequest, and returns a value or aResponse. - Auto-imports are off. Import from
silgi/app,silgi/serverand@silgi/<library>, or turn onimports.autoImport. - Bun builds it.
buildisBun.buildoptions, not Vite or webpack. Vite plugins and Nuxt modules do not run.
The tables list a Nuxt name, what to use in Silgi, and where they differ. none means Silgi has no equivalent today. Coming from another framework? See Next, Remix and React Router or SvelteKit.
Folders and files
| Nuxt | Silgi | Notes |
|---|---|---|
nuxt.config.ts | silgi.config.ts | defineConfig; keys below |
app/ | app/ | the same name; srcDir |
app/pages/ | app/pages/ | same file syntax, with the extension of your library |
app/layouts/ | app/layouts/ | default wraps every page; a layout receives the page as children (Vue: the default slot) |
app/middleware/ | app/middleware/ | *.global.ts, .client.ts and .server.ts work as in Nuxt |
app/plugins/ | app/plugins/ | file name order; .client.ts and .server.ts work |
app/app.vue | app/app.<ext> | optional root component around every layout; it renders Outlet |
app/error.vue | app/error.<ext> | rendered without a layout |
app/app.config.ts | app/app.config.ts | defineAppConfig |
app/components/ | app/components/ | with components: true; the name is the path in PascalCase, as in Nuxt |
app/composables/, app/utils/ | the same | auto-imported only with imports.autoImport |
server/api/, server/routes/ | server/index.ts | a route is a handler with a route and a method: defineHandler({ route: "/api/posts/:id", method: "GET", … }), exported from the door. A file name carries no path and no method |
server/middleware/, server/plugins/ | middleware and plugins of defineServer in server/index.ts | the order written, not file name order |
server/utils/ | server/utils/ | auto-imported only with imports.autoImport |
public/ | public/ | |
layers (extends) | extends | |
shared/ | an alias #shared to shared/ | no auto-imports |
.nuxt/ | .silgi/ | generated |
.output/ | .output/ | |
.env, .env.<envName> | .env in the app root is read by default; --mode <name> reads .env.<name> and .env.<name>.local; --dotenv <file> on dev, build, generate and preview adds another file | set variables in the environment in production. See Environment |
app/assets/ | none | import files from your components |
modules/ | none | list local modules in modules |
content/ | none | |
server/tasks/ | server/tasks/ | the same folder; the file shape differs, see Tasks, database, blob and storage |
server/database/migrations/ (NuxtHub) | server/db/migrations/ | with @silgi/db; a SQL file named <number>_<name>.sql |
.nuxtignore, .nuxtrc | none | a file or folder that starts with _ or . is ignored in pages/ |
The path aliases are ~ and @ for app/, and #shared. There is no ~~ or #imports.
A page that reads its data with a handler:
import { useAsyncData, useHead, useRoute } from "@silgi/react";
import type { PageMeta } from "silgi/app";
import { getPost } from "#server";
export const meta = { layout: "blog", middleware: "auth" } satisfies PageMeta;
export default function Post() {
const { slug } = useRoute().params;
const { data } = useAsyncData(getPost, () => ({ id: slug as string }));
useHead({ title: data?.title ?? "Post" });
return <h1>{data?.title}</h1>;
}
Pages and routing
The file syntax is Nuxt's: [slug], [[slug]], [...slug], (group) folders and parent.vue next to a parent/ folder. Silgi adds [[...path]] for an optional catch-all. See Routing.
| Nuxt | Silgi |
|---|---|
definePageMeta({ layout, middleware }) | export const meta = { layout, middleware } satisfies PageMeta, a plain object that Silgi reads when the page loads; PageMeta comes from silgi/app. middleware takes one name ("auth"), a function or an array of them, as in Nuxt. In Vue it goes in a plain <script lang="ts"> block, in Svelte in <script module lang="ts">. |
declare module "#app" { interface PageMeta { title?: string } } | declare module "silgi/app" { interface SilgiPageMeta { title?: string } }; PageMeta is a type and cannot be augmented, and once SilgiPageMeta has a key, a meta key it does not declare fails. Only the app declares them, never a module |
definePageMeta({ ssr: false }) | meta.ssr = false, or a routeRules entry |
definePageMeta({ name, path, alias, validate, redirect, props, key, keepalive, pageTransition }) | none; the route name comes from the file, and a redirect is a redirect route rule or a load's redirect() |
<NuxtPage> | Outlet, for nested routes. The top-level page is the layout's children. |
<NuxtLayout> | none; layouts apply by name, from meta.layout |
setPageLayout | none |
<NuxtLink to> | Link with to, replace, prefetch (boolean), prefetchOn ("interaction" by default, where Nuxt's is "visibility"), noPrefetch, activeClass, exactActiveClass and ariaCurrentValue. It has no external prop. |
useAsyncData in a page for first-render data | useAsyncData(fn, input) with a handler from #server; x.load.ts for a status or redirect decided before render, see Data |
Composables and components
| Nuxt | Silgi | Differences |
|---|---|---|
useFetch(url, opts) | useFetch | options server, lazy, default, transform, pick, key, watch (Vue also watch: false), getCachedData(key, app, { cause }) and the $fetch ones. The key is made for you unless you pass key; the URL, query and body may be getters. immediate and dedupe as in Nuxt. No deep. |
useAsyncData(key, fn, opts) | useAsyncData(fn, input, opts) for a handler of the app, or useAsyncData(key, fn, opts) | the key of a handler call is made from the handler and the input; a written key may be a getter; the function gets { signal }; options as above |
useLazyFetch, useLazyAsyncData | the same names, or useFetch(url, { lazy: true }) | see useLazyFetch |
result data, pending, status, error, refresh, execute, clear | data, error, status, refresh(), execute(), clear() | status is idle, pending, success or error; pending is true while status is pending |
refreshNuxtData(keys?) | useRouter().refresh(key?) | refreshes the load data and the live async-data entries; a getCachedData sees cause === "refresh:hook", and "refresh:manual" for refresh() and execute(), as in Nuxt 5 (there is no "refresh") |
useNuxtData(key) | useAsyncDataEntry(key) in every adapter, also exported as useNuxtData by Vue | reads the entry of key, whichever component started it, and writes data without changing status; the next run of the handler replaces it |
clearNuxtData(key) | clearAsyncData(app, key) | app is useApp() |
createUseAsyncData, createUseFetch | the same names | |
useState(key, init) | useState(key, init) | the key is optional: the build adds one per call site (build.keyedComposables) |
clearNuxtState(keys?) | clearState(keys?, { reset }) of the adapter, clearState(app, keys?, { reset }) of silgi/app | puts the keys back to the init their useState gave; reset: false sets them to undefined |
useCookie(name, opts) | useCookie(name, opts) | JSON-encoded as Nuxt's, with default, readonly, encode, decode and the options of setCookie; watch only in Vue. refreshCookie(name) is the same function in silgi/app and every adapter |
useRoute() | useRoute() | has path, fullPath, params, query, hash, name, meta, matched (one { name, path, meta } per matched route, parent first) |
useRouter() | useRouter() | navigate (and push), replace, back, forward, go, prefetch, refresh, resolve, beforeEach, afterEach, onError, isReady, getRoutes, hasRoute, currentRoute; no addRoute, removeRoute or options. See Routing |
router.resolve(to) | useRouter().resolve(to) | gives { href, path, name?, params, query, hash } of an app path or a { name, params } location |
navigateTo(to, opts) | navigateTo(to, { replace, external, redirectCode }) of silgi/app and every adapter; useRouter().navigate(to, { replace, scroll, external }) in components; redirect(to, { status, external }) in a load and in route middleware; sendRedirect(event, to, { external }) in server routes | a target on another origin needs external: true |
abortNavigation() | abortNavigation() of silgi/app, or return false, in route middleware | with an argument it throws createError(error) |
defineNuxtRouteMiddleware | defineRouteMiddleware | the arguments are (to, from, { state, app, event }); event is set on the server only. It runs in the browser on a navigation and guards no data: check in the handler |
addRouteMiddleware() | none at run time; modules use addRouteMiddleware of silgi/kit | |
onBeforeRouteLeave, onBeforeRouteUpdate | the same names in every adapter | fn(to, from) answers false, a path, a location or nothing, as router.beforeEach does; it belongs to the page of the component, and a layout's never runs. For a confirm dialog use useBlocker. See Routing |
useHead(input) | useHead(input) | title, titleTemplate, meta, link, script, style, htmlAttrs, bodyAttrs, and key on a tag. No tagPosition, tagPriority or textContent. |
useSeoMeta | useSeoMeta | |
useServerSeoMeta, useHeadSafe | none | |
useRuntimeConfig() | useRuntimeConfig() in components, with the same { public, app } shape; useRuntimeConfig(event) on the server | private keys only through useRuntimeConfig(event). Up to this change useRuntimeConfig() returned the public object itself; write useRuntimeConfig().public.<key> |
useAppConfig, updateAppConfig | useAppConfig(); updateAppConfig(patch) deep-merges and tells the readers | typed from the config once silgi typegen ran: reading a key the config does not have fails tsc |
useError, showError, clearError, createError | the same names | createError takes status, statusText, message, data, fatal and cause; Nuxt's statusCode and statusMessage are accepted as deprecated names for status and statusText and warn once in dev |
<NuxtErrorBoundary> | ErrorBoundary with a fallback that gets { error, reset } | |
<ClientOnly> | ClientOnly with fallback | not in html |
useNuxtApp() | useApp() | state, asyncData, hooks, config, head, cookies, error (browser only), adapter, and what plugins provide |
defineNuxtPlugin | definePlugin | no parallel, dependsOn or enforce; order is by file name |
useNuxtApp().hook, hooks | app.hooks.hook | hooks app:created, app:adapter, app:mounted, page:start, page:finish, page:error, app:error |
onNuxtReady | the app:mounted hook | |
useLoadingIndicator, <NuxtLoadingIndicator> | useNavigation(), which is idle or loading | draw the bar yourself |
preloadRouteComponents, prefetchComponents | useRouter().prefetch(to) and Link's prefetch | |
useRequestEvent, useRequestHeaders, useRequestHeader, useRequestURL, useRequestFetch, useResponseHeader | the same names in every adapter, without the app argument; useResponseHeader(name) returns [value, set]. In a load: ctx.event, ctx.url, ctx.fetch, ctx.setHeaders. In server code outside a component: useEvent() of silgi/server (Server) | on the server they read the request being rendered; in the browser they give undefined, {} and new URL(location.href). See useRequestEvent and the others |
$fetch | $fetch | in silgi/app and silgi/server; FetchError has status, statusText, data, url |
callOnce(key, fn, { mode }) | callOnce(key, fn, { mode }), "render" or "navigation" | returns a promise; the server records the keys it ran in the page. See State |
useHydration(key, get, set) | useHydration(app, key, get, set) of silgi/app, in an app plugin | |
definePayloadReducer(name, reduce), definePayloadReviver(name, revive) | definePayloadReducer(app, name, reduce) and definePayloadReviver(app, name, revive) of silgi/app, in an app plugin; app/transport.ts for a type that handlers send too | see Data |
onPrehydrate, useRouteAnnouncer, useAnnouncer, usePreviewMode, useLayout | none | |
<NuxtIsland>, server components, <NuxtImg>, <NuxtPicture>, <NuxtTime>, <DevOnly>, <NuxtClientFallback>, <Teleport> helpers | none | |
defineNuxtComponent, defineLazyHydrationComponent | none |
A plugin that provides a helper and listens to a hook:
import { definePlugin } from "silgi/app";
export default definePlugin((app) => {
app.hooks.hook("app:mounted", () => console.log("mounted"));
return { provide: { hello: (name: string) => `Hello ${name}` } };
});
Forms and mutations
In a Nuxt app a form is usually a @submit.prevent handler that calls $fetch and then refreshes with refreshNuxtData. Silgi has a path with fewer steps: the mutation is a handler, Form action={fn} posts to it with and without JavaScript, and the page's data is read again by itself. A handler with a route keeps working as an API for other clients. See Forms and login and Handlers.
| Nuxt | Silgi | Differences |
|---|---|---|
$fetch("/api/x", { method: "POST", body }) in a submit handler, then refreshNuxtData() | a handler and <Form action={fn}> | the form posts without a handler of yours; after a success the page's load data and live entries are fetched again, with no call from you. A handler with a route and $fetch still work for an API that other clients call |
readBody and a hand-written check in the POST route | defineHandler({ input, handler }) | the body is decoded (formToObject), validated by any Standard Schema, and a failure is a 400 with data: { issues, values } |
createError({ statusCode: 400, data }) thrown and read from a FetchError | return fail(400, data) and useAction(fn).failure | data is the handler's result on the page; no try and catch in the component |
navigateTo("/done") after the $fetch | return redirect("/done") from the handler | a 303 without JavaScript |
useFetch(url, { method: "POST", immediate: false }) and execute() for a mutation | useAction(fn) | state, failure, pending and error; run(input) calls it |
a pending flag in ref | useFormStatus(), useForm(fn).pending, useAction(fn).pending | pendingRuns(app) lists the running calls with their formData |
a $fetch to server/api/ that every client may call | a handler with a route | typed from the handler; the id is public, so authorize inside |
useState for an optimistic row, rolled back by hand | useOptimistic(value, reduce) | the row leaves when the data is fresh or the call fails |
readMultipartFormData for uploads | form: { maxFileSize, maxFiles, onFile } on the handler | limits answer 413 |
refreshNuxtData | useRouter().refresh(), refresh(event, fn, input) on the server | refresh(event, fn, input) sends the next read with the answer |
A login that $fetches and then sets the shared state becomes a login handler. The Forms and login guide builds one.
Config keys
| Nuxt | Silgi | Differences |
|---|---|---|
ssr | ssr | |
| — | framework | the adapter: react, vue, preact, solid, svelte or html |
app.baseURL, app.buildAssetsDir, app.head, app.rootId | the same | app.head.titleTemplate is a string here; useHead also takes a function |
app.pageTransition, app.layoutTransition, app.keepalive, app.cdnURL | none | |
runtimeConfig | runtimeConfig | public goes to the browser. SILGI_<KEY> and SILGI_PUBLIC_<KEY> override a declared top-level key (the key in upper snake case, apiBase is API_BASE); there is no NUXT_ prefix and no nested keys, so override a whole object with JSON. |
appConfig | appConfig | merged with app/app.config.ts |
routeRules | routeRules | keys prerender, ssr, swr, cache, headers, redirect, cors, proxy and noindex. No isr, appMiddleware or robots. |
modules | modules | Silgi modules only |
extends | extends | |
alias | alias | ~, @ and #shared by default; #server is the door, server/index.ts, not an alias |
hooks | hooks | build hooks, listed in Modules; the names differ from Nuxt's |
imports | imports | autoImport (off) and dirs |
components | components | a boolean |
devServer | devServer | port, host, https |
nitro.preset | preset | bun, node, static, cloudflare, vercel, netlify |
nitro.storage | storage | memory, fs, sqlite, redis, s3, cloudflare-kv, cloudflare-r2, cloudflare-cache, netlify-blobs, vercel-blob, vercel-runtime-cache, azure-blob, github, planetscale, overlay, null, http and db; see Storage |
nitro.devStorage | devStorage | replaces the mount of the same name under silgi dev and while prerendering |
nitro.serverAssets | server/assets/ | read with useStorage("assets:server"); a folder is not configured, see Server assets |
nitro.scheduledTasks | scheduledTasks | cron expressions to task names, always in UTC |
nitro.database, nitro.devDatabase | db.connections, and dev of a connection | with @silgi/db |
nitro.experimental.tasks, nitro.experimental.database | tasks: none to set; database: bun add @silgi/db and add it to modules | tasks are on when a file is in server/tasks/; the database needs the module |
nitro.prerender | prerender | routes, crawl, failOnError, concurrency, ignore |
A GraphQL module such as nitro-graphql | server.graphql | Silgi has its own endpoint, at /api/graphql by default, with SDL in server/graphql/ and resolvers in *.resolver.ts files. The module's own options and plugins do not carry over. See GraphQL. |
nitro (the rest), serverHandlers, devServerHandlers | server.trustProxy, server.maxBodySize; handlers through addServer in a module | |
vite, webpack, builder, build, esbuild, postcss, optimization | build | minify, sourcemap, define, plugins (Bun plugins), external, compressPublic |
sourcemap | build.sourcemap | "none", "linked" or "external" |
srcDir, serverDir, buildDir, rootDir, dev | the same | buildDir is .silgi; outputDir and publicDir are also keys |
plugins | none in the config; modules use addAppPlugin | |
css | none | import the CSS file from a layout or component |
typescript.strict | typescript.strict | default true, set in all the generated tsconfigs |
typescript.typeCheck | build.typecheck, or silgi build --typecheck | checks before bundling and stops the build on an error. silgi dev does not check types; run silgi typecheck |
typescript.tsConfig | typescript.tsConfig | the object is merged into the compilerOptions of each generated file; there is no compilerOptions key around it, and no include or exclude. A module changes one file in the prepare:types hook. The tsconfigs are app, server, shared and node |
experimental.typedPages | none to set; typescript.typedPaths | route names, params and link paths are typed after silgi typegen, with no flag. typedPaths: false turns the path check off |
$development, $production, $test, $env | the same | see below |
experimental.envExpansion | experimental.envExpansion | ${NAME} in the strings of runtimeConfig. See Environment |
typesDir | none | silgi typegen writes .silgi/ |
pages, router, dir, extensions, ignore, watch, watchers, experimental | none | |
spaLoadingTemplate, unhead, vue, features, future, compatibilityDate, devtools, telemetry, logLevel, debug, test, theme, analyzeDir, appId, buildId | none | --analyze on build prints the largest modules per client chunk |
import { defineConfig } from "silgi";
export default defineConfig({
framework: "vue",
ssr: true,
app: { baseURL: "/", head: { titleTemplate: "%s · Shop" } },
runtimeConfig: { apiSecret: "", public: { apiBase: "/api" } },
routeRules: { "/blog/**": { prerender: true }, "/admin/**": { ssr: false } },
storage: { cache: { driver: "fs", base: ".data/cache" } },
preset: "node",
imports: { autoImport: true },
components: true,
});
Tasks, database, blob and storage
Nitro's tasks and database and NuxtHub's blob, database and key-value storage have Silgi counterparts. The names are close and the shapes are not, so read each row.
| Nuxt | Silgi | Differences |
|---|---|---|
defineTask({ meta: { name, description }, run({ payload, name, context }) }) | defineTask({ description, input, run(payload, ctx) }) in server/tasks/** | the name is the path (server/tasks/db/seed.ts is db:seed); run gets the payload, then a context with name, scheduledTime, cron, signal, waitUntil and log; input is a Standard Schema; it returns the result itself, not { result } |
runTask(name, { payload, context }) giving { result } | runTask(name, payload) giving the result | typed from the task files; a call with the same name and payload joins the pending run |
scheduledTasks: { "0 9 * * 1": "report" } | scheduledTasks | the same shape; always UTC; Silgi writes the weekday the way each platform reads it. devServer.tasks runs them under silgi dev |
nitro task list, nitro task run | silgi task list, silgi task run <name> --payload '<json>' | --prod runs with the production config |
useDatabase(name) (db0) | useDatabase(name) from @silgi/db/server | db.sql`…` gives the rows, not { rows }; first, run, exec, batch and transaction; ${value} is always a bound parameter |
nitro.database, nitro.devDatabase | db.connections.<name> and its dev | devDatabase is the connection's own dev, and also covers the prerender |
hubDatabase() (D1-style prepare().bind().all()) | useDatabase().sql on a d1 connection | write the query as a tagged template; batch([...]) is D1's batch; transaction is refused on D1 |
NuxtHub's server/database/migrations/*.sql | server/db/migrations/<number>_<name>.sql | applied at silgi dev start, silgi db migrate, or at build or start by db.migrate; D1 migrations are applied by Wrangler |
Drizzle through NuxtHub's hub:db | your own drizzle-orm | see the Drizzle recipe |
hubBlob() | useBlob() from @silgi/blob/server | put, get, head, list, serve and presign are there; hubBlob().del is useBlob().delete; dir and httpEtag have no counterpart (the option is base, and etag is on the object) |
hubBlob().handleUpload, useUpload | a handler whose input holds a File, with ensureBlob; handleMultipartUpload and useMultipartUpload for big files | see Uploads |
hubKV(), hubCache() | useStorage("kv"), useStorage("cache") | see KV and cache as mounts |
useStorage(base) with a redis or mongodb driver | storage: { name: { driver: "redis" } }; a path driver for MongoDB | see MongoDB |
Server utilities
The Nitro and h3 names on the left, with the Silgi name from silgi/server. See Server.
| Nuxt | Silgi | Differences |
|---|---|---|
defineEventHandler | defineHandler({ route, method, handler }), with raw: true to read the event yourself | handler(input, { event }); the path and the query (and the body, unless raw) arrive as input. onRequest and onBeforeResponse are defineMiddleware |
defineCachedEventHandler | cache on a GET handler; or defineCachedHandler | cache takes maxAge, swr, staleMaxAge, varies, getKey, base, maxResolveTime |
defineCachedFunction | defineCachedFunction | |
defineNitroPlugin | definePlugin, listed in plugins of defineServer | hooks request, beforeResponse, afterResponse, error, render:html, close |
defineWebSocketHandler | defineHandler({ route, websocket }) | hooks open, message, close, error with a peer; the bun preset only; a handshake from another origin gets 403 unless origin in websocket allows it (["https://app.example"], a function or "any") |
useRuntimeConfig(event) | useRuntimeConfig(event?) | the event is optional; on cloudflare, vercel and netlify a call without it throws before the first request |
useStorage(base) | useStorage(mount) | a mount named in storage; unnamed ones are in memory |
useSession | useSession(event, { password }) | sealed cookie; id, data, update, clear, and regenerate({ data }), which you call on login |
getQuery, getValidatedQuery | the same | |
getRouterParam, getRouterParams | the same | getValidatedRouterParams: none |
readBody, readValidatedBody | the same | 413 above server.maxBodySize |
readRawBody | readBodyBytes | h3's returns a string; Silgi's returns Uint8Array | undefined, so decode it with TextDecoder, or use readBody for text bodies. readRawBody stays as a deprecated alias through 1.x |
readMultipartFormData | readFormData, readMultipart | return FormData; readMultipart(event, { maxFileSize, maxFiles, onFile }) streams the files and enforces limits |
getRequestHeader, getRequestHeaders | the same | getHeader and setStatus stay as deprecated aliases through 1.x |
setResponseHeader, setResponseHeaders, appendResponseHeader | setHeader, setHeaders, appendHeader | |
setResponseStatus | the same | |
getCookie, setCookie, deleteCookie | the same | |
sendRedirect | sendRedirect | returns a Response: return it |
createError, isError, sendError | createError, isSilgiError; throw instead of sendError | |
sendStream, createEventStream | createEventStream(event) with push, close, send; or return a ReadableStream | |
sendNoContent | return null | |
getRequestURL, getRequestIP | the same | getRequestURL uses forwarded host and proto only with server.trustProxy; getRequestIP(event, { xForwardedFor: true }) reads x-forwarded-for from the right (trustedHops, default 1) |
handleCors | handleCors | returns a Response for a preflight, else undefined |
getMethod, event.method | event.method | |
event.node.req, event.node.res | event.request, event.response | Web Request; the response object holds status and headers |
event.context, event.waitUntil | the same | |
proxyRequest, sendProxy | the proxy route rule | |
the nuxt-security module | server.securityHeaders, server.originCheck | Nuxt adds no such headers or check without the nuxt-security module; Silgi sends X-Content-Type-Options, Referrer-Policy and X-Frame-Options and refuses cross-origin form posts unless server.securityHeaders or server.originCheck say otherwise |
consola calls in server code, nitro-opentelemetry or a vendor SDK | event.log and useLogger(), server.tracing, server.metrics | Nitro has no built-in logger or tracing. Silgi has a leveled logger with redaction, a request id and request log, OpenTelemetry tracing with an OTLP exporter, Prometheus metrics and Server-Timing; see Observability |
useNitroApp, defineRenderHandler, defineNitroErrorHandler, fromNodeMiddleware, serveStatic | none | |
defineTask, runTask, useDatabase | defineTask, runTask, useDatabase | not the same shapes; see below |
A route that reads the event, as a Nitro handler does:
import { createError, defineHandler, getQuery, getRouterParam, setResponseStatus, useRuntimeConfig } from "silgi/server";
export const getPost = defineHandler({
route: "/api/posts/:id",
method: "GET",
raw: true,
handler(_, { event }) {
const { draft } = getQuery(event);
const id = getRouterParam(event, "id");
if (!id) throw createError({ status: 400, message: "id is required" });
setResponseStatus(event, 200);
return { id, draft: draft === "1", site: useRuntimeConfig(event).public };
},
});
Modules
defineNuxtModule is defineModule: meta.name, meta.configKey (defaulting to meta.name), defaults (an object or a function of the context), hooks and setup(options, silgi). Options merge as in Nuxt, inline options over the config key over defaults. A module types its options by adding to SilgiModuleOptions with declare module "silgi", so defineConfig checks them. See Modules.
| Nuxt kit | silgi/kit |
|---|---|
defineNuxtModule | defineModule |
useNuxt() | useSilgi(), or the second argument of setup |
createResolver | createResolver |
useLogger | useLogger; logger is its deprecated alias |
addPlugin | addAppPlugin |
addServerPlugin, addServerHandler | addServer(file, { root? }): one door with the module's handlers, plugins and middleware |
addImports, addImportsDir, addServerImports | addImports; it applies to both app and server files. addImportsDir: none |
addTemplate, addTypeTemplate | the same |
extendPages | extendPages; Silgi also has addPage |
addLayout, addRouteMiddleware | the same |
addVitePlugin, addWebpackPlugin | addBunPlugin |
updateRuntimeConfig | updateRuntimeConfig |
addPrerenderRoutes | the prerender:routes hook |
addComponent, addComponentsDir, installModule, addDevServerHandler, extendViteConfig, addBuildPlugin, defineNuxtConfigSchema | none |
Internationalization
@nuxtjs/i18n is @silgi/i18n, a first-party module with the same route names (about___fr) and a smaller option set. Messages use an ICU MessageFormat subset, not vue-i18n's syntax: rewrite a | b plurals as {count, plural, one {a} other {b}} and linked messages (@:key) as plain text. See Internationalization.
@nuxtjs/i18n | Silgi | Differences |
|---|---|---|
modules: ["@nuxtjs/i18n"] and the i18n key | modules: ["@silgi/i18n"] and the i18n key | a Vue-only module becomes one for all six libraries |
locales: [{ code, iso, dir, name, file }] | i18n.locales | { code, language, dir, name }: iso is language; file is none, the file is always locales/<code>.json |
defaultLocale | i18n.defaultLocale | |
strategy | i18n.strategy | prefix_except_default (the default) and prefix. no_prefix and prefix_and_default: not in v1 |
detectBrowserLanguage | i18n.detect.cookie, i18n.detect.redirect | the cookie and Accept-Language choose where / redirects, and nothing else redirects. cookieKey is detect.cookie; detectBrowserLanguage: false is detect: { redirect: false, cookie: false }. alwaysRedirect, redirectOn, useCookie and fallbackLocale: not in v1 |
customRoutes: "config" and pages | i18n.pages | { about: { fr: "/a-propos" } } by route name; the params must stay the same |
customRoutes: "page", defineI18nRoute | none | paths are config, not page meta |
langDir | none | the folder is locales/ in the app root and in each layer, JSON only; every locale is one lazy chunk |
lazy, bundle, compilation, vueI18n | none | messages are compiled at build time |
baseUrl | i18n.baseUrl | a string origin, or { runtimeConfig: "siteUrl" } for a public runtime config key |
useI18n().t, $t | useI18n().t of @silgi/i18n/<library> | typed by typegen: a wrong key or argument fails tsc. Arguments are an object, t("home.hello", { name }) |
useI18n().n, d, $n, $d | n, d | Intl options; d() uses i18n.timeZone |
useI18n().locale, locales | useLocale() | { code, language, dir, name, locales }; in Vue a ComputedRef, in Solid an Accessor |
useI18n().setLocale | none | switching is a navigation: Link to={switchLocalePath("fr")} |
useLocalePath() | useI18n().localePath(to) | to is a path or { name, params, query, hash } with the base route name |
useSwitchLocalePath() | useI18n().switchLocalePath(code) | the argument is the code and the current route is bound |
useLocaleHead(), $localeHead | none to call | the module writes lang, dir, hreflang, canonical and og:locale itself |
<NuxtLinkLocale> | Link with localePath(to) | Link is not localized |
differentDomains, domain | none | not in v1 |
skipSettingLocaleOnNavigate, customBlocks, experimental, parallelPlugin, types, debug | none |
Types and environment
| Nuxt | Silgi | Differences |
|---|---|---|
useRoute("blog-slug") | useRoute("blog-slug"), or useRoute<Route.Name>() | types params of that route and never throws. Before silgi typegen the params are loose. See TypeScript |
typed navigateTo and NuxtLink paths (typedPages) | Link to, navigate, redirect and navigateTo | a literal path that no page, route or public file can match is a type error; { external: true } is needed for an external target |
$fetch typed from server/api/ | $fetch typed from a handler's input and result | body and query are checked for a literal URL; a Date in the result is a string (JsonSerialized) |
useAsyncData data typed | useData(), useAsyncData(fn, input) | Serialized<T>: what the payload carries, so a Date stays a Date |
definePageMeta({ layout }) with typed names | meta.layout and meta.middleware | typed from the layouts and the named middleware; see TypeScript |
useState with a typed key | defineState<T>(key, init?) and useState(key) | the type comes from the key |
useRuntimeConfig().public | useRuntimeConfig().public | and #silgi/env/public, #silgi/env/private for values that app/env.ts declares and validates |
import.meta.dev, import.meta.client, import.meta.server, import.meta.prerender | the same | import.meta.prerender is true while silgi generate renders |
process.env.NUXT_PUBLIC_* | SILGI_PUBLIC_<KEY> | read when the server starts |
$development applies to silgi dev, $test when NODE_ENV is test, and $production to every other command. $env.<name> applies for --mode <name>, over the block of the kind:
import { defineConfig } from "silgi";
export default defineConfig({
framework: "react",
build: { minify: true },
runtimeConfig: { public: { apiBase: "/api" } },
$development: { runtimeConfig: { public: { apiBase: "http://localhost:4000/api" } } },
$test: { storage: { default: { driver: "memory" } } },
$production: { build: { sourcemap: "linked" } },
$env: {
staging: { runtimeConfig: { public: { apiBase: "https://staging.example.com/api" } } },
},
});
Layers apply their own blocks to their own keys. $env applies by --mode where Nuxt applies it by --envName. A block cannot hold rootDir, extends, dev or another block.
From React Router and Vite
| React Router, Vite | Silgi | Differences |
|---|---|---|
import type { Route } from "./+types/post" | import type { Route } from "./+types/[slug]" | written by silgi typegen beside the page; the same +types spelling. Route.Params, Route.Name, Route.LoadContext, Route.LoadData, Route.Errors |
Route.LoaderArgs, Route.ComponentProps | Route.LoadContext; useData<Route.Name>(), useRoute<Route.Name>() | no adapter passes params or data as props, so there is no ComponentProps |
Route.ActionArgs | none | a handler's input is typed; see Handlers |
href("/blog/:slug", { slug }) | resolveRoute("blog-slug", { slug }) from silgi/app, or a { name, params } location, or a literal path | Link to and navigate check a literal path against the pages |
import.meta.env.MODE, DEV, PROD, SSR, BASE_URL | the same | replaced in both bundles. A key that is not listed is undefined, on the server too |
import.meta.env.VITE_*, envPrefix | import.meta.env.SILGI_PUBLIC_*, build.envPrefix | the default prefix is SILGI_PUBLIC_. A prefix that would expose Silgi's private SILGI_ variables is refused |
vite build --mode staging, .env.staging | silgi build --mode staging, .env.staging and .env.staging.local | MODE is staging and PROD is true. NODE_ENV is not changed by --mode |
envDir | none | the mode files are read from the app root, before the config file; --dotenv <file> adds a shared file |
loadEnv(mode, dir, prefix) | none | app/env.ts with defineEnv declares, validates and types the variables |
define: { __X__: … } | build.define |
Commands
| Nuxt | Silgi |
|---|---|
nuxt dev | silgi dev |
nuxt build | silgi build |
nuxt generate | silgi generate |
nuxt preview | silgi preview; it serves bun, node and static builds, and prints the platform's command for the others |
nuxt init | silgi init <dir> --framework <id> |
nuxt prepare | silgi typegen |
nuxt typecheck | silgi typecheck: writes the types, then runs tsc -b, vue-tsc -b or svelte-check from the app's node_modules; it prints Type errors: <n> and exits 1 |
--envName <name> on any command | --mode <name> |
nuxt analyze | silgi build --analyze, a text report |
nuxt info | silgi info |
nuxt add, upgrade, cleanup, devtools, module | none |