Adapters

One core runs every library. An adapter, @silgi/<library>, renders pages and layouts with its library and hands you the hooks. Routing, loaders, head, state and the server are the same everywhere. Pick the library in silgi.config.ts with framework, or install one @silgi/<library> and Silgi detects it. One app uses one library.

LibraryPackageInstall next to itFilesServer rendering
React 19@silgi/reactreact, react-dom.tsxstreams
Vue 3.5@silgi/vuevue.vuestring, no streaming
Preact 11@silgi/preactpreact, preact-render-to-string.tsxstring, no streaming
Svelte 5@silgi/sveltesvelte.sveltestring, no streaming
Solid 1.9@silgi/solidsolid-js, babel-preset-solid, @babel/core, @babel/preset-typescript.tsxstreams
html@silgi/htmlnothing.tsstring; no client library

Silgi has no dependencies of its own. The library and its compiler are peers that your app installs.

The same page in each library

A page that reads loader data, sets the title and keeps a counter in shared state. Every library has useData, useHead and useState; the form of the value differs.

React:

import { useData, useHead, useState } from "@silgi/react";

export default function Post() {
  const { title } = useData<{ title: string }>();
  const [likes, setLikes] = useState("likes", 0);
  useHead({ title });
  return (
    <article>
      <h1>{title}</h1>
      <button type="button" onClick={() => setLikes(likes + 1)}>{likes} likes</button>
    </article>
  );
}

Preact:

import { useData, useHead, useState } from "@silgi/preact";

export default function Post() {
  const { title } = useData<{ title: string }>();
  const [likes, setLikes] = useState("likes", 0);
  useHead({ title });
  return (
    <article>
      <h1>{title}</h1>
      <button type="button" onClick={() => setLikes(likes + 1)}>{likes} likes</button>
    </article>
  );
}

Solid reads values through accessors:

import { useData, useHead, useState } from "@silgi/solid";

export default function Post() {
  const data = useData<{ title: string }>();
  const [likes, setLikes] = useState("likes", 0);
  useHead(() => ({ title: data().title }));
  return (
    <article>
      <h1>{data().title}</h1>
      <button type="button" onClick={() => setLikes(likes() + 1)}>{likes()} likes</button>
    </article>
  );
}

Vue uses <script setup lang="ts"> and refs:

<script setup lang="ts">
import { useData, useHead, useState } from "@silgi/vue";

const data = useData<{ title: string }>();
const likes = useState("likes", 0);
useHead(() => ({ title: data.value.title }));
</script>

<template>
  <article>
    <h1>{{ data.title }}</h1>
    <button type="button" @click="likes++">{{ likes }} likes</button>
  </article>
</template>

Svelte 5 uses <script lang="ts"> and objects with value:

<script lang="ts">
  import { useData, useHead, useState } from "@silgi/svelte";

  const data = useData<{ title: string }>();
  const likes = useState("likes", 0);
  useHead(() => ({ title: data.value.title }));
</script>

<article>
  <h1>{data.value.title}</h1>
  <button type="button" onclick={() => likes.value++}>{likes.value} likes</button>
</article>

Svelte reserves the $ prefix, so import $fetch under another name: import { $fetch as api } from "silgi/app".

html returns a string. It does not escape what a template literal interpolates, so an html page must escape every value it interpolates: route parameters, loader data and state. Only the HTML that Link and Outlet return goes in as it is.

import { useData, useHead, useState } from "@silgi/html";

const escape = (value: unknown) => String(value).replace(/[&<>"']/g, (char) => `&#${char.charCodeAt(0)};`);

export default function Post(): string {
  const { title } = useData<{ title: string }>();
  const likes = useState("likes", 0);
  useHead({ title });
  return `<article>
  <h1>${escape(title)}</h1>
  <p>${escape(likes.value)} likes</p>
</article>`;
}

What differs

React, PreactVueSvelteSolidhtml
Layout contentchildrendefault slotchildren snippetprops.childrenchildren, a string
Outletcomponentcomponentcomponentcomponentfunction returning a string
useRoute()the routea reactive objectan object of gettersan accessorthe route of this render
useData()the dataComputedRef{ value }accessorthe data
useState()[value, set]Ref{ value }[accessor, set]{ value }, not reactive
useHead() takesan objectobject, ref or getterobject or getterobject or accessoran object
useRouter()yesyesyesyesyes
ClientOnlyyesyesyesyesno

useCookie, useRuntimeConfig, useAppConfig, useApp, useError, useNavigation, Link, ErrorBoundary and DefaultErrorPage exist in every library. useNavigation() tells you whether a navigation is idle or loading, and whether refresh() is revalidating. useAction(), useForm() and useFormStatus() run a handler from a component and a form; they exist in every library too. useRuntimeConfig() returns { public, app }: the public runtime config and the base URL and assets directory.

Link takes to, replace, prefetch, prefetchOn and noPrefetch; other props go to the <a>.

Page meta

meta is a named export of the page module. In React, Preact, Solid and html it is export const meta in the page file. In Vue it goes in a plain <script lang="ts"> block next to <script setup>; in Svelte it goes in <script module>.

<script lang="ts">
import type { PageMeta } from "silgi/app";

export const meta = { layout: "admin", middleware: ["auth"] } satisfies PageMeta;
</script>

<script setup lang="ts">
import { useHead } from "@silgi/vue";

useHead({ title: "Admin" });
</script>

<template>
  <h1>Admin</h1>
</template>
<script module lang="ts">
  import type { PageMeta } from "silgi/app";

  export const meta = { layout: "admin", middleware: ["auth"] } satisfies PageMeta;
</script>

<script lang="ts">
  import { useHead } from "@silgi/svelte";

  useHead({ title: "Admin" });
</script>

<h1>Admin</h1>

Data in components

useFetch and useAsyncData return { data, error, status, refresh } in every library, in the form of the library's values: plain values in React and Preact, ComputedRefs in Vue, accessors in Solid, getters in Svelte, and plain values with subscribe in html. watch is a list of values in React and Preact, watch sources in Vue, and accessors or getters in Solid and Svelte; html has no watch.

The server waits for the data in every library, as the table in Data says, so it is in the first HTML.

Error boundaries

ErrorBoundary catches render errors, in the browser and on the server, and gives its fallback { error, reset }. React, Preact and Solid take a fallback function. Vue takes a fallback function or a #fallback slot. Svelte takes a fallback snippet. html takes children and fallback functions that return strings.