Primate

Svelte

Primate runs Svelte with server-side rendering, hydration, client navigation, layouts, validation and i18n.

Setup

Install

npm install @primate/svelte svelte

Configure

import config from "primate/config";
import svelte from "@primate/svelte";

export default config({
  modules: [svelte()],
});

Components

Create Svelte components in components using Svelte's template syntax.

<!-- components/PostIndex.svelte -->
<script lang="ts">
  export let title: string;
  export let posts: Array<{title: string; excerpt?: string}> = [];
</script>

<div>
  <h1>{title}</h1>
  <article>
    {#each posts as post}
      <div>
        <h2>{post.title}</h2>
        {#if post.excerpt}
          <p>{post.excerpt}</p>
        {/if}
      </div>
    {/each}
  </article>
</div>

Serve the component from a route:

// routes/posts.ts
import response from "primate/response";
import route from "primate/route";

route.get(() => {
  const posts = [
    { title: "First Post", excerpt: "Introduction to Primate with Svelte" },
    { title: "Second Post", excerpt: "Building reactive applications" },
  ];

  return response.view("PostIndex.svelte", { title: "Blog", posts });
});

Props

Props passed to response.view map directly to component props.

Pass props from a route:

import response from "primate/response";
import route from "primate/route";

route.get(() => response.view("User.svelte", {
  user: { name: "John", role: "Developer" },
  permissions: ["read", "write"],
}));

Access the props in the component:

<!-- components/User.svelte -->
<script lang="ts">
  export let user: {name: string; role: string};
  export let permissions: string[] = [];
</script>

<div>
  <h2>{user.name}</h2>
  <p>Role: {user.role}</p>
  <ul>
    {#each permissions as permission}
      <li>{permission}</li>
    {/each}
  </ul>
</div>

Reactivity with Stores

Svelte's reactivity system uses reactive statements and stores for state management.

<script lang="ts">
  let count = 0;
  $: doubled = count * 2;
</script>

<div>
  <button on:click={() => count--}>-</button>
  <span>{count}</span>
  <button on:click={() => count++}>+</button>
  <p>Doubled: {doubled}</p>
</div>

Validation

Use Primate's validated state wrapper to synchronize with backend routes.

<script lang="ts">
  import validate from "@primate/svelte/validate";
  export let id: string;
  export let value: number;

  const counter = validate<number>(value).post(`/counter?id=${id}`);
</script>

<div style="margin-top: 2rem; text-align: center;">
  <h2>Counter Example</h2>
  <div>
    <button
      on:click={() => counter.update((n) => n - 1)}
      disabled={$counter.loading}
    >
      -
    </button>

    <span style="margin: 0 1rem;">{$counter.value}</span>

    <button
      on:click={() => counter.update((n) => n + 1)}
      disabled={$counter.loading}
    >
      +
    </button>
  </div>

  {#if $counter.error}
    <p style="color: red; margin-top: 1rem;">
      {$counter.error.message}
    </p>
  {/if}
</div>

Add corresponding backend validation in the route:

// routes/counter.ts
import Counter from "#store/Counter";
import route from "primate/route";
import response from "primate/response";
import number from "pema/number";
import string from "pema/string";

await Counter.schema.create();

route.get(async () => {
  const counters = await Counter.find({});

  const counter = counters.length === 0
    ? await Counter.insert({ counter: 10 })
    : counters[0];

  return response.view("Counter.svelte", {
    id: counter.id,
    value: counter.counter
  });
});

route.post(async request => {
  const id = string.parse(request.query.get("id"));
  const body = request.body.json(number.coerce);
  await Counter.update({ id }, { counter: body });
  return null;
});

The wrapper automatically tracks loading states, captures validation errors, and posts updates on state changes.

Forms

Create forms with Svelte's reactive statements and two-way binding.

<script lang="ts">
  let email = "";
  let password = "";
  let errors: {email?: string; password?: string} = {};

  function validateForm() {
    errors = {};

    if (!email) {
      errors.email = "Email is required";
    } else if (!/\S+@\S+\.\S+/.test(email)) {
      errors.email = "Email must be valid";
    }

    if (!password) {
      errors.password = "Password is required";
    } else if (password.length < 8) {
      errors.password = "Password must be at least 8 characters";
    }

    return Object.keys(errors).length === 0;
  }

  async function handleSubmit(e: Event) {
    e.preventDefault();

    if (!validateForm()) return;

    await fetch("/login", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ email, password }),
    });
  }
</script>

<form on:submit={handleSubmit} style="max-width: 400px; margin: 2rem auto;">
  <h2>Login</h2>

  <div style="margin-bottom: 1rem;">
    <input type="email" placeholder="Email" bind:value={email} />
    {#if errors.email}
      <p>{errors.email}</p>
    {/if}
  </div>

  <div style="margin-bottom: 1rem;">
    <input type="password" placeholder="Password" bind:value={password} />
    {#if errors.password}
      <p>{errors.password}</p>
    {/if}
  </div>

  <button
    type="submit"
    disabled={!email || !password}
    style="width: 100%; padding: 0.75rem;
           background-color: {!email || !password ? '#ccc' : '#007bff'};
           color: white; border: none; border-radius: 4px; font-size: 1rem;
           cursor: {!email || !password ? 'not-allowed' : 'pointer'};"
  >
    Submit
  </button>
</form>

Add the corresponding route:

// routes/login.ts
import route from "primate/route";
import response from "primate/response";
import pema from "pema";
import string from "pema/string";

const LoginSchema = pema({
  email: string.email(),
  password: string.min(8),
});

route.get(() => response.view("LoginForm.svelte"));

route.post(async request => {
  const body = await request.body.json(LoginSchema);

  // implement authentication logic

  return null;
});

Layouts

Create layout components that wrap your pages using <slot>.

Create a layout component:

<!-- components/Layout.svelte -->
<script lang="ts">
  export let brand = "My App";
</script>

<div>
  <header>
    <nav style="padding: 1rem; background-color: #f8f9fa;">
      <h1>{brand}</h1>
      <a href="/" style="margin-right: 1rem;">Home</a>
      <a href="/about" style="margin-right: 1rem;">About</a>
    </nav>
  </header>

  <main style="padding: 2rem;">
    <slot />
  </main>

  <footer style="padding: 1rem; background-color: #f8f9fa; text-align: center;">
    © 1996 {brand}
  </footer>
</div>

Next, register the layout via a +layout.ts file:

// routes/+layout.ts
import response from "primate/response";
import route from "primate/route";

route.get(() => response.view("Layout.svelte", { brand: "Primate Svelte Demo" }));

Pages under this route subtree render inside the layout's <slot>.

Internationalization

Primate's t function is framework-agnostic. In Svelte, call it directly:

<script lang="ts">
  import t from "#i18n";
</script>

<div>
  <h1>{$t("welcome")}</h1>
  <button on:click={() => t.locale.set("en-US")}>{$t("english")}</button>
  <button on:click={() => t.locale.set("de-DE")}>{$t("german")}</button>
  <p>{$t("current_locale")}: {t.locale.get()}</p>
</div>

Primate's integration automatically subscribes to locale changes and triggers rerenders when switching languages.

Head Tags

Use Svelte's <svelte:head> to manage document head elements.

<script lang="ts">
  // component logic here
</script>

<svelte:head>
  <title>About Us - Primate Svelte Demo</title>
  <meta name="description" content="Learn more about our company" />
  <meta property="og:title" content="About Us - Primate Svelte Demo" />
  <meta property="og:description" content="Learn more about our company" />
  <meta property="og:type" content="website" />
</svelte:head>

<div style="max-width: 800px; margin: 2rem auto; padding: 0 1rem;">
  <h1>About Us</h1>
  <p>
    Welcome to our Primate Svelte demo application. This page demonstrates
    how to manage document head elements including the title and meta tags
    for better SEO and social media sharing.
  </p>
</div>

Configuration

Option Type Default Description
fileExtensions string[] [".svelte"] Associated file extensions
ssr boolean true Active server-side rendering
spa boolean true Active client-browsing

Example

import svelte from "@primate/svelte";
import config from "primate/config";

export default config({
  modules: [
    svelte({
      // add `.component.svelte` to associated file extensions
      fileExtensions: [".svelte", ".component.svelte"],
    }),
  ],
});

Resources

Previous
Solid
Next
Voby